pi-browser-harness 0.12.0 → 0.13.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/CHANGELOG.md +36 -0
- package/README.md +86 -0
- package/package.json +12 -6
- package/skills/pi-browser-harness/SKILL.md +57 -2
- package/src/cdp/commands.ts +200 -14
- package/src/cdp/console-buffer.ts +37 -23
- package/src/cdp/discovery.ts +57 -25
- package/src/cdp/events.ts +8 -3
- package/src/cdp/network-buffer.ts +38 -32
- package/src/cdp/ownership.ts +22 -4
- package/src/cdp/record-store.ts +2 -2
- package/src/cdp/session.ts +320 -217
- package/src/cdp/target-factory.ts +34 -15
- package/src/cdp/types.ts +17 -1
- package/src/client.ts +76 -43
- package/src/daemon/bridge.ts +110 -349
- package/src/daemon/connection.ts +323 -0
- package/src/daemon/index.ts +77 -48
- package/src/daemon/lines.ts +40 -0
- package/src/daemon/main.ts +12 -0
- package/src/daemon/protocol.ts +55 -25
- package/src/daemon/server.ts +100 -63
- package/src/daemon/socket.ts +10 -0
- package/src/daemon/spawn.ts +21 -22
- package/src/daemon/transport.ts +80 -49
- package/src/domains/ax-tree.ts +8 -8
- package/src/domains/cdp-call.ts +14 -7
- package/src/domains/click.ts +2 -2
- package/src/domains/concurrency.ts +50 -0
- package/src/domains/console.ts +20 -64
- package/src/domains/drag.ts +26 -28
- package/src/domains/element-call.ts +102 -68
- package/src/domains/files.ts +13 -9
- package/src/domains/form-batch.ts +13 -10
- package/src/domains/form.ts +78 -107
- package/src/domains/headers.ts +2 -2
- package/src/domains/history.ts +4 -4
- package/src/domains/io.ts +35 -0
- package/src/domains/isolated-tab.ts +6 -12
- package/src/domains/js.ts +65 -44
- package/src/domains/keyboard.ts +14 -11
- package/src/domains/navigate.ts +102 -66
- package/src/domains/network.ts +55 -95
- package/src/domains/page/fill.page.ts +139 -0
- package/src/domains/page/form.page.ts +61 -0
- package/src/domains/page/inject.ts +37 -0
- package/src/domains/page/keyboard.page.ts +16 -0
- package/src/domains/page/live-value.page.ts +10 -0
- package/src/domains/page/read-page.page.ts +51 -0
- package/src/domains/page/serp.page.ts +39 -0
- package/src/domains/page.ts +18 -51
- package/src/domains/poll.ts +42 -0
- package/src/domains/readpage/read-page.ts +48 -53
- package/src/domains/ref-resolve.ts +43 -29
- package/src/domains/ref-signature.ts +11 -0
- package/src/domains/render.ts +12 -23
- package/src/domains/screenshot-capture.ts +4 -2
- package/src/domains/screenshot.ts +13 -10
- package/src/domains/scroll.ts +4 -4
- package/src/domains/search/google-serp.ts +1 -1
- package/src/domains/search/web-search.ts +38 -33
- package/src/domains/snapshot.ts +71 -73
- package/src/domains/tab-resolve.ts +36 -0
- package/src/domains/tabs.ts +18 -69
- package/src/domains/target.ts +1 -1
- package/src/index.ts +90 -33
- package/src/profile/bind.ts +45 -14
- package/src/profile/browser-process.ts +50 -12
- package/src/profile/command.ts +20 -12
- package/src/profile/launch.ts +13 -9
- package/src/profile/list.ts +9 -13
- package/src/profile/store.ts +2 -1
- package/src/prompt.ts +32 -7
- package/src/registry.ts +65 -4
- package/src/schemas/parse.ts +2 -2
- package/src/setup.ts +28 -19
- package/src/state.ts +2 -1
- package/src/util/debug.ts +7 -1
- package/src/util/guards.ts +3 -0
- package/src/util/mutex.ts +1 -1
- package/src/util/paths.ts +3 -2
- package/src/util/result.ts +8 -0
- package/src/util/sharp-shim.ts +18 -11
- package/src/util/sleep.ts +18 -0
- package/src/util/tool.ts +163 -114
- package/src/util/truncate.ts +1 -1
- package/tsconfig.json +2 -1
- package/src/domains/fill-engine.ts +0 -80
package/src/daemon/bridge.ts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
import WebSocket from "ws";
|
|
2
|
-
import { discoverWsUrl } from "../cdp/discovery";
|
|
3
1
|
import { isCdpRawMessage } from "../cdp/types";
|
|
4
2
|
import type { CdpRawMessage } from "../cdp/types";
|
|
5
3
|
import type { WireRequest, WireResponse, WireEvent } from "./protocol";
|
|
6
|
-
import {
|
|
4
|
+
import { CDP_COMMAND_TIMEOUT_MS, DAEMON_RECONNECT_METHOD } from "./protocol";
|
|
7
5
|
import { asString, isRecord } from "../util/guards";
|
|
8
|
-
import type
|
|
9
|
-
|
|
6
|
+
import { type ChromeConnectionDependencies, type ConnectionStatus, createChromeConnection } from "./connection";
|
|
7
|
+
|
|
8
|
+
// The text a tool sees when the daemon has no Chrome connection to offer; it tells the model
|
|
9
|
+
// (and through it the user) whether to wait, to look at Chrome, or to run /browser-setup.
|
|
10
|
+
export const describeDisconnected = (status: ConnectionStatus): string => {
|
|
11
|
+
switch (status.state) {
|
|
12
|
+
case "open":
|
|
13
|
+
return "Chrome not connected";
|
|
14
|
+
case "connecting":
|
|
15
|
+
return `Chrome not connected: a connection attempt has been waiting ${Math.round(status.pendingForMs / 1000)}s. `
|
|
16
|
+
+ "If Chrome is showing an \"Allow remote debugging\" prompt, accept it; no further prompts will be opened while this attempt is pending.";
|
|
17
|
+
case "cooldown":
|
|
18
|
+
return `Chrome not connected: the last attempt failed (${status.lastFailure}). `
|
|
19
|
+
+ `The next automatic attempt is in ${Math.ceil(status.retryInMs / 1000)}s; run /browser-setup to retry now.`;
|
|
20
|
+
case "disconnected":
|
|
21
|
+
return status.lastFailure ? `Chrome not connected: ${status.lastFailure}` : "Chrome not connected";
|
|
22
|
+
case "stopped":
|
|
23
|
+
return "Chrome not connected: the daemon is shutting down";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
10
26
|
|
|
11
27
|
export type SendToClient = (clientId: string, msg: WireResponse | WireEvent) => void;
|
|
12
28
|
|
|
@@ -15,14 +31,16 @@ export type EventHandler = (event: WireEvent, targetClientIds: string[]) => void
|
|
|
15
31
|
export type CloseHandler = () => void;
|
|
16
32
|
|
|
17
33
|
export type CdpBridge = {
|
|
18
|
-
start()
|
|
19
|
-
stop()
|
|
20
|
-
handleRequest(req: WireRequest, clientId: string, send: SendToClient)
|
|
21
|
-
isAlive()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
readonly start: () => Promise<void>;
|
|
35
|
+
readonly stop: () => Promise<void>;
|
|
36
|
+
readonly handleRequest: (req: WireRequest, clientId: string, send: SendToClient) => Promise<void>;
|
|
37
|
+
readonly isAlive: () => boolean;
|
|
38
|
+
// Not used by the daemon itself; test/manual/bridge-test.ts asserts session ownership through it.
|
|
39
|
+
readonly getSessionOwner: (sessionId: string) => string | undefined;
|
|
40
|
+
readonly removeClient: (clientId: string) => void;
|
|
41
|
+
readonly onEvent: (handler: EventHandler) => void;
|
|
42
|
+
// Fires when an established Chrome connection drops; never for an attempt that failed to open.
|
|
43
|
+
readonly onClose: (handler: CloseHandler) => void;
|
|
26
44
|
};
|
|
27
45
|
|
|
28
46
|
export type InFlight = {
|
|
@@ -31,14 +49,16 @@ export type InFlight = {
|
|
|
31
49
|
readonly send: SendToClient;
|
|
32
50
|
readonly timer: ReturnType<typeof setTimeout>;
|
|
33
51
|
readonly isAttach: boolean;
|
|
52
|
+
// The Chrome connection the request went out on, so that only its loss fails the request.
|
|
53
|
+
readonly generation: number;
|
|
34
54
|
};
|
|
35
55
|
|
|
36
56
|
// One map, keyed by the daemon-side id: the client that asked, the id it used, and everything needed to answer it.
|
|
37
57
|
export type IdMultiplexer = {
|
|
38
|
-
allocate(entry: Omit<InFlight, "timer">, arm: (daemonId: number) => InFlight["timer"])
|
|
39
|
-
take(daemonId: number)
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
readonly allocate: (entry: Omit<InFlight, "timer">, arm: (daemonId: number) => InFlight["timer"]) => number;
|
|
59
|
+
readonly take: (daemonId: number) => InFlight | undefined;
|
|
60
|
+
// Removes every matching entry and cancels its timeout, so nothing taken can be answered later.
|
|
61
|
+
readonly takeWhere: (pred: (entry: InFlight) => boolean) => ReadonlyArray<InFlight>;
|
|
42
62
|
};
|
|
43
63
|
|
|
44
64
|
export const createIdMultiplexer = (): IdMultiplexer => {
|
|
@@ -58,29 +78,25 @@ export const createIdMultiplexer = (): IdMultiplexer => {
|
|
|
58
78
|
clearTimeout(e.timer);
|
|
59
79
|
return e;
|
|
60
80
|
},
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
inFlight.clear();
|
|
64
|
-
for (const e of all) clearTimeout(e.timer);
|
|
65
|
-
return all;
|
|
66
|
-
},
|
|
67
|
-
// A disconnected client must not be answered later: dropping its entries also cancels their timeouts.
|
|
68
|
-
clearClient(clientId) {
|
|
81
|
+
takeWhere(pred) {
|
|
82
|
+
const taken: InFlight[] = [];
|
|
69
83
|
for (const [daemonId, e] of inFlight) {
|
|
70
|
-
if (e
|
|
84
|
+
if (!pred(e)) continue;
|
|
71
85
|
clearTimeout(e.timer);
|
|
72
86
|
inFlight.delete(daemonId);
|
|
87
|
+
taken.push(e);
|
|
73
88
|
}
|
|
89
|
+
return taken;
|
|
74
90
|
},
|
|
75
91
|
};
|
|
76
92
|
};
|
|
77
93
|
|
|
78
94
|
export type EventRouter = {
|
|
79
|
-
record(clientId: string, sessionId: string)
|
|
80
|
-
release(sessionId: string)
|
|
81
|
-
getOwner(sessionId: string)
|
|
82
|
-
removeClient(clientId: string)
|
|
83
|
-
route(event: WireEvent)
|
|
95
|
+
readonly record: (clientId: string, sessionId: string) => void;
|
|
96
|
+
readonly release: (sessionId: string) => void;
|
|
97
|
+
readonly getOwner: (sessionId: string) => string | undefined;
|
|
98
|
+
readonly removeClient: (clientId: string) => void;
|
|
99
|
+
readonly route: (event: WireEvent) => string[];
|
|
84
100
|
};
|
|
85
101
|
|
|
86
102
|
export const createEventRouter = (): EventRouter => {
|
|
@@ -119,39 +135,22 @@ export const createEventRouter = (): EventRouter => {
|
|
|
119
135
|
};
|
|
120
136
|
};
|
|
121
137
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
};
|
|
138
|
+
const errorResponse = (id: number, message: string): WireResponse => ({
|
|
139
|
+
type: "response",
|
|
140
|
+
id,
|
|
141
|
+
error: { code: -32000, message },
|
|
142
|
+
});
|
|
128
143
|
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
createWebSocket: createWebSocketImpl = (url: string): WebSocket =>
|
|
133
|
-
new WebSocket(url, { perMessageDeflate: false }),
|
|
134
|
-
connectTimeoutMs = CDP_CONNECT_TIMEOUT_MS,
|
|
135
|
-
onAttemptSettled,
|
|
136
|
-
} = options;
|
|
144
|
+
const failAll = (entries: ReadonlyArray<InFlight>, message: string): void => {
|
|
145
|
+
for (const e of entries) e.send(e.clientId, errorResponse(e.localId, message));
|
|
146
|
+
};
|
|
137
147
|
|
|
138
|
-
|
|
139
|
-
let wsUrl: string | null = null;
|
|
148
|
+
export const createCdpBridge = (options: ChromeConnectionDependencies = {}): CdpBridge => {
|
|
140
149
|
const mux = createIdMultiplexer();
|
|
141
150
|
const router = createEventRouter();
|
|
142
151
|
let eventHandler: EventHandler | null = null;
|
|
143
152
|
let closeHandler: CloseHandler | null = null;
|
|
144
153
|
|
|
145
|
-
// daemonId -> { generation, clientId }: lets a failed attempt reject only its own
|
|
146
|
-
// in-flight requests, and a gone client's entries be dropped without late answers.
|
|
147
|
-
const generations = new Map<number, { generation: number; clientId: string }>();
|
|
148
|
-
|
|
149
|
-
let stopped = false;
|
|
150
|
-
let connecting = false;
|
|
151
|
-
let attemptGeneration = 0;
|
|
152
|
-
let activeAttempt = 0;
|
|
153
|
-
let activeSocket: WebSocket | null = null;
|
|
154
|
-
|
|
155
154
|
const onChromeMessage = (raw: string): void => {
|
|
156
155
|
let parsed: unknown;
|
|
157
156
|
try { parsed = JSON.parse(raw); } catch { return; }
|
|
@@ -159,26 +158,22 @@ export const createCdpBridge = (options: CdpBridgeDependencies = {}): CdpBridge
|
|
|
159
158
|
const msg: CdpRawMessage = parsed;
|
|
160
159
|
|
|
161
160
|
if (msg.id !== undefined) {
|
|
162
|
-
generations.delete(msg.id);
|
|
163
161
|
const cb = mux.take(msg.id);
|
|
164
162
|
if (!cb) return;
|
|
165
|
-
const localId = cb.localId;
|
|
166
163
|
|
|
167
164
|
if (msg.error) {
|
|
168
165
|
cb.send(cb.clientId, {
|
|
169
166
|
type: "response",
|
|
170
|
-
id: localId,
|
|
167
|
+
id: cb.localId,
|
|
171
168
|
error: { code: msg.error.code ?? -1, message: msg.error.message },
|
|
172
169
|
});
|
|
173
|
-
|
|
174
|
-
if (cb.isAttach && msg.result) {
|
|
175
|
-
const sessionId = isRecord(msg.result) ? asString(msg.result["sessionId"]) : undefined;
|
|
176
|
-
if (sessionId) {
|
|
177
|
-
router.record(cb.clientId, sessionId);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
cb.send(cb.clientId, { type: "response", id: localId, result: msg.result });
|
|
170
|
+
return;
|
|
181
171
|
}
|
|
172
|
+
if (cb.isAttach && isRecord(msg.result)) {
|
|
173
|
+
const sessionId = asString(msg.result["sessionId"]);
|
|
174
|
+
if (sessionId !== undefined) router.record(cb.clientId, sessionId);
|
|
175
|
+
}
|
|
176
|
+
cb.send(cb.clientId, { type: "response", id: cb.localId, result: msg.result });
|
|
182
177
|
return;
|
|
183
178
|
}
|
|
184
179
|
|
|
@@ -203,245 +198,27 @@ export const createCdpBridge = (options: CdpBridgeDependencies = {}): CdpBridge
|
|
|
203
198
|
}
|
|
204
199
|
};
|
|
205
200
|
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
type: "response",
|
|
216
|
-
id: cb.localId,
|
|
217
|
-
error: { code: -32000, message: "Chrome disconnected" },
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const rejectAttemptCallbacks = (generation: number): void => {
|
|
223
|
-
rejectCallbacksWhere((info) => info.generation === generation);
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const rejectAllPendingCallbacks = (): void => {
|
|
227
|
-
rejectCallbacksWhere(() => true);
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const closeSocket = (socket: WebSocket): void => {
|
|
231
|
-
try {
|
|
232
|
-
socket.close();
|
|
233
|
-
} catch {}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const isCurrentAttempt = (generation: number): boolean =>
|
|
237
|
-
generation === activeAttempt && !stopped;
|
|
238
|
-
|
|
239
|
-
const isCurrentAttemptSocket = (generation: number, socket: WebSocket): boolean =>
|
|
240
|
-
isCurrentAttempt(generation) && activeSocket === socket;
|
|
241
|
-
|
|
242
|
-
const clearActiveAttempt = (generation: number): void => {
|
|
243
|
-
if (activeAttempt === generation) {
|
|
244
|
-
activeAttempt = 0;
|
|
245
|
-
activeSocket = null;
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
// On-demand Chrome connection. Attempts happen only from start() and from
|
|
250
|
-
// handleRequest() while disconnected; a failed or disconnected bridge stays
|
|
251
|
-
// disconnected until the next explicit browser demand. No background retry loop.
|
|
252
|
-
const tryConnect = (): void => {
|
|
253
|
-
if (stopped || connecting || (ws && ws.readyState === WebSocket.OPEN)) return;
|
|
254
|
-
|
|
255
|
-
const generation = ++attemptGeneration;
|
|
256
|
-
activeAttempt = generation;
|
|
257
|
-
connecting = true;
|
|
258
|
-
|
|
259
|
-
const attempt = async (): Promise<void> => {
|
|
260
|
-
let settled = false;
|
|
261
|
-
let sock: WebSocket | null = null;
|
|
262
|
-
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
263
|
-
let settleAttempt: () => void = () => {};
|
|
264
|
-
|
|
265
|
-
const settledPromise = new Promise<void>((resolve) => {
|
|
266
|
-
settleAttempt = (): void => {
|
|
267
|
-
if (settled) return;
|
|
268
|
-
settled = true;
|
|
269
|
-
if (timeout) {
|
|
270
|
-
clearTimeout(timeout);
|
|
271
|
-
timeout = null;
|
|
272
|
-
}
|
|
273
|
-
if (generation === activeAttempt && !stopped) {
|
|
274
|
-
wsUrl = null;
|
|
275
|
-
connecting = false;
|
|
276
|
-
}
|
|
277
|
-
rejectAttemptCallbacks(generation);
|
|
278
|
-
onAttemptSettled?.();
|
|
279
|
-
resolve();
|
|
280
|
-
};
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
const clearAttemptTimeout = (): void => {
|
|
284
|
-
if (timeout) {
|
|
285
|
-
clearTimeout(timeout);
|
|
286
|
-
timeout = null;
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
const failCurrentAttempt = (notifyClose: boolean): void => {
|
|
291
|
-
clearAttemptTimeout();
|
|
292
|
-
const isCurrent = isCurrentAttempt(generation);
|
|
293
|
-
if (!isCurrent) {
|
|
294
|
-
rejectAttemptCallbacks(generation);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
clearActiveAttempt(generation);
|
|
299
|
-
if (ws === sock) ws = null;
|
|
300
|
-
wsUrl = null;
|
|
301
|
-
connecting = false;
|
|
302
|
-
// Not via settleAttempt: a connection that opened already settled its attempt, so
|
|
303
|
-
// its requests would otherwise hang to the command timeout instead of failing now.
|
|
304
|
-
rejectAttemptCallbacks(generation);
|
|
305
|
-
if (notifyClose) closeHandler?.();
|
|
306
|
-
settleAttempt();
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
timeout = setTimeout(() => {
|
|
310
|
-
if (!isCurrentAttempt(generation) || stopped) {
|
|
311
|
-
failCurrentAttempt(false);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (sock && sock.readyState !== WebSocket.CLOSED) {
|
|
316
|
-
closeSocket(sock);
|
|
317
|
-
}
|
|
318
|
-
failCurrentAttempt(true);
|
|
319
|
-
}, connectTimeoutMs);
|
|
320
|
-
|
|
321
|
-
let url = wsUrl;
|
|
322
|
-
if (!url) {
|
|
323
|
-
const d = await discoverWsUrlImpl();
|
|
324
|
-
if (!isCurrentAttempt(generation) || stopped) {
|
|
325
|
-
failCurrentAttempt(false);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
if (!d.success) {
|
|
329
|
-
failCurrentAttempt(false);
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
url = d.data;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (!isCurrentAttempt(generation) || stopped) {
|
|
336
|
-
failCurrentAttempt(false);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
sock = createWebSocketImpl(url);
|
|
342
|
-
} catch {
|
|
343
|
-
failCurrentAttempt(false);
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (!isCurrentAttempt(generation) || stopped) {
|
|
348
|
-
closeSocket(sock);
|
|
349
|
-
failCurrentAttempt(false);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
wsUrl = url;
|
|
354
|
-
activeSocket = sock;
|
|
355
|
-
|
|
356
|
-
const onOpen = (): void => {
|
|
357
|
-
if (!sock || !isCurrentAttemptSocket(generation, sock)) {
|
|
358
|
-
clearAttemptTimeout();
|
|
359
|
-
if (sock && sock.readyState !== WebSocket.CLOSED) {
|
|
360
|
-
closeSocket(sock);
|
|
361
|
-
}
|
|
362
|
-
failCurrentAttempt(false);
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
clearAttemptTimeout();
|
|
367
|
-
ws = sock;
|
|
368
|
-
try {
|
|
369
|
-
ws.send(JSON.stringify({ id: 0, method: "Target.setDiscoverTargets", params: { discover: true } }));
|
|
370
|
-
} catch {
|
|
371
|
-
failCurrentAttempt(true);
|
|
372
|
-
if (sock.readyState !== WebSocket.CLOSED) {
|
|
373
|
-
closeSocket(sock);
|
|
374
|
-
}
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
console.log("[pi-browser-daemon] Connected to Chrome ✓");
|
|
378
|
-
settleAttempt();
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
const onMessage = (data: WebSocket.Data): void => {
|
|
382
|
-
if (!sock || !isCurrentAttemptSocket(generation, sock)) return;
|
|
383
|
-
onChromeMessage(typeof data === "string" ? data : data.toString());
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
const onError = (): void => {
|
|
387
|
-
clearAttemptTimeout();
|
|
388
|
-
const notifyClose = !!sock && isCurrentAttemptSocket(generation, sock);
|
|
389
|
-
failCurrentAttempt(notifyClose);
|
|
390
|
-
if (sock && sock.readyState !== WebSocket.CLOSED) {
|
|
391
|
-
closeSocket(sock);
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
const onClose = (): void => {
|
|
396
|
-
clearAttemptTimeout();
|
|
397
|
-
const notifyClose = !!sock && isCurrentAttemptSocket(generation, sock);
|
|
398
|
-
failCurrentAttempt(notifyClose);
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
sock.on("open", onOpen);
|
|
402
|
-
sock.on("message", onMessage);
|
|
403
|
-
sock.on("error", onError);
|
|
404
|
-
sock.on("close", onClose);
|
|
405
|
-
|
|
406
|
-
// The promise must always settle even on constructor errors.
|
|
407
|
-
await settledPromise;
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
attempt().catch(() => {
|
|
411
|
-
if (isCurrentAttempt(generation)) {
|
|
412
|
-
connecting = false;
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
const start = async (): Promise<void> => {
|
|
418
|
-
stopped = false;
|
|
419
|
-
tryConnect();
|
|
420
|
-
};
|
|
201
|
+
const connection = createChromeConnection(options, {
|
|
202
|
+
onMessage: onChromeMessage,
|
|
203
|
+
onEnd: (generation, reason) => {
|
|
204
|
+
// Requests that went out on this socket can never be answered now; fail them at once
|
|
205
|
+
// rather than leaving them to the command timeout.
|
|
206
|
+
failAll(mux.takeWhere((e) => e.generation === generation), "Chrome disconnected");
|
|
207
|
+
if (reason === "dropped") closeHandler?.();
|
|
208
|
+
},
|
|
209
|
+
});
|
|
421
210
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
connecting = false;
|
|
428
|
-
rejectAllPendingCallbacks();
|
|
429
|
-
if (ws) {
|
|
430
|
-
try {
|
|
431
|
-
ws.close(1000, "Shutdown");
|
|
432
|
-
} catch {}
|
|
433
|
-
ws = null;
|
|
434
|
-
}
|
|
435
|
-
wsUrl = null;
|
|
211
|
+
// Kicks off the first attempt without waiting for it: the daemon serves its IPC socket
|
|
212
|
+
// meanwhile, and handleRequest() joins the attempt in flight.
|
|
213
|
+
const start = (): Promise<void> => {
|
|
214
|
+
void connection.start();
|
|
215
|
+
return Promise.resolve();
|
|
436
216
|
};
|
|
437
217
|
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
443
|
-
}
|
|
444
|
-
return ws !== null && ws.readyState === WebSocket.OPEN;
|
|
218
|
+
const stop = (): Promise<void> => {
|
|
219
|
+
connection.stop();
|
|
220
|
+
failAll(mux.takeWhere(() => true), "Chrome disconnected");
|
|
221
|
+
return Promise.resolve();
|
|
445
222
|
};
|
|
446
223
|
|
|
447
224
|
const handleRequest = async (
|
|
@@ -449,50 +226,40 @@ export const createCdpBridge = (options: CdpBridgeDependencies = {}): CdpBridge
|
|
|
449
226
|
clientId: string,
|
|
450
227
|
send: SendToClient,
|
|
451
228
|
): Promise<void> => {
|
|
452
|
-
|
|
453
|
-
if (
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
type: "response",
|
|
460
|
-
id: req.id,
|
|
461
|
-
error: { code: -32000, message: "Chrome not connected" },
|
|
462
|
-
});
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
229
|
+
// The one request the daemon answers itself: a human asked for a fresh dial.
|
|
230
|
+
if (req.method === DAEMON_RECONNECT_METHOD) {
|
|
231
|
+
const open = await connection.reconnectNow();
|
|
232
|
+
send(clientId, open
|
|
233
|
+
? { type: "response", id: req.id, result: { connected: true } }
|
|
234
|
+
: errorResponse(req.id, describeDisconnected(connection.status())));
|
|
235
|
+
return;
|
|
465
236
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
send(clientId, {
|
|
470
|
-
type: "response",
|
|
471
|
-
id: req.id,
|
|
472
|
-
error: { code: -32000, message: "Chrome not connected" },
|
|
473
|
-
});
|
|
237
|
+
// Connection attempts happen only in response to explicit browser work.
|
|
238
|
+
if (!connection.isOpen() && !(await connection.connect())) {
|
|
239
|
+
send(clientId, errorResponse(req.id, describeDisconnected(connection.status())));
|
|
474
240
|
return;
|
|
475
241
|
}
|
|
476
242
|
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
243
|
+
// The session being detached travels in params (src/client.ts sends the envelope sessionId as null).
|
|
244
|
+
if (req.method === "Target.detachFromTarget") {
|
|
245
|
+
const sessionId = asString(req.params?.["sessionId"]);
|
|
246
|
+
if (sessionId !== undefined) router.release(sessionId);
|
|
247
|
+
}
|
|
481
248
|
|
|
482
249
|
const daemonId = mux.allocate(
|
|
483
|
-
{
|
|
250
|
+
{
|
|
251
|
+
clientId,
|
|
252
|
+
localId: req.id,
|
|
253
|
+
send,
|
|
254
|
+
isAttach: req.method === "Target.attachToTarget",
|
|
255
|
+
generation: connection.generation(),
|
|
256
|
+
},
|
|
484
257
|
(id) =>
|
|
485
258
|
setTimeout(() => {
|
|
486
259
|
mux.take(id);
|
|
487
|
-
|
|
488
|
-
send(clientId, {
|
|
489
|
-
type: "response",
|
|
490
|
-
id: req.id,
|
|
491
|
-
error: { code: -32000, message: `Timeout after ${CDP_COMMAND_TIMEOUT_MS}ms: ${req.method}` },
|
|
492
|
-
});
|
|
260
|
+
send(clientId, errorResponse(req.id, `Timeout after ${CDP_COMMAND_TIMEOUT_MS}ms: ${req.method}`));
|
|
493
261
|
}, CDP_COMMAND_TIMEOUT_MS),
|
|
494
262
|
);
|
|
495
|
-
generations.set(daemonId, { generation: attemptGeneration, clientId });
|
|
496
263
|
|
|
497
264
|
const payload: Record<string, unknown> = {
|
|
498
265
|
id: daemonId,
|
|
@@ -501,16 +268,10 @@ export const createCdpBridge = (options: CdpBridgeDependencies = {}): CdpBridge
|
|
|
501
268
|
};
|
|
502
269
|
if (req.sessionId) payload["sessionId"] = req.sessionId;
|
|
503
270
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
} catch (e) {
|
|
271
|
+
const sent = connection.send(JSON.stringify(payload));
|
|
272
|
+
if (!sent.success) {
|
|
507
273
|
mux.take(daemonId);
|
|
508
|
-
|
|
509
|
-
send(clientId, {
|
|
510
|
-
type: "response",
|
|
511
|
-
id: req.id,
|
|
512
|
-
error: { code: -32000, message: e instanceof Error ? e.message : String(e) },
|
|
513
|
-
});
|
|
274
|
+
send(clientId, errorResponse(req.id, sent.error));
|
|
514
275
|
}
|
|
515
276
|
};
|
|
516
277
|
|
|
@@ -518,11 +279,11 @@ export const createCdpBridge = (options: CdpBridgeDependencies = {}): CdpBridge
|
|
|
518
279
|
start,
|
|
519
280
|
stop,
|
|
520
281
|
handleRequest,
|
|
521
|
-
isAlive: () =>
|
|
282
|
+
isAlive: () => connection.isOpen(),
|
|
522
283
|
getSessionOwner: (sid) => router.getOwner(sid),
|
|
523
284
|
removeClient: (cid) => {
|
|
524
|
-
|
|
525
|
-
mux.
|
|
285
|
+
// A gone client must never be answered: drop its requests silently and release its sessions.
|
|
286
|
+
mux.takeWhere((e) => e.clientId === cid);
|
|
526
287
|
router.removeClient(cid);
|
|
527
288
|
},
|
|
528
289
|
onEvent: (h) => { eventHandler = h; },
|