diff --git a/scripts/verify-windows-installer-lifecycle.mjs b/scripts/verify-windows-installer-lifecycle.mjs index 4f20445308..4e14c3c2f2 100644 --- a/scripts/verify-windows-installer-lifecycle.mjs +++ b/scripts/verify-windows-installer-lifecycle.mjs @@ -21,7 +21,10 @@ function delay(milliseconds) { return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)); } -export async function listInstalledProcesses(installDirectory, { run = runCommand } = {}) { +export async function listInstalledProcesses( + installDirectory, + { run = runCommand, timeoutMs = 10_000 } = {}, +) { const root = `${resolve(installDirectory)}${sep}`; const script = String.raw` $root = [IO.Path]::GetFullPath(${powerShellLiteral(root)}) @@ -37,7 +40,21 @@ $matches = @( ) $matches | ConvertTo-Json -Compress `; - const { stdout } = await run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script]); + // Bounded because this probe runs inside deadline-bounded poll loops, and a + // loop only checks its deadline between probes: an unbounded one makes the + // deadline a lie. A `Get-CimInstance Win32_Process` query hung for the rest + // of the job during an auto-update relaunch, so the 120s relaunch deadline + // never fired and the runner cancelled the run an hour later. + // + // Well under the 60s and 120s budgets above it, so a stalled probe leaves + // room for several more attempts rather than consuming the whole window: a + // loop that can only try twice cannot survive a stall, which is the entire + // point of tolerating one. + const { stdout } = await run( + 'powershell', + ['-NoProfile', '-NonInteractive', '-Command', script], + { timeoutMs }, + ); if (!stdout.trim()) return []; const parsed = JSON.parse(stdout); return Array.isArray(parsed) ? parsed : [parsed]; @@ -53,16 +70,38 @@ export async function waitForInstalledProcessesToExit( } = {}, ) { const deadline = Date.now() + timeoutMs; - let processes = await listProcesses(installDirectory); - while (processes.length > 0) { + let processes = []; + let observed = false; + let probeError; + for (;;) { + try { + processes = await listProcesses(installDirectory); + observed = true; + probeError = undefined; + if (processes.length === 0) return; + } catch (error) { + // A bounded probe that fails says nothing about the processes, so the + // loop's own deadline stays the authority — the Windows process query + // stalls while an installer is churning the process table, and one + // stalled probe must not decide a verification. + probeError = error; + } if (Date.now() >= deadline) { + // Never having seen the process table is a different fault from seeing + // processes that would not exit, and only one of them is about Maka. + if (!observed) { + throw new Error( + `Could not read the process table within ${timeoutMs}ms, so whether installed Maka processes exited is unknown.\nLast process probe failed: ${probeError?.message}`, + ); + } const summary = processes.map(({ processId, name }) => `${name} (${processId})`).join(', '); throw new Error( - `Installed Maka processes did not exit within ${timeoutMs}ms: ${summary || ''}.`, + `Installed Maka processes did not exit within ${timeoutMs}ms: ${summary || ''}.${ + probeError ? `\nThe last probe also failed: ${probeError.message}` : '' + }`, ); } await sleep(pollIntervalMs); - processes = await listProcesses(installDirectory); } }