opendevbrowser 0.0.11 → 0.0.15
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/LICENSE +21 -0
- package/README.md +289 -28
- package/dist/chunk-JVBMT2O5.js +7173 -0
- package/dist/chunk-JVBMT2O5.js.map +1 -0
- package/dist/cli/index.js +3690 -275
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1080 -2857
- package/dist/index.js.map +1 -1
- package/dist/opendevbrowser.js +1080 -2857
- package/dist/opendevbrowser.js.map +1 -1
- package/extension/dist/annotate-content.css +237 -0
- package/extension/dist/annotate-content.js +934 -0
- package/extension/dist/background.js +1291 -8
- package/extension/dist/logging.js +50 -0
- package/extension/dist/ops/dom-bridge.js +355 -0
- package/extension/dist/ops/ops-runtime.js +1249 -0
- package/extension/dist/ops/ops-session-store.js +189 -0
- package/extension/dist/ops/redaction.js +52 -0
- package/extension/dist/ops/snapshot-builder.js +4 -0
- package/extension/dist/ops/snapshot-shared.js +220 -0
- package/extension/dist/popup.js +398 -21
- package/extension/dist/relay-settings.js +3 -1
- package/extension/dist/services/CDPRouter.js +501 -103
- package/extension/dist/services/ConnectionManager.js +464 -57
- package/extension/dist/services/NativePortManager.js +182 -0
- package/extension/dist/services/RelayClient.js +227 -26
- package/extension/dist/services/TabManager.js +81 -0
- package/extension/dist/services/TargetSessionMap.js +146 -0
- package/extension/dist/services/cdp-router-commands.js +203 -0
- package/extension/dist/services/url-restrictions.js +41 -0
- package/extension/dist/types.js +3 -1
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon32.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +17 -3
- package/extension/popup.html +469 -65
- package/package.json +2 -2
- package/skills/AGENTS.md +34 -61
- package/skills/data-extraction/SKILL.md +95 -103
- package/skills/form-testing/SKILL.md +75 -82
- package/skills/login-automation/SKILL.md +76 -66
- package/skills/opendevbrowser-best-practices/SKILL.md +90 -49
- package/skills/opendevbrowser-continuity-ledger/SKILL.md +57 -23
- package/dist/chunk-R5VUZEUU.js +0 -128
- package/dist/chunk-R5VUZEUU.js.map +0 -1
- package/extension/dist/popup.jsx +0 -150
|
@@ -1,79 +1,510 @@
|
|
|
1
|
+
import { TabManager } from "./TabManager.js";
|
|
2
|
+
import { TargetSessionMap } from "./TargetSessionMap.js";
|
|
3
|
+
import { logError } from "../logging.js";
|
|
4
|
+
import { handleSetDiscoverTargets, handleSetAutoAttach, handleCreateTarget, handleCloseTarget, handleActivateTarget, handleAttachToTarget, handleRoutedCommand } from "./cdp-router-commands.js";
|
|
5
|
+
const FLAT_SESSION_ERROR = "Chrome 125+ required for extension relay (flat sessions).";
|
|
6
|
+
const DEPRECATED_SEND_MESSAGE = "Target.sendMessageToTarget is deprecated in flat session mode. Use sessionId routing.";
|
|
7
|
+
const DEFAULT_BROWSER_CONTEXT_ID = "default";
|
|
1
8
|
export class CDPRouter {
|
|
2
|
-
|
|
9
|
+
debuggees = new Map();
|
|
10
|
+
sessions = new TargetSessionMap();
|
|
11
|
+
tabManager = new TabManager();
|
|
12
|
+
rootAttachedSessions = new Set();
|
|
3
13
|
callbacks = null;
|
|
14
|
+
autoAttachOptions = { autoAttach: false, waitForDebuggerOnStart: false, flatten: true };
|
|
15
|
+
discoverTargets = false;
|
|
16
|
+
listenersActive = false;
|
|
17
|
+
flatSessionValidated = false;
|
|
18
|
+
primaryTabId = null;
|
|
19
|
+
lastActiveTabId = null;
|
|
20
|
+
sessionCounter = 1;
|
|
21
|
+
quarantinedSessions = new Map();
|
|
22
|
+
churnTracker = new Map();
|
|
23
|
+
churnWindowMs = 5000;
|
|
24
|
+
churnThreshold = 3;
|
|
4
25
|
handleEventBound = (source, method, params) => {
|
|
5
26
|
this.handleEvent(source, method, params);
|
|
6
27
|
};
|
|
7
|
-
handleDetachBound = (source) => {
|
|
8
|
-
this.handleDetach(source);
|
|
28
|
+
handleDetachBound = (source, reason) => {
|
|
29
|
+
this.handleDetach(source, reason);
|
|
9
30
|
};
|
|
10
31
|
setCallbacks(callbacks) {
|
|
11
32
|
this.callbacks = callbacks;
|
|
12
33
|
}
|
|
13
34
|
async attach(tabId) {
|
|
14
|
-
|
|
35
|
+
await this.attachInternal(tabId, true);
|
|
36
|
+
}
|
|
37
|
+
async attachInternal(tabId, allowRetry) {
|
|
38
|
+
if (this.debuggees.has(tabId)) {
|
|
39
|
+
this.updatePrimaryTab(tabId);
|
|
15
40
|
return;
|
|
16
41
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.debuggee = { tabId };
|
|
42
|
+
const debuggee = { tabId };
|
|
43
|
+
this.debuggees.set(tabId, debuggee);
|
|
44
|
+
this.ensureListeners();
|
|
21
45
|
try {
|
|
22
46
|
await this.runDebuggerAction((done) => {
|
|
23
|
-
chrome.debugger.attach(
|
|
47
|
+
chrome.debugger.attach(debuggee, "1.3", done);
|
|
24
48
|
});
|
|
25
|
-
|
|
26
|
-
|
|
49
|
+
await this.ensureFlatSessionSupport(debuggee);
|
|
50
|
+
const targetInfo = await this.registerRootTab(tabId);
|
|
51
|
+
if (this.discoverTargets) {
|
|
52
|
+
this.emitTargetCreated(targetInfo);
|
|
53
|
+
}
|
|
54
|
+
if (this.autoAttachOptions.autoAttach) {
|
|
55
|
+
await this.applyAutoAttach(debuggee);
|
|
56
|
+
this.emitRootAttached(targetInfo);
|
|
57
|
+
}
|
|
58
|
+
this.updatePrimaryTab(tabId);
|
|
27
59
|
}
|
|
28
60
|
catch (error) {
|
|
29
|
-
this.
|
|
61
|
+
this.debuggees.delete(tabId);
|
|
62
|
+
if (this.debuggees.size === 0) {
|
|
63
|
+
this.removeListeners();
|
|
64
|
+
}
|
|
65
|
+
await this.safeDetach(debuggee);
|
|
66
|
+
if (allowRetry && this.isStaleTabError(error)) {
|
|
67
|
+
const activeTabId = await this.tabManager.getActiveTabId();
|
|
68
|
+
if (activeTabId && activeTabId !== tabId) {
|
|
69
|
+
return await this.attachInternal(activeTabId, false);
|
|
70
|
+
}
|
|
71
|
+
const fallbackTabId = await this.tabManager.getFirstHttpTabId();
|
|
72
|
+
if (fallbackTabId && fallbackTabId !== tabId) {
|
|
73
|
+
return await this.attachInternal(fallbackTabId, false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
30
76
|
throw error;
|
|
31
77
|
}
|
|
32
78
|
}
|
|
33
|
-
async
|
|
34
|
-
|
|
79
|
+
async detachAll() {
|
|
80
|
+
const entries = Array.from(this.debuggees.entries());
|
|
81
|
+
this.debuggees.clear();
|
|
82
|
+
this.removeListeners();
|
|
83
|
+
for (const [tabId, debuggee] of entries) {
|
|
84
|
+
this.detachTabState(tabId);
|
|
85
|
+
await this.safeDetach(debuggee);
|
|
86
|
+
}
|
|
87
|
+
this.primaryTabId = null;
|
|
88
|
+
this.lastActiveTabId = null;
|
|
89
|
+
this.callbacks?.onDetach({ reason: "manual_disconnect" });
|
|
90
|
+
}
|
|
91
|
+
async detachTab(tabId) {
|
|
92
|
+
const debuggee = this.debuggees.get(tabId);
|
|
93
|
+
if (!debuggee) {
|
|
35
94
|
return;
|
|
36
|
-
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
95
|
+
}
|
|
96
|
+
this.debuggees.delete(tabId);
|
|
97
|
+
this.detachTabState(tabId);
|
|
98
|
+
await this.safeDetach(debuggee);
|
|
99
|
+
if (this.debuggees.size === 0) {
|
|
100
|
+
this.removeListeners();
|
|
101
|
+
this.primaryTabId = null;
|
|
102
|
+
this.lastActiveTabId = null;
|
|
103
|
+
}
|
|
104
|
+
else if (this.primaryTabId === tabId) {
|
|
105
|
+
this.updatePrimaryTab(this.selectFallbackPrimary());
|
|
106
|
+
}
|
|
107
|
+
this.callbacks?.onDetach({ tabId, reason: "manual_disconnect" });
|
|
108
|
+
}
|
|
109
|
+
getPrimaryTabId() {
|
|
110
|
+
return this.primaryTabId;
|
|
43
111
|
}
|
|
44
|
-
|
|
45
|
-
return this.
|
|
112
|
+
getAttachedTabIds() {
|
|
113
|
+
return Array.from(this.debuggees.keys());
|
|
46
114
|
}
|
|
47
115
|
async handleCommand(command) {
|
|
48
|
-
if (!this.
|
|
49
|
-
|
|
116
|
+
if (!this.callbacks)
|
|
117
|
+
return;
|
|
118
|
+
if (this.debuggees.size === 0) {
|
|
119
|
+
this.respondError(command.id, "No tab attached");
|
|
50
120
|
return;
|
|
51
121
|
}
|
|
52
122
|
const { method, params, sessionId } = command.params;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
123
|
+
const commandParams = isRecord(params) ? params : {};
|
|
124
|
+
const ctx = this.buildCommandContext();
|
|
125
|
+
switch (method) {
|
|
126
|
+
case "Browser.getVersion": {
|
|
127
|
+
const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "OpenDevBrowser Relay";
|
|
128
|
+
this.respond(command.id, {
|
|
129
|
+
protocolVersion: "1.3",
|
|
130
|
+
product: "Chrome",
|
|
131
|
+
revision: "",
|
|
132
|
+
userAgent,
|
|
133
|
+
jsVersion: ""
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
57
136
|
}
|
|
58
|
-
|
|
59
|
-
this.
|
|
137
|
+
case "Browser.setDownloadBehavior":
|
|
138
|
+
this.respond(command.id, {});
|
|
139
|
+
return;
|
|
140
|
+
case "Target.getBrowserContexts":
|
|
141
|
+
this.respond(command.id, { browserContextIds: [DEFAULT_BROWSER_CONTEXT_ID] });
|
|
142
|
+
return;
|
|
143
|
+
case "Target.attachToBrowserTarget": {
|
|
144
|
+
const rootSession = await this.ensureRootSessionForPrimary();
|
|
145
|
+
if (!rootSession) {
|
|
146
|
+
this.respondError(command.id, "No tab attached");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.respond(command.id, { sessionId: rootSession.sessionId });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
case "Target.sendMessageToTarget":
|
|
153
|
+
this.respondError(command.id, DEPRECATED_SEND_MESSAGE);
|
|
154
|
+
return;
|
|
155
|
+
case "Target.setDiscoverTargets":
|
|
156
|
+
await handleSetDiscoverTargets(ctx, command.id, commandParams);
|
|
157
|
+
return;
|
|
158
|
+
case "Target.getTargets":
|
|
159
|
+
this.respond(command.id, { targetInfos: this.sessions.listTargetInfos() });
|
|
160
|
+
return;
|
|
161
|
+
case "Target.getTargetInfo": {
|
|
162
|
+
const targetId = typeof commandParams.targetId === "string" ? commandParams.targetId : "";
|
|
163
|
+
const record = targetId ? this.sessions.getByTargetId(targetId) : null;
|
|
164
|
+
const targetInfo = record?.targetInfo
|
|
165
|
+
?? (record?.kind === "root" ? this.sessions.getByTabId(record.tabId)?.targetInfo ?? null : null);
|
|
166
|
+
this.respond(command.id, { targetInfo });
|
|
167
|
+
return;
|
|
60
168
|
}
|
|
169
|
+
case "Target.setAutoAttach":
|
|
170
|
+
await handleSetAutoAttach(ctx, command.id, commandParams, sessionId);
|
|
171
|
+
return;
|
|
172
|
+
case "Target.createTarget":
|
|
173
|
+
await handleCreateTarget(ctx, command.id, commandParams);
|
|
174
|
+
return;
|
|
175
|
+
case "Target.closeTarget":
|
|
176
|
+
await handleCloseTarget(ctx, command.id, commandParams);
|
|
177
|
+
return;
|
|
178
|
+
case "Target.activateTarget":
|
|
179
|
+
await handleActivateTarget(ctx, command.id, commandParams);
|
|
180
|
+
return;
|
|
181
|
+
case "Target.attachToTarget":
|
|
182
|
+
await handleAttachToTarget(ctx, command.id, commandParams, sessionId);
|
|
183
|
+
return;
|
|
184
|
+
default:
|
|
185
|
+
await handleRoutedCommand(ctx, command.id, method, commandParams, sessionId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
buildCommandContext() {
|
|
189
|
+
return {
|
|
190
|
+
debuggees: this.debuggees,
|
|
191
|
+
sessions: this.sessions,
|
|
192
|
+
tabManager: this.tabManager,
|
|
193
|
+
autoAttachOptions: this.autoAttachOptions,
|
|
194
|
+
discoverTargets: this.discoverTargets,
|
|
195
|
+
flatSessionError: FLAT_SESSION_ERROR,
|
|
196
|
+
setAutoAttachOptions: (next) => {
|
|
197
|
+
this.autoAttachOptions = next;
|
|
198
|
+
},
|
|
199
|
+
setDiscoverTargets: (value) => {
|
|
200
|
+
this.discoverTargets = value;
|
|
201
|
+
},
|
|
202
|
+
respond: this.respond.bind(this),
|
|
203
|
+
respondError: this.respondError.bind(this),
|
|
204
|
+
emitTargetCreated: this.emitTargetCreated.bind(this),
|
|
205
|
+
emitRootAttached: this.emitRootAttached.bind(this),
|
|
206
|
+
emitRootDetached: this.emitRootDetached.bind(this),
|
|
207
|
+
resetRootAttached: this.resetRootAttached.bind(this),
|
|
208
|
+
updatePrimaryTab: this.updatePrimaryTab.bind(this),
|
|
209
|
+
detachTabState: this.detachTabState.bind(this),
|
|
210
|
+
safeDetach: this.safeDetach.bind(this),
|
|
211
|
+
attach: this.attach.bind(this),
|
|
212
|
+
registerRootTab: this.registerRootTab.bind(this),
|
|
213
|
+
applyAutoAttach: this.applyAutoAttach.bind(this),
|
|
214
|
+
sendCommand: this.sendCommand.bind(this),
|
|
215
|
+
getPrimaryDebuggee: this.getPrimaryDebuggee.bind(this)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async registerRootTab(tabId) {
|
|
219
|
+
const existing = this.sessions.getByTabId(tabId);
|
|
220
|
+
const sessionId = existing?.rootSessionId ?? this.createRootSessionId();
|
|
221
|
+
const targetInfo = await this.buildTargetInfo(tabId);
|
|
222
|
+
this.sessions.registerRootTab(tabId, targetInfo, sessionId);
|
|
223
|
+
return targetInfo;
|
|
224
|
+
}
|
|
225
|
+
updatePrimaryTab(tabId) {
|
|
226
|
+
if (tabId === this.primaryTabId)
|
|
227
|
+
return;
|
|
228
|
+
this.primaryTabId = tabId;
|
|
229
|
+
if (tabId !== null) {
|
|
230
|
+
this.lastActiveTabId = tabId;
|
|
231
|
+
}
|
|
232
|
+
this.callbacks?.onPrimaryTabChange?.(tabId);
|
|
233
|
+
}
|
|
234
|
+
selectFallbackPrimary() {
|
|
235
|
+
if (this.lastActiveTabId && this.debuggees.has(this.lastActiveTabId)) {
|
|
236
|
+
return this.lastActiveTabId;
|
|
237
|
+
}
|
|
238
|
+
const [first] = this.debuggees.keys();
|
|
239
|
+
return first ?? null;
|
|
240
|
+
}
|
|
241
|
+
getPrimaryDebuggee() {
|
|
242
|
+
if (this.primaryTabId !== null && this.debuggees.has(this.primaryTabId)) {
|
|
243
|
+
return { tabId: this.primaryTabId };
|
|
244
|
+
}
|
|
245
|
+
const [first] = this.debuggees.keys();
|
|
246
|
+
return typeof first === "number" ? { tabId: first } : null;
|
|
247
|
+
}
|
|
248
|
+
async ensureRootSessionForPrimary() {
|
|
249
|
+
const debuggee = this.getPrimaryDebuggee();
|
|
250
|
+
if (!debuggee || typeof debuggee.tabId !== "number") {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const existing = this.sessions.getByTabId(debuggee.tabId);
|
|
254
|
+
if (existing) {
|
|
255
|
+
return { sessionId: existing.rootSessionId, targetInfo: existing.targetInfo };
|
|
256
|
+
}
|
|
257
|
+
const targetInfo = await this.registerRootTab(debuggee.tabId);
|
|
258
|
+
const refreshed = this.sessions.getByTabId(debuggee.tabId);
|
|
259
|
+
if (!refreshed) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return { sessionId: refreshed.rootSessionId, targetInfo: targetInfo ?? refreshed.targetInfo };
|
|
263
|
+
}
|
|
264
|
+
ensureListeners() {
|
|
265
|
+
if (this.listenersActive)
|
|
266
|
+
return;
|
|
267
|
+
chrome.debugger.onEvent.addListener(this.handleEventBound);
|
|
268
|
+
chrome.debugger.onDetach.addListener(this.handleDetachBound);
|
|
269
|
+
this.listenersActive = true;
|
|
270
|
+
}
|
|
271
|
+
removeListeners() {
|
|
272
|
+
if (!this.listenersActive)
|
|
273
|
+
return;
|
|
274
|
+
chrome.debugger.onEvent.removeListener(this.handleEventBound);
|
|
275
|
+
chrome.debugger.onDetach.removeListener(this.handleDetachBound);
|
|
276
|
+
this.listenersActive = false;
|
|
277
|
+
}
|
|
278
|
+
async ensureFlatSessionSupport(debuggee) {
|
|
279
|
+
if (this.flatSessionValidated)
|
|
61
280
|
return;
|
|
281
|
+
try {
|
|
282
|
+
await this.sendCommand(debuggee, "Target.setAutoAttach", {
|
|
283
|
+
autoAttach: false,
|
|
284
|
+
waitForDebuggerOnStart: false,
|
|
285
|
+
flatten: true
|
|
286
|
+
});
|
|
287
|
+
this.flatSessionValidated = true;
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
const detail = getErrorMessage(error);
|
|
291
|
+
console.warn(`[opendevbrowser] Target.setAutoAttach(flatten) failed: ${detail}`);
|
|
292
|
+
throw new Error(`${FLAT_SESSION_ERROR} (${detail})`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async applyAutoAttach(debuggee) {
|
|
296
|
+
const params = {
|
|
297
|
+
autoAttach: this.autoAttachOptions.autoAttach,
|
|
298
|
+
waitForDebuggerOnStart: this.autoAttachOptions.waitForDebuggerOnStart,
|
|
299
|
+
flatten: true
|
|
300
|
+
};
|
|
301
|
+
if (typeof this.autoAttachOptions.filter !== "undefined") {
|
|
302
|
+
params.filter = this.autoAttachOptions.filter;
|
|
62
303
|
}
|
|
63
304
|
try {
|
|
64
|
-
|
|
65
|
-
this.callbacks.onResponse({ id: command.id, result });
|
|
305
|
+
await this.sendCommand(debuggee, "Target.setAutoAttach", params);
|
|
66
306
|
}
|
|
67
307
|
catch (error) {
|
|
68
|
-
|
|
308
|
+
const detail = getErrorMessage(error);
|
|
309
|
+
console.warn(`[opendevbrowser] Target.setAutoAttach failed: ${detail}`);
|
|
310
|
+
throw new Error(`${FLAT_SESSION_ERROR} (${detail})`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async applyAutoAttachToChild(tabId, sessionId) {
|
|
314
|
+
if (!this.autoAttachOptions.autoAttach)
|
|
315
|
+
return;
|
|
316
|
+
const params = {
|
|
317
|
+
autoAttach: true,
|
|
318
|
+
waitForDebuggerOnStart: this.autoAttachOptions.waitForDebuggerOnStart,
|
|
319
|
+
flatten: true
|
|
320
|
+
};
|
|
321
|
+
if (typeof this.autoAttachOptions.filter !== "undefined") {
|
|
322
|
+
params.filter = this.autoAttachOptions.filter;
|
|
69
323
|
}
|
|
324
|
+
await this.sendCommand({ tabId, sessionId }, "Target.setAutoAttach", params);
|
|
70
325
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
326
|
+
recordSessionChurn(tabId, sessionId, reason) {
|
|
327
|
+
const now = Date.now();
|
|
328
|
+
const existing = this.churnTracker.get(tabId);
|
|
329
|
+
const record = !existing || now > existing.resetAt
|
|
330
|
+
? { count: 0, resetAt: now + this.churnWindowMs }
|
|
331
|
+
: existing;
|
|
332
|
+
record.count += 1;
|
|
333
|
+
this.churnTracker.set(tabId, record);
|
|
334
|
+
const quarantined = this.quarantinedSessions.get(sessionId);
|
|
335
|
+
if (!quarantined) {
|
|
336
|
+
this.quarantinedSessions.set(sessionId, { tabId, count: 1, lastSeen: now });
|
|
337
|
+
}
|
|
338
|
+
if (record.count >= this.churnThreshold) {
|
|
339
|
+
this.churnTracker.delete(tabId);
|
|
340
|
+
this.reapplyAutoAttach(tabId, reason).catch((error) => {
|
|
341
|
+
logError("cdp.reapply_auto_attach", error, { code: "auto_attach_failed" });
|
|
342
|
+
});
|
|
74
343
|
}
|
|
344
|
+
}
|
|
345
|
+
quarantineUnknownSession(tabId, sessionId, method) {
|
|
346
|
+
const now = Date.now();
|
|
347
|
+
const existing = this.quarantinedSessions.get(sessionId);
|
|
348
|
+
if (existing) {
|
|
349
|
+
existing.count += 1;
|
|
350
|
+
existing.lastSeen = now;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
this.quarantinedSessions.set(sessionId, { tabId, count: 1, lastSeen: now });
|
|
354
|
+
this.recordSessionChurn(tabId, sessionId, `unknown_${method}`);
|
|
355
|
+
}
|
|
356
|
+
async reapplyAutoAttach(tabId, reason) {
|
|
357
|
+
if (!this.autoAttachOptions.autoAttach)
|
|
358
|
+
return;
|
|
359
|
+
const debuggee = this.debuggees.get(tabId);
|
|
360
|
+
if (!debuggee)
|
|
361
|
+
return;
|
|
362
|
+
try {
|
|
363
|
+
await this.applyAutoAttach(debuggee);
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
const detail = getErrorMessage(error);
|
|
367
|
+
console.warn(`[opendevbrowser] Auto-attach retry failed (${reason}): ${detail}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
handleEvent(source, method, params) {
|
|
371
|
+
if (!this.callbacks)
|
|
372
|
+
return;
|
|
373
|
+
const tabId = typeof source.tabId === "number" ? source.tabId : null;
|
|
374
|
+
if (tabId === null || !this.debuggees.has(tabId))
|
|
375
|
+
return;
|
|
376
|
+
if (method === "Target.receivedMessageFromTarget")
|
|
377
|
+
return;
|
|
378
|
+
if (method === "Target.attachedToTarget" && params && isRecord(params)) {
|
|
379
|
+
const sessionId = typeof params.sessionId === "string" ? params.sessionId : null;
|
|
380
|
+
const targetInfo = isTargetInfo(params.targetInfo) ? params.targetInfo : null;
|
|
381
|
+
if (sessionId && targetInfo) {
|
|
382
|
+
this.sessions.registerChildSession(tabId, targetInfo, sessionId);
|
|
383
|
+
this.quarantinedSessions.delete(sessionId);
|
|
384
|
+
this.applyAutoAttachToChild(tabId, sessionId).catch((error) => {
|
|
385
|
+
logError("cdp.apply_auto_attach_child", error, { code: "auto_attach_failed" });
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
else if (sessionId) {
|
|
389
|
+
this.recordSessionChurn(tabId, sessionId, "attach_missing_target");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (method === "Target.detachedFromTarget" && params && isRecord(params)) {
|
|
393
|
+
const detachedSessionId = typeof params.sessionId === "string" ? params.sessionId : null;
|
|
394
|
+
if (detachedSessionId) {
|
|
395
|
+
const removed = this.sessions.removeBySessionId(detachedSessionId);
|
|
396
|
+
if (!removed) {
|
|
397
|
+
this.recordSessionChurn(tabId, detachedSessionId, "detach_unknown");
|
|
398
|
+
this.quarantineUnknownSession(tabId, detachedSessionId, method);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const sourceSessionId = source.sessionId;
|
|
404
|
+
if (typeof sourceSessionId === "string" && !this.sessions.hasSession(sourceSessionId)) {
|
|
405
|
+
this.quarantineUnknownSession(tabId, sourceSessionId, method);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const forwardSessionId = this.resolveForwardSessionId(method, source);
|
|
409
|
+
this.emitEvent(method, params, forwardSessionId);
|
|
410
|
+
}
|
|
411
|
+
handleDetach(source, reason) {
|
|
412
|
+
const tabId = typeof source.tabId === "number" ? source.tabId : null;
|
|
413
|
+
if (tabId === null || !this.debuggees.has(tabId))
|
|
414
|
+
return;
|
|
415
|
+
this.debuggees.delete(tabId);
|
|
416
|
+
this.detachTabState(tabId);
|
|
417
|
+
if (this.debuggees.size === 0) {
|
|
418
|
+
this.removeListeners();
|
|
419
|
+
this.callbacks?.onDetach({ tabId, reason });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
detachTabState(tabId) {
|
|
423
|
+
const record = this.sessions.removeByTabId(tabId);
|
|
424
|
+
if (record) {
|
|
425
|
+
this.rootAttachedSessions.delete(record.rootSessionId);
|
|
426
|
+
if (this.autoAttachOptions.autoAttach) {
|
|
427
|
+
this.emitTargetDetached(record.rootSessionId, record.targetInfo.targetId);
|
|
428
|
+
}
|
|
429
|
+
if (this.discoverTargets) {
|
|
430
|
+
this.emitTargetDestroyed(record.targetInfo.targetId);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (tabId === this.primaryTabId) {
|
|
434
|
+
const next = this.selectFallbackPrimary();
|
|
435
|
+
this.updatePrimaryTab(next);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
resolveForwardSessionId(method, source) {
|
|
439
|
+
if (method === "Target.attachedToTarget" || method === "Target.detachedFromTarget") {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
const sessionId = source.sessionId;
|
|
443
|
+
if (typeof sessionId === "string") {
|
|
444
|
+
return this.sessions.getBySessionId(sessionId) ? sessionId : undefined;
|
|
445
|
+
}
|
|
446
|
+
const tabId = typeof source.tabId === "number" ? source.tabId : null;
|
|
447
|
+
if (tabId === null)
|
|
448
|
+
return undefined;
|
|
449
|
+
const record = this.sessions.getByTabId(tabId);
|
|
450
|
+
if (!record)
|
|
451
|
+
return undefined;
|
|
452
|
+
return this.rootAttachedSessions.has(record.rootSessionId) ? record.rootSessionId : undefined;
|
|
453
|
+
}
|
|
454
|
+
async buildTargetInfo(tabId) {
|
|
455
|
+
const tab = await this.tabManager.getTab(tabId);
|
|
456
|
+
return {
|
|
457
|
+
targetId: `tab-${tabId}`,
|
|
458
|
+
type: "page",
|
|
459
|
+
browserContextId: DEFAULT_BROWSER_CONTEXT_ID,
|
|
460
|
+
title: tab?.title ?? undefined,
|
|
461
|
+
url: tab?.url ?? undefined
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
emitTargetCreated(targetInfo) {
|
|
465
|
+
this.emitEvent("Target.targetCreated", { targetInfo });
|
|
466
|
+
}
|
|
467
|
+
emitTargetDestroyed(targetId) {
|
|
468
|
+
this.emitEvent("Target.targetDestroyed", { targetId });
|
|
469
|
+
}
|
|
470
|
+
emitTargetDetached(sessionId, targetId) {
|
|
471
|
+
this.emitEvent("Target.detachedFromTarget", { sessionId, targetId });
|
|
472
|
+
}
|
|
473
|
+
emitRootAttached(targetInfo) {
|
|
474
|
+
const record = this.sessions.getByTargetId(targetInfo.targetId);
|
|
475
|
+
if (!record || record.kind !== "root")
|
|
476
|
+
return;
|
|
477
|
+
if (this.rootAttachedSessions.has(record.sessionId))
|
|
478
|
+
return;
|
|
479
|
+
this.rootAttachedSessions.add(record.sessionId);
|
|
480
|
+
this.emitEvent("Target.attachedToTarget", {
|
|
481
|
+
sessionId: record.sessionId,
|
|
482
|
+
targetInfo,
|
|
483
|
+
waitingForDebugger: false
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
emitRootDetached() {
|
|
487
|
+
for (const targetInfo of this.sessions.listTargetInfos()) {
|
|
488
|
+
const record = this.sessions.getByTargetId(targetInfo.targetId);
|
|
489
|
+
if (!record || record.kind !== "root")
|
|
490
|
+
continue;
|
|
491
|
+
if (!this.rootAttachedSessions.has(record.sessionId))
|
|
492
|
+
continue;
|
|
493
|
+
this.rootAttachedSessions.delete(record.sessionId);
|
|
494
|
+
this.emitTargetDetached(record.sessionId, targetInfo.targetId);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
resetRootAttached() {
|
|
498
|
+
this.rootAttachedSessions.clear();
|
|
499
|
+
}
|
|
500
|
+
createRootSessionId() {
|
|
501
|
+
const sessionId = `pw-tab-${this.sessionCounter}`;
|
|
502
|
+
this.sessionCounter += 1;
|
|
503
|
+
return sessionId;
|
|
504
|
+
}
|
|
505
|
+
async sendCommand(debuggee, method, params) {
|
|
75
506
|
return new Promise((resolve, reject) => {
|
|
76
|
-
chrome.debugger.sendCommand(
|
|
507
|
+
chrome.debugger.sendCommand(debuggee, method, params, (result) => {
|
|
77
508
|
const lastError = chrome.runtime.lastError;
|
|
78
509
|
if (lastError) {
|
|
79
510
|
reject(new Error(lastError.message));
|
|
@@ -83,6 +514,10 @@ export class CDPRouter {
|
|
|
83
514
|
});
|
|
84
515
|
});
|
|
85
516
|
}
|
|
517
|
+
isStaleTabError(error) {
|
|
518
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
519
|
+
return message.includes("No tab with given id");
|
|
520
|
+
}
|
|
86
521
|
async runDebuggerAction(action) {
|
|
87
522
|
return new Promise((resolve, reject) => {
|
|
88
523
|
action(() => {
|
|
@@ -95,68 +530,38 @@ export class CDPRouter {
|
|
|
95
530
|
});
|
|
96
531
|
});
|
|
97
532
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const nested = parseNestedMessage(params.message);
|
|
104
|
-
const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
|
|
105
|
-
if (nested && (typeof nested.id === "string" || typeof nested.id === "number")) {
|
|
106
|
-
const error = normalizeError(nested.error);
|
|
107
|
-
this.callbacks.onResponse({
|
|
108
|
-
id: nested.id,
|
|
109
|
-
result: nested.result,
|
|
110
|
-
error,
|
|
111
|
-
sessionId
|
|
112
|
-
});
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
if (nested && typeof nested.method === "string") {
|
|
116
|
-
this.callbacks.onEvent({
|
|
117
|
-
method: "forwardCDPEvent",
|
|
118
|
-
params: {
|
|
119
|
-
method: nested.method,
|
|
120
|
-
params: nested.params,
|
|
121
|
-
sessionId
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
533
|
+
async safeDetach(debuggee) {
|
|
534
|
+
try {
|
|
535
|
+
await this.runDebuggerAction((done) => {
|
|
536
|
+
chrome.debugger.detach(debuggee, done);
|
|
537
|
+
});
|
|
126
538
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
params: {
|
|
130
|
-
method,
|
|
131
|
-
params
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
handleDetach(source) {
|
|
136
|
-
if (!this.matchesDebuggee(source) || !this.callbacks) {
|
|
137
|
-
return;
|
|
539
|
+
catch (error) {
|
|
540
|
+
logError("cdp.safe_detach", error, { code: "detach_failed" });
|
|
138
541
|
}
|
|
139
|
-
this.callbacks.onDetach();
|
|
140
542
|
}
|
|
141
|
-
|
|
142
|
-
if (!this.
|
|
143
|
-
return
|
|
144
|
-
|
|
543
|
+
respond(id, result, sessionId) {
|
|
544
|
+
if (!this.callbacks)
|
|
545
|
+
return;
|
|
546
|
+
this.callbacks.onResponse({ id, result, ...(sessionId ? { sessionId } : {}) });
|
|
145
547
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const parsed = JSON.parse(value);
|
|
152
|
-
return parsed;
|
|
548
|
+
respondError(id, message, sessionId) {
|
|
549
|
+
if (!this.callbacks)
|
|
550
|
+
return;
|
|
551
|
+
this.callbacks.onResponse({ id, error: { message }, ...(sessionId ? { sessionId } : {}) });
|
|
153
552
|
}
|
|
154
|
-
|
|
155
|
-
|
|
553
|
+
emitEvent(method, params, sessionId) {
|
|
554
|
+
if (!this.callbacks)
|
|
555
|
+
return;
|
|
556
|
+
const payload = { method, params };
|
|
557
|
+
if (sessionId) {
|
|
558
|
+
payload.sessionId = sessionId;
|
|
559
|
+
}
|
|
560
|
+
this.callbacks.onEvent({ method: "forwardCDPEvent", params: payload });
|
|
156
561
|
}
|
|
157
|
-
}
|
|
158
|
-
const
|
|
159
|
-
return typeof value === "
|
|
562
|
+
}
|
|
563
|
+
const isTargetInfo = (value) => {
|
|
564
|
+
return isRecord(value) && typeof value.targetId === "string" && typeof value.type === "string";
|
|
160
565
|
};
|
|
161
566
|
const getErrorMessage = (error) => {
|
|
162
567
|
if (error instanceof Error) {
|
|
@@ -164,13 +569,6 @@ const getErrorMessage = (error) => {
|
|
|
164
569
|
}
|
|
165
570
|
return "Unknown error";
|
|
166
571
|
};
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
const message = value.message;
|
|
172
|
-
if (typeof message !== "string") {
|
|
173
|
-
return undefined;
|
|
174
|
-
}
|
|
175
|
-
return { message };
|
|
572
|
+
const isRecord = (value) => {
|
|
573
|
+
return typeof value === "object" && value !== null;
|
|
176
574
|
};
|