cdp-tunnel 3.6.1 → 3.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "CDP Bridge",
4
- "version": "3.6.0",
4
+ "version": "3.6.2",
5
5
  "description": "Chrome DevTools Protocol Bridge for Playwright/Puppeteer automation",
6
6
  "permissions": [
7
7
  "debugger",
package/package.json CHANGED
@@ -1,20 +1,13 @@
1
1
  {
2
2
  "name": "cdp-tunnel",
3
- "version": "3.6.1",
3
+ "version": "3.6.2",
4
4
  "description": "Bridge Chrome's debugger API to WebSocket — control your existing browser with Playwright/Puppeteer via CDP",
5
5
  "main": "server/proxy-server.js",
6
- "bin": "./cli/index.js",
6
+ "bin": {
7
+ "cdp-tunnel": "./cli/index.js"
8
+ },
7
9
  "scripts": {
8
10
  "start": "node server/proxy-server.js",
9
- "client": "node client/playwright-client.js",
10
- "client:puppeteer": "node client/puppeteer-client.js",
11
- "test:compat": "node client/test-compat-client.js",
12
- "test:newpage": "node client/test-newpage-sequence.js",
13
- "record": "node cdp-recorder/record.js",
14
- "benchmark": "node benchmark/run-all.js",
15
- "benchmark:native": "node benchmark/run-all.js --native",
16
- "benchmark:proxy": "node benchmark/run-all.js --proxy",
17
- "demo": "node demo/server.js",
18
11
  "test:compare": "node tests/cdp-compare/cdp-compare-runner.js",
19
12
  "test": "node tests/e2e/run-all.js",
20
13
  "test:e2e": "node tests/e2e/run-all.js",
@@ -68,11 +61,6 @@
68
61
  "devDependencies": {
69
62
  "chrome-remote-interface": "^0.34.0",
70
63
  "husky": "^9.1.7",
71
- "lz-string": "^1.5.0",
72
- "pako": "^2.1.0",
73
- "playwright": "^1.58.2",
74
- "playwright-core": "^1.58.2",
75
- "puppeteer-core": "^22.0.0",
76
- "sharp": "^0.34.0"
64
+ "playwright": "^1.58.2"
77
65
  }
78
66
  }
@@ -334,7 +334,15 @@ class PortPoolManager {
334
334
 
335
335
  ws.on('close', () => {
336
336
  session.clients.delete(ws);
337
- console.log(`[PORT ${session.port}] Client disconnected (remaining: ${session.clients.size})`);
337
+ // 内存泄漏修复:清理该 client pendingRequests 和 sessionToClient 引用,
338
+ // 避免 plugin 永不响应时 entry 一直挂着 ws 对象
339
+ if (ws._origIds) ws._origIds.clear();
340
+ for (const [reqId, pending] of session.pendingRequests.entries()) {
341
+ if (pending.clientWs === ws) session.pendingRequests.delete(reqId);
342
+ }
343
+ for (const [sid, cws] of session.sessionToClient.entries()) {
344
+ if (cws === ws) session.sessionToClient.delete(sid);
345
+ }
338
346
  // 对齐原生 Chrome:断开不清理 tab
339
347
  });
340
348
 
@@ -481,6 +489,34 @@ class PortPoolManager {
481
489
  }
482
490
  }
483
491
 
492
+ // 内存泄漏修复:target 销毁/detach 时清理 sessionToPort、targetToPort、sessionToClient
493
+ // 之前只在 closeTarget 响应路径清理,事件路径漏了 → sessionToPort 无界增长
494
+ if (msg.method === 'Target.targetDestroyed' && targetId) {
495
+ const portIdx = this.targetToPort.get(targetId);
496
+ this.targetToPort.delete(targetId);
497
+ if (portIdx !== undefined) {
498
+ const sess = this.portSessions[portIdx];
499
+ if (sess) {
500
+ sess.targetIds.delete(targetId);
501
+ sess.targetUrls.delete(targetId);
502
+ }
503
+ }
504
+ }
505
+ if (msg.method === 'Target.detachedFromTarget') {
506
+ const sid = msg.params.sessionId;
507
+ if (sid) {
508
+ this.sessionToPort.delete(sid);
509
+ const portIdx = this.sessionToPort.get(sid);
510
+ if (portIdx !== undefined && this.portSessions[portIdx]) {
511
+ this.portSessions[portIdx].sessionToClient.delete(sid);
512
+ }
513
+ }
514
+ if (targetId) {
515
+ // detach 也意味着 target 不再活跃,清理 targetToPort(保留 targetIds 历史用于 list)
516
+ this.targetToPort.delete(targetId);
517
+ }
518
+ }
519
+
484
520
  if (targetId) {
485
521
  const portIndex = this.targetToPort.get(targetId);
486
522
  if (portIndex !== undefined) {
@@ -158,7 +158,19 @@ function updateExtensionState(connected) {
158
158
  clearLog();
159
159
 
160
160
  const wss = new WebSocket.Server({ noServer: true });
161
- const server = http.createServer((req, res) => handleHttpRequest(req, res));
161
+ const server = http.createServer(async (req, res) => {
162
+ try {
163
+ await handleHttpRequest(req, res);
164
+ } catch (err) {
165
+ logCDP('error', 'Unhandled error in handleHttpRequest', { url: req.url, message: err.message });
166
+ if (!res.headersSent) {
167
+ try {
168
+ res.writeHead(500, { 'Content-Type': 'application/json' });
169
+ res.end(JSON.stringify({ error: 'Internal proxy error' }));
170
+ } catch (_) {}
171
+ }
172
+ }
173
+ });
162
174
 
163
175
  const pluginConnections = new Set();
164
176
  const clientConnections = new Set();
@@ -236,8 +248,6 @@ async function requestVersionFromPlugin(pluginWs) {
236
248
 
237
249
  return new Promise((resolve) => {
238
250
  const requestId = `version_${Date.now()}`;
239
- const timeout = setTimeout(() => resolve(null), 2000);
240
-
241
251
  const handler = (data) => {
242
252
  try {
243
253
  const msg = JSON.parse(data.toString());
@@ -251,9 +261,13 @@ async function requestVersionFromPlugin(pluginWs) {
251
261
  }
252
262
  } catch (e) {}
253
263
  };
264
+ const timeout = setTimeout(() => {
265
+ pluginWs.off('message', handler);
266
+ resolve(null);
267
+ }, 2000);
254
268
 
255
269
  pluginWs.on('message', handler);
256
- pluginWs.send(JSON.stringify({ id: requestId, method: 'Browser.getVersion' }));
270
+ safeSend(pluginWs, JSON.stringify({ id: requestId, method: 'Browser.getVersion' }), 'plugin');
257
271
  });
258
272
  }
259
273
 
@@ -275,10 +289,6 @@ async function requestTargetsFromPlugin(pluginWs) {
275
289
 
276
290
  return new Promise((resolve) => {
277
291
  const requestId = `targets_${Date.now()}`;
278
- const timeout = setTimeout(() => {
279
- resolve(ns.cachedTargets);
280
- }, CONFIG.TARGETS_REQUEST_TIMEOUT);
281
-
282
292
  const handler = (data) => {
283
293
  try {
284
294
  const msg = JSON.parse(data.toString());
@@ -291,9 +301,13 @@ async function requestTargetsFromPlugin(pluginWs) {
291
301
  }
292
302
  } catch (e) {}
293
303
  };
304
+ const timeout = setTimeout(() => {
305
+ pluginWs.off('message', handler);
306
+ resolve(ns.cachedTargets);
307
+ }, CONFIG.TARGETS_REQUEST_TIMEOUT);
294
308
 
295
309
  pluginWs.on('message', handler);
296
- pluginWs.send(JSON.stringify({ id: requestId, method: 'Target.getTargets' }));
310
+ safeSend(pluginWs, JSON.stringify({ id: requestId, method: 'Target.getTargets' }), 'plugin');
297
311
  });
298
312
  }
299
313
 
@@ -439,9 +453,7 @@ async function execCdpViaPlugin(pluginId, params) {
439
453
  }
440
454
  }
441
455
  // 发 client-disconnected 让扩展关分组 + 关 tab
442
- try {
443
- pluginWs.send(JSON.stringify({ type: 'client-disconnected', clientId }));
444
- } catch (e) {}
456
+ safeSend(pluginWs, JSON.stringify({ type: 'client-disconnected', clientId }), 'plugin');
445
457
  // 清理 key session(让下次连接重新分配)
446
458
  if (pluginWs.apiKey && portPool) {
447
459
  portPool.keySessions.delete(pluginWs.apiKey);
@@ -952,12 +964,10 @@ function cleanupPlugin(ws, id, reason) {
952
964
  }
953
965
  clientWs.pairedPlugin = null;
954
966
  affectedClients.push(clientWs.id);
955
- if (clientWs.readyState === WebSocket.OPEN) {
956
- clientWs.send(JSON.stringify({
957
- type: 'plugin-disconnected',
958
- message: 'Plugin connection lost'
959
- }));
960
- }
967
+ safeSend(clientWs, JSON.stringify({
968
+ type: 'plugin-disconnected',
969
+ message: 'Plugin connection lost'
970
+ }), 'client');
961
971
  }
962
972
  });
963
973
 
@@ -1196,7 +1206,7 @@ function handlePluginConnection(ws, clientInfo, request) {
1196
1206
  if (eventClientId) {
1197
1207
  const clientWs = clientById.get(eventClientId);
1198
1208
  if (clientWs && clientWs.readyState === WebSocket.OPEN) {
1199
- clientWs.send(cdpData);
1209
+ safeSend(clientWs, cdpData, 'client');
1200
1210
  console.log(`[TARGET EVENT ROUTED] ${parsed.method} targetId=${targetId?.substring(0,8)} -> clientId=${eventClientId}`);
1201
1211
  }
1202
1212
  } else if (targetId && (parsed.method === 'Target.targetCreated' || parsed.method === 'Target.attachedToTarget')) {
@@ -1329,7 +1339,7 @@ function handlePluginConnection(ws, clientInfo, request) {
1329
1339
 
1330
1340
  const cachedCreated = ns.pendingTargetCreatedEvents.get(targetId);
1331
1341
  if (cachedCreated) {
1332
- clientWs.send(cachedCreated.cdpData);
1342
+ safeSend(clientWs, cachedCreated.cdpData, 'client');
1333
1343
  console.log(`[TARGET CREATED EVENT] Sent cached Target.targetCreated to client: ${mapping.clientId}`);
1334
1344
  ns.pendingTargetCreatedEvents.delete(targetId);
1335
1345
  }
@@ -1348,7 +1358,7 @@ function handlePluginConnection(ws, clientInfo, request) {
1348
1358
  };
1349
1359
  const msgStr = JSON.stringify(cdpMsg);
1350
1360
  console.log(`[ATTACHED EVENT] Full message: ${msgStr}`);
1351
- clientWs.send(msgStr);
1361
+ safeSend(clientWs, msgStr, 'client');
1352
1362
  console.log(`[ATTACHED EVENT] Sent cached event to client: ${mapping.clientId}`);
1353
1363
  }
1354
1364
  const newTargetInfo = cachedCreated?.parsed?.params?.targetInfo
@@ -1391,7 +1401,7 @@ function handlePluginConnection(ws, clientInfo, request) {
1391
1401
  }
1392
1402
  const responseStr = JSON.stringify(parsed);
1393
1403
  console.log(`[SEND TO CLIENT] ${responseStr.substring(0, 300)}`);
1394
- clientWs.send(responseStr);
1404
+ safeSend(clientWs, responseStr, 'client');
1395
1405
  console.log(`[ROUTE] Response global=${globalId} -> original=${originalId} -> client=${mapping.clientId} sessionId=${parsed.sessionId?.substring(0,8) || 'none'}`);
1396
1406
  }
1397
1407
  }
@@ -1410,7 +1420,7 @@ function handlePluginConnection(ws, clientInfo, request) {
1410
1420
  if (targetClientId) {
1411
1421
  const clientWs = clientById.get(targetClientId);
1412
1422
  if (clientWs && clientWs.readyState === WebSocket.OPEN) {
1413
- clientWs.send(data);
1423
+ safeSend(clientWs, data, 'client');
1414
1424
  logCDP('DEBUG', `FORWARDED to client: ${targetClientId} (sessionId route)`, parsed?.sessionId);
1415
1425
  }
1416
1426
  } else {
@@ -1562,10 +1572,10 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
1562
1572
 
1563
1573
  logConnectionEvent('CLIENT_PAIRED', { clientId: id, pluginId: pluginWs.id });
1564
1574
 
1565
- pluginWs.send(JSON.stringify({
1575
+ safeSend(pluginWs, JSON.stringify({
1566
1576
  type: 'client-connected',
1567
1577
  clientId: id
1568
- }));
1578
+ }), 'plugin');
1569
1579
 
1570
1580
  broadcastClientList();
1571
1581
  }
@@ -2086,7 +2096,7 @@ function safeSend(ws, data, label = '') {
2086
2096
  }
2087
2097
  }
2088
2098
 
2089
- setInterval(() => {
2099
+ const flushInterval = setInterval(() => {
2090
2100
  messageQueues.forEach((queue, wsId) => {
2091
2101
  let ws = null;
2092
2102
  for (const conn of pluginConnections) {
@@ -2186,7 +2196,7 @@ const heartbeatInterval = setInterval(() => {
2186
2196
  });
2187
2197
  }, CONFIG.HEARTBEAT_INTERVAL);
2188
2198
 
2189
- setInterval(() => {
2199
+ const zombieCleanupInterval = setInterval(() => {
2190
2200
  const toRemove = [];
2191
2201
  pluginConnections.forEach(ws => {
2192
2202
  if (ws.readyState !== WebSocket.OPEN) {
@@ -2219,10 +2229,10 @@ wss.on('close', () => {
2219
2229
  /**
2220
2230
  * 定期打印状态
2221
2231
  */
2222
- setInterval(() => {
2232
+ const statusPrintInterval = setInterval(() => {
2223
2233
  const now = new Date().toISOString();
2224
2234
  const nowMs = Date.now();
2225
-
2235
+
2226
2236
  const validPlugins = Array.from(pluginConnections).filter(ws => ws.readyState === WebSocket.OPEN);
2227
2237
  const zombiePlugins = Array.from(pluginConnections).filter(ws => ws.readyState !== WebSocket.OPEN);
2228
2238
  const validClients = Array.from(clientConnections).filter(ws => ws.readyState === WebSocket.OPEN);
@@ -2284,7 +2294,10 @@ setInterval(() => {
2284
2294
  process.on('SIGINT', () => {
2285
2295
  console.log('\n[SERVER] Shutting down (SIGINT)...');
2286
2296
  logCDP('SERVER', 'Shutting down (SIGINT)');
2297
+ clearInterval(flushInterval);
2287
2298
  clearInterval(heartbeatInterval);
2299
+ clearInterval(zombieCleanupInterval);
2300
+ clearInterval(statusPrintInterval);
2288
2301
 
2289
2302
  pluginConnections.forEach(ws => ws.close(1001, 'Server shutting down'));
2290
2303
  clientConnections.forEach(ws => ws.close(1001, 'Server shutting down'));
@@ -2299,6 +2312,10 @@ process.on('SIGINT', () => {
2299
2312
  process.on('SIGTERM', () => {
2300
2313
  console.log('[SERVER] Shutting down (SIGTERM)...');
2301
2314
  logCDP('SERVER', 'Shutting down (SIGTERM)');
2315
+ clearInterval(flushInterval);
2316
+ clearInterval(heartbeatInterval);
2317
+ clearInterval(zombieCleanupInterval);
2318
+ clearInterval(statusPrintInterval);
2302
2319
  flushAllLogs();
2303
2320
  process.exit(0);
2304
2321
  });