Skip to content

Commit e224c84

Browse files
committed
fix(update): ignore restart script spawn failures
1 parent acf265d commit e224c84

3 files changed

Lines changed: 45 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Docs: https://docs.openclaw.ai
8181
- Models: prune retired Groq, GitHub Copilot, OpenAI, xAI, and old Claude catalog entries, with doctor migration to upgrade existing configs to current provider refs.
8282
- Doctor/update: recognize junction-backed source checkouts as git installs by comparing canonical paths before showing package-manager update guidance. Fixes #82215. Thanks @igormf.
8383
- Channels: honor `/verbose on` for tool/progress summaries across direct chats, groups, channels, and forum topics while preserving quiet default behavior. (#85488) Thanks @kurplunkin.
84+
- Update: keep the detached gateway restart handoff best-effort when the restart script process cannot be spawned. (#83892) Thanks @davinci282828.
8485
- Telegram: persist the prompt-context message cache through plugin state and record bot-authored replies after sends and draft streaming so later turns can include prior assistant replies without relying on the JSON sidecar. (#85231) Thanks @keshavbotagent.
8586
- CLI/skills: show an all-ready note with next-step commands when skill setup has no missing dependencies to install. (#85032) Thanks @aniruddhaadak80.
8687
- Microsoft Foundry: route DeepSeek V4 Pro and Flash models through the Foundry Responses API while keeping older DeepSeek models on their existing path. (#85549) Thanks @roslinmahmud.

src/cli/update-cli/restart-helper.test.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ exit 0
593593
it("spawns the script as a detached process on Linux", async () => {
594594
Object.defineProperty(process, "platform", { value: "linux" });
595595
const scriptPath = "/tmp/fake-script.sh";
596-
const mockChild = { unref: vi.fn() };
596+
const mockChild = { on: vi.fn(), unref: vi.fn() };
597597
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
598598

599599
await runRestartScript(scriptPath);
@@ -603,13 +603,14 @@ exit 0
603603
stdio: "ignore",
604604
windowsHide: true,
605605
});
606+
expect(mockChild.on).toHaveBeenCalledWith("error", expect.any(Function));
606607
expect(mockChild.unref).toHaveBeenCalledTimes(1);
607608
});
608609

609610
it("uses cmd.exe on Windows", async () => {
610611
Object.defineProperty(process, "platform", { value: "win32" });
611612
const scriptPath = "C:\\Temp\\fake-script.bat";
612-
const mockChild = { unref: vi.fn() };
613+
const mockChild = { on: vi.fn(), unref: vi.fn() };
613614
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
614615

615616
await runRestartScript(scriptPath);
@@ -619,13 +620,14 @@ exit 0
619620
stdio: "ignore",
620621
windowsHide: true,
621622
});
623+
expect(mockChild.on).toHaveBeenCalledWith("error", expect.any(Function));
622624
expect(mockChild.unref).toHaveBeenCalledTimes(1);
623625
});
624626

625627
it("quotes cmd.exe /c paths with metacharacters on Windows", async () => {
626628
Object.defineProperty(process, "platform", { value: "win32" });
627629
const scriptPath = "C:\\Temp\\me&(ow)\\fake-script.bat";
628-
const mockChild = { unref: vi.fn() };
630+
const mockChild = { on: vi.fn(), unref: vi.fn() };
629631
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
630632

631633
await runRestartScript(scriptPath);
@@ -636,5 +638,33 @@ exit 0
636638
windowsHide: true,
637639
});
638640
});
641+
642+
it("does not throw when spawn fails synchronously", async () => {
643+
Object.defineProperty(process, "platform", { value: "linux" });
644+
vi.mocked(spawn).mockImplementation(() => {
645+
throw Object.assign(new Error("spawn /bin/sh ENOENT"), { code: "ENOENT" });
646+
});
647+
648+
await expect(runRestartScript("/tmp/fake-script.sh")).resolves.toBeUndefined();
649+
});
650+
651+
it("handles child process spawn errors after the detached handoff", async () => {
652+
Object.defineProperty(process, "platform", { value: "linux" });
653+
let errorHandler: ((error: Error) => void) | undefined;
654+
const mockChild = {
655+
on: vi.fn((event: string, handler: (error: Error) => void) => {
656+
if (event === "error") {
657+
errorHandler = handler;
658+
}
659+
return mockChild;
660+
}),
661+
unref: vi.fn(),
662+
};
663+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
664+
665+
await runRestartScript("/tmp/fake-script.sh");
666+
expect(errorHandler).toBeDefined();
667+
expect(() => errorHandler?.(new Error("spawn /bin/sh ENOENT"))).not.toThrow();
668+
});
639669
});
640670
});

src/cli/update-cli/restart-helper.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -406,10 +406,15 @@ export async function runRestartScript(scriptPath: string): Promise<void> {
406406
const file = isWindows ? "cmd.exe" : "/bin/sh";
407407
const args = isWindows ? ["/d", "/s", "/c", quoteCmdScriptArg(scriptPath)] : [scriptPath];
408408

409-
const child = spawn(file, args, {
410-
detached: true,
411-
stdio: "ignore",
412-
windowsHide: true,
413-
});
414-
child.unref();
409+
try {
410+
const child = spawn(file, args, {
411+
detached: true,
412+
stdio: "ignore",
413+
windowsHide: true,
414+
});
415+
child.on("error", () => {});
416+
child.unref();
417+
} catch {
418+
// Restart handoff is best-effort; update completion must not crash here.
419+
}
415420
}

0 commit comments

Comments
 (0)