Skip to content

Commit

Permalink
Inspector proxy: Add ping/pong keepalive/heartbeat to debugger connec…
Browse files Browse the repository at this point in the history
…tion (facebook#44086)

Summary:

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
  • Loading branch information
robhogan authored and facebook-github-bot committed Apr 15, 2024
1 parent 99d0c54 commit a3d11d6
Showing 1 changed file with 50 additions and 0 deletions.
50 changes: 50 additions & 0 deletions packages/dev-middleware/src/inspector-proxy/InspectorProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ import type {
PageDescription,
} from './types';
import type {IncomingMessage, ServerResponse} from 'http';
// $FlowFixMe[cannot-resolve-module] libdef missing in RN OSS
import type {Timeout} from 'timers';

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

Expand All @@ -32,6 +37,8 @@ const WS_DEBUGGER_URL = '/inspector/debug';
const PAGES_LIST_JSON_URL = '/json';
const PAGES_LIST_JSON_URL_2 = '/json/list';
const PAGES_LIST_JSON_VERSION_URL = '/json/version';
const MAX_PONG_LATENCY_MS = 5000;
const DEBUGGER_HEARTBEAT_INTERVAL_MS = 10000;

const INTERNAL_ERROR_CODE = 1011;

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

this.#startHeartbeat(socket, DEBUGGER_HEARTBEAT_INTERVAL_MS);

device.handleDebuggerConnection(socket, pageId, {
userAgent: req.headers['user-agent'] ?? query.userAgent ?? null,
});
Expand All @@ -279,4 +288,45 @@ export default class InspectorProxy implements InspectorProxyQueries {
});
return wss;
}

// Starts pinging the socket at the given interval. Compliant clients will
// respond with pong frame. This serves both to detect when the client
// has gone away without sending a close frame, and as a keepalive in cases
// where proxies may drop idle connections (e.g., VS Code tunnels).
//
// https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2
#startHeartbeat(socket: WS, intervalMs: number) {
let terminateTimeout = null;

const pingTimeout: Timeout = setTimeout(() => {
if (socket.readyState !== WS.OPEN) {
// May be connecting or closing, try again later.
pingTimeout.refresh();
return;
}
socket.ping();
terminateTimeout = setTimeout(() => {
if (socket.readyState !== WS.OPEN) {
return;
}
// We don't use close() here because that initiates a closing handshake,
// which will not complete if the other end has gone away - 'close'
// would not be emitted.
//
// terminate() emits 'close' immediately, allowing us to handle it and
// inform any clients.
socket.terminate();
}, MAX_PONG_LATENCY_MS).unref();
}, intervalMs).unref();

socket.on('pong', () => {
terminateTimeout && clearTimeout(terminateTimeout);
pingTimeout.refresh();
});

socket.on('close', () => {
terminateTimeout && clearTimeout(terminateTimeout);
clearTimeout(pingTimeout);
});
}
}

0 comments on commit a3d11d6

Please sign in to comment.