@btraut/browser-bridge 0.1.0
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 +117 -0
- package/dist/api.d.ts +35 -0
- package/dist/api.js +4646 -0
- package/dist/api.js.map +7 -0
- package/dist/index.js +2087 -0
- package/dist/index.js.map +7 -0
- package/extension/assets/icons/icon-1024.png +0 -0
- package/extension/assets/icons/icon-128.png +0 -0
- package/extension/assets/icons/icon-16.png +0 -0
- package/extension/assets/icons/icon-32.png +0 -0
- package/extension/assets/icons/icon-48.png +0 -0
- package/extension/assets/store-listing-128.png +0 -0
- package/extension/assets/store-listing.png +0 -0
- package/extension/dist/background.js +1137 -0
- package/extension/dist/background.js.map +7 -0
- package/extension/dist/content.js +780 -0
- package/extension/dist/content.js.map +7 -0
- package/extension/manifest.json +32 -0
- package/package.json +37 -0
- package/skills/browser-bridge/SKILL.md +102 -0
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
// packages/extension/src/background.ts
|
|
2
|
+
var DEFAULT_CORE_PORT = 3210;
|
|
3
|
+
var CORE_PORT_KEY = "corePort";
|
|
4
|
+
var CORE_WS_PATH = "/drive";
|
|
5
|
+
var DEBUGGER_PROTOCOL_VERSION = "1.3";
|
|
6
|
+
var DEBUGGER_IDLE_TIMEOUT_KEY = "debuggerIdleTimeoutMs";
|
|
7
|
+
var DEFAULT_DEBUGGER_IDLE_TIMEOUT_MS = 15e3;
|
|
8
|
+
var DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS = 1e4;
|
|
9
|
+
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
10
|
+
var makeEventId = /* @__PURE__ */ (() => {
|
|
11
|
+
let counter = 0;
|
|
12
|
+
return () => `evt-${Date.now()}-${counter += 1}`;
|
|
13
|
+
})();
|
|
14
|
+
var lastActiveAtByTab = /* @__PURE__ */ new Map();
|
|
15
|
+
var ensureLastActiveAt = (tabId) => {
|
|
16
|
+
const existing = lastActiveAtByTab.get(tabId);
|
|
17
|
+
if (existing) {
|
|
18
|
+
return existing;
|
|
19
|
+
}
|
|
20
|
+
const timestamp = nowIso();
|
|
21
|
+
lastActiveAtByTab.set(tabId, timestamp);
|
|
22
|
+
return timestamp;
|
|
23
|
+
};
|
|
24
|
+
var markTabActive = (tabId) => {
|
|
25
|
+
const timestamp = nowIso();
|
|
26
|
+
lastActiveAtByTab.set(tabId, timestamp);
|
|
27
|
+
return timestamp;
|
|
28
|
+
};
|
|
29
|
+
var wrapChromeCallback = (invoker) => new Promise((resolve, reject) => {
|
|
30
|
+
invoker((value) => {
|
|
31
|
+
const error = chrome.runtime.lastError;
|
|
32
|
+
if (error) {
|
|
33
|
+
reject(new Error(error.message));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
resolve(value);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
var wrapChromeVoid = (invoker) => {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
invoker(() => {
|
|
42
|
+
const error = chrome.runtime.lastError;
|
|
43
|
+
if (error) {
|
|
44
|
+
reject(new Error(error.message));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
resolve();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
var readCorePort = async () => {
|
|
52
|
+
return await new Promise((resolve) => {
|
|
53
|
+
chrome.storage.local.get(
|
|
54
|
+
[CORE_PORT_KEY],
|
|
55
|
+
(result) => {
|
|
56
|
+
const raw = result?.[CORE_PORT_KEY];
|
|
57
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
58
|
+
resolve(raw);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (typeof raw === "string") {
|
|
62
|
+
const parsed = Number(raw);
|
|
63
|
+
if (Number.isFinite(parsed)) {
|
|
64
|
+
resolve(parsed);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
resolve(DEFAULT_CORE_PORT);
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
var readDebuggerIdleTimeoutMs = async () => {
|
|
74
|
+
return await new Promise((resolve) => {
|
|
75
|
+
chrome.storage.local.get(
|
|
76
|
+
[DEBUGGER_IDLE_TIMEOUT_KEY],
|
|
77
|
+
(result) => {
|
|
78
|
+
const raw = result?.[DEBUGGER_IDLE_TIMEOUT_KEY];
|
|
79
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
|
|
80
|
+
resolve(raw);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (typeof raw === "string") {
|
|
84
|
+
const parsed = Number(raw);
|
|
85
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
86
|
+
resolve(parsed);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
resolve(DEFAULT_DEBUGGER_IDLE_TIMEOUT_MS);
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
var RESTRICTED_URL_PREFIXES = [
|
|
96
|
+
"chrome://",
|
|
97
|
+
"chrome-extension://",
|
|
98
|
+
"chrome-devtools://",
|
|
99
|
+
"devtools://",
|
|
100
|
+
"edge://",
|
|
101
|
+
"brave://",
|
|
102
|
+
"view-source:"
|
|
103
|
+
];
|
|
104
|
+
var isRestrictedUrl = (url) => {
|
|
105
|
+
if (!url || typeof url !== "string") {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const lowered = url.toLowerCase();
|
|
109
|
+
if (RESTRICTED_URL_PREFIXES.some((prefix) => lowered.startsWith(prefix))) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const parsed = new URL(url);
|
|
114
|
+
if (parsed.hostname === "chromewebstore.google.com") {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (parsed.hostname === "chrome.google.com") {
|
|
118
|
+
return parsed.pathname.startsWith("/webstore");
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.debug("Ignoring invalid URL in restriction check.", error);
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
};
|
|
125
|
+
var mapDebuggerErrorMessage = (message, fallbackCode = "INSPECT_UNAVAILABLE") => {
|
|
126
|
+
const normalized = message.toLowerCase();
|
|
127
|
+
if (normalized.includes("already attached") || normalized.includes("another debugger") || normalized.includes("attached to this target")) {
|
|
128
|
+
return {
|
|
129
|
+
code: "DEBUGGER_IN_USE",
|
|
130
|
+
message: "Debugger already attached. Close DevTools on the target tab and retry.",
|
|
131
|
+
retryable: true,
|
|
132
|
+
details: {
|
|
133
|
+
reason: "debugger_in_use",
|
|
134
|
+
hint: "Close DevTools on the target tab and retry.",
|
|
135
|
+
original_message: message
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (normalized.includes("no tab") || normalized.includes("no target") || normalized.includes("tab id")) {
|
|
140
|
+
return {
|
|
141
|
+
code: "TAB_NOT_FOUND",
|
|
142
|
+
message,
|
|
143
|
+
retryable: false
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (normalized.includes("not allowed") || normalized.includes("permission") || normalized.includes("denied")) {
|
|
147
|
+
return {
|
|
148
|
+
code: "ATTACH_DENIED",
|
|
149
|
+
message,
|
|
150
|
+
retryable: false
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (normalized.includes("cannot access") || normalized.includes("not supported") || normalized.includes("disallowed")) {
|
|
154
|
+
return {
|
|
155
|
+
code: "NOT_SUPPORTED",
|
|
156
|
+
message,
|
|
157
|
+
retryable: false
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
code: fallbackCode,
|
|
162
|
+
message,
|
|
163
|
+
retryable: false
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
var buildTabInfo = (tab) => {
|
|
167
|
+
const tabId = tab.id;
|
|
168
|
+
const windowId = tab.windowId;
|
|
169
|
+
if (typeof tabId !== "number" || typeof windowId !== "number") {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
tab_id: tabId,
|
|
174
|
+
window_id: windowId,
|
|
175
|
+
url: typeof tab.url === "string" ? tab.url : void 0,
|
|
176
|
+
title: typeof tab.title === "string" ? tab.title : void 0,
|
|
177
|
+
active: typeof tab.active === "boolean" ? tab.active : void 0,
|
|
178
|
+
last_active_at: ensureLastActiveAt(tabId)
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
var queryTabs = async () => {
|
|
182
|
+
const tabs = await wrapChromeCallback(
|
|
183
|
+
(callback) => chrome.tabs.query({}, callback)
|
|
184
|
+
);
|
|
185
|
+
const result = [];
|
|
186
|
+
for (const tab of tabs) {
|
|
187
|
+
const info = buildTabInfo(tab);
|
|
188
|
+
if (info) {
|
|
189
|
+
result.push(info);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
};
|
|
194
|
+
var getTab = async (tabId) => {
|
|
195
|
+
return await wrapChromeCallback(
|
|
196
|
+
(callback) => chrome.tabs.get(tabId, callback)
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
var getActiveTabId = async () => {
|
|
200
|
+
const tabs = await wrapChromeCallback(
|
|
201
|
+
(callback) => chrome.tabs.query({ active: true, lastFocusedWindow: true }, callback)
|
|
202
|
+
);
|
|
203
|
+
const first = tabs[0];
|
|
204
|
+
if (first && typeof first.id === "number") {
|
|
205
|
+
return first.id;
|
|
206
|
+
}
|
|
207
|
+
throw new Error("No active tab found.");
|
|
208
|
+
};
|
|
209
|
+
var sendToTab = async (tabId, action, params) => {
|
|
210
|
+
return await new Promise((resolve) => {
|
|
211
|
+
const message = { action, params };
|
|
212
|
+
chrome.tabs.sendMessage(tabId, message, (response) => {
|
|
213
|
+
const error = chrome.runtime.lastError;
|
|
214
|
+
if (error) {
|
|
215
|
+
resolve({
|
|
216
|
+
ok: false,
|
|
217
|
+
error: {
|
|
218
|
+
code: "EVALUATION_FAILED",
|
|
219
|
+
message: error.message,
|
|
220
|
+
retryable: false
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (!response || typeof response !== "object") {
|
|
226
|
+
resolve({
|
|
227
|
+
ok: false,
|
|
228
|
+
error: {
|
|
229
|
+
code: "EVALUATION_FAILED",
|
|
230
|
+
message: "Empty response from content script.",
|
|
231
|
+
retryable: false
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
resolve(response);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
var waitForDomContentLoaded = async (tabId, timeoutMs) => {
|
|
241
|
+
return await new Promise((resolve, reject) => {
|
|
242
|
+
let timeout;
|
|
243
|
+
const cleanup = () => {
|
|
244
|
+
if (timeout !== void 0) {
|
|
245
|
+
clearTimeout(timeout);
|
|
246
|
+
}
|
|
247
|
+
chrome.webNavigation.onDOMContentLoaded.removeListener(listener);
|
|
248
|
+
};
|
|
249
|
+
const listener = (details) => {
|
|
250
|
+
if (details.tabId !== tabId || details.frameId !== 0) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
cleanup();
|
|
254
|
+
resolve();
|
|
255
|
+
};
|
|
256
|
+
chrome.webNavigation.onDOMContentLoaded.addListener(listener);
|
|
257
|
+
timeout = self.setTimeout(() => {
|
|
258
|
+
cleanup();
|
|
259
|
+
reject(new Error("Timed out waiting for domcontentloaded."));
|
|
260
|
+
}, timeoutMs);
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
var getWsUrl = async () => {
|
|
264
|
+
const port = await readCorePort();
|
|
265
|
+
return `ws://127.0.0.1:${port}${CORE_WS_PATH}`;
|
|
266
|
+
};
|
|
267
|
+
var DriveSocket = class {
|
|
268
|
+
constructor() {
|
|
269
|
+
this.socket = null;
|
|
270
|
+
this.reconnectTimer = null;
|
|
271
|
+
this.reconnectDelayMs = 1e3;
|
|
272
|
+
this.maxReconnectDelayMs = 1e4;
|
|
273
|
+
this.keepAliveTimer = null;
|
|
274
|
+
this.keepAliveIntervalMs = 3e4;
|
|
275
|
+
this.debuggerSessions = /* @__PURE__ */ new Map();
|
|
276
|
+
this.debuggerIdleTimeoutMs = null;
|
|
277
|
+
}
|
|
278
|
+
start() {
|
|
279
|
+
void this.connect().catch((error) => {
|
|
280
|
+
console.error("DriveSocket connect failed:", error);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
stop() {
|
|
284
|
+
if (this.reconnectTimer !== null) {
|
|
285
|
+
clearTimeout(this.reconnectTimer);
|
|
286
|
+
this.reconnectTimer = null;
|
|
287
|
+
}
|
|
288
|
+
this.stopKeepAlive();
|
|
289
|
+
if (this.socket) {
|
|
290
|
+
this.socket.close();
|
|
291
|
+
this.socket = null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
sendTabReport() {
|
|
295
|
+
void this.emitTabReport().catch((error) => {
|
|
296
|
+
console.error("DriveSocket emitTabReport failed:", error);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
scheduleReconnect() {
|
|
300
|
+
if (this.reconnectTimer !== null) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const delay = this.reconnectDelayMs;
|
|
304
|
+
this.reconnectTimer = self.setTimeout(() => {
|
|
305
|
+
this.reconnectTimer = null;
|
|
306
|
+
void this.connect().catch((error) => {
|
|
307
|
+
console.error("DriveSocket reconnect failed:", error);
|
|
308
|
+
});
|
|
309
|
+
}, delay);
|
|
310
|
+
this.reconnectDelayMs = Math.min(
|
|
311
|
+
this.maxReconnectDelayMs,
|
|
312
|
+
this.reconnectDelayMs * 2
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
async connect() {
|
|
316
|
+
const url = await getWsUrl();
|
|
317
|
+
try {
|
|
318
|
+
const socket2 = new WebSocket(url);
|
|
319
|
+
this.socket = socket2;
|
|
320
|
+
socket2.addEventListener("open", () => {
|
|
321
|
+
this.reconnectDelayMs = 1e3;
|
|
322
|
+
this.startKeepAlive();
|
|
323
|
+
void this.sendHello().catch((error) => {
|
|
324
|
+
console.error("DriveSocket hello failed:", error);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
socket2.addEventListener("message", (event) => {
|
|
328
|
+
this.handleMessage(event.data);
|
|
329
|
+
});
|
|
330
|
+
socket2.addEventListener("close", () => {
|
|
331
|
+
this.socket = null;
|
|
332
|
+
this.stopKeepAlive();
|
|
333
|
+
this.scheduleReconnect();
|
|
334
|
+
});
|
|
335
|
+
socket2.addEventListener("error", () => {
|
|
336
|
+
this.socket = null;
|
|
337
|
+
this.stopKeepAlive();
|
|
338
|
+
this.scheduleReconnect();
|
|
339
|
+
});
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.debug("DriveSocket connect failed, scheduling reconnect.", error);
|
|
342
|
+
this.scheduleReconnect();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async sendHello() {
|
|
346
|
+
const manifest = chrome.runtime.getManifest();
|
|
347
|
+
let tabs = [];
|
|
348
|
+
try {
|
|
349
|
+
tabs = await queryTabs();
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.debug("DriveSocket sendHello failed to read tabs.", error);
|
|
352
|
+
tabs = [];
|
|
353
|
+
}
|
|
354
|
+
const params = {
|
|
355
|
+
version: manifest.version,
|
|
356
|
+
tabs
|
|
357
|
+
};
|
|
358
|
+
this.sendEvent("drive.hello", params);
|
|
359
|
+
}
|
|
360
|
+
async emitTabReport() {
|
|
361
|
+
try {
|
|
362
|
+
const tabs = await queryTabs();
|
|
363
|
+
this.sendEvent("drive.tab_report", { tabs });
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.debug("DriveSocket emitTabReport failed.", error);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
sendEvent(action, params) {
|
|
369
|
+
const message = {
|
|
370
|
+
id: makeEventId(),
|
|
371
|
+
action,
|
|
372
|
+
status: "event",
|
|
373
|
+
params
|
|
374
|
+
};
|
|
375
|
+
this.sendMessage(message);
|
|
376
|
+
}
|
|
377
|
+
startKeepAlive() {
|
|
378
|
+
this.stopKeepAlive();
|
|
379
|
+
this.keepAliveTimer = self.setInterval(() => {
|
|
380
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
this.sendEvent("drive.keepalive", {});
|
|
384
|
+
}, this.keepAliveIntervalMs);
|
|
385
|
+
}
|
|
386
|
+
stopKeepAlive() {
|
|
387
|
+
if (this.keepAliveTimer !== null) {
|
|
388
|
+
clearInterval(this.keepAliveTimer);
|
|
389
|
+
this.keepAliveTimer = null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
sendDebuggerEvent(params) {
|
|
393
|
+
const message = {
|
|
394
|
+
id: makeEventId(),
|
|
395
|
+
action: "debugger.event",
|
|
396
|
+
status: "event",
|
|
397
|
+
params
|
|
398
|
+
};
|
|
399
|
+
this.sendMessage(message);
|
|
400
|
+
}
|
|
401
|
+
sendMessage(message) {
|
|
402
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
this.socket.send(JSON.stringify(message));
|
|
406
|
+
}
|
|
407
|
+
handleMessage(raw) {
|
|
408
|
+
if (typeof raw !== "string") {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
let message = null;
|
|
412
|
+
try {
|
|
413
|
+
message = JSON.parse(raw);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.debug("DriveSocket received invalid JSON message.", error);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (!message || typeof message !== "object") {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (message.status === "request") {
|
|
422
|
+
void this.handleRequest(message).catch((error) => {
|
|
423
|
+
console.error("DriveSocket handleRequest failed:", error);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async handleRequest(message) {
|
|
428
|
+
let driveMessage = null;
|
|
429
|
+
const respondOk = (result) => {
|
|
430
|
+
if (!driveMessage) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const response = {
|
|
434
|
+
id: driveMessage.id,
|
|
435
|
+
action: driveMessage.action,
|
|
436
|
+
status: "ok",
|
|
437
|
+
result
|
|
438
|
+
};
|
|
439
|
+
this.sendMessage(response);
|
|
440
|
+
};
|
|
441
|
+
const respondError = (error) => {
|
|
442
|
+
if (!driveMessage) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const response = {
|
|
446
|
+
id: driveMessage.id,
|
|
447
|
+
action: driveMessage.action,
|
|
448
|
+
status: "error",
|
|
449
|
+
error
|
|
450
|
+
};
|
|
451
|
+
this.sendMessage(response);
|
|
452
|
+
};
|
|
453
|
+
try {
|
|
454
|
+
if (!message || typeof message !== "object" || typeof message.id !== "string" || typeof message.action !== "string") {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (message.action.startsWith("debugger.")) {
|
|
458
|
+
await this.handleDebuggerRequest(message);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!message.action.startsWith("drive.")) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
driveMessage = message;
|
|
465
|
+
switch (message.action) {
|
|
466
|
+
case "drive.ping": {
|
|
467
|
+
respondOk({ ok: true });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
case "drive.navigate": {
|
|
471
|
+
const params = message.params ?? {};
|
|
472
|
+
const url = params.url;
|
|
473
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
474
|
+
respondError({
|
|
475
|
+
code: "INVALID_ARGUMENT",
|
|
476
|
+
message: "url must be a non-empty string.",
|
|
477
|
+
retryable: false
|
|
478
|
+
});
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
let tabId = params.tab_id;
|
|
482
|
+
if (tabId !== void 0 && typeof tabId !== "number") {
|
|
483
|
+
respondError({
|
|
484
|
+
code: "INVALID_ARGUMENT",
|
|
485
|
+
message: "tab_id must be a number when provided.",
|
|
486
|
+
retryable: false
|
|
487
|
+
});
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (tabId === void 0) {
|
|
491
|
+
tabId = await getActiveTabId();
|
|
492
|
+
}
|
|
493
|
+
const waitMode = params.wait === "none" || params.wait === "domcontentloaded" ? params.wait : "domcontentloaded";
|
|
494
|
+
await wrapChromeVoid(
|
|
495
|
+
(callback) => chrome.tabs.update(tabId, { url }, () => callback())
|
|
496
|
+
);
|
|
497
|
+
markTabActive(tabId);
|
|
498
|
+
if (waitMode === "domcontentloaded") {
|
|
499
|
+
try {
|
|
500
|
+
await waitForDomContentLoaded(tabId, 3e4);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
respondError({
|
|
503
|
+
code: "TIMEOUT",
|
|
504
|
+
message: error instanceof Error ? error.message : "Timed out waiting.",
|
|
505
|
+
retryable: true
|
|
506
|
+
});
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
respondOk({ ok: true });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
case "drive.go_back":
|
|
514
|
+
case "drive.back":
|
|
515
|
+
case "drive.go_forward":
|
|
516
|
+
case "drive.forward": {
|
|
517
|
+
const params = message.params ?? {};
|
|
518
|
+
let tabId = params.tab_id;
|
|
519
|
+
if (tabId !== void 0 && typeof tabId !== "number") {
|
|
520
|
+
respondError({
|
|
521
|
+
code: "INVALID_ARGUMENT",
|
|
522
|
+
message: "tab_id must be a number when provided.",
|
|
523
|
+
retryable: false
|
|
524
|
+
});
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (tabId === void 0) {
|
|
528
|
+
tabId = await getActiveTabId();
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
const isBack = message.action === "drive.go_back" || message.action === "drive.back";
|
|
532
|
+
await wrapChromeVoid((callback) => {
|
|
533
|
+
if (isBack) {
|
|
534
|
+
chrome.tabs.goBack(tabId, () => callback());
|
|
535
|
+
} else {
|
|
536
|
+
chrome.tabs.goForward(tabId, () => callback());
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
} catch (error) {
|
|
540
|
+
respondError({
|
|
541
|
+
code: "FAILED_PRECONDITION",
|
|
542
|
+
message: error instanceof Error ? error.message : "No history entry.",
|
|
543
|
+
retryable: false
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
markTabActive(tabId);
|
|
548
|
+
try {
|
|
549
|
+
await waitForDomContentLoaded(tabId, 3e4);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
respondError({
|
|
552
|
+
code: "TIMEOUT",
|
|
553
|
+
message: error instanceof Error ? error.message : "Timed out waiting.",
|
|
554
|
+
retryable: true
|
|
555
|
+
});
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
respondOk({ ok: true });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
case "drive.tab_list": {
|
|
562
|
+
const tabs = await queryTabs();
|
|
563
|
+
const result = { tabs };
|
|
564
|
+
respondOk(result);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
case "drive.tab_activate": {
|
|
568
|
+
const tabId = message.params?.tab_id;
|
|
569
|
+
if (typeof tabId !== "number") {
|
|
570
|
+
respondError({
|
|
571
|
+
code: "INVALID_ARGUMENT",
|
|
572
|
+
message: "tab_id must be a number.",
|
|
573
|
+
retryable: false
|
|
574
|
+
});
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const tab = await getTab(tabId);
|
|
578
|
+
await wrapChromeVoid(
|
|
579
|
+
(callback) => chrome.tabs.update(tabId, { active: true }, () => callback())
|
|
580
|
+
);
|
|
581
|
+
const windowId = tab.windowId;
|
|
582
|
+
if (typeof windowId === "number") {
|
|
583
|
+
await wrapChromeVoid(
|
|
584
|
+
(callback) => chrome.windows.update(
|
|
585
|
+
windowId,
|
|
586
|
+
{ focused: true },
|
|
587
|
+
() => callback()
|
|
588
|
+
)
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
markTabActive(tabId);
|
|
592
|
+
respondOk({ ok: true });
|
|
593
|
+
this.sendTabReport();
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
case "drive.tab_close": {
|
|
597
|
+
const tabId = message.params?.tab_id;
|
|
598
|
+
if (typeof tabId !== "number") {
|
|
599
|
+
respondError({
|
|
600
|
+
code: "INVALID_ARGUMENT",
|
|
601
|
+
message: "tab_id must be a number.",
|
|
602
|
+
retryable: false
|
|
603
|
+
});
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
await wrapChromeVoid(
|
|
607
|
+
(callback) => chrome.tabs.remove(tabId, () => callback())
|
|
608
|
+
);
|
|
609
|
+
lastActiveAtByTab.delete(tabId);
|
|
610
|
+
respondOk({ ok: true });
|
|
611
|
+
this.sendTabReport();
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
case "drive.handle_dialog": {
|
|
615
|
+
const params = message.params ?? {};
|
|
616
|
+
const action = params.action;
|
|
617
|
+
if (action !== "accept" && action !== "dismiss") {
|
|
618
|
+
respondError({
|
|
619
|
+
code: "INVALID_ARGUMENT",
|
|
620
|
+
message: "action must be accept or dismiss.",
|
|
621
|
+
retryable: false
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const promptText = params.promptText;
|
|
626
|
+
if (promptText !== void 0 && typeof promptText !== "string") {
|
|
627
|
+
respondError({
|
|
628
|
+
code: "INVALID_ARGUMENT",
|
|
629
|
+
message: "promptText must be a string when provided.",
|
|
630
|
+
retryable: false
|
|
631
|
+
});
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
let tabId = params.tab_id;
|
|
635
|
+
if (tabId !== void 0 && typeof tabId !== "number") {
|
|
636
|
+
respondError({
|
|
637
|
+
code: "INVALID_ARGUMENT",
|
|
638
|
+
message: "tab_id must be a number when provided.",
|
|
639
|
+
retryable: false
|
|
640
|
+
});
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (tabId === void 0) {
|
|
644
|
+
tabId = await getActiveTabId();
|
|
645
|
+
}
|
|
646
|
+
const error = await this.ensureDebuggerAttached(tabId);
|
|
647
|
+
if (error) {
|
|
648
|
+
respondError(error);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
await this.sendDebuggerCommand(
|
|
653
|
+
tabId,
|
|
654
|
+
"Page.handleJavaScriptDialog",
|
|
655
|
+
{
|
|
656
|
+
accept: action === "accept",
|
|
657
|
+
...promptText ? { promptText } : {}
|
|
658
|
+
},
|
|
659
|
+
DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS
|
|
660
|
+
);
|
|
661
|
+
this.touchDebuggerSession(tabId);
|
|
662
|
+
respondOk({ ok: true });
|
|
663
|
+
} catch (error2) {
|
|
664
|
+
const info = mapDebuggerErrorMessage(
|
|
665
|
+
error2 instanceof Error ? error2.message : "Dialog handling failed."
|
|
666
|
+
);
|
|
667
|
+
respondError(info);
|
|
668
|
+
}
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
case "drive.click":
|
|
672
|
+
case "drive.hover":
|
|
673
|
+
case "drive.select":
|
|
674
|
+
case "drive.type":
|
|
675
|
+
case "drive.fill_form":
|
|
676
|
+
case "drive.drag":
|
|
677
|
+
case "drive.key":
|
|
678
|
+
case "drive.key_press":
|
|
679
|
+
case "drive.scroll":
|
|
680
|
+
case "drive.wait_for": {
|
|
681
|
+
const params = message.params ?? {};
|
|
682
|
+
let tabId = params.tab_id;
|
|
683
|
+
if (tabId !== void 0 && typeof tabId !== "number") {
|
|
684
|
+
respondError({
|
|
685
|
+
code: "INVALID_ARGUMENT",
|
|
686
|
+
message: "tab_id must be a number when provided.",
|
|
687
|
+
retryable: false
|
|
688
|
+
});
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (tabId === void 0) {
|
|
692
|
+
tabId = await getActiveTabId();
|
|
693
|
+
}
|
|
694
|
+
const result = await sendToTab(
|
|
695
|
+
tabId,
|
|
696
|
+
message.action,
|
|
697
|
+
params
|
|
698
|
+
);
|
|
699
|
+
if (result.ok) {
|
|
700
|
+
respondOk(result.result ?? { ok: true });
|
|
701
|
+
} else {
|
|
702
|
+
respondError(result.error);
|
|
703
|
+
}
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
default:
|
|
707
|
+
respondError({
|
|
708
|
+
code: "NOT_IMPLEMENTED",
|
|
709
|
+
message: `${message.action} not implemented in extension yet.`,
|
|
710
|
+
retryable: false
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
} catch (error) {
|
|
714
|
+
const messageText = error instanceof Error ? error.message : "Unknown error";
|
|
715
|
+
respondError({
|
|
716
|
+
code: "EVALUATION_FAILED",
|
|
717
|
+
message: messageText,
|
|
718
|
+
retryable: false
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async handleDebuggerRequest(message) {
|
|
723
|
+
const respondAck = (result) => {
|
|
724
|
+
this.sendMessage({
|
|
725
|
+
id: message.id,
|
|
726
|
+
action: message.action,
|
|
727
|
+
status: "ack",
|
|
728
|
+
result
|
|
729
|
+
});
|
|
730
|
+
};
|
|
731
|
+
const respondError = (error) => {
|
|
732
|
+
this.sendMessage({
|
|
733
|
+
id: message.id,
|
|
734
|
+
action: message.action,
|
|
735
|
+
status: "error",
|
|
736
|
+
error
|
|
737
|
+
});
|
|
738
|
+
};
|
|
739
|
+
try {
|
|
740
|
+
switch (message.action) {
|
|
741
|
+
case "debugger.attach": {
|
|
742
|
+
const params = message.params ?? {};
|
|
743
|
+
const tabId = params.tab_id;
|
|
744
|
+
if (typeof tabId !== "number") {
|
|
745
|
+
respondError({
|
|
746
|
+
code: "INVALID_ARGUMENT",
|
|
747
|
+
message: "tab_id must be a number.",
|
|
748
|
+
retryable: false
|
|
749
|
+
});
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const error = await this.ensureDebuggerAttached(tabId);
|
|
753
|
+
if (error) {
|
|
754
|
+
respondError(error);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
respondAck({ ok: true });
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
case "debugger.detach": {
|
|
761
|
+
const params = message.params ?? {};
|
|
762
|
+
const tabId = params.tab_id;
|
|
763
|
+
if (typeof tabId !== "number") {
|
|
764
|
+
respondError({
|
|
765
|
+
code: "INVALID_ARGUMENT",
|
|
766
|
+
message: "tab_id must be a number.",
|
|
767
|
+
retryable: false
|
|
768
|
+
});
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const error = await this.detachDebugger(tabId);
|
|
772
|
+
if (error) {
|
|
773
|
+
respondError(error);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
respondAck({ ok: true });
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
case "debugger.command": {
|
|
780
|
+
const params = message.params ?? {};
|
|
781
|
+
const tabId = params.tab_id;
|
|
782
|
+
if (typeof tabId !== "number") {
|
|
783
|
+
respondError({
|
|
784
|
+
code: "INVALID_ARGUMENT",
|
|
785
|
+
message: "tab_id must be a number.",
|
|
786
|
+
retryable: false
|
|
787
|
+
});
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (typeof params.method !== "string" || params.method.length === 0) {
|
|
791
|
+
respondError({
|
|
792
|
+
code: "INVALID_ARGUMENT",
|
|
793
|
+
message: "method must be a non-empty string.",
|
|
794
|
+
retryable: false
|
|
795
|
+
});
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const session = this.debuggerSessions.get(tabId);
|
|
799
|
+
if (session?.attachPromise) {
|
|
800
|
+
try {
|
|
801
|
+
await session.attachPromise;
|
|
802
|
+
} catch (error) {
|
|
803
|
+
const info = mapDebuggerErrorMessage(
|
|
804
|
+
error instanceof Error ? error.message : "Debugger attach failed."
|
|
805
|
+
);
|
|
806
|
+
this.clearDebuggerSession(tabId);
|
|
807
|
+
respondError(info);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const attachedSession = this.debuggerSessions.get(tabId);
|
|
812
|
+
if (!attachedSession?.attached) {
|
|
813
|
+
respondError({
|
|
814
|
+
code: "FAILED_PRECONDITION",
|
|
815
|
+
message: "Debugger is not attached to the requested tab.",
|
|
816
|
+
retryable: false
|
|
817
|
+
});
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
const result = await this.sendDebuggerCommand(
|
|
822
|
+
tabId,
|
|
823
|
+
params.method,
|
|
824
|
+
params.params,
|
|
825
|
+
DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS
|
|
826
|
+
);
|
|
827
|
+
this.touchDebuggerSession(tabId);
|
|
828
|
+
respondAck(result);
|
|
829
|
+
} catch (error) {
|
|
830
|
+
if (error instanceof DebuggerTimeoutError) {
|
|
831
|
+
respondError({
|
|
832
|
+
code: "TIMEOUT",
|
|
833
|
+
message: error.message,
|
|
834
|
+
retryable: true
|
|
835
|
+
});
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const info = mapDebuggerErrorMessage(
|
|
839
|
+
error instanceof Error ? error.message : "Debugger command failed."
|
|
840
|
+
);
|
|
841
|
+
respondError(info);
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
default:
|
|
846
|
+
respondError({
|
|
847
|
+
code: "NOT_IMPLEMENTED",
|
|
848
|
+
message: `${message.action} not implemented in extension yet.`,
|
|
849
|
+
retryable: false
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
} catch (error) {
|
|
853
|
+
const messageText = error instanceof Error ? error.message : "Unexpected debugger error.";
|
|
854
|
+
respondError({
|
|
855
|
+
code: "INSPECT_UNAVAILABLE",
|
|
856
|
+
message: messageText,
|
|
857
|
+
retryable: false
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async handleDebuggerEvent(source, method, params) {
|
|
862
|
+
const tabId = source.tabId;
|
|
863
|
+
if (typeof tabId !== "number") {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
this.touchDebuggerSession(tabId);
|
|
867
|
+
this.sendDebuggerEvent({
|
|
868
|
+
tab_id: tabId,
|
|
869
|
+
method,
|
|
870
|
+
params,
|
|
871
|
+
timestamp: nowIso()
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
async handleDebuggerDetach(source, reason) {
|
|
875
|
+
const tabId = source.tabId;
|
|
876
|
+
if (typeof tabId !== "number") {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
this.clearDebuggerSession(tabId);
|
|
880
|
+
this.sendDebuggerEvent({
|
|
881
|
+
tab_id: tabId,
|
|
882
|
+
method: "Debugger.detached",
|
|
883
|
+
params: { reason: reason ?? "unknown" },
|
|
884
|
+
timestamp: nowIso()
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
async ensureDebuggerAttached(tabId) {
|
|
888
|
+
const existing = this.debuggerSessions.get(tabId);
|
|
889
|
+
if (existing?.attached) {
|
|
890
|
+
this.touchDebuggerSession(tabId);
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
if (existing?.attachPromise) {
|
|
894
|
+
try {
|
|
895
|
+
await existing.attachPromise;
|
|
896
|
+
} catch (error) {
|
|
897
|
+
const info = mapDebuggerErrorMessage(
|
|
898
|
+
error instanceof Error ? error.message : "Debugger attach failed."
|
|
899
|
+
);
|
|
900
|
+
this.clearDebuggerSession(tabId);
|
|
901
|
+
return info;
|
|
902
|
+
}
|
|
903
|
+
if (existing.attached) {
|
|
904
|
+
this.touchDebuggerSession(tabId);
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const preflightError = await this.checkDebuggerTarget(tabId);
|
|
909
|
+
if (preflightError) {
|
|
910
|
+
return preflightError;
|
|
911
|
+
}
|
|
912
|
+
const session = {
|
|
913
|
+
attached: false,
|
|
914
|
+
lastActivityAt: nowIso()
|
|
915
|
+
};
|
|
916
|
+
this.debuggerSessions.set(tabId, session);
|
|
917
|
+
session.attachPromise = wrapChromeVoid(
|
|
918
|
+
(callback) => chrome.debugger.attach(
|
|
919
|
+
{ tabId },
|
|
920
|
+
DEBUGGER_PROTOCOL_VERSION,
|
|
921
|
+
() => callback()
|
|
922
|
+
)
|
|
923
|
+
);
|
|
924
|
+
try {
|
|
925
|
+
await session.attachPromise;
|
|
926
|
+
session.attached = true;
|
|
927
|
+
session.attachPromise = void 0;
|
|
928
|
+
const initError = await this.initializeDebuggerDomains(tabId);
|
|
929
|
+
if (initError) {
|
|
930
|
+
await this.detachDebugger(tabId);
|
|
931
|
+
return initError;
|
|
932
|
+
}
|
|
933
|
+
session.initialized = true;
|
|
934
|
+
this.touchDebuggerSession(tabId);
|
|
935
|
+
return null;
|
|
936
|
+
} catch (error) {
|
|
937
|
+
const info = mapDebuggerErrorMessage(
|
|
938
|
+
error instanceof Error ? error.message : "Debugger attach failed."
|
|
939
|
+
);
|
|
940
|
+
this.clearDebuggerSession(tabId);
|
|
941
|
+
return info;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
async initializeDebuggerDomains(tabId) {
|
|
945
|
+
const methods = [
|
|
946
|
+
["Page.enable", void 0],
|
|
947
|
+
["Runtime.enable", void 0],
|
|
948
|
+
["Log.enable", void 0],
|
|
949
|
+
["Network.enable", void 0]
|
|
950
|
+
];
|
|
951
|
+
try {
|
|
952
|
+
for (const [method, params] of methods) {
|
|
953
|
+
await this.sendDebuggerCommand(
|
|
954
|
+
tabId,
|
|
955
|
+
method,
|
|
956
|
+
params,
|
|
957
|
+
DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
} catch (error) {
|
|
962
|
+
return mapDebuggerErrorMessage(
|
|
963
|
+
error instanceof Error ? error.message : "Debugger initialization failed."
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
async checkDebuggerTarget(tabId) {
|
|
968
|
+
try {
|
|
969
|
+
const tab = await getTab(tabId);
|
|
970
|
+
const url = typeof tab.url === "string" ? tab.url : void 0;
|
|
971
|
+
if (isRestrictedUrl(url)) {
|
|
972
|
+
return {
|
|
973
|
+
code: "NOT_SUPPORTED",
|
|
974
|
+
message: "Debugger cannot attach to restricted pages.",
|
|
975
|
+
retryable: false,
|
|
976
|
+
details: { url }
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
} catch (error) {
|
|
980
|
+
return mapDebuggerErrorMessage(
|
|
981
|
+
error instanceof Error ? error.message : "Failed to locate tab.",
|
|
982
|
+
"TAB_NOT_FOUND"
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
async detachDebugger(tabId) {
|
|
988
|
+
const session = this.debuggerSessions.get(tabId);
|
|
989
|
+
if (!session) {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
if (session.attachPromise) {
|
|
993
|
+
try {
|
|
994
|
+
await session.attachPromise;
|
|
995
|
+
} catch (error) {
|
|
996
|
+
console.debug("Debugger attach promise failed before detach.", error);
|
|
997
|
+
this.clearDebuggerSession(tabId);
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (!session.attached) {
|
|
1002
|
+
this.clearDebuggerSession(tabId);
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
try {
|
|
1006
|
+
await wrapChromeVoid(
|
|
1007
|
+
(callback) => chrome.debugger.detach({ tabId }, () => callback())
|
|
1008
|
+
);
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
return mapDebuggerErrorMessage(
|
|
1011
|
+
error instanceof Error ? error.message : "Debugger detach failed."
|
|
1012
|
+
);
|
|
1013
|
+
} finally {
|
|
1014
|
+
this.clearDebuggerSession(tabId);
|
|
1015
|
+
}
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
async sendDebuggerCommand(tabId, method, params, timeoutMs) {
|
|
1019
|
+
return await new Promise((resolve, reject) => {
|
|
1020
|
+
let finished = false;
|
|
1021
|
+
const timeout = self.setTimeout(() => {
|
|
1022
|
+
finished = true;
|
|
1023
|
+
reject(new DebuggerTimeoutError(timeoutMs));
|
|
1024
|
+
}, timeoutMs);
|
|
1025
|
+
chrome.debugger.sendCommand(
|
|
1026
|
+
{ tabId },
|
|
1027
|
+
method,
|
|
1028
|
+
params ?? {},
|
|
1029
|
+
(result) => {
|
|
1030
|
+
if (finished) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
finished = true;
|
|
1034
|
+
clearTimeout(timeout);
|
|
1035
|
+
const error = chrome.runtime.lastError;
|
|
1036
|
+
if (error) {
|
|
1037
|
+
reject(new Error(error.message));
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
resolve(result);
|
|
1041
|
+
}
|
|
1042
|
+
);
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
touchDebuggerSession(tabId) {
|
|
1046
|
+
const session = this.debuggerSessions.get(tabId);
|
|
1047
|
+
if (!session) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
session.lastActivityAt = nowIso();
|
|
1051
|
+
void this.refreshDebuggerIdleTimer(tabId).catch((error) => {
|
|
1052
|
+
console.error("DriveSocket refreshDebuggerIdleTimer failed:", error);
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
async refreshDebuggerIdleTimer(tabId) {
|
|
1056
|
+
const session = this.debuggerSessions.get(tabId);
|
|
1057
|
+
if (!session) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (session.idleTimer) {
|
|
1061
|
+
clearTimeout(session.idleTimer);
|
|
1062
|
+
}
|
|
1063
|
+
const timeoutMs = await this.getDebuggerIdleTimeoutMs();
|
|
1064
|
+
session.idleTimer = self.setTimeout(() => {
|
|
1065
|
+
void this.detachDebugger(tabId).catch((error) => {
|
|
1066
|
+
console.error("DriveSocket detachDebugger failed:", error);
|
|
1067
|
+
});
|
|
1068
|
+
}, timeoutMs);
|
|
1069
|
+
}
|
|
1070
|
+
clearDebuggerSession(tabId) {
|
|
1071
|
+
const session = this.debuggerSessions.get(tabId);
|
|
1072
|
+
if (!session) {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
if (session.idleTimer) {
|
|
1076
|
+
clearTimeout(session.idleTimer);
|
|
1077
|
+
}
|
|
1078
|
+
this.debuggerSessions.delete(tabId);
|
|
1079
|
+
}
|
|
1080
|
+
async getDebuggerIdleTimeoutMs() {
|
|
1081
|
+
if (this.debuggerIdleTimeoutMs !== null) {
|
|
1082
|
+
return this.debuggerIdleTimeoutMs;
|
|
1083
|
+
}
|
|
1084
|
+
const timeout = await readDebuggerIdleTimeoutMs();
|
|
1085
|
+
this.debuggerIdleTimeoutMs = timeout;
|
|
1086
|
+
return timeout;
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
var DebuggerTimeoutError = class extends Error {
|
|
1090
|
+
constructor(timeoutMs) {
|
|
1091
|
+
super(`Debugger command timed out after ${timeoutMs}ms.`);
|
|
1092
|
+
this.name = "DebuggerTimeoutError";
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
var socket = new DriveSocket();
|
|
1096
|
+
chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
1097
|
+
markTabActive(activeInfo.tabId);
|
|
1098
|
+
socket.sendTabReport();
|
|
1099
|
+
});
|
|
1100
|
+
chrome.tabs.onCreated.addListener((tab) => {
|
|
1101
|
+
if (typeof tab.id === "number") {
|
|
1102
|
+
ensureLastActiveAt(tab.id);
|
|
1103
|
+
}
|
|
1104
|
+
socket.sendTabReport();
|
|
1105
|
+
});
|
|
1106
|
+
chrome.tabs.onUpdated.addListener(
|
|
1107
|
+
(tabId, changeInfo, tab) => {
|
|
1108
|
+
const shouldReport = Boolean(changeInfo.url) || Boolean(changeInfo.title) || changeInfo.status === "complete";
|
|
1109
|
+
if (!shouldReport) {
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (tab && tab.active) {
|
|
1113
|
+
markTabActive(tabId);
|
|
1114
|
+
}
|
|
1115
|
+
socket.sendTabReport();
|
|
1116
|
+
}
|
|
1117
|
+
);
|
|
1118
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
1119
|
+
lastActiveAtByTab.delete(tabId);
|
|
1120
|
+
socket.sendTabReport();
|
|
1121
|
+
});
|
|
1122
|
+
chrome.debugger.onEvent.addListener(
|
|
1123
|
+
(source, method, params) => {
|
|
1124
|
+
void socket.handleDebuggerEvent(source, method, params).catch((error) => {
|
|
1125
|
+
console.error("DriveSocket handleDebuggerEvent failed:", error);
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
);
|
|
1129
|
+
chrome.debugger.onDetach.addListener(
|
|
1130
|
+
(source, reason) => {
|
|
1131
|
+
void socket.handleDebuggerDetach(source, reason).catch((error) => {
|
|
1132
|
+
console.error("DriveSocket handleDebuggerDetach failed:", error);
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
);
|
|
1136
|
+
socket.start();
|
|
1137
|
+
//# sourceMappingURL=background.js.map
|