cdp-tunnel 2.7.10 → 2.8.1

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.
@@ -0,0 +1,355 @@
1
+ function ConnectionState(connectionId) {
2
+ this.connectionId = connectionId;
3
+ this.ws = null;
4
+ this.reconnectTimer = null;
5
+ this._hasConnectedClient = false;
6
+ this.cdpClients = [];
7
+ this.currentTabId = null;
8
+ this.isAttached = false;
9
+
10
+ this.sessionIdToTabId = new Map();
11
+ this.sessionIdToTargetId = new Map();
12
+ this.tabIdToClientId = new Map();
13
+ this.clientIdToGroupId = new Map();
14
+ this.attachedTabIds = new Set();
15
+ this.cdpCreatedTabIds = new Set();
16
+ this.emittedTargets = new Set();
17
+ this.pendingCreatedTabUrls = new Set();
18
+ this.preExistingTabIds = new Set();
19
+ this.pendingDebuggerTabs = new Set();
20
+ this.autoAttachConfig = {
21
+ autoAttach: false,
22
+ waitForDebuggerOnStart: false,
23
+ flatten: true
24
+ };
25
+ this.discoverTargetsEnabled = false;
26
+ this.browserContextIds = new Set(['default']);
27
+ this.screencastPollingSessions = new Map();
28
+ this.automatedTabs = new Set();
29
+ }
30
+
31
+ ConnectionState.prototype.mapSession = function(sessionId, tabId, targetId) {
32
+ this.sessionIdToTabId.set(sessionId, tabId);
33
+ this.sessionIdToTargetId.set(sessionId, targetId);
34
+ this.attachedTabIds.add(tabId);
35
+ };
36
+
37
+ ConnectionState.prototype.unmapSession = function(sessionId) {
38
+ var tabId = this.sessionIdToTabId.get(sessionId);
39
+ this.sessionIdToTabId.delete(sessionId);
40
+ this.sessionIdToTargetId.delete(sessionId);
41
+ if (tabId && !this.hasOtherSessionForTab(tabId)) {
42
+ this.attachedTabIds.delete(tabId);
43
+ }
44
+ return tabId;
45
+ };
46
+
47
+ ConnectionState.prototype.getTabIdBySession = function(sessionId) {
48
+ return this.sessionIdToTabId.get(sessionId);
49
+ };
50
+
51
+ ConnectionState.prototype.getTargetIdBySession = function(sessionId) {
52
+ return this.sessionIdToTargetId.get(sessionId);
53
+ };
54
+
55
+ ConnectionState.prototype.findSessionByTabId = function(tabId) {
56
+ var lastMatch = null;
57
+ this.sessionIdToTabId.forEach(function(mappedTabId, sessionId) {
58
+ if (mappedTabId === tabId) lastMatch = sessionId;
59
+ });
60
+ return lastMatch;
61
+ };
62
+
63
+ ConnectionState.prototype.findSessionsByTabId = function(tabId) {
64
+ var sessions = [];
65
+ this.sessionIdToTabId.forEach(function(mappedTabId, sessionId) {
66
+ if (mappedTabId === tabId) sessions.push(sessionId);
67
+ });
68
+ return sessions;
69
+ };
70
+
71
+ ConnectionState.prototype.findSessionByTargetId = function(targetId) {
72
+ var entries = this.sessionIdToTargetId.entries();
73
+ var entry = entries.next();
74
+ while (!entry.done) {
75
+ if (entry.value[1] === targetId) return entry.value[0];
76
+ entry = entries.next();
77
+ }
78
+ return null;
79
+ };
80
+
81
+ ConnectionState.prototype.getTabIdByTargetId = function(targetId) {
82
+ var sessionId = this.findSessionByTargetId(targetId);
83
+ if (sessionId) return this.sessionIdToTabId.get(sessionId);
84
+ return null;
85
+ };
86
+
87
+ ConnectionState.prototype.hasOtherSessionForTab = function(tabId) {
88
+ var count = 0;
89
+ this.sessionIdToTabId.forEach(function(mappedTabId) {
90
+ if (mappedTabId === tabId) count++;
91
+ });
92
+ return count > 0;
93
+ };
94
+
95
+ ConnectionState.prototype.addAttachedTab = function(tabId) {
96
+ this.attachedTabIds.add(tabId);
97
+ };
98
+
99
+ ConnectionState.prototype.removeAttachedTab = function(tabId) {
100
+ this.attachedTabIds.delete(tabId);
101
+ };
102
+
103
+ ConnectionState.prototype.isTabAttached = function(tabId) {
104
+ return this.attachedTabIds.has(tabId);
105
+ };
106
+
107
+ ConnectionState.prototype.getAttachedTabIds = function() {
108
+ return Array.from(this.attachedTabIds);
109
+ };
110
+
111
+ ConnectionState.prototype.addEmittedTarget = function(targetId) {
112
+ this.emittedTargets.add(targetId);
113
+ };
114
+
115
+ ConnectionState.prototype.hasEmittedTarget = function(targetId) {
116
+ return this.emittedTargets.has(targetId);
117
+ };
118
+
119
+ ConnectionState.prototype.setAutoAttachConfig = function(config) {
120
+ Object.assign(this.autoAttachConfig, config);
121
+ };
122
+
123
+ ConnectionState.prototype.getAutoAttachConfig = function() {
124
+ return Object.assign({}, this.autoAttachConfig);
125
+ };
126
+
127
+ ConnectionState.prototype.setDiscoverTargets = function(enabled) {
128
+ this.discoverTargetsEnabled = enabled;
129
+ };
130
+
131
+ ConnectionState.prototype.addPendingDebuggerTab = function(tabId) {
132
+ this.pendingDebuggerTabs.add(tabId);
133
+ };
134
+
135
+ ConnectionState.prototype.removePendingDebuggerTab = function(tabId) {
136
+ this.pendingDebuggerTabs.delete(tabId);
137
+ };
138
+
139
+ ConnectionState.prototype.isPendingDebuggerTab = function(tabId) {
140
+ return this.pendingDebuggerTabs.has(tabId);
141
+ };
142
+
143
+ ConnectionState.prototype.addBrowserContext = function(id) {
144
+ this.browserContextIds.add(id);
145
+ };
146
+
147
+ ConnectionState.prototype.removeBrowserContext = function(id) {
148
+ this.browserContextIds.delete(id);
149
+ };
150
+
151
+ ConnectionState.prototype.getBrowserContexts = function() {
152
+ return Array.from(this.browserContextIds);
153
+ };
154
+
155
+ ConnectionState.prototype.getCurrentTabId = function() {
156
+ return this.currentTabId;
157
+ };
158
+
159
+ ConnectionState.prototype.setCurrentTabId = function(tabId) {
160
+ this.currentTabId = tabId;
161
+ };
162
+
163
+ ConnectionState.prototype.getWs = function() {
164
+ return this.ws;
165
+ };
166
+
167
+ ConnectionState.prototype.setWs = function(ws) {
168
+ this.ws = ws;
169
+ };
170
+
171
+ ConnectionState.prototype.clearReconnectTimer = function() {
172
+ if (this.reconnectTimer) {
173
+ clearTimeout(this.reconnectTimer);
174
+ this.reconnectTimer = null;
175
+ }
176
+ };
177
+
178
+ ConnectionState.prototype.setReconnectTimer = function(timer) {
179
+ this.reconnectTimer = timer;
180
+ };
181
+
182
+ ConnectionState.prototype.hasConnectedClient = function() {
183
+ return this._hasConnectedClient;
184
+ };
185
+
186
+ ConnectionState.prototype.setHasConnectedClient = function(value) {
187
+ this._hasConnectedClient = value;
188
+ };
189
+
190
+ ConnectionState.prototype.addPendingCreatedTabUrl = function(url) {
191
+ this.pendingCreatedTabUrls.add(url);
192
+ };
193
+
194
+ ConnectionState.prototype.removePendingCreatedTabUrl = function(url) {
195
+ this.pendingCreatedTabUrls.delete(url);
196
+ };
197
+
198
+ ConnectionState.prototype.hasPendingCreatedTabUrl = function(url) {
199
+ return this.pendingCreatedTabUrls.has(url);
200
+ };
201
+
202
+ ConnectionState.prototype.addCDPCreatedTab = function(tabId) {
203
+ this.cdpCreatedTabIds.add(tabId);
204
+ };
205
+
206
+ ConnectionState.prototype.isCDPCreatedTab = function(tabId) {
207
+ return this.cdpCreatedTabIds.has(tabId);
208
+ };
209
+
210
+ ConnectionState.prototype.getCDPCreatedTabIds = function() {
211
+ return Array.from(this.cdpCreatedTabIds);
212
+ };
213
+
214
+ ConnectionState.prototype.getScreencastSession = function(tabId) {
215
+ return this.screencastPollingSessions.get(tabId);
216
+ };
217
+
218
+ ConnectionState.prototype.setScreencastSession = function(tabId, session) {
219
+ this.screencastPollingSessions.set(tabId, session);
220
+ };
221
+
222
+ ConnectionState.prototype.deleteScreencastSession = function(tabId) {
223
+ this.screencastPollingSessions.delete(tabId);
224
+ };
225
+
226
+ ConnectionState.prototype.addAutomatedTab = function(tabId) {
227
+ this.automatedTabs.add(tabId);
228
+ };
229
+
230
+ ConnectionState.prototype.removeAutomatedTab = function(tabId) {
231
+ this.automatedTabs.delete(tabId);
232
+ };
233
+
234
+ ConnectionState.prototype.getAutomatedTabs = function() {
235
+ return Array.from(this.automatedTabs);
236
+ };
237
+
238
+ ConnectionState.prototype.setTabIdToClientId = function(tabId, clientId) {
239
+ this.tabIdToClientId.set(tabId, clientId);
240
+ };
241
+
242
+ ConnectionState.prototype.removeTabIdToClientId = function(tabId) {
243
+ this.tabIdToClientId.delete(tabId);
244
+ };
245
+
246
+ ConnectionState.prototype.getClientIdByTabId = function(tabId) {
247
+ return this.tabIdToClientId.get(tabId);
248
+ };
249
+
250
+ ConnectionState.prototype.setGroupIdForClient = function(clientId, groupId) {
251
+ this.clientIdToGroupId.set(clientId, groupId);
252
+ };
253
+
254
+ ConnectionState.prototype.getGroupIdForClient = function(clientId) {
255
+ return this.clientIdToGroupId.get(clientId);
256
+ };
257
+
258
+ ConnectionState.prototype.removeGroupForClient = function(clientId) {
259
+ this.clientIdToGroupId.delete(clientId);
260
+ };
261
+
262
+ ConnectionState.prototype.addPreExistingTab = function(tabId) {
263
+ this.preExistingTabIds.add(tabId);
264
+ };
265
+
266
+ ConnectionState.prototype.isPreExistingTab = function(tabId) {
267
+ return this.preExistingTabIds.has(tabId);
268
+ };
269
+
270
+ ConnectionState.prototype.getPreExistingTabs = function() {
271
+ return Array.from(this.preExistingTabIds);
272
+ };
273
+
274
+ ConnectionState.prototype.removePreExistingTab = function(tabId) {
275
+ this.preExistingTabIds.delete(tabId);
276
+ };
277
+
278
+ ConnectionState.prototype.clearPreExistingTabsForClient = function(clientId) {
279
+ var self = this;
280
+ this.preExistingTabIds.forEach(function(tabId) {
281
+ if (self.getClientIdByTabId(tabId) === clientId) {
282
+ self.preExistingTabIds.delete(tabId);
283
+ }
284
+ });
285
+ };
286
+
287
+ ConnectionState.prototype.addCDPClient = function(clientId, info) {
288
+ var exists = false;
289
+ for (var i = 0; i < this.cdpClients.length; i++) {
290
+ if (this.cdpClients[i].id === clientId) { exists = true; break; }
291
+ }
292
+ if (!exists) {
293
+ this.cdpClients.push({ id: clientId, connectedAt: Date.now() });
294
+ }
295
+ };
296
+
297
+ ConnectionState.prototype.removeCDPClient = function(clientId) {
298
+ var newClients = [];
299
+ for (var i = 0; i < this.cdpClients.length; i++) {
300
+ if (this.cdpClients[i].id !== clientId) newClients.push(this.cdpClients[i]);
301
+ }
302
+ this.cdpClients = newClients;
303
+ };
304
+
305
+ ConnectionState.prototype.getCDPClients = function() {
306
+ return this.cdpClients;
307
+ };
308
+
309
+ ConnectionState.prototype.setCDPClients = function(clients) {
310
+ this.cdpClients = clients || [];
311
+ };
312
+
313
+ ConnectionState.prototype.clearSessionState = function() {
314
+ this.sessionIdToTabId.clear();
315
+ this.sessionIdToTargetId.clear();
316
+ this.pendingDebuggerTabs.clear();
317
+ this.emittedTargets.clear();
318
+ };
319
+
320
+ ConnectionState.prototype.clearAllState = function() {
321
+ this.clearSessionState();
322
+ this.attachedTabIds.clear();
323
+ this.emittedTargets.clear();
324
+ this.screencastPollingSessions.clear();
325
+ this.browserContextIds = new Set(['default']);
326
+ this.autoAttachConfig = { autoAttach: false, waitForDebuggerOnStart: false, flatten: true };
327
+ this.discoverTargetsEnabled = false;
328
+ this._hasConnectedClient = false;
329
+ this.tabIdToClientId.clear();
330
+ this.clientIdToGroupId.clear();
331
+ this.preExistingTabIds.clear();
332
+ this.pendingDebuggerTabs.clear();
333
+ this.automatedTabs.clear();
334
+ this.pendingCreatedTabUrls.clear();
335
+ this.cdpCreatedTabIds.clear();
336
+ this.cdpClients = [];
337
+ };
338
+
339
+ ConnectionState.prototype.persist = function(tabId, attached) {
340
+ this.currentTabId = tabId;
341
+ this.isAttached = attached;
342
+ return new Promise(function(resolve) {
343
+ chrome.storage.local.set({ currentTabId: tabId, isAttached: attached }, resolve);
344
+ }.bind(this));
345
+ };
346
+
347
+ ConnectionState.prototype.loadPersisted = function() {
348
+ return new Promise(function(resolve) {
349
+ chrome.storage.local.get(['currentTabId', 'isAttached'], function(result) {
350
+ this.currentTabId = result.currentTabId || null;
351
+ this.isAttached = result.isAttached || false;
352
+ resolve(result);
353
+ }.bind(this));
354
+ }.bind(this));
355
+ };
@@ -8,17 +8,14 @@ var DebuggerManager = (function() {
8
8
 
9
9
  function isInternalUrl(url) {
10
10
  if (!url) return false;
11
- // Whitelist: only allow http/https/about/data/blob/file
12
11
  if (url.startsWith('http:') || url.startsWith('https:') || url.startsWith('about:') || url.startsWith('data:') || url.startsWith('blob:') || url.startsWith('file:')) {
13
12
  return false;
14
13
  }
15
- // Also check explicit blocked list for common custom protocols
16
14
  for (var i = 0; i < blockedProtocols.length; i++) {
17
15
  if (url.startsWith(blockedProtocols[i])) {
18
16
  return true;
19
17
  }
20
18
  }
21
- // Block any other custom protocol (xxx://)
22
19
  var colonIdx = url.indexOf(':');
23
20
  if (colonIdx > 0 && colonIdx < 20 && url.substring(colonIdx, colonIdx + 3) === '://') {
24
21
  return true;
@@ -26,7 +23,6 @@ var DebuggerManager = (function() {
26
23
  return false;
27
24
  }
28
25
 
29
- // 拦截 iframe 创建
30
26
  var originalCreateElement = document.createElement;
31
27
  document.createElement = function(tagName) {
32
28
  var element = originalCreateElement.call(document, tagName);
@@ -43,7 +39,6 @@ var DebuggerManager = (function() {
43
39
  return element;
44
40
  };
45
41
 
46
- // 拦截 window.open
47
42
  var originalWindowOpen = window.open;
48
43
  window.open = function(url) {
49
44
  if (isInternalUrl(url)) {
@@ -53,7 +48,6 @@ var DebuggerManager = (function() {
53
48
  return originalWindowOpen.apply(this, arguments);
54
49
  };
55
50
 
56
- // 拦截 location 修改
57
51
  var locationDescriptor = Object.getOwnPropertyDescriptor(window, 'location');
58
52
  if (locationDescriptor && typeof locationDescriptor.set === 'function') {
59
53
  try {
@@ -71,7 +65,6 @@ var DebuggerManager = (function() {
71
65
  configurable: true
72
66
  });
73
67
  } catch(e) {
74
- // location property cannot be redefined on some contexts, skip silently
75
68
  }
76
69
  }
77
70
 
@@ -79,7 +72,8 @@ var DebuggerManager = (function() {
79
72
  })();
80
73
  `;
81
74
 
82
- function attach(tabId) {
75
+ function attach(tabId, connState) {
76
+ var state = connState || _getAnyStateForTab(tabId);
83
77
  if (tabId == null) {
84
78
  return Promise.resolve(false);
85
79
  }
@@ -87,7 +81,7 @@ var DebuggerManager = (function() {
87
81
  return ensureTabExists(tabId).then(function(exists) {
88
82
  if (!exists) {
89
83
  Logger.warn('[Debugger] Tab', tabId, 'does not exist');
90
- State.persist(null, false);
84
+ if (state) state.persist(null, false);
91
85
  return false;
92
86
  }
93
87
 
@@ -95,23 +89,28 @@ var DebuggerManager = (function() {
95
89
  if (isAttached) {
96
90
  Logger.info('[Debugger] Tab', tabId, 'already attached, detaching first...');
97
91
  return chrome.debugger.detach({ tabId: tabId }).catch(function() {}).then(function() {
98
- return doAttach(tabId);
92
+ return doAttach(tabId, state);
99
93
  });
100
94
  }
101
- return doAttach(tabId);
95
+ return doAttach(tabId, state);
102
96
  });
103
97
  });
104
98
  }
105
99
 
106
- function doAttach(tabId) {
100
+ function _getAnyStateForTab(tabId) {
101
+ var entry = ConnectionManager.getConnectionByTabId(tabId);
102
+ return entry ? entry.state : null;
103
+ }
104
+
105
+ function doAttach(tabId, state) {
107
106
  return chrome.debugger.attach({ tabId: tabId }, Config.DEBUGGER_VERSION)
108
107
  .then(function() {
109
108
  Logger.info('[Debugger] Attached to tab', tabId);
110
- State.addAttachedTab(tabId);
111
- State.setCurrentTabId(tabId);
112
- State.persist(tabId, true);
113
-
114
- // 注入内部URL拦截脚本
109
+ if (state) {
110
+ state.addAttachedTab(tabId);
111
+ state.setCurrentTabId(tabId);
112
+ state.persist(tabId, true);
113
+ }
115
114
  return injectInternalUrlBlocker(tabId);
116
115
  })
117
116
  .then(function() {
@@ -142,7 +141,8 @@ var DebuggerManager = (function() {
142
141
  });
143
142
  }
144
143
 
145
- function detach(tabId) {
144
+ function detach(tabId, connState) {
145
+ var state = connState || _getAnyStateForTab(tabId);
146
146
  if (tabId == null) {
147
147
  return Promise.resolve();
148
148
  }
@@ -152,16 +152,18 @@ var DebuggerManager = (function() {
152
152
  return chrome.debugger.detach({ tabId: tabId })
153
153
  .then(function() {
154
154
  Logger.info('[Debugger] Detached from tab', tabId);
155
- State.removeAttachedTab(tabId);
155
+ if (state) {
156
+ state.removeAttachedTab(tabId);
157
+ if (state.getCurrentTabId() === tabId) {
158
+ state.persist(null, false);
159
+ }
160
+ }
156
161
  AutomationBadge.remove(tabId);
157
162
  Screencast.stopPolling(tabId);
158
- if (State.getCurrentTabId() === tabId) {
159
- State.persist(null, false);
160
- }
161
163
  })
162
164
  .catch(function(error) {
163
165
  Logger.error('[Debugger] Failed to detach from tab', tabId, ':', error.message);
164
- State.removeAttachedTab(tabId);
166
+ if (state) state.removeAttachedTab(tabId);
165
167
  AutomationBadge.remove(tabId);
166
168
  Screencast.stopPolling(tabId);
167
169
  });
@@ -191,11 +193,17 @@ var DebuggerManager = (function() {
191
193
  }
192
194
 
193
195
  function handleDebuggerEvent(source, method, params) {
194
- if (!State.isTabAttached(source.tabId)) {
196
+ var entry = ConnectionManager.getConnectionByTabId(source.tabId);
197
+ if (!entry) return;
198
+
199
+ var state = entry.state;
200
+ var wsManager = entry.wsManager;
201
+
202
+ if (!state.isTabAttached(source.tabId)) {
195
203
  return;
196
204
  }
197
205
 
198
- if (!State.hasConnectedClient()) {
206
+ if (!state.hasConnectedClient()) {
199
207
  return;
200
208
  }
201
209
 
@@ -203,20 +211,19 @@ var DebuggerManager = (function() {
203
211
  return;
204
212
  }
205
213
 
206
- var sessionIds = State.findSessionsByTabId(source.tabId);
214
+ var sessionIds = state.findSessionsByTabId(source.tabId);
207
215
  Logger.info('[Event] method=' + method + ' tabId=' + source.tabId + ' sessions=' + sessionIds.length);
208
-
216
+
209
217
  if (method === 'Runtime.executionContextCreated' && params && params.context) {
210
218
  var context = params.context;
211
219
  var isPlaywrightContext = context.name && context.name.indexOf('__playwright') === 0;
212
220
  var isDefaultContext = context.auxData && context.auxData.isDefault;
213
-
221
+
214
222
  if (!isPlaywrightContext && !isDefaultContext) {
215
223
  Logger.info('[Event] Filtering non-Playwright Runtime.executionContextCreated:', context.name);
216
224
  return;
217
225
  }
218
-
219
- // 在新的执行上下文中也注入拦截脚本(data: URL 上下文跳过,避免干扰导航)
226
+
220
227
  if (isDefaultContext) {
221
228
  chrome.tabs.get(source.tabId, function(tab) {
222
229
  if (chrome.runtime.lastError) return;
@@ -234,7 +241,7 @@ var DebuggerManager = (function() {
234
241
  });
235
242
  }
236
243
  }
237
-
244
+
238
245
  if (method === 'Page.frameRequestedNavigation' && params) {
239
246
  var url = params.url || '';
240
247
  var reason = params.reason || '';
@@ -245,12 +252,11 @@ var DebuggerManager = (function() {
245
252
  Logger.warn(' Reason:', reason);
246
253
  Logger.warn(' Disposition:', disposition);
247
254
  Logger.warn(' FrameId:', frameId);
248
-
255
+
249
256
  if (url && !url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('about:') && !url.startsWith('data:') && !url.startsWith('blob:') && !url.startsWith('file://')) {
250
257
  Logger.error('[NAVIGATION] ⚠️ 检测到导航到内部页面,尝试阻止!');
251
258
  Logger.error('[NAVIGATION] 目标URL:', url);
252
-
253
- // 尝试方法1: 停止页面加载
259
+
254
260
  chrome.debugger.sendCommand(
255
261
  { tabId: source.tabId },
256
262
  'Page.stopLoading',
@@ -260,8 +266,7 @@ var DebuggerManager = (function() {
260
266
  }).catch(function(e) {
261
267
  Logger.error('[NAVIGATION] Page.stopLoading 失败:', e.message);
262
268
  });
263
-
264
- // 尝试方法2: 获取当前页面URL并导航回去
269
+
265
270
  chrome.tabs.get(source.tabId, function(tab) {
266
271
  if (tab && tab.url && !tab.url.startsWith('bitbrowser://')) {
267
272
  Logger.info('[NAVIGATION] 尝试导航回原页面:', tab.url);
@@ -278,36 +283,44 @@ var DebuggerManager = (function() {
278
283
  }, 100);
279
284
  }
280
285
  });
281
-
282
- // 不转发这个导航事件给客户端
286
+
283
287
  Logger.warn('[NAVIGATION] 阻止转发导航事件到客户端');
284
288
  return;
285
289
  }
286
290
  }
287
-
291
+
288
292
  for (var i = 0; i < sessionIds.length; i++) {
289
- EventBuilder.send(method, params, sessionIds[i]);
293
+ EventBuilder.send(method, params, sessionIds[i], wsManager);
290
294
  }
291
295
  }
292
296
 
293
297
  function handleDetach(source, reason) {
294
298
  Logger.info('[Debugger] Detached from tab', source.tabId, ', reason:', reason);
295
- State.removeAttachedTab(source.tabId);
299
+
300
+ var entry = ConnectionManager.getConnectionByTabId(source.tabId);
301
+ var state = entry ? entry.state : null;
302
+ var wsManager = entry ? entry.wsManager : null;
303
+
304
+ if (state) {
305
+ state.removeAttachedTab(source.tabId);
306
+ }
296
307
  Screencast.stopPolling(source.tabId);
297
308
  AutomationBadge.remove(source.tabId);
298
309
 
299
- var sessionId = State.findSessionByTabId(source.tabId);
300
- if (sessionId) {
301
- var targetId = State.getTargetIdBySession(sessionId);
302
- EventBuilder.send('Target.detachedFromTarget', {
303
- sessionId: sessionId,
304
- targetId: targetId
305
- });
306
- State.unmapSession(sessionId);
307
- }
310
+ if (state) {
311
+ var sessionId = state.findSessionByTabId(source.tabId);
312
+ if (sessionId) {
313
+ var targetId = state.getTargetIdBySession(sessionId);
314
+ EventBuilder.send('Target.detachedFromTarget', {
315
+ sessionId: sessionId,
316
+ targetId: targetId
317
+ }, null, wsManager);
318
+ state.unmapSession(sessionId);
319
+ }
308
320
 
309
- if (State.getCurrentTabId() === source.tabId) {
310
- State.persist(null, false);
321
+ if (state.getCurrentTabId() === source.tabId) {
322
+ state.persist(null, false);
323
+ }
311
324
  }
312
325
  }
313
326