Skip to content

Commit 7047563

Browse files
robhoganfacebook-github-bot
authored andcommitted
Inspector proxy: Add ping/pong keepalive/heartbeat to debugger connection (#44086)
Summary: Pull Request resolved: #44086 When a debugger frontend is connected to inspector-proxy via another proxy or tunnel that times out on idle (such as [VS Code's remote tunnel](https://github.com/microsoft/vscode/blob/main/src/vs/platform/tunnel/node/tunnelService.ts)), the connection between proxy and debugger may be dropped. In addition, when the connection is dropped without a closing handshake, the proxy does *not* detect the disconnection - no disconnect is logged to the reporter and no notifications are sent to any connected devices. This adds a mechanism using the WebSocket-standard `ping` and `pong` frames to: 1. Keep the connection alive 2. Detect when the debugger has gone away Note that as all WebSocket clients already **must** reply to a ping with a pong, this is non-breaking for compliant implementations: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 Changelog: [General][Added] Inspector proxy: Add ping/pong keepalive to debugger connections. Reviewed By: hoxyq Differential Revision: D56069185 fbshipit-source-id: e322de631c652a502f3d554c15ed5412a751ee04
1 parent 718041a commit 7047563

1 file changed

Lines changed: 50 additions & 0 deletions

File tree

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ import type {
1919
PageDescription,
2020
} from './types';
2121
import type {IncomingMessage, ServerResponse} from 'http';
22+
// $FlowFixMe[cannot-resolve-module] libdef missing in RN OSS
23+
import type {Timeout} from 'timers';
2224

2325
import Device from './Device';
2426
import nullthrows from 'nullthrows';
27+
// Import these from node:timers to get the correct Flow types.
28+
// $FlowFixMe[cannot-resolve-module] libdef missing in RN OSS
29+
import {clearTimeout, setTimeout} from 'timers';
2530
import url from 'url';
2631
import WS from 'ws';
2732

@@ -32,6 +37,8 @@ const WS_DEBUGGER_URL = '/inspector/debug';
3237
const PAGES_LIST_JSON_URL = '/json';
3338
const PAGES_LIST_JSON_URL_2 = '/json/list';
3439
const PAGES_LIST_JSON_VERSION_URL = '/json/version';
40+
const MAX_PONG_LATENCY_MS = 5000;
41+
const DEBUGGER_HEARTBEAT_INTERVAL_MS = 10000;
3542

3643
const INTERNAL_ERROR_CODE = 1011;
3744

@@ -264,6 +271,8 @@ export default class InspectorProxy implements InspectorProxyQueries {
264271
throw new Error('Unknown device with ID ' + deviceId);
265272
}
266273

274+
this.#startHeartbeat(socket, DEBUGGER_HEARTBEAT_INTERVAL_MS);
275+
267276
device.handleDebuggerConnection(socket, pageId, {
268277
userAgent: req.headers['user-agent'] ?? query.userAgent ?? null,
269278
});
@@ -279,4 +288,45 @@ export default class InspectorProxy implements InspectorProxyQueries {
279288
});
280289
return wss;
281290
}
291+
292+
// Starts pinging the socket at the given interval. Compliant clients will
293+
// respond with pong frame. This serves both to detect when the client
294+
// has gone away without sending a close frame, and as a keepalive in cases
295+
// where proxies may drop idle connections (e.g., VS Code tunnels).
296+
//
297+
// https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2
298+
#startHeartbeat(socket: WS, intervalMs: number) {
299+
let terminateTimeout = null;
300+
301+
const pingTimeout: Timeout = setTimeout(() => {
302+
if (socket.readyState !== WS.OPEN) {
303+
// May be connecting or closing, try again later.
304+
pingTimeout.refresh();
305+
return;
306+
}
307+
socket.ping();
308+
terminateTimeout = setTimeout(() => {
309+
if (socket.readyState !== WS.OPEN) {
310+
return;
311+
}
312+
// We don't use close() here because that initiates a closing handshake,
313+
// which will not complete if the other end has gone away - 'close'
314+
// would not be emitted.
315+
//
316+
// terminate() emits 'close' immediately, allowing us to handle it and
317+
// inform any clients.
318+
socket.terminate();
319+
}, MAX_PONG_LATENCY_MS).unref();
320+
}, intervalMs).unref();
321+
322+
socket.on('pong', () => {
323+
terminateTimeout && clearTimeout(terminateTimeout);
324+
pingTimeout.refresh();
325+
});
326+
327+
socket.on('close', () => {
328+
terminateTimeout && clearTimeout(terminateTimeout);
329+
clearTimeout(pingTimeout);
330+
});
331+
}
282332
}

0 commit comments

Comments
 (0)