Skip to content

Commit 624dfa6

Browse files
fix(google-meet): contain node host stream failures (#102105)
Co-authored-by: Peter Steinberger <[email protected]>
1 parent 9b4cbe4 commit 624dfa6

2 files changed

Lines changed: 62 additions & 21 deletions

File tree

extensions/google-meet/node-host.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type MockChild = EventEmitter & {
99
kill: ReturnType<typeof vi.fn>;
1010
stdout?: EventEmitter;
1111
stderr?: EventEmitter;
12-
stdin?: { write: ReturnType<typeof vi.fn> };
12+
stdin?: EventEmitter & { write: ReturnType<typeof vi.fn> };
1313
};
1414

1515
const children: MockChild[] = [];
@@ -34,7 +34,7 @@ vi.mock("node:child_process", async (importOriginal) => {
3434
}),
3535
stdout: new EventEmitter(),
3636
stderr: new EventEmitter(),
37-
stdin: { write: vi.fn() },
37+
stdin: Object.assign(new EventEmitter(), { write: vi.fn() }),
3838
}) as MockChild;
3939
children.push(child);
4040
return child;
@@ -201,6 +201,48 @@ describe("google-meet node host bridge sessions", () => {
201201
}
202202
});
203203

204+
it("closes once when command-pair streams fail together", async () => {
205+
const originalPlatform = process.platform;
206+
children.length = 0;
207+
208+
Object.defineProperty(process, "platform", { configurable: true, value: "darwin" });
209+
try {
210+
const start = JSON.parse(
211+
await handleGoogleMeetNodeHostCommand(
212+
JSON.stringify({
213+
action: "start",
214+
url: "https://meet.google.com/xyz-abcd-uvw",
215+
mode: "realtime",
216+
launch: false,
217+
audioInputCommand: ["mock-rec"],
218+
audioOutputCommand: ["mock-play"],
219+
}),
220+
),
221+
);
222+
const [outputProcess, inputProcess] = children;
223+
if (!outputProcess || !inputProcess) {
224+
throw new Error("expected Google Meet node host command-pair processes");
225+
}
226+
227+
outputProcess.stderr?.emit("error", new Error("output stderr failed"));
228+
inputProcess.stdout?.emit("error", new Error("input stdout failed"));
229+
inputProcess.stderr?.emit("error", new Error("input stderr failed"));
230+
231+
const status = JSON.parse(
232+
await handleGoogleMeetNodeHostCommand(
233+
JSON.stringify({ action: "status", bridgeId: start.bridgeId }),
234+
),
235+
);
236+
expect(status.bridge.closed).toBe(true);
237+
expect(outputProcess.kill).toHaveBeenCalledTimes(1);
238+
expect(inputProcess.kill).toHaveBeenCalledTimes(1);
239+
expect(outputProcess.kill).toHaveBeenCalledWith("SIGTERM");
240+
expect(inputProcess.kill).toHaveBeenCalledWith("SIGTERM");
241+
} finally {
242+
Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
243+
}
244+
});
245+
204246
it("lists active bridge sessions and hides closed sessions", async () => {
205247
const originalPlatform = process.platform;
206248
children.length = 0;

extensions/google-meet/src/node-host.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -107,32 +107,28 @@ function wake(session: NodeBridgeSession) {
107107
}
108108

109109
function stopSession(session: NodeBridgeSession) {
110-
const wasClosed = session.closed;
110+
// Process and stream errors can arrive together during teardown. Close once
111+
// so the same children do not get duplicate termination timers.
112+
if (session.closed) {
113+
return;
114+
}
111115
session.closed = true;
112-
session.closedAt ??= new Date().toISOString();
116+
session.closedAt = new Date().toISOString();
113117
terminateChild(session.input);
114118
terminateChild(session.output);
115-
if (!wasClosed) {
116-
wake(session);
117-
}
119+
wake(session);
118120
}
119121

120122
function attachOutputProcessHandlers(session: NodeBridgeSession, outputProcess: ChildProcess) {
121-
outputProcess.on("exit", () => {
122-
if (session.output === outputProcess) {
123-
stopSession(session);
124-
}
125-
});
126-
outputProcess.on("error", () => {
123+
const stopIfCurrent = () => {
127124
if (session.output === outputProcess) {
128125
stopSession(session);
129126
}
130-
});
131-
outputProcess.stdin?.on?.("error", () => {
132-
if (session.output === outputProcess) {
133-
stopSession(session);
134-
}
135-
});
127+
};
128+
outputProcess.on("exit", stopIfCurrent);
129+
outputProcess.on("error", stopIfCurrent);
130+
outputProcess.stdin?.on("error", stopIfCurrent);
131+
outputProcess.stderr?.on("error", stopIfCurrent);
136132
}
137133

138134
function startOutputProcess(command: { command: string; args: string[] }) {
@@ -178,9 +174,12 @@ function startCommandPair(params: {
178174
}
179175
wake(session);
180176
});
181-
inputProcess.on("exit", () => stopSession(session));
177+
const stop = () => stopSession(session);
178+
inputProcess.on("exit", stop);
179+
inputProcess.on("error", stop);
180+
inputProcess.stdout?.on("error", stop);
181+
inputProcess.stderr?.on("error", stop);
182182
attachOutputProcessHandlers(session, outputProcess);
183-
inputProcess.on("error", () => stopSession(session));
184183
sessions.set(session.id, session);
185184
return session;
186185
}

0 commit comments

Comments
 (0)