Skip to content

Commit e55ea2d

Browse files
motiz88facebook-github-bot
authored andcommitted
Fix debugger handoff logic to prevent "zombie" state (facebook#45035)
Summary: Pull Request resolved: facebook#45035 Changelog: [General][Fixed] Avoid a zombie state when opening a second debugger frontend concurrently. The problem here was that we were sending proxy-protocol messages to the device in the wrong order (`disconnect` *after* `connect`): {F1701266597} The root cause was that we were depending on the outgoing debugger socket's async `close` event to trigger sending the `disconnect` message to the device. This would happen after we'd already (synchronously) sent the `connect` message. With this diff, we send the `disconnect` message synchronously with calling `close()` on the debugger socket, which fixes the ordering problem at the source. To avoid sending duplicate `disconnect` messages (e.g. one before calling `close()` and one from the `close` event handler), we store some extra state on `Device` (`#connectedPageIds`). Reviewed By: robhogan, huntie Differential Revision: D58730634 fbshipit-source-id: 0f54af2e4f8071a8f6d97cc9e3d8a4ea89a46f43
1 parent 9cca4c1 commit e55ea2d

File tree

2 files changed

+52
-47
lines changed

2 files changed

+52
-47
lines changed

packages/dev-middleware/src/__tests__/InspectorProxyCdpTransport-test.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -247,19 +247,16 @@ describe.each(['HTTP', 'HTTPS'])(
247247
event: 'create-debugger-mock',
248248
name: 'debugger2',
249249
},
250-
// FIXME: We currently send `connect` (for debugger2) before
251-
// `disconnect` (for debugger1), which is wrong - it leaves the
252-
// device thinking the connection is gone while the proxy keeps the
253-
// debugger connection open. The user of debugger2 sees the frontend
254-
// in a "zombie" state (not disconnected, but unresponsive).
255250
{
256-
event: 'connect',
251+
// NOTE: For debugger1
252+
event: 'disconnect',
257253
payload: {
258254
pageId: 'page1',
259255
},
260256
},
261257
{
262-
event: 'disconnect',
258+
// NOTE: For debugger2
259+
event: 'connect',
263260
payload: {
264261
pageId: 'page1',
265262
},

packages/dev-middleware/src/inspector-proxy/Device.js

Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const REWRITE_HOSTS_TO_LOCALHOST: Array<string> = [
5858
// more details.
5959
const FILE_PREFIX = 'file://';
6060

61-
type DebuggerInfo = {
61+
type DebuggerConnection = {
6262
// Debugger web socket connection
6363
socket: WS,
6464
// If we replaced address (like '10.0.2.2') to localhost we need to store original
@@ -67,10 +67,6 @@ type DebuggerInfo = {
6767
prependedFilePrefix: boolean,
6868
pageId: string,
6969
userAgent: string | null,
70-
};
71-
72-
type DebuggerConnection = {
73-
...DebuggerInfo,
7470
customHandler: ?CustomMessageHandler,
7571
};
7672

@@ -128,6 +124,8 @@ export default class Device {
128124
// The device message middleware factory function allowing implementers to handle unsupported CDP messages.
129125
#createCustomMessageHandler: ?CreateCustomMessageHandlerFn;
130126

127+
#connectedPageIds: Set<string> = new Set();
128+
131129
constructor(
132130
id: string,
133131
name: string,
@@ -215,15 +213,23 @@ export default class Device {
215213
if (socket === this.#deviceSocket) {
216214
this.#deviceEventReporter?.logDisconnection('device');
217215
// Device disconnected - close debugger connection.
218-
if (this.#debuggerConnection) {
219-
this.#debuggerConnection.socket.close();
220-
this.#debuggerConnection = null;
221-
}
216+
this.#terminateDebuggerConnection();
222217
clearInterval(this.#pagesPollingIntervalId);
223218
}
224219
});
225220
}
226221

222+
#terminateDebuggerConnection() {
223+
const debuggerConnection = this.#debuggerConnection;
224+
if (debuggerConnection) {
225+
this.#sendDisconnectEventToDevice(
226+
this.#mapToDevicePageId(debuggerConnection.pageId),
227+
);
228+
debuggerConnection.socket.close();
229+
this.#debuggerConnection = null;
230+
}
231+
}
232+
227233
/**
228234
* Used to recreate the device connection if there is a device ID collision.
229235
* 1. Checks if the same device is attempting to reconnect for the same app.
@@ -246,12 +252,14 @@ export default class Device {
246252
id === this.#id,
247253
'dangerouslyRecreateDevice() can only be used for the same device ID',
248254
);
255+
256+
const oldDebugger = this.#debuggerConnection;
257+
249258
if (this.#app !== app || this.#name !== name) {
250259
this.#deviceSocket.close();
251-
this.#debuggerConnection?.socket.close();
260+
this.#terminateDebuggerConnection();
252261
}
253262

254-
const oldDebugger = this.#debuggerConnection;
255263
this.#debuggerConnection = null;
256264

257265
if (oldDebugger) {
@@ -309,10 +317,7 @@ export default class Device {
309317
});
310318

311319
// Disconnect current debugger if we already have debugger connected.
312-
if (this.#debuggerConnection) {
313-
this.#debuggerConnection.socket.close();
314-
this.#debuggerConnection = null;
315-
}
320+
this.#terminateDebuggerConnection();
316321

317322
const debuggerInfo = {
318323
socket,
@@ -377,12 +382,7 @@ export default class Device {
377382
}
378383
}
379384

380-
this.#sendMessageToDevice({
381-
event: 'connect',
382-
payload: {
383-
pageId: this.#mapToDevicePageId(pageId),
384-
},
385-
});
385+
this.#sendConnectEventToDevice(this.#mapToDevicePageId(pageId));
386386

387387
// $FlowFixMe[incompatible-call]
388388
socket.on('message', (message: string) => {
@@ -426,13 +426,9 @@ export default class Device {
426426
socket.on('close', () => {
427427
debug(`Debugger for page ${pageId} and ${this.#name} disconnected.`);
428428
this.#deviceEventReporter?.logDisconnection('debugger');
429-
this.#sendMessageToDevice({
430-
event: 'disconnect',
431-
payload: {
432-
pageId: this.#mapToDevicePageId(pageId),
433-
},
434-
});
435-
this.#debuggerConnection = null;
429+
if (this.#debuggerConnection?.socket === socket) {
430+
this.#terminateDebuggerConnection();
431+
}
436432
});
437433

438434
// $FlowFixMe[method-unbinding]
@@ -444,6 +440,28 @@ export default class Device {
444440
};
445441
}
446442

443+
#sendConnectEventToDevice(devicePageId: string) {
444+
if (this.#connectedPageIds.has(devicePageId)) {
445+
return;
446+
}
447+
this.#connectedPageIds.add(devicePageId);
448+
this.#sendMessageToDevice({
449+
event: 'connect',
450+
payload: {pageId: devicePageId},
451+
});
452+
}
453+
454+
#sendDisconnectEventToDevice(devicePageId: string) {
455+
if (!this.#connectedPageIds.has(devicePageId)) {
456+
return;
457+
}
458+
this.#connectedPageIds.delete(devicePageId);
459+
this.#sendMessageToDevice({
460+
event: 'disconnect',
461+
payload: {pageId: devicePageId},
462+
});
463+
}
464+
447465
/**
448466
* Returns `true` if a page supports the given target capability flag.
449467
*/
@@ -621,20 +639,10 @@ export default class Device {
621639
// page.
622640

623641
if (oldPageId != null) {
624-
this.#sendMessageToDevice({
625-
event: 'disconnect',
626-
payload: {
627-
pageId: oldPageId,
628-
},
629-
});
642+
this.#sendDisconnectEventToDevice(oldPageId);
630643
}
631644

632-
this.#sendMessageToDevice({
633-
event: 'connect',
634-
payload: {
635-
pageId: page.id,
636-
},
637-
});
645+
this.#sendConnectEventToDevice(page.id);
638646

639647
const toSend = [
640648
{method: 'Runtime.enable', id: 1e9},

0 commit comments

Comments
 (0)