mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-29 04:32:57 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2197805b3d | ||
|
|
117cda20b6 | ||
|
|
94349a133e | ||
|
|
d33e8f3c08 |
@@ -223,7 +223,13 @@ function runWorker(argv: string[]): void {
|
||||
...record,
|
||||
terminals: record.terminals.filter((entry) => sessionsById.has(entry.id)),
|
||||
};
|
||||
writeTerminalWorkerRecord(args.paseoHome, record);
|
||||
console.error("CI terminal record write attempted");
|
||||
try {
|
||||
writeTerminalWorkerRecord(args.paseoHome, record);
|
||||
} catch (error) {
|
||||
console.error("CI terminal record write failed:", (error as NodeJS.ErrnoException).code);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function removeOwnFiles(): void {
|
||||
|
||||
@@ -2,7 +2,9 @@ import { afterEach, expect, it } from "vitest";
|
||||
import { isPlatform } from "../test-utils/platform.js";
|
||||
import { EventEmitter } from "node:events";
|
||||
import {
|
||||
closeSync,
|
||||
existsSync,
|
||||
openSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
@@ -43,6 +45,8 @@ import {
|
||||
listTerminalWorkerRecords,
|
||||
readTerminalWorkerRecord,
|
||||
terminalWorkerDirectory,
|
||||
terminalWorkerLogPath,
|
||||
terminalWorkerRecordPath,
|
||||
type TerminalWorkerRecord,
|
||||
} from "./terminal-worker-record.js";
|
||||
import { TERMINAL_WORKER_PROTOCOL_OVERRIDE_ENV } from "./terminal-worker-protocol.js";
|
||||
@@ -765,52 +769,91 @@ async function recordedTerminalPids(paseoHome: string): Promise<Map<string, numb
|
||||
return pids;
|
||||
}
|
||||
|
||||
it("never adopts a live worker's terminals after a kill cleanup it could not finish, even once the worker rewrites its record", async () => {
|
||||
const paseoHome = createTemporaryHome();
|
||||
const cwd = mkdtempSync(join(tmpdir(), "worker-terminal-manager-unfinished-kill-"));
|
||||
temporaryDirs.push(cwd);
|
||||
const first = await startRealWorkerManager({ paseoHome });
|
||||
manager = first;
|
||||
const ending = await first.createTerminal({
|
||||
workspaceId: "ws-unfinished-kill",
|
||||
cwd,
|
||||
...nodeTerminalCommand("setInterval(() => {}, 1000);"),
|
||||
});
|
||||
const remaining = await first.createTerminal({
|
||||
workspaceId: "ws-unfinished-kill",
|
||||
cwd,
|
||||
...nodeTerminalCommand("setInterval(() => {}, 1000);"),
|
||||
});
|
||||
const pids = await recordedTerminalPids(paseoHome);
|
||||
const endingPid = pids.get(ending.id)!;
|
||||
const remainingPid = pids.get(remaining.id)!;
|
||||
it.each([false, true])(
|
||||
"never adopts a live worker's terminals after a kill cleanup it could not finish, even once the worker rewrites its record (held reader: %s)",
|
||||
async (holdReader) => {
|
||||
const paseoHome = createTemporaryHome();
|
||||
const cwd = mkdtempSync(join(tmpdir(), "worker-terminal-manager-unfinished-kill-"));
|
||||
temporaryDirs.push(cwd);
|
||||
const first = await startRealWorkerManager({ paseoHome });
|
||||
manager = first;
|
||||
const exitRequestPath = join(cwd, "exit-requested");
|
||||
const exitObservedPath = join(cwd, "exit-observed");
|
||||
const ending = await first.createTerminal({
|
||||
workspaceId: "ws-unfinished-kill",
|
||||
cwd,
|
||||
...nodeTerminalCommand(`
|
||||
const { existsSync, writeFileSync } = require("node:fs");
|
||||
setInterval(() => {
|
||||
if (existsSync(${JSON.stringify(exitRequestPath)})) {
|
||||
writeFileSync(${JSON.stringify(exitObservedPath)}, "");
|
||||
process.exit(0);
|
||||
}
|
||||
}, 25);
|
||||
`),
|
||||
});
|
||||
const remaining = await first.createTerminal({
|
||||
workspaceId: "ws-unfinished-kill",
|
||||
cwd,
|
||||
...nodeTerminalCommand("setInterval(() => {}, 1000);"),
|
||||
});
|
||||
const pids = await recordedTerminalPids(paseoHome);
|
||||
const endingPid = pids.get(ending.id)!;
|
||||
const remainingPid = pids.get(remaining.id)!;
|
||||
const [record] = await workerRecords(paseoHome);
|
||||
|
||||
// A kill cleanup whose process table read fails signals nothing and leaves the worker live.
|
||||
const inspector = createTerminalWorkerProcessInspector();
|
||||
await expect(
|
||||
reapTerminalWorkers({
|
||||
paseoHome,
|
||||
inspector: { ...inspector, listProcesses: () => Promise.reject(new Error("ps timed out")) },
|
||||
}),
|
||||
).rejects.toBeInstanceOf(TerminalCleanupNotConfirmedError);
|
||||
await first.detach();
|
||||
expect((await readUnconfirmedTerminalCleanup(paseoHome)).length).toBeGreaterThan(0);
|
||||
// A kill cleanup whose process table read fails signals nothing and leaves the worker live.
|
||||
const inspector = createTerminalWorkerProcessInspector();
|
||||
await expect(
|
||||
reapTerminalWorkers({
|
||||
paseoHome,
|
||||
inspector: { ...inspector, listProcesses: () => Promise.reject(new Error("ps timed out")) },
|
||||
}),
|
||||
).rejects.toBeInstanceOf(TerminalCleanupNotConfirmedError);
|
||||
await first.detach();
|
||||
expect((await readUnconfirmedTerminalCleanup(paseoHome)).length).toBeGreaterThan(0);
|
||||
|
||||
// One terminal ends on its own: the live worker rewrites its record from memory.
|
||||
process.kill(endingPid, "SIGKILL");
|
||||
await waitForCondition(
|
||||
async () => !(await recordedTerminalPids(paseoHome)).has(ending.id),
|
||||
10000,
|
||||
);
|
||||
expect((await readUnconfirmedTerminalCleanup(paseoHome)).length).toBeGreaterThan(0);
|
||||
// Diagnostic: keep the same read handle the poller uses open across the exit write.
|
||||
const recordFd = holdReader
|
||||
? openSync(terminalWorkerRecordPath(paseoHome, record!.pid), "r")
|
||||
: null;
|
||||
const attemptsBeforeExit =
|
||||
readFileSync(terminalWorkerLogPath(paseoHome), "utf8").match(
|
||||
/CI terminal record write attempted/g,
|
||||
)?.length ?? 0;
|
||||
try {
|
||||
writeFileSync(exitRequestPath, "");
|
||||
await waitForCondition(
|
||||
async () => !(await recordedTerminalPids(paseoHome)).has(ending.id),
|
||||
10000,
|
||||
);
|
||||
} catch (error) {
|
||||
const workerLog = readFileSync(terminalWorkerLogPath(paseoHome), "utf8");
|
||||
console.error("CI terminal exit diagnostics", {
|
||||
holdReader,
|
||||
attemptsAfterExit:
|
||||
(workerLog.match(/CI terminal record write attempted/g)?.length ?? 0) -
|
||||
attemptsBeforeExit,
|
||||
uncaughtExceptionLogged: workerLog.includes("Terminal worker uncaught exception"),
|
||||
exitObserved: existsSync(exitObservedPath),
|
||||
endingAlive: isProcessAlive(endingPid),
|
||||
remainingAlive: isProcessAlive(remainingPid),
|
||||
recordWriteErrors: workerLog.match(/CI terminal record write failed: \w+/g) ?? [],
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
if (recordFd !== null) closeSync(recordFd);
|
||||
}
|
||||
expect((await readUnconfirmedTerminalCleanup(paseoHome)).length).toBeGreaterThan(0);
|
||||
|
||||
manager = await startRealWorkerManager({ paseoHome });
|
||||
manager = await startRealWorkerManager({ paseoHome });
|
||||
|
||||
expect(manager.getTerminal(remaining.id)).toBeUndefined();
|
||||
expect(await manager.getTerminals(cwd)).toEqual([]);
|
||||
await waitForCondition(() => !isProcessAlive(remainingPid), 10000);
|
||||
expect(await readUnconfirmedTerminalCleanup(paseoHome)).toEqual([]);
|
||||
});
|
||||
expect(manager.getTerminal(remaining.id)).toBeUndefined();
|
||||
expect(await manager.getTerminals(cwd)).toEqual([]);
|
||||
await waitForCondition(() => !isProcessAlive(remainingPid), 10000);
|
||||
expect(await readUnconfirmedTerminalCleanup(paseoHome)).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
// The worker's own `ps`, delayed: a program that exits while the worker reads its birth time
|
||||
// lands its exit in the middle of creation. Only the worker launched while this PATH is set sees
|
||||
|
||||
Reference in New Issue
Block a user