cdp-tunnel 2.7.4 → 2.7.6

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.
@@ -65,8 +65,7 @@ var SpecialHandler = (function() {
65
65
 
66
66
  if (State.isCDPCreatedTab(tabId)) {
67
67
  addTabToAutomationGroup(tabId, clientId);
68
- } else {
69
- State.addPreExistingTab(tabId);
68
+ } else { State.addPreExistingTab(tabId);
70
69
  Logger.info('[CDP] Target.attachToTarget: user tab not CDP-created, treating as pre-existing. tabId:', tabId);
71
70
  }
72
71
 
@@ -141,12 +140,9 @@ var SpecialHandler = (function() {
141
140
 
142
141
  function groupTabSilently(tabId, clientId) {
143
142
  return new Promise(function(resolve) {
144
- try {
145
- addTabToAutomationGroup(tabId, clientId);
146
- } catch (e) {
147
- Logger.error('[TabGroup] addTabToAutomationGroup threw:', e.message || e);
148
- }
149
- setTimeout(resolve, 100);
143
+ addTabToAutomationGroup(tabId, clientId, function(success) {
144
+ setTimeout(resolve, 50);
145
+ });
150
146
  });
151
147
  }
152
148
 
@@ -162,7 +158,7 @@ var SpecialHandler = (function() {
162
158
  });
163
159
  }
164
160
 
165
- function addTabToAutomationGroup(tabId, clientId) {
161
+ function addTabToAutomationGroup(tabId, clientId, callback) {
166
162
  Logger.info('[TabGroup] Starting addTabToAutomationGroup for tabId:', tabId, 'clientId:', clientId);
167
163
 
168
164
  WebSocketManager.send({ type: 'tabgroup-debug', tabId: tabId, clientId: clientId, phase: 'start' });
@@ -183,21 +179,23 @@ var SpecialHandler = (function() {
183
179
  Logger.warn('[TabGroup] No clientId for tab:', tabId, 'fallback to first client:', groupClientId);
184
180
  } else {
185
181
  Logger.warn('[TabGroup] No clientId for tab:', tabId, '— skipping group operation');
182
+ if (callback) callback(false);
186
183
  return;
187
184
  }
188
185
  }
189
186
  var baseName = CDPUtils.getGroupBaseName(groupClientId);
190
187
 
191
188
  Logger.info('[TabGroup] Grouping tab immediately for:', baseName);
192
- doGroup(tabId, groupClientId, baseName);
189
+ doGroup(tabId, groupClientId, baseName, 0, callback);
193
190
  }
194
191
 
195
- function doGroup(tabId, clientId, baseName, retries) {
192
+ function doGroup(tabId, clientId, baseName, retries, callback) {
196
193
  retries = retries || 0;
197
194
  Logger.info('[TabGroup] doGroup: tabId=' + tabId + ' clientId=' + (clientId || 'none') + ' baseName=' + baseName + ' retry=' + retries);
198
195
  if (!chrome.tabGroups) {
199
196
  Logger.warn('[TabGroup] chrome.tabGroups API not available (headless mode?), skipping grouping for tab:', tabId);
200
197
  EventBuilder.send('CDPTunnel.debug', { source: 'doGroup', phase: 'skip', reason: 'tabGroups-unavailable', tabId: tabId });
198
+ if (callback) callback(false);
201
199
  return;
202
200
  }
203
201
  chrome.tabGroups.query({}, function(allGroups) {
@@ -214,12 +212,15 @@ var SpecialHandler = (function() {
214
212
  Logger.error('[TabGroup] Failed to add tab to group:', chrome.runtime.lastError.message, 'retries:', retries);
215
213
  EventBuilder.send('CDPTunnel.debug', { source: 'doGroup', phase: 'addToExisting', error: chrome.runtime.lastError.message, tabId: tabId, groupId: existing.id });
216
214
  if (retries < 3) {
217
- setTimeout(function() { doGroup(tabId, clientId, baseName, retries + 1); }, 500);
215
+ setTimeout(function() { doGroup(tabId, clientId, baseName, retries + 1, callback); }, 500);
216
+ } else {
217
+ if (callback) callback(false);
218
218
  }
219
219
  } else {
220
220
  State.setGroupIdForClient(clientId, existing.id);
221
221
  updateTabGroupName(clientId);
222
222
  Logger.info('[TabGroup] Tab', tabId, 'added to existing group:', existing.id);
223
+ if (callback) callback(true);
223
224
  }
224
225
  });
225
226
  } else {
@@ -229,7 +230,9 @@ var SpecialHandler = (function() {
229
230
  Logger.error('[TabGroup] Failed to create group:', chrome.runtime.lastError.message, 'retries:', retries);
230
231
  EventBuilder.send('CDPTunnel.debug', { source: 'doGroup', phase: 'createGroup', error: chrome.runtime.lastError.message, tabId: tabId });
231
232
  if (retries < 3) {
232
- setTimeout(function() { doGroup(tabId, clientId, baseName, retries + 1); }, 500);
233
+ setTimeout(function() { doGroup(tabId, clientId, baseName, retries + 1, callback); }, 500);
234
+ } else {
235
+ if (callback) callback(false);
233
236
  }
234
237
  return;
235
238
  }
@@ -250,13 +253,16 @@ var SpecialHandler = (function() {
250
253
  updateTabGroupName(clientId);
251
254
  Logger.info('[TabGroup] Group updated:', groupId, baseName);
252
255
  }
256
+ if (callback) callback(true);
253
257
  });
254
258
  } else {
255
259
  State.setGroupIdForClient(clientId, groupId);
256
260
  Logger.info('[TabGroup] Group created but tabGroups.update unavailable (headless):', groupId);
261
+ if (callback) callback(true);
257
262
  }
258
263
  } else {
259
264
  Logger.error('[TabGroup] chrome.tabs.group returned null groupId');
265
+ if (callback) callback(false);
260
266
  }
261
267
  });
262
268
  }
@@ -281,22 +281,50 @@ var WebSocketManager = (function() {
281
281
  Logger.info('[WS] Closing tab group for client:', clientId);
282
282
 
283
283
  return new Promise(function(resolve) {
284
- var groupId = State.getGroupIdForClient(clientId);
284
+ var timeoutId = setTimeout(function() {
285
+ Logger.warn('[WS] closeTabGroupByClientId timeout for client:', clientId, '— forcing cleanup');
286
+ cleanupStaleState(clientId);
287
+ resolve();
288
+ }, 5000);
285
289
 
286
- if (groupId) {
287
- closeGroupById(groupId, clientId, resolve);
288
- } else {
289
- var baseName = CDPUtils.getGroupBaseName(clientId);
290
- chrome.tabGroups.query({}, function(allGroups) {
291
- var match = CDPUtils.findGroupByName(allGroups, baseName);
292
- if (match) {
293
- closeGroupById(match.id, clientId, resolve);
294
- } else {
295
- Logger.info('[WS] No tab group found, closing tabs by clientId:', clientId);
296
- closeTabsByClientId(clientId, resolve);
297
- }
290
+ var groupId = State.getGroupIdForClient(clientId);
291
+
292
+ if (groupId) {
293
+ closeGroupById(groupId, clientId, function() {
294
+ clearTimeout(timeoutId);
295
+ cleanupStaleState(clientId);
296
+ resolve();
297
+ });
298
+ } else {
299
+ var baseName = CDPUtils.getGroupBaseName(clientId);
300
+ chrome.tabGroups.query({}, function(allGroups) {
301
+ var match = CDPUtils.findGroupByName(allGroups, baseName);
302
+ if (match) {
303
+ closeGroupById(match.id, clientId, function() {
304
+ clearTimeout(timeoutId);
305
+ cleanupStaleState(clientId);
306
+ resolve();
298
307
  });
299
- }
308
+ } else {
309
+ Logger.info('[WS] No tab group found, closing tabs by clientId:', clientId);
310
+ closeTabsByClientId(clientId, function() {
311
+ clearTimeout(timeoutId);
312
+ cleanupStaleState(clientId);
313
+ resolve();
314
+ });
315
+ }
316
+ });
317
+ }
318
+ });
319
+ }
320
+
321
+ function cleanupStaleState(clientId) {
322
+ if (!clientId) return;
323
+ var attachedTabs = State.getAttachedTabIds();
324
+ attachedTabs.forEach(function(tabId) {
325
+ if (State.getClientIdByTabId(tabId) === clientId) {
326
+ State.removeTabIdToClientId(tabId);
327
+ }
300
328
  });
301
329
  }
302
330
 
@@ -306,6 +334,7 @@ var WebSocketManager = (function() {
306
334
  if (!tabs || tabs.length === 0) {
307
335
  Logger.info('[WS] No tabs in group:', groupId);
308
336
  State.removeGroupForClient(clientId);
337
+ removeEmptyGroup(groupId);
309
338
  resolve();
310
339
  return;
311
340
  }
@@ -338,11 +367,33 @@ var WebSocketManager = (function() {
338
367
  });
339
368
 
340
369
  State.removeGroupForClient(clientId);
370
+ removeEmptyGroup(groupId);
341
371
  resolve();
342
372
  });
343
373
  });
344
374
  }
345
375
 
376
+ function removeEmptyGroup(groupId) {
377
+ if (!groupId || !chrome.tabGroups) return;
378
+ setTimeout(function() {
379
+ chrome.tabGroups.query({ groupId: groupId }, function(groups) {
380
+ if (chrome.runtime.lastError) return;
381
+ if (groups && groups.length > 0) {
382
+ chrome.tabs.query({ groupId: groupId }, function(tabs) {
383
+ if (chrome.runtime.lastError) return;
384
+ if (!tabs || tabs.length === 0) {
385
+ chrome.tabGroups.remove(groupId, function() {
386
+ if (!chrome.runtime.lastError) {
387
+ Logger.info('[WS] Removed empty group:', groupId);
388
+ }
389
+ });
390
+ }
391
+ });
392
+ }
393
+ });
394
+ }, 500);
395
+ }
396
+
346
397
  function closeTabsByClientId(clientId, resolve) {
347
398
  var attachedTabs = State.getAttachedTabIds();
348
399
  var cdpCreatedTabs = State.getCDPCreatedTabIds();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "CDP Bridge",
4
- "version": "2.7.4",
4
+ "version": "2.7.6",
5
5
  "description": "Chrome DevTools Protocol Bridge for Playwright/Puppeteer automation",
6
6
  "permissions": [
7
7
  "debugger",
@@ -1,5 +1,5 @@
1
1
  var Config = {
2
- WS_URL: 'ws://localhost:9221/plugin',
2
+ WS_URL: 'ws://localhost:58288/plugin',
3
3
  RECONNECT_DELAY: 3000,
4
4
  DEBUGGER_VERSION: '1.3',
5
5
  HEARTBEAT_INTERVAL: 25000,
@@ -44,7 +44,13 @@ var CDPUtils = (function() {
44
44
 
45
45
  function buildGroupName(clientId) {
46
46
  if (!clientId) return 'CDP';
47
- var suffix = clientId.length > 8 ? clientId.substring(clientId.length - 8) : clientId;
47
+ var hash = 0;
48
+ for (var i = 0; i < clientId.length; i++) {
49
+ var chr = clientId.charCodeAt(i);
50
+ hash = ((hash << 5) - hash) + chr;
51
+ hash = hash | 0;
52
+ }
53
+ var suffix = Math.abs(hash).toString(16).substring(0, 8).padStart(8, '0');
48
54
  return 'CDP-' + suffix;
49
55
  }
50
56
 
@@ -55,7 +61,7 @@ var CDPUtils = (function() {
55
61
  function findGroupByName(allGroups, baseName) {
56
62
  for (var i = 0; i < allGroups.length; i++) {
57
63
  var g = allGroups[i];
58
- if (g.title && g.title.indexOf(baseName) === 0) return g;
64
+ if (g.title === baseName || (g.title && g.title.indexOf(baseName + ' (') === 0)) return g;
59
65
  }
60
66
  return null;
61
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdp-tunnel",
3
- "version": "2.7.4",
3
+ "version": "2.7.6",
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
6
  "bin": "./cli/index.js",
@@ -16,6 +16,10 @@
16
16
  "benchmark:proxy": "node benchmark/run-all.js --proxy",
17
17
  "demo": "node demo/server.js",
18
18
  "test:compare": "node tests/cdp-compare/cdp-compare-runner.js",
19
+ "test": "node tests/e2e/run-all.js",
20
+ "test:e2e": "node tests/e2e/run-all.js",
21
+ "test:core": "SKIP_EXTENDED=1 node tests/e2e/run-all.js",
22
+ "test:smoke": "ONLY=test-default-page,test-strict-isolation,test-no-user-tab-grab node tests/e2e/run-all.js",
19
23
  "prepare": "husky"
20
24
  },
21
25
  "keywords": [
@@ -487,11 +487,33 @@ function cleanupClient(ws, id, reason) {
487
487
  }
488
488
 
489
489
  if (ws.pairedPlugin) {
490
- safeSend(ws.pairedPlugin, JSON.stringify({
490
+ const sendOk = safeSend(ws.pairedPlugin, JSON.stringify({
491
491
  type: 'client-disconnected',
492
492
  clientId: id,
493
493
  sessions: []
494
494
  }), 'plugin');
495
+ if (!sendOk) {
496
+ console.log(`[WARN] cleanupClient: failed to send client-disconnected for ${id} to plugin`);
497
+ }
498
+
499
+ const pluginNs = getNamespace(ws.pairedPlugin);
500
+ if (pluginNs) {
501
+ const targetsToClose = [];
502
+ for (const [tId, cId] of pluginNs.targetIdToClientId.entries()) {
503
+ if (cId === id) {
504
+ targetsToClose.push(tId);
505
+ }
506
+ }
507
+ targetsToClose.forEach(function(tId) {
508
+ const closeReq = JSON.stringify({
509
+ id: -1,
510
+ method: 'Target.closeTarget',
511
+ params: { targetId: tId },
512
+ __clientId: id
513
+ });
514
+ safeSend(ws.pairedPlugin, closeReq, 'plugin');
515
+ });
516
+ }
495
517
  }
496
518
 
497
519
  broadcastClientList();
@@ -797,25 +819,7 @@ function handlePluginConnection(ws, clientInfo, request) {
797
819
  pendingMap.set(targetId, { parsed: JSON.parse(JSON.stringify(parsed)), cdpData });
798
820
  console.log(`[TARGET EVENT PENDING] ${parsed.method} targetId=${targetId?.substring(0,8) || 'none'} (cached, waiting for createTarget response)`);
799
821
  } else {
800
- // Broadcast to discovering clients
801
- let routedToDiscoverer = false;
802
- if (ns.discoveringClientIds.size > 0) {
803
- for (const [discClientId, timestamp] of ns.discoveringClientIds) {
804
- if (Date.now() - timestamp < 30000) {
805
- const discWs = clientById.get(discClientId);
806
- if (discWs && discWs.readyState === WebSocket.OPEN) {
807
- discWs.send(cdpData);
808
- routedToDiscoverer = true;
809
- }
810
- } else {
811
- ns.discoveringClientIds.delete(discClientId);
812
- }
813
- }
814
- }
815
- if (!routedToDiscoverer) {
816
- pendingMap.set(targetId, { parsed: JSON.parse(JSON.stringify(parsed)), cdpData });
817
- console.log(`[TARGET EVENT PENDING] ${parsed.method} targetId=${targetId?.substring(0,8) || 'none'} (cached, no discoverer)`);
818
- }
822
+ console.log(`[TARGET EVENT DROPPED] ${parsed.method} targetId=${targetId?.substring(0,8) || 'none'} (no owner, dropped for isolation)`);
819
823
  }
820
824
  } else {
821
825
  console.log(`[TARGET EVENT DROPPED] ${parsed.method} targetId=${targetId?.substring(0,8) || 'none'} (no owner, dropped for isolation)`);
@@ -948,15 +952,33 @@ function handlePluginConnection(ws, clientInfo, request) {
948
952
  invalidateTargetsCache(ws);
949
953
  }
950
954
 
951
- const originalId = mapping.originalId;
952
- parsed.id = originalId;
953
- if (mapping.sessionId && !parsed.sessionId) {
954
- parsed.sessionId = mapping.sessionId;
955
+ if (mapping.isAutoDefaultPage) {
956
+ console.log(`[AUTO DEFAULT PAGE] createTarget response received for client=${mapping.clientId}, targetId=${parsed.result?.targetId?.substring(0,8) || 'none'} — skipping response send to client`);
957
+
958
+ if (mapping.pendingSetAutoAttach) {
959
+ const pending = mapping.pendingSetAutoAttach;
960
+ const pendingParsed = pending.parsed;
961
+ const pendingClientId = pending.clientId;
962
+
963
+ console.log(`[AUTO DEFAULT PAGE] Now forwarding pending setAutoAttach for client=${pendingClientId}`);
964
+
965
+ if (ws.readyState === WebSocket.OPEN) {
966
+ const forwardMsg = { ...pendingParsed, __clientId: pendingClientId };
967
+ ws.send(JSON.stringify(forwardMsg));
968
+ console.log(`[SEND TO PLUGIN] Forwarding setAutoAttach for client=${pendingClientId}`);
969
+ }
970
+ }
971
+ } else {
972
+ const originalId = mapping.originalId;
973
+ parsed.id = originalId;
974
+ if (mapping.sessionId && !parsed.sessionId) {
975
+ parsed.sessionId = mapping.sessionId;
976
+ }
977
+ const responseStr = JSON.stringify(parsed);
978
+ console.log(`[SEND TO CLIENT] ${responseStr.substring(0, 300)}`);
979
+ clientWs.send(responseStr);
980
+ console.log(`[ROUTE] Response global=${globalId} -> original=${originalId} -> client=${mapping.clientId} sessionId=${parsed.sessionId?.substring(0,8) || 'none'}`);
955
981
  }
956
- const responseStr = JSON.stringify(parsed);
957
- console.log(`[SEND TO CLIENT] ${responseStr.substring(0, 300)}`);
958
- clientWs.send(responseStr);
959
- console.log(`[ROUTE] Response global=${globalId} -> original=${originalId} -> client=${mapping.clientId} sessionId=${parsed.sessionId?.substring(0,8) || 'none'}`);
960
982
  }
961
983
  globalRequestIdMap.delete(globalId);
962
984
  } else {
@@ -1052,6 +1074,52 @@ function handlePluginConnection(ws, clientInfo, request) {
1052
1074
  }));
1053
1075
  }
1054
1076
 
1077
+ function autoCreateDefaultPageAndForward(clientWs, setAutoAttachParsed, originalData, clientId, originalRequestId) {
1078
+ const pluginWs = clientWs.pairedPlugin;
1079
+ if (!pluginWs || pluginWs.readyState !== WebSocket.OPEN) {
1080
+ forwardToPlugin(clientWs, originalData, clientId);
1081
+ return;
1082
+ }
1083
+
1084
+ globalRequestIdCounter++;
1085
+ const createGlobalId = globalRequestIdCounter;
1086
+
1087
+ globalRequestIdMap.set(createGlobalId, {
1088
+ clientId: clientId,
1089
+ originalId: -1,
1090
+ sessionId: null,
1091
+ method: 'Target.createTarget',
1092
+ isCreateTarget: true,
1093
+ isAutoDefaultPage: true,
1094
+ pendingSetAutoAttach: {
1095
+ parsed: setAutoAttachParsed,
1096
+ data: originalData,
1097
+ clientId: clientId,
1098
+ originalRequestId: originalRequestId
1099
+ }
1100
+ });
1101
+
1102
+ const request = {
1103
+ id: createGlobalId,
1104
+ method: 'Target.createTarget',
1105
+ params: { url: 'about:blank' },
1106
+ __clientId: clientId
1107
+ };
1108
+
1109
+ console.log(`[AUTO DEFAULT PAGE] Sending Target.createTarget for client=${clientId} globalId=${createGlobalId}, will forward setAutoAttach after`);
1110
+ pluginWs.send(JSON.stringify(request));
1111
+ }
1112
+
1113
+ function forwardToPlugin(clientWs, data, clientId) {
1114
+ const pluginWs = clientWs.pairedPlugin;
1115
+ if (pluginWs && pluginWs.readyState === WebSocket.OPEN) {
1116
+ console.log(`[SEND TO PLUGIN] method=Target.setAutoAttach clientId=${clientId}`);
1117
+ pluginWs.send(data);
1118
+ } else {
1119
+ broadcastToPlugins(data, clientWs);
1120
+ }
1121
+ }
1122
+
1055
1123
  /**
1056
1124
  * 处理 CDP 客户端连接 (Playwright/Puppeteer)
1057
1125
  */
@@ -1254,6 +1322,12 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
1254
1322
  }
1255
1323
  }
1256
1324
 
1325
+ if (parsed && parsed.method === 'Target.setAutoAttach' && parsed.params?.autoAttach && !ws._autoDefaultPageSent) {
1326
+ ws._autoDefaultPageSent = true;
1327
+ autoCreateDefaultPageAndForward(ws, parsed, modifiedData, id, originalId);
1328
+ return;
1329
+ }
1330
+
1257
1331
  if (parsed && parsed.method === 'Browser.close') {
1258
1332
  if (shouldLog('info')) {
1259
1333
  console.log(`\n[BROWSER CLOSE] Client ${id} requested Browser.close, forwarding to plugin`);