diff --git a/scripts/e2e/parallels/linux-smoke.ts b/scripts/e2e/parallels/linux-smoke.ts index 5823c69859e..4355f9e7bb8 100755 --- a/scripts/e2e/parallels/linux-smoke.ts +++ b/scripts/e2e/parallels/linux-smoke.ts @@ -363,7 +363,8 @@ class LinuxSmoke extends SmokeRunController { private phase = async (name: string, timeoutSeconds: number, fn: () => Promise | void) => await this.phases.phase(name, timeoutSeconds, fn); - private remainingPhaseTimeoutMs = (): number | undefined => this.phases.remainingTimeoutMs(); + private remainingPhaseTimeoutMs = (fallbackMs?: number): number | undefined => + this.phases.remainingTimeoutMs(fallbackMs); private logGuestPreflight(): void { this.guestBash(String.raw`set -euo pipefail @@ -406,11 +407,17 @@ printf 'preflight.npmRoot=%s\n' "$(npm root -g 2>/dev/null || true)"`); say(`Restore snapshot ${this.options.snapshotHint} (${this.snapshot.id})`); run("prlctl", ["snapshot-switch", this.options.vmName, "--id", this.snapshot.id], { quiet: true, + timeoutMs: this.remainingPhaseTimeoutMs(), }); if (this.snapshot.state === "poweroff") { - waitForVmStatus(this.options.vmName, "stopped", 180); + waitForVmStatus(this.options.vmName, "stopped", 180, { + probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000), + }); say(`Start restored poweroff snapshot ${this.snapshot.name}`); - run("prlctl", ["start", this.options.vmName], { quiet: true }); + run("prlctl", ["start", this.options.vmName], { + quiet: true, + timeoutMs: this.remainingPhaseTimeoutMs(120_000), + }); } this.waitForGuestReady(); } diff --git a/scripts/e2e/parallels/macos-smoke.ts b/scripts/e2e/parallels/macos-smoke.ts index c6ade7f78dd..26385040982 100755 --- a/scripts/e2e/parallels/macos-smoke.ts +++ b/scripts/e2e/parallels/macos-smoke.ts @@ -565,8 +565,8 @@ class MacosSmoke { await this.phases.phase(name, timeoutSeconds, fn); } - private remainingPhaseTimeoutMs(): number | undefined { - return this.phases.remainingTimeoutMs(); + private remainingPhaseTimeoutMs(fallbackMs?: number): number | undefined { + return this.phases.remainingTimeoutMs(fallbackMs); } private async phaseReturns( @@ -653,6 +653,7 @@ exec node "$entry" ${argv}`, run("prlctl", ["exec", this.options.vmName, "/usr/bin/stat", "-f", "%Su", "/dev/console"], { check: false, quiet: true, + timeoutMs: this.remainingPhaseTimeoutMs(30_000), }) .stdout.trim() .replaceAll("\r", "") @@ -671,6 +672,7 @@ exec node "$entry" ${argv}`, { check: false, quiet: true, + timeoutMs: this.remainingPhaseTimeoutMs(30_000), }, ).stdout.replaceAll("\r", ""); for (const line of users.split("\n")) { @@ -700,7 +702,7 @@ exec node "$entry" ${argv}`, `/Users/${user}`, "NFSHomeDirectory", ], - { check: false, quiet: true }, + { check: false, quiet: true, timeoutMs: this.remainingPhaseTimeoutMs(30_000) }, ).stdout.replaceAll("\r", ""); const match = /^NFSHomeDirectory:\s+(.+)$/m.exec(output); return match?.[1]?.trim() || `/Users/${user}`; @@ -713,7 +715,7 @@ exec node "$entry" ${argv}`, const result = run( "prlctl", ["snapshot-switch", this.options.vmName, "--id", this.snapshot.id, "--skip-resume"], - { check: false, quiet: true, timeoutMs: 360_000 }, + { check: false, quiet: true, timeoutMs: this.remainingPhaseTimeoutMs(360_000) }, ); this.log(result.stdout); this.log(result.stderr); @@ -725,10 +727,17 @@ exec node "$entry" ${argv}`, const status = run("prlctl", ["status", this.options.vmName], { check: false, quiet: true, + timeoutMs: this.remainingPhaseTimeoutMs(60_000), }).stdout; if (status.includes(" running") || status.includes(" suspended")) { - run("prlctl", ["stop", this.options.vmName, "--kill"], { check: false, quiet: true }); - waitForVmStatus(this.options.vmName, "stopped", 360); + run("prlctl", ["stop", this.options.vmName, "--kill"], { + check: false, + quiet: true, + timeoutMs: this.remainingPhaseTimeoutMs(120_000), + }); + waitForVmStatus(this.options.vmName, "stopped", 360, { + probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000), + }); } run("sleep", ["3"], { quiet: true }); } @@ -738,15 +747,23 @@ exec node "$entry" ${argv}`, const status = run("prlctl", ["status", this.options.vmName], { check: false, quiet: true, - timeoutMs: 60_000, + timeoutMs: this.remainingPhaseTimeoutMs(60_000), }).stdout; if (this.snapshot.state === "poweroff" || status.includes(" stopped")) { - waitForVmStatus(this.options.vmName, "stopped", 360); + waitForVmStatus(this.options.vmName, "stopped", 360, { + probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000), + }); say(`Start restored poweroff snapshot ${this.snapshot.name}`); - run("prlctl", ["start", this.options.vmName], { quiet: true }); + run("prlctl", ["start", this.options.vmName], { + quiet: true, + timeoutMs: this.remainingPhaseTimeoutMs(120_000), + }); } else if (status.includes(" suspended")) { say(`Resume restored snapshot ${this.snapshot.name}`); - run("prlctl", ["start", this.options.vmName], { quiet: true }); + run("prlctl", ["start", this.options.vmName], { + quiet: true, + timeoutMs: this.remainingPhaseTimeoutMs(120_000), + }); } this.waitForCurrentUser(); } diff --git a/scripts/e2e/parallels/parallels-vm.ts b/scripts/e2e/parallels/parallels-vm.ts index 616346b4ff1..1a219986fd0 100644 --- a/scripts/e2e/parallels/parallels-vm.ts +++ b/scripts/e2e/parallels/parallels-vm.ts @@ -1,10 +1,17 @@ import { die, run, say, warn } from "./host-command.ts"; +const PRLCTL_STATUS_TIMEOUT_MS = 30_000; +const PRLCTL_TRANSITION_TIMEOUT_MS = 120_000; + interface PrlctlVmListItem { name?: string; status?: string; } +export interface WaitForVmStatusOptions { + probeTimeoutMs?: () => number | undefined; +} + export function listVmNames(): string[] { return listVms() .map((item) => (item.name ?? "").trim()) @@ -15,12 +22,18 @@ export function vmStatus(vmName: string): string { return listVms().find((vm) => vm.name === vmName)?.status || "missing"; } -export function waitForVmStatus(vmName: string, expected: string, timeoutSeconds: number): void { +export function waitForVmStatus( + vmName: string, + expected: string, + timeoutSeconds: number, + options: WaitForVmStatusOptions = {}, +): void { const deadline = Date.now() + timeoutSeconds * 1000; while (Date.now() < deadline) { const status = run("prlctl", ["status", vmName], { check: false, quiet: true, + timeoutMs: options.probeTimeoutMs?.() ?? PRLCTL_STATUS_TIMEOUT_MS, }).stdout; if (status.includes(` ${expected}`)) { return; @@ -39,10 +52,16 @@ export function ensureVmRunning(vmName: string, timeoutSeconds = 180): void { } if (status === "stopped") { say(`Start ${vmName} before update phase`); - run("prlctl", ["start", vmName], { quiet: true }); + run("prlctl", ["start", vmName], { + quiet: true, + timeoutMs: PRLCTL_TRANSITION_TIMEOUT_MS, + }); } else if (status === "suspended" || status === "paused") { say(`Resume ${vmName} before update phase`); - run("prlctl", ["resume", vmName], { quiet: true }); + run("prlctl", ["resume", vmName], { + quiet: true, + timeoutMs: PRLCTL_TRANSITION_TIMEOUT_MS, + }); } else if (status === "missing") { die(`VM not found before update phase: ${vmName}`); } @@ -79,7 +98,10 @@ export function resolveUbuntuVmName(requested: string, explicit = false): string function listVms(): PrlctlVmListItem[] { return JSON.parse( - run("prlctl", ["list", "--all", "--json"], { quiet: true }).stdout, + run("prlctl", ["list", "--all", "--json"], { + quiet: true, + timeoutMs: PRLCTL_STATUS_TIMEOUT_MS, + }).stdout, ) as PrlctlVmListItem[]; } diff --git a/scripts/e2e/parallels/snapshots.ts b/scripts/e2e/parallels/snapshots.ts index 6b1289bd5b1..c889d546cf8 100644 --- a/scripts/e2e/parallels/snapshots.ts +++ b/scripts/e2e/parallels/snapshots.ts @@ -1,8 +1,13 @@ import { die, run } from "./host-command.ts"; import type { SnapshotInfo } from "./types.ts"; +const SNAPSHOT_LIST_TIMEOUT_MS = 120_000; + export function resolveSnapshot(vmName: string, hint: string): SnapshotInfo { - const output = run("prlctl", ["snapshot-list", vmName, "--json"], { quiet: true }).stdout; + const output = run("prlctl", ["snapshot-list", vmName, "--json"], { + quiet: true, + timeoutMs: SNAPSHOT_LIST_TIMEOUT_MS, + }).stdout; const payload = JSON.parse(output) as Record; let best: SnapshotInfo | null = null; let bestScore = -1; diff --git a/scripts/e2e/parallels/windows-smoke.ts b/scripts/e2e/parallels/windows-smoke.ts index 6c2cb676cf2..87bde373a40 100755 --- a/scripts/e2e/parallels/windows-smoke.ts +++ b/scripts/e2e/parallels/windows-smoke.ts @@ -449,6 +449,7 @@ class WindowsSmoke extends SmokeRunController { { check: false, quiet: true, + timeoutMs: this.remainingPhaseTimeoutMs(), }, ); this.log(result.stdout); @@ -469,9 +470,14 @@ class WindowsSmoke extends SmokeRunController { } this.waitForVmNotRestoring(240); if (this.snapshot.state === "poweroff") { - waitForVmStatus(this.options.vmName, "stopped", 240); + waitForVmStatus(this.options.vmName, "stopped", 240, { + probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000), + }); say(`Start restored poweroff snapshot ${this.snapshot.name}`); - run("prlctl", ["start", this.options.vmName], { quiet: true }); + run("prlctl", ["start", this.options.vmName], { + quiet: true, + timeoutMs: this.remainingPhaseTimeoutMs(120_000), + }); } } @@ -481,6 +487,7 @@ class WindowsSmoke extends SmokeRunController { const status = run("prlctl", ["status", this.options.vmName], { check: false, quiet: true, + timeoutMs: this.remainingPhaseTimeoutMs(30_000), }).stdout; if (!status.includes(" restoring")) { return; diff --git a/test/scripts/parallels-smoke-model.test.ts b/test/scripts/parallels-smoke-model.test.ts index dab133bc457..8e8b710492a 100644 --- a/test/scripts/parallels-smoke-model.test.ts +++ b/test/scripts/parallels-smoke-model.test.ts @@ -602,10 +602,15 @@ if (isPrlctl) { it("clears phase timers and applies phase deadlines to guest commands", () => { const phaseRunner = readFileSync(TS_PATHS.phaseRunner, "utf8"); const guestTransports = readFileSync(TS_PATHS.guestTransports, "utf8"); + const parallelsVm = readFileSync(TS_PATHS.parallelsVm, "utf8"); + const snapshots = readFileSync(TS_PATHS.snapshots, "utf8"); expect(phaseRunner).toContain("clearTimeout(timer)"); expect(phaseRunner).toContain("remainingTimeoutMs"); expect(guestTransports).toContain("this.phases.remainingTimeoutMs"); + expect(parallelsVm).toContain("PRLCTL_STATUS_TIMEOUT_MS"); + expect(parallelsVm).toContain("probeTimeoutMs"); + expect(snapshots).toContain("SNAPSHOT_LIST_TIMEOUT_MS"); for (const scriptPath of OS_TS_PATHS) { const script = readFileSync(scriptPath, "utf8"); @@ -614,6 +619,14 @@ if (isPrlctl) { expect(script, scriptPath).toContain("remainingPhaseTimeoutMs"); expect(script, scriptPath).toContain("timeoutMs:"); } + + const linux = readFileSync(TS_PATHS.linux, "utf8"); + const macos = readFileSync(TS_PATHS.macos, "utf8"); + const windows = readFileSync(TS_PATHS.windows, "utf8"); + expect(linux).toContain("probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000)"); + expect(windows).toContain("probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000)"); + expect(macos).toContain("probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000)"); + expect(macos).toContain("timeoutMs: this.remainingPhaseTimeoutMs(360_000)"); }); it("streams full phase logs to disk while bounding the failure tail", async () => {