@anvia/browser 1.0.8 → 1.0.11
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/MIGRATION.md +68 -0
- package/README.md +202 -26
- package/dist/automation-worker.d.ts +2 -0
- package/dist/automation-worker.js +298 -0
- package/dist/automation-worker.js.map +1 -0
- package/dist/index.d.ts +71 -21
- package/dist/index.js +2061 -568
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/dist/index.js
CHANGED
|
@@ -1,82 +1,722 @@
|
|
|
1
|
-
// src/docker-browser.ts
|
|
1
|
+
// src/docker-browser-client.ts
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
3
|
|
|
4
|
-
// src/
|
|
5
|
-
import {
|
|
6
|
-
|
|
4
|
+
// src/automation-client.ts
|
|
5
|
+
import { fork } from "child_process";
|
|
6
|
+
|
|
7
|
+
// src/automation-protocol.ts
|
|
8
|
+
var safeSummaryPropertyNames = /* @__PURE__ */ new Set([
|
|
9
|
+
"cmd",
|
|
10
|
+
"error",
|
|
11
|
+
"event",
|
|
12
|
+
"id",
|
|
13
|
+
"kind",
|
|
14
|
+
"ok",
|
|
15
|
+
"value",
|
|
16
|
+
"watch:import",
|
|
17
|
+
"watch:require"
|
|
18
|
+
]);
|
|
19
|
+
var maxSummaryPropertyNames = 12;
|
|
20
|
+
function isAutomationResponse(value) {
|
|
21
|
+
if (!isRecord(value)) return false;
|
|
22
|
+
switch (value.kind) {
|
|
23
|
+
case "event":
|
|
24
|
+
return value.event === "disconnected";
|
|
25
|
+
case "cancelled":
|
|
26
|
+
return isRequestId(value.id);
|
|
27
|
+
case "response":
|
|
28
|
+
if (!isRequestId(value.id) || typeof value.ok !== "boolean") return false;
|
|
29
|
+
return value.ok ? Object.hasOwn(value, "value") : isSerializedError(value.error);
|
|
30
|
+
default:
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function summarizeAutomationMessage(value) {
|
|
35
|
+
const isObject = isRecord(value);
|
|
36
|
+
const prototype = isObject ? Object.getPrototypeOf(value) : void 0;
|
|
37
|
+
const propertyNames = isObject ? Object.getOwnPropertyNames(value) : [];
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
type: value === null ? "null" : Array.isArray(value) ? "array" : typeof value,
|
|
40
|
+
ownPropertyNames: Object.freeze(
|
|
41
|
+
propertyNames.slice(0, maxSummaryPropertyNames).map((name) => safeSummaryPropertyNames.has(name) ? name : "<other>")
|
|
42
|
+
),
|
|
43
|
+
ownPropertyCount: propertyNames.length,
|
|
44
|
+
kind: !isObject || value.kind === void 0 ? void 0 : value.kind === "response" || value.kind === "cancelled" || value.kind === "event" ? value.kind : "other",
|
|
45
|
+
id: !isObject || value.id === void 0 ? void 0 : typeof value.id === "number" ? value.id : "non-number",
|
|
46
|
+
ok: !isObject || value.ok === void 0 ? void 0 : typeof value.ok === "boolean" ? value.ok : "non-boolean",
|
|
47
|
+
constructorName: !isObject ? void 0 : prototype === null ? "null-prototype" : prototype === Object.prototype ? "Object" : "other",
|
|
48
|
+
hasExpectedPrototype: isObject && prototype === Object.prototype,
|
|
49
|
+
hasRequiredFields: isAutomationResponse(value)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function isSerializedError(value) {
|
|
53
|
+
return isRecord(value) && typeof value.name === "string" && typeof value.message === "string" && (value.stack === void 0 || typeof value.stack === "string") && (value.code === void 0 || typeof value.code === "string");
|
|
54
|
+
}
|
|
55
|
+
function isRequestId(value) {
|
|
56
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
57
|
+
}
|
|
58
|
+
function isRecord(value) {
|
|
59
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
7
61
|
|
|
8
62
|
// src/errors.ts
|
|
63
|
+
var defaults = {
|
|
64
|
+
action_timeout: { retryable: true, recovery: "retry" },
|
|
65
|
+
agent_action_busy: { retryable: true, recovery: "retry" },
|
|
66
|
+
cancelled: { retryable: true, recovery: "retry" },
|
|
67
|
+
connection_closed: { retryable: true, recovery: "reconnect" },
|
|
68
|
+
connection_timeout: { retryable: true, recovery: "reconnect" },
|
|
69
|
+
human_control_conflict: { retryable: true, recovery: "retry" },
|
|
70
|
+
human_controlled: { retryable: true, recovery: "retry" },
|
|
71
|
+
invalid_state: { retryable: false, recovery: "none" },
|
|
72
|
+
lifecycle_timeout: { retryable: true, recovery: "retry" },
|
|
73
|
+
navigation_blocked: { retryable: false, recovery: "none" },
|
|
74
|
+
not_ready: { retryable: true, recovery: "retry" },
|
|
75
|
+
readiness_timeout: { retryable: true, recovery: "retry" },
|
|
76
|
+
runtime_destroyed: { retryable: false, recovery: "recreate" },
|
|
77
|
+
startup_failed: { retryable: true, recovery: "recreate" },
|
|
78
|
+
tool_failed: { retryable: true, recovery: "retry" },
|
|
79
|
+
transport_failure: { retryable: true, recovery: "reconnect" }
|
|
80
|
+
};
|
|
9
81
|
var BrowserError = class extends Error {
|
|
10
|
-
constructor(message, code, options) {
|
|
82
|
+
constructor(message, code, options = {}) {
|
|
11
83
|
super(message, options);
|
|
12
84
|
this.code = code;
|
|
13
85
|
this.name = "BrowserError";
|
|
86
|
+
this.retryable = options.retryable ?? defaults[code].retryable;
|
|
87
|
+
this.recovery = options.recovery ?? defaults[code].recovery;
|
|
88
|
+
this.capability = options.capability;
|
|
89
|
+
this.phase = options.phase;
|
|
14
90
|
}
|
|
15
91
|
code;
|
|
92
|
+
retryable;
|
|
93
|
+
recovery;
|
|
94
|
+
capability;
|
|
95
|
+
phase;
|
|
96
|
+
};
|
|
97
|
+
function cancellationError(reason, phase) {
|
|
98
|
+
return new BrowserError("Browser operation was cancelled by the caller.", "cancelled", {
|
|
99
|
+
cause: reason,
|
|
100
|
+
phase
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/automation-client.ts
|
|
105
|
+
var cancellationCleanupTimeoutMs = 5e3;
|
|
106
|
+
var workerTerminationTimeoutMs = 3e3;
|
|
107
|
+
var maxWorkerStderrChars = 32768;
|
|
108
|
+
var AutomationWorkerClient = class _AutomationWorkerClient {
|
|
109
|
+
child;
|
|
110
|
+
pending = /* @__PURE__ */ new Map();
|
|
111
|
+
disconnectedListeners = /* @__PURE__ */ new Set();
|
|
112
|
+
nextId = 1;
|
|
113
|
+
isClosed = false;
|
|
114
|
+
disconnecting = false;
|
|
115
|
+
termination;
|
|
116
|
+
workerStderr = "";
|
|
117
|
+
onStderrData = (chunk) => {
|
|
118
|
+
this.workerStderr = `${this.workerStderr}${chunk}`.slice(-maxWorkerStderrChars);
|
|
119
|
+
};
|
|
120
|
+
onStderrError = () => {
|
|
121
|
+
};
|
|
122
|
+
onMessage = (message) => {
|
|
123
|
+
try {
|
|
124
|
+
this.handleMessage(message);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
this.handleExit(
|
|
127
|
+
new BrowserError(
|
|
128
|
+
"Browser automation worker sent an invalid response.",
|
|
129
|
+
"transport_failure",
|
|
130
|
+
{
|
|
131
|
+
cause: error,
|
|
132
|
+
phase: "automation-worker"
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
void this.terminate().catch(() => void 0);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
onError = (error) => {
|
|
140
|
+
this.handleExit(error);
|
|
141
|
+
void this.terminate().catch(() => void 0);
|
|
142
|
+
};
|
|
143
|
+
onExit = (code, signal) => {
|
|
144
|
+
const diagnostics = this.workerStderr.trim();
|
|
145
|
+
this.handleExit(
|
|
146
|
+
new Error(
|
|
147
|
+
`Browser automation worker exited: code=${code ?? "null"} signal=${signal ?? "null"}${diagnostics.length === 0 ? "" : `
|
|
148
|
+
${diagnostics}`}`
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
constructor(factory) {
|
|
153
|
+
this.child = factory();
|
|
154
|
+
this.child.stderr?.setEncoding("utf8");
|
|
155
|
+
this.child.stderr?.on("data", this.onStderrData);
|
|
156
|
+
this.child.stderr?.on("error", this.onStderrError);
|
|
157
|
+
this.child.on("message", this.onMessage);
|
|
158
|
+
this.child.on("error", this.onError);
|
|
159
|
+
this.child.once("exit", this.onExit);
|
|
160
|
+
}
|
|
161
|
+
static async connect(options) {
|
|
162
|
+
assertPositiveSafeInteger(options.timeoutMs, "timeoutMs");
|
|
163
|
+
if (options.abortSignal?.aborted) {
|
|
164
|
+
throw cancellationError(options.abortSignal.reason, "connect");
|
|
165
|
+
}
|
|
166
|
+
let client;
|
|
167
|
+
try {
|
|
168
|
+
client = new _AutomationWorkerClient(options.workerFactory ?? spawnAutomationWorker);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw new BrowserError(
|
|
171
|
+
"Unable to start the browser automation worker.",
|
|
172
|
+
"transport_failure",
|
|
173
|
+
{
|
|
174
|
+
cause: error,
|
|
175
|
+
phase: "connect",
|
|
176
|
+
recovery: "reconnect"
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const timeoutController = new AbortController();
|
|
181
|
+
const timer = setTimeout(
|
|
182
|
+
() => timeoutController.abort(
|
|
183
|
+
new BrowserError(
|
|
184
|
+
"Browser connection did not initialize before the timeout.",
|
|
185
|
+
"connection_timeout",
|
|
186
|
+
{ phase: "connect" }
|
|
187
|
+
)
|
|
188
|
+
),
|
|
189
|
+
options.timeoutMs
|
|
190
|
+
);
|
|
191
|
+
timer.unref?.();
|
|
192
|
+
const abortSignal = options.abortSignal === void 0 ? timeoutController.signal : AbortSignal.any([options.abortSignal, timeoutController.signal]);
|
|
193
|
+
try {
|
|
194
|
+
await client.command(
|
|
195
|
+
{
|
|
196
|
+
method: "connect",
|
|
197
|
+
params: { endpointUrl: options.endpointUrl, timeoutMs: options.timeoutMs }
|
|
198
|
+
},
|
|
199
|
+
{ abortSignal, phase: "connect" }
|
|
200
|
+
);
|
|
201
|
+
return client;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
await client.terminate();
|
|
204
|
+
if (options.abortSignal?.aborted) {
|
|
205
|
+
throw cancellationError(options.abortSignal.reason, "connect");
|
|
206
|
+
}
|
|
207
|
+
if (timeoutController.signal.aborted) throw timeoutController.signal.reason;
|
|
208
|
+
if (error instanceof Error && (error.name === "TimeoutError" || /timeout/i.test(error.message))) {
|
|
209
|
+
throw new BrowserError(
|
|
210
|
+
"Browser connection did not initialize before the timeout.",
|
|
211
|
+
"connection_timeout",
|
|
212
|
+
{ cause: error, phase: "connect" }
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
throw new BrowserError("Unable to initialize browser automation.", "transport_failure", {
|
|
216
|
+
cause: error,
|
|
217
|
+
phase: "connect"
|
|
218
|
+
});
|
|
219
|
+
} finally {
|
|
220
|
+
clearTimeout(timer);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
get closed() {
|
|
224
|
+
return this.isClosed || this.child.exitCode !== null || this.child.signalCode !== null;
|
|
225
|
+
}
|
|
226
|
+
command(command, options) {
|
|
227
|
+
if (this.closed) {
|
|
228
|
+
return Promise.reject(
|
|
229
|
+
new BrowserError("Browser automation connection is closed.", "connection_closed", {
|
|
230
|
+
phase: options.phase
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (options.abortSignal?.aborted) {
|
|
235
|
+
return Promise.reject(this.abortError(options.abortSignal, options.phase));
|
|
236
|
+
}
|
|
237
|
+
const id = this.nextId++;
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const pending = {
|
|
240
|
+
state: "active",
|
|
241
|
+
resolve: (value) => resolve(value),
|
|
242
|
+
reject
|
|
243
|
+
};
|
|
244
|
+
if (options.abortSignal !== void 0) {
|
|
245
|
+
pending.abortSignal = options.abortSignal;
|
|
246
|
+
pending.abort = () => this.cancelRequest(id, pending, options);
|
|
247
|
+
options.abortSignal.addEventListener("abort", pending.abort, { once: true });
|
|
248
|
+
}
|
|
249
|
+
this.pending.set(id, pending);
|
|
250
|
+
const request = { kind: "request", id, ...command };
|
|
251
|
+
try {
|
|
252
|
+
this.child.send(request, (error) => {
|
|
253
|
+
if (error == null) return;
|
|
254
|
+
this.rejectSend(options.phase, error);
|
|
255
|
+
});
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.rejectSend(options.phase, error);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
async disconnect(options = {}) {
|
|
262
|
+
if (this.termination !== void 0) return this.termination;
|
|
263
|
+
this.disconnecting = true;
|
|
264
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
265
|
+
const timeoutController = new AbortController();
|
|
266
|
+
const timer = setTimeout(
|
|
267
|
+
() => timeoutController.abort(
|
|
268
|
+
new BrowserError("Browser disconnect timed out.", "connection_timeout", {
|
|
269
|
+
phase: "disconnect"
|
|
270
|
+
})
|
|
271
|
+
),
|
|
272
|
+
timeoutMs
|
|
273
|
+
);
|
|
274
|
+
timer.unref?.();
|
|
275
|
+
const abortSignal = options.abortSignal === void 0 ? timeoutController.signal : AbortSignal.any([options.abortSignal, timeoutController.signal]);
|
|
276
|
+
try {
|
|
277
|
+
if (!this.closed) {
|
|
278
|
+
await this.command(
|
|
279
|
+
{ method: "disconnect", params: {} },
|
|
280
|
+
{ abortSignal, phase: "disconnect" }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (options.abortSignal?.aborted) {
|
|
285
|
+
throw cancellationError(options.abortSignal.reason, "disconnect");
|
|
286
|
+
}
|
|
287
|
+
if (timeoutController.signal.aborted) throw timeoutController.signal.reason;
|
|
288
|
+
if (!this.closed) throw error;
|
|
289
|
+
} finally {
|
|
290
|
+
clearTimeout(timer);
|
|
291
|
+
await this.terminate();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
onDisconnected(listener) {
|
|
295
|
+
if (this.closed) {
|
|
296
|
+
queueMicrotask(() => {
|
|
297
|
+
try {
|
|
298
|
+
listener();
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
return () => void 0;
|
|
303
|
+
}
|
|
304
|
+
this.disconnectedListeners.add(listener);
|
|
305
|
+
return () => this.disconnectedListeners.delete(listener);
|
|
306
|
+
}
|
|
307
|
+
async terminate() {
|
|
308
|
+
if (this.termination !== void 0) return this.termination;
|
|
309
|
+
this.termination = this.performTerminate();
|
|
310
|
+
return this.termination;
|
|
311
|
+
}
|
|
312
|
+
async performTerminate() {
|
|
313
|
+
this.isClosed = true;
|
|
314
|
+
if (this.child.exitCode !== null || this.child.signalCode !== null) return;
|
|
315
|
+
const exited = new Promise((resolve) => this.child.once("exit", () => resolve()));
|
|
316
|
+
this.child.kill("SIGTERM");
|
|
317
|
+
let forceTimer;
|
|
318
|
+
await Promise.race([
|
|
319
|
+
exited,
|
|
320
|
+
new Promise((resolve) => {
|
|
321
|
+
forceTimer = setTimeout(() => {
|
|
322
|
+
this.child.kill("SIGKILL");
|
|
323
|
+
resolve();
|
|
324
|
+
}, workerTerminationTimeoutMs);
|
|
325
|
+
forceTimer.unref?.();
|
|
326
|
+
})
|
|
327
|
+
]);
|
|
328
|
+
if (forceTimer !== void 0) clearTimeout(forceTimer);
|
|
329
|
+
if (this.child.exitCode === null && this.child.signalCode === null) await exited;
|
|
330
|
+
}
|
|
331
|
+
cancelRequest(id, pending, options) {
|
|
332
|
+
if (pending.state !== "active") return;
|
|
333
|
+
pending.state = "cancelling";
|
|
334
|
+
pending.abortError = this.abortError(pending.abortSignal, options.phase);
|
|
335
|
+
if (options.cancelOperation === true && this.child.connected) {
|
|
336
|
+
try {
|
|
337
|
+
this.child.send({ kind: "cancel", id }, (error) => {
|
|
338
|
+
if (error !== null) void this.terminate().catch(() => void 0);
|
|
339
|
+
});
|
|
340
|
+
} catch {
|
|
341
|
+
void this.terminate().catch(() => void 0);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
pending.cleanupTimer = setTimeout(() => {
|
|
345
|
+
void this.terminate().catch(() => void 0);
|
|
346
|
+
}, cancellationCleanupTimeoutMs);
|
|
347
|
+
pending.cleanupTimer.unref?.();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
void this.terminate().catch(() => void 0);
|
|
351
|
+
}
|
|
352
|
+
abortError(abortSignal, phase) {
|
|
353
|
+
return abortSignal.reason instanceof BrowserError ? abortSignal.reason : cancellationError(abortSignal.reason, phase);
|
|
354
|
+
}
|
|
355
|
+
handleMessage(message) {
|
|
356
|
+
if (!isAutomationResponse(message)) {
|
|
357
|
+
throw new InvalidAutomationResponseError(summarizeAutomationMessage(message));
|
|
358
|
+
}
|
|
359
|
+
if (message.kind === "event") {
|
|
360
|
+
if (!this.disconnecting) {
|
|
361
|
+
this.handleExit(
|
|
362
|
+
new BrowserError("Browser disconnected from CDP.", "connection_closed", {
|
|
363
|
+
phase: "automation-event"
|
|
364
|
+
})
|
|
365
|
+
);
|
|
366
|
+
void this.terminate().catch(() => void 0);
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const pending = this.pending.get(message.id);
|
|
371
|
+
if (pending === void 0) return;
|
|
372
|
+
if (message.kind === "cancelled") {
|
|
373
|
+
if (pending.state === "cancelling") {
|
|
374
|
+
this.settle(message.id, pending, () => pending.reject(pending.abortError));
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (pending.state === "cancelling") return;
|
|
379
|
+
this.settle(message.id, pending, () => {
|
|
380
|
+
if (message.ok) pending.resolve(message.value);
|
|
381
|
+
else pending.reject(deserializeError(message.error));
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
handleExit(cause) {
|
|
385
|
+
if (this.isClosed && this.pending.size === 0) {
|
|
386
|
+
this.cleanupChildListeners();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
this.isClosed = true;
|
|
390
|
+
const error = cause instanceof BrowserError ? cause : new BrowserError("Browser automation worker terminated.", "transport_failure", {
|
|
391
|
+
cause,
|
|
392
|
+
phase: "automation-worker"
|
|
393
|
+
});
|
|
394
|
+
for (const [id, pending] of this.pending) {
|
|
395
|
+
this.settle(
|
|
396
|
+
id,
|
|
397
|
+
pending,
|
|
398
|
+
() => pending.reject(pending.state === "cancelling" ? pending.abortError : error)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const listeners = [...this.disconnectedListeners];
|
|
402
|
+
this.disconnectedListeners.clear();
|
|
403
|
+
for (const listener of listeners) {
|
|
404
|
+
try {
|
|
405
|
+
listener();
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
this.cleanupChildListeners();
|
|
410
|
+
}
|
|
411
|
+
cleanupChildListeners() {
|
|
412
|
+
this.child.off("message", this.onMessage);
|
|
413
|
+
this.child.off("error", this.onError);
|
|
414
|
+
this.child.off("exit", this.onExit);
|
|
415
|
+
this.child.stderr?.off("data", this.onStderrData);
|
|
416
|
+
this.child.stderr?.off("error", this.onStderrError);
|
|
417
|
+
}
|
|
418
|
+
rejectSend(phase, cause) {
|
|
419
|
+
this.handleExit(
|
|
420
|
+
new BrowserError("Unable to send browser automation command.", "transport_failure", {
|
|
421
|
+
cause,
|
|
422
|
+
phase
|
|
423
|
+
})
|
|
424
|
+
);
|
|
425
|
+
void this.terminate().catch(() => void 0);
|
|
426
|
+
}
|
|
427
|
+
settle(id, pending, callback) {
|
|
428
|
+
if (this.pending.get(id) !== pending) return;
|
|
429
|
+
this.pending.delete(id);
|
|
430
|
+
if (pending.cleanupTimer !== void 0) clearTimeout(pending.cleanupTimer);
|
|
431
|
+
if (pending.abort !== void 0) {
|
|
432
|
+
pending.abortSignal?.removeEventListener("abort", pending.abort);
|
|
433
|
+
}
|
|
434
|
+
callback();
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
function spawnAutomationWorker(workerUrl = automationWorkerUrl()) {
|
|
438
|
+
const env = { ...process.env };
|
|
439
|
+
delete env.WATCH_REPORT_DEPENDENCIES;
|
|
440
|
+
return fork(workerUrl, [], {
|
|
441
|
+
env,
|
|
442
|
+
execArgv: [],
|
|
443
|
+
serialization: "advanced",
|
|
444
|
+
stdio: ["ignore", "ignore", "pipe", "ipc"]
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
function automationWorkerUrl() {
|
|
448
|
+
return import.meta.url.endsWith("/src/automation-client.ts") ? new URL("../dist/automation-worker.js", import.meta.url) : new URL("./automation-worker.js", import.meta.url);
|
|
449
|
+
}
|
|
450
|
+
function deserializeError(value) {
|
|
451
|
+
const error = new Error(value.message);
|
|
452
|
+
error.name = value.name;
|
|
453
|
+
if (value.stack !== void 0) error.stack = value.stack;
|
|
454
|
+
if (value.code !== void 0) Object.assign(error, { code: value.code });
|
|
455
|
+
return error;
|
|
456
|
+
}
|
|
457
|
+
function assertPositiveSafeInteger(value, name) {
|
|
458
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > 2147483647) {
|
|
459
|
+
throw new RangeError(`${name} must be a positive integer no greater than 2147483647.`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
var InvalidAutomationResponseError = class extends TypeError {
|
|
463
|
+
constructor(responseSummary) {
|
|
464
|
+
super("Invalid browser automation worker response.");
|
|
465
|
+
this.responseSummary = responseSummary;
|
|
466
|
+
}
|
|
467
|
+
responseSummary;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// src/scheduler.ts
|
|
471
|
+
var ResourceScheduler = class {
|
|
472
|
+
resources = /* @__PURE__ */ new Map();
|
|
473
|
+
readyResources = [];
|
|
474
|
+
activeTasks = /* @__PURE__ */ new Set();
|
|
475
|
+
serial;
|
|
476
|
+
maxConcurrentResources;
|
|
477
|
+
maxQueuedActions;
|
|
478
|
+
activeResources = 0;
|
|
479
|
+
queuedActions = 0;
|
|
480
|
+
closedError;
|
|
481
|
+
constructor(options) {
|
|
482
|
+
this.serial = options.mode === "serial";
|
|
483
|
+
this.maxConcurrentResources = options.maxConcurrentResources;
|
|
484
|
+
this.maxQueuedActions = options.maxQueuedActions;
|
|
485
|
+
}
|
|
486
|
+
run(resourceKey, abortSignal, operation) {
|
|
487
|
+
if (this.closedError !== void 0) return Promise.reject(this.closedError);
|
|
488
|
+
if (abortSignal?.aborted) {
|
|
489
|
+
return Promise.reject(abortReason(abortSignal, "agent-action-queue"));
|
|
490
|
+
}
|
|
491
|
+
if (this.queuedActions >= this.maxQueuedActions) {
|
|
492
|
+
return Promise.reject(
|
|
493
|
+
new BrowserError("Browser action queue capacity was reached.", "agent_action_busy", {
|
|
494
|
+
phase: "agent-action-queue"
|
|
495
|
+
})
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
const key = this.serial ? "browser" : resourceKey;
|
|
499
|
+
let resource = this.resources.get(key);
|
|
500
|
+
if (resource === void 0) {
|
|
501
|
+
resource = { running: false, ready: false, tasks: [] };
|
|
502
|
+
this.resources.set(key, resource);
|
|
503
|
+
}
|
|
504
|
+
return new Promise((resolve, reject) => {
|
|
505
|
+
const task = {
|
|
506
|
+
controller: new AbortController(),
|
|
507
|
+
operation,
|
|
508
|
+
resolve,
|
|
509
|
+
reject,
|
|
510
|
+
...abortSignal === void 0 ? {} : { abortSignal }
|
|
511
|
+
};
|
|
512
|
+
if (abortSignal !== void 0) {
|
|
513
|
+
task.abort = () => {
|
|
514
|
+
const current = this.resources.get(key);
|
|
515
|
+
const index = current?.tasks.indexOf(task) ?? -1;
|
|
516
|
+
if (index === -1) return;
|
|
517
|
+
current?.tasks.splice(index, 1);
|
|
518
|
+
this.queuedActions -= 1;
|
|
519
|
+
abortSignal.removeEventListener("abort", task.abort);
|
|
520
|
+
reject(abortReason(abortSignal, "agent-action-queue"));
|
|
521
|
+
this.deleteIdleResource(key, current);
|
|
522
|
+
};
|
|
523
|
+
abortSignal.addEventListener("abort", task.abort, { once: true });
|
|
524
|
+
}
|
|
525
|
+
resource.tasks.push(task);
|
|
526
|
+
this.queuedActions += 1;
|
|
527
|
+
this.markReady(key, resource);
|
|
528
|
+
this.pump();
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
close(error = new BrowserError("Browser connection is closed.", "connection_closed")) {
|
|
532
|
+
if (this.closedError !== void 0) return;
|
|
533
|
+
this.closedError = error;
|
|
534
|
+
for (const task of this.activeTasks) task.controller.abort(error);
|
|
535
|
+
for (const [key, resource] of this.resources) {
|
|
536
|
+
for (const task of resource.tasks.splice(0)) {
|
|
537
|
+
this.queuedActions -= 1;
|
|
538
|
+
if (task.abort !== void 0) task.abortSignal?.removeEventListener("abort", task.abort);
|
|
539
|
+
task.reject(error);
|
|
540
|
+
}
|
|
541
|
+
resource.ready = false;
|
|
542
|
+
if (!resource.running) this.resources.delete(key);
|
|
543
|
+
}
|
|
544
|
+
this.readyResources.length = 0;
|
|
545
|
+
}
|
|
546
|
+
markReady(key, resource) {
|
|
547
|
+
if (resource.running || resource.ready || resource.tasks.length === 0) return;
|
|
548
|
+
resource.ready = true;
|
|
549
|
+
this.readyResources.push(key);
|
|
550
|
+
}
|
|
551
|
+
pump() {
|
|
552
|
+
while (this.closedError === void 0 && this.activeResources < this.maxConcurrentResources && this.readyResources.length > 0) {
|
|
553
|
+
const key = this.readyResources.shift();
|
|
554
|
+
if (key === void 0) return;
|
|
555
|
+
const resource = this.resources.get(key);
|
|
556
|
+
if (resource === void 0 || resource.running || resource.tasks.length === 0) continue;
|
|
557
|
+
resource.ready = false;
|
|
558
|
+
const task = resource.tasks.shift();
|
|
559
|
+
if (task === void 0) continue;
|
|
560
|
+
this.queuedActions -= 1;
|
|
561
|
+
if (task.abort !== void 0) task.abortSignal?.removeEventListener("abort", task.abort);
|
|
562
|
+
resource.running = true;
|
|
563
|
+
this.activeResources += 1;
|
|
564
|
+
this.activeTasks.add(task);
|
|
565
|
+
const effectiveSignal = task.abortSignal === void 0 ? task.controller.signal : AbortSignal.any([task.abortSignal, task.controller.signal]);
|
|
566
|
+
let operation;
|
|
567
|
+
try {
|
|
568
|
+
operation = task.operation(effectiveSignal);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
operation = Promise.reject(error);
|
|
571
|
+
}
|
|
572
|
+
void operation.then(task.resolve, task.reject).finally(() => {
|
|
573
|
+
this.activeTasks.delete(task);
|
|
574
|
+
resource.running = false;
|
|
575
|
+
this.activeResources -= 1;
|
|
576
|
+
this.markReady(key, resource);
|
|
577
|
+
this.deleteIdleResource(key, resource);
|
|
578
|
+
this.pump();
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
deleteIdleResource(key, resource) {
|
|
583
|
+
if (resource !== void 0 && !resource.running && resource.tasks.length === 0) {
|
|
584
|
+
this.resources.delete(key);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
var AsyncMutex = class {
|
|
589
|
+
waiters = [];
|
|
590
|
+
locked = false;
|
|
591
|
+
closedError;
|
|
592
|
+
async run(abortSignal, operation) {
|
|
593
|
+
const release = await this.acquire(abortSignal);
|
|
594
|
+
try {
|
|
595
|
+
return await operation();
|
|
596
|
+
} finally {
|
|
597
|
+
release();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
close(error = new BrowserError("Browser connection is closed.", "connection_closed")) {
|
|
601
|
+
if (this.closedError !== void 0) return;
|
|
602
|
+
this.closedError = error;
|
|
603
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
604
|
+
if (waiter.abort !== void 0) {
|
|
605
|
+
waiter.abortSignal?.removeEventListener("abort", waiter.abort);
|
|
606
|
+
}
|
|
607
|
+
waiter.reject(error);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
acquire(abortSignal) {
|
|
611
|
+
if (this.closedError !== void 0) return Promise.reject(this.closedError);
|
|
612
|
+
if (abortSignal?.aborted) {
|
|
613
|
+
return Promise.reject(abortReason(abortSignal, "browser-resource-lock"));
|
|
614
|
+
}
|
|
615
|
+
if (!this.locked) {
|
|
616
|
+
this.locked = true;
|
|
617
|
+
return Promise.resolve(this.releaseCallback());
|
|
618
|
+
}
|
|
619
|
+
return new Promise((resolve, reject) => {
|
|
620
|
+
const waiter = {
|
|
621
|
+
resolve,
|
|
622
|
+
reject,
|
|
623
|
+
...abortSignal === void 0 ? {} : { abortSignal }
|
|
624
|
+
};
|
|
625
|
+
if (abortSignal !== void 0) {
|
|
626
|
+
waiter.abort = () => {
|
|
627
|
+
const index = this.waiters.indexOf(waiter);
|
|
628
|
+
if (index === -1) return;
|
|
629
|
+
this.waiters.splice(index, 1);
|
|
630
|
+
reject(abortReason(abortSignal, "browser-resource-lock"));
|
|
631
|
+
};
|
|
632
|
+
abortSignal.addEventListener("abort", waiter.abort, { once: true });
|
|
633
|
+
}
|
|
634
|
+
this.waiters.push(waiter);
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
releaseCallback() {
|
|
638
|
+
let released = false;
|
|
639
|
+
return () => {
|
|
640
|
+
if (released) return;
|
|
641
|
+
released = true;
|
|
642
|
+
const waiter = this.waiters.shift();
|
|
643
|
+
if (waiter === void 0) {
|
|
644
|
+
this.locked = false;
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (waiter.abort !== void 0) {
|
|
648
|
+
waiter.abortSignal?.removeEventListener("abort", waiter.abort);
|
|
649
|
+
}
|
|
650
|
+
waiter.resolve(this.releaseCallback());
|
|
651
|
+
};
|
|
652
|
+
}
|
|
16
653
|
};
|
|
654
|
+
function abortReason(abortSignal, phase) {
|
|
655
|
+
return abortSignal.reason instanceof BrowserError ? abortSignal.reason : cancellationError(abortSignal.reason, phase);
|
|
656
|
+
}
|
|
17
657
|
|
|
18
658
|
// src/connection.ts
|
|
659
|
+
var defaultConnectionTimeoutMs = 3e4;
|
|
660
|
+
var defaultActionTimeoutMs = 1e4;
|
|
661
|
+
var defaultMaxConcurrentTabs = 8;
|
|
662
|
+
var defaultMaxQueuedActions = 1e3;
|
|
663
|
+
var maxClosedTabTombstones = 1e4;
|
|
664
|
+
var maxTimerMs = 2147483647;
|
|
19
665
|
async function connectPlaywrightBrowser(options) {
|
|
666
|
+
const timeoutMs = options.timeoutMs ?? defaultConnectionTimeoutMs;
|
|
667
|
+
assertPositiveSafeInteger2(timeoutMs, "timeoutMs");
|
|
20
668
|
options.abortSignal?.throwIfAborted();
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
options.abortSignal.
|
|
25
|
-
|
|
26
|
-
|
|
669
|
+
const backend = await AutomationWorkerClient.connect({
|
|
670
|
+
endpointUrl: options.endpointUrl,
|
|
671
|
+
timeoutMs,
|
|
672
|
+
...options.abortSignal === void 0 ? {} : { abortSignal: options.abortSignal },
|
|
673
|
+
...options.workerFactory === void 0 ? {} : { workerFactory: options.workerFactory }
|
|
674
|
+
});
|
|
675
|
+
return new PlaywrightBrowserConnectionImpl({
|
|
676
|
+
backend,
|
|
677
|
+
control: options.control,
|
|
678
|
+
scheduling: options.scheduling,
|
|
679
|
+
...options.onClosed === void 0 ? {} : { onClosed: options.onClosed }
|
|
680
|
+
});
|
|
27
681
|
}
|
|
28
682
|
var PlaywrightBrowserConnectionImpl = class {
|
|
29
|
-
|
|
683
|
+
backend;
|
|
30
684
|
control;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
685
|
+
scheduler;
|
|
686
|
+
compatibilityMutex = new AsyncMutex();
|
|
687
|
+
contextMutex = new AsyncMutex();
|
|
688
|
+
closingTabs = /* @__PURE__ */ new Set();
|
|
689
|
+
closedTabs = /* @__PURE__ */ new Set();
|
|
35
690
|
navigationPolicyKey;
|
|
36
691
|
navigationGuard = Promise.resolve();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
692
|
+
isClosed = false;
|
|
693
|
+
disconnectCompleted = false;
|
|
694
|
+
disconnectPromise;
|
|
695
|
+
removeDisconnectedListener;
|
|
696
|
+
onClosed;
|
|
697
|
+
constructor(options) {
|
|
698
|
+
this.backend = options.backend;
|
|
699
|
+
this.control = options.control;
|
|
700
|
+
this.onClosed = options.onClosed;
|
|
701
|
+
const scheduling = normalizeScheduling(options.scheduling);
|
|
702
|
+
this.scheduler = new ResourceScheduler(scheduling);
|
|
703
|
+
this.removeDisconnectedListener = this.backend.onDisconnected(() => this.markClosed());
|
|
44
704
|
}
|
|
45
705
|
get closed() {
|
|
46
|
-
return this.isClosed ||
|
|
47
|
-
}
|
|
48
|
-
async listTabs() {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
title: await page.title(),
|
|
58
|
-
url: page.url(),
|
|
59
|
-
selected: page === this.selected
|
|
60
|
-
})
|
|
61
|
-
)
|
|
706
|
+
return this.isClosed || this.backend.closed;
|
|
707
|
+
}
|
|
708
|
+
async listTabs(options = {}) {
|
|
709
|
+
assertOptionsObject(options);
|
|
710
|
+
return this.runContextAction(
|
|
711
|
+
options.abortSignal,
|
|
712
|
+
options.timeoutMs ?? defaultActionTimeoutMs,
|
|
713
|
+
"list-tabs",
|
|
714
|
+
(signal) => this.backend.command(
|
|
715
|
+
{ method: "listTabs", params: {} },
|
|
716
|
+
{ abortSignal: signal, cancelOperation: true, phase: "list-tabs" }
|
|
62
717
|
)
|
|
63
718
|
);
|
|
64
719
|
}
|
|
65
|
-
async runAction(abortSignal, operation) {
|
|
66
|
-
const previous = this.actionTail;
|
|
67
|
-
let release;
|
|
68
|
-
this.actionTail = new Promise((resolve) => {
|
|
69
|
-
release = resolve;
|
|
70
|
-
});
|
|
71
|
-
await previous;
|
|
72
|
-
try {
|
|
73
|
-
this.assertOpen();
|
|
74
|
-
await this.navigationGuard;
|
|
75
|
-
return await this.control.runAgentAction(() => this.withAbort(abortSignal, operation));
|
|
76
|
-
} finally {
|
|
77
|
-
release?.();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
720
|
setNavigationPolicy(policy) {
|
|
81
721
|
this.assertOpen();
|
|
82
722
|
const key = JSON.stringify(policy);
|
|
@@ -87,111 +727,387 @@ var PlaywrightBrowserConnectionImpl = class {
|
|
|
87
727
|
return;
|
|
88
728
|
}
|
|
89
729
|
this.navigationPolicyKey = key;
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
730
|
+
const installation = this.withActionTimeout(
|
|
731
|
+
void 0,
|
|
732
|
+
defaultActionTimeoutMs,
|
|
733
|
+
"navigation-policy",
|
|
734
|
+
(signal) => this.scheduler.run(
|
|
735
|
+
"browser-context",
|
|
736
|
+
signal,
|
|
737
|
+
(scheduledSignal) => this.contextMutex.run(
|
|
738
|
+
scheduledSignal,
|
|
739
|
+
() => this.control.runAgentAction(async () => {
|
|
740
|
+
try {
|
|
741
|
+
await this.backend.command(
|
|
742
|
+
{ method: "setNavigationPolicy", params: { policy } },
|
|
743
|
+
{
|
|
744
|
+
abortSignal: scheduledSignal,
|
|
745
|
+
cancelOperation: true,
|
|
746
|
+
phase: "navigation-policy"
|
|
747
|
+
}
|
|
748
|
+
);
|
|
749
|
+
} catch (error) {
|
|
750
|
+
throw normalizeAutomationError(error, "navigation-policy");
|
|
751
|
+
}
|
|
752
|
+
})
|
|
753
|
+
)
|
|
754
|
+
)
|
|
755
|
+
);
|
|
756
|
+
this.navigationGuard = installation.catch(async (error) => {
|
|
757
|
+
let cleanupError;
|
|
758
|
+
try {
|
|
759
|
+
await this.disconnect();
|
|
760
|
+
} catch (caught) {
|
|
761
|
+
cleanupError = caught;
|
|
762
|
+
}
|
|
763
|
+
throw new BrowserError(
|
|
764
|
+
"Unable to install the browser navigation policy.",
|
|
765
|
+
"transport_failure",
|
|
766
|
+
{
|
|
767
|
+
cause: combineCleanupError(error, cleanupError),
|
|
768
|
+
phase: "navigation-policy"
|
|
769
|
+
}
|
|
770
|
+
);
|
|
771
|
+
});
|
|
94
772
|
void this.navigationGuard.catch(() => void 0);
|
|
95
773
|
}
|
|
96
|
-
|
|
97
|
-
this.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
async
|
|
134
|
-
|
|
135
|
-
this.
|
|
136
|
-
|
|
774
|
+
async openTab(abortSignal, timeoutMs) {
|
|
775
|
+
return this.withActionTimeout(
|
|
776
|
+
abortSignal,
|
|
777
|
+
timeoutMs,
|
|
778
|
+
"open-tab",
|
|
779
|
+
(signal) => this.compatibilityMutex.run(
|
|
780
|
+
signal,
|
|
781
|
+
() => this.runContextActionScheduled(
|
|
782
|
+
signal,
|
|
783
|
+
"open-tab",
|
|
784
|
+
(scheduledSignal) => this.backend.command(
|
|
785
|
+
{ method: "openTab", params: {} },
|
|
786
|
+
{ abortSignal: scheduledSignal, cancelOperation: true, phase: "open-tab" }
|
|
787
|
+
)
|
|
788
|
+
)
|
|
789
|
+
)
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
async selectTab(tabId, abortSignal, timeoutMs) {
|
|
793
|
+
this.assertTabUsable(tabId);
|
|
794
|
+
return this.withActionTimeout(
|
|
795
|
+
abortSignal,
|
|
796
|
+
timeoutMs,
|
|
797
|
+
"select-tab",
|
|
798
|
+
(signal) => this.compatibilityMutex.run(
|
|
799
|
+
signal,
|
|
800
|
+
() => this.runContextActionScheduled(
|
|
801
|
+
signal,
|
|
802
|
+
"select-tab",
|
|
803
|
+
(scheduledSignal) => this.backend.command(
|
|
804
|
+
{ method: "selectTab", params: { tabId } },
|
|
805
|
+
{ abortSignal: scheduledSignal, cancelOperation: true, phase: "select-tab" }
|
|
806
|
+
)
|
|
807
|
+
)
|
|
808
|
+
)
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
async closeTab(tabId, abortSignal, timeoutMs) {
|
|
812
|
+
this.assertTabUsable(tabId);
|
|
813
|
+
this.closingTabs.add(tabId);
|
|
814
|
+
let closed = false;
|
|
815
|
+
try {
|
|
816
|
+
const result = await this.withActionTimeout(
|
|
817
|
+
abortSignal,
|
|
818
|
+
timeoutMs,
|
|
819
|
+
"close-tab",
|
|
820
|
+
(signal) => this.scheduler.run(`tab:${tabId}`, signal, (scheduledSignal) => {
|
|
821
|
+
return this.contextMutex.run(
|
|
822
|
+
scheduledSignal,
|
|
823
|
+
() => this.control.runAgentAction(async () => {
|
|
824
|
+
try {
|
|
825
|
+
return await this.backend.command(
|
|
826
|
+
{ method: "closeTab", params: { tabId } },
|
|
827
|
+
{ abortSignal: scheduledSignal, cancelOperation: true, phase: "close-tab" }
|
|
828
|
+
);
|
|
829
|
+
} catch (error) {
|
|
830
|
+
throw normalizeAutomationError(error, "close-tab");
|
|
831
|
+
}
|
|
832
|
+
})
|
|
833
|
+
);
|
|
834
|
+
})
|
|
835
|
+
);
|
|
836
|
+
closed = true;
|
|
837
|
+
return result;
|
|
838
|
+
} finally {
|
|
839
|
+
this.closingTabs.delete(tabId);
|
|
840
|
+
if (closed && !this.isClosed) this.rememberClosedTab(tabId);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async runTabCommand(options) {
|
|
844
|
+
return this.withActionTimeout(
|
|
845
|
+
options.abortSignal,
|
|
846
|
+
options.timeoutMs,
|
|
847
|
+
options.phase,
|
|
848
|
+
async (signal) => {
|
|
849
|
+
if (options.tabId !== void 0) {
|
|
850
|
+
this.assertTabUsable(options.tabId);
|
|
851
|
+
return this.runScheduledTabCommand(options.tabId, options, signal);
|
|
852
|
+
}
|
|
853
|
+
return this.compatibilityMutex.run(signal, async () => {
|
|
854
|
+
const tabId = await this.selectedTabId(signal);
|
|
855
|
+
this.assertTabUsable(tabId);
|
|
856
|
+
return this.runScheduledTabCommand(tabId, options, signal);
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
async disconnect(options = {}) {
|
|
862
|
+
assertOptionsObject(options);
|
|
863
|
+
if (options.timeoutMs !== void 0) assertPositiveSafeInteger2(options.timeoutMs, "timeoutMs");
|
|
864
|
+
if (this.disconnectCompleted) return;
|
|
865
|
+
if (options.abortSignal?.aborted) {
|
|
866
|
+
throw cancellationError(options.abortSignal.reason, "disconnect");
|
|
867
|
+
}
|
|
868
|
+
if (this.disconnectPromise !== void 0) {
|
|
869
|
+
return waitForSharedDisconnect(this.disconnectPromise, options);
|
|
870
|
+
}
|
|
871
|
+
this.markClosed();
|
|
872
|
+
const promise = this.backend.disconnect(options);
|
|
873
|
+
this.disconnectPromise = promise;
|
|
874
|
+
try {
|
|
875
|
+
await promise;
|
|
876
|
+
this.disconnectCompleted = true;
|
|
877
|
+
} finally {
|
|
878
|
+
if (this.disconnectPromise === promise) this.disconnectPromise = void 0;
|
|
879
|
+
}
|
|
137
880
|
}
|
|
138
881
|
async [Symbol.asyncDispose]() {
|
|
139
882
|
await this.disconnect();
|
|
140
883
|
}
|
|
141
|
-
|
|
142
|
-
return this.
|
|
884
|
+
async runScheduledTabCommand(tabId, options, abortSignal) {
|
|
885
|
+
return this.scheduler.run(
|
|
886
|
+
`tab:${tabId}`,
|
|
887
|
+
abortSignal,
|
|
888
|
+
(scheduledSignal) => this.control.runAgentAction(async () => {
|
|
889
|
+
await waitForOwnedPromise(this.navigationGuard, scheduledSignal);
|
|
890
|
+
try {
|
|
891
|
+
return await this.backend.command(options.command(tabId), {
|
|
892
|
+
abortSignal: scheduledSignal,
|
|
893
|
+
cancelOperation: true,
|
|
894
|
+
phase: options.phase
|
|
895
|
+
});
|
|
896
|
+
} catch (error) {
|
|
897
|
+
throw normalizeAutomationError(error, options.phase);
|
|
898
|
+
}
|
|
899
|
+
})
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
async selectedTabId(abortSignal) {
|
|
903
|
+
const tabs = await this.runContextActionScheduled(
|
|
904
|
+
abortSignal,
|
|
905
|
+
"selected-tab-lookup",
|
|
906
|
+
(signal) => this.backend.command(
|
|
907
|
+
{ method: "listTabs", params: {} },
|
|
908
|
+
{ abortSignal: signal, cancelOperation: true, phase: "selected-tab-lookup" }
|
|
909
|
+
)
|
|
910
|
+
);
|
|
911
|
+
const selected = tabs.find((tab) => tab.selected) ?? tabs[0];
|
|
912
|
+
if (selected === void 0) {
|
|
913
|
+
throw new BrowserError("Browser has no open tabs.", "invalid_state", {
|
|
914
|
+
phase: "selected-tab-lookup"
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
return selected.id;
|
|
918
|
+
}
|
|
919
|
+
runContextAction(abortSignal, timeoutMs, phase, operation) {
|
|
920
|
+
return this.withActionTimeout(
|
|
921
|
+
abortSignal,
|
|
922
|
+
timeoutMs,
|
|
923
|
+
phase,
|
|
924
|
+
(signal) => this.runContextActionScheduled(signal, phase, operation)
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
runContextActionScheduled(abortSignal, phase, operation) {
|
|
928
|
+
return this.scheduler.run(
|
|
929
|
+
"browser-context",
|
|
930
|
+
abortSignal,
|
|
931
|
+
(scheduledSignal) => this.contextMutex.run(
|
|
932
|
+
scheduledSignal,
|
|
933
|
+
() => this.control.runAgentAction(async () => {
|
|
934
|
+
this.assertOpen();
|
|
935
|
+
await waitForOwnedPromise(this.navigationGuard, scheduledSignal);
|
|
936
|
+
try {
|
|
937
|
+
return await operation(scheduledSignal);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
throw normalizeAutomationError(error, phase);
|
|
940
|
+
}
|
|
941
|
+
})
|
|
942
|
+
)
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
async withActionTimeout(abortSignal, timeoutMs, phase, operation) {
|
|
946
|
+
assertPositiveSafeInteger2(timeoutMs, "timeoutMs");
|
|
947
|
+
if (abortSignal?.aborted) throw cancellationError(abortSignal.reason, phase);
|
|
948
|
+
const timeoutController = new AbortController();
|
|
949
|
+
const timer = setTimeout(
|
|
950
|
+
() => timeoutController.abort(
|
|
951
|
+
new BrowserError("Browser action timed out.", "action_timeout", { phase })
|
|
952
|
+
),
|
|
953
|
+
timeoutMs
|
|
954
|
+
);
|
|
955
|
+
timer.unref?.();
|
|
956
|
+
const effectiveSignal = abortSignal === void 0 ? timeoutController.signal : AbortSignal.any([abortSignal, timeoutController.signal]);
|
|
957
|
+
try {
|
|
958
|
+
const value = await operation(effectiveSignal);
|
|
959
|
+
if (abortSignal?.aborted) throw cancellationError(abortSignal.reason, phase);
|
|
960
|
+
if (timeoutController.signal.aborted) throw timeoutController.signal.reason;
|
|
961
|
+
return value;
|
|
962
|
+
} catch (error) {
|
|
963
|
+
if (abortSignal?.aborted) throw cancellationError(abortSignal.reason, phase);
|
|
964
|
+
if (timeoutController.signal.aborted) throw timeoutController.signal.reason;
|
|
965
|
+
throw error;
|
|
966
|
+
} finally {
|
|
967
|
+
clearTimeout(timer);
|
|
968
|
+
}
|
|
143
969
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
throw new BrowserError(`Browser tab
|
|
970
|
+
assertTabUsable(tabId) {
|
|
971
|
+
this.assertOpen();
|
|
972
|
+
if (this.closingTabs.has(tabId) || this.closedTabs.has(tabId)) {
|
|
973
|
+
throw new BrowserError(`Browser tab is closing or closed: ${tabId}`, "invalid_state", {
|
|
974
|
+
phase: "tab-lookup"
|
|
975
|
+
});
|
|
148
976
|
}
|
|
149
|
-
|
|
977
|
+
}
|
|
978
|
+
rememberClosedTab(tabId) {
|
|
979
|
+
this.closedTabs.add(tabId);
|
|
980
|
+
if (this.closedTabs.size <= maxClosedTabTombstones) return;
|
|
981
|
+
const oldest = this.closedTabs.values().next().value;
|
|
982
|
+
if (oldest !== void 0) this.closedTabs.delete(oldest);
|
|
150
983
|
}
|
|
151
984
|
assertOpen() {
|
|
152
|
-
if (this.closed)
|
|
985
|
+
if (this.closed) {
|
|
986
|
+
throw new BrowserError("Browser connection is closed.", "connection_closed");
|
|
987
|
+
}
|
|
153
988
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
void this.disconnect().finally(() => finish(() => reject(abortSignal.reason)));
|
|
167
|
-
};
|
|
168
|
-
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
169
|
-
void operation().then(
|
|
170
|
-
(value) => finish(() => resolve(value)),
|
|
171
|
-
(error) => finish(() => reject(error))
|
|
172
|
-
);
|
|
173
|
-
});
|
|
989
|
+
markClosed() {
|
|
990
|
+
if (this.isClosed) return;
|
|
991
|
+
this.isClosed = true;
|
|
992
|
+
const error = new BrowserError("Browser connection is closed.", "connection_closed");
|
|
993
|
+
this.scheduler.close(error);
|
|
994
|
+
this.compatibilityMutex.close(error);
|
|
995
|
+
this.contextMutex.close(error);
|
|
996
|
+
this.closingTabs.clear();
|
|
997
|
+
this.closedTabs.clear();
|
|
998
|
+
this.removeDisconnectedListener?.();
|
|
999
|
+
this.removeDisconnectedListener = void 0;
|
|
1000
|
+
this.onClosed?.(this);
|
|
174
1001
|
}
|
|
175
1002
|
};
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
1003
|
+
function normalizeScheduling(value) {
|
|
1004
|
+
if (value === void 0) {
|
|
1005
|
+
return {
|
|
1006
|
+
mode: "serial",
|
|
1007
|
+
maxConcurrentResources: 1,
|
|
1008
|
+
maxQueuedActions: defaultMaxQueuedActions
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
assertOptionsObject(value);
|
|
1012
|
+
const maxQueuedActions = value.maxQueuedActions ?? defaultMaxQueuedActions;
|
|
1013
|
+
assertPositiveSafeInteger2(maxQueuedActions, "scheduling.maxQueuedActions");
|
|
1014
|
+
if (value.mode === "serial") {
|
|
1015
|
+
return { mode: "serial", maxConcurrentResources: 1, maxQueuedActions };
|
|
182
1016
|
}
|
|
183
|
-
|
|
1017
|
+
if (value.mode !== "per-tab") throw new TypeError("Unsupported browser scheduling mode.");
|
|
1018
|
+
const maxConcurrentResources = value.maxConcurrentTabs ?? defaultMaxConcurrentTabs;
|
|
1019
|
+
assertPositiveSafeInteger2(maxConcurrentResources, "scheduling.maxConcurrentTabs");
|
|
1020
|
+
return { mode: "per-tab", maxConcurrentResources, maxQueuedActions };
|
|
184
1021
|
}
|
|
185
|
-
function
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
1022
|
+
function normalizeAutomationError(error, phase) {
|
|
1023
|
+
if (error instanceof BrowserError) return error;
|
|
1024
|
+
if (error instanceof Error && /ERR_BLOCKED_BY_CLIENT|blockedbyclient/i.test(error.message)) {
|
|
1025
|
+
return new BrowserError("Browser navigation was rejected by policy.", "navigation_blocked", {
|
|
1026
|
+
cause: error,
|
|
1027
|
+
phase
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
1031
|
+
return new BrowserError("Browser action timed out.", "action_timeout", {
|
|
1032
|
+
cause: error,
|
|
1033
|
+
phase
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if (error instanceof Error && /tab does not exist|has no open tabs|target page.*closed|page has been closed/i.test(
|
|
1037
|
+
error.message
|
|
1038
|
+
)) {
|
|
1039
|
+
return new BrowserError(error.message, "invalid_state", { cause: error, phase });
|
|
1040
|
+
}
|
|
1041
|
+
if (error instanceof Error && /browser.*closed|disconnected|connection.*closed|websocket.*closed/i.test(error.message)) {
|
|
1042
|
+
return new BrowserError("Browser automation connection closed.", "connection_closed", {
|
|
1043
|
+
cause: error,
|
|
1044
|
+
phase
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
return new BrowserError("Browser tool operation failed.", "tool_failed", {
|
|
1048
|
+
cause: error,
|
|
1049
|
+
phase
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
function assertPositiveSafeInteger2(value, name) {
|
|
1053
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maxTimerMs) {
|
|
1054
|
+
throw new RangeError(`${name} must be a positive integer no greater than ${maxTimerMs}.`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function assertOptionsObject(value) {
|
|
1058
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1059
|
+
throw new TypeError("options must be an object.");
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function combineCleanupError(error, cleanupError) {
|
|
1063
|
+
return cleanupError === void 0 ? error : new AggregateError([error, cleanupError], "Browser automation cleanup also failed.");
|
|
1064
|
+
}
|
|
1065
|
+
async function waitForOwnedPromise(promise, abortSignal) {
|
|
1066
|
+
abortSignal.throwIfAborted();
|
|
1067
|
+
return new Promise((resolve, reject) => {
|
|
1068
|
+
let settled = false;
|
|
1069
|
+
const finish = (callback) => {
|
|
1070
|
+
if (settled) return;
|
|
1071
|
+
settled = true;
|
|
1072
|
+
abortSignal.removeEventListener("abort", abort);
|
|
1073
|
+
callback();
|
|
1074
|
+
};
|
|
1075
|
+
const abort = () => finish(() => reject(abortSignal.reason));
|
|
1076
|
+
abortSignal.addEventListener("abort", abort, { once: true });
|
|
1077
|
+
void promise.then(
|
|
1078
|
+
(value) => finish(() => resolve(value)),
|
|
1079
|
+
(error) => finish(() => reject(error))
|
|
1080
|
+
);
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
async function waitForSharedDisconnect(promise, options) {
|
|
1084
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
1085
|
+
assertPositiveSafeInteger2(timeoutMs, "timeoutMs");
|
|
1086
|
+
if (options.abortSignal?.aborted) {
|
|
1087
|
+
throw cancellationError(options.abortSignal.reason, "disconnect");
|
|
1088
|
+
}
|
|
1089
|
+
const controller = new AbortController();
|
|
1090
|
+
const timer = setTimeout(
|
|
1091
|
+
() => controller.abort(
|
|
1092
|
+
new BrowserError("Browser disconnect timed out.", "connection_timeout", {
|
|
1093
|
+
phase: "disconnect"
|
|
1094
|
+
})
|
|
1095
|
+
),
|
|
1096
|
+
timeoutMs
|
|
1097
|
+
);
|
|
1098
|
+
timer.unref?.();
|
|
1099
|
+
const signal = options.abortSignal === void 0 ? controller.signal : AbortSignal.any([options.abortSignal, controller.signal]);
|
|
1100
|
+
try {
|
|
1101
|
+
await waitForOwnedPromise(promise, signal);
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
if (options.abortSignal?.aborted) {
|
|
1104
|
+
throw cancellationError(options.abortSignal.reason, "disconnect");
|
|
1105
|
+
}
|
|
1106
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
1107
|
+
throw error;
|
|
1108
|
+
} finally {
|
|
1109
|
+
clearTimeout(timer);
|
|
191
1110
|
}
|
|
192
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
193
|
-
if (url.username.length > 0 || url.password.length > 0) return false;
|
|
194
|
-
return policy.mode === "allow-all-http" || policy.origins.includes(url.origin);
|
|
195
1111
|
}
|
|
196
1112
|
function asConnection(connection) {
|
|
197
1113
|
if (!(connection instanceof PlaywrightBrowserConnectionImpl)) {
|
|
@@ -201,27 +1117,48 @@ function asConnection(connection) {
|
|
|
201
1117
|
}
|
|
202
1118
|
|
|
203
1119
|
// src/control.ts
|
|
204
|
-
import { randomUUID
|
|
1120
|
+
import { randomUUID } from "crypto";
|
|
1121
|
+
var defaultAcquireTimeoutMs = 3e4;
|
|
1122
|
+
var maxTimerMs2 = 2147483647;
|
|
205
1123
|
var BrowserControlState = class {
|
|
206
1124
|
activeAgentActions = 0;
|
|
207
1125
|
pendingAcquire;
|
|
208
1126
|
activeLease;
|
|
209
1127
|
humanPending = false;
|
|
210
1128
|
destroyed = false;
|
|
1129
|
+
availability = "available";
|
|
211
1130
|
snapshot() {
|
|
212
1131
|
const lease = this.activeLease;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
1132
|
+
const state = lease !== void 0 ? "human" : this.humanPending ? "human-pending" : this.activeAgentActions > 0 ? "agent-active" : "agent";
|
|
1133
|
+
return Object.freeze({
|
|
1134
|
+
mode: lease === void 0 ? "agent" : "human",
|
|
1135
|
+
state,
|
|
1136
|
+
availability: this.availability,
|
|
1137
|
+
activeAgentActions: this.activeAgentActions,
|
|
1138
|
+
humanPending: this.humanPending,
|
|
1139
|
+
...lease === void 0 ? {} : {
|
|
1140
|
+
ownerId: lease.ownerId,
|
|
1141
|
+
expiresAt: lease.expiresAt
|
|
1142
|
+
}
|
|
217
1143
|
});
|
|
218
1144
|
}
|
|
219
1145
|
async acquireHumanControl(options) {
|
|
220
1146
|
validateAcquireOptions(options);
|
|
221
1147
|
this.assertActive();
|
|
222
|
-
options.abortSignal?.
|
|
223
|
-
|
|
224
|
-
|
|
1148
|
+
if (options.abortSignal?.aborted) {
|
|
1149
|
+
throw cancellationError(options.abortSignal.reason, "human-control-acquire");
|
|
1150
|
+
}
|
|
1151
|
+
if (this.activeLease !== void 0) {
|
|
1152
|
+
throw new BrowserError("Browser human control is already acquired.", "human_controlled", {
|
|
1153
|
+
phase: "human-control-acquire"
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
if (this.humanPending) {
|
|
1157
|
+
throw new BrowserError(
|
|
1158
|
+
"Browser human control acquisition is already pending.",
|
|
1159
|
+
"human_control_conflict",
|
|
1160
|
+
{ phase: "human-control-acquire" }
|
|
1161
|
+
);
|
|
225
1162
|
}
|
|
226
1163
|
this.humanPending = true;
|
|
227
1164
|
try {
|
|
@@ -232,16 +1169,30 @@ var BrowserControlState = class {
|
|
|
232
1169
|
pending.abortSignal = options.abortSignal;
|
|
233
1170
|
pending.abort = () => {
|
|
234
1171
|
if (this.pendingAcquire !== pending) return;
|
|
235
|
-
this.
|
|
236
|
-
reject(options.abortSignal?.reason);
|
|
1172
|
+
this.clearPendingAcquire(pending);
|
|
1173
|
+
reject(cancellationError(options.abortSignal?.reason, "human-control-acquire"));
|
|
237
1174
|
};
|
|
238
1175
|
options.abortSignal.addEventListener("abort", pending.abort, { once: true });
|
|
239
1176
|
}
|
|
1177
|
+
pending.timer = setTimeout(() => {
|
|
1178
|
+
if (this.pendingAcquire !== pending) return;
|
|
1179
|
+
this.clearPendingAcquire(pending);
|
|
1180
|
+
reject(
|
|
1181
|
+
new BrowserError(
|
|
1182
|
+
"Timed out waiting to acquire browser human control.",
|
|
1183
|
+
"human_control_conflict",
|
|
1184
|
+
{ phase: "human-control-acquire" }
|
|
1185
|
+
)
|
|
1186
|
+
);
|
|
1187
|
+
}, options.timeoutMs ?? defaultAcquireTimeoutMs);
|
|
1188
|
+
pending.timer.unref?.();
|
|
240
1189
|
this.pendingAcquire = pending;
|
|
241
1190
|
});
|
|
242
1191
|
}
|
|
243
1192
|
this.assertActive();
|
|
244
|
-
options.abortSignal?.
|
|
1193
|
+
if (options.abortSignal?.aborted) {
|
|
1194
|
+
throw cancellationError(options.abortSignal.reason, "human-control-acquire");
|
|
1195
|
+
}
|
|
245
1196
|
const lease = new BrowserHumanControlLeaseImpl({
|
|
246
1197
|
owner: this,
|
|
247
1198
|
ownerId: options.ownerId,
|
|
@@ -252,118 +1203,802 @@ var BrowserControlState = class {
|
|
|
252
1203
|
} finally {
|
|
253
1204
|
this.humanPending = false;
|
|
254
1205
|
}
|
|
255
|
-
}
|
|
256
|
-
async runAgentAction(operation) {
|
|
257
|
-
this.assertActive();
|
|
258
|
-
if (this.activeLease !== void 0
|
|
259
|
-
throw new BrowserError("Browser is controlled by a human viewer.", "human_controlled"
|
|
1206
|
+
}
|
|
1207
|
+
async runAgentAction(operation) {
|
|
1208
|
+
this.assertActive();
|
|
1209
|
+
if (this.activeLease !== void 0) {
|
|
1210
|
+
throw new BrowserError("Browser is controlled by a human viewer.", "human_controlled", {
|
|
1211
|
+
phase: "agent-action"
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
if (this.humanPending) {
|
|
1215
|
+
throw new BrowserError(
|
|
1216
|
+
"Browser human control acquisition is pending.",
|
|
1217
|
+
"human_control_conflict",
|
|
1218
|
+
{ phase: "agent-action" }
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
this.activeAgentActions += 1;
|
|
1222
|
+
try {
|
|
1223
|
+
return await operation();
|
|
1224
|
+
} finally {
|
|
1225
|
+
this.activeAgentActions -= 1;
|
|
1226
|
+
if (this.activeAgentActions === 0) this.resolvePendingAcquire();
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
renewLease(lease, options) {
|
|
1230
|
+
if (this.activeLease !== lease) {
|
|
1231
|
+
throw new BrowserError("Browser human control lease is no longer active.", "invalid_state");
|
|
1232
|
+
}
|
|
1233
|
+
assertPositiveSafeInteger3(options.leaseTimeoutMs, "leaseTimeoutMs");
|
|
1234
|
+
lease.resetExpiration(options.leaseTimeoutMs);
|
|
1235
|
+
return this.snapshot();
|
|
1236
|
+
}
|
|
1237
|
+
releaseLease(lease) {
|
|
1238
|
+
if (this.activeLease !== lease) return;
|
|
1239
|
+
this.activeLease = void 0;
|
|
1240
|
+
lease.markReleased();
|
|
1241
|
+
}
|
|
1242
|
+
destroy() {
|
|
1243
|
+
if (this.destroyed) return;
|
|
1244
|
+
this.destroyed = true;
|
|
1245
|
+
this.activeLease?.release();
|
|
1246
|
+
if (this.pendingAcquire !== void 0) {
|
|
1247
|
+
const pending = this.pendingAcquire;
|
|
1248
|
+
this.pendingAcquire = void 0;
|
|
1249
|
+
this.clearPendingAcquire(pending);
|
|
1250
|
+
pending.reject(new BrowserError("Browser was destroyed.", "runtime_destroyed"));
|
|
1251
|
+
}
|
|
1252
|
+
this.humanPending = false;
|
|
1253
|
+
this.availability = "destroyed";
|
|
1254
|
+
}
|
|
1255
|
+
setAvailability(availability) {
|
|
1256
|
+
if (this.destroyed) return;
|
|
1257
|
+
if (availability === "destroyed") {
|
|
1258
|
+
this.destroy();
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
this.availability = availability;
|
|
1262
|
+
if (availability !== "disconnected") return;
|
|
1263
|
+
this.activeLease?.release();
|
|
1264
|
+
if (this.pendingAcquire !== void 0) {
|
|
1265
|
+
const pending = this.pendingAcquire;
|
|
1266
|
+
this.clearPendingAcquire(pending);
|
|
1267
|
+
pending.reject(
|
|
1268
|
+
new BrowserError("Browser runtime is disconnected.", "connection_closed", {
|
|
1269
|
+
phase: "human-control-acquire"
|
|
1270
|
+
})
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
this.humanPending = false;
|
|
1274
|
+
}
|
|
1275
|
+
assertActive() {
|
|
1276
|
+
if (this.destroyed) throw new BrowserError("Browser was destroyed.", "runtime_destroyed");
|
|
1277
|
+
if (this.availability === "disconnected") {
|
|
1278
|
+
throw new BrowserError("Browser runtime is disconnected.", "connection_closed");
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
resolvePendingAcquire() {
|
|
1282
|
+
const pending = this.pendingAcquire;
|
|
1283
|
+
if (pending === void 0) return;
|
|
1284
|
+
this.clearPendingAcquire(pending);
|
|
1285
|
+
pending.resolve();
|
|
1286
|
+
}
|
|
1287
|
+
clearPendingAcquire(pending) {
|
|
1288
|
+
if (this.pendingAcquire === pending) this.pendingAcquire = void 0;
|
|
1289
|
+
if (pending.timer !== void 0) clearTimeout(pending.timer);
|
|
1290
|
+
if (pending.abort !== void 0) {
|
|
1291
|
+
pending.abortSignal?.removeEventListener("abort", pending.abort);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
var BrowserHumanControlLeaseImpl = class {
|
|
1296
|
+
id = randomUUID();
|
|
1297
|
+
ownerId;
|
|
1298
|
+
owner;
|
|
1299
|
+
expiration = 0;
|
|
1300
|
+
timer;
|
|
1301
|
+
released = false;
|
|
1302
|
+
constructor(options) {
|
|
1303
|
+
this.owner = options.owner;
|
|
1304
|
+
this.ownerId = options.ownerId;
|
|
1305
|
+
this.resetExpiration(options.leaseTimeoutMs);
|
|
1306
|
+
}
|
|
1307
|
+
get expiresAt() {
|
|
1308
|
+
return new Date(this.expiration).toISOString();
|
|
1309
|
+
}
|
|
1310
|
+
renew(options) {
|
|
1311
|
+
if (this.released) {
|
|
1312
|
+
throw new BrowserError("Browser human control lease is no longer active.", "invalid_state");
|
|
1313
|
+
}
|
|
1314
|
+
return this.owner.renewLease(this, options);
|
|
1315
|
+
}
|
|
1316
|
+
release() {
|
|
1317
|
+
this.owner.releaseLease(this);
|
|
1318
|
+
}
|
|
1319
|
+
markReleased() {
|
|
1320
|
+
if (this.released) return;
|
|
1321
|
+
this.released = true;
|
|
1322
|
+
if (this.timer !== void 0) clearTimeout(this.timer);
|
|
1323
|
+
this.timer = void 0;
|
|
1324
|
+
}
|
|
1325
|
+
resetExpiration(leaseTimeoutMs) {
|
|
1326
|
+
if (this.timer !== void 0) clearTimeout(this.timer);
|
|
1327
|
+
this.expiration = Date.now() + leaseTimeoutMs;
|
|
1328
|
+
this.timer = setTimeout(() => this.release(), leaseTimeoutMs);
|
|
1329
|
+
this.timer.unref?.();
|
|
1330
|
+
}
|
|
1331
|
+
async [Symbol.asyncDispose]() {
|
|
1332
|
+
this.release();
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
function validateAcquireOptions(options) {
|
|
1336
|
+
if (!isRecord2(options)) throw new TypeError("options must be an object.");
|
|
1337
|
+
if (typeof options.ownerId !== "string" || options.ownerId.length === 0) {
|
|
1338
|
+
throw new TypeError("ownerId must be a non-empty string.");
|
|
1339
|
+
}
|
|
1340
|
+
assertPositiveSafeInteger3(options.leaseTimeoutMs, "leaseTimeoutMs");
|
|
1341
|
+
if (options.timeoutMs !== void 0) assertPositiveSafeInteger3(options.timeoutMs, "timeoutMs");
|
|
1342
|
+
}
|
|
1343
|
+
function assertPositiveSafeInteger3(value, name) {
|
|
1344
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maxTimerMs2) {
|
|
1345
|
+
throw new RangeError(`${name} must be a positive integer no greater than ${maxTimerMs2}.`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function isRecord2(value) {
|
|
1349
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// src/lifecycle.ts
|
|
1353
|
+
var defaultLifecycleTimeoutMs = 12e4;
|
|
1354
|
+
var maxTimerMs3 = 2147483647;
|
|
1355
|
+
function boundedSignal(options, defaultTimeoutMs, phase) {
|
|
1356
|
+
const timeoutMs = options.timeoutMs ?? defaultTimeoutMs;
|
|
1357
|
+
assertPositiveSafeInteger4(timeoutMs, "timeoutMs");
|
|
1358
|
+
if (options.abortSignal?.aborted) throw cancellationError(options.abortSignal.reason, phase);
|
|
1359
|
+
const controller = new AbortController();
|
|
1360
|
+
const timer = setTimeout(
|
|
1361
|
+
() => controller.abort(
|
|
1362
|
+
new BrowserError(`Browser lifecycle operation timed out: ${phase}.`, "lifecycle_timeout", {
|
|
1363
|
+
phase
|
|
1364
|
+
})
|
|
1365
|
+
),
|
|
1366
|
+
timeoutMs
|
|
1367
|
+
);
|
|
1368
|
+
timer.unref?.();
|
|
1369
|
+
const signal = options.abortSignal === void 0 ? controller.signal : AbortSignal.any([options.abortSignal, controller.signal]);
|
|
1370
|
+
return {
|
|
1371
|
+
signal,
|
|
1372
|
+
timedOut: () => controller.signal.aborted,
|
|
1373
|
+
throwIfAborted: () => {
|
|
1374
|
+
if (options.abortSignal?.aborted) {
|
|
1375
|
+
throw cancellationError(options.abortSignal.reason, phase);
|
|
1376
|
+
}
|
|
1377
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
1378
|
+
},
|
|
1379
|
+
normalize: (error, message, code) => {
|
|
1380
|
+
if (options.abortSignal?.aborted) {
|
|
1381
|
+
return error instanceof AggregateError ? new BrowserError("Browser operation was cancelled by the caller.", "cancelled", {
|
|
1382
|
+
cause: error,
|
|
1383
|
+
phase
|
|
1384
|
+
}) : cancellationError(options.abortSignal.reason, phase);
|
|
1385
|
+
}
|
|
1386
|
+
if (controller.signal.aborted) {
|
|
1387
|
+
const reason = controller.signal.reason;
|
|
1388
|
+
return error === reason ? reason : new BrowserError(reason.message, "lifecycle_timeout", { cause: error, phase });
|
|
1389
|
+
}
|
|
1390
|
+
if (error instanceof BrowserError) return error;
|
|
1391
|
+
return new BrowserError(message, code, { cause: error, phase });
|
|
1392
|
+
},
|
|
1393
|
+
dispose: () => clearTimeout(timer)
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
function validateLifecycleOptions(options, phase) {
|
|
1397
|
+
if (options.timeoutMs !== void 0) assertPositiveSafeInteger4(options.timeoutMs, "timeoutMs");
|
|
1398
|
+
if (options.abortSignal?.aborted) {
|
|
1399
|
+
throw cancellationError(options.abortSignal.reason, phase);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async function waitForSharedLifecycle(promise, options, phase) {
|
|
1403
|
+
const bounded = boundedSignal(options, defaultLifecycleTimeoutMs, phase);
|
|
1404
|
+
try {
|
|
1405
|
+
await waitForPromise(promise, bounded.signal);
|
|
1406
|
+
bounded.throwIfAborted();
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
throw bounded.normalize(error, `Unable to wait for ${phase}.`, "transport_failure");
|
|
1409
|
+
} finally {
|
|
1410
|
+
bounded.dispose();
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
function assertPositiveSafeInteger4(value, name) {
|
|
1414
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maxTimerMs3) {
|
|
1415
|
+
throw new RangeError(`${name} must be a positive integer no greater than ${maxTimerMs3}.`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function assertOptionsObject2(value) {
|
|
1419
|
+
if (!isRecord3(value)) throw new TypeError("options must be an object.");
|
|
1420
|
+
}
|
|
1421
|
+
function isRecord3(value) {
|
|
1422
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1423
|
+
}
|
|
1424
|
+
async function waitForPromise(promise, abortSignal) {
|
|
1425
|
+
abortSignal.throwIfAborted();
|
|
1426
|
+
return new Promise((resolve, reject) => {
|
|
1427
|
+
let settled = false;
|
|
1428
|
+
const finish = (callback) => {
|
|
1429
|
+
if (settled) return;
|
|
1430
|
+
settled = true;
|
|
1431
|
+
abortSignal.removeEventListener("abort", abort);
|
|
1432
|
+
callback();
|
|
1433
|
+
};
|
|
1434
|
+
const abort = () => finish(() => reject(abortSignal.reason));
|
|
1435
|
+
abortSignal.addEventListener("abort", abort, { once: true });
|
|
1436
|
+
void promise.then(
|
|
1437
|
+
(value) => finish(() => resolve(value)),
|
|
1438
|
+
(error) => finish(() => reject(error))
|
|
1439
|
+
);
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// src/readiness.ts
|
|
1444
|
+
var cdpPort = 9222;
|
|
1445
|
+
var noVncPort = 6080;
|
|
1446
|
+
var browserCapabilities = ["runtime", "browser", "automation", "desktop"];
|
|
1447
|
+
var automationProbeAttemptTimeoutMs = 5e3;
|
|
1448
|
+
async function probeBrowserCapability(options) {
|
|
1449
|
+
const { capability, sandbox, abortSignal, deadline } = options;
|
|
1450
|
+
switch (capability) {
|
|
1451
|
+
case "runtime":
|
|
1452
|
+
if (sandbox.state !== "running") {
|
|
1453
|
+
throw new BrowserError(`Browser is not running: ${sandbox.state}`, "invalid_state");
|
|
1454
|
+
}
|
|
1455
|
+
return;
|
|
1456
|
+
case "browser":
|
|
1457
|
+
await sandbox.runtime.waitForPort({
|
|
1458
|
+
containerPort: cdpPort,
|
|
1459
|
+
timeoutMs: remainingTime(deadline),
|
|
1460
|
+
abortSignal
|
|
1461
|
+
});
|
|
1462
|
+
await assertCdpHttpReady(`${endpointFor(sandbox, cdpPort)}/json/version`, abortSignal);
|
|
1463
|
+
return;
|
|
1464
|
+
case "desktop":
|
|
1465
|
+
await sandbox.runtime.waitForPort({
|
|
1466
|
+
containerPort: noVncPort,
|
|
1467
|
+
timeoutMs: remainingTime(deadline),
|
|
1468
|
+
abortSignal
|
|
1469
|
+
});
|
|
1470
|
+
await assertHttpReady(`${endpointFor(sandbox, noVncPort)}/vnc.html`, abortSignal);
|
|
1471
|
+
return;
|
|
1472
|
+
case "automation": {
|
|
1473
|
+
await sandbox.runtime.waitForPort({
|
|
1474
|
+
containerPort: cdpPort,
|
|
1475
|
+
timeoutMs: remainingTime(deadline),
|
|
1476
|
+
abortSignal
|
|
1477
|
+
});
|
|
1478
|
+
await assertCdpHttpReady(`${endpointFor(sandbox, cdpPort)}/json/version`, abortSignal);
|
|
1479
|
+
let lastProbeFailure;
|
|
1480
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1481
|
+
abortSignal.throwIfAborted();
|
|
1482
|
+
try {
|
|
1483
|
+
const connection = await options.connectBrowser({
|
|
1484
|
+
endpointUrl: endpointFor(sandbox, cdpPort),
|
|
1485
|
+
control: options.control,
|
|
1486
|
+
timeoutMs: Math.min(automationProbeAttemptTimeoutMs, remainingTime(deadline)),
|
|
1487
|
+
abortSignal
|
|
1488
|
+
});
|
|
1489
|
+
try {
|
|
1490
|
+
if (connection.closed) {
|
|
1491
|
+
throw new Error("CDP connection closed during initialization.");
|
|
1492
|
+
}
|
|
1493
|
+
} finally {
|
|
1494
|
+
await connection.disconnect({
|
|
1495
|
+
timeoutMs: remainingTime(deadline),
|
|
1496
|
+
abortSignal
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
abortSignal.throwIfAborted();
|
|
1500
|
+
return;
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
if (abortSignal.aborted) throw lastProbeFailure ?? error;
|
|
1503
|
+
lastProbeFailure = error;
|
|
1504
|
+
try {
|
|
1505
|
+
await waitForRetry(abortSignal, Math.min(500, 50 * 2 ** Math.min(attempt, 3)));
|
|
1506
|
+
} catch (retryError) {
|
|
1507
|
+
if (abortSignal.aborted) throw lastProbeFailure;
|
|
1508
|
+
throw retryError;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
function endpointFor(sandbox, containerPort) {
|
|
1516
|
+
const published = publishedPort(sandbox, containerPort);
|
|
1517
|
+
return `http://${hostForUrl(published.host)}:${published.hostPort}`;
|
|
1518
|
+
}
|
|
1519
|
+
function normalizeReadinessError(error, capability, state) {
|
|
1520
|
+
if (state.callerAborted) {
|
|
1521
|
+
return new BrowserError("Browser readiness was cancelled by the caller.", "cancelled", {
|
|
1522
|
+
cause: error,
|
|
1523
|
+
capability,
|
|
1524
|
+
phase: "readiness"
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
if (state.timedOut) {
|
|
1528
|
+
if (error instanceof BrowserError && error.code === "transport_failure") {
|
|
1529
|
+
return new BrowserError(error.message, error.code, {
|
|
1530
|
+
cause: error,
|
|
1531
|
+
capability,
|
|
1532
|
+
phase: "readiness",
|
|
1533
|
+
recovery: error.recovery,
|
|
1534
|
+
retryable: error.retryable
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
return new BrowserError(
|
|
1538
|
+
`Browser ${capability} capability did not become ready before the timeout.`,
|
|
1539
|
+
"readiness_timeout",
|
|
1540
|
+
{ cause: error, capability, phase: "readiness" }
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
if (error instanceof BrowserError && (error.code === "connection_closed" || error.code === "runtime_destroyed")) {
|
|
1544
|
+
return error;
|
|
1545
|
+
}
|
|
1546
|
+
return new BrowserError(`Browser ${capability} capability is not ready.`, "not_ready", {
|
|
1547
|
+
cause: error,
|
|
1548
|
+
capability,
|
|
1549
|
+
phase: "readiness"
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
async function assertCdpHttpReady(url, abortSignal) {
|
|
1553
|
+
await assertHttpReady(url, abortSignal, async (response) => {
|
|
1554
|
+
const value = await response.json();
|
|
1555
|
+
if (!isRecord4(value) || typeof value.webSocketDebuggerUrl !== "string" || !value.webSocketDebuggerUrl.startsWith("ws")) {
|
|
1556
|
+
throw new Error("CDP version endpoint did not expose a WebSocket debugger URL.");
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
async function assertHttpReady(url, abortSignal, verify) {
|
|
1561
|
+
while (true) {
|
|
1562
|
+
abortSignal.throwIfAborted();
|
|
1563
|
+
try {
|
|
1564
|
+
const response = await fetch(url, { signal: abortSignal, redirect: "error" });
|
|
1565
|
+
try {
|
|
1566
|
+
if (response.ok) {
|
|
1567
|
+
await verify?.(response);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
} finally {
|
|
1571
|
+
await response.body?.cancel().catch(() => void 0);
|
|
1572
|
+
}
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
if (abortSignal.aborted) throw abortSignal.reason ?? error;
|
|
1575
|
+
}
|
|
1576
|
+
await waitForRetry(abortSignal);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
async function waitForRetry(abortSignal, delayMs = 50) {
|
|
1580
|
+
await new Promise((resolve, reject) => {
|
|
1581
|
+
const timer = setTimeout(() => finish(), delayMs);
|
|
1582
|
+
timer.unref?.();
|
|
1583
|
+
const abort = () => finish(abortSignal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
1584
|
+
abortSignal.addEventListener("abort", abort, { once: true });
|
|
1585
|
+
function finish(error) {
|
|
1586
|
+
clearTimeout(timer);
|
|
1587
|
+
abortSignal.removeEventListener("abort", abort);
|
|
1588
|
+
if (error === void 0) resolve();
|
|
1589
|
+
else reject(error);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
function remainingTime(deadline) {
|
|
1594
|
+
return Math.max(1, deadline - Date.now());
|
|
1595
|
+
}
|
|
1596
|
+
function publishedPort(sandbox, containerPort) {
|
|
1597
|
+
const port = sandbox.runtime.publishedPorts.find(
|
|
1598
|
+
(candidate) => candidate.containerPort === containerPort && candidate.protocol === "tcp"
|
|
1599
|
+
);
|
|
1600
|
+
if (port === void 0) {
|
|
1601
|
+
throw new BrowserError(`Browser port is not published: ${containerPort}`, "invalid_state");
|
|
1602
|
+
}
|
|
1603
|
+
return port;
|
|
1604
|
+
}
|
|
1605
|
+
function hostForUrl(host) {
|
|
1606
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
1607
|
+
}
|
|
1608
|
+
function isRecord4(value) {
|
|
1609
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// src/docker-browser.ts
|
|
1613
|
+
var defaultConnectTimeoutMs = 3e4;
|
|
1614
|
+
var capabilities = browserCapabilities;
|
|
1615
|
+
var DockerBrowserHandle = class {
|
|
1616
|
+
id;
|
|
1617
|
+
sandbox;
|
|
1618
|
+
desktop;
|
|
1619
|
+
control = new BrowserControlState();
|
|
1620
|
+
connections = /* @__PURE__ */ new Set();
|
|
1621
|
+
ownedOperations = /* @__PURE__ */ new Set();
|
|
1622
|
+
capabilityStates = new Map(
|
|
1623
|
+
capabilities.map((capability) => [capability, Object.freeze({ capability, state: "unknown" })])
|
|
1624
|
+
);
|
|
1625
|
+
capabilityGenerations = new Map(
|
|
1626
|
+
capabilities.map((capability) => [capability, 0])
|
|
1627
|
+
);
|
|
1628
|
+
handleState = "active";
|
|
1629
|
+
connectionPending = false;
|
|
1630
|
+
stopPromise;
|
|
1631
|
+
destroyPromise;
|
|
1632
|
+
connectBrowser;
|
|
1633
|
+
constructor(sandbox, connectBrowser = connectPlaywrightBrowser) {
|
|
1634
|
+
this.sandbox = sandbox;
|
|
1635
|
+
this.id = sandbox.id;
|
|
1636
|
+
this.connectBrowser = connectBrowser;
|
|
1637
|
+
this.desktop = Object.freeze({
|
|
1638
|
+
protocol: "novnc",
|
|
1639
|
+
containerPort: noVncPort,
|
|
1640
|
+
control: this.control
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
get state() {
|
|
1644
|
+
return this.handleState === "active" ? this.sandbox.state : this.handleState;
|
|
1645
|
+
}
|
|
1646
|
+
inspector(options) {
|
|
1647
|
+
return this.sandbox.inspector(options);
|
|
1648
|
+
}
|
|
1649
|
+
readiness() {
|
|
1650
|
+
const terminal = this.handleState === "destroyed" || this.handleState === "destroying" ? "destroyed" : this.handleState === "stopped" || this.handleState === "stopping" ? "stopped" : void 0;
|
|
1651
|
+
const entries = Object.fromEntries(
|
|
1652
|
+
capabilities.map((capability) => {
|
|
1653
|
+
const current = this.capabilityStates.get(capability);
|
|
1654
|
+
return [
|
|
1655
|
+
capability,
|
|
1656
|
+
terminal === void 0 ? current : Object.freeze({ capability, state: terminal })
|
|
1657
|
+
];
|
|
1658
|
+
})
|
|
1659
|
+
);
|
|
1660
|
+
const values = Object.values(entries);
|
|
1661
|
+
const state = this.handleState === "error" ? "failed" : terminal ?? (values.some((value) => value.state === "checking") ? "checking" : values.every((value) => value.state === "unknown") ? "unknown" : values.every((value) => value.state === "ready") ? "ready" : values.some((value) => value.state === "failed") ? values.some((value) => value.state === "ready") ? "degraded" : "failed" : "partial");
|
|
1662
|
+
return Object.freeze({ state, capabilities: Object.freeze(entries) });
|
|
1663
|
+
}
|
|
1664
|
+
async waitForCapabilities(options) {
|
|
1665
|
+
validateReadinessOptions(options);
|
|
1666
|
+
this.assertRunning();
|
|
1667
|
+
if (options.abortSignal?.aborted) {
|
|
1668
|
+
throw cancellationError(options.abortSignal.reason, "readiness");
|
|
1669
|
+
}
|
|
1670
|
+
const requested = options.capabilities ?? capabilities;
|
|
1671
|
+
const ownedController = new AbortController();
|
|
1672
|
+
const timeoutController = new AbortController();
|
|
1673
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
1674
|
+
const timer = setTimeout(
|
|
1675
|
+
() => timeoutController.abort(
|
|
1676
|
+
new BrowserError("Browser readiness timed out.", "readiness_timeout", {
|
|
1677
|
+
phase: "readiness"
|
|
1678
|
+
})
|
|
1679
|
+
),
|
|
1680
|
+
options.timeoutMs
|
|
1681
|
+
);
|
|
1682
|
+
timer.unref?.();
|
|
1683
|
+
const signals = [ownedController.signal, timeoutController.signal];
|
|
1684
|
+
if (options.abortSignal !== void 0) signals.push(options.abortSignal);
|
|
1685
|
+
const abortSignal = AbortSignal.any(signals);
|
|
1686
|
+
const previous = new Map(
|
|
1687
|
+
requested.map((capability) => [capability, this.capabilityStates.get(capability)])
|
|
1688
|
+
);
|
|
1689
|
+
const generations = new Map(
|
|
1690
|
+
requested.map((capability) => {
|
|
1691
|
+
const generation = (this.capabilityGenerations.get(capability) ?? 0) + 1;
|
|
1692
|
+
this.capabilityGenerations.set(capability, generation);
|
|
1693
|
+
return [capability, generation];
|
|
1694
|
+
})
|
|
1695
|
+
);
|
|
1696
|
+
for (const capability of requested) this.setCapability(capability, "checking");
|
|
1697
|
+
const promise = Promise.allSettled(
|
|
1698
|
+
requested.map(async (capability) => {
|
|
1699
|
+
try {
|
|
1700
|
+
await probeBrowserCapability({
|
|
1701
|
+
capability,
|
|
1702
|
+
sandbox: this.sandbox,
|
|
1703
|
+
control: this.control,
|
|
1704
|
+
connectBrowser: this.connectBrowser,
|
|
1705
|
+
abortSignal,
|
|
1706
|
+
deadline
|
|
1707
|
+
});
|
|
1708
|
+
abortSignal.throwIfAborted();
|
|
1709
|
+
if (this.capabilityGenerations.get(capability) === generations.get(capability)) {
|
|
1710
|
+
this.setCapability(capability, "ready");
|
|
1711
|
+
}
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
const normalized = normalizeReadinessError(error, capability, {
|
|
1714
|
+
callerAborted: options.abortSignal?.aborted === true,
|
|
1715
|
+
timedOut: timeoutController.signal.aborted
|
|
1716
|
+
});
|
|
1717
|
+
if (this.capabilityGenerations.get(capability) === generations.get(capability)) {
|
|
1718
|
+
if (options.abortSignal?.aborted) {
|
|
1719
|
+
this.capabilityStates.set(capability, previous.get(capability));
|
|
1720
|
+
} else {
|
|
1721
|
+
this.setCapability(capability, "failed", normalized);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
throw normalized;
|
|
1725
|
+
}
|
|
1726
|
+
})
|
|
1727
|
+
);
|
|
1728
|
+
const owned = { controller: ownedController, promise };
|
|
1729
|
+
this.ownedOperations.add(owned);
|
|
1730
|
+
try {
|
|
1731
|
+
const results = await promise;
|
|
1732
|
+
if (options.abortSignal?.aborted) {
|
|
1733
|
+
throw cancellationError(options.abortSignal.reason, "readiness");
|
|
1734
|
+
}
|
|
1735
|
+
const failures = results.filter(
|
|
1736
|
+
(result) => result.status === "rejected"
|
|
1737
|
+
);
|
|
1738
|
+
this.updateControlAvailability();
|
|
1739
|
+
if (failures.length > 0) throw failures[0].reason;
|
|
1740
|
+
return this.readiness();
|
|
1741
|
+
} finally {
|
|
1742
|
+
clearTimeout(timer);
|
|
1743
|
+
this.ownedOperations.delete(owned);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
async waitUntilReady(options) {
|
|
1747
|
+
await this.waitForCapabilities(options);
|
|
1748
|
+
}
|
|
1749
|
+
async connect(options = {}) {
|
|
1750
|
+
assertOptionsObject2(options);
|
|
1751
|
+
if (options.timeoutMs !== void 0) assertPositiveSafeInteger4(options.timeoutMs, "timeoutMs");
|
|
1752
|
+
this.assertRunning();
|
|
1753
|
+
if (options.abortSignal?.aborted) {
|
|
1754
|
+
throw cancellationError(options.abortSignal.reason, "connect");
|
|
1755
|
+
}
|
|
1756
|
+
if (this.connectionPending || this.connections.size > 0) {
|
|
1757
|
+
throw new BrowserError(
|
|
1758
|
+
"Browser already has an active or pending automation connection.",
|
|
1759
|
+
"agent_action_busy",
|
|
1760
|
+
{ phase: "connect" }
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
this.connectionPending = true;
|
|
1764
|
+
const controller = new AbortController();
|
|
1765
|
+
const signals = [controller.signal];
|
|
1766
|
+
if (options.abortSignal !== void 0) signals.push(options.abortSignal);
|
|
1767
|
+
const abortSignal = AbortSignal.any(signals);
|
|
1768
|
+
let owned;
|
|
1769
|
+
const promise = (async () => {
|
|
1770
|
+
await Promise.resolve();
|
|
1771
|
+
try {
|
|
1772
|
+
abortSignal.throwIfAborted();
|
|
1773
|
+
const connection = await this.connectBrowser({
|
|
1774
|
+
endpointUrl: endpointFor(this.sandbox, cdpPort),
|
|
1775
|
+
control: this.control,
|
|
1776
|
+
timeoutMs: options.timeoutMs ?? defaultConnectTimeoutMs,
|
|
1777
|
+
abortSignal,
|
|
1778
|
+
scheduling: options.scheduling,
|
|
1779
|
+
onClosed: (closedConnection) => this.connections.delete(closedConnection)
|
|
1780
|
+
});
|
|
1781
|
+
this.connections.add(connection);
|
|
1782
|
+
const disconnectedDuringInitialization = connection.closed;
|
|
1783
|
+
if (disconnectedDuringInitialization || this.handleState !== "active" || this.state !== "running") {
|
|
1784
|
+
this.connections.delete(connection);
|
|
1785
|
+
let cleanupError;
|
|
1786
|
+
try {
|
|
1787
|
+
await connection.disconnect();
|
|
1788
|
+
} catch (caught) {
|
|
1789
|
+
cleanupError = caught;
|
|
1790
|
+
}
|
|
1791
|
+
if (disconnectedDuringInitialization && this.handleState === "active" && this.state === "running") {
|
|
1792
|
+
throw new BrowserError(
|
|
1793
|
+
"Browser disconnected while automation was initializing.",
|
|
1794
|
+
"connection_closed",
|
|
1795
|
+
{
|
|
1796
|
+
...cleanupError === void 0 ? {} : { cause: cleanupError },
|
|
1797
|
+
phase: "connect"
|
|
1798
|
+
}
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
const lifecycleError = this.lifecycleError(
|
|
1802
|
+
"Browser stopped while the connection was initializing."
|
|
1803
|
+
);
|
|
1804
|
+
throw cleanupError === void 0 ? lifecycleError : new BrowserError(lifecycleError.message, lifecycleError.code, {
|
|
1805
|
+
cause: cleanupError,
|
|
1806
|
+
...lifecycleError.phase === void 0 ? {} : { phase: lifecycleError.phase }
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
return connection;
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
if (options.abortSignal?.aborted) {
|
|
1812
|
+
throw cancellationError(options.abortSignal.reason, "connect");
|
|
1813
|
+
}
|
|
1814
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
1815
|
+
if (error instanceof BrowserError) throw error;
|
|
1816
|
+
throw new BrowserError("Unable to connect to Chromium over CDP.", "transport_failure", {
|
|
1817
|
+
cause: error,
|
|
1818
|
+
phase: "connect"
|
|
1819
|
+
});
|
|
1820
|
+
} finally {
|
|
1821
|
+
this.connectionPending = false;
|
|
1822
|
+
this.ownedOperations.delete(owned);
|
|
1823
|
+
}
|
|
1824
|
+
})();
|
|
1825
|
+
owned = { controller, promise };
|
|
1826
|
+
this.ownedOperations.add(owned);
|
|
1827
|
+
return promise;
|
|
1828
|
+
}
|
|
1829
|
+
async stop(options = {}) {
|
|
1830
|
+
assertOptionsObject2(options);
|
|
1831
|
+
if (this.handleState === "stopped") return;
|
|
1832
|
+
if (this.handleState === "destroyed" || this.handleState === "destroying") {
|
|
1833
|
+
throw new BrowserError("Browser runtime is destroyed.", "runtime_destroyed");
|
|
1834
|
+
}
|
|
1835
|
+
validateLifecycleOptions(options, "stop-browser");
|
|
1836
|
+
if (this.stopPromise !== void 0) {
|
|
1837
|
+
return waitForSharedLifecycle(this.stopPromise, options, "stop-browser");
|
|
260
1838
|
}
|
|
261
|
-
this.
|
|
1839
|
+
this.handleState = "stopping";
|
|
1840
|
+
this.control.setAvailability("disconnected");
|
|
1841
|
+
const promise = this.performStop(options);
|
|
1842
|
+
this.stopPromise = promise;
|
|
262
1843
|
try {
|
|
263
|
-
|
|
1844
|
+
await promise;
|
|
264
1845
|
} finally {
|
|
265
|
-
this.
|
|
266
|
-
if (this.activeAgentActions === 0) this.resolvePendingAcquire();
|
|
1846
|
+
if (this.stopPromise === promise) this.stopPromise = void 0;
|
|
267
1847
|
}
|
|
268
1848
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
1849
|
+
async destroy(options = {}) {
|
|
1850
|
+
assertOptionsObject2(options);
|
|
1851
|
+
if (this.handleState === "destroyed") return;
|
|
1852
|
+
validateLifecycleOptions(options, "destroy-browser");
|
|
1853
|
+
if (this.destroyPromise === void 0) {
|
|
1854
|
+
this.handleState = "destroying";
|
|
1855
|
+
this.control.destroy();
|
|
1856
|
+
const activeStop = this.stopPromise;
|
|
1857
|
+
const promise = (async () => {
|
|
1858
|
+
if (activeStop !== void 0) await activeStop.catch(() => void 0);
|
|
1859
|
+
await this.performDestroy();
|
|
1860
|
+
})();
|
|
1861
|
+
this.destroyPromise = promise;
|
|
1862
|
+
void promise.then(
|
|
1863
|
+
() => {
|
|
1864
|
+
if (this.destroyPromise === promise) this.destroyPromise = void 0;
|
|
1865
|
+
},
|
|
1866
|
+
() => {
|
|
1867
|
+
if (this.destroyPromise === promise) this.destroyPromise = void 0;
|
|
1868
|
+
}
|
|
1869
|
+
);
|
|
272
1870
|
}
|
|
273
|
-
|
|
274
|
-
lease.resetExpiration(options.leaseTimeoutMs);
|
|
275
|
-
return this.snapshot();
|
|
1871
|
+
return waitForSharedLifecycle(this.destroyPromise, options, "destroy-browser");
|
|
276
1872
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
this.activeLease = void 0;
|
|
280
|
-
lease.markReleased();
|
|
1873
|
+
async [Symbol.asyncDispose]() {
|
|
1874
|
+
await this.destroy();
|
|
281
1875
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
1876
|
+
async performStop(options) {
|
|
1877
|
+
const bounded = boundedSignal(options, defaultLifecycleTimeoutMs, "stop-browser");
|
|
1878
|
+
try {
|
|
1879
|
+
await this.abortOwnedOperations(
|
|
1880
|
+
new BrowserError("Browser runtime is stopping.", "connection_closed", {
|
|
1881
|
+
phase: "stop-browser"
|
|
1882
|
+
})
|
|
1883
|
+
);
|
|
1884
|
+
const disconnectFailures = await this.disconnectAll({ abortSignal: bounded.signal });
|
|
1885
|
+
await this.sandbox.stop({ abortSignal: bounded.signal });
|
|
1886
|
+
if (this.handleState === "stopping") this.handleState = "stopped";
|
|
1887
|
+
bounded.throwIfAborted();
|
|
1888
|
+
if (disconnectFailures.length > 0) {
|
|
1889
|
+
throw new BrowserError(
|
|
1890
|
+
"Browser stopped, but one or more automation workers failed to disconnect.",
|
|
1891
|
+
"transport_failure",
|
|
1892
|
+
{
|
|
1893
|
+
cause: new AggregateError(disconnectFailures, "Automation worker cleanup failed."),
|
|
1894
|
+
phase: "stop-browser",
|
|
1895
|
+
recovery: "restart"
|
|
1896
|
+
}
|
|
1897
|
+
);
|
|
291
1898
|
}
|
|
292
|
-
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
if (this.handleState === "stopping") this.handleState = "error";
|
|
1901
|
+
throw bounded.normalize(error, "Unable to stop browser runtime.", "transport_failure");
|
|
1902
|
+
} finally {
|
|
1903
|
+
bounded.dispose();
|
|
293
1904
|
}
|
|
294
1905
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
1906
|
+
async performDestroy() {
|
|
1907
|
+
try {
|
|
1908
|
+
await this.abortOwnedOperations(
|
|
1909
|
+
new BrowserError("Browser runtime was destroyed.", "runtime_destroyed", {
|
|
1910
|
+
phase: "destroy-browser"
|
|
1911
|
+
})
|
|
1912
|
+
);
|
|
1913
|
+
const disconnectFailures = await this.disconnectAll({});
|
|
1914
|
+
await this.sandbox.destroy();
|
|
1915
|
+
this.handleState = "destroyed";
|
|
1916
|
+
if (disconnectFailures.length > 0) {
|
|
1917
|
+
throw new BrowserError(
|
|
1918
|
+
"Browser was destroyed, but one or more automation workers failed to disconnect.",
|
|
1919
|
+
"transport_failure",
|
|
1920
|
+
{
|
|
1921
|
+
cause: new AggregateError(disconnectFailures, "Automation worker cleanup failed."),
|
|
1922
|
+
phase: "destroy-browser",
|
|
1923
|
+
retryable: false,
|
|
1924
|
+
recovery: "none"
|
|
1925
|
+
}
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
if (this.handleState !== "destroyed") this.handleState = "error";
|
|
1930
|
+
if (error instanceof BrowserError) throw error;
|
|
1931
|
+
throw new BrowserError("Unable to destroy browser runtime.", "transport_failure", {
|
|
1932
|
+
cause: error,
|
|
1933
|
+
phase: "destroy-browser",
|
|
1934
|
+
recovery: "retry"
|
|
1935
|
+
});
|
|
304
1936
|
}
|
|
305
|
-
pending.resolve();
|
|
306
1937
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
timer;
|
|
314
|
-
released = false;
|
|
315
|
-
constructor(options) {
|
|
316
|
-
this.owner = options.owner;
|
|
317
|
-
this.ownerId = options.ownerId;
|
|
318
|
-
this.resetExpiration(options.leaseTimeoutMs);
|
|
319
|
-
}
|
|
320
|
-
get expiresAt() {
|
|
321
|
-
return new Date(this.expiration).toISOString();
|
|
322
|
-
}
|
|
323
|
-
renew(options) {
|
|
324
|
-
if (this.released) {
|
|
325
|
-
throw new BrowserError("Browser human control lease is no longer active.", "invalid_state");
|
|
1938
|
+
assertRunning() {
|
|
1939
|
+
if (this.handleState === "destroyed" || this.handleState === "destroying") {
|
|
1940
|
+
throw new BrowserError("Browser runtime is destroyed.", "runtime_destroyed");
|
|
1941
|
+
}
|
|
1942
|
+
if (this.handleState !== "active" || this.state !== "running") {
|
|
1943
|
+
throw new BrowserError(`Browser is not running: ${this.state}`, "invalid_state");
|
|
326
1944
|
}
|
|
327
|
-
return this.owner.renewLease(this, options);
|
|
328
1945
|
}
|
|
329
|
-
|
|
330
|
-
this.
|
|
1946
|
+
lifecycleError(message) {
|
|
1947
|
+
return this.handleState === "destroying" || this.handleState === "destroyed" ? new BrowserError(message, "runtime_destroyed") : new BrowserError(message, "connection_closed", { phase: "connect" });
|
|
331
1948
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
this.timer = void 0;
|
|
1949
|
+
async abortOwnedOperations(error) {
|
|
1950
|
+
const operations = [...this.ownedOperations];
|
|
1951
|
+
for (const operation of operations) operation.controller.abort(error);
|
|
1952
|
+
await Promise.allSettled(operations.map((operation) => operation.promise));
|
|
337
1953
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
this.
|
|
341
|
-
|
|
342
|
-
|
|
1954
|
+
async disconnectAll(options) {
|
|
1955
|
+
const connections = [...this.connections];
|
|
1956
|
+
this.connections.clear();
|
|
1957
|
+
const results = await Promise.allSettled(
|
|
1958
|
+
connections.map((connection) => connection.disconnect(options))
|
|
1959
|
+
);
|
|
1960
|
+
return results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
1961
|
+
}
|
|
1962
|
+
setCapability(capability, state, error) {
|
|
1963
|
+
this.capabilityStates.set(
|
|
1964
|
+
capability,
|
|
1965
|
+
Object.freeze({
|
|
1966
|
+
capability,
|
|
1967
|
+
state,
|
|
1968
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1969
|
+
...error === void 0 ? {} : { error }
|
|
1970
|
+
})
|
|
1971
|
+
);
|
|
343
1972
|
}
|
|
344
|
-
|
|
345
|
-
this.
|
|
1973
|
+
updateControlAvailability() {
|
|
1974
|
+
if (this.handleState !== "active" || this.state !== "running") {
|
|
1975
|
+
this.control.setAvailability("disconnected");
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
const failed = capabilities.some(
|
|
1979
|
+
(capability) => this.capabilityStates.get(capability)?.state === "failed"
|
|
1980
|
+
);
|
|
1981
|
+
this.control.setAvailability(failed ? "degraded" : "available");
|
|
346
1982
|
}
|
|
347
1983
|
};
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
1984
|
+
function validateReadinessOptions(options) {
|
|
1985
|
+
assertOptionsObject2(options);
|
|
1986
|
+
assertPositiveSafeInteger4(options.timeoutMs, "timeoutMs");
|
|
1987
|
+
if (options.capabilities === void 0) return;
|
|
1988
|
+
if (!Array.isArray(options.capabilities) || options.capabilities.length === 0) {
|
|
1989
|
+
throw new TypeError("capabilities must be a non-empty array.");
|
|
1990
|
+
}
|
|
1991
|
+
const unique = /* @__PURE__ */ new Set();
|
|
1992
|
+
for (const capability of options.capabilities) {
|
|
1993
|
+
if (!capabilities.includes(capability)) {
|
|
1994
|
+
throw new TypeError(`Unsupported browser capability: ${capability}`);
|
|
1995
|
+
}
|
|
1996
|
+
if (unique.has(capability)) throw new TypeError(`Duplicate browser capability: ${capability}`);
|
|
1997
|
+
unique.add(capability);
|
|
358
1998
|
}
|
|
359
1999
|
}
|
|
360
|
-
function isRecord(value) {
|
|
361
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
362
|
-
}
|
|
363
2000
|
|
|
364
|
-
// src/docker-browser.ts
|
|
365
|
-
var cdpPort = 9222;
|
|
366
|
-
var noVncPort = 6080;
|
|
2001
|
+
// src/docker-browser-client.ts
|
|
367
2002
|
var browserSchema = "1";
|
|
368
2003
|
var defaultSharedMemoryMb = 1024;
|
|
369
2004
|
var seccompProfilePath = fileURLToPath(
|
|
@@ -373,7 +2008,7 @@ var DockerBrowserClient = class {
|
|
|
373
2008
|
sandboxClient;
|
|
374
2009
|
image;
|
|
375
2010
|
constructor(options) {
|
|
376
|
-
if (!
|
|
2011
|
+
if (!isRecord3(options)) throw new TypeError("options must be an object.");
|
|
377
2012
|
if (!isSandboxClient(options.sandboxClient)) {
|
|
378
2013
|
throw new TypeError("sandboxClient must be a DockerSandboxClient.");
|
|
379
2014
|
}
|
|
@@ -382,16 +2017,20 @@ var DockerBrowserClient = class {
|
|
|
382
2017
|
this.image = options.image;
|
|
383
2018
|
}
|
|
384
2019
|
async pullImage(options = {}) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
2020
|
+
assertOptionsObject2(options);
|
|
2021
|
+
const bounded = boundedSignal(options, defaultLifecycleTimeoutMs, "pull-image");
|
|
2022
|
+
try {
|
|
2023
|
+
await this.sandboxClient.pullImage({ image: this.image, abortSignal: bounded.signal });
|
|
2024
|
+
bounded.throwIfAborted();
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
throw bounded.normalize(error, "Unable to pull the browser image.", "startup_failed");
|
|
2027
|
+
} finally {
|
|
2028
|
+
bounded.dispose();
|
|
389
2029
|
}
|
|
390
|
-
await this.sandboxClient.pullImage(pullOptions);
|
|
391
2030
|
}
|
|
392
2031
|
async createBrowser(options) {
|
|
393
2032
|
validateCreateOptions(options);
|
|
394
|
-
options
|
|
2033
|
+
const bounded = boundedSignal(options, defaultLifecycleTimeoutMs, "create-browser");
|
|
395
2034
|
const resources = {
|
|
396
2035
|
...options.resources,
|
|
397
2036
|
sharedMemoryMb: options.resources?.sharedMemoryMb ?? defaultSharedMemoryMb
|
|
@@ -402,252 +2041,122 @@ var DockerBrowserClient = class {
|
|
|
402
2041
|
workspace: options.workspace,
|
|
403
2042
|
network: { mode: "bridge", ports: [cdpPort, noVncPort] },
|
|
404
2043
|
user: "pwuser",
|
|
405
|
-
labels: {
|
|
406
|
-
"anvia.browser.schema": browserSchema
|
|
407
|
-
},
|
|
2044
|
+
labels: { "anvia.browser.schema": browserSchema },
|
|
408
2045
|
resources,
|
|
409
2046
|
security: {
|
|
410
2047
|
noNewPrivileges: true,
|
|
411
2048
|
dropCapabilities: ["ALL"],
|
|
412
2049
|
addCapabilities: ["SYS_CHROOT"],
|
|
413
2050
|
seccompProfile: { type: "path", path: seccompProfilePath }
|
|
414
|
-
}
|
|
2051
|
+
},
|
|
2052
|
+
abortSignal: bounded.signal
|
|
415
2053
|
};
|
|
416
2054
|
if (options.id !== void 0) sandboxOptions = { ...sandboxOptions, id: options.id };
|
|
417
2055
|
if (options.runtime !== void 0) {
|
|
418
2056
|
sandboxOptions = { ...sandboxOptions, runtime: options.runtime };
|
|
419
2057
|
}
|
|
420
|
-
|
|
421
|
-
sandboxOptions = { ...sandboxOptions, abortSignal: options.abortSignal };
|
|
422
|
-
}
|
|
423
|
-
const sandbox = await this.sandboxClient.createSandbox(sandboxOptions);
|
|
2058
|
+
let sandbox;
|
|
424
2059
|
try {
|
|
425
|
-
await
|
|
426
|
-
await
|
|
2060
|
+
sandbox = await this.sandboxClient.createSandbox(sandboxOptions);
|
|
2061
|
+
await configureBrowser(sandbox, options, bounded.signal);
|
|
2062
|
+
await startBrowserServices(sandbox, bounded.signal);
|
|
2063
|
+
bounded.throwIfAborted();
|
|
427
2064
|
return new DockerBrowserHandle(sandbox);
|
|
428
2065
|
} catch (error) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
})
|
|
2066
|
+
let cleanupError;
|
|
2067
|
+
try {
|
|
2068
|
+
await sandbox?.destroy();
|
|
2069
|
+
} catch (caught) {
|
|
2070
|
+
cleanupError = caught;
|
|
2071
|
+
}
|
|
2072
|
+
throw bounded.normalize(
|
|
2073
|
+
combineCleanupError2(error, cleanupError, "Browser sandbox cleanup also failed."),
|
|
2074
|
+
"Unable to configure the browser sandbox.",
|
|
2075
|
+
"startup_failed"
|
|
2076
|
+
);
|
|
2077
|
+
} finally {
|
|
2078
|
+
bounded.dispose();
|
|
433
2079
|
}
|
|
434
2080
|
}
|
|
435
2081
|
async resumeBrowser(options) {
|
|
436
|
-
|
|
2082
|
+
assertOptionsObject2(options);
|
|
437
2083
|
assertNonEmptyString(options.id, "id");
|
|
438
|
-
options
|
|
439
|
-
|
|
2084
|
+
const bounded = boundedSignal(options, defaultLifecycleTimeoutMs, "resume-browser");
|
|
2085
|
+
let sandbox;
|
|
440
2086
|
try {
|
|
441
|
-
await
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
} catch (error) {
|
|
445
|
-
await sandbox.stop().catch(() => void 0);
|
|
446
|
-
throw new BrowserError("Unable to resume browser services.", "startup_failed", {
|
|
447
|
-
cause: error
|
|
2087
|
+
sandbox = await this.sandboxClient.resumeSandbox({
|
|
2088
|
+
id: options.id,
|
|
2089
|
+
abortSignal: bounded.signal
|
|
448
2090
|
});
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
id;
|
|
454
|
-
sandbox;
|
|
455
|
-
desktop;
|
|
456
|
-
control = new BrowserControlState();
|
|
457
|
-
connections = /* @__PURE__ */ new Set();
|
|
458
|
-
constructor(sandbox) {
|
|
459
|
-
this.sandbox = sandbox;
|
|
460
|
-
this.id = sandbox.id;
|
|
461
|
-
this.desktop = Object.freeze({
|
|
462
|
-
protocol: "novnc",
|
|
463
|
-
containerPort: noVncPort,
|
|
464
|
-
control: this.control
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
get state() {
|
|
468
|
-
return this.sandbox.state;
|
|
469
|
-
}
|
|
470
|
-
inspector(options) {
|
|
471
|
-
return this.sandbox.inspector(options);
|
|
472
|
-
}
|
|
473
|
-
async waitUntilReady(options) {
|
|
474
|
-
assertOptionsObject(options);
|
|
475
|
-
assertPositiveSafeInteger2(options.timeoutMs, "timeoutMs");
|
|
476
|
-
this.assertRunning();
|
|
477
|
-
const timeout = AbortSignal.timeout(options.timeoutMs);
|
|
478
|
-
const abortSignal = options.abortSignal === void 0 ? timeout : AbortSignal.any([options.abortSignal, timeout]);
|
|
479
|
-
try {
|
|
480
|
-
await Promise.all([
|
|
481
|
-
this.sandbox.runtime.waitForPort({
|
|
482
|
-
containerPort: cdpPort,
|
|
483
|
-
timeoutMs: options.timeoutMs,
|
|
484
|
-
abortSignal
|
|
485
|
-
}),
|
|
486
|
-
this.sandbox.runtime.waitForPort({
|
|
487
|
-
containerPort: noVncPort,
|
|
488
|
-
timeoutMs: options.timeoutMs,
|
|
489
|
-
abortSignal
|
|
490
|
-
})
|
|
491
|
-
]);
|
|
492
|
-
await Promise.all([
|
|
493
|
-
assertHttpReady(`${endpointFor(this.sandbox, cdpPort)}/json/version`, abortSignal),
|
|
494
|
-
assertHttpReady(`${endpointFor(this.sandbox, noVncPort)}/vnc.html`, abortSignal)
|
|
495
|
-
]);
|
|
2091
|
+
await assertBrowserImage(sandbox, bounded.signal);
|
|
2092
|
+
await startBrowserServices(sandbox, bounded.signal);
|
|
2093
|
+
bounded.throwIfAborted();
|
|
2094
|
+
return new DockerBrowserHandle(sandbox);
|
|
496
2095
|
} catch (error) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
})
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
async connect(options = {}) {
|
|
504
|
-
assertOptionsObject(options);
|
|
505
|
-
this.assertRunning();
|
|
506
|
-
try {
|
|
507
|
-
let connectOptions = {
|
|
508
|
-
endpointUrl: endpointFor(this.sandbox, cdpPort),
|
|
509
|
-
control: this.control
|
|
510
|
-
};
|
|
511
|
-
if (options.abortSignal !== void 0) {
|
|
512
|
-
connectOptions = { ...connectOptions, abortSignal: options.abortSignal };
|
|
2096
|
+
let cleanupError;
|
|
2097
|
+
try {
|
|
2098
|
+
await sandbox?.stop();
|
|
2099
|
+
} catch (caught) {
|
|
2100
|
+
cleanupError = caught;
|
|
513
2101
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
async stop(options = {}) {
|
|
525
|
-
assertOptionsObject(options);
|
|
526
|
-
await this.disconnectAll();
|
|
527
|
-
await this.sandbox.stop(options);
|
|
528
|
-
}
|
|
529
|
-
async destroy() {
|
|
530
|
-
this.control.destroy();
|
|
531
|
-
await this.disconnectAll();
|
|
532
|
-
await this.sandbox.destroy();
|
|
533
|
-
}
|
|
534
|
-
async [Symbol.asyncDispose]() {
|
|
535
|
-
await this.destroy();
|
|
536
|
-
}
|
|
537
|
-
assertRunning() {
|
|
538
|
-
if (this.state !== "running") {
|
|
539
|
-
throw new BrowserError(`Browser is not running: ${this.state}`, "invalid_state");
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
async disconnectAll() {
|
|
543
|
-
const connections = [...this.connections];
|
|
544
|
-
this.connections.clear();
|
|
545
|
-
const results = await Promise.allSettled(
|
|
546
|
-
connections.map((connection) => connection.disconnect())
|
|
547
|
-
);
|
|
548
|
-
const failures = results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
549
|
-
if (failures.length > 0) {
|
|
550
|
-
throw new AggregateError(failures, "Unable to disconnect browser automation clients.");
|
|
2102
|
+
throw bounded.normalize(
|
|
2103
|
+
combineCleanupError2(error, cleanupError, "Resumed browser cleanup also failed."),
|
|
2104
|
+
"Unable to resume browser services.",
|
|
2105
|
+
"startup_failed"
|
|
2106
|
+
);
|
|
2107
|
+
} finally {
|
|
2108
|
+
bounded.dispose();
|
|
551
2109
|
}
|
|
552
2110
|
}
|
|
553
2111
|
};
|
|
554
|
-
async function configureBrowser(sandbox, options) {
|
|
555
|
-
|
|
2112
|
+
async function configureBrowser(sandbox, options, abortSignal) {
|
|
2113
|
+
const result = await sandbox.runtime.exec({
|
|
556
2114
|
command: "/usr/local/bin/anvia-browser-configure",
|
|
557
2115
|
input: JSON.stringify({
|
|
558
2116
|
password: options.desktop.password,
|
|
559
2117
|
width: options.desktop.viewport.width,
|
|
560
2118
|
height: options.desktop.viewport.height
|
|
561
|
-
})
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
execOptions = { ...execOptions, abortSignal: options.abortSignal };
|
|
565
|
-
}
|
|
566
|
-
const result = await sandbox.runtime.exec(execOptions);
|
|
2119
|
+
}),
|
|
2120
|
+
abortSignal
|
|
2121
|
+
});
|
|
567
2122
|
if (result.status !== "exited" || result.exitCode !== 0) {
|
|
568
2123
|
throw new Error("Browser image rejected its runtime configuration.");
|
|
569
2124
|
}
|
|
570
2125
|
}
|
|
571
2126
|
async function assertBrowserImage(sandbox, abortSignal) {
|
|
572
|
-
|
|
573
|
-
command: "/usr/local/bin/anvia-browser-version"
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const result = await sandbox.runtime.exec(execOptions);
|
|
2127
|
+
const result = await sandbox.runtime.exec({
|
|
2128
|
+
command: "/usr/local/bin/anvia-browser-version",
|
|
2129
|
+
abortSignal
|
|
2130
|
+
});
|
|
577
2131
|
if (result.status !== "exited" || result.exitCode !== 0 || new TextDecoder("utf-8", { fatal: true }).decode(result.stdout).trim() !== browserSchema) {
|
|
578
2132
|
throw new Error("Sandbox does not contain a compatible Anvia browser image.");
|
|
579
2133
|
}
|
|
580
2134
|
}
|
|
581
2135
|
async function startBrowserServices(sandbox, abortSignal) {
|
|
582
|
-
|
|
583
|
-
command: "/usr/local/bin/anvia-browser-start"
|
|
584
|
-
|
|
585
|
-
if (abortSignal !== void 0) startOptions = { ...startOptions, abortSignal };
|
|
586
|
-
await sandbox.runtime.startProcess(startOptions);
|
|
587
|
-
}
|
|
588
|
-
async function assertHttpReady(url, abortSignal) {
|
|
589
|
-
while (true) {
|
|
590
|
-
abortSignal.throwIfAborted();
|
|
591
|
-
try {
|
|
592
|
-
const response = await fetch(url, { signal: abortSignal, redirect: "error" });
|
|
593
|
-
if (response.ok) {
|
|
594
|
-
await response.body?.cancel();
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
await response.body?.cancel();
|
|
598
|
-
} catch (error) {
|
|
599
|
-
if (abortSignal.aborted) throw abortSignal.reason ?? error;
|
|
600
|
-
}
|
|
601
|
-
await waitForRetry(abortSignal);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
async function waitForRetry(abortSignal) {
|
|
605
|
-
await new Promise((resolve, reject) => {
|
|
606
|
-
const timeout = setTimeout(finish, 50);
|
|
607
|
-
const abort = () => finish(abortSignal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
608
|
-
abortSignal.addEventListener("abort", abort, { once: true });
|
|
609
|
-
function finish(error) {
|
|
610
|
-
clearTimeout(timeout);
|
|
611
|
-
abortSignal.removeEventListener("abort", abort);
|
|
612
|
-
if (error === void 0) resolve();
|
|
613
|
-
else reject(error);
|
|
614
|
-
}
|
|
2136
|
+
await sandbox.runtime.startProcess({
|
|
2137
|
+
command: "/usr/local/bin/anvia-browser-start",
|
|
2138
|
+
abortSignal
|
|
615
2139
|
});
|
|
616
2140
|
}
|
|
617
|
-
function endpointFor(sandbox, containerPort) {
|
|
618
|
-
const published = publishedPort(sandbox, containerPort);
|
|
619
|
-
return `http://${hostForUrl(published.host)}:${published.hostPort}`;
|
|
620
|
-
}
|
|
621
|
-
function publishedPort(sandbox, containerPort) {
|
|
622
|
-
const port = sandbox.runtime.publishedPorts.find(
|
|
623
|
-
(candidate) => candidate.containerPort === containerPort && candidate.protocol === "tcp"
|
|
624
|
-
);
|
|
625
|
-
if (port === void 0) {
|
|
626
|
-
throw new BrowserError(`Browser port is not published: ${containerPort}`, "invalid_state");
|
|
627
|
-
}
|
|
628
|
-
return port;
|
|
629
|
-
}
|
|
630
|
-
function hostForUrl(host) {
|
|
631
|
-
return host.includes(":") ? `[${host}]` : host;
|
|
632
|
-
}
|
|
633
2141
|
function validateCreateOptions(options) {
|
|
634
|
-
|
|
2142
|
+
assertOptionsObject2(options);
|
|
635
2143
|
if (options.id !== void 0) assertNonEmptyString(options.id, "id");
|
|
636
|
-
if (!
|
|
637
|
-
if (!
|
|
2144
|
+
if (!isRecord3(options.workspace)) throw new TypeError("workspace must be an object.");
|
|
2145
|
+
if (!isRecord3(options.network) || options.network.mode !== "bridge") {
|
|
638
2146
|
throw new TypeError('network must be { mode: "bridge" }.');
|
|
639
2147
|
}
|
|
640
|
-
if (!
|
|
2148
|
+
if (!isRecord3(options.desktop) || options.desktop.protocol !== "novnc") {
|
|
641
2149
|
throw new TypeError('desktop must use protocol: "novnc".');
|
|
642
2150
|
}
|
|
643
2151
|
if (!/^[\x20-\x7e]{8}$/.test(options.desktop.password)) {
|
|
644
2152
|
throw new TypeError("desktop.password must contain exactly 8 printable ASCII characters.");
|
|
645
2153
|
}
|
|
646
|
-
if (!
|
|
2154
|
+
if (!isRecord3(options.desktop.viewport)) {
|
|
647
2155
|
throw new TypeError("desktop.viewport must be an object.");
|
|
648
2156
|
}
|
|
649
2157
|
assertIntegerInRange(options.desktop.viewport.width, "desktop.viewport.width", 640, 3840);
|
|
650
2158
|
assertIntegerInRange(options.desktop.viewport.height, "desktop.viewport.height", 480, 2160);
|
|
2159
|
+
if (options.timeoutMs !== void 0) assertPositiveSafeInteger4(options.timeoutMs, "timeoutMs");
|
|
651
2160
|
if (options.runtime?.maxProcesses !== void 0 && options.runtime.maxProcesses < 1) {
|
|
652
2161
|
throw new RangeError("runtime.maxProcesses must allow the browser service process.");
|
|
653
2162
|
}
|
|
@@ -657,24 +2166,16 @@ function assertIntegerInRange(value, name, min, max) {
|
|
|
657
2166
|
throw new RangeError(`${name} must be a safe integer between ${min} and ${max}.`);
|
|
658
2167
|
}
|
|
659
2168
|
}
|
|
660
|
-
function assertPositiveSafeInteger2(value, name) {
|
|
661
|
-
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
662
|
-
throw new RangeError(`${name} must be a positive safe integer.`);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
2169
|
function assertNonEmptyString(value, name) {
|
|
666
2170
|
if (typeof value !== "string" || value.length === 0) {
|
|
667
2171
|
throw new TypeError(`${name} must be a non-empty string.`);
|
|
668
2172
|
}
|
|
669
2173
|
}
|
|
670
|
-
function assertOptionsObject(value) {
|
|
671
|
-
if (!isRecord2(value)) throw new TypeError("options must be an object.");
|
|
672
|
-
}
|
|
673
2174
|
function isSandboxClient(value) {
|
|
674
|
-
return
|
|
2175
|
+
return isRecord3(value) && typeof value.pullImage === "function" && typeof value.createSandbox === "function" && typeof value.resumeSandbox === "function";
|
|
675
2176
|
}
|
|
676
|
-
function
|
|
677
|
-
return
|
|
2177
|
+
function combineCleanupError2(error, cleanupError, message) {
|
|
2178
|
+
return cleanupError === void 0 ? error : new AggregateError([error, cleanupError], message);
|
|
678
2179
|
}
|
|
679
2180
|
|
|
680
2181
|
// src/tools.ts
|
|
@@ -699,15 +2200,18 @@ var targetSchema = z.discriminatedUnion("by", [
|
|
|
699
2200
|
]);
|
|
700
2201
|
var noInput = z.object({});
|
|
701
2202
|
var tabInput = z.object({ tabId: z.string().uuid() });
|
|
2203
|
+
var explicitTab = { tabId: z.string().uuid().optional() };
|
|
702
2204
|
var navigateInput = z.object({
|
|
2205
|
+
...explicitTab,
|
|
703
2206
|
url: z.url(),
|
|
704
2207
|
waitUntil: z.enum(["commit", "domcontentloaded", "load", "networkidle"]).optional()
|
|
705
2208
|
});
|
|
706
|
-
var clickInput = z.object({ target: targetSchema });
|
|
707
|
-
var typeInput = z.object({ target: targetSchema, text: z.string() });
|
|
708
|
-
var pressKeyInput = z.object({ key: z.string().min(1).max(100) });
|
|
709
|
-
var screenshotInput = z.object(
|
|
710
|
-
var
|
|
2209
|
+
var clickInput = z.object({ ...explicitTab, target: targetSchema });
|
|
2210
|
+
var typeInput = z.object({ ...explicitTab, target: targetSchema, text: z.string() });
|
|
2211
|
+
var pressKeyInput = z.object({ ...explicitTab, key: z.string().min(1).max(100) });
|
|
2212
|
+
var screenshotInput = z.object(explicitTab);
|
|
2213
|
+
var snapshotInput = z.object(explicitTab);
|
|
2214
|
+
var defaultActionTimeoutMs2 = 1e4;
|
|
711
2215
|
var defaultNavigationTimeoutMs = 3e4;
|
|
712
2216
|
var defaultSnapshotMaxChars = 5e4;
|
|
713
2217
|
var maxTimeoutMs = 3e5;
|
|
@@ -718,20 +2222,20 @@ function createBrowserTools(options) {
|
|
|
718
2222
|
const policy = snapshotNavigationPolicy(options.navigation);
|
|
719
2223
|
connection.setNavigationPolicy(policy);
|
|
720
2224
|
const limits = {
|
|
721
|
-
actionTimeoutMs: options.limits?.actionTimeoutMs ??
|
|
2225
|
+
actionTimeoutMs: options.limits?.actionTimeoutMs ?? defaultActionTimeoutMs2,
|
|
722
2226
|
navigationTimeoutMs: options.limits?.navigationTimeoutMs ?? defaultNavigationTimeoutMs,
|
|
723
2227
|
snapshotMaxChars: options.limits?.snapshotMaxChars ?? defaultSnapshotMaxChars
|
|
724
2228
|
};
|
|
725
2229
|
const tools = options.tools.map((name) => {
|
|
726
2230
|
switch (name) {
|
|
727
2231
|
case "browser_list_tabs":
|
|
728
|
-
return createListTabsTool(connection);
|
|
2232
|
+
return createListTabsTool(connection, limits.actionTimeoutMs);
|
|
729
2233
|
case "browser_open_tab":
|
|
730
|
-
return createOpenTabTool(connection);
|
|
2234
|
+
return createOpenTabTool(connection, limits.actionTimeoutMs);
|
|
731
2235
|
case "browser_select_tab":
|
|
732
|
-
return createSelectTabTool(connection);
|
|
2236
|
+
return createSelectTabTool(connection, limits.actionTimeoutMs);
|
|
733
2237
|
case "browser_close_tab":
|
|
734
|
-
return createCloseTabTool(connection);
|
|
2238
|
+
return createCloseTabTool(connection, limits.actionTimeoutMs);
|
|
735
2239
|
case "browser_navigate":
|
|
736
2240
|
return createNavigateTool(connection, policy, limits.navigationTimeoutMs);
|
|
737
2241
|
case "browser_snapshot":
|
|
@@ -741,185 +2245,174 @@ function createBrowserTools(options) {
|
|
|
741
2245
|
case "browser_type":
|
|
742
2246
|
return createTypeTool(connection, limits.actionTimeoutMs);
|
|
743
2247
|
case "browser_press_key":
|
|
744
|
-
return createPressKeyTool(connection);
|
|
2248
|
+
return createPressKeyTool(connection, limits.actionTimeoutMs);
|
|
745
2249
|
case "browser_screenshot":
|
|
746
|
-
return createScreenshotTool(connection);
|
|
2250
|
+
return createScreenshotTool(connection, limits.actionTimeoutMs);
|
|
747
2251
|
default:
|
|
748
2252
|
return assertNever(name);
|
|
749
2253
|
}
|
|
750
2254
|
});
|
|
751
2255
|
return Object.freeze(tools);
|
|
752
2256
|
}
|
|
753
|
-
function createListTabsTool(connection) {
|
|
2257
|
+
function createListTabsTool(connection, timeoutMs) {
|
|
754
2258
|
return createTool({
|
|
755
2259
|
name: "browser_list_tabs",
|
|
756
2260
|
description: "List Chromium tabs and identify the tab selected for subsequent browser tools.",
|
|
757
2261
|
inputSchema: noInput,
|
|
758
|
-
execute: async () => ({
|
|
2262
|
+
execute: async (_args, context) => ({
|
|
2263
|
+
tabs: await connection.listTabs({ abortSignal: context.abortSignal, timeoutMs })
|
|
2264
|
+
})
|
|
759
2265
|
});
|
|
760
2266
|
}
|
|
761
|
-
function createOpenTabTool(connection) {
|
|
2267
|
+
function createOpenTabTool(connection, timeoutMs) {
|
|
762
2268
|
return createTool({
|
|
763
2269
|
name: "browser_open_tab",
|
|
764
2270
|
description: "Open and select a new blank Chromium tab.",
|
|
765
2271
|
inputSchema: noInput,
|
|
766
|
-
execute: async (_args, context) => connection.
|
|
767
|
-
const page = await connection.openTab();
|
|
768
|
-
return tabResult(connection, page);
|
|
769
|
-
})
|
|
2272
|
+
execute: async (_args, context) => connection.openTab(context.abortSignal, timeoutMs)
|
|
770
2273
|
});
|
|
771
2274
|
}
|
|
772
|
-
function createSelectTabTool(connection) {
|
|
2275
|
+
function createSelectTabTool(connection, timeoutMs) {
|
|
773
2276
|
return createTool({
|
|
774
2277
|
name: "browser_select_tab",
|
|
775
2278
|
description: "Select an existing Chromium tab by its browser tab ID.",
|
|
776
2279
|
inputSchema: tabInput,
|
|
777
|
-
execute: async ({ tabId }, context) => connection.
|
|
778
|
-
context.abortSignal,
|
|
779
|
-
async () => tabResult(connection, connection.selectTab(tabId))
|
|
780
|
-
)
|
|
2280
|
+
execute: async ({ tabId }, context) => connection.selectTab(tabId, context.abortSignal, timeoutMs)
|
|
781
2281
|
});
|
|
782
2282
|
}
|
|
783
|
-
function createCloseTabTool(connection) {
|
|
2283
|
+
function createCloseTabTool(connection, timeoutMs) {
|
|
784
2284
|
return createTool({
|
|
785
2285
|
name: "browser_close_tab",
|
|
786
2286
|
description: "Close an existing Chromium tab by its browser tab ID.",
|
|
787
2287
|
inputSchema: tabInput,
|
|
788
|
-
execute: async ({ tabId }, context) => connection.
|
|
789
|
-
await connection.closeTab(tabId);
|
|
790
|
-
return { closedTabId: tabId, tabs: await connection.tabSummaries() };
|
|
791
|
-
})
|
|
2288
|
+
execute: async ({ tabId }, context) => connection.closeTab(tabId, context.abortSignal, timeoutMs)
|
|
792
2289
|
});
|
|
793
2290
|
}
|
|
794
2291
|
function createNavigateTool(connection, policy, timeoutMs) {
|
|
795
2292
|
return createTool({
|
|
796
2293
|
name: "browser_navigate",
|
|
797
|
-
description: "Navigate the selected
|
|
2294
|
+
description: "Navigate an explicit tab, or the selected tab in serial compatibility mode, to an allowed HTTP or HTTPS URL.",
|
|
798
2295
|
inputSchema: navigateInput,
|
|
799
|
-
execute: async ({ url, waitUntil }, context) =>
|
|
2296
|
+
execute: async ({ tabId, url, waitUntil }, context) => {
|
|
800
2297
|
assertNavigationAllowed(url, policy);
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
2298
|
+
const result = await connection.runTabCommand({
|
|
2299
|
+
tabId,
|
|
2300
|
+
abortSignal: context.abortSignal,
|
|
2301
|
+
timeoutMs,
|
|
2302
|
+
phase: "navigate",
|
|
2303
|
+
command: (targetTabId) => ({
|
|
2304
|
+
method: "navigate",
|
|
2305
|
+
params: {
|
|
2306
|
+
tabId: targetTabId,
|
|
2307
|
+
url,
|
|
2308
|
+
waitUntil: waitUntil ?? "load",
|
|
2309
|
+
timeoutMs
|
|
2310
|
+
}
|
|
2311
|
+
})
|
|
805
2312
|
});
|
|
806
|
-
assertNavigationAllowed(
|
|
807
|
-
return
|
|
808
|
-
}
|
|
2313
|
+
assertNavigationAllowed(result.url, policy);
|
|
2314
|
+
return result;
|
|
2315
|
+
}
|
|
809
2316
|
});
|
|
810
2317
|
}
|
|
811
2318
|
function createSnapshotTool(connection, timeoutMs, maxChars) {
|
|
812
2319
|
return createTool({
|
|
813
2320
|
name: "browser_snapshot",
|
|
814
|
-
description: "Inspect the selected tab using a bounded ARIA accessibility snapshot.",
|
|
815
|
-
inputSchema:
|
|
816
|
-
execute: async (
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
2321
|
+
description: "Inspect an explicit tab, or the selected tab in serial compatibility mode, using a bounded ARIA accessibility snapshot.",
|
|
2322
|
+
inputSchema: snapshotInput,
|
|
2323
|
+
execute: async ({ tabId }, context) => connection.runTabCommand({
|
|
2324
|
+
tabId,
|
|
2325
|
+
abortSignal: context.abortSignal,
|
|
2326
|
+
timeoutMs,
|
|
2327
|
+
phase: "snapshot",
|
|
2328
|
+
command: (targetTabId) => ({
|
|
2329
|
+
method: "snapshot",
|
|
2330
|
+
params: { tabId: targetTabId, timeoutMs, maxChars }
|
|
2331
|
+
})
|
|
825
2332
|
})
|
|
826
2333
|
});
|
|
827
2334
|
}
|
|
828
2335
|
function createClickTool(connection, timeoutMs) {
|
|
829
2336
|
return createTool({
|
|
830
2337
|
name: "browser_click",
|
|
831
|
-
description: "Click one strictly matched element in the selected tab.",
|
|
2338
|
+
description: "Click one strictly matched element in an explicit tab or the selected tab.",
|
|
832
2339
|
inputSchema: clickInput,
|
|
833
|
-
execute: async ({ target }, context) => connection.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
2340
|
+
execute: async ({ tabId, target }, context) => connection.runTabCommand({
|
|
2341
|
+
tabId,
|
|
2342
|
+
abortSignal: context.abortSignal,
|
|
2343
|
+
timeoutMs,
|
|
2344
|
+
phase: "click",
|
|
2345
|
+
command: (targetTabId) => ({
|
|
2346
|
+
method: "click",
|
|
2347
|
+
params: { tabId: targetTabId, target, timeoutMs }
|
|
2348
|
+
})
|
|
837
2349
|
})
|
|
838
2350
|
});
|
|
839
2351
|
}
|
|
840
2352
|
function createTypeTool(connection, timeoutMs) {
|
|
841
2353
|
return createTool({
|
|
842
2354
|
name: "browser_type",
|
|
843
|
-
description: "Replace the value of one strictly matched editable element.",
|
|
2355
|
+
description: "Replace the value of one strictly matched editable element in an explicit or selected tab.",
|
|
844
2356
|
inputSchema: typeInput,
|
|
845
|
-
execute: async ({ target, text }, context) => connection.
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
2357
|
+
execute: async ({ tabId, target, text }, context) => connection.runTabCommand({
|
|
2358
|
+
tabId,
|
|
2359
|
+
abortSignal: context.abortSignal,
|
|
2360
|
+
timeoutMs,
|
|
2361
|
+
phase: "type",
|
|
2362
|
+
command: (targetTabId) => ({
|
|
2363
|
+
method: "type",
|
|
2364
|
+
params: { tabId: targetTabId, target, text, timeoutMs }
|
|
2365
|
+
})
|
|
849
2366
|
})
|
|
850
2367
|
});
|
|
851
2368
|
}
|
|
852
|
-
function createPressKeyTool(connection) {
|
|
2369
|
+
function createPressKeyTool(connection, timeoutMs) {
|
|
853
2370
|
return createTool({
|
|
854
2371
|
name: "browser_press_key",
|
|
855
|
-
description: "Press an explicit keyboard key or key combination in
|
|
2372
|
+
description: "Press an explicit keyboard key or key combination in an explicit or selected tab.",
|
|
856
2373
|
inputSchema: pressKeyInput,
|
|
857
|
-
execute: async ({ key }, context) => connection.
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
2374
|
+
execute: async ({ tabId, key }, context) => connection.runTabCommand({
|
|
2375
|
+
tabId,
|
|
2376
|
+
abortSignal: context.abortSignal,
|
|
2377
|
+
timeoutMs,
|
|
2378
|
+
phase: "press-key",
|
|
2379
|
+
command: (targetTabId) => ({
|
|
2380
|
+
method: "pressKey",
|
|
2381
|
+
params: { tabId: targetTabId, key }
|
|
2382
|
+
})
|
|
861
2383
|
})
|
|
862
2384
|
});
|
|
863
2385
|
}
|
|
864
|
-
function createScreenshotTool(connection) {
|
|
2386
|
+
function createScreenshotTool(connection, timeoutMs) {
|
|
865
2387
|
return createTool({
|
|
866
2388
|
name: "browser_screenshot",
|
|
867
|
-
description: "Capture the visible viewport of
|
|
2389
|
+
description: "Capture the visible viewport of an explicit or selected Chromium tab as PNG.",
|
|
868
2390
|
inputSchema: screenshotInput,
|
|
869
|
-
execute: async (
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
2391
|
+
execute: async ({ tabId }, context) => {
|
|
2392
|
+
const result = await connection.runTabCommand({
|
|
2393
|
+
tabId,
|
|
2394
|
+
abortSignal: context.abortSignal,
|
|
2395
|
+
timeoutMs,
|
|
2396
|
+
phase: "screenshot",
|
|
2397
|
+
command: (targetTabId) => ({
|
|
2398
|
+
method: "screenshot",
|
|
2399
|
+
params: { tabId: targetTabId, timeoutMs }
|
|
2400
|
+
})
|
|
2401
|
+
});
|
|
873
2402
|
return ToolOutput.content([
|
|
874
|
-
{ type: "text", text: JSON.stringify(metadata) },
|
|
2403
|
+
{ type: "text", text: JSON.stringify(result.metadata) },
|
|
875
2404
|
{
|
|
876
2405
|
type: "file",
|
|
877
|
-
data: { type: "data", data:
|
|
2406
|
+
data: { type: "data", data: result.pngBase64 },
|
|
878
2407
|
mediaType: "image/png",
|
|
879
2408
|
filename: "browser-screenshot.png"
|
|
880
2409
|
}
|
|
881
2410
|
]);
|
|
882
|
-
})
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
async function tabResult(connection, page) {
|
|
886
|
-
return {
|
|
887
|
-
tabId: connection.idFor(page),
|
|
888
|
-
title: await page.title(),
|
|
889
|
-
url: page.url()
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
function locatorFor(page, target) {
|
|
893
|
-
switch (target.by) {
|
|
894
|
-
case "role": {
|
|
895
|
-
const options = {};
|
|
896
|
-
if (target.name !== void 0) options.name = target.name;
|
|
897
|
-
if (target.exact !== void 0) options.exact = target.exact;
|
|
898
|
-
return page.getByRole(target.role, options);
|
|
899
|
-
}
|
|
900
|
-
case "text": {
|
|
901
|
-
const options = {};
|
|
902
|
-
if (target.exact !== void 0) options.exact = target.exact;
|
|
903
|
-
return page.getByText(target.text, options);
|
|
904
|
-
}
|
|
905
|
-
case "label": {
|
|
906
|
-
const options = {};
|
|
907
|
-
if (target.exact !== void 0) options.exact = target.exact;
|
|
908
|
-
return page.getByLabel(target.label, options);
|
|
909
|
-
}
|
|
910
|
-
case "placeholder": {
|
|
911
|
-
const options = {};
|
|
912
|
-
if (target.exact !== void 0) options.exact = target.exact;
|
|
913
|
-
return page.getByPlaceholder(target.placeholder, options);
|
|
914
2411
|
}
|
|
915
|
-
|
|
916
|
-
return page.getByTestId(target.testId);
|
|
917
|
-
case "css":
|
|
918
|
-
return page.locator(target.selector);
|
|
919
|
-
}
|
|
2412
|
+
});
|
|
920
2413
|
}
|
|
921
2414
|
function snapshotNavigationPolicy(policy) {
|
|
922
|
-
if (!
|
|
2415
|
+
if (!isRecord5(policy)) throw new TypeError("navigation must be an object.");
|
|
923
2416
|
if (policy.mode === "allow-all-http") return Object.freeze({ mode: "allow-all-http" });
|
|
924
2417
|
if (policy.mode !== "origins" || !Array.isArray(policy.origins) || policy.origins.length === 0) {
|
|
925
2418
|
throw new TypeError(
|
|
@@ -941,11 +2434,11 @@ function normalizeOrigin(value) {
|
|
|
941
2434
|
return url.origin;
|
|
942
2435
|
}
|
|
943
2436
|
function assertNavigationAllowed(value, policy) {
|
|
944
|
-
if (!
|
|
2437
|
+
if (!isNavigationAllowed(value, policy)) {
|
|
945
2438
|
throw new BrowserError(`Browser navigation is blocked: ${value}`, "navigation_blocked");
|
|
946
2439
|
}
|
|
947
2440
|
}
|
|
948
|
-
function
|
|
2441
|
+
function isNavigationAllowed(value, policy) {
|
|
949
2442
|
let url;
|
|
950
2443
|
try {
|
|
951
2444
|
url = parseHttpUrl(value);
|
|
@@ -965,7 +2458,7 @@ function parseHttpUrl(value) {
|
|
|
965
2458
|
return url;
|
|
966
2459
|
}
|
|
967
2460
|
function validateFactoryOptions(options) {
|
|
968
|
-
if (!
|
|
2461
|
+
if (!isRecord5(options)) throw new TypeError("options must be an object.");
|
|
969
2462
|
if (!Array.isArray(options.tools) || options.tools.length === 0) {
|
|
970
2463
|
throw new TypeError("tools must be a non-empty array.");
|
|
971
2464
|
}
|
|
@@ -976,7 +2469,7 @@ function validateFactoryOptions(options) {
|
|
|
976
2469
|
seen.add(name);
|
|
977
2470
|
}
|
|
978
2471
|
snapshotNavigationPolicy(options.navigation);
|
|
979
|
-
if (options.limits !== void 0 && !
|
|
2472
|
+
if (options.limits !== void 0 && !isRecord5(options.limits)) {
|
|
980
2473
|
throw new TypeError("limits must be an object.");
|
|
981
2474
|
}
|
|
982
2475
|
assertOptionalBoundedInteger(options.limits?.actionTimeoutMs, "actionTimeoutMs", 1, maxTimeoutMs);
|
|
@@ -1012,7 +2505,7 @@ function isToolName(value) {
|
|
|
1012
2505
|
"browser_screenshot"
|
|
1013
2506
|
].includes(value);
|
|
1014
2507
|
}
|
|
1015
|
-
function
|
|
2508
|
+
function isRecord5(value) {
|
|
1016
2509
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1017
2510
|
}
|
|
1018
2511
|
function assertNever(value) {
|