cdp-tunnel 2.7.5 → 2.7.7

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.
@@ -13,9 +13,7 @@ var LocalHandler = (function() {
13
13
  }
14
14
 
15
15
  function browserClose() {
16
- return State.cleanupAllTabs().then(function() {
17
- return {};
18
- });
16
+ return Promise.resolve({});
19
17
  }
20
18
 
21
19
  function getWindowForTarget() {
@@ -1,4 +1,6 @@
1
1
  var SpecialHandler = (function() {
2
+ var _groupQueue = new Map();
3
+
2
4
  function muteTabIfNeeded(tabId) {
3
5
  Config.getAutoMute(function(enabled) {
4
6
  if (!enabled) return;
@@ -65,8 +67,7 @@ var SpecialHandler = (function() {
65
67
 
66
68
  if (State.isCDPCreatedTab(tabId)) {
67
69
  addTabToAutomationGroup(tabId, clientId);
68
- } else {
69
- State.addPreExistingTab(tabId);
70
+ } else { State.addPreExistingTab(tabId);
70
71
  Logger.info('[CDP] Target.attachToTarget: user tab not CDP-created, treating as pre-existing. tabId:', tabId);
71
72
  }
72
73
 
@@ -141,12 +142,9 @@ var SpecialHandler = (function() {
141
142
 
142
143
  function groupTabSilently(tabId, clientId) {
143
144
  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);
145
+ addTabToAutomationGroup(tabId, clientId, function(success) {
146
+ setTimeout(resolve, 50);
147
+ });
150
148
  });
151
149
  }
152
150
 
@@ -162,7 +160,34 @@ var SpecialHandler = (function() {
162
160
  });
163
161
  }
164
162
 
165
- function addTabToAutomationGroup(tabId, clientId) {
163
+ function addTabToAutomationGroup(tabId, clientId, callback) {
164
+ var key = clientId || '__no_client__';
165
+ var prev = _groupQueue.get(key) || Promise.resolve();
166
+
167
+ var next = prev.then(function() {
168
+ return new Promise(function(resolve) {
169
+ _addTabToAutomationGroupInner(tabId, clientId, function(success) {
170
+ resolve(success);
171
+ });
172
+ });
173
+ }).catch(function(err) {
174
+ Logger.error('[addTabToAutomationGroup] queue error:', err.message || err);
175
+ });
176
+
177
+ _groupQueue.set(key, next);
178
+
179
+ next.finally(function() {
180
+ if (_groupQueue.get(key) === next) {
181
+ _groupQueue.delete(key);
182
+ }
183
+ });
184
+
185
+ if (callback) {
186
+ next.then(function(success) { callback(success); });
187
+ }
188
+ }
189
+
190
+ function _addTabToAutomationGroupInner(tabId, clientId, callback) {
166
191
  Logger.info('[TabGroup] Starting addTabToAutomationGroup for tabId:', tabId, 'clientId:', clientId);
167
192
 
168
193
  WebSocketManager.send({ type: 'tabgroup-debug', tabId: tabId, clientId: clientId, phase: 'start' });
@@ -183,21 +208,23 @@ var SpecialHandler = (function() {
183
208
  Logger.warn('[TabGroup] No clientId for tab:', tabId, 'fallback to first client:', groupClientId);
184
209
  } else {
185
210
  Logger.warn('[TabGroup] No clientId for tab:', tabId, '— skipping group operation');
211
+ if (callback) callback(false);
186
212
  return;
187
213
  }
188
214
  }
189
215
  var baseName = CDPUtils.getGroupBaseName(groupClientId);
190
216
 
191
217
  Logger.info('[TabGroup] Grouping tab immediately for:', baseName);
192
- doGroup(tabId, groupClientId, baseName);
218
+ doGroup(tabId, groupClientId, baseName, 0, callback);
193
219
  }
194
220
 
195
- function doGroup(tabId, clientId, baseName, retries) {
221
+ function doGroup(tabId, clientId, baseName, retries, callback) {
196
222
  retries = retries || 0;
197
223
  Logger.info('[TabGroup] doGroup: tabId=' + tabId + ' clientId=' + (clientId || 'none') + ' baseName=' + baseName + ' retry=' + retries);
198
224
  if (!chrome.tabGroups) {
199
225
  Logger.warn('[TabGroup] chrome.tabGroups API not available (headless mode?), skipping grouping for tab:', tabId);
200
226
  EventBuilder.send('CDPTunnel.debug', { source: 'doGroup', phase: 'skip', reason: 'tabGroups-unavailable', tabId: tabId });
227
+ if (callback) callback(false);
201
228
  return;
202
229
  }
203
230
  chrome.tabGroups.query({}, function(allGroups) {
@@ -214,12 +241,15 @@ var SpecialHandler = (function() {
214
241
  Logger.error('[TabGroup] Failed to add tab to group:', chrome.runtime.lastError.message, 'retries:', retries);
215
242
  EventBuilder.send('CDPTunnel.debug', { source: 'doGroup', phase: 'addToExisting', error: chrome.runtime.lastError.message, tabId: tabId, groupId: existing.id });
216
243
  if (retries < 3) {
217
- setTimeout(function() { doGroup(tabId, clientId, baseName, retries + 1); }, 500);
244
+ setTimeout(function() { doGroup(tabId, clientId, baseName, retries + 1, callback); }, 500);
245
+ } else {
246
+ if (callback) callback(false);
218
247
  }
219
248
  } else {
220
249
  State.setGroupIdForClient(clientId, existing.id);
221
250
  updateTabGroupName(clientId);
222
251
  Logger.info('[TabGroup] Tab', tabId, 'added to existing group:', existing.id);
252
+ if (callback) callback(true);
223
253
  }
224
254
  });
225
255
  } else {
@@ -229,7 +259,9 @@ var SpecialHandler = (function() {
229
259
  Logger.error('[TabGroup] Failed to create group:', chrome.runtime.lastError.message, 'retries:', retries);
230
260
  EventBuilder.send('CDPTunnel.debug', { source: 'doGroup', phase: 'createGroup', error: chrome.runtime.lastError.message, tabId: tabId });
231
261
  if (retries < 3) {
232
- setTimeout(function() { doGroup(tabId, clientId, baseName, retries + 1); }, 500);
262
+ setTimeout(function() { doGroup(tabId, clientId, baseName, retries + 1, callback); }, 500);
263
+ } else {
264
+ if (callback) callback(false);
233
265
  }
234
266
  return;
235
267
  }
@@ -250,13 +282,16 @@ var SpecialHandler = (function() {
250
282
  updateTabGroupName(clientId);
251
283
  Logger.info('[TabGroup] Group updated:', groupId, baseName);
252
284
  }
285
+ if (callback) callback(true);
253
286
  });
254
287
  } else {
255
288
  State.setGroupIdForClient(clientId, groupId);
256
289
  Logger.info('[TabGroup] Group created but tabGroups.update unavailable (headless):', groupId);
290
+ if (callback) callback(true);
257
291
  }
258
292
  } else {
259
293
  Logger.error('[TabGroup] chrome.tabs.group returned null groupId');
294
+ if (callback) callback(false);
260
295
  }
261
296
  });
262
297
  }
@@ -203,8 +203,8 @@ var DebuggerManager = (function() {
203
203
  return;
204
204
  }
205
205
 
206
- var sessionId = State.findSessionByTabId(source.tabId);
207
- Logger.info('[Event] method=' + method + ' tabId=' + source.tabId + ' sessionId=' + (sessionId || 'null'));
206
+ var sessionIds = State.findSessionsByTabId(source.tabId);
207
+ Logger.info('[Event] method=' + method + ' tabId=' + source.tabId + ' sessions=' + sessionIds.length);
208
208
 
209
209
  if (method === 'Runtime.executionContextCreated' && params && params.context) {
210
210
  var context = params.context;
@@ -285,7 +285,9 @@ var DebuggerManager = (function() {
285
285
  }
286
286
  }
287
287
 
288
- EventBuilder.send(method, params, sessionId);
288
+ for (var i = 0; i < sessionIds.length; i++) {
289
+ EventBuilder.send(method, params, sessionIds[i]);
290
+ }
289
291
  }
290
292
 
291
293
  function handleDetach(source, reason) {
@@ -54,15 +54,23 @@ var State = (function() {
54
54
  }
55
55
 
56
56
  function findSessionByTabId(tabId) {
57
- var entries = _state.sessionIdToTabId.entries();
58
- var entry = entries.next();
59
- while (!entry.done) {
60
- if (entry.value[1] === tabId) {
61
- return entry.value[0];
57
+ var lastMatch = null;
58
+ _state.sessionIdToTabId.forEach(function(mappedTabId, sessionId) {
59
+ if (mappedTabId === tabId) {
60
+ lastMatch = sessionId;
62
61
  }
63
- entry = entries.next();
64
- }
65
- return null;
62
+ });
63
+ return lastMatch;
64
+ }
65
+
66
+ function findSessionsByTabId(tabId) {
67
+ var sessions = [];
68
+ _state.sessionIdToTabId.forEach(function(mappedTabId, sessionId) {
69
+ if (mappedTabId === tabId) {
70
+ sessions.push(sessionId);
71
+ }
72
+ });
73
+ return sessions;
66
74
  }
67
75
 
68
76
  function findSessionByTargetId(targetId) {
@@ -453,6 +461,7 @@ var State = (function() {
453
461
  getTabIdBySession: getTabIdBySession,
454
462
  getTargetIdBySession: getTargetIdBySession,
455
463
  findSessionByTabId: findSessionByTabId,
464
+ findSessionsByTabId: findSessionsByTabId,
456
465
  findSessionByTargetId: findSessionByTargetId,
457
466
  getTabIdByTargetId: getTabIdByTargetId,
458
467
  hasOtherSessionForTab: hasOtherSessionForTab,
@@ -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.5",
4
+ "version": "2.7.7",
5
5
  "description": "Chrome DevTools Protocol Bridge for Playwright/Puppeteer automation",
6
6
  "permissions": [
7
7
  "debugger",
@@ -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.5",
3
+ "version": "2.7.7",
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();
@@ -1307,9 +1329,15 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
1307
1329
  }
1308
1330
 
1309
1331
  if (parsed && parsed.method === 'Browser.close') {
1310
- if (shouldLog('info')) {
1311
- console.log(`\n[BROWSER CLOSE] Client ${id} requested Browser.close, forwarding to plugin`);
1332
+ console.log(`[BROWSER CLOSE] Client ${id} requested Browser.close — closing client group only`);
1333
+ if (ws.pairedPlugin) {
1334
+ safeSend(ws.pairedPlugin, JSON.stringify({
1335
+ type: 'browser-close',
1336
+ clientId: id
1337
+ }), 'plugin');
1312
1338
  }
1339
+ safeSend(ws, JSON.stringify({ id: originalId, result: {} }), 'client');
1340
+ return;
1313
1341
  }
1314
1342
 
1315
1343
  if (parsed && parsed.method) {