fix(terminal): start the daemon when the terminal worker cannot start (#8810)

On 0.15.0 the daemon launches the detached terminal worker while it is
being created, and the worker refuses to start unless it can read its own
birth time and command line (Windows PowerShell 5.1 `Get-CimInstance`,
`ps` elsewhere). Any failure of that query used to escape
startWorkerTerminalManager and take the whole daemon down with
"Daemon failed to start (exit 1)", as two Windows machines hit after the
update: one whose PowerShell 5.1 cannot load CimCmdlets, one where
powershell.exe cannot be spawned at all. 0.14.0 never ran that query at
startup.

The launch error is now logged and the daemon starts without a current
worker. The pool already launches one lazily on the first terminal
creation, so that request reports the same error while the machine still
cannot run the query, and terminals come back once it can. The identity
requirement itself is unchanged: a worker without a verified identity is
never written or adopted.
This commit is contained in:
Mohamed Boudra
2026-11-05 08:59:04 +07:00
committed by GitHub
parent 5d1a2c10f8
commit 8dfdb479ab
2 changed files with 43 additions and 3 deletions
@@ -70,7 +70,7 @@ import {
TerminalWorkerIncompatibleError,
} from "./terminal-worker-pool.js";
import { connectTerminalWorker } from "./terminal-worker-client.js";
import { launchTerminalWorker } from "./terminal-worker-launch.js";
import { launchTerminalWorker, TerminalWorkerLaunchError } from "./terminal-worker-launch.js";
type TerminalRow = TerminalState["grid"][number];
@@ -2671,3 +2671,34 @@ it.each([false, true])(
},
45000,
);
// Both Windows reports in #8766: the worker's process identity query (PowerShell there, `ps` here)
// fails, the worker exits before writing its record, and the daemon used to exit with it.
it.skipIf(isPlatform("win32"))(
"starts without a worker when the worker cannot read its own identity, and launches one once it can",
async () => {
const paseoHome = createTemporaryHome();
const binDir = mkdtempSync(join(tmpdir(), "worker-terminal-manager-no-ps-"));
temporaryDirs.push(binDir);
writeFileSync(join(binDir, "ps"), "#!/bin/sh\nexit 1\n", { mode: 0o755 });
const originalPath = process.env.PATH;
process.env.PATH = `${binDir}${delimiter}${originalPath ?? ""}`;
try {
manager = await startRealWorkerManager({ paseoHome });
expect(await workerRecords(paseoHome)).toEqual([]);
await expect(
manager.createTerminal({ workspaceId: "ws-no-identity", cwd: paseoHome }),
).rejects.toThrow(TerminalWorkerLaunchError);
expect(await workerRecords(paseoHome)).toEqual([]);
} finally {
process.env.PATH = originalPath;
}
const terminal = trackTerminal(
await manager.createTerminal({ workspaceId: "ws-no-identity", cwd: paseoHome }),
);
expect(terminal.id).toBeTruthy();
expect((await workerRecords(paseoHome)).map((record) => record.pid)).toHaveLength(1);
},
30000,
);
@@ -1269,8 +1269,17 @@ export async function startWorkerTerminalManager(
try {
current = await startCurrent();
} catch (error) {
if (!(error instanceof NoTerminalWorkerAvailableError)) throw error;
logger.warn(error.message);
if (error instanceof NoTerminalWorkerAvailableError) {
logger.warn(error.message);
} else {
// The daemon starts without terminals rather than not at all. A worker that cannot start
// here (its process identity query failed, say) is launched again by the first terminal
// creation, which reports the same error if it still cannot.
logger.error(
{ err: error },
"Terminal worker did not start; terminals are unavailable until one starts",
);
}
}
}