Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/update/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1134,8 +1134,16 @@ async function restartAfterUpdate(
`Port ${port} still busy after ${Math.trunc(RESTART_PORT_RECLAIM_MS / 1000)}s; refusing to hop — reinstall may fail until the port is free.`
+ ` ${formatPortHolders(port, listPids, verifyOcx, preServiceAllow)}`,
);
const liveAfter = listPids(port).filter(pid => pid !== process.pid && aliveFn(pid));
if (liveAfter.length === 0) {
const liveScan: ListenPidScan = io.scanListenPidsFn
? io.scanListenPidsFn(port)
: io.listListenPidsFn
// Test seam: an injected list represents a successful scan.
? { ok: true, pids: io.listListenPidsFn(port) }
: scanListenPids(port);
const liveAfter = liveScan.ok
? liveScan.pids.filter(pid => pid !== process.pid && aliveFn(pid))
: null;
if (liveAfter !== null && liveAfter.length === 0) {
// Non-elevated `service install` will UAC-fail anyway; skip straight to
// the direct-start fallthrough instead of burning another minute on it.
updateJob(job, {}, "Skipping service reinstall after reclaim timeout with no live holders; falling back to a direct proxy start.");
Expand Down
39 changes: 39 additions & 0 deletions tests/update-job.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,45 @@ describe("GUI update execution decisions", () => {
});
});

test("service restart is not skipped when the listener scan fails", async () => {
let serviceRuns = 0;
const serviceArgs: string[][] = [];
const job: UpdateJobState = {
id: "svc-scan-failure",
status: "restarting",
startedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
currentVersion: "2.10.2",
latestVersion: "2.10.3",
channel: "latest",
installer: "npm",
restart: true,
command: "",
log: [],
releaseNotesUrl: "",
};
writeFileSync(updateJobPath(), JSON.stringify(job));
await restartAfterUpdateForTests(job, { port: 19997, hostname: "127.0.0.1" }, {
serviceInstalledFn: () => true,
serviceViableFn: () => true,
waitForPort: async () => false,
listListenPidsFn: () => [],
scanListenPidsFn: () => ({ ok: false, error: "listener tools unavailable" }),
runService: (_job, _bin, args) => {
serviceRuns += 1;
serviceArgs.push(args);
return { status: 0 };
},
spawnStart: () => {},
probeProxy: async () => true,
});
Comment thread
luvs01 marked this conversation as resolved.
expect(serviceRuns).toBe(1);
expect(serviceArgs[0]).toContain("repair");
expect(readUpdateJob(job.id)?.log.some(line =>
line.includes("Skipping service reinstall after reclaim timeout"),
)).toBe(false);
});

test("proxy restart pins --port so post-update start does not hop to an ephemeral port", () => {
const proxy = restartCommand(false, "npm", "/pkg/bin/ocx.mjs", 10100);
expect(proxy.mode).toBe("proxy");
Expand Down
Loading