@orkestrel/browser 0.0.29 → 0.0.31
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/dist/bin/main.js +1 -2
- package/dist/src/browser/index.d.ts +86 -6
- package/dist/src/browser/index.js +359 -271
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +2281 -1745
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +342 -156
- package/dist/src/core/index.d.ts +342 -156
- package/dist/src/core/index.js +2261 -1744
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +377 -344
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +47 -9
- package/dist/src/server/index.d.ts +47 -9
- package/dist/src/server/index.js +376 -344
- package/dist/src/server/index.js.map +1 -1
- package/package.json +27 -26
|
@@ -12,11 +12,12 @@ let node_https = require("node:https");
|
|
|
12
12
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
13
13
|
let _orkestrel_websocket = require("@orkestrel/websocket");
|
|
14
14
|
let node_events = require("node:events");
|
|
15
|
+
let _orkestrel_process_server = require("@orkestrel/process/server");
|
|
15
16
|
let _orkestrel_mcp = require("@orkestrel/mcp");
|
|
16
17
|
let _orkestrel_pool = require("@orkestrel/pool");
|
|
17
18
|
let _orkestrel_mcp_server = require("@orkestrel/mcp/server");
|
|
18
19
|
let _orkestrel_tool = require("@orkestrel/tool");
|
|
19
|
-
|
|
20
|
+
// src/server/constants.ts
|
|
20
21
|
/**
|
|
21
22
|
* Sets the default CDP port probed for an existing browser and used for launches, `9222`.
|
|
22
23
|
*/
|
|
@@ -332,8 +333,43 @@ var BROWSER_SERVER_COPY = Object.freeze({
|
|
|
332
333
|
annotations: Object.freeze({ pure: false })
|
|
333
334
|
})
|
|
334
335
|
});
|
|
335
|
-
|
|
336
|
-
|
|
336
|
+
// src/server/BrowserEndpoint.ts
|
|
337
|
+
/** Reads the announced CDP endpoint while continuing to drain the browser's standard error. */
|
|
338
|
+
var BrowserEndpoint = class {
|
|
339
|
+
#completion = Promise.withResolvers();
|
|
340
|
+
#pending = "";
|
|
341
|
+
#endpoint;
|
|
342
|
+
constructor(stream, signal) {
|
|
343
|
+
if (signal.aborted) this.#completion.reject(signal.reason);
|
|
344
|
+
signal.addEventListener("abort", () => this.#completion.reject(signal.reason), { once: true });
|
|
345
|
+
stream.on("error", this.#completion.reject);
|
|
346
|
+
stream.setEncoding("utf8");
|
|
347
|
+
stream.on("data", this.#handleChunk.bind(this));
|
|
348
|
+
stream.on("close", this.#handleClose.bind(this));
|
|
349
|
+
}
|
|
350
|
+
get endpoint() {
|
|
351
|
+
return this.#completion.promise;
|
|
352
|
+
}
|
|
353
|
+
#handleChunk(chunk) {
|
|
354
|
+
if (this.#endpoint !== void 0 || !(0, _orkestrel_contract.isString)(chunk)) return;
|
|
355
|
+
const lines = (this.#pending + chunk).split(/\r\n|\n/);
|
|
356
|
+
this.#pending = lines.pop() ?? "";
|
|
357
|
+
for (const line of lines) {
|
|
358
|
+
const endpoint = BROWSER_DEVTOOLS_PATTERN.exec(line)?.[1];
|
|
359
|
+
if (endpoint === void 0) continue;
|
|
360
|
+
this.#endpoint = endpoint;
|
|
361
|
+
this.#pending = "";
|
|
362
|
+
this.#completion.resolve(endpoint);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
#handleClose() {
|
|
367
|
+
const endpoint = this.#endpoint === void 0 ? BROWSER_DEVTOOLS_PATTERN.exec(this.#pending)?.[1] : void 0;
|
|
368
|
+
if (endpoint !== void 0) this.#completion.resolve(endpoint);
|
|
369
|
+
else this.#completion.reject(new _src_core.BrowserError("CONNECTION", "Browser closed its standard error before reporting a CDP endpoint"));
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
// src/server/helpers.ts
|
|
337
373
|
/**
|
|
338
374
|
* Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
|
|
339
375
|
* @param value - The viewport environment variable, or undefined when unset
|
|
@@ -354,21 +390,6 @@ function parseBrowserViewport(value) {
|
|
|
354
390
|
};
|
|
355
391
|
}
|
|
356
392
|
/**
|
|
357
|
-
* Probes whether a process has not been confirmed absent.
|
|
358
|
-
* @param pid - Process identifier to probe with signal zero
|
|
359
|
-
* @returns True if the host has not confirmed absence with ESRCH; false otherwise
|
|
360
|
-
* @example
|
|
361
|
-
* probeProcess(process.pid) // true
|
|
362
|
-
*/
|
|
363
|
-
function probeProcess(pid) {
|
|
364
|
-
try {
|
|
365
|
-
process.kill(pid, 0);
|
|
366
|
-
return true;
|
|
367
|
-
} catch (error) {
|
|
368
|
-
return !((0, _orkestrel_contract.isError)(error) && "code" in error && error.code === "ESRCH");
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
393
|
* Parses a profile record naming a positive process identifier and a loopback DevTools endpoint.
|
|
373
394
|
* @param text - JSON record read from a browse profile
|
|
374
395
|
* @returns The record, or undefined when its shape or endpoint is refused
|
|
@@ -700,32 +721,7 @@ function launchBrowserProcess(executable, port, headless, profile, extra) {
|
|
|
700
721
|
* @throws When the stream closes without the line, or the signal aborts first
|
|
701
722
|
*/
|
|
702
723
|
function readBrowserEndpoint(stream, signal) {
|
|
703
|
-
|
|
704
|
-
let pending = "";
|
|
705
|
-
let found = false;
|
|
706
|
-
if (signal.aborted) read.reject(signal.reason);
|
|
707
|
-
signal.addEventListener("abort", () => read.reject(signal.reason), { once: true });
|
|
708
|
-
stream.on("error", read.reject);
|
|
709
|
-
stream.setEncoding("utf8");
|
|
710
|
-
stream.on("data", (chunk) => {
|
|
711
|
-
if (found || !(0, _orkestrel_contract.isString)(chunk)) return;
|
|
712
|
-
const lines = (pending + chunk).split(/\r\n|\n/);
|
|
713
|
-
pending = lines.pop() ?? "";
|
|
714
|
-
for (const line of lines) {
|
|
715
|
-
const endpoint = BROWSER_DEVTOOLS_PATTERN.exec(line)?.[1];
|
|
716
|
-
if (endpoint === void 0) continue;
|
|
717
|
-
found = true;
|
|
718
|
-
pending = "";
|
|
719
|
-
read.resolve(endpoint);
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
stream.on("close", () => {
|
|
724
|
-
const endpoint = found ? void 0 : BROWSER_DEVTOOLS_PATTERN.exec(pending)?.[1];
|
|
725
|
-
if (endpoint !== void 0) read.resolve(endpoint);
|
|
726
|
-
else read.reject(new _src_core.BrowserError("CONNECTION", "Browser closed its standard error before reporting a CDP endpoint"));
|
|
727
|
-
});
|
|
728
|
-
return read.promise;
|
|
724
|
+
return new BrowserEndpoint(stream, signal).endpoint;
|
|
729
725
|
}
|
|
730
726
|
/**
|
|
731
727
|
* Fetches the current CDP target list from a browser's `/json/list` endpoint, as a `Result`
|
|
@@ -796,8 +792,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
796
792
|
clearTimeout(timer);
|
|
797
793
|
}
|
|
798
794
|
}
|
|
799
|
-
|
|
800
|
-
//#region src/server/transports/WebSocketCDPTransport.ts
|
|
795
|
+
// src/server/transports/WebSocketCDPTransport.ts
|
|
801
796
|
/**
|
|
802
797
|
* Provides a raw CDP text transport backed by `@orkestrel/websocket`.
|
|
803
798
|
*
|
|
@@ -991,8 +986,7 @@ var WebSocketCDPTransport = class {
|
|
|
991
986
|
this.#closeResolve?.();
|
|
992
987
|
}
|
|
993
988
|
};
|
|
994
|
-
|
|
995
|
-
//#region src/server/writers/FileBrowserWriter.ts
|
|
989
|
+
// src/server/writers/FileBrowserWriter.ts
|
|
996
990
|
/**
|
|
997
991
|
* Persists captured browser bytes to the filesystem through `node:fs/promises`.
|
|
998
992
|
*
|
|
@@ -1021,11 +1015,35 @@ var FileBrowserWriter = class {
|
|
|
1021
1015
|
await (0, node_fs_promises.writeFile)(path, data);
|
|
1022
1016
|
}
|
|
1023
1017
|
};
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
var
|
|
1027
|
-
|
|
1028
|
-
|
|
1018
|
+
// src/server/BrowserProcessDrain.ts
|
|
1019
|
+
/** Waits for a process remainder whose host exposes no exit event. */
|
|
1020
|
+
var BrowserProcessDrain = class {
|
|
1021
|
+
#inspect;
|
|
1022
|
+
#deadline;
|
|
1023
|
+
#result;
|
|
1024
|
+
#confirmed = false;
|
|
1025
|
+
constructor(inspect, timeout) {
|
|
1026
|
+
this.#inspect = inspect;
|
|
1027
|
+
this.#deadline = performance.now() + timeout;
|
|
1028
|
+
this.#result = this.#wait();
|
|
1029
|
+
}
|
|
1030
|
+
get result() {
|
|
1031
|
+
return this.#result;
|
|
1032
|
+
}
|
|
1033
|
+
async #wait() {
|
|
1034
|
+
while (true) {
|
|
1035
|
+
const alive = this.#inspect();
|
|
1036
|
+
if (alive === false) return true;
|
|
1037
|
+
if (alive === true) this.#confirmed = true;
|
|
1038
|
+
const remaining = this.#deadline - performance.now();
|
|
1039
|
+
if (remaining <= 0) return this.#confirmed ? false : void 0;
|
|
1040
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100, remaining)));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
// package.json
|
|
1045
|
+
var version = "0.0.31";
|
|
1046
|
+
// src/server/BrowserMCPServer.ts
|
|
1029
1047
|
/**
|
|
1030
1048
|
* Implements `BrowserMCPServerInterface`: serves the browser vocabulary and the journey tools over
|
|
1031
1049
|
* the Model Context Protocol on stdio, and warms Chromium at server start.
|
|
@@ -1245,7 +1263,7 @@ var BrowserMCPServer = class {
|
|
|
1245
1263
|
for (const folder of this.#folders) {
|
|
1246
1264
|
try {
|
|
1247
1265
|
const record = parseBrowserProfileRecord(await (0, node_fs_promises.readFile)((0, node_path.join)(folder, BROWSER_SERVER_RECORD), "utf8"));
|
|
1248
|
-
if (record === void 0 ||
|
|
1266
|
+
if (record === void 0 || (0, _orkestrel_process_server.inspectProcess)(record.pid) !== false) continue;
|
|
1249
1267
|
} catch (error) {
|
|
1250
1268
|
if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ENOENT") continue;
|
|
1251
1269
|
}
|
|
@@ -1403,16 +1421,34 @@ var BrowserMCPServer = class {
|
|
|
1403
1421
|
}
|
|
1404
1422
|
async #perform(holder, name, args, context) {
|
|
1405
1423
|
const signal = holder === this.#shared ? context.signal : AbortSignal.any([context.signal, holder.abort.signal]);
|
|
1406
|
-
|
|
1424
|
+
const lease = await this.#serveCall(holder, context, signal);
|
|
1425
|
+
const result = await this.#executeCall(lease, name, args, context, signal);
|
|
1426
|
+
context.signal.throwIfAborted();
|
|
1427
|
+
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call");
|
|
1428
|
+
if (!result.success) {
|
|
1429
|
+
if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
|
|
1430
|
+
await this.#race(this.#ping(lease.token.value), context.signal);
|
|
1431
|
+
} catch {}
|
|
1432
|
+
context.signal.throwIfAborted();
|
|
1433
|
+
if (this.#leases.get(holder) !== lease) {
|
|
1434
|
+
const loss = this.#departures.get(lease);
|
|
1435
|
+
throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease));
|
|
1436
|
+
}
|
|
1437
|
+
throw new _src_core.BrowserError("PROTOCOL", (0, _src_core.boundBrowserText)(this.#annotate(holder, result.error, lease), _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER));
|
|
1438
|
+
}
|
|
1439
|
+
return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
|
|
1440
|
+
}
|
|
1441
|
+
async #serveCall(holder, context, signal) {
|
|
1407
1442
|
try {
|
|
1408
|
-
|
|
1443
|
+
return await this.#serve(holder, signal);
|
|
1409
1444
|
} catch (error) {
|
|
1410
1445
|
context.signal.throwIfAborted();
|
|
1411
1446
|
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, `${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`);
|
|
1412
1447
|
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(error.code, this.#annotate(holder, `${error.code}: ${error.message}`));
|
|
1413
1448
|
throw error;
|
|
1414
1449
|
}
|
|
1415
|
-
|
|
1450
|
+
}
|
|
1451
|
+
async #executeCall(lease, name, args, context, signal) {
|
|
1416
1452
|
try {
|
|
1417
1453
|
const execution = lease.generation.toolset.tools.execute({
|
|
1418
1454
|
id: name,
|
|
@@ -1424,29 +1460,15 @@ var BrowserMCPServer = class {
|
|
|
1424
1460
|
});
|
|
1425
1461
|
const outcome = await (name === "replay" || name === "forget" ? execution : this.#race(execution, signal));
|
|
1426
1462
|
if (outcome === void 0) throw this.#ended();
|
|
1427
|
-
|
|
1463
|
+
return outcome;
|
|
1428
1464
|
} catch (error) {
|
|
1429
|
-
|
|
1465
|
+
return {
|
|
1430
1466
|
id: name,
|
|
1431
1467
|
name,
|
|
1432
1468
|
success: false,
|
|
1433
1469
|
error: (0, _orkestrel_contract.isError)(error) ? error.message : String(error)
|
|
1434
1470
|
};
|
|
1435
1471
|
}
|
|
1436
|
-
context.signal.throwIfAborted();
|
|
1437
|
-
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call");
|
|
1438
|
-
if (!result.success) {
|
|
1439
|
-
if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
|
|
1440
|
-
await this.#race(this.#ping(lease.token.value), context.signal);
|
|
1441
|
-
} catch {}
|
|
1442
|
-
context.signal.throwIfAborted();
|
|
1443
|
-
if (this.#leases.get(holder) !== lease) {
|
|
1444
|
-
const loss = this.#departures.get(lease);
|
|
1445
|
-
throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease));
|
|
1446
|
-
}
|
|
1447
|
-
throw new _src_core.BrowserError("PROTOCOL", (0, _src_core.boundBrowserText)(this.#annotate(holder, result.error, lease), _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER));
|
|
1448
|
-
}
|
|
1449
|
-
return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
|
|
1450
1472
|
}
|
|
1451
1473
|
#annotate(holder, value, lease) {
|
|
1452
1474
|
const notes = [];
|
|
@@ -1485,23 +1507,35 @@ var BrowserMCPServer = class {
|
|
|
1485
1507
|
async #grant(holder, signal) {
|
|
1486
1508
|
const held = this.#leases.get(holder);
|
|
1487
1509
|
if (held !== void 0) return held;
|
|
1488
|
-
|
|
1489
|
-
if (granting === void 0) {
|
|
1490
|
-
granting = Promise.resolve().then(async () => {
|
|
1491
|
-
await Promise.allSettled(this.#drains.get(holder) ?? []);
|
|
1492
|
-
if (this.#retentions.has(holder)) throw this.#unavailable(this.#retentions.get(holder));
|
|
1493
|
-
const token = await this.#pool.acquire(holder.abort.signal);
|
|
1494
|
-
return this.#hold(holder, token);
|
|
1495
|
-
});
|
|
1496
|
-
this.#grants.set(holder, granting);
|
|
1497
|
-
granting.finally(() => {
|
|
1498
|
-
if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
|
|
1499
|
-
}).catch(() => void 0);
|
|
1500
|
-
}
|
|
1510
|
+
const granting = this.#grants.get(holder) ?? this.#beginGrant(holder);
|
|
1501
1511
|
const lease = await this.#race(granting, signal);
|
|
1502
1512
|
if (lease === void 0) throw this.#ended();
|
|
1503
1513
|
return lease;
|
|
1504
1514
|
}
|
|
1515
|
+
async #prepareGeneration(slot, token) {
|
|
1516
|
+
const prepared = this.#prepared.get(slot);
|
|
1517
|
+
this.#prepared.delete(slot);
|
|
1518
|
+
try {
|
|
1519
|
+
return prepared ?? await this.#build(slot, token);
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
if (this.#losses.has(slot)) await this.#endings.get(slot)?.promise;
|
|
1522
|
+
else token.release();
|
|
1523
|
+
throw this.#unavailable(error);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
#beginGrant(holder) {
|
|
1527
|
+
const granting = Promise.resolve().then(async () => {
|
|
1528
|
+
await Promise.allSettled(this.#drains.get(holder) ?? []);
|
|
1529
|
+
if (this.#retentions.has(holder)) throw this.#unavailable(this.#retentions.get(holder));
|
|
1530
|
+
const token = await this.#pool.acquire(holder.abort.signal);
|
|
1531
|
+
return this.#hold(holder, token);
|
|
1532
|
+
});
|
|
1533
|
+
this.#grants.set(holder, granting);
|
|
1534
|
+
granting.finally(() => {
|
|
1535
|
+
if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
|
|
1536
|
+
}).catch(() => void 0);
|
|
1537
|
+
return granting;
|
|
1538
|
+
}
|
|
1505
1539
|
async #hold(holder, token) {
|
|
1506
1540
|
const slot = token.value;
|
|
1507
1541
|
if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted) {
|
|
@@ -1517,15 +1551,7 @@ var BrowserMCPServer = class {
|
|
|
1517
1551
|
token.release();
|
|
1518
1552
|
throw this.#unavailable(this.#losses.get(slot)?.cause);
|
|
1519
1553
|
}
|
|
1520
|
-
|
|
1521
|
-
this.#prepared.delete(slot);
|
|
1522
|
-
try {
|
|
1523
|
-
generation ??= await this.#build(slot, token);
|
|
1524
|
-
} catch (error) {
|
|
1525
|
-
if (this.#losses.has(slot)) await this.#endings.get(slot)?.promise;
|
|
1526
|
-
else token.release();
|
|
1527
|
-
throw this.#unavailable(error);
|
|
1528
|
-
}
|
|
1554
|
+
const generation = await this.#prepareGeneration(slot, token);
|
|
1529
1555
|
const lease = {
|
|
1530
1556
|
token,
|
|
1531
1557
|
generation
|
|
@@ -1701,9 +1727,9 @@ var BrowserMCPServer = class {
|
|
|
1701
1727
|
} else if (removed) lease.token.release();
|
|
1702
1728
|
}
|
|
1703
1729
|
#clean(slot, generation) {
|
|
1704
|
-
|
|
1705
|
-
if (
|
|
1706
|
-
cleaning = Promise.resolve().then(async () => {
|
|
1730
|
+
const pending = this.#cleaning.get(generation);
|
|
1731
|
+
if (pending !== void 0) return pending;
|
|
1732
|
+
const cleaning = Promise.resolve().then(async () => {
|
|
1707
1733
|
await generation.toolset.destroy().catch(this.#fault.bind(this));
|
|
1708
1734
|
await generation.context.close().catch((error) => {
|
|
1709
1735
|
if (!this.#losses.has(slot) && slot.browser.status === "connected") this.#fault(error);
|
|
@@ -1733,26 +1759,11 @@ var BrowserMCPServer = class {
|
|
|
1733
1759
|
}
|
|
1734
1760
|
async #construct(slot, token) {
|
|
1735
1761
|
const directory = (0, node_path.join)(slot.profile, "contexts", (0, node_crypto.randomUUID)(), "downloads");
|
|
1736
|
-
|
|
1737
|
-
let toolset;
|
|
1762
|
+
const cleanup = [];
|
|
1738
1763
|
try {
|
|
1739
1764
|
await (0, node_fs_promises.mkdir)(directory, { recursive: true });
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
context = await slot.browser.isolate({
|
|
1743
|
-
reference: this.#reference,
|
|
1744
|
-
downloads: { path: directory },
|
|
1745
|
-
...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
|
|
1746
|
-
});
|
|
1747
|
-
page = await context.create();
|
|
1748
|
-
} catch (error) {
|
|
1749
|
-
if (token !== void 0) {
|
|
1750
|
-
if (![...this.#leases.values()].some((lease) => lease.token.value === slot) && (this.#building.get(slot)?.size ?? 0) === 1) token.release();
|
|
1751
|
-
this.#lose(slot, error);
|
|
1752
|
-
}
|
|
1753
|
-
throw error;
|
|
1754
|
-
}
|
|
1755
|
-
toolset = (0, _src_core.createBrowserToolset)(page, {
|
|
1765
|
+
const { context, page } = await this.#openGeneration(slot, directory, cleanup, token);
|
|
1766
|
+
const toolset = (0, _src_core.createBrowserToolset)(page, {
|
|
1756
1767
|
context,
|
|
1757
1768
|
notes: this.#notice.bind(this, context),
|
|
1758
1769
|
journeys: {
|
|
@@ -1761,6 +1772,9 @@ var BrowserMCPServer = class {
|
|
|
1761
1772
|
readonly: this.#readonly
|
|
1762
1773
|
}
|
|
1763
1774
|
});
|
|
1775
|
+
cleanup.push(async () => {
|
|
1776
|
+
await toolset.destroy().catch(this.#fault.bind(this));
|
|
1777
|
+
});
|
|
1764
1778
|
await toolset.start();
|
|
1765
1779
|
const generation = {
|
|
1766
1780
|
context,
|
|
@@ -1772,10 +1786,7 @@ var BrowserMCPServer = class {
|
|
|
1772
1786
|
this.#owned.set(slot, owned);
|
|
1773
1787
|
return generation;
|
|
1774
1788
|
} catch (error) {
|
|
1775
|
-
|
|
1776
|
-
await context?.close().catch((failure) => {
|
|
1777
|
-
if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !((0, _src_core.isBrowserError)(failure) && failure.code === "DISCONNECTED")) this.#fault(failure);
|
|
1778
|
-
});
|
|
1789
|
+
for (const release of cleanup.toReversed()) await release();
|
|
1779
1790
|
await (0, node_fs_promises.rm)(directory, {
|
|
1780
1791
|
recursive: true,
|
|
1781
1792
|
force: true
|
|
@@ -1783,6 +1794,31 @@ var BrowserMCPServer = class {
|
|
|
1783
1794
|
throw error;
|
|
1784
1795
|
}
|
|
1785
1796
|
}
|
|
1797
|
+
async #openGeneration(slot, directory, cleanup, token) {
|
|
1798
|
+
try {
|
|
1799
|
+
const context = await slot.browser.isolate({
|
|
1800
|
+
reference: this.#reference,
|
|
1801
|
+
downloads: { path: directory },
|
|
1802
|
+
...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
|
|
1803
|
+
});
|
|
1804
|
+
cleanup.push(this.#closeGenerationContext.bind(this, context, slot));
|
|
1805
|
+
return {
|
|
1806
|
+
context,
|
|
1807
|
+
page: await context.create()
|
|
1808
|
+
};
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
if (token !== void 0) {
|
|
1811
|
+
if (![...this.#leases.values()].some((lease) => lease.token.value === slot) && (this.#building.get(slot)?.size ?? 0) === 1) token.release();
|
|
1812
|
+
this.#lose(slot, error);
|
|
1813
|
+
}
|
|
1814
|
+
throw error;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
async #closeGenerationContext(context, slot) {
|
|
1818
|
+
await context.close().catch((failure) => {
|
|
1819
|
+
if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !((0, _src_core.isBrowserError)(failure) && failure.code === "DISCONNECTED")) this.#fault(failure);
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1786
1822
|
async #destroyRecord(slot) {
|
|
1787
1823
|
const cause = this.#losses.get(slot)?.cause;
|
|
1788
1824
|
if ((0, _src_core.isBrowserError)(cause) && cause.code === "TIMEOUT" && slot.browser.pid !== void 0) try {
|
|
@@ -1812,12 +1848,23 @@ var BrowserMCPServer = class {
|
|
|
1812
1848
|
if (this.#closing !== void 0) throw this.#ended();
|
|
1813
1849
|
if (this.#stranded !== void 0) throw new _src_core.BrowserError(BROWSER_SERVER_TEARDOWN, (0, _orkestrel_contract.isError)(this.#stranded) ? this.#stranded.message : String(this.#stranded), (0, _src_core.isBrowserError)(this.#stranded) ? this.#stranded.context : void 0);
|
|
1814
1850
|
const profile = (0, node_path.join)(this.#root, ".profiles", formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)()));
|
|
1815
|
-
let browser;
|
|
1816
|
-
let slot;
|
|
1817
1851
|
try {
|
|
1818
1852
|
await (0, node_fs_promises.mkdir)(profile);
|
|
1819
1853
|
this.#folders.add(profile);
|
|
1820
|
-
browser = this.#
|
|
1854
|
+
const browser = await this.#launchWarm(profile);
|
|
1855
|
+
await this.#connectWarm(profile, browser);
|
|
1856
|
+
return await this.#prepareWarm({
|
|
1857
|
+
browser,
|
|
1858
|
+
profile
|
|
1859
|
+
});
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_LAUNCH, error)}`);
|
|
1862
|
+
throw error;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
async #launchWarm(profile) {
|
|
1866
|
+
try {
|
|
1867
|
+
return this.#launcher({
|
|
1821
1868
|
headless: this.#headless,
|
|
1822
1869
|
profile,
|
|
1823
1870
|
cdp: { discover: false },
|
|
@@ -1826,6 +1873,13 @@ var BrowserMCPServer = class {
|
|
|
1826
1873
|
...this.#viewport === void 0 ? {} : { viewport: this.#viewport },
|
|
1827
1874
|
...process.platform === "linux" && process.getuid?.() === 0 ? { args: ["--no-sandbox"] } : {}
|
|
1828
1875
|
});
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
await this.#discardWarm(profile, void 0);
|
|
1878
|
+
throw error;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
async #connectWarm(profile, browser) {
|
|
1882
|
+
try {
|
|
1829
1883
|
await browser.connect();
|
|
1830
1884
|
if (browser.pid !== void 0 && browser.endpoint !== void 0) {
|
|
1831
1885
|
const record = (0, node_path.join)(profile, BROWSER_SERVER_RECORD);
|
|
@@ -1835,10 +1889,13 @@ var BrowserMCPServer = class {
|
|
|
1835
1889
|
}));
|
|
1836
1890
|
await (0, node_fs_promises.rename)(`${record}.tmp`, record);
|
|
1837
1891
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
await this.#discardWarm(profile, browser);
|
|
1894
|
+
throw error;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
async #prepareWarm(slot) {
|
|
1898
|
+
try {
|
|
1842
1899
|
const ending = Promise.withResolvers();
|
|
1843
1900
|
ending.promise.catch(() => void 0);
|
|
1844
1901
|
this.#endings.set(slot, ending);
|
|
@@ -1847,13 +1904,15 @@ var BrowserMCPServer = class {
|
|
|
1847
1904
|
this.#observe(slot, generation);
|
|
1848
1905
|
return slot;
|
|
1849
1906
|
} catch (error) {
|
|
1850
|
-
|
|
1851
|
-
this.#stranded = failure;
|
|
1852
|
-
});
|
|
1853
|
-
if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_LAUNCH, error)}`);
|
|
1907
|
+
await this.#discardWarm(slot.profile, slot.browser, slot);
|
|
1854
1908
|
throw error;
|
|
1855
1909
|
}
|
|
1856
1910
|
}
|
|
1911
|
+
async #discardWarm(profile, browser, slot) {
|
|
1912
|
+
if (this.#folders.has(profile)) await (slot === void 0 ? this.#destroySlot(profile, browser) : this.#destroyRecord(slot)).catch((failure) => {
|
|
1913
|
+
this.#stranded = failure;
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1857
1916
|
async #destroySlot(profile, browser) {
|
|
1858
1917
|
try {
|
|
1859
1918
|
await browser?.destroy();
|
|
@@ -1876,13 +1935,13 @@ var BrowserMCPServer = class {
|
|
|
1876
1935
|
for (const entry of await (0, node_fs_promises.readdir)(profiles)) {
|
|
1877
1936
|
if (this.#closing !== void 0) return;
|
|
1878
1937
|
const pid = parseBrowserLockEntry(entry);
|
|
1879
|
-
if (pid === void 0 || pid === process.pid ||
|
|
1938
|
+
if (pid === void 0 || pid === process.pid || (0, _orkestrel_process_server.inspectProcess)(pid) !== false) continue;
|
|
1880
1939
|
const folder = (0, node_path.join)(profiles, entry);
|
|
1881
1940
|
this.#folders.add(folder);
|
|
1882
1941
|
try {
|
|
1883
1942
|
const record = parseBrowserProfileRecord(await (0, node_fs_promises.readFile)((0, node_path.join)(folder, BROWSER_SERVER_RECORD), "utf8"));
|
|
1884
1943
|
if (record === void 0) continue;
|
|
1885
|
-
if (
|
|
1944
|
+
if ((0, _orkestrel_process_server.inspectProcess)(record.pid) !== false) {
|
|
1886
1945
|
const transport = createWebSocketCDPTransport({ url: record.endpoint });
|
|
1887
1946
|
const client = (0, _src_core.createCDPClient)({ transport });
|
|
1888
1947
|
try {
|
|
@@ -1893,7 +1952,7 @@ var BrowserMCPServer = class {
|
|
|
1893
1952
|
await transport.close();
|
|
1894
1953
|
await client.close();
|
|
1895
1954
|
}
|
|
1896
|
-
if (
|
|
1955
|
+
if ((0, _orkestrel_process_server.inspectProcess)(record.pid) !== false) continue;
|
|
1897
1956
|
}
|
|
1898
1957
|
} catch (error) {
|
|
1899
1958
|
if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ENOENT") continue;
|
|
@@ -1960,8 +2019,7 @@ var BrowserMCPServer = class {
|
|
|
1960
2019
|
return new _src_core.BrowserError("CLOSED", "the browser session ended", { subject: "toolset" });
|
|
1961
2020
|
}
|
|
1962
2021
|
};
|
|
1963
|
-
|
|
1964
|
-
//#region src/server/stores/FileBrowserStore.ts
|
|
2022
|
+
// src/server/stores/FileBrowserStore.ts
|
|
1965
2023
|
/**
|
|
1966
2024
|
* Shares confined filesystem operations between the journey and run stores.
|
|
1967
2025
|
* @remarks Requires an existing root directory. Links present during a component check are refused;
|
|
@@ -2006,9 +2064,11 @@ var FileBrowserStore = class FileBrowserStore {
|
|
|
2006
2064
|
*/
|
|
2007
2065
|
async check(path, options) {
|
|
2008
2066
|
this.resolvePath(path);
|
|
2009
|
-
let current = this.#root;
|
|
2010
2067
|
const parts = (0, node_path.relative)(this.#root, path).split(node_path.sep).filter(Boolean);
|
|
2011
|
-
|
|
2068
|
+
return (await [void 0, ...parts].reduce(async (previous, part, index) => {
|
|
2069
|
+
const ancestor = await previous;
|
|
2070
|
+
if (!ancestor.exists) return ancestor;
|
|
2071
|
+
const current = part === void 0 ? ancestor.path : this.resolvePath(ancestor.path, part);
|
|
2012
2072
|
options?.signal?.throwIfAborted();
|
|
2013
2073
|
try {
|
|
2014
2074
|
const status = await (0, node_fs_promises.lstat)(current);
|
|
@@ -2017,13 +2077,20 @@ var FileBrowserStore = class FileBrowserStore {
|
|
|
2017
2077
|
if (index < parts.length && !status.isDirectory()) throw new _src_core.BrowserError("STORE_PATH", `Not a directory: ${current}`);
|
|
2018
2078
|
} catch (error) {
|
|
2019
2079
|
options?.signal?.throwIfAborted();
|
|
2020
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") return
|
|
2080
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return {
|
|
2081
|
+
path: current,
|
|
2082
|
+
exists: false
|
|
2083
|
+
};
|
|
2021
2084
|
throw this.translateError(current, error);
|
|
2022
2085
|
}
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2086
|
+
return {
|
|
2087
|
+
path: current,
|
|
2088
|
+
exists: true
|
|
2089
|
+
};
|
|
2090
|
+
}, Promise.resolve({
|
|
2091
|
+
path: this.#root,
|
|
2092
|
+
exists: true
|
|
2093
|
+
}))).exists;
|
|
2027
2094
|
}
|
|
2028
2095
|
/** Creates checked directories one component at a time. @param path - Directory @param options - Cancellation options */
|
|
2029
2096
|
async createDirectory(path, options) {
|
|
@@ -2058,35 +2125,45 @@ var FileBrowserStore = class FileBrowserStore {
|
|
|
2058
2125
|
/** Atomically replaces a file and cleans its owned temporary file on failure. @param path - Destination @param value - Bytes or UTF-8 @param options - Cancellation options */
|
|
2059
2126
|
async write(path, value, options) {
|
|
2060
2127
|
const temporary = this.resolvePath((0, node_path.dirname)(path), `.${(0, node_crypto.randomUUID)()}.tmp`);
|
|
2061
|
-
|
|
2128
|
+
const file = await this.#openTemporary(path, temporary, options);
|
|
2062
2129
|
try {
|
|
2063
|
-
await this
|
|
2064
|
-
|
|
2065
|
-
options?.signal?.throwIfAborted();
|
|
2066
|
-
const file = await (0, node_fs_promises.open)(temporary, "wx");
|
|
2067
|
-
owned = true;
|
|
2130
|
+
await this.#commitTemporary(file, path, temporary, value, options);
|
|
2131
|
+
} catch (error) {
|
|
2068
2132
|
try {
|
|
2069
2133
|
options?.signal?.throwIfAborted();
|
|
2070
|
-
|
|
2134
|
+
throw this.translateError(path, error);
|
|
2071
2135
|
} finally {
|
|
2072
|
-
await
|
|
2136
|
+
await this.remove(temporary);
|
|
2073
2137
|
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
async #openTemporary(path, temporary, options) {
|
|
2141
|
+
try {
|
|
2074
2142
|
await this.check(path, options);
|
|
2075
2143
|
await this.check(temporary, options);
|
|
2076
2144
|
options?.signal?.throwIfAborted();
|
|
2077
|
-
|
|
2078
|
-
await (0, node_fs_promises.rename)(temporary, path);
|
|
2079
|
-
} catch (error) {
|
|
2080
|
-
const target = await (0, node_fs_promises.lstat)(path).catch(() => void 0);
|
|
2081
|
-
if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new _src_core.BrowserError("STORE_FILE", `Cannot replace directory: ${path}`, { path });
|
|
2082
|
-
throw error;
|
|
2083
|
-
}
|
|
2084
|
-
owned = false;
|
|
2145
|
+
return await (0, node_fs_promises.open)(temporary, "wx");
|
|
2085
2146
|
} catch (error) {
|
|
2086
2147
|
options?.signal?.throwIfAborted();
|
|
2087
2148
|
throw this.translateError(path, error);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
async #commitTemporary(file, path, temporary, value, options) {
|
|
2152
|
+
try {
|
|
2153
|
+
options?.signal?.throwIfAborted();
|
|
2154
|
+
await file.writeFile(value, { signal: options?.signal });
|
|
2088
2155
|
} finally {
|
|
2089
|
-
|
|
2156
|
+
await file.close();
|
|
2157
|
+
}
|
|
2158
|
+
await this.check(path, options);
|
|
2159
|
+
await this.check(temporary, options);
|
|
2160
|
+
options?.signal?.throwIfAborted();
|
|
2161
|
+
try {
|
|
2162
|
+
await (0, node_fs_promises.rename)(temporary, path);
|
|
2163
|
+
} catch (error) {
|
|
2164
|
+
const target = await (0, node_fs_promises.lstat)(path).catch(() => void 0);
|
|
2165
|
+
if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new _src_core.BrowserError("STORE_FILE", `Cannot replace directory: ${path}`, { path });
|
|
2166
|
+
throw error;
|
|
2090
2167
|
}
|
|
2091
2168
|
}
|
|
2092
2169
|
/** Removes a checked file, accepting absence. @param path - File @param options - Cancellation options */
|
|
@@ -2126,43 +2203,43 @@ var FileBrowserStore = class FileBrowserStore {
|
|
|
2126
2203
|
await this.createDirectory((0, node_path.dirname)(path), options);
|
|
2127
2204
|
const name = formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)());
|
|
2128
2205
|
const entry = this.resolvePath(path, name);
|
|
2129
|
-
for (
|
|
2130
|
-
await this.check(
|
|
2206
|
+
for (const lock of Array.from({ length: 8 }, () => path)) {
|
|
2207
|
+
await this.check(lock, options);
|
|
2131
2208
|
options?.signal?.throwIfAborted();
|
|
2132
2209
|
try {
|
|
2133
|
-
await (0, node_fs_promises.mkdir)(
|
|
2210
|
+
await (0, node_fs_promises.mkdir)(lock);
|
|
2134
2211
|
} catch (error) {
|
|
2135
|
-
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw this.translateError(
|
|
2136
|
-
const entries = await this.#readLock(
|
|
2212
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw this.translateError(lock, error);
|
|
2213
|
+
const entries = await this.#readLock(lock, options);
|
|
2137
2214
|
if (entries === void 0) continue;
|
|
2138
2215
|
if (entries.length > 1) break;
|
|
2139
2216
|
const holder = entries[0];
|
|
2140
2217
|
if (holder !== void 0) {
|
|
2141
2218
|
const pid = parseBrowserLockEntry(holder);
|
|
2142
|
-
if (pid === void 0 ||
|
|
2143
|
-
if (!await this.#unlinkLock(this.resolvePath(
|
|
2219
|
+
if (pid === void 0 || (0, _orkestrel_process_server.inspectProcess)(pid) !== false) break;
|
|
2220
|
+
if (!await this.#unlinkLock(this.resolvePath(lock, holder))) continue;
|
|
2144
2221
|
}
|
|
2145
|
-
await this.#removeLock(
|
|
2222
|
+
await this.#removeLock(lock);
|
|
2146
2223
|
continue;
|
|
2147
2224
|
}
|
|
2148
|
-
|
|
2225
|
+
const cleanup = [];
|
|
2149
2226
|
try {
|
|
2150
2227
|
await this.check(entry, options);
|
|
2151
2228
|
try {
|
|
2152
2229
|
const file = await (0, node_fs_promises.open)(entry, "wx");
|
|
2153
|
-
|
|
2230
|
+
cleanup.push(this.#unlinkLock.bind(this, entry));
|
|
2154
2231
|
await file.close();
|
|
2155
2232
|
} catch (error) {
|
|
2156
2233
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
|
|
2157
2234
|
throw this.translateError(entry, error);
|
|
2158
2235
|
}
|
|
2159
|
-
const entries = await this.#readLock(
|
|
2236
|
+
const entries = await this.#readLock(lock, options);
|
|
2160
2237
|
if (entries?.length !== 1 || entries[0] !== name) break;
|
|
2161
2238
|
options?.signal?.throwIfAborted();
|
|
2162
2239
|
return await action();
|
|
2163
2240
|
} finally {
|
|
2164
|
-
|
|
2165
|
-
await this.#removeLock(
|
|
2241
|
+
for (const release of cleanup) await release();
|
|
2242
|
+
await this.#removeLock(lock);
|
|
2166
2243
|
}
|
|
2167
2244
|
}
|
|
2168
2245
|
throw new _src_core.BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
|
|
@@ -2186,27 +2263,18 @@ var FileBrowserStore = class FileBrowserStore {
|
|
|
2186
2263
|
faults,
|
|
2187
2264
|
truncated: false
|
|
2188
2265
|
};
|
|
2189
|
-
|
|
2190
|
-
try {
|
|
2191
|
-
options?.signal?.throwIfAborted();
|
|
2192
|
-
names = (await (0, node_fs_promises.readdir)(path)).sort();
|
|
2193
|
-
} catch (error) {
|
|
2194
|
-
options?.signal?.throwIfAborted();
|
|
2195
|
-
throw this.translateError(path, error);
|
|
2196
|
-
}
|
|
2197
|
-
let count = 0;
|
|
2266
|
+
const names = await this.#readNames(path, options);
|
|
2198
2267
|
for (const name of names) {
|
|
2199
2268
|
options?.signal?.throwIfAborted();
|
|
2200
2269
|
try {
|
|
2201
2270
|
const entry = await reader(name);
|
|
2202
2271
|
if (entry === void 0) continue;
|
|
2203
|
-
|
|
2204
|
-
if (entries.length
|
|
2205
|
-
entries,
|
|
2272
|
+
entries.push(entry);
|
|
2273
|
+
if (entries.length > offset + limit) return {
|
|
2274
|
+
entries: entries.filter((_entry, index) => index >= offset && index < offset + limit),
|
|
2206
2275
|
faults,
|
|
2207
2276
|
truncated: true
|
|
2208
2277
|
};
|
|
2209
|
-
entries.push(entry);
|
|
2210
2278
|
} catch (error) {
|
|
2211
2279
|
options?.signal?.throwIfAborted();
|
|
2212
2280
|
const reason = error instanceof _src_core.BrowserError ? error.context?.["reason"] : void 0;
|
|
@@ -2217,11 +2285,20 @@ var FileBrowserStore = class FileBrowserStore {
|
|
|
2217
2285
|
}
|
|
2218
2286
|
}
|
|
2219
2287
|
return {
|
|
2220
|
-
entries,
|
|
2288
|
+
entries: entries.filter((_entry, index) => index >= offset),
|
|
2221
2289
|
faults,
|
|
2222
2290
|
truncated: false
|
|
2223
2291
|
};
|
|
2224
2292
|
}
|
|
2293
|
+
async #readNames(path, options) {
|
|
2294
|
+
try {
|
|
2295
|
+
options?.signal?.throwIfAborted();
|
|
2296
|
+
return (await (0, node_fs_promises.readdir)(path)).sort();
|
|
2297
|
+
} catch (error) {
|
|
2298
|
+
options?.signal?.throwIfAborted();
|
|
2299
|
+
throw this.translateError(path, error);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2225
2302
|
/**
|
|
2226
2303
|
* Allocates a run directory exclusively, retrying colliding producer candidates.
|
|
2227
2304
|
* @param parent - Checked parent directory
|
|
@@ -2301,8 +2378,7 @@ var FileBrowserStore = class FileBrowserStore {
|
|
|
2301
2378
|
}
|
|
2302
2379
|
}
|
|
2303
2380
|
};
|
|
2304
|
-
|
|
2305
|
-
//#region src/server/stores/FileBrowserJourneyStore.ts
|
|
2381
|
+
// src/server/stores/FileBrowserJourneyStore.ts
|
|
2306
2382
|
/**
|
|
2307
2383
|
* Persists journeys with exclusive writes and revision counters retained across deletion.
|
|
2308
2384
|
* @remarks Requires an existing root. A live holder refuses immediately; dead or empty locks are reclaimed within a bounded attempt count.
|
|
@@ -2396,8 +2472,7 @@ var FileBrowserJourneyStore = class {
|
|
|
2396
2472
|
}, options);
|
|
2397
2473
|
}
|
|
2398
2474
|
};
|
|
2399
|
-
|
|
2400
|
-
//#region src/server/stores/FileBrowserRunStore.ts
|
|
2475
|
+
// src/server/stores/FileBrowserRunStore.ts
|
|
2401
2476
|
/**
|
|
2402
2477
|
* Persists runs and captures only in directories allocated by this instance.
|
|
2403
2478
|
* @remarks Requires an existing root. Reopened stores can read and delete saved runs.
|
|
@@ -2554,8 +2629,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2554
2629
|
}
|
|
2555
2630
|
}
|
|
2556
2631
|
};
|
|
2557
|
-
|
|
2558
|
-
//#region src/server/factories.ts
|
|
2632
|
+
// src/server/factories.ts
|
|
2559
2633
|
/**
|
|
2560
2634
|
* Creates a raw-CDP `BrowserInterface` façade with discovery, connection, and lifecycle
|
|
2561
2635
|
* management.
|
|
@@ -2635,8 +2709,7 @@ function createFileBrowserRunStore(options) {
|
|
|
2635
2709
|
function createBrowserMCPServer(options) {
|
|
2636
2710
|
return new BrowserMCPServer(options);
|
|
2637
2711
|
}
|
|
2638
|
-
|
|
2639
|
-
//#region src/server/Browser.ts
|
|
2712
|
+
// src/server/Browser.ts
|
|
2640
2713
|
/**
|
|
2641
2714
|
* Discovers, launches, connects to, and owns Chromium-family browser sessions.
|
|
2642
2715
|
*
|
|
@@ -2753,30 +2826,30 @@ var Browser = class {
|
|
|
2753
2826
|
return [...this.#contexts];
|
|
2754
2827
|
}
|
|
2755
2828
|
async isolate(options) {
|
|
2756
|
-
const
|
|
2757
|
-
this.#isolating.add(
|
|
2829
|
+
const pending = this.#isolate(options);
|
|
2830
|
+
this.#isolating.add(pending);
|
|
2758
2831
|
try {
|
|
2759
|
-
return await
|
|
2832
|
+
return await pending;
|
|
2760
2833
|
} finally {
|
|
2761
|
-
this.#isolating.delete(
|
|
2834
|
+
this.#isolating.delete(pending);
|
|
2762
2835
|
}
|
|
2763
2836
|
}
|
|
2764
2837
|
async create(options) {
|
|
2765
2838
|
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2766
2839
|
const client = this.#client;
|
|
2767
2840
|
if (this.#status !== "connected" || client === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
|
|
2768
|
-
|
|
2769
|
-
if (context === void 0) {
|
|
2770
|
-
context = new _src_core.BrowserContext(client, {
|
|
2771
|
-
...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
|
|
2772
|
-
writer: createFileBrowserWriter()
|
|
2773
|
-
});
|
|
2774
|
-
this.#registerContext(context);
|
|
2775
|
-
}
|
|
2776
|
-
const page = await context.create(options);
|
|
2841
|
+
const page = await (this.#contexts[0] ?? this.#defaultContext(client)).create(options);
|
|
2777
2842
|
this.#emitter.emit("page", page);
|
|
2778
2843
|
return page;
|
|
2779
2844
|
}
|
|
2845
|
+
#defaultContext(client) {
|
|
2846
|
+
const context = new _src_core.BrowserContext(client, {
|
|
2847
|
+
...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
|
|
2848
|
+
writer: createFileBrowserWriter()
|
|
2849
|
+
});
|
|
2850
|
+
this.#registerContext(context);
|
|
2851
|
+
return context;
|
|
2852
|
+
}
|
|
2780
2853
|
destroy() {
|
|
2781
2854
|
const active = this.#shutdown;
|
|
2782
2855
|
if (active !== void 0) return active;
|
|
@@ -3070,41 +3143,40 @@ var Browser = class {
|
|
|
3070
3143
|
async #launch() {
|
|
3071
3144
|
if (this.#process !== void 0) throw new _src_core.BrowserError("CONNECTION", "A browser process is already active on this instance");
|
|
3072
3145
|
const requestedEngine = this.#options.browsers?.engine;
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
}
|
|
3146
|
+
const supplied = this.#options.executable;
|
|
3147
|
+
const selected = supplied === void 0 ? findSystemBrowsers({
|
|
3148
|
+
...this.#options.browsers,
|
|
3149
|
+
...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
|
|
3150
|
+
})[0] : {
|
|
3151
|
+
executable: supplied,
|
|
3152
|
+
engine: parseBrowserEngine(supplied) ?? "chromium"
|
|
3153
|
+
};
|
|
3154
|
+
const executable = selected?.executable;
|
|
3155
|
+
const resolvedEngine = selected?.engine;
|
|
3084
3156
|
if (executable === void 0) throw new _src_core.BrowserError("CONNECTION", "No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
|
|
3085
3157
|
this.#engine = resolvedEngine ?? "chromium";
|
|
3086
3158
|
await this.#releaseProfile();
|
|
3087
3159
|
const profile = await createBrowserProfile(this.#options.profile);
|
|
3088
3160
|
this.#profile = profile;
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
process = launchBrowserProcess(executable, this.#options.cdp?.port, this.#options.headless ?? true, profile.path, this.#options.args);
|
|
3092
|
-
} catch (error) {
|
|
3161
|
+
const launched = (0, _orkestrel_contract.attempt)(() => launchBrowserProcess(executable, this.#options.cdp?.port, this.#options.headless ?? true, profile.path, this.#options.args));
|
|
3162
|
+
if (!launched.success) {
|
|
3093
3163
|
await this.#releaseProfile();
|
|
3094
|
-
throw error;
|
|
3164
|
+
throw launched.error;
|
|
3095
3165
|
}
|
|
3166
|
+
const process = launched.value;
|
|
3096
3167
|
this.#process = process;
|
|
3097
|
-
|
|
3168
|
+
const cleanup = [];
|
|
3098
3169
|
try {
|
|
3099
3170
|
const endpoint = await this.#waitForLaunch(process, executable, this.#options.args);
|
|
3100
3171
|
const transport = createWebSocketCDPTransport({
|
|
3101
3172
|
url: endpoint,
|
|
3102
3173
|
timeout: this.#timeout()
|
|
3103
3174
|
});
|
|
3104
|
-
client = new _src_core.CDPClient({
|
|
3175
|
+
const client = new _src_core.CDPClient({
|
|
3105
3176
|
transport,
|
|
3106
3177
|
timeout: this.#timeout()
|
|
3107
3178
|
});
|
|
3179
|
+
cleanup.push(this.#closeClient.bind(this, client));
|
|
3108
3180
|
await this.#raceAbort(client.connect());
|
|
3109
3181
|
await this.#takeEndpointOwner(process, client);
|
|
3110
3182
|
const connection = this.#options.profile === void 0 ? "launch" : "persistent";
|
|
@@ -3121,7 +3193,7 @@ var Browser = class {
|
|
|
3121
3193
|
} catch (error) {
|
|
3122
3194
|
this.#unbindTransport();
|
|
3123
3195
|
this.#unbindProcess();
|
|
3124
|
-
await
|
|
3196
|
+
for (const release of cleanup) await release();
|
|
3125
3197
|
await this.#terminate(process);
|
|
3126
3198
|
await this.#releaseProfile();
|
|
3127
3199
|
this.#process = void 0;
|
|
@@ -3147,18 +3219,8 @@ var Browser = class {
|
|
|
3147
3219
|
*/
|
|
3148
3220
|
async #takeEndpointOwner(process, client) {
|
|
3149
3221
|
if (process.exitCode === null && process.signalCode === null) return;
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
result = await client.send("SystemInfo.getProcessInfo");
|
|
3153
|
-
} catch {
|
|
3154
|
-
result = void 0;
|
|
3155
|
-
}
|
|
3156
|
-
let pid;
|
|
3157
|
-
if ((0, _orkestrel_contract.isRecord)(result) && (0, _orkestrel_contract.isArray)(result["processInfo"])) for (const entry of result["processInfo"]) {
|
|
3158
|
-
if (!(0, _orkestrel_contract.isRecord)(entry) || entry["type"] !== "browser" || !(0, _orkestrel_contract.isInteger)(entry["id"])) continue;
|
|
3159
|
-
pid = entry["id"];
|
|
3160
|
-
break;
|
|
3161
|
-
}
|
|
3222
|
+
const result = await client.send("SystemInfo.getProcessInfo").catch(() => void 0);
|
|
3223
|
+
const pid = (0, _orkestrel_contract.isRecord)(result) && (0, _orkestrel_contract.isArray)(result["processInfo"]) ? Iterator.from(result["processInfo"]).map((entry) => (0, _orkestrel_contract.isRecord)(entry) && entry["type"] === "browser" && (0, _orkestrel_contract.isInteger)(entry["id"]) ? entry["id"] : void 0).find((id) => id !== void 0) : void 0;
|
|
3162
3224
|
if (pid === void 0) {
|
|
3163
3225
|
await this.#closeRemote(client);
|
|
3164
3226
|
throw new _src_core.BrowserError("CONNECTION", "The browser launcher exited without naming the process serving its CDP endpoint", {
|
|
@@ -3226,12 +3288,7 @@ var Browser = class {
|
|
|
3226
3288
|
async #syncContexts() {
|
|
3227
3289
|
const client = this.#client;
|
|
3228
3290
|
if (client === void 0) return;
|
|
3229
|
-
|
|
3230
|
-
try {
|
|
3231
|
-
result = await client.send("Target.getTargets");
|
|
3232
|
-
} catch {
|
|
3233
|
-
return;
|
|
3234
|
-
}
|
|
3291
|
+
const result = await client.send("Target.getTargets").catch(() => void 0);
|
|
3235
3292
|
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(result["targetInfos"])) return;
|
|
3236
3293
|
const pages = [];
|
|
3237
3294
|
for (const target of result["targetInfos"]) {
|
|
@@ -3252,63 +3309,63 @@ var Browser = class {
|
|
|
3252
3309
|
this.#registerContext(context);
|
|
3253
3310
|
}
|
|
3254
3311
|
async #destroyResources() {
|
|
3255
|
-
let terminated = false;
|
|
3256
3312
|
try {
|
|
3257
|
-
await this.#
|
|
3258
|
-
|
|
3259
|
-
this.#
|
|
3260
|
-
|
|
3261
|
-
if (this.#owned === true) {
|
|
3262
|
-
await this.#closeContexts();
|
|
3263
|
-
if (this.#process === void 0) await this.#closeRemote(client);
|
|
3264
|
-
} else await this.#destroyContexts();
|
|
3265
|
-
this.#contexts = [];
|
|
3266
|
-
await this.#terminate(this.#process);
|
|
3267
|
-
this.#process = void 0;
|
|
3268
|
-
this.#servingPid = void 0;
|
|
3269
|
-
terminated = true;
|
|
3270
|
-
} finally {
|
|
3271
|
-
const contexts = this.#contexts;
|
|
3272
|
-
this.#contexts = [];
|
|
3273
|
-
const client = this.#client;
|
|
3274
|
-
this.#client = void 0;
|
|
3275
|
-
try {
|
|
3276
|
-
await this.#cleanupLocal(contexts, client);
|
|
3277
|
-
if (terminated) await this.#releaseProfile();
|
|
3278
|
-
} finally {
|
|
3279
|
-
this.#finish();
|
|
3280
|
-
}
|
|
3313
|
+
await this.#destroySession();
|
|
3314
|
+
} catch (error) {
|
|
3315
|
+
await this.#finishResources(false);
|
|
3316
|
+
throw error;
|
|
3281
3317
|
}
|
|
3318
|
+
await this.#finishResources(true);
|
|
3319
|
+
}
|
|
3320
|
+
async #destroySession() {
|
|
3321
|
+
await this.#settle();
|
|
3322
|
+
this.#unbindTransport();
|
|
3323
|
+
this.#unbindProcess();
|
|
3324
|
+
const client = this.#client;
|
|
3325
|
+
if (this.#owned === true) {
|
|
3326
|
+
await this.#closeContexts();
|
|
3327
|
+
if (this.#process === void 0) await this.#closeRemote(client);
|
|
3328
|
+
} else await this.#destroyContexts();
|
|
3329
|
+
this.#contexts = [];
|
|
3330
|
+
await this.#terminate(this.#process);
|
|
3331
|
+
this.#process = void 0;
|
|
3332
|
+
this.#servingPid = void 0;
|
|
3282
3333
|
}
|
|
3283
3334
|
async #closeResources() {
|
|
3284
|
-
let terminated = false;
|
|
3285
3335
|
try {
|
|
3286
|
-
await this.#
|
|
3287
|
-
|
|
3288
|
-
this.#
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3336
|
+
await this.#closeSession();
|
|
3337
|
+
} catch (error) {
|
|
3338
|
+
await this.#finishResources(false);
|
|
3339
|
+
throw error;
|
|
3340
|
+
}
|
|
3341
|
+
await this.#finishResources(true);
|
|
3342
|
+
}
|
|
3343
|
+
async #closeSession() {
|
|
3344
|
+
await this.#settle();
|
|
3345
|
+
this.#unbindTransport();
|
|
3346
|
+
this.#unbindProcess();
|
|
3347
|
+
const client = this.#client;
|
|
3348
|
+
await this.#closeContexts();
|
|
3349
|
+
this.#contexts = [];
|
|
3350
|
+
await this.#closeRemote(client);
|
|
3351
|
+
const process = this.#process;
|
|
3352
|
+
if (process !== void 0) {
|
|
3353
|
+
if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
|
|
3354
|
+
await this.#closeProcessPipe(process);
|
|
3355
|
+
}
|
|
3356
|
+
this.#process = void 0;
|
|
3357
|
+
this.#servingPid = void 0;
|
|
3358
|
+
}
|
|
3359
|
+
async #finishResources(release) {
|
|
3360
|
+
const contexts = this.#contexts;
|
|
3361
|
+
this.#contexts = [];
|
|
3362
|
+
const client = this.#client;
|
|
3363
|
+
this.#client = void 0;
|
|
3364
|
+
try {
|
|
3365
|
+
await this.#cleanupLocal(contexts, client);
|
|
3366
|
+
if (release) await this.#releaseProfile();
|
|
3301
3367
|
} finally {
|
|
3302
|
-
|
|
3303
|
-
this.#contexts = [];
|
|
3304
|
-
const client = this.#client;
|
|
3305
|
-
this.#client = void 0;
|
|
3306
|
-
try {
|
|
3307
|
-
await this.#cleanupLocal(contexts, client);
|
|
3308
|
-
if (terminated) await this.#releaseProfile();
|
|
3309
|
-
} finally {
|
|
3310
|
-
this.#finish();
|
|
3311
|
-
}
|
|
3368
|
+
this.#finish();
|
|
3312
3369
|
}
|
|
3313
3370
|
}
|
|
3314
3371
|
async #settle() {
|
|
@@ -3324,29 +3381,29 @@ var Browser = class {
|
|
|
3324
3381
|
if (this.#exitCleanup === cleanup) this.#exitCleanup = void 0;
|
|
3325
3382
|
}
|
|
3326
3383
|
async #closeRemote(client) {
|
|
3327
|
-
|
|
3328
|
-
let temporary = false;
|
|
3329
|
-
if (remote === void 0 && this.#owned === true && this.#endpoint !== void 0) {
|
|
3330
|
-
const transport = createWebSocketCDPTransport({
|
|
3331
|
-
url: this.#endpoint,
|
|
3332
|
-
timeout: this.#timeout()
|
|
3333
|
-
});
|
|
3334
|
-
remote = new _src_core.CDPClient({
|
|
3335
|
-
transport,
|
|
3336
|
-
timeout: this.#timeout()
|
|
3337
|
-
});
|
|
3338
|
-
try {
|
|
3339
|
-
await remote.connect();
|
|
3340
|
-
temporary = true;
|
|
3341
|
-
} catch {
|
|
3342
|
-
await this.#closeClient(remote);
|
|
3343
|
-
return;
|
|
3344
|
-
}
|
|
3345
|
-
}
|
|
3384
|
+
const remote = client ?? await this.#openRemote();
|
|
3346
3385
|
if (remote !== void 0) try {
|
|
3347
3386
|
await remote.send("Browser.close");
|
|
3348
3387
|
} catch {}
|
|
3349
|
-
if (
|
|
3388
|
+
if (client === void 0 && remote !== void 0) await this.#closeClient(remote);
|
|
3389
|
+
}
|
|
3390
|
+
async #openRemote() {
|
|
3391
|
+
if (this.#owned !== true || this.#endpoint === void 0) return void 0;
|
|
3392
|
+
const transport = createWebSocketCDPTransport({
|
|
3393
|
+
url: this.#endpoint,
|
|
3394
|
+
timeout: this.#timeout()
|
|
3395
|
+
});
|
|
3396
|
+
const remote = new _src_core.CDPClient({
|
|
3397
|
+
transport,
|
|
3398
|
+
timeout: this.#timeout()
|
|
3399
|
+
});
|
|
3400
|
+
try {
|
|
3401
|
+
await remote.connect();
|
|
3402
|
+
return remote;
|
|
3403
|
+
} catch {
|
|
3404
|
+
await this.#closeClient(remote);
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3350
3407
|
}
|
|
3351
3408
|
async #closeContexts() {
|
|
3352
3409
|
for (const context of [...this.#contexts]) try {
|
|
@@ -3381,14 +3438,12 @@ var Browser = class {
|
|
|
3381
3438
|
if (profile !== void 0) await removeBrowserProfile(profile);
|
|
3382
3439
|
}
|
|
3383
3440
|
async #cleanupExitedProcess(process, contexts, client) {
|
|
3384
|
-
let terminated = false;
|
|
3385
3441
|
try {
|
|
3386
3442
|
await this.#terminate(process);
|
|
3387
|
-
terminated = true;
|
|
3388
3443
|
} finally {
|
|
3389
3444
|
await this.#cleanupLocal(contexts, client);
|
|
3390
3445
|
}
|
|
3391
|
-
|
|
3446
|
+
await this.#releaseProfile();
|
|
3392
3447
|
}
|
|
3393
3448
|
async #cleanupLocal(contexts, client) {
|
|
3394
3449
|
await this.#destroyContextList(contexts);
|
|
@@ -3438,31 +3493,9 @@ var Browser = class {
|
|
|
3438
3493
|
});
|
|
3439
3494
|
}
|
|
3440
3495
|
}
|
|
3441
|
-
/**
|
|
3442
|
-
* Waits within a bound for the rest of a terminated process group, or the process a
|
|
3443
|
-
* launcher handed the endpoint to, to disappear.
|
|
3444
|
-
*
|
|
3445
|
-
* @remarks
|
|
3446
|
-
* This is the one interval loop in the server. Node raises an exit event for the direct
|
|
3447
|
-
* child only, so no event source reports the remainder of a POSIX process group or a
|
|
3448
|
-
* handed-off process, and the drain probes on `BROWSER_DRAIN_INTERVAL_MS` until the
|
|
3449
|
-
* remainder is gone or `timeout` elapses.
|
|
3450
|
-
*/
|
|
3451
|
-
async #waitForRemainderWithin(process, timeout) {
|
|
3452
|
-
const deadline = performance.now() + timeout;
|
|
3453
|
-
let confirmed = false;
|
|
3454
|
-
while (true) {
|
|
3455
|
-
const alive = this.#inspectRemainder(process);
|
|
3456
|
-
if (alive === false) return true;
|
|
3457
|
-
if (alive === true) confirmed = true;
|
|
3458
|
-
const remaining = deadline - performance.now();
|
|
3459
|
-
if (remaining <= 0) return confirmed ? false : void 0;
|
|
3460
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(100, remaining)));
|
|
3461
|
-
}
|
|
3462
|
-
}
|
|
3463
3496
|
async #waitForTerminationWithin(process, timeout, final = false) {
|
|
3464
3497
|
const exited = this.#waitForProcessWithin(process, timeout);
|
|
3465
|
-
const drained = this.#
|
|
3498
|
+
const drained = new BrowserProcessDrain(this.#inspectRemainder.bind(this, process), timeout).result;
|
|
3466
3499
|
const results = await Promise.all([exited, drained]);
|
|
3467
3500
|
return results[0] && (results[1] === true || final && results[1] === void 0);
|
|
3468
3501
|
}
|
|
@@ -3502,7 +3535,6 @@ var Browser = class {
|
|
|
3502
3535
|
this.#emitter.destroy();
|
|
3503
3536
|
}
|
|
3504
3537
|
};
|
|
3505
|
-
//#endregion
|
|
3506
3538
|
exports.BROWSER_CDP_LIST_PATH = BROWSER_CDP_LIST_PATH;
|
|
3507
3539
|
exports.BROWSER_CDP_PROTOCOL = BROWSER_CDP_PROTOCOL;
|
|
3508
3540
|
exports.BROWSER_CDP_VERSION_PATH = BROWSER_CDP_VERSION_PATH;
|
|
@@ -3554,7 +3586,9 @@ exports.BROWSER_TRANSPORT_LOSS_DEFER_MS = BROWSER_TRANSPORT_LOSS_DEFER_MS;
|
|
|
3554
3586
|
exports.BROWSER_WINDOWS_ROOT_FALLBACKS = BROWSER_WINDOWS_ROOT_FALLBACKS;
|
|
3555
3587
|
exports.BROWSER_WINDOWS_SUFFIXES = BROWSER_WINDOWS_SUFFIXES;
|
|
3556
3588
|
exports.Browser = Browser;
|
|
3589
|
+
exports.BrowserEndpoint = BrowserEndpoint;
|
|
3557
3590
|
exports.BrowserMCPServer = BrowserMCPServer;
|
|
3591
|
+
exports.BrowserProcessDrain = BrowserProcessDrain;
|
|
3558
3592
|
exports.FileBrowserJourneyStore = FileBrowserJourneyStore;
|
|
3559
3593
|
exports.FileBrowserRunStore = FileBrowserRunStore;
|
|
3560
3594
|
exports.FileBrowserWriter = FileBrowserWriter;
|
|
@@ -3584,7 +3618,6 @@ exports.parseBrowserLockEntry = parseBrowserLockEntry;
|
|
|
3584
3618
|
exports.parseBrowserProfileRecord = parseBrowserProfileRecord;
|
|
3585
3619
|
exports.parseBrowserViewport = parseBrowserViewport;
|
|
3586
3620
|
exports.probePathNames = probePathNames;
|
|
3587
|
-
exports.probeProcess = probeProcess;
|
|
3588
3621
|
exports.readBrowserEndpoint = readBrowserEndpoint;
|
|
3589
3622
|
exports.readFirstLine = readFirstLine;
|
|
3590
3623
|
exports.removeBrowserProfile = removeBrowserProfile;
|