From 05fbdd4b2855d72b80520fc40788182475d94577 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 04:28:27 +0100 Subject: [PATCH] fix: handle missing tailscale binary --- .../voice-call/src/webhook/tailscale.test.ts | 19 ++++++++++++++++ .../voice-call/src/webhook/tailscale.ts | 22 +++++++++++++++---- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/extensions/voice-call/src/webhook/tailscale.test.ts b/extensions/voice-call/src/webhook/tailscale.test.ts index e42bc5cafa1..55e6b786972 100644 --- a/extensions/voice-call/src/webhook/tailscale.test.ts +++ b/extensions/voice-call/src/webhook/tailscale.test.ts @@ -40,6 +40,19 @@ function createProc(params?: { code?: number; stdout?: string }) { return proc; } +function createErrorProc() { + const proc = new EventEmitter() as EventEmitter & { + stdout: EventEmitter; + kill: ReturnType; + }; + proc.stdout = new EventEmitter(); + proc.kill = vi.fn(); + setTimeout(() => { + proc.emit("error", Object.assign(new Error("spawn tailscale ENOENT"), { code: "ENOENT" })); + }, 0); + return proc; +} + describe("voice-call tailscale helpers", () => { beforeEach(() => { vi.clearAllMocks(); @@ -83,6 +96,12 @@ describe("voice-call tailscale helpers", () => { await expect(getTailscaleSelfInfo()).resolves.toBeNull(); }); + it("treats missing tailscale binary as unavailable instead of leaking spawn errors", async () => { + spawnMock.mockReturnValueOnce(createErrorProc()); + + await expect(getTailscaleSelfInfo()).resolves.toBeNull(); + }); + it("sets up and cleans up exposure routes with the selected mode", async () => { spawnMock .mockReturnValueOnce( diff --git a/extensions/voice-call/src/webhook/tailscale.ts b/extensions/voice-call/src/webhook/tailscale.ts index d0051fbcb53..03717ad932b 100644 --- a/extensions/voice-call/src/webhook/tailscale.ts +++ b/extensions/voice-call/src/webhook/tailscale.ts @@ -16,18 +16,32 @@ function runTailscaleCommand( }); let stdout = ""; + let settled = false; + let timer: ReturnType; + const finish = (result: { code: number; stdout: string }) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + resolve(result); + }; + proc.stdout.on("data", (data) => { stdout += data; }); - const timer = setTimeout(() => { + timer = setTimeout(() => { proc.kill("SIGKILL"); - resolve({ code: -1, stdout: "" }); + finish({ code: -1, stdout: "" }); }, timeoutMs); + proc.on("error", () => { + finish({ code: -1, stdout: "" }); + }); + proc.on("close", (code) => { - clearTimeout(timer); - resolve({ code: code ?? -1, stdout }); + finish({ code: code ?? -1, stdout }); }); }); }