cdp-tunnel 3.6.1 → 3.6.3
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.
package/package.json
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdp-tunnel",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.3",
|
|
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":
|
|
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
|
-
"
|
|
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
|
}
|
|
@@ -261,22 +261,12 @@ class PortPoolManager {
|
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
// SW 冷启动 warmup:第一个 client 连接时创建一个临时 tab 再关闭
|
|
265
|
-
if (!session.warmedUp) {
|
|
266
|
-
session.warmedUp = true;
|
|
267
|
-
const warmupId = `pool${session.portIndex}_internal_warmup`;
|
|
268
|
-
pluginWs.send(JSON.stringify({
|
|
269
|
-
id: warmupId,
|
|
270
|
-
method: 'Target.createTarget',
|
|
271
|
-
params: { url: 'about:blank' },
|
|
272
|
-
__portIndex: session.portIndex,
|
|
273
|
-
__clientId: `pool_${session.port}`
|
|
274
|
-
}));
|
|
275
|
-
// warmup 响应回来后在 handlePluginMessage 里关闭这个 tab
|
|
276
|
-
session.pendingWarmupClose = true;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
264
|
// 通知扩展有 client 连接(带端口号作为分组标识)
|
|
265
|
+
// 对齐原生 CDP:不创建任何隐式的 about:blank 页面。
|
|
266
|
+
// 原生 chrome --remote-debugging-port 在 client 连接后不创建任何 target,
|
|
267
|
+
// 只在 connectOverCDP 的 setAutoAttach 期间发现已有 target。
|
|
268
|
+
// (移除旧版 warmup 逻辑:它用 Target.createTarget 创建临时 about:blank 再关闭,
|
|
269
|
+
// 但时间窗口内 Playwright 的 setAutoAttach 会发现它,导致用户看到多余空白页。)
|
|
280
270
|
// clientId 固定为 pool_{port},让扩展为每个端口建一个独立分组
|
|
281
271
|
// __groupName 带 key 名称,让扩展用 key 名称命名 Chrome 分组(一眼看出是谁的浏览器)
|
|
282
272
|
const poolClientId = `pool_${session.port}`;
|
|
@@ -334,7 +324,15 @@ class PortPoolManager {
|
|
|
334
324
|
|
|
335
325
|
ws.on('close', () => {
|
|
336
326
|
session.clients.delete(ws);
|
|
337
|
-
|
|
327
|
+
// 内存泄漏修复:清理该 client 的 pendingRequests 和 sessionToClient 引用,
|
|
328
|
+
// 避免 plugin 永不响应时 entry 一直挂着 ws 对象
|
|
329
|
+
if (ws._origIds) ws._origIds.clear();
|
|
330
|
+
for (const [reqId, pending] of session.pendingRequests.entries()) {
|
|
331
|
+
if (pending.clientWs === ws) session.pendingRequests.delete(reqId);
|
|
332
|
+
}
|
|
333
|
+
for (const [sid, cws] of session.sessionToClient.entries()) {
|
|
334
|
+
if (cws === ws) session.sessionToClient.delete(sid);
|
|
335
|
+
}
|
|
338
336
|
// 对齐原生 Chrome:断开不清理 tab
|
|
339
337
|
});
|
|
340
338
|
|
|
@@ -352,23 +350,8 @@ class PortPoolManager {
|
|
|
352
350
|
|
|
353
351
|
// 1. 响应消息:id 以 pool 开头(排除事件——事件有 method 字段)
|
|
354
352
|
if (msg.id && typeof msg.id === 'string' && msg.id.startsWith('pool') && !msg.method) {
|
|
355
|
-
//
|
|
353
|
+
// 内部命令响应(_ensureVisible 的 bringToFront/waitVis 等):丢弃,不路由给 client
|
|
356
354
|
if (msg.id.includes('_internal')) {
|
|
357
|
-
// warmup createTarget 响应——关掉 warmup tab
|
|
358
|
-
const m = msg.id.match(/^pool(\d+)_/);
|
|
359
|
-
if (m && msg.id.includes('_warmup') && msg.result && msg.result.targetId) {
|
|
360
|
-
const pidx = parseInt(m[1]);
|
|
361
|
-
const sess = this.portSessions[pidx];
|
|
362
|
-
const warmupTid = msg.result.targetId;
|
|
363
|
-
const closeId = `pool${pidx}_internal_warmup_close_${Date.now()}`;
|
|
364
|
-
pluginWs.send(JSON.stringify({
|
|
365
|
-
id: closeId,
|
|
366
|
-
method: 'Target.closeTarget',
|
|
367
|
-
params: { targetId: warmupTid },
|
|
368
|
-
__portIndex: pidx,
|
|
369
|
-
__clientId: `pool_${sess.port}`
|
|
370
|
-
}));
|
|
371
|
-
}
|
|
372
355
|
return true;
|
|
373
356
|
}
|
|
374
357
|
const match = msg.id.match(/^pool(\d+)_(.+)$/);
|
|
@@ -481,6 +464,34 @@ class PortPoolManager {
|
|
|
481
464
|
}
|
|
482
465
|
}
|
|
483
466
|
|
|
467
|
+
// 内存泄漏修复:target 销毁/detach 时清理 sessionToPort、targetToPort、sessionToClient
|
|
468
|
+
// 之前只在 closeTarget 响应路径清理,事件路径漏了 → sessionToPort 无界增长
|
|
469
|
+
if (msg.method === 'Target.targetDestroyed' && targetId) {
|
|
470
|
+
const portIdx = this.targetToPort.get(targetId);
|
|
471
|
+
this.targetToPort.delete(targetId);
|
|
472
|
+
if (portIdx !== undefined) {
|
|
473
|
+
const sess = this.portSessions[portIdx];
|
|
474
|
+
if (sess) {
|
|
475
|
+
sess.targetIds.delete(targetId);
|
|
476
|
+
sess.targetUrls.delete(targetId);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (msg.method === 'Target.detachedFromTarget') {
|
|
481
|
+
const sid = msg.params.sessionId;
|
|
482
|
+
if (sid) {
|
|
483
|
+
this.sessionToPort.delete(sid);
|
|
484
|
+
const portIdx = this.sessionToPort.get(sid);
|
|
485
|
+
if (portIdx !== undefined && this.portSessions[portIdx]) {
|
|
486
|
+
this.portSessions[portIdx].sessionToClient.delete(sid);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (targetId) {
|
|
490
|
+
// detach 也意味着 target 不再活跃,清理 targetToPort(保留 targetIds 历史用于 list)
|
|
491
|
+
this.targetToPort.delete(targetId);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
484
495
|
if (targetId) {
|
|
485
496
|
const portIndex = this.targetToPort.get(targetId);
|
|
486
497
|
if (portIndex !== undefined) {
|
package/server/proxy-server.js
CHANGED
|
@@ -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) =>
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|