@orkestrel/browser 0.0.24 → 0.0.26
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 +10 -2
- package/dist/bin/main.js.map +1 -1
- package/dist/src/core/index.cjs +42 -13
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +13 -4
- package/dist/src/core/index.d.ts +13 -4
- package/dist/src/core/index.js +42 -13
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +392 -169
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +60 -14
- package/dist/src/server/index.d.ts +60 -14
- package/dist/src/server/index.js +392 -171
- package/dist/src/server/index.js.map +1 -1
- package/package.json +6 -6
package/dist/src/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_JOURNEY_FORMAT_VERSION, BROWSER_JOURNEY_NAME_PATTERN, BROWSER_JOURNEY_TOOL_NAMES, BROWSER_RUN_ID_PATTERN, BROWSER_TOOL_COPY, BROWSER_TOOL_NAMES, BrowserConnectionError, BrowserContext, BrowserError, BrowserTransition, CDPClient, createBrowserToolset, createCDPClient, generateBrowserRunId, isBrowserConnectionError, isBrowserError, isCDPTimeoutError, normalizeBrowserJourneyReason, validateBrowserContextOptions, validateBrowserJourney, validateBrowserRun, validateBrowserStorePage } from "../core/index.js";
|
|
1
|
+
import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_JOURNEY_FORMAT_VERSION, BROWSER_JOURNEY_NAME_PATTERN, BROWSER_JOURNEY_TOOL_NAMES, BROWSER_RUN_ID_PATTERN, BROWSER_TOOL_COPY, BROWSER_TOOL_NAMES, BrowserConnectionError, BrowserContext, BrowserError, BrowserTransition, CDPClient, createBrowserToolset, createCDPClient, generateBrowserRunId, isBrowserConnectionError, isBrowserError, isCDPConnectionError, isCDPTimeoutError, normalizeBrowserJourneyReason, validateBrowserContextOptions, validateBrowserJourney, validateBrowserRun, validateBrowserStorePage } from "../core/index.js";
|
|
2
2
|
import { attempt, isArray, isError, isInstance, isInteger, isNumber, isRecord, isString, parseInteger, parseJSON } from "@orkestrel/contract";
|
|
3
3
|
import { existsSync, globSync, realpathSync } from "node:fs";
|
|
4
4
|
import { lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, rmdir, unlink, writeFile } from "node:fs/promises";
|
|
@@ -37,8 +37,21 @@ var BROWSER_CDP_VERSION_PATH = "/json/version";
|
|
|
37
37
|
* other target category Chromium reports — `'/json/list'`.
|
|
38
38
|
*/
|
|
39
39
|
var BROWSER_CDP_LIST_PATH = "/json/list";
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Lists the flags always passed to a launched browser process, alongside the caller's own.
|
|
42
|
+
*
|
|
43
|
+
* @remarks
|
|
44
|
+
* Disables account sync so extension installs cannot reset URL loader factories during page imports.
|
|
45
|
+
* Disables Edge's implicit sign-in so fresh automation profiles carry no Microsoft account.
|
|
46
|
+
* Chromium ignores the unknown Edge feature name on Chrome and Chromium.
|
|
47
|
+
* The launcher merges disabled features with the caller's into one switch.
|
|
48
|
+
*/
|
|
49
|
+
var BROWSER_LAUNCH_ARGS = Object.freeze([
|
|
50
|
+
"--no-first-run",
|
|
51
|
+
"--no-default-browser-check",
|
|
52
|
+
"--disable-sync",
|
|
53
|
+
"--disable-features=msImplicitSignin"
|
|
54
|
+
]);
|
|
42
55
|
/**
|
|
43
56
|
* Names the flag that enables headless mode on a launched browser process, `'--headless=new'`.
|
|
44
57
|
*/
|
|
@@ -218,6 +231,10 @@ var BROWSER_SERVER_UNAVAILABLE = "BROWSER_SERVER_UNAVAILABLE";
|
|
|
218
231
|
var BROWSER_SERVER_LAUNCH = "BROWSER_SERVER_LAUNCH";
|
|
219
232
|
/** Sets the default number of warm browsers to 1. */
|
|
220
233
|
var BROWSER_SERVER_POOL_SIZE = 1;
|
|
234
|
+
/** Sets the default context capacity per browser, including the shared holder. */
|
|
235
|
+
var BROWSER_SERVER_CONTEXTS = 2;
|
|
236
|
+
/** Limits context capacity to the largest measured per-browser count. */
|
|
237
|
+
var BROWSER_SERVER_CONTEXTS_LIMIT = 4;
|
|
221
238
|
/** Limits the number of warm browsers to 3. */
|
|
222
239
|
var BROWSER_SERVER_POOL_LIMIT = 3;
|
|
223
240
|
/** Permits one failed refill before the next failure spends the bound. */
|
|
@@ -672,6 +689,10 @@ function findStorePaths(base, platform) {
|
|
|
672
689
|
* Launches a browser process with raw-CDP debugging flags.
|
|
673
690
|
*
|
|
674
691
|
* @remarks
|
|
692
|
+
* Merges every caller and library `--disable-features` value into one switch because Chromium
|
|
693
|
+
* reads only the last occurrence. Keeps `msImplicitSignin` disabled alongside caller features;
|
|
694
|
+
* Chrome and Chromium ignore this Edge feature name. Preserves the caller's other arguments.
|
|
695
|
+
*
|
|
675
696
|
* POSIX launches own an isolated process group so lifecycle teardown can
|
|
676
697
|
* signal and await every Chromium subprocess without affecting the caller.
|
|
677
698
|
* Windows launches are not detached and own no group, so teardown there
|
|
@@ -689,8 +710,18 @@ function findStorePaths(base, platform) {
|
|
|
689
710
|
*/
|
|
690
711
|
function launchBrowserProcess(executable, port, headless, profile, extra) {
|
|
691
712
|
const args = [];
|
|
692
|
-
|
|
693
|
-
|
|
713
|
+
const features = /* @__PURE__ */ new Set();
|
|
714
|
+
for (const arg of [
|
|
715
|
+
...extra ?? [],
|
|
716
|
+
`--remote-debugging-port=${port ?? 0}`,
|
|
717
|
+
...BROWSER_LAUNCH_ARGS
|
|
718
|
+
]) {
|
|
719
|
+
if (arg === "--disable-features") continue;
|
|
720
|
+
if (arg.startsWith("--disable-features=")) {
|
|
721
|
+
for (const feature of arg.slice(19).split(",")) if (feature.trim() !== "") features.add(feature.trim());
|
|
722
|
+
} else args.push(arg);
|
|
723
|
+
}
|
|
724
|
+
args.push(`--disable-features=${[...features].join(",")}`);
|
|
694
725
|
if (headless) args.push(BROWSER_HEADLESS_ARG);
|
|
695
726
|
if (profile !== void 0) args.push(`--user-data-dir=${profile}`);
|
|
696
727
|
return spawn(executable, args, {
|
|
@@ -1041,7 +1072,7 @@ var FileBrowserWriter = class {
|
|
|
1041
1072
|
};
|
|
1042
1073
|
//#endregion
|
|
1043
1074
|
//#region package.json
|
|
1044
|
-
var version = "0.0.
|
|
1075
|
+
var version = "0.0.26";
|
|
1045
1076
|
//#endregion
|
|
1046
1077
|
//#region src/server/BrowserMCPServer.ts
|
|
1047
1078
|
/**
|
|
@@ -1057,17 +1088,18 @@ var version = "0.0.24";
|
|
|
1057
1088
|
* tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
|
|
1058
1089
|
* removed when the toolset withdraws the tool, so the server's tool list changes and a subscribed
|
|
1059
1090
|
* client is notified. The server also advertises `acquire`, `execute`, `tools`, and `destroy` with
|
|
1060
|
-
* `BROWSER_SERVER_COPY`. Startup warms the pool and
|
|
1091
|
+
* `BROWSER_SERVER_COPY`. Startup warms the pool and assigns a prepared context to the shared holder. The legacy
|
|
1061
1092
|
* handshake and browser calls await that setup. Named holders each own a lease, a pending grant,
|
|
1062
1093
|
* and loss notices; concurrent calls on a holder share its replacement grant. Ending a holder
|
|
1063
|
-
*
|
|
1094
|
+
* closes its context and releases its lease, and admission counts that holder until disposal settles.
|
|
1064
1095
|
* A launch creates `ROOT/.profiles/PID-UUID/` exclusively, connects a browser that never attaches to an
|
|
1065
1096
|
* existing endpoint, opens one page in an isolated context, and constructs the page toolset with
|
|
1066
1097
|
* that context and the file journey and run stores under the root, on a manager the toolset owns.
|
|
1067
1098
|
* A dispatcher forwards its arguments and the call's context, signal included, to the toolset's
|
|
1068
1099
|
* manager and returns that manager's value, or rejects with its failure message; a text value
|
|
1069
1100
|
* reaches the client as one text block rather than as its JSON text.
|
|
1070
|
-
* A disconnect
|
|
1101
|
+
* A disconnect retires its browser and every hosted generation. A current-page crash retires only
|
|
1102
|
+
* that holder's context. A failed call receives a liveness ping;
|
|
1071
1103
|
* loss makes its outcome unresolved, while a known success remains successful. Pending losses
|
|
1072
1104
|
* prefix the next outcome on a successor, or a refusal, and calls are never repeated.
|
|
1073
1105
|
* Concurrent checks share one ping per slot. Cancelling a call ends only its wait; the ping keeps
|
|
@@ -1075,7 +1107,8 @@ var version = "0.0.24";
|
|
|
1075
1107
|
*
|
|
1076
1108
|
* `start()` serves stdio and destroys the server at the end of its input, on `SIGINT`, and on
|
|
1077
1109
|
* `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys every
|
|
1078
|
-
* slot through the pool —
|
|
1110
|
+
* slot through the pool — each toolset, which aborts its active replay, then its context and downloads,
|
|
1111
|
+
* then its browser and its
|
|
1079
1112
|
* profile — and rechecks the profile folders this server made or kept.
|
|
1080
1113
|
*
|
|
1081
1114
|
* Chromium on Linux refuses to start as the root user unless its sandbox is disabled, so a server
|
|
@@ -1110,8 +1143,16 @@ var BrowserMCPServer = class {
|
|
|
1110
1143
|
#folders = /* @__PURE__ */ new Set();
|
|
1111
1144
|
#faults = /* @__PURE__ */ new Set();
|
|
1112
1145
|
#losses = /* @__PURE__ */ new WeakMap();
|
|
1146
|
+
#departures = /* @__PURE__ */ new WeakMap();
|
|
1113
1147
|
#pings = /* @__PURE__ */ new WeakMap();
|
|
1114
1148
|
#watches = /* @__PURE__ */ new Map();
|
|
1149
|
+
#observed = /* @__PURE__ */ new Map();
|
|
1150
|
+
#prepared = /* @__PURE__ */ new Map();
|
|
1151
|
+
#owned = /* @__PURE__ */ new Map();
|
|
1152
|
+
#building = /* @__PURE__ */ new Map();
|
|
1153
|
+
#cleaning = /* @__PURE__ */ new WeakMap();
|
|
1154
|
+
#endings = /* @__PURE__ */ new WeakMap();
|
|
1155
|
+
#retentions = /* @__PURE__ */ new Map();
|
|
1115
1156
|
#shared = {
|
|
1116
1157
|
id: "shared",
|
|
1117
1158
|
purpose: "Shared browser",
|
|
@@ -1125,7 +1166,7 @@ var BrowserMCPServer = class {
|
|
|
1125
1166
|
#drains = /* @__PURE__ */ new Map();
|
|
1126
1167
|
#readers = /* @__PURE__ */ new Map();
|
|
1127
1168
|
#writers = /* @__PURE__ */ new Set();
|
|
1128
|
-
#
|
|
1169
|
+
#admission;
|
|
1129
1170
|
#reference;
|
|
1130
1171
|
#references = 0;
|
|
1131
1172
|
#failure;
|
|
@@ -1140,17 +1181,20 @@ var BrowserMCPServer = class {
|
|
|
1140
1181
|
*/
|
|
1141
1182
|
constructor(options) {
|
|
1142
1183
|
const size = options?.pool?.size ?? 1;
|
|
1184
|
+
const contexts = options?.pool?.contexts ?? 2;
|
|
1143
1185
|
if (!isInteger(size) || size < 1 || size > 3) throw new BrowserError(`pool.size must be an integer from 1 through 3`, BROWSER_SERVER_OPTIONS);
|
|
1144
|
-
|
|
1186
|
+
if (!isInteger(contexts) || contexts < 1 || contexts > 4) throw new BrowserError(`pool.contexts must be an integer from 1 through 4`, BROWSER_SERVER_OPTIONS);
|
|
1187
|
+
this.#admission = size * contexts;
|
|
1145
1188
|
this.#log = options?.log ?? process.stderr;
|
|
1146
1189
|
this.#reference = this.#issue.bind(this);
|
|
1147
1190
|
this.#pool = createPool({
|
|
1148
1191
|
create: this.#warm.bind(this),
|
|
1149
|
-
destroy:
|
|
1192
|
+
destroy: this.#destroyRecord.bind(this),
|
|
1150
1193
|
validate: this.#validate.bind(this),
|
|
1151
1194
|
watch: this.#watch.bind(this),
|
|
1152
1195
|
error: this.#fault.bind(this),
|
|
1153
1196
|
min: size,
|
|
1197
|
+
capacity: contexts,
|
|
1154
1198
|
restarts: 1
|
|
1155
1199
|
});
|
|
1156
1200
|
this.#root = resolve(options?.root ?? "tmp/browsers");
|
|
@@ -1233,6 +1277,7 @@ var BrowserMCPServer = class {
|
|
|
1233
1277
|
this.#transport.stop();
|
|
1234
1278
|
for (const tool of this.#tools.tools()) this.#tools.remove(tool.name);
|
|
1235
1279
|
this.#unmirror();
|
|
1280
|
+
for (const generation of this.#observed.keys()) this.#untrack(generation);
|
|
1236
1281
|
this.#leases.clear();
|
|
1237
1282
|
for (const holder of this.#holders.values()) holder.abort.abort();
|
|
1238
1283
|
this.#abort.abort();
|
|
@@ -1241,6 +1286,7 @@ var BrowserMCPServer = class {
|
|
|
1241
1286
|
});
|
|
1242
1287
|
await this.#starting?.catch(() => void 0);
|
|
1243
1288
|
await barrier;
|
|
1289
|
+
await Promise.allSettled(this.#grants.values());
|
|
1244
1290
|
await Promise.allSettled(this.#disposals.values());
|
|
1245
1291
|
this.#holders.clear();
|
|
1246
1292
|
await this.#sweeping;
|
|
@@ -1292,7 +1338,7 @@ var BrowserMCPServer = class {
|
|
|
1292
1338
|
if (this.#closing !== void 0) throw this.#ended();
|
|
1293
1339
|
const purpose = args["purpose"];
|
|
1294
1340
|
if (!isString(purpose) || purpose.trim().length === 0) throw new BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
|
|
1295
|
-
if (this.#holders.size >= this.#
|
|
1341
|
+
if (this.#holders.size >= this.#admission) throw new BrowserError(`${BROWSER_SERVER_BUSY}: ${[...this.#holders.values()].map((holder) => `${holder.id} (${holder.purpose})`).join(", ")}. Call destroy for a holder no longer needed or call the named tools to share the shared browser.`, BROWSER_SERVER_BUSY);
|
|
1296
1342
|
const holder = {
|
|
1297
1343
|
id: randomUUID(),
|
|
1298
1344
|
purpose,
|
|
@@ -1301,13 +1347,13 @@ var BrowserMCPServer = class {
|
|
|
1301
1347
|
this.#holders.set(holder.id, holder);
|
|
1302
1348
|
const listener = addAbortListener(context.signal, () => holder.abort.abort(context.signal.reason));
|
|
1303
1349
|
try {
|
|
1304
|
-
const
|
|
1350
|
+
const lease = await this.#serve(holder, context.signal);
|
|
1305
1351
|
context.signal.throwIfAborted();
|
|
1306
1352
|
if (holder.abort.signal.aborted || this.#holders.get(holder.id) !== holder) throw this.#ended();
|
|
1307
|
-
if (this.#leases.get(holder)
|
|
1353
|
+
if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
|
|
1308
1354
|
const catalog = {
|
|
1309
1355
|
holder: holder.id,
|
|
1310
|
-
tools:
|
|
1356
|
+
tools: lease.generation.toolset.tools.definitions()
|
|
1311
1357
|
};
|
|
1312
1358
|
return JSON.stringify(catalog);
|
|
1313
1359
|
} catch (error) {
|
|
@@ -1333,12 +1379,12 @@ var BrowserMCPServer = class {
|
|
|
1333
1379
|
const holder = this.#holder(args["holder"]);
|
|
1334
1380
|
const signal = AbortSignal.any([context.signal, holder.abort.signal]);
|
|
1335
1381
|
try {
|
|
1336
|
-
const
|
|
1382
|
+
const lease = await this.#serve(holder, signal);
|
|
1337
1383
|
signal.throwIfAborted();
|
|
1338
|
-
if (this.#leases.get(holder)
|
|
1384
|
+
if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
|
|
1339
1385
|
const catalog = {
|
|
1340
1386
|
holder: holder.id,
|
|
1341
|
-
tools:
|
|
1387
|
+
tools: lease.generation.toolset.tools.definitions()
|
|
1342
1388
|
};
|
|
1343
1389
|
return this.#annotate(holder, JSON.stringify(catalog));
|
|
1344
1390
|
} catch (error) {
|
|
@@ -1357,17 +1403,19 @@ var BrowserMCPServer = class {
|
|
|
1357
1403
|
const pending = this.#disposals.get(holder.id);
|
|
1358
1404
|
if (pending !== void 0) return pending;
|
|
1359
1405
|
holder.abort.abort();
|
|
1360
|
-
const
|
|
1406
|
+
const lease = this.#leases.get(holder);
|
|
1361
1407
|
this.#leases.delete(holder);
|
|
1408
|
+
if (lease !== void 0) this.#untrack(lease.generation);
|
|
1362
1409
|
const disposal = Promise.resolve().then(async () => {
|
|
1363
1410
|
try {
|
|
1364
1411
|
await this.#grants.get(holder)?.catch(() => void 0);
|
|
1365
|
-
await
|
|
1412
|
+
if (lease !== void 0) await this.#release(holder, lease);
|
|
1366
1413
|
await Promise.all(this.#drains.get(holder) ?? []);
|
|
1414
|
+
await Promise.all([...this.#notices.get(holder) ?? []].filter((lost) => this.#losses.has(lost.token.value)).map((lost) => this.#endings.get(lost.token.value)?.promise));
|
|
1367
1415
|
} catch (error) {
|
|
1368
1416
|
this.#faults.add(error);
|
|
1369
1417
|
} finally {
|
|
1370
|
-
if (this.#holders.get(holder.id) === holder) this.#holders.delete(holder.id);
|
|
1418
|
+
if (this.#holders.get(holder.id) === holder && !this.#retentions.has(holder)) this.#holders.delete(holder.id);
|
|
1371
1419
|
this.#notices.delete(holder);
|
|
1372
1420
|
this.#drains.delete(holder);
|
|
1373
1421
|
}
|
|
@@ -1400,9 +1448,9 @@ var BrowserMCPServer = class {
|
|
|
1400
1448
|
}
|
|
1401
1449
|
async #perform(holder, name, args, context) {
|
|
1402
1450
|
const signal = holder === this.#shared ? context.signal : AbortSignal.any([context.signal, holder.abort.signal]);
|
|
1403
|
-
let
|
|
1451
|
+
let lease;
|
|
1404
1452
|
try {
|
|
1405
|
-
|
|
1453
|
+
lease = await this.#serve(holder, signal);
|
|
1406
1454
|
} catch (error) {
|
|
1407
1455
|
context.signal.throwIfAborted();
|
|
1408
1456
|
if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
|
|
@@ -1411,7 +1459,7 @@ var BrowserMCPServer = class {
|
|
|
1411
1459
|
}
|
|
1412
1460
|
let result;
|
|
1413
1461
|
try {
|
|
1414
|
-
const execution =
|
|
1462
|
+
const execution = lease.generation.toolset.tools.execute({
|
|
1415
1463
|
id: name,
|
|
1416
1464
|
name,
|
|
1417
1465
|
arguments: args
|
|
@@ -1433,23 +1481,23 @@ var BrowserMCPServer = class {
|
|
|
1433
1481
|
context.signal.throwIfAborted();
|
|
1434
1482
|
if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call", BROWSER_SERVER_UNRESOLVED);
|
|
1435
1483
|
if (!result.success) {
|
|
1436
|
-
if (this.#leases.get(holder)
|
|
1437
|
-
await this.#race(this.#ping(
|
|
1484
|
+
if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
|
|
1485
|
+
await this.#race(this.#ping(lease.token.value), context.signal);
|
|
1438
1486
|
} catch {}
|
|
1439
1487
|
context.signal.throwIfAborted();
|
|
1440
|
-
if (this.#leases.get(holder)
|
|
1441
|
-
const loss = this.#
|
|
1442
|
-
throw new BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url),
|
|
1488
|
+
if (this.#leases.get(holder) !== lease) {
|
|
1489
|
+
const loss = this.#departures.get(lease);
|
|
1490
|
+
throw new BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease), BROWSER_SERVER_UNRESOLVED);
|
|
1443
1491
|
}
|
|
1444
|
-
throw new BrowserError(this.#annotate(holder, result.error,
|
|
1492
|
+
throw new BrowserError(this.#annotate(holder, result.error, lease));
|
|
1445
1493
|
}
|
|
1446
|
-
return typeof result.value === "string" ? this.#annotate(holder, result.value,
|
|
1494
|
+
return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
|
|
1447
1495
|
}
|
|
1448
|
-
#annotate(holder, value,
|
|
1496
|
+
#annotate(holder, value, lease) {
|
|
1449
1497
|
const notes = [];
|
|
1450
1498
|
for (const lost of this.#notices.get(holder) ?? []) {
|
|
1451
|
-
if (lost ===
|
|
1452
|
-
const loss = this.#
|
|
1499
|
+
if (lost === lease) continue;
|
|
1500
|
+
const loss = this.#departures.get(lost);
|
|
1453
1501
|
notes.push(describeBrowserServerLoss(BROWSER_SERVER_CRASH, loss?.cause, loss?.url));
|
|
1454
1502
|
this.#notices.get(holder)?.delete(lost);
|
|
1455
1503
|
}
|
|
@@ -1480,41 +1528,70 @@ var BrowserMCPServer = class {
|
|
|
1480
1528
|
if (held !== void 0) return held;
|
|
1481
1529
|
let granting = this.#grants.get(holder);
|
|
1482
1530
|
if (granting === void 0) {
|
|
1483
|
-
granting =
|
|
1531
|
+
granting = Promise.resolve().then(async () => {
|
|
1532
|
+
await Promise.allSettled(this.#drains.get(holder) ?? []);
|
|
1533
|
+
if (this.#retentions.has(holder)) throw this.#unavailable(this.#retentions.get(holder));
|
|
1534
|
+
const token = await this.#pool.acquire(holder.abort.signal);
|
|
1535
|
+
return this.#hold(holder, token);
|
|
1536
|
+
});
|
|
1484
1537
|
this.#grants.set(holder, granting);
|
|
1485
1538
|
granting.finally(() => {
|
|
1486
1539
|
if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
|
|
1487
1540
|
}).catch(() => void 0);
|
|
1488
1541
|
}
|
|
1489
|
-
const
|
|
1490
|
-
if (
|
|
1491
|
-
return
|
|
1542
|
+
const lease = await this.#race(granting, signal);
|
|
1543
|
+
if (lease === void 0) throw this.#ended();
|
|
1544
|
+
return lease;
|
|
1492
1545
|
}
|
|
1493
1546
|
async #hold(holder, token) {
|
|
1547
|
+
const slot = token.value;
|
|
1494
1548
|
if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted) {
|
|
1495
|
-
|
|
1549
|
+
token.release();
|
|
1496
1550
|
throw this.#ended();
|
|
1497
1551
|
}
|
|
1498
|
-
if (this.#
|
|
1499
|
-
|
|
1500
|
-
|
|
1552
|
+
if (!this.#prepared.has(slot)) await this.#ping(slot);
|
|
1553
|
+
if (this.#closing !== void 0 || holder.abort.signal.aborted) {
|
|
1554
|
+
token.release();
|
|
1555
|
+
throw this.#ended();
|
|
1501
1556
|
}
|
|
1502
|
-
this.#
|
|
1557
|
+
if (this.#losses.has(slot)) {
|
|
1558
|
+
token.release();
|
|
1559
|
+
throw this.#unavailable(this.#losses.get(slot)?.cause);
|
|
1560
|
+
}
|
|
1561
|
+
let generation = this.#prepared.get(slot);
|
|
1562
|
+
this.#prepared.delete(slot);
|
|
1563
|
+
try {
|
|
1564
|
+
generation ??= await this.#build(slot, token);
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
if (this.#losses.has(slot)) await this.#endings.get(slot)?.promise;
|
|
1567
|
+
else token.release();
|
|
1568
|
+
throw this.#unavailable(error);
|
|
1569
|
+
}
|
|
1570
|
+
const lease = {
|
|
1571
|
+
token,
|
|
1572
|
+
generation
|
|
1573
|
+
};
|
|
1574
|
+
if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted || this.#losses.has(slot)) {
|
|
1575
|
+
await this.#release(holder, lease);
|
|
1576
|
+
throw this.#ended();
|
|
1577
|
+
}
|
|
1578
|
+
this.#leases.set(holder, lease);
|
|
1579
|
+
this.#observe(slot, generation);
|
|
1503
1580
|
if (holder === this.#shared) {
|
|
1504
|
-
const tools =
|
|
1581
|
+
const tools = generation.toolset.tools;
|
|
1505
1582
|
const mirrored = {
|
|
1506
|
-
|
|
1507
|
-
added: this.#mirror.bind(this,
|
|
1508
|
-
removed: this.#withdraw.bind(this,
|
|
1509
|
-
cleared: this.#clear.bind(this,
|
|
1583
|
+
lease,
|
|
1584
|
+
added: this.#mirror.bind(this, lease),
|
|
1585
|
+
removed: this.#withdraw.bind(this, lease),
|
|
1586
|
+
cleared: this.#clear.bind(this, lease)
|
|
1510
1587
|
};
|
|
1511
1588
|
this.#mirrored = mirrored;
|
|
1512
|
-
for (const tool of tools.tools()) this.#mirror(
|
|
1589
|
+
for (const tool of tools.tools()) this.#mirror(lease, tool);
|
|
1513
1590
|
tools.emitter.on("add", mirrored.added);
|
|
1514
1591
|
tools.emitter.on("remove", mirrored.removed);
|
|
1515
1592
|
tools.emitter.on("clear", mirrored.cleared);
|
|
1516
1593
|
}
|
|
1517
|
-
return
|
|
1594
|
+
return lease;
|
|
1518
1595
|
}
|
|
1519
1596
|
async #serve(holder, signal) {
|
|
1520
1597
|
if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
|
|
@@ -1522,16 +1599,16 @@ var BrowserMCPServer = class {
|
|
|
1522
1599
|
if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
|
|
1523
1600
|
const held = this.#leases.get(holder);
|
|
1524
1601
|
if (held !== void 0) try {
|
|
1525
|
-
await this.#race(this.#ping(held.value), signal);
|
|
1526
|
-
if (this.#leases.get(holder) === held) return held
|
|
1602
|
+
await this.#race(this.#ping(held.token.value), signal);
|
|
1603
|
+
if (this.#leases.get(holder) === held) return held;
|
|
1527
1604
|
} catch {
|
|
1528
1605
|
signal.throwIfAborted();
|
|
1529
1606
|
if (holder.abort.signal.aborted) throw this.#ended();
|
|
1530
1607
|
}
|
|
1531
1608
|
try {
|
|
1532
|
-
const
|
|
1533
|
-
if (this.#leases.get(holder) !==
|
|
1534
|
-
return
|
|
1609
|
+
const lease = await this.#grant(holder, signal);
|
|
1610
|
+
if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
|
|
1611
|
+
return lease;
|
|
1535
1612
|
} catch (error) {
|
|
1536
1613
|
signal.throwIfAborted();
|
|
1537
1614
|
if (holder.abort.signal.aborted || isPoolError(error) && error.code === "destroyed") throw this.#ended();
|
|
@@ -1562,78 +1639,211 @@ var BrowserMCPServer = class {
|
|
|
1562
1639
|
}
|
|
1563
1640
|
#lose(slot, cause) {
|
|
1564
1641
|
if (this.#closing !== void 0 || this.#losses.has(slot)) return;
|
|
1565
|
-
this.#losses.set(slot, {
|
|
1566
|
-
cause,
|
|
1567
|
-
url: slot.toolset.view.url
|
|
1568
|
-
});
|
|
1642
|
+
this.#losses.set(slot, { cause });
|
|
1569
1643
|
this.#failure = cause;
|
|
1570
|
-
for (const [holder,
|
|
1571
|
-
if (token.value !== slot
|
|
1572
|
-
|
|
1573
|
-
this.#leases.delete(holder);
|
|
1574
|
-
const notices = this.#notices.get(holder) ?? /* @__PURE__ */ new Set();
|
|
1575
|
-
notices.add(slot);
|
|
1576
|
-
this.#notices.set(holder, notices);
|
|
1577
|
-
const drain = token.destroy();
|
|
1578
|
-
const drains = this.#drains.get(holder) ?? /* @__PURE__ */ new Set();
|
|
1579
|
-
drains.add(drain);
|
|
1580
|
-
this.#drains.set(holder, drains);
|
|
1581
|
-
drain.finally(() => drains.delete(drain)).catch(() => void 0);
|
|
1644
|
+
for (const [holder, lease] of this.#leases) {
|
|
1645
|
+
if (lease.token.value !== slot) continue;
|
|
1646
|
+
this.#detach(holder, lease, cause);
|
|
1582
1647
|
}
|
|
1648
|
+
const watch = this.#watches.get(slot);
|
|
1649
|
+
this.#unwatch(slot);
|
|
1650
|
+
watch?.resolve(cause);
|
|
1651
|
+
}
|
|
1652
|
+
#detach(holder, lease, cause) {
|
|
1653
|
+
if (this.#leases.get(holder) !== lease || this.#holders.get(holder.id) !== holder) return;
|
|
1654
|
+
this.#departures.set(lease, {
|
|
1655
|
+
cause,
|
|
1656
|
+
url: lease.generation.toolset.view.url
|
|
1657
|
+
});
|
|
1658
|
+
if (holder === this.#shared) this.#unmirror();
|
|
1659
|
+
this.#leases.delete(holder);
|
|
1660
|
+
this.#untrack(lease.generation);
|
|
1661
|
+
const notices = this.#notices.get(holder) ?? /* @__PURE__ */ new Set();
|
|
1662
|
+
notices.add(lease);
|
|
1663
|
+
this.#notices.set(holder, notices);
|
|
1583
1664
|
}
|
|
1584
1665
|
#watch(slot, signal) {
|
|
1585
1666
|
const loss = Promise.withResolvers();
|
|
1586
1667
|
const disconnect = this.#disconnect.bind(this, slot);
|
|
1587
|
-
const page = this.#track.bind(this, slot);
|
|
1588
1668
|
const subscription = addAbortListener(signal, this.#unwatch.bind(this, slot));
|
|
1589
1669
|
this.#watches.set(slot, {
|
|
1590
1670
|
resolve: loss.resolve,
|
|
1591
1671
|
disconnect,
|
|
1592
|
-
|
|
1593
|
-
subscription,
|
|
1594
|
-
crashes: /* @__PURE__ */ new Map()
|
|
1672
|
+
subscription
|
|
1595
1673
|
});
|
|
1596
1674
|
slot.browser.emitter.on("disconnect", disconnect);
|
|
1597
|
-
slot.context.emitter.on("page", page);
|
|
1598
|
-
for (const current of slot.context.pages()) this.#track(slot, current);
|
|
1599
1675
|
if (signal.aborted) this.#unwatch(slot);
|
|
1600
1676
|
else if (slot.browser.status !== "connected") this.#disconnect(slot);
|
|
1601
1677
|
return loss.promise;
|
|
1602
1678
|
}
|
|
1603
|
-
#
|
|
1604
|
-
|
|
1679
|
+
#observe(slot, generation) {
|
|
1680
|
+
if (this.#observed.has(generation)) return;
|
|
1681
|
+
const page = this.#track.bind(this, slot, generation);
|
|
1682
|
+
this.#observed.set(generation, {
|
|
1683
|
+
page,
|
|
1684
|
+
crashes: /* @__PURE__ */ new Map()
|
|
1685
|
+
});
|
|
1686
|
+
generation.context.emitter.on("page", page);
|
|
1687
|
+
for (const current of generation.context.pages()) this.#track(slot, generation, current);
|
|
1688
|
+
}
|
|
1689
|
+
#track(slot, generation, page) {
|
|
1690
|
+
const watch = this.#observed.get(generation);
|
|
1605
1691
|
if (watch === void 0 || watch.crashes.has(page)) return;
|
|
1606
|
-
const crash = this.#
|
|
1692
|
+
const crash = this.#crash.bind(this, slot, generation, page);
|
|
1607
1693
|
page.emitter.on("crash", crash);
|
|
1608
|
-
this.#
|
|
1694
|
+
this.#observed.set(generation, {
|
|
1609
1695
|
...watch,
|
|
1610
1696
|
crashes: new Map([...watch.crashes, [page, crash]])
|
|
1611
1697
|
});
|
|
1612
1698
|
}
|
|
1613
|
-
#
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1699
|
+
#crash(slot, generation, page) {
|
|
1700
|
+
if (this.#prepared.get(slot) === generation) {
|
|
1701
|
+
this.#prepared.delete(slot);
|
|
1702
|
+
this.#untrack(generation);
|
|
1703
|
+
this.#clean(slot, generation).catch(this.#fault.bind(this));
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
const held = [...this.#leases].find(([, lease]) => lease.generation === generation);
|
|
1707
|
+
if (held === void 0 || page !== generation.toolset.view) return;
|
|
1708
|
+
const [holder, lease] = held;
|
|
1709
|
+
this.#detach(holder, lease, /* @__PURE__ */ new Error("The current page renderer crashed"));
|
|
1710
|
+
const drain = this.#release(holder, lease);
|
|
1711
|
+
const drains = this.#drains.get(holder) ?? /* @__PURE__ */ new Set();
|
|
1712
|
+
drains.add(drain);
|
|
1713
|
+
this.#drains.set(holder, drains);
|
|
1714
|
+
drain.finally(() => drains.delete(drain)).catch(this.#fault.bind(this));
|
|
1715
|
+
}
|
|
1716
|
+
#untrack(generation) {
|
|
1717
|
+
const watch = this.#observed.get(generation);
|
|
1718
|
+
if (watch === void 0) return;
|
|
1719
|
+
this.#observed.delete(generation);
|
|
1720
|
+
generation.context.emitter.off("page", watch.page);
|
|
1721
|
+
for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
|
|
1617
1722
|
}
|
|
1618
1723
|
#disconnect(slot) {
|
|
1619
|
-
this.#
|
|
1620
|
-
}
|
|
1621
|
-
#report(slot, cause) {
|
|
1622
|
-
const watch = this.#watches.get(slot);
|
|
1623
|
-
if (watch === void 0) return;
|
|
1624
|
-
this.#unwatch(slot);
|
|
1625
|
-
this.#lose(slot, cause);
|
|
1626
|
-
watch.resolve(cause);
|
|
1724
|
+
this.#lose(slot, /* @__PURE__ */ new Error(slot.browser.pid === void 0 ? "The browser process exited" : "The browser transport disconnected"));
|
|
1627
1725
|
}
|
|
1628
1726
|
#unwatch(slot) {
|
|
1629
1727
|
const watch = this.#watches.get(slot);
|
|
1630
1728
|
if (watch === void 0) return;
|
|
1631
1729
|
this.#watches.delete(slot);
|
|
1632
1730
|
slot.browser.emitter.off("disconnect", watch.disconnect);
|
|
1633
|
-
slot.context.emitter.off("page", watch.page);
|
|
1634
|
-
for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
|
|
1635
1731
|
watch.subscription[Symbol.dispose]();
|
|
1636
1732
|
}
|
|
1733
|
+
async #release(holder, lease) {
|
|
1734
|
+
const slot = lease.token.value;
|
|
1735
|
+
const removed = await this.#clean(slot, lease.generation);
|
|
1736
|
+
const disposal = lease.generation.context.disposal;
|
|
1737
|
+
if (!removed) this.#retentions.set(holder, /* @__PURE__ */ new Error("The context downloads directory remains owned"));
|
|
1738
|
+
if (disposal?.confirmed !== true && !this.#losses.has(slot) && this.#closing === void 0) {
|
|
1739
|
+
if (![...this.#leases.values()].some((held) => held.token.value === slot && held !== lease) && (this.#building.get(slot)?.size ?? 0) === 0) lease.token.release();
|
|
1740
|
+
this.#lose(slot, disposal?.error ?? /* @__PURE__ */ new Error("The context disposal is unconfirmed"));
|
|
1741
|
+
await this.#endings.get(slot)?.promise;
|
|
1742
|
+
} else if (removed) lease.token.release();
|
|
1743
|
+
}
|
|
1744
|
+
#clean(slot, generation) {
|
|
1745
|
+
let cleaning = this.#cleaning.get(generation);
|
|
1746
|
+
if (cleaning !== void 0) return cleaning;
|
|
1747
|
+
cleaning = Promise.resolve().then(async () => {
|
|
1748
|
+
await generation.toolset.destroy().catch(this.#fault.bind(this));
|
|
1749
|
+
await generation.context.close().catch((error) => {
|
|
1750
|
+
if (!this.#losses.has(slot) && slot.browser.status === "connected") this.#fault(error);
|
|
1751
|
+
});
|
|
1752
|
+
try {
|
|
1753
|
+
await rm(generation.directory, {
|
|
1754
|
+
recursive: true,
|
|
1755
|
+
force: true
|
|
1756
|
+
});
|
|
1757
|
+
this.#owned.get(slot)?.delete(generation);
|
|
1758
|
+
return true;
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
this.#fault(error);
|
|
1761
|
+
return false;
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
this.#cleaning.set(generation, cleaning);
|
|
1765
|
+
return cleaning;
|
|
1766
|
+
}
|
|
1767
|
+
#build(slot, token) {
|
|
1768
|
+
const construction = this.#construct(slot, token);
|
|
1769
|
+
const building = this.#building.get(slot) ?? /* @__PURE__ */ new Set();
|
|
1770
|
+
building.add(construction);
|
|
1771
|
+
this.#building.set(slot, building);
|
|
1772
|
+
construction.finally(() => building.delete(construction)).catch(() => void 0);
|
|
1773
|
+
return construction;
|
|
1774
|
+
}
|
|
1775
|
+
async #construct(slot, token) {
|
|
1776
|
+
const directory = join(slot.profile, "contexts", randomUUID(), "downloads");
|
|
1777
|
+
let context;
|
|
1778
|
+
let toolset;
|
|
1779
|
+
try {
|
|
1780
|
+
await mkdir(directory, { recursive: true });
|
|
1781
|
+
let page;
|
|
1782
|
+
try {
|
|
1783
|
+
context = await slot.browser.isolate({
|
|
1784
|
+
reference: this.#reference,
|
|
1785
|
+
downloads: { path: directory },
|
|
1786
|
+
...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
|
|
1787
|
+
});
|
|
1788
|
+
page = await context.create();
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
if (token !== void 0) {
|
|
1791
|
+
if (![...this.#leases.values()].some((lease) => lease.token.value === slot) && (this.#building.get(slot)?.size ?? 0) === 1) token.release();
|
|
1792
|
+
this.#lose(slot, error);
|
|
1793
|
+
}
|
|
1794
|
+
throw error;
|
|
1795
|
+
}
|
|
1796
|
+
toolset = createBrowserToolset(page, {
|
|
1797
|
+
context,
|
|
1798
|
+
journeys: {
|
|
1799
|
+
store: createFileBrowserJourneyStore({ root: this.#root }),
|
|
1800
|
+
runs: createFileBrowserRunStore({ root: this.#root }),
|
|
1801
|
+
readonly: this.#readonly
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
await toolset.start();
|
|
1805
|
+
const generation = {
|
|
1806
|
+
context,
|
|
1807
|
+
toolset,
|
|
1808
|
+
directory
|
|
1809
|
+
};
|
|
1810
|
+
const owned = this.#owned.get(slot) ?? /* @__PURE__ */ new Set();
|
|
1811
|
+
owned.add(generation);
|
|
1812
|
+
this.#owned.set(slot, owned);
|
|
1813
|
+
return generation;
|
|
1814
|
+
} catch (error) {
|
|
1815
|
+
await toolset?.destroy().catch(this.#fault.bind(this));
|
|
1816
|
+
await context?.close().catch((failure) => {
|
|
1817
|
+
if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !isCDPConnectionError(failure)) this.#fault(failure);
|
|
1818
|
+
});
|
|
1819
|
+
await rm(directory, {
|
|
1820
|
+
recursive: true,
|
|
1821
|
+
force: true
|
|
1822
|
+
}).catch(this.#fault.bind(this));
|
|
1823
|
+
throw error;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
async #destroyRecord(slot) {
|
|
1827
|
+
if (isCDPTimeoutError(this.#losses.get(slot)?.cause) && slot.browser.pid !== void 0) try {
|
|
1828
|
+
process.kill(slot.browser.pid, "SIGKILL");
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
if (!isError(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
|
|
1831
|
+
}
|
|
1832
|
+
try {
|
|
1833
|
+
await Promise.allSettled(this.#building.get(slot) ?? []);
|
|
1834
|
+
const prepared = this.#prepared.get(slot);
|
|
1835
|
+
if (prepared !== void 0) this.#untrack(prepared);
|
|
1836
|
+
this.#prepared.delete(slot);
|
|
1837
|
+
await Promise.all([...this.#owned.get(slot) ?? []].map((generation) => this.#clean(slot, generation)));
|
|
1838
|
+
await this.#destroySlot(slot.profile, slot.browser);
|
|
1839
|
+
this.#owned.delete(slot);
|
|
1840
|
+
this.#building.delete(slot);
|
|
1841
|
+
this.#endings.get(slot)?.resolve();
|
|
1842
|
+
} catch (error) {
|
|
1843
|
+
this.#endings.get(slot)?.reject(error);
|
|
1844
|
+
throw error;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1637
1847
|
#fault(error) {
|
|
1638
1848
|
this.#faults.add(error);
|
|
1639
1849
|
}
|
|
@@ -1642,7 +1852,7 @@ var BrowserMCPServer = class {
|
|
|
1642
1852
|
if (this.#stranded !== void 0) throw new BrowserError(isError(this.#stranded) ? this.#stranded.message : String(this.#stranded), BROWSER_SERVER_TEARDOWN, isBrowserError(this.#stranded) ? this.#stranded.context : void 0);
|
|
1643
1853
|
const profile = join(this.#root, ".profiles", formatBrowserLockEntry(process.pid, randomUUID()));
|
|
1644
1854
|
let browser;
|
|
1645
|
-
let
|
|
1855
|
+
let slot;
|
|
1646
1856
|
try {
|
|
1647
1857
|
await mkdir(profile);
|
|
1648
1858
|
this.#folders.add(profile);
|
|
@@ -1664,42 +1874,26 @@ var BrowserMCPServer = class {
|
|
|
1664
1874
|
}));
|
|
1665
1875
|
await rename(`${record}.tmp`, record);
|
|
1666
1876
|
}
|
|
1667
|
-
|
|
1668
|
-
reference: this.#reference,
|
|
1669
|
-
downloads: { path: join(profile, "downloads") },
|
|
1670
|
-
...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
|
|
1671
|
-
});
|
|
1672
|
-
const page = await context.create();
|
|
1673
|
-
toolset = createBrowserToolset(page, {
|
|
1674
|
-
context,
|
|
1675
|
-
journeys: {
|
|
1676
|
-
store: createFileBrowserJourneyStore({ root: this.#root }),
|
|
1677
|
-
runs: createFileBrowserRunStore({ root: this.#root }),
|
|
1678
|
-
readonly: this.#readonly
|
|
1679
|
-
}
|
|
1680
|
-
});
|
|
1681
|
-
await toolset.start();
|
|
1682
|
-
return {
|
|
1877
|
+
slot = {
|
|
1683
1878
|
browser,
|
|
1684
|
-
profile
|
|
1685
|
-
context,
|
|
1686
|
-
toolset
|
|
1879
|
+
profile
|
|
1687
1880
|
};
|
|
1881
|
+
const ending = Promise.withResolvers();
|
|
1882
|
+
ending.promise.catch(() => void 0);
|
|
1883
|
+
this.#endings.set(slot, ending);
|
|
1884
|
+
const generation = await this.#build(slot);
|
|
1885
|
+
this.#prepared.set(slot, generation);
|
|
1886
|
+
this.#observe(slot, generation);
|
|
1887
|
+
return slot;
|
|
1688
1888
|
} catch (error) {
|
|
1689
|
-
if (this.#folders.has(profile)) await this.#destroySlot(profile, browser
|
|
1889
|
+
if (this.#folders.has(profile)) await (slot === void 0 ? this.#destroySlot(profile, browser) : this.#destroyRecord(slot)).catch((failure) => {
|
|
1690
1890
|
this.#stranded = failure;
|
|
1691
1891
|
});
|
|
1692
1892
|
if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_LAUNCH, error)}`);
|
|
1693
1893
|
throw error;
|
|
1694
1894
|
}
|
|
1695
1895
|
}
|
|
1696
|
-
async #destroySlot(profile, browser
|
|
1697
|
-
if (isCDPTimeoutError(cause) && browser?.pid !== void 0) try {
|
|
1698
|
-
process.kill(browser.pid, "SIGKILL");
|
|
1699
|
-
} catch (error) {
|
|
1700
|
-
if (!isError(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
|
|
1701
|
-
}
|
|
1702
|
-
await toolset?.destroy().catch((error) => this.#faults.add(error));
|
|
1896
|
+
async #destroySlot(profile, browser) {
|
|
1703
1897
|
try {
|
|
1704
1898
|
await browser?.destroy();
|
|
1705
1899
|
} catch (error) {
|
|
@@ -1775,27 +1969,27 @@ var BrowserMCPServer = class {
|
|
|
1775
1969
|
#issue() {
|
|
1776
1970
|
return `e${++this.#references}`;
|
|
1777
1971
|
}
|
|
1778
|
-
#mirror(
|
|
1779
|
-
if (this.#leases.get(this.#shared) !==
|
|
1972
|
+
#mirror(lease, tool) {
|
|
1973
|
+
if (this.#leases.get(this.#shared) !== lease || this.#vocabulary.has(tool.name)) return;
|
|
1780
1974
|
this.#tools.add(createTool({
|
|
1781
1975
|
...toolToDefinition(tool),
|
|
1782
1976
|
execute: this.#forward.bind(this, this.#shared, tool.name)
|
|
1783
1977
|
}));
|
|
1784
1978
|
}
|
|
1785
|
-
#withdraw(
|
|
1786
|
-
if (this.#leases.get(this.#shared) !==
|
|
1787
|
-
const current =
|
|
1979
|
+
#withdraw(lease, tool) {
|
|
1980
|
+
if (this.#leases.get(this.#shared) !== lease || this.#vocabulary.has(tool.name)) return;
|
|
1981
|
+
const current = lease.generation.toolset.tools.tool(tool.name);
|
|
1788
1982
|
if (current === void 0) this.#tools.remove(tool.name);
|
|
1789
|
-
else this.#mirror(
|
|
1983
|
+
else this.#mirror(lease, current);
|
|
1790
1984
|
}
|
|
1791
|
-
#clear(
|
|
1792
|
-
for (const tool of tools) this.#withdraw(
|
|
1985
|
+
#clear(lease, tools) {
|
|
1986
|
+
for (const tool of tools) this.#withdraw(lease, tool);
|
|
1793
1987
|
}
|
|
1794
1988
|
#unmirror() {
|
|
1795
1989
|
const mirrored = this.#mirrored;
|
|
1796
1990
|
if (mirrored === void 0) return;
|
|
1797
1991
|
this.#mirrored = void 0;
|
|
1798
|
-
const tools = mirrored.
|
|
1992
|
+
const tools = mirrored.lease.generation.toolset.tools;
|
|
1799
1993
|
for (const tool of tools.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
|
|
1800
1994
|
tools.emitter.off("add", mirrored.added);
|
|
1801
1995
|
tools.emitter.off("remove", mirrored.removed);
|
|
@@ -2226,11 +2420,14 @@ var FileBrowserJourneyStore = class {
|
|
|
2226
2420
|
/**
|
|
2227
2421
|
* Persists runs and captures only in directories allocated by this instance.
|
|
2228
2422
|
* @remarks Requires an existing root. Reopened stores can read and delete saved runs.
|
|
2423
|
+
* Allocations of one journey share a process-local queue across instances; the filesystem lock
|
|
2424
|
+
* still refuses conflicting mutations and allocations from another process.
|
|
2229
2425
|
* @example
|
|
2230
2426
|
* const store = new FileBrowserRunStore({ root: directory })
|
|
2231
2427
|
* const slot = await store.open('check-ready')
|
|
2232
2428
|
*/
|
|
2233
|
-
var FileBrowserRunStore = class {
|
|
2429
|
+
var FileBrowserRunStore = class FileBrowserRunStore {
|
|
2430
|
+
static #allocations = /* @__PURE__ */ new Map();
|
|
2234
2431
|
#files;
|
|
2235
2432
|
#slots = /* @__PURE__ */ new WeakMap();
|
|
2236
2433
|
#directories = /* @__PURE__ */ new Set();
|
|
@@ -2258,16 +2455,27 @@ var FileBrowserRunStore = class {
|
|
|
2258
2455
|
async open(name, options) {
|
|
2259
2456
|
options?.signal?.throwIfAborted();
|
|
2260
2457
|
this.#files.validateName(name);
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
this.#
|
|
2269
|
-
|
|
2270
|
-
|
|
2458
|
+
const path = this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY);
|
|
2459
|
+
const previous = FileBrowserRunStore.#allocations.get(path);
|
|
2460
|
+
const allocation = Promise.withResolvers();
|
|
2461
|
+
FileBrowserRunStore.#allocations.set(path, allocation.promise);
|
|
2462
|
+
try {
|
|
2463
|
+
await previous;
|
|
2464
|
+
options?.signal?.throwIfAborted();
|
|
2465
|
+
return await this.#files.lock(path, async () => {
|
|
2466
|
+
const directory = await this.#files.allocate(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), generateBrowserRunId, options);
|
|
2467
|
+
const slot = Object.freeze({
|
|
2468
|
+
id: directory.id,
|
|
2469
|
+
directory: directory.path
|
|
2470
|
+
});
|
|
2471
|
+
this.#directories.add(directory.path);
|
|
2472
|
+
this.#slots.set(slot, directory.path);
|
|
2473
|
+
return slot;
|
|
2474
|
+
}, options);
|
|
2475
|
+
} finally {
|
|
2476
|
+
allocation.resolve();
|
|
2477
|
+
if (FileBrowserRunStore.#allocations.get(path) === allocation.promise) FileBrowserRunStore.#allocations.delete(path);
|
|
2478
|
+
}
|
|
2271
2479
|
}
|
|
2272
2480
|
async get(name, id, options) {
|
|
2273
2481
|
options?.signal?.throwIfAborted();
|
|
@@ -2477,6 +2685,7 @@ var Browser = class {
|
|
|
2477
2685
|
#servingPid;
|
|
2478
2686
|
#profile;
|
|
2479
2687
|
#contexts = [];
|
|
2688
|
+
#isolating = /* @__PURE__ */ new Set();
|
|
2480
2689
|
#destroyed = false;
|
|
2481
2690
|
#connecting = new BrowserTransition();
|
|
2482
2691
|
#disconnecting = new BrowserTransition();
|
|
@@ -2563,33 +2772,13 @@ var Browser = class {
|
|
|
2563
2772
|
return [...this.#contexts];
|
|
2564
2773
|
}
|
|
2565
2774
|
async isolate(options) {
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
|
|
2569
|
-
validateBrowserContextOptions(options);
|
|
2570
|
-
const params = { disposeOnDetach: false };
|
|
2571
|
-
if (options?.proxy !== void 0) {
|
|
2572
|
-
params["proxyServer"] = options.proxy.server;
|
|
2573
|
-
if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
|
|
2574
|
-
}
|
|
2575
|
-
if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
|
|
2576
|
-
const result = await client.send("Target.createBrowserContext", params);
|
|
2577
|
-
if (!isRecord(result) || !isString(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
|
|
2578
|
-
const id = result["browserContextId"];
|
|
2579
|
-
const context = new BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
|
|
2775
|
+
const attempt = this.#isolate(options);
|
|
2776
|
+
this.#isolating.add(attempt);
|
|
2580
2777
|
try {
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
downloadPath: options.downloads.path,
|
|
2585
|
-
eventsEnabled: true
|
|
2586
|
-
});
|
|
2587
|
-
} catch (error) {
|
|
2588
|
-
await context.close().catch(() => void 0);
|
|
2589
|
-
throw error;
|
|
2778
|
+
return await attempt;
|
|
2779
|
+
} finally {
|
|
2780
|
+
this.#isolating.delete(attempt);
|
|
2590
2781
|
}
|
|
2591
|
-
this.#registerContext(context);
|
|
2592
|
-
return context;
|
|
2593
2782
|
}
|
|
2594
2783
|
async create(options) {
|
|
2595
2784
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
@@ -2624,6 +2813,37 @@ var Browser = class {
|
|
|
2624
2813
|
this.#shutdown = shutdown;
|
|
2625
2814
|
return shutdown;
|
|
2626
2815
|
}
|
|
2816
|
+
async #isolate(options) {
|
|
2817
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
2818
|
+
const client = this.#client;
|
|
2819
|
+
if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
|
|
2820
|
+
validateBrowserContextOptions(options);
|
|
2821
|
+
const params = { disposeOnDetach: false };
|
|
2822
|
+
if (options?.proxy !== void 0) {
|
|
2823
|
+
params["proxyServer"] = options.proxy.server;
|
|
2824
|
+
if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
|
|
2825
|
+
}
|
|
2826
|
+
if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
|
|
2827
|
+
const result = await client.send("Target.createBrowserContext", params);
|
|
2828
|
+
if (!isRecord(result) || !isString(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
|
|
2829
|
+
const id = result["browserContextId"];
|
|
2830
|
+
const context = new BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
|
|
2831
|
+
try {
|
|
2832
|
+
if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
|
|
2833
|
+
if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
|
|
2834
|
+
behavior: options.downloads.named === true ? "allowAndName" : "allow",
|
|
2835
|
+
browserContextId: id,
|
|
2836
|
+
downloadPath: options.downloads.path,
|
|
2837
|
+
eventsEnabled: true
|
|
2838
|
+
});
|
|
2839
|
+
if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
|
|
2840
|
+
} catch (error) {
|
|
2841
|
+
await context.close().catch(() => void 0);
|
|
2842
|
+
throw error;
|
|
2843
|
+
}
|
|
2844
|
+
this.#registerContext(context);
|
|
2845
|
+
return context;
|
|
2846
|
+
}
|
|
2627
2847
|
async #establish() {
|
|
2628
2848
|
const disconnecting = this.#disconnecting.pending;
|
|
2629
2849
|
if (disconnecting !== void 0) await disconnecting;
|
|
@@ -3097,6 +3317,7 @@ var Browser = class {
|
|
|
3097
3317
|
}
|
|
3098
3318
|
}
|
|
3099
3319
|
async #settle() {
|
|
3320
|
+
await Promise.allSettled(this.#isolating);
|
|
3100
3321
|
await this.#connecting.pending?.catch(() => void 0);
|
|
3101
3322
|
await this.#disconnecting.pending?.catch(() => void 0);
|
|
3102
3323
|
await this.#settleExit();
|
|
@@ -3285,6 +3506,6 @@ var Browser = class {
|
|
|
3285
3506
|
}
|
|
3286
3507
|
};
|
|
3287
3508
|
//#endregion
|
|
3288
|
-
export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_DEVTOOLS_PATTERN, BROWSER_DRAIN_INTERVAL_MS, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_FILE_STORE_LIMIT, BROWSER_HEADLESS_ARG, BROWSER_JOURNEY_LOCK_ATTEMPTS, BROWSER_JOURNEY_LOCK_DIRECTORY, BROWSER_JOURNEY_REVISION_FILE, BROWSER_JOURNEY_SNAPSHOT_FILE, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS, BROWSER_PORT_PROBE_TIMEOUT_MS, BROWSER_PROCESS_EXIT_CAUSE, BROWSER_PROFILE_PREFIX, BROWSER_RUN_DIRECTORY, BROWSER_RUN_FILE, BROWSER_SERVER_BUSY, BROWSER_SERVER_COPY, BROWSER_SERVER_CRASH, BROWSER_SERVER_EXHAUSTED, BROWSER_SERVER_HOLDER, BROWSER_SERVER_LAUNCH, BROWSER_SERVER_OPTIONS, BROWSER_SERVER_POOL_LIMIT, BROWSER_SERVER_POOL_SIZE, BROWSER_SERVER_RECORD, BROWSER_SERVER_RESTARTS, BROWSER_SERVER_SWEEP, BROWSER_SERVER_TEARDOWN, BROWSER_SERVER_UNAVAILABLE, BROWSER_SERVER_UNRESOLVED, BROWSER_STORE_CACHE_DIRS, BROWSER_STORE_DEFAULT_DIRS, BROWSER_STORE_ENV_KEY, BROWSER_STORE_GLOBS, BROWSER_STORE_LINK_NAME, BROWSER_TRANSPORT_LOSS_CAUSE, BROWSER_TRANSPORT_LOSS_DEFER_MS, BROWSER_WINDOWS_ROOT_FALLBACKS, BROWSER_WINDOWS_SUFFIXES, Browser, BrowserDestroyedError, BrowserMCPServer, BrowserNotConnectedError, FileBrowserJourneyStore, FileBrowserRunStore, FileBrowserWriter, WebSocketCDPTransport, browserToEngine, buildInstallPaths, buildStoreBases, buildWindowsRoots, createBrowser, createBrowserMCPServer, createBrowserProfile, createBrowserWriter, createCDPTransport, createFileBrowserJourneyStore, createFileBrowserRunStore, describeBrowserServerLoss, fetchCDPTargets, findEnvOverrides, findInstallPaths, findStorePaths, findSystemBrowser, findSystemBrowsers, formatBrowserLockEntry, isBrowserDestroyedError, isBrowserNotConnectedError, launchBrowserProcess, normalizeExecutablePath, parseBrowserEngine, parseBrowserLockEntry, parseBrowserProfileRecord, parseBrowserViewport, probePathNames, probeProcess, readBrowserEndpoint, readFirstLine, removeBrowserProfile };
|
|
3509
|
+
export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_DEVTOOLS_PATTERN, BROWSER_DRAIN_INTERVAL_MS, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_FILE_STORE_LIMIT, BROWSER_HEADLESS_ARG, BROWSER_JOURNEY_LOCK_ATTEMPTS, BROWSER_JOURNEY_LOCK_DIRECTORY, BROWSER_JOURNEY_REVISION_FILE, BROWSER_JOURNEY_SNAPSHOT_FILE, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS, BROWSER_PORT_PROBE_TIMEOUT_MS, BROWSER_PROCESS_EXIT_CAUSE, BROWSER_PROFILE_PREFIX, BROWSER_RUN_DIRECTORY, BROWSER_RUN_FILE, BROWSER_SERVER_BUSY, BROWSER_SERVER_CONTEXTS, BROWSER_SERVER_CONTEXTS_LIMIT, BROWSER_SERVER_COPY, BROWSER_SERVER_CRASH, BROWSER_SERVER_EXHAUSTED, BROWSER_SERVER_HOLDER, BROWSER_SERVER_LAUNCH, BROWSER_SERVER_OPTIONS, BROWSER_SERVER_POOL_LIMIT, BROWSER_SERVER_POOL_SIZE, BROWSER_SERVER_RECORD, BROWSER_SERVER_RESTARTS, BROWSER_SERVER_SWEEP, BROWSER_SERVER_TEARDOWN, BROWSER_SERVER_UNAVAILABLE, BROWSER_SERVER_UNRESOLVED, BROWSER_STORE_CACHE_DIRS, BROWSER_STORE_DEFAULT_DIRS, BROWSER_STORE_ENV_KEY, BROWSER_STORE_GLOBS, BROWSER_STORE_LINK_NAME, BROWSER_TRANSPORT_LOSS_CAUSE, BROWSER_TRANSPORT_LOSS_DEFER_MS, BROWSER_WINDOWS_ROOT_FALLBACKS, BROWSER_WINDOWS_SUFFIXES, Browser, BrowserDestroyedError, BrowserMCPServer, BrowserNotConnectedError, FileBrowserJourneyStore, FileBrowserRunStore, FileBrowserWriter, WebSocketCDPTransport, browserToEngine, buildInstallPaths, buildStoreBases, buildWindowsRoots, createBrowser, createBrowserMCPServer, createBrowserProfile, createBrowserWriter, createCDPTransport, createFileBrowserJourneyStore, createFileBrowserRunStore, describeBrowserServerLoss, fetchCDPTargets, findEnvOverrides, findInstallPaths, findStorePaths, findSystemBrowser, findSystemBrowsers, formatBrowserLockEntry, isBrowserDestroyedError, isBrowserNotConnectedError, launchBrowserProcess, normalizeExecutablePath, parseBrowserEngine, parseBrowserLockEntry, parseBrowserProfileRecord, parseBrowserViewport, probePathNames, probeProcess, readBrowserEndpoint, readFirstLine, removeBrowserProfile };
|
|
3289
3510
|
|
|
3290
3511
|
//# sourceMappingURL=index.js.map
|