diff --git a/deploy/windows/installer/shopdb-install.ps1 b/deploy/windows/installer/shopdb-install.ps1 index b2d21cd..ba6edb3 100644 --- a/deploy/windows/installer/shopdb-install.ps1 +++ b/deploy/windows/installer/shopdb-install.ps1 @@ -232,38 +232,44 @@ function Invoke-Native { # ERROR 1049 Unknown database ''. $common['RedirectStandardInput'] = $StdinFile } - if ($TimeoutSec -le 0) { $common['Wait'] = $true } + # -Wait ALWAYS. Dropping it to make -TimeoutSec enforceable left + # $p.ExitCode unreadable: a Python install that had actually worked + # reported "Python install failed (exit )" with an empty code, because + # PowerShell only reliably populates ExitCode on a waited process. + # + # So the trade is deliberate. Exit codes are load-bearing here - 1639 vs + # 1603 vs 3010 is the whole diagnosis - and a bounded wait is not worth + # losing them for. -TimeoutSec is therefore ADVISORY: it is recorded in + # the log so a hang is identifiable, but the wait is unbounded. + $common['Wait'] = $true + if ($TimeoutSec -gt 0) { Write-Log (" (expected to finish within {0}s)" -f $TimeoutSec) } $p = Start-Process @common - # A hung child must not hang the install. msiexec in particular deadlocks - # when a second Windows Installer transaction holds the global mutex: two - # msiexec processes sat there for 40 minutes with no output and no error. - if ($TimeoutSec -gt 0) { - if (-not $p.WaitForExit($TimeoutSec * 1000)) { - try { $p.Kill() } catch { } - Fail "$What timed out after $TimeoutSec seconds" @' -The command was killed. For an MSI this usually means another Windows Installer -transaction held the global mutex: check for stray msiexec processes -(Get-Process msiexec), end them, then re-run this stage. -'@ - } - # The parameterless overload waits for the redirected streams to be - # flushed and closed. Without it the log files can still be being - # written when they are read below, losing the tail of a failure. - $p.WaitForExit() - } + # NOTE: msiexec can deadlock when another Windows Installer transaction + # holds the global mutex - two msiexec processes once sat for 40 minutes + # with no output and no error. That is why the expected duration is + # logged above: an operator seeing no progress well past it should check + # for stray msiexec processes (Get-Process msiexec) and end them. $out = @() if (Test-Path $so) { $out += Get-Content $so -ErrorAction SilentlyContinue } if (Test-Path $se) { $out += Get-Content $se -ErrorAction SilentlyContinue } - if ($OkExit -notcontains $p.ExitCode) { + $code = $null + try { $code = $p.ExitCode } catch { } + if ($null -eq $code) { + # Never silently treat "cannot tell" as success. $out | Select-Object -Last 8 | ForEach-Object { Write-Log " $_" 'FAIL' } - Fail "$What failed (exit $($p.ExitCode))" + Fail "$What finished but its exit code could not be read" ` + 'Check the log above and the tool''s own log before re-running.' } - if ($p.ExitCode -ne 0) { + if ($OkExit -notcontains $code) { + $out | Select-Object -Last 8 | ForEach-Object { Write-Log " $_" 'FAIL' } + Fail "$What failed (exit $code)" + } + if ($code -ne 0) { # Accepted, but say so: 3010/1641 mean the server needs a reboot to # finish, and an operator who never sees that wonders later why # something only half works. - Write-Log ("$What returned {0} - completed, but this server needs a reboot to finish" -f $p.ExitCode) 'WARN' + Write-Log ("$What returned {0} - completed, but this server needs a reboot to finish" -f $code) 'WARN' $script:RebootPending = $true } return $out @@ -1192,6 +1198,34 @@ a page that cannot load its own assets. Rebuild with scripts/build-site.sh } # --- .env --------------------------------------------------------------- + # The stage-0 handoff is checked BEFORE the .env branch, not inside its else. + # It used to sit in the else, so it was consulted only when .env was absent - + # and on a part-finished install .env DOES exist, holding whatever password + # stage 2 last wrote. If stage 0 has since regenerated the credential, .env + # is stale, .dbpass is correct, and the installer preferred the stale one: + # "Access denied", with the right password sitting unread on disk. + # + # .dbpass only exists between stage 0 and stage 5 - stage 5 shreds it once + # the install is proven - so its presence means "this install is unfinished + # and THIS is the current password". Applied only when .env points at the + # local server, because the handoff describes the bundled database and must + # never redirect a site whose database lives elsewhere. + $handoff = Join-Path $AppRoot '.dbpass' + if (-not $DbPasswordFile -and (Test-Path $handoff)) { + $envIsLocal = $true + if (Test-Path $EnvFile) { + $envIsLocal = $false + $urlLine = Get-Content $EnvFile -ErrorAction SilentlyContinue | + Where-Object { $_ -like 'DATABASE_URL=*' } | Select-Object -First 1 + if ($urlLine -and ($urlLine -match '@(127\.0\.0\.1|localhost|\[::1\]):')) { $envIsLocal = $true } + } + if ($envIsLocal) { + $DbPasswordFile = $handoff + if (-not $DbHost) { $DbHost = '127.0.0.1' } + Write-Log 'using the stage-0 password handoff (an unfinished install; .env may be stale)' + } + } + if (Test-Path $EnvFile) { # Preserving .env is right for a plain re-run: regenerating JWT_SECRET_KEY # would invalidate every issued session. @@ -1284,17 +1318,7 @@ a page that cannot load its own assets. Rebuild with scripts/build-site.sh } else { if (-not $SiteHost) { $SiteHost = $env:COMPUTERNAME } - # Stage 0 leaves an ACL'd handoff file when it created the database itself. - # Prefer it: a greenfield install then needs no password from anyone, and - # the generated password never passes through a human or a command line. - # This MUST come before the -DbHost check below - stage 0 always installs - # to the local box, so the handoff implies the host as well as the password. - $handoff = Join-Path $AppRoot '.dbpass' - if (-not $DbPasswordFile -and (Test-Path $handoff)) { - $DbPasswordFile = $handoff - if (-not $DbHost) { $DbHost = '127.0.0.1' } - Write-Log 'using the bundled-MySQL password handoff from stage 0 (host 127.0.0.1)' - } + # The stage-0 handoff was already applied above, for both branches. if (-not $DbHost) { Fail 'no database host supplied and no existing .env' @'