fix(google-meet): contain node host stream failures (#102105)

Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit 624dfa6cf6)
This commit is contained in:
wangmiao0668000666
2026-07-09 12:59:35 +08:00
committed by Dallin Romney
parent 75f0470c6c
commit 1918ea2563
2 changed files with 62 additions and 21 deletions
+44 -2
View File
@@ -9,7 +9,7 @@ type MockChild = EventEmitter & {
kill: ReturnType<typeof vi.fn>;
stdout?: EventEmitter;
stderr?: EventEmitter;
stdin?: { write: ReturnType<typeof vi.fn> };
stdin?: EventEmitter & { write: ReturnType<typeof vi.fn> };
};
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;
+18 -19
View File
@@ -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;
}