From 1918ea256312182b9a3ff60408b6e059c48639fc Mon Sep 17 00:00:00 2001 From: wangmiao0668000666 Date: Thu, 9 Jul 2026 12:59:35 +0800 Subject: [PATCH] fix(google-meet): contain node host stream failures (#102105) Co-authored-by: Peter Steinberger (cherry picked from commit 624dfa6cf6dc19d5f52553b45e503f1d4a4678c7) --- extensions/google-meet/node-host.test.ts | 46 ++++++++++++++++++++++-- extensions/google-meet/src/node-host.ts | 37 ++++++++++--------- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/extensions/google-meet/node-host.test.ts b/extensions/google-meet/node-host.test.ts index f046ccfa91b9..5d4730ad50f4 100644 --- a/extensions/google-meet/node-host.test.ts +++ b/extensions/google-meet/node-host.test.ts @@ -9,7 +9,7 @@ type MockChild = EventEmitter & { kill: ReturnType; stdout?: EventEmitter; stderr?: EventEmitter; - stdin?: { write: ReturnType }; + stdin?: EventEmitter & { write: ReturnType }; }; const children: MockChild[] = []; @@ -34,7 +34,7 @@ vi.mock("node:child_process", async (importOriginal) => { }), stdout: new EventEmitter(), stderr: new EventEmitter(), - stdin: { write: vi.fn() }, + stdin: Object.assign(new EventEmitter(), { write: vi.fn() }), }) as MockChild; children.push(child); return child; @@ -166,6 +166,48 @@ describe("google-meet node host bridge sessions", () => { } }); + it("closes once when command-pair streams fail together", async () => { + const originalPlatform = process.platform; + children.length = 0; + + Object.defineProperty(process, "platform", { configurable: true, value: "darwin" }); + try { + const start = JSON.parse( + await handleGoogleMeetNodeHostCommand( + JSON.stringify({ + action: "start", + url: "https://meet.google.com/xyz-abcd-uvw", + mode: "realtime", + launch: false, + audioInputCommand: ["mock-rec"], + audioOutputCommand: ["mock-play"], + }), + ), + ); + const [outputProcess, inputProcess] = children; + if (!outputProcess || !inputProcess) { + throw new Error("expected Google Meet node host command-pair processes"); + } + + outputProcess.stderr?.emit("error", new Error("output stderr failed")); + inputProcess.stdout?.emit("error", new Error("input stdout failed")); + inputProcess.stderr?.emit("error", new Error("input stderr failed")); + + const status = JSON.parse( + await handleGoogleMeetNodeHostCommand( + JSON.stringify({ action: "status", bridgeId: start.bridgeId }), + ), + ); + expect(status.bridge.closed).toBe(true); + expect(outputProcess.kill).toHaveBeenCalledTimes(1); + expect(inputProcess.kill).toHaveBeenCalledTimes(1); + expect(outputProcess.kill).toHaveBeenCalledWith("SIGTERM"); + expect(inputProcess.kill).toHaveBeenCalledWith("SIGTERM"); + } finally { + Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform }); + } + }); + it("lists active bridge sessions and hides closed sessions", async () => { const originalPlatform = process.platform; children.length = 0; diff --git a/extensions/google-meet/src/node-host.ts b/extensions/google-meet/src/node-host.ts index 27ddae829420..96c0cc794564 100644 --- a/extensions/google-meet/src/node-host.ts +++ b/extensions/google-meet/src/node-host.ts @@ -107,32 +107,28 @@ function wake(session: NodeBridgeSession) { } function stopSession(session: NodeBridgeSession) { - const wasClosed = session.closed; + // Process and stream errors can arrive together during teardown. Close once + // so the same children do not get duplicate termination timers. + if (session.closed) { + return; + } session.closed = true; - session.closedAt ??= new Date().toISOString(); + session.closedAt = new Date().toISOString(); terminateChild(session.input); terminateChild(session.output); - if (!wasClosed) { - wake(session); - } + wake(session); } function attachOutputProcessHandlers(session: NodeBridgeSession, outputProcess: ChildProcess) { - outputProcess.on("exit", () => { + const stopIfCurrent = () => { if (session.output === outputProcess) { stopSession(session); } - }); - outputProcess.on("error", () => { - if (session.output === outputProcess) { - stopSession(session); - } - }); - outputProcess.stdin?.on?.("error", () => { - if (session.output === outputProcess) { - stopSession(session); - } - }); + }; + outputProcess.on("exit", stopIfCurrent); + outputProcess.on("error", stopIfCurrent); + outputProcess.stdin?.on("error", stopIfCurrent); + outputProcess.stderr?.on("error", stopIfCurrent); } function startOutputProcess(command: { command: string; args: string[] }) { @@ -178,9 +174,12 @@ function startCommandPair(params: { } wake(session); }); - inputProcess.on("exit", () => stopSession(session)); + const stop = () => stopSession(session); + inputProcess.on("exit", stop); + inputProcess.on("error", stop); + inputProcess.stdout?.on("error", stop); + inputProcess.stderr?.on("error", stop); attachOutputProcessHandlers(session, outputProcess); - inputProcess.on("error", () => stopSession(session)); sessions.set(session.id, session); return session; }