mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(infra): handle detached respawn child errors (#101489)
* fix(infra): handle detached respawn child errors * docs(infra): explain detached respawn error listener * fix(infra): handle detached respawn child errors * fix(infra): handle detached respawn child errors --------- Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
@@ -54,6 +54,15 @@ function clearSupervisorHints() {
|
||||
}
|
||||
}
|
||||
|
||||
function mockDetachedChild(pid: number) {
|
||||
return {
|
||||
pid,
|
||||
kill: vi.fn(),
|
||||
on: vi.fn(),
|
||||
unref: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function expectLaunchdSupervisedWithoutKickstart(params?: { launchJobLabel?: string }) {
|
||||
setPlatform("darwin");
|
||||
if (params?.launchJobLabel) {
|
||||
@@ -302,7 +311,7 @@ describe("respawnGatewayProcessForUpdate", () => {
|
||||
"gateway",
|
||||
"run",
|
||||
];
|
||||
spawnMock.mockReturnValue({ pid: 5151, unref: vi.fn(), kill: vi.fn() });
|
||||
spawnMock.mockReturnValue(mockDetachedChild(5151));
|
||||
|
||||
const result = respawnGatewayProcessForUpdate();
|
||||
|
||||
@@ -329,7 +338,7 @@ describe("respawnGatewayProcessForUpdate", () => {
|
||||
"gateway",
|
||||
"run",
|
||||
];
|
||||
spawnMock.mockReturnValue({ pid: 7171, unref: vi.fn(), kill: vi.fn() });
|
||||
spawnMock.mockReturnValue(mockDetachedChild(7171));
|
||||
|
||||
const result = respawnGatewayProcessForUpdate();
|
||||
|
||||
@@ -352,7 +361,7 @@ describe("respawnGatewayProcessForUpdate", () => {
|
||||
const entry =
|
||||
"/app/node_modules/.pnpm/@anthropic+sdk@1.0.0/node_modules/@anthropic/sdk/dist/index.js";
|
||||
process.argv = ["/usr/local/bin/node", entry, "gateway", "run"];
|
||||
spawnMock.mockReturnValue({ pid: 8181, unref: vi.fn(), kill: vi.fn() });
|
||||
spawnMock.mockReturnValue(mockDetachedChild(8181));
|
||||
|
||||
respawnGatewayProcessForUpdate();
|
||||
|
||||
@@ -369,7 +378,7 @@ describe("respawnGatewayProcessForUpdate", () => {
|
||||
process.env.XPC_SERVICE_NAME = "ai.openclaw.mac";
|
||||
process.execArgv = [];
|
||||
process.argv = ["/usr/local/bin/node", "/repo/dist/index.js", "gateway", "run"];
|
||||
spawnMock.mockReturnValue({ pid: 6161, unref: vi.fn(), kill: vi.fn() });
|
||||
spawnMock.mockReturnValue(mockDetachedChild(6161));
|
||||
|
||||
const result = respawnGatewayProcessForUpdate();
|
||||
|
||||
@@ -386,6 +395,27 @@ describe("respawnGatewayProcessForUpdate", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("registers a no-op detached child error listener before unref", () => {
|
||||
clearSupervisorHints();
|
||||
setPlatform("linux");
|
||||
process.execArgv = [];
|
||||
process.argv = ["/usr/local/bin/node", "/repo/dist/index.js", "gateway", "run"];
|
||||
const child = mockDetachedChild(9191);
|
||||
spawnMock.mockReturnValue(child);
|
||||
|
||||
const result = respawnGatewayProcessForUpdate();
|
||||
|
||||
expect(result.mode).toBe("spawned");
|
||||
expect(result.child).toBe(child);
|
||||
expect(child.on).toHaveBeenCalledWith("error", expect.any(Function));
|
||||
const errorListener = child.on.mock.calls.find(([event]) => event === "error")?.[1];
|
||||
expect(() => errorListener?.(new Error("spawn ENOENT"))).not.toThrow();
|
||||
expect(child.unref).toHaveBeenCalledOnce();
|
||||
const onCallOrder = child.on.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY;
|
||||
const unrefCallOrder = child.unref.mock.invocationCallOrder[0] ?? Number.NEGATIVE_INFINITY;
|
||||
expect(onCallOrder).toBeLessThan(unrefCallOrder);
|
||||
});
|
||||
|
||||
it("exits to a managed supervisor for updates even when respawn is disabled", () => {
|
||||
clearSupervisorHints();
|
||||
setPlatform("linux");
|
||||
|
||||
@@ -53,6 +53,9 @@ function spawnDetachedGatewayProcess(opts: GatewayRespawnOptions = {}): {
|
||||
detached: true,
|
||||
stdio: "inherit",
|
||||
});
|
||||
// Detached spawn failures can arrive asynchronously after spawn() returns.
|
||||
// Keep this listener before unref() so the parent does not crash during handoff.
|
||||
child.on("error", () => {});
|
||||
child.unref();
|
||||
return { child, pid: child.pid ?? undefined };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user