@orkestrel/browser 0.0.21 → 0.0.23
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/README.md +7 -4
- package/dist/bin/main.js +12 -3
- package/dist/bin/main.js.map +1 -1
- package/dist/src/browser/index.d.ts +77 -19
- package/dist/src/browser/index.js +361 -21
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +957 -174
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +356 -78
- package/dist/src/core/index.d.ts +356 -78
- package/dist/src/core/index.js +951 -176
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +540 -92
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +127 -18
- package/dist/src/server/index.d.ts +127 -18
- package/dist/src/server/index.js +529 -96
- package/dist/src/server/index.js.map +1 -1
- package/package.json +4 -3
|
@@ -13,6 +13,7 @@ let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
|
13
13
|
let _orkestrel_websocket = require("@orkestrel/websocket");
|
|
14
14
|
let node_events = require("node:events");
|
|
15
15
|
let _orkestrel_mcp = require("@orkestrel/mcp");
|
|
16
|
+
let _orkestrel_pool = require("@orkestrel/pool");
|
|
16
17
|
let _orkestrel_mcp_server = require("@orkestrel/mcp/server");
|
|
17
18
|
let _orkestrel_tool = require("@orkestrel/tool");
|
|
18
19
|
//#region src/server/constants.ts
|
|
@@ -212,6 +213,30 @@ var BROWSER_RUN_FILE = "run.json";
|
|
|
212
213
|
var BROWSER_RUN_DIRECTORY = "runs";
|
|
213
214
|
/** Bounds a file-store listing page by default. */
|
|
214
215
|
var BROWSER_FILE_STORE_LIMIT = 100;
|
|
216
|
+
/** Names the refusal when no browser can serve a call. */
|
|
217
|
+
var BROWSER_SERVER_UNAVAILABLE = "BROWSER_SERVER_UNAVAILABLE";
|
|
218
|
+
/** Names a failed attempt to warm a browser. */
|
|
219
|
+
var BROWSER_SERVER_LAUNCH = "BROWSER_SERVER_LAUNCH";
|
|
220
|
+
/** Sets the default number of warm browsers to 1. */
|
|
221
|
+
var BROWSER_SERVER_POOL_SIZE = 1;
|
|
222
|
+
/** Limits the number of warm browsers to 3. */
|
|
223
|
+
var BROWSER_SERVER_POOL_LIMIT = 3;
|
|
224
|
+
/** Permits one failed refill before the next failure spends the bound. */
|
|
225
|
+
var BROWSER_SERVER_RESTARTS = 1;
|
|
226
|
+
/** Names the browser process record in each profile. */
|
|
227
|
+
var BROWSER_SERVER_RECORD = "browse.json";
|
|
228
|
+
/** Names the diagnostic when the warm floor spends its restart bound. */
|
|
229
|
+
var BROWSER_SERVER_EXHAUSTED = "BROWSER_SERVER_EXHAUSTED";
|
|
230
|
+
/** Names a failed browser server teardown. */
|
|
231
|
+
var BROWSER_SERVER_TEARDOWN = "BROWSER_SERVER_TEARDOWN";
|
|
232
|
+
/** Names a failed profile sweep. */
|
|
233
|
+
var BROWSER_SERVER_SWEEP = "BROWSER_SERVER_SWEEP";
|
|
234
|
+
/** Names a refused browser server option. */
|
|
235
|
+
var BROWSER_SERVER_OPTIONS = "BROWSER_SERVER_OPTIONS";
|
|
236
|
+
/** Names the notice that a browser and its session state were lost. */
|
|
237
|
+
var BROWSER_SERVER_CRASH = "BROWSER_SERVER_CRASH";
|
|
238
|
+
/** Names an interrupted call whose outcome is unknown. */
|
|
239
|
+
var BROWSER_SERVER_UNRESOLVED = "BROWSER_SERVER_UNRESOLVED";
|
|
215
240
|
//#endregion
|
|
216
241
|
//#region src/server/errors.ts
|
|
217
242
|
/**
|
|
@@ -255,6 +280,58 @@ function isBrowserDestroyedError(value) {
|
|
|
255
280
|
//#endregion
|
|
256
281
|
//#region src/server/helpers.ts
|
|
257
282
|
/**
|
|
283
|
+
* Probes whether a process has not been confirmed absent.
|
|
284
|
+
* @param pid - Process identifier to probe with signal zero
|
|
285
|
+
* @returns True if the host has not confirmed absence with ESRCH; false otherwise
|
|
286
|
+
* @example
|
|
287
|
+
* probeProcess(process.pid) // true
|
|
288
|
+
*/
|
|
289
|
+
function probeProcess(pid) {
|
|
290
|
+
try {
|
|
291
|
+
process.kill(pid, 0);
|
|
292
|
+
return true;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return !((0, _orkestrel_contract.isError)(error) && "code" in error && error.code === "ESRCH");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Parses a profile record naming a positive process identifier and a loopback DevTools endpoint.
|
|
299
|
+
* @param text - JSON record read from a browse profile
|
|
300
|
+
* @returns The record, or undefined when its shape or endpoint is refused
|
|
301
|
+
* @example
|
|
302
|
+
* parseBrowserProfileRecord('{"pid":123,"endpoint":"ws://127.0.0.1:9222/devtools/browser/session"}')
|
|
303
|
+
*/
|
|
304
|
+
function parseBrowserProfileRecord(text) {
|
|
305
|
+
const record = (0, _orkestrel_contract.parseJSON)(text);
|
|
306
|
+
if (!(0, _orkestrel_contract.isRecord)(record)) return void 0;
|
|
307
|
+
const pid = record["pid"];
|
|
308
|
+
const endpoint = record["endpoint"];
|
|
309
|
+
if (!(0, _orkestrel_contract.isNumber)(pid) || !Number.isSafeInteger(pid) || pid <= 0 || !(0, _orkestrel_contract.isString)(endpoint)) return void 0;
|
|
310
|
+
const match = /^ws:\/\/127\.0\.0\.1:([1-9]\d*)\/devtools\/browser\/[^/?#\s\\]+$/.exec(endpoint);
|
|
311
|
+
if (match === null || Number(match[1]) > 65535) return void 0;
|
|
312
|
+
return {
|
|
313
|
+
pid,
|
|
314
|
+
endpoint
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Describes a browser loss with its code, cause, and the session state it invalidates.
|
|
319
|
+
* @param code - Browser server diagnostic code
|
|
320
|
+
* @param cause - The loss or refusal, including its process context when present
|
|
321
|
+
* @param url - The lost page's last URL, when known
|
|
322
|
+
* @returns The coded diagnostic and any operation-lifecycle guidance
|
|
323
|
+
* @example
|
|
324
|
+
* describeBrowserServerLoss('BROWSER_SERVER_UNAVAILABLE', new Error('Browser exited'))
|
|
325
|
+
*/
|
|
326
|
+
function describeBrowserServerLoss(code, cause, url) {
|
|
327
|
+
const pid = (0, _src_core.isBrowserError)(cause) ? cause.context?.["pid"] : void 0;
|
|
328
|
+
const message = `${code}: ${(cause === void 0 ? "Browser session lost" : (0, _orkestrel_contract.isError)(cause) ? cause.message : String(cause)).replace(/\.+$/u, "")}${(0, _orkestrel_contract.isNumber)(pid) ? ` (pid ${pid})` : ""}.`;
|
|
329
|
+
if (code !== "BROWSER_SERVER_CRASH" && code !== "BROWSER_SERVER_UNRESOLVED") return message;
|
|
330
|
+
const lost = `Lost the page${url === void 0 ? "" : ` at ${url}`}, its tabs, every element reference, the retained reading, dialogs, holds, an unsaved recording, the active replay, and the isolated context's cookies.`;
|
|
331
|
+
const next = `The next call acquires a browser that starts at about:blank, or answers ${BROWSER_SERVER_UNAVAILABLE} when none can serve.`;
|
|
332
|
+
return code === "BROWSER_SERVER_UNRESOLVED" ? `${message} The outcome is unknown. ${lost} Browse did not repeat the call. ${next}` : `${message} ${lost}`;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
258
335
|
* Formats a process identifier and UUID token as a lock entry name.
|
|
259
336
|
* @param pid - Positive safe integer identifying the holder process
|
|
260
337
|
* @param token - Lowercase UUID token identifying this acquisition
|
|
@@ -868,33 +945,37 @@ var FileBrowserWriter = class {
|
|
|
868
945
|
};
|
|
869
946
|
//#endregion
|
|
870
947
|
//#region package.json
|
|
871
|
-
var version = "0.0.
|
|
948
|
+
var version = "0.0.23";
|
|
872
949
|
//#endregion
|
|
873
950
|
//#region src/server/BrowserMCPServer.ts
|
|
874
951
|
/**
|
|
875
952
|
* Implements `BrowserMCPServerInterface`: serves the browser vocabulary and the journey tools over
|
|
876
|
-
* the Model Context Protocol on stdio, and
|
|
953
|
+
* the Model Context Protocol on stdio, and warms Chromium at server start.
|
|
877
954
|
*
|
|
878
955
|
* @remarks
|
|
879
|
-
* The server's own manager holds one dispatcher per name of the vocabulary — `look`, `read`,
|
|
956
|
+
* The server's own manager holds one dispatcher per name of the vocabulary — `look`, `read`, `plain`,
|
|
880
957
|
* `click`, `type`, `press`, `navigate`, `wait`, `dialog`, `tabs`, `switch`, `record`, `save`,
|
|
881
|
-
* `journeys`, `edit`, and `
|
|
958
|
+
* `journeys`, `edit`, `replay`, and `forget` — each carrying the description, parameters, and annotations
|
|
882
959
|
* `BROWSER_TOOL_COPY` gives, so `tools/list` answers before Chromium starts and with Chromium
|
|
883
960
|
* absent. After the launch, a tool the toolset's manager adds under another name, such as a page
|
|
884
961
|
* tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
|
|
885
962
|
* removed when the toolset withdraws the tool, so the server's tool list changes and a subscribed
|
|
886
|
-
* client is notified.
|
|
887
|
-
*
|
|
888
|
-
* A launch creates `ROOT/.profiles/
|
|
963
|
+
* client is notified. Startup warms the pool and leases one validated browser. The legacy
|
|
964
|
+
* handshake and every tool call await that setup; concurrent callers share a replacement lease.
|
|
965
|
+
* A launch creates `ROOT/.profiles/PID-UUID/` exclusively, connects a browser that never attaches to an
|
|
889
966
|
* existing endpoint, opens one page in an isolated context, and constructs the page toolset with
|
|
890
967
|
* that context and the file journey and run stores under the root, on a manager the toolset owns.
|
|
891
968
|
* A dispatcher forwards its arguments and the call's context, signal included, to the toolset's
|
|
892
969
|
* manager and returns that manager's value, or rejects with its failure message; a text value
|
|
893
970
|
* reaches the client as one text block rather than as its JSON text.
|
|
971
|
+
* A disconnect or current-page crash retires its slot. A failed call receives a liveness ping;
|
|
972
|
+
* loss makes its outcome unresolved, while a known success remains successful. Pending losses
|
|
973
|
+
* prefix the next outcome on a successor, or a refusal, and calls are never repeated.
|
|
894
974
|
*
|
|
895
975
|
* `start()` serves stdio and destroys the server at the end of its input, on `SIGINT`, and on
|
|
896
|
-
* `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys
|
|
897
|
-
* toolset, which aborts the active replay, then
|
|
976
|
+
* `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys every
|
|
977
|
+
* slot through the pool — its toolset, which aborts the active replay, then its browser, then its
|
|
978
|
+
* profile — and rechecks the profile folders this server made or kept.
|
|
898
979
|
*
|
|
899
980
|
* Chromium on Linux refuses to start as the root user unless its sandbox is disabled, so a server
|
|
900
981
|
* running as root on Linux launches Chromium with `--no-sandbox`; a launch that could run
|
|
@@ -924,11 +1005,21 @@ var BrowserMCPServer = class {
|
|
|
924
1005
|
#added;
|
|
925
1006
|
#removed;
|
|
926
1007
|
#cleared;
|
|
927
|
-
#
|
|
928
|
-
#
|
|
929
|
-
#
|
|
930
|
-
#
|
|
931
|
-
#
|
|
1008
|
+
#pool;
|
|
1009
|
+
#log;
|
|
1010
|
+
#folders = /* @__PURE__ */ new Set();
|
|
1011
|
+
#faults = /* @__PURE__ */ new Set();
|
|
1012
|
+
#losses = /* @__PURE__ */ new WeakMap();
|
|
1013
|
+
#watches = /* @__PURE__ */ new Map();
|
|
1014
|
+
#notice = /* @__PURE__ */ new Set();
|
|
1015
|
+
#reference;
|
|
1016
|
+
#references = 0;
|
|
1017
|
+
#failure;
|
|
1018
|
+
#stranded;
|
|
1019
|
+
#lease;
|
|
1020
|
+
#granting;
|
|
1021
|
+
#starting;
|
|
1022
|
+
#sweeping;
|
|
932
1023
|
#closing;
|
|
933
1024
|
/**
|
|
934
1025
|
* Registers the dispatchers and binds the protocol to the stdio streams without reading them.
|
|
@@ -936,6 +1027,19 @@ var BrowserMCPServer = class {
|
|
|
936
1027
|
* @param options - The root, the launch, the journeys' read-only switch, and the streams
|
|
937
1028
|
*/
|
|
938
1029
|
constructor(options) {
|
|
1030
|
+
const size = options?.pool?.size ?? 1;
|
|
1031
|
+
if (!(0, _orkestrel_contract.isInteger)(size) || size < 1 || size > 3) throw new _src_core.BrowserError(`pool.size must be an integer from 1 through 3`, BROWSER_SERVER_OPTIONS);
|
|
1032
|
+
this.#log = options?.log ?? process.stderr;
|
|
1033
|
+
this.#reference = this.#issue.bind(this);
|
|
1034
|
+
this.#pool = (0, _orkestrel_pool.createPool)({
|
|
1035
|
+
create: this.#warm.bind(this),
|
|
1036
|
+
destroy: (slot) => this.#destroySlot(slot.profile, slot.browser, slot.toolset, this.#losses.get(slot)?.cause),
|
|
1037
|
+
validate: this.#validate.bind(this),
|
|
1038
|
+
watch: this.#watch.bind(this),
|
|
1039
|
+
error: this.#fault.bind(this),
|
|
1040
|
+
min: size,
|
|
1041
|
+
restarts: 1
|
|
1042
|
+
});
|
|
939
1043
|
this.#root = (0, node_path.resolve)(options?.root ?? "tmp/browsers");
|
|
940
1044
|
this.#headless = options?.headless ?? true;
|
|
941
1045
|
this.#executable = options?.executable;
|
|
@@ -955,7 +1059,8 @@ var BrowserMCPServer = class {
|
|
|
955
1059
|
version
|
|
956
1060
|
},
|
|
957
1061
|
tools: this.#tools,
|
|
958
|
-
execution: this.#execute.bind(this)
|
|
1062
|
+
execution: this.#execute.bind(this),
|
|
1063
|
+
handshake: this.#handshake.bind(this)
|
|
959
1064
|
});
|
|
960
1065
|
this.#transport = (0, _orkestrel_mcp_server.createStdioServer)((0, _orkestrel_mcp.createMCPLegacy)(server), {
|
|
961
1066
|
input: this.#input,
|
|
@@ -968,17 +1073,32 @@ var BrowserMCPServer = class {
|
|
|
968
1073
|
}
|
|
969
1074
|
async start() {
|
|
970
1075
|
if (this.#closing !== void 0) throw this.#ended();
|
|
971
|
-
|
|
972
|
-
this.#
|
|
973
|
-
this.#transport.start();
|
|
974
|
-
this.#input.on("end", this.#signal);
|
|
975
|
-
process.on("SIGINT", this.#signal);
|
|
976
|
-
process.on("SIGTERM", this.#signal);
|
|
1076
|
+
this.#starting ??= this.#setup();
|
|
1077
|
+
return this.#starting;
|
|
977
1078
|
}
|
|
978
1079
|
destroy() {
|
|
979
|
-
this.#closing ??= this.#destroy();
|
|
1080
|
+
this.#closing ??= Promise.resolve().then(this.#destroy.bind(this));
|
|
980
1081
|
return this.#closing;
|
|
981
1082
|
}
|
|
1083
|
+
async #setup() {
|
|
1084
|
+
this.#input.on("end", this.#signal);
|
|
1085
|
+
process.on("SIGINT", this.#signal);
|
|
1086
|
+
process.on("SIGTERM", this.#signal);
|
|
1087
|
+
this.#transport.start();
|
|
1088
|
+
try {
|
|
1089
|
+
await (0, node_fs_promises.mkdir)((0, node_path.join)(this.#root, ".profiles"), { recursive: true });
|
|
1090
|
+
if (this.#closing !== void 0) return;
|
|
1091
|
+
this.#sweeping = this.#sweep();
|
|
1092
|
+
const warming = this.#pool.start().then(void 0, (error) => error);
|
|
1093
|
+
await this.#grant(this.#abort.signal);
|
|
1094
|
+
if (this.#closing !== void 0) return;
|
|
1095
|
+
warming.then(this.#exhaust.bind(this));
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
if (this.#closing !== void 0) return;
|
|
1098
|
+
if ((0, _src_core.isBrowserError)(error) && error.code === "BROWSER_SERVER_UNAVAILABLE") throw error;
|
|
1099
|
+
throw this.#unavailable((0, _orkestrel_pool.isPoolError)(error) ? error.cause ?? this.#failure : error);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
982
1102
|
async #destroy() {
|
|
983
1103
|
process.removeListener("SIGINT", this.#signal);
|
|
984
1104
|
process.removeListener("SIGTERM", this.#signal);
|
|
@@ -986,19 +1106,34 @@ var BrowserMCPServer = class {
|
|
|
986
1106
|
this.#transport.stop();
|
|
987
1107
|
for (const tool of this.#tools.tools()) this.#tools.remove(tool.name);
|
|
988
1108
|
this.#unmirror();
|
|
1109
|
+
this.#lease = void 0;
|
|
989
1110
|
this.#abort.abort();
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
await this.#browser?.destroy().catch((error) => failures.push(error));
|
|
993
|
-
this.#browser = void 0;
|
|
994
|
-
const profile = this.#profile;
|
|
995
|
-
this.#profile = void 0;
|
|
996
|
-
if (profile !== void 0) await (0, node_fs_promises.rm)(profile, {
|
|
997
|
-
recursive: true,
|
|
998
|
-
force: true,
|
|
999
|
-
maxRetries: 5
|
|
1111
|
+
const barrier = this.#pool.destroy().catch((error) => {
|
|
1112
|
+
for (const failure of (0, _orkestrel_pool.isPoolError)(error) ? error.context?.failures ?? [error] : [error]) this.#faults.add(failure);
|
|
1000
1113
|
});
|
|
1001
|
-
|
|
1114
|
+
await this.#starting?.catch(() => void 0);
|
|
1115
|
+
await barrier;
|
|
1116
|
+
await this.#sweeping;
|
|
1117
|
+
if (this.#stranded !== void 0) this.#faults.add(this.#stranded);
|
|
1118
|
+
for (const folder of this.#folders) {
|
|
1119
|
+
try {
|
|
1120
|
+
const record = parseBrowserProfileRecord(await (0, node_fs_promises.readFile)((0, node_path.join)(folder, BROWSER_SERVER_RECORD), "utf8"));
|
|
1121
|
+
if (record === void 0 || probeProcess(record.pid)) continue;
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ENOENT") continue;
|
|
1124
|
+
}
|
|
1125
|
+
try {
|
|
1126
|
+
await (0, node_fs_promises.rm)(folder, {
|
|
1127
|
+
recursive: true,
|
|
1128
|
+
force: true,
|
|
1129
|
+
maxRetries: 5
|
|
1130
|
+
});
|
|
1131
|
+
this.#folders.delete(folder);
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
this.#faults.add(error);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (this.#faults.size > 0) throw new AggregateError([...this.#faults], "The browse server teardown failed");
|
|
1002
1137
|
}
|
|
1003
1138
|
async #execute(context) {
|
|
1004
1139
|
const result = await context.tools.execute(context.call, {
|
|
@@ -1015,38 +1150,224 @@ var BrowserMCPServer = class {
|
|
|
1015
1150
|
};
|
|
1016
1151
|
}
|
|
1017
1152
|
#end() {
|
|
1018
|
-
this
|
|
1153
|
+
if (this.#closing !== void 0) return;
|
|
1154
|
+
this.destroy().catch(this.#endFailure.bind(this));
|
|
1155
|
+
}
|
|
1156
|
+
#endFailure(error) {
|
|
1157
|
+
process.exitCode = 1;
|
|
1158
|
+
this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_TEARDOWN, error)}`);
|
|
1019
1159
|
}
|
|
1020
1160
|
async #forward(name, args, context) {
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1161
|
+
let slot;
|
|
1162
|
+
try {
|
|
1163
|
+
slot = await this.#serve(context.signal);
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
context.signal.throwIfAborted();
|
|
1166
|
+
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(`${error.code}: ${error.message}`), error.code);
|
|
1167
|
+
throw error;
|
|
1168
|
+
}
|
|
1169
|
+
let result;
|
|
1170
|
+
try {
|
|
1171
|
+
result = await slot.toolset.tools.execute({
|
|
1172
|
+
id: name,
|
|
1173
|
+
name,
|
|
1174
|
+
arguments: args
|
|
1175
|
+
}, context);
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
result = {
|
|
1178
|
+
id: name,
|
|
1179
|
+
name,
|
|
1180
|
+
success: false,
|
|
1181
|
+
error: (0, _orkestrel_contract.isError)(error) ? error.message : String(error)
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
context.signal.throwIfAborted();
|
|
1185
|
+
if (!result.success) {
|
|
1186
|
+
if (this.#lease?.value === slot && !context.signal.aborted) try {
|
|
1187
|
+
await slot.browser.ping({ signal: AbortSignal.any([context.signal, this.#abort.signal]) });
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
if (!context.signal.aborted && !this.#abort.signal.aborted) this.#lose(slot, error);
|
|
1190
|
+
}
|
|
1191
|
+
context.signal.throwIfAborted();
|
|
1192
|
+
if (this.#lease?.value !== slot) {
|
|
1193
|
+
const loss = this.#losses.get(slot);
|
|
1194
|
+
throw new _src_core.BrowserError(this.#annotate(describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), slot), BROWSER_SERVER_UNRESOLVED);
|
|
1195
|
+
}
|
|
1196
|
+
throw new _src_core.BrowserError(this.#annotate(result.error, slot));
|
|
1197
|
+
}
|
|
1198
|
+
return typeof result.value === "string" ? this.#annotate(result.value, slot) : result.value;
|
|
1199
|
+
}
|
|
1200
|
+
#annotate(value, slot) {
|
|
1201
|
+
const notes = [];
|
|
1202
|
+
for (const lost of this.#notice) {
|
|
1203
|
+
if (lost === slot) continue;
|
|
1204
|
+
const loss = this.#losses.get(lost);
|
|
1205
|
+
notes.push(describeBrowserServerLoss(BROWSER_SERVER_CRASH, loss?.cause, loss?.url));
|
|
1206
|
+
this.#notice.delete(lost);
|
|
1207
|
+
}
|
|
1208
|
+
return [...notes, value].join("\n");
|
|
1209
|
+
}
|
|
1210
|
+
async #handshake(options) {
|
|
1211
|
+
try {
|
|
1212
|
+
await this.#race(this.#starting, options.signal);
|
|
1213
|
+
if (this.#closing !== void 0) throw this.#ended();
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
options.signal.throwIfAborted();
|
|
1216
|
+
if ((0, _src_core.isBrowserError)(error)) throw new _orkestrel_mcp.MCPError(`${error.code}: ${error.message}`, _orkestrel_mcp.JSONRPC_SERVER_ERROR, { code: error.code });
|
|
1217
|
+
throw error;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
async #race(promise, signal) {
|
|
1221
|
+
signal.throwIfAborted();
|
|
1222
|
+
const aborted = Promise.withResolvers();
|
|
1223
|
+
const listener = (0, node_events.addAbortListener)(signal, () => aborted.reject(signal.reason));
|
|
1224
|
+
try {
|
|
1225
|
+
return await Promise.race([promise, aborted.promise]);
|
|
1226
|
+
} finally {
|
|
1227
|
+
listener[Symbol.dispose]();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async #grant(signal) {
|
|
1231
|
+
if (this.#lease !== void 0) return this.#lease;
|
|
1232
|
+
this.#granting ??= this.#pool.acquire(this.#abort.signal).then(this.#hold.bind(this), this.#refuse.bind(this));
|
|
1233
|
+
const token = await this.#race(this.#granting, signal);
|
|
1234
|
+
if (token === void 0) throw this.#ended();
|
|
1235
|
+
return token;
|
|
1236
|
+
}
|
|
1237
|
+
#hold(token) {
|
|
1238
|
+
this.#granting = void 0;
|
|
1239
|
+
if (this.#closing !== void 0) throw this.#ended();
|
|
1240
|
+
if (this.#losses.has(token.value)) {
|
|
1241
|
+
token.destroy().catch(() => void 0);
|
|
1242
|
+
throw this.#unavailable(this.#losses.get(token.value)?.cause);
|
|
1243
|
+
}
|
|
1244
|
+
this.#lease = token;
|
|
1245
|
+
const tools = token.value.toolset.tools;
|
|
1246
|
+
for (const tool of tools.tools()) this.#mirror(tool);
|
|
1247
|
+
tools.emitter.on("add", this.#added);
|
|
1248
|
+
tools.emitter.on("remove", this.#removed);
|
|
1249
|
+
tools.emitter.on("clear", this.#cleared);
|
|
1250
|
+
return token;
|
|
1251
|
+
}
|
|
1252
|
+
#refuse(error) {
|
|
1253
|
+
this.#granting = void 0;
|
|
1254
|
+
throw error;
|
|
1255
|
+
}
|
|
1256
|
+
async #serve(signal) {
|
|
1257
|
+
if (this.#closing !== void 0) throw this.#ended();
|
|
1258
|
+
await this.#race(this.#starting, signal);
|
|
1259
|
+
if (this.#closing !== void 0) throw this.#ended();
|
|
1260
|
+
const held = this.#lease;
|
|
1261
|
+
if (held !== void 0) try {
|
|
1262
|
+
await held.value.browser.ping({ signal: AbortSignal.any([signal, this.#abort.signal]) });
|
|
1263
|
+
if (this.#lease === held) return held.value;
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
signal.throwIfAborted();
|
|
1266
|
+
if (this.#abort.signal.aborted) throw this.#ended();
|
|
1267
|
+
this.#lose(held.value, error);
|
|
1268
|
+
}
|
|
1269
|
+
try {
|
|
1270
|
+
const token = await this.#grant(signal);
|
|
1271
|
+
if (this.#lease !== token) throw this.#unavailable(this.#losses.get(token.value)?.cause);
|
|
1272
|
+
return token.value;
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
signal.throwIfAborted();
|
|
1275
|
+
if (this.#abort.signal.aborted || (0, _orkestrel_pool.isPoolError)(error) && error.code === "destroyed") throw this.#ended();
|
|
1276
|
+
if ((0, _orkestrel_pool.isPoolError)(error) && (error.code === "create" || error.code === "cleanup")) {
|
|
1277
|
+
this.#exhaust(error);
|
|
1278
|
+
throw this.#unavailable(error.cause ?? this.#failure);
|
|
1279
|
+
}
|
|
1280
|
+
throw error;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
async #validate(slot) {
|
|
1284
|
+
try {
|
|
1285
|
+
await slot.browser.ping({ signal: this.#abort.signal });
|
|
1286
|
+
return true;
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
if (!this.#abort.signal.aborted) this.#lose(slot, error);
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
#lose(slot, cause) {
|
|
1293
|
+
if (this.#closing !== void 0 || this.#losses.has(slot)) return;
|
|
1294
|
+
this.#losses.set(slot, {
|
|
1295
|
+
cause,
|
|
1296
|
+
url: slot.toolset.view.url
|
|
1297
|
+
});
|
|
1298
|
+
this.#failure = cause;
|
|
1299
|
+
if (this.#lease?.value !== slot) return;
|
|
1300
|
+
const token = this.#lease;
|
|
1301
|
+
this.#unmirror();
|
|
1302
|
+
this.#lease = void 0;
|
|
1303
|
+
this.#notice.add(slot);
|
|
1304
|
+
token.destroy().catch(() => void 0);
|
|
1305
|
+
}
|
|
1306
|
+
#watch(slot, signal) {
|
|
1307
|
+
const loss = Promise.withResolvers();
|
|
1308
|
+
const disconnect = this.#disconnect.bind(this, slot);
|
|
1309
|
+
const page = this.#track.bind(this, slot);
|
|
1310
|
+
const subscription = (0, node_events.addAbortListener)(signal, this.#unwatch.bind(this, slot));
|
|
1311
|
+
this.#watches.set(slot, {
|
|
1312
|
+
resolve: loss.resolve,
|
|
1313
|
+
disconnect,
|
|
1314
|
+
page,
|
|
1315
|
+
subscription,
|
|
1316
|
+
crashes: /* @__PURE__ */ new Map()
|
|
1317
|
+
});
|
|
1318
|
+
slot.browser.emitter.on("disconnect", disconnect);
|
|
1319
|
+
slot.context.emitter.on("page", page);
|
|
1320
|
+
for (const current of slot.context.pages()) this.#track(slot, current);
|
|
1321
|
+
if (signal.aborted) this.#unwatch(slot);
|
|
1322
|
+
else if (slot.browser.status !== "connected") this.#disconnect(slot);
|
|
1323
|
+
return loss.promise;
|
|
1324
|
+
}
|
|
1325
|
+
#track(slot, page) {
|
|
1326
|
+
const watch = this.#watches.get(slot);
|
|
1327
|
+
if (watch === void 0 || watch.crashes.has(page)) return;
|
|
1328
|
+
const crash = this.#createCrash(slot, page);
|
|
1329
|
+
page.emitter.on("crash", crash);
|
|
1330
|
+
this.#watches.set(slot, {
|
|
1331
|
+
...watch,
|
|
1332
|
+
crashes: new Map([...watch.crashes, [page, crash]])
|
|
1037
1333
|
});
|
|
1038
|
-
return session;
|
|
1039
1334
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
this.#
|
|
1335
|
+
#createCrash(slot, page) {
|
|
1336
|
+
return () => {
|
|
1337
|
+
if (page === slot.toolset.view) this.#report(slot, /* @__PURE__ */ new Error("The current page renderer crashed"));
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
#disconnect(slot) {
|
|
1341
|
+
this.#report(slot, /* @__PURE__ */ new Error(slot.browser.pid === void 0 ? "The browser process exited" : "The browser transport disconnected"));
|
|
1342
|
+
}
|
|
1343
|
+
#report(slot, cause) {
|
|
1344
|
+
const watch = this.#watches.get(slot);
|
|
1345
|
+
if (watch === void 0) return;
|
|
1346
|
+
this.#unwatch(slot);
|
|
1347
|
+
this.#lose(slot, cause);
|
|
1348
|
+
watch.resolve(cause);
|
|
1349
|
+
}
|
|
1350
|
+
#unwatch(slot) {
|
|
1351
|
+
const watch = this.#watches.get(slot);
|
|
1352
|
+
if (watch === void 0) return;
|
|
1353
|
+
this.#watches.delete(slot);
|
|
1354
|
+
slot.browser.emitter.off("disconnect", watch.disconnect);
|
|
1355
|
+
slot.context.emitter.off("page", watch.page);
|
|
1356
|
+
for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
|
|
1357
|
+
watch.subscription[Symbol.dispose]();
|
|
1358
|
+
}
|
|
1359
|
+
#fault(error) {
|
|
1360
|
+
this.#faults.add(error);
|
|
1361
|
+
}
|
|
1362
|
+
async #warm() {
|
|
1363
|
+
if (this.#closing !== void 0) throw this.#ended();
|
|
1364
|
+
if (this.#stranded !== void 0) throw new _src_core.BrowserError((0, _orkestrel_contract.isError)(this.#stranded) ? this.#stranded.message : String(this.#stranded), BROWSER_SERVER_TEARDOWN, (0, _src_core.isBrowserError)(this.#stranded) ? this.#stranded.context : void 0);
|
|
1365
|
+
const profile = (0, node_path.join)(this.#root, ".profiles", formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)()));
|
|
1047
1366
|
let browser;
|
|
1048
1367
|
let toolset;
|
|
1049
1368
|
try {
|
|
1369
|
+
await (0, node_fs_promises.mkdir)(profile);
|
|
1370
|
+
this.#folders.add(profile);
|
|
1050
1371
|
browser = this.#launcher({
|
|
1051
1372
|
headless: this.#headless,
|
|
1052
1373
|
profile,
|
|
@@ -1055,9 +1376,16 @@ var BrowserMCPServer = class {
|
|
|
1055
1376
|
...this.#executable === void 0 ? {} : { executable: this.#executable },
|
|
1056
1377
|
...process.platform === "linux" && process.getuid?.() === 0 ? { args: ["--no-sandbox"] } : {}
|
|
1057
1378
|
});
|
|
1058
|
-
this.#browser = browser;
|
|
1059
1379
|
await browser.connect();
|
|
1060
|
-
|
|
1380
|
+
if (browser.pid !== void 0 && browser.endpoint !== void 0) {
|
|
1381
|
+
const record = (0, node_path.join)(profile, BROWSER_SERVER_RECORD);
|
|
1382
|
+
await (0, node_fs_promises.writeFile)(`${record}.tmp`, JSON.stringify({
|
|
1383
|
+
pid: browser.pid,
|
|
1384
|
+
endpoint: browser.endpoint
|
|
1385
|
+
}));
|
|
1386
|
+
await (0, node_fs_promises.rename)(`${record}.tmp`, record);
|
|
1387
|
+
}
|
|
1388
|
+
const context = await browser.isolate({ reference: this.#reference });
|
|
1061
1389
|
const page = await context.create();
|
|
1062
1390
|
toolset = (0, _src_core.createBrowserToolset)(page, {
|
|
1063
1391
|
context,
|
|
@@ -1067,26 +1395,103 @@ var BrowserMCPServer = class {
|
|
|
1067
1395
|
readonly: this.#readonly
|
|
1068
1396
|
}
|
|
1069
1397
|
});
|
|
1070
|
-
this.#mirrored = toolset.tools;
|
|
1071
|
-
toolset.tools.emitter.on("add", this.#added);
|
|
1072
|
-
toolset.tools.emitter.on("remove", this.#removed);
|
|
1073
|
-
toolset.tools.emitter.on("clear", this.#cleared);
|
|
1074
1398
|
await toolset.start();
|
|
1075
|
-
return
|
|
1399
|
+
return {
|
|
1400
|
+
browser,
|
|
1401
|
+
profile,
|
|
1402
|
+
context,
|
|
1403
|
+
toolset
|
|
1404
|
+
};
|
|
1076
1405
|
} catch (error) {
|
|
1077
|
-
this.#
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1406
|
+
if (this.#folders.has(profile)) await this.#destroySlot(profile, browser, toolset).catch((failure) => {
|
|
1407
|
+
this.#stranded = failure;
|
|
1408
|
+
});
|
|
1409
|
+
if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_LAUNCH, error)}`);
|
|
1410
|
+
throw error;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
async #destroySlot(profile, browser, toolset, cause) {
|
|
1414
|
+
if ((0, _src_core.isCDPTimeoutError)(cause) && browser?.pid !== void 0) try {
|
|
1415
|
+
process.kill(browser.pid, "SIGKILL");
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
|
|
1418
|
+
}
|
|
1419
|
+
await toolset?.destroy().catch((error) => this.#faults.add(error));
|
|
1420
|
+
try {
|
|
1421
|
+
await browser?.destroy();
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_TEARDOWN, error)}`);
|
|
1424
|
+
throw error;
|
|
1425
|
+
}
|
|
1426
|
+
try {
|
|
1082
1427
|
await (0, node_fs_promises.rm)(profile, {
|
|
1083
1428
|
recursive: true,
|
|
1084
1429
|
force: true,
|
|
1085
1430
|
maxRetries: 5
|
|
1086
1431
|
});
|
|
1087
|
-
|
|
1432
|
+
this.#folders.delete(profile);
|
|
1433
|
+
} catch {}
|
|
1434
|
+
}
|
|
1435
|
+
async #sweep() {
|
|
1436
|
+
const profiles = (0, node_path.join)(this.#root, ".profiles");
|
|
1437
|
+
try {
|
|
1438
|
+
for (const entry of await (0, node_fs_promises.readdir)(profiles)) {
|
|
1439
|
+
if (this.#closing !== void 0) return;
|
|
1440
|
+
const pid = parseBrowserLockEntry(entry);
|
|
1441
|
+
if (pid === void 0 || pid === process.pid || probeProcess(pid)) continue;
|
|
1442
|
+
const folder = (0, node_path.join)(profiles, entry);
|
|
1443
|
+
this.#folders.add(folder);
|
|
1444
|
+
try {
|
|
1445
|
+
const record = parseBrowserProfileRecord(await (0, node_fs_promises.readFile)((0, node_path.join)(folder, BROWSER_SERVER_RECORD), "utf8"));
|
|
1446
|
+
if (record === void 0) continue;
|
|
1447
|
+
if (probeProcess(record.pid)) {
|
|
1448
|
+
const transport = createCDPTransport({ url: record.endpoint });
|
|
1449
|
+
const client = (0, _src_core.createCDPClient)({ transport });
|
|
1450
|
+
try {
|
|
1451
|
+
this.#abort.signal.throwIfAborted();
|
|
1452
|
+
await this.#race(client.connect(), this.#abort.signal);
|
|
1453
|
+
await client.send("Browser.close", void 0, { signal: this.#abort.signal });
|
|
1454
|
+
} catch {} finally {
|
|
1455
|
+
await transport.close();
|
|
1456
|
+
await client.close();
|
|
1457
|
+
}
|
|
1458
|
+
if (probeProcess(record.pid)) continue;
|
|
1459
|
+
}
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ENOENT") continue;
|
|
1462
|
+
}
|
|
1463
|
+
try {
|
|
1464
|
+
await (0, node_fs_promises.rm)(folder, {
|
|
1465
|
+
recursive: true,
|
|
1466
|
+
force: true,
|
|
1467
|
+
maxRetries: 5
|
|
1468
|
+
});
|
|
1469
|
+
this.#folders.delete(folder);
|
|
1470
|
+
} catch {}
|
|
1471
|
+
}
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_SWEEP, error)}`);
|
|
1088
1474
|
}
|
|
1089
1475
|
}
|
|
1476
|
+
#write(line) {
|
|
1477
|
+
try {
|
|
1478
|
+
this.#log.write(`${line}\n`, (error) => {
|
|
1479
|
+
if (error !== void 0 && error !== null) this.#faults.add(error);
|
|
1480
|
+
});
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
this.#faults.add(error);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
#exhaust(error) {
|
|
1486
|
+
if ((0, _orkestrel_pool.isPoolError)(error) && error.code === "create") this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_EXHAUSTED, error.cause ?? this.#failure)}`);
|
|
1487
|
+
}
|
|
1488
|
+
#unavailable(cause) {
|
|
1489
|
+
const message = describeBrowserServerLoss(BROWSER_SERVER_UNAVAILABLE, cause);
|
|
1490
|
+
return new _src_core.BrowserError(message.slice(BROWSER_SERVER_UNAVAILABLE.length + 2), BROWSER_SERVER_UNAVAILABLE);
|
|
1491
|
+
}
|
|
1492
|
+
#issue() {
|
|
1493
|
+
return `e${++this.#references}`;
|
|
1494
|
+
}
|
|
1090
1495
|
#mirror(tool) {
|
|
1091
1496
|
if (this.#vocabulary.has(tool.name)) return;
|
|
1092
1497
|
this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
@@ -1096,7 +1501,7 @@ var BrowserMCPServer = class {
|
|
|
1096
1501
|
}
|
|
1097
1502
|
#withdraw(tool) {
|
|
1098
1503
|
if (this.#vocabulary.has(tool.name)) return;
|
|
1099
|
-
const current = this.#
|
|
1504
|
+
const current = this.#lease?.value.toolset.tools.tool(tool.name);
|
|
1100
1505
|
if (current === void 0) this.#tools.remove(tool.name);
|
|
1101
1506
|
else this.#mirror(current);
|
|
1102
1507
|
}
|
|
@@ -1104,9 +1509,9 @@ var BrowserMCPServer = class {
|
|
|
1104
1509
|
for (const tool of tools) this.#withdraw(tool);
|
|
1105
1510
|
}
|
|
1106
1511
|
#unmirror() {
|
|
1107
|
-
const mirrored = this.#
|
|
1512
|
+
const mirrored = this.#lease?.value.toolset.tools;
|
|
1108
1513
|
if (mirrored === void 0) return;
|
|
1109
|
-
|
|
1514
|
+
for (const tool of mirrored.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
|
|
1110
1515
|
mirrored.emitter.off("add", this.#added);
|
|
1111
1516
|
mirrored.emitter.off("remove", this.#removed);
|
|
1112
1517
|
mirrored.emitter.off("clear", this.#cleared);
|
|
@@ -1228,7 +1633,13 @@ var FileBrowserStore = class {
|
|
|
1228
1633
|
await this.check(path, options);
|
|
1229
1634
|
await this.check(temporary, options);
|
|
1230
1635
|
options?.signal?.throwIfAborted();
|
|
1231
|
-
|
|
1636
|
+
try {
|
|
1637
|
+
await (0, node_fs_promises.rename)(temporary, path);
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
const target = await (0, node_fs_promises.lstat)(path).catch(() => void 0);
|
|
1640
|
+
if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new _src_core.BrowserError(`Cannot replace directory: ${path}`, "BROWSER_JOURNEY_FILE", { path });
|
|
1641
|
+
throw error;
|
|
1642
|
+
}
|
|
1232
1643
|
owned = false;
|
|
1233
1644
|
} catch (error) {
|
|
1234
1645
|
options?.signal?.throwIfAborted();
|
|
@@ -1273,13 +1684,7 @@ var FileBrowserStore = class {
|
|
|
1273
1684
|
const holder = entries[0];
|
|
1274
1685
|
if (holder !== void 0) {
|
|
1275
1686
|
const pid = parseBrowserLockEntry(holder);
|
|
1276
|
-
if (pid === void 0) break;
|
|
1277
|
-
try {
|
|
1278
|
-
process.kill(pid, 0);
|
|
1279
|
-
break;
|
|
1280
|
-
} catch (cause) {
|
|
1281
|
-
if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH")) break;
|
|
1282
|
-
}
|
|
1687
|
+
if (pid === void 0 || probeProcess(pid)) break;
|
|
1283
1688
|
if (!await this.#unlinkLock(this.resolvePath(path, holder))) continue;
|
|
1284
1689
|
}
|
|
1285
1690
|
await this.#removeLock(path);
|
|
@@ -1429,7 +1834,14 @@ var FileBrowserStore = class {
|
|
|
1429
1834
|
try {
|
|
1430
1835
|
await (0, node_fs_promises.rmdir)(path);
|
|
1431
1836
|
} catch (error) {
|
|
1432
|
-
if (error instanceof Error && "code" in error
|
|
1837
|
+
if (error instanceof Error && "code" in error) {
|
|
1838
|
+
if (error.code === "ENOTEMPTY") return;
|
|
1839
|
+
if (error.code === "ENOENT" || error.code === "EPERM") try {
|
|
1840
|
+
if ((await (0, node_fs_promises.lstat)(path)).isDirectory()) return;
|
|
1841
|
+
} catch (cause) {
|
|
1842
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") return;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1433
1845
|
throw new _src_core.BrowserError(`Cannot remove lock directory: ${path}`, "BROWSER_JOURNEY_ACCESS", { path });
|
|
1434
1846
|
}
|
|
1435
1847
|
}
|
|
@@ -1714,7 +2126,7 @@ function createFileBrowserRunStore(options) {
|
|
|
1714
2126
|
}
|
|
1715
2127
|
/**
|
|
1716
2128
|
* Creates the browse server, which serves the browser vocabulary and the journey tools over the
|
|
1717
|
-
* Model Context Protocol on stdio and
|
|
2129
|
+
* Model Context Protocol on stdio and warms Chromium at server start.
|
|
1718
2130
|
*
|
|
1719
2131
|
* @param options - The root, the launch, the journeys' read-only switch, and the streams
|
|
1720
2132
|
* @returns A {@link BrowserMCPServerInterface} that reads nothing until `start()`
|
|
@@ -1724,7 +2136,7 @@ function createFileBrowserRunStore(options) {
|
|
|
1724
2136
|
* import { createBrowserMCPServer } from '@orkestrel/browser/server'
|
|
1725
2137
|
*
|
|
1726
2138
|
* const server = createBrowserMCPServer({ root: 'tmp/browsers', headless: true })
|
|
1727
|
-
* await server.start() //
|
|
2139
|
+
* await server.start() // resolves after leasing a connected browser
|
|
1728
2140
|
* await server.destroy()
|
|
1729
2141
|
* ```
|
|
1730
2142
|
*/
|
|
@@ -1800,6 +2212,15 @@ var Browser = class {
|
|
|
1800
2212
|
get pid() {
|
|
1801
2213
|
return this.#servingPid ?? this.#process?.pid;
|
|
1802
2214
|
}
|
|
2215
|
+
get endpoint() {
|
|
2216
|
+
return this.#endpoint;
|
|
2217
|
+
}
|
|
2218
|
+
async ping(options) {
|
|
2219
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
2220
|
+
const client = this.#client;
|
|
2221
|
+
if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
|
|
2222
|
+
await client.send("Browser.getVersion", void 0, options);
|
|
2223
|
+
}
|
|
1803
2224
|
async discover() {
|
|
1804
2225
|
const result = await this.#discoverCDP();
|
|
1805
2226
|
this.#emitter.emit("discover", result);
|
|
@@ -1925,7 +2346,7 @@ var Browser = class {
|
|
|
1925
2346
|
await this.#connectCDP(discovery.endpoint);
|
|
1926
2347
|
return;
|
|
1927
2348
|
}
|
|
1928
|
-
} else await this.#raceAbort(this.#assertPortFree());
|
|
2349
|
+
} else if (this.#options.cdp?.port !== void 0) await this.#raceAbort(this.#assertPortFree());
|
|
1929
2350
|
this.#assertNotAborted();
|
|
1930
2351
|
await this.#launch();
|
|
1931
2352
|
} catch (error) {
|
|
@@ -2354,6 +2775,7 @@ var Browser = class {
|
|
|
2354
2775
|
const process = this.#process;
|
|
2355
2776
|
if (process !== void 0) {
|
|
2356
2777
|
if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
|
|
2778
|
+
await this.#closeProcessPipe(process);
|
|
2357
2779
|
}
|
|
2358
2780
|
this.#process = void 0;
|
|
2359
2781
|
this.#servingPid = void 0;
|
|
@@ -2525,12 +2947,23 @@ var Browser = class {
|
|
|
2525
2947
|
return results[0] && (results[1] === true || final && results[1] === void 0);
|
|
2526
2948
|
}
|
|
2527
2949
|
async #terminate(process) {
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2950
|
+
try {
|
|
2951
|
+
if (process === void 0 || (process.exitCode !== null || process.signalCode !== null) && this.#inspectRemainder(process) === false) return;
|
|
2952
|
+
if (this.#signalProcess(process, "SIGTERM") === false) return;
|
|
2953
|
+
if (await this.#waitForTerminationWithin(process, 3e3)) return;
|
|
2954
|
+
if (this.#signalProcess(process, "SIGKILL") === false) return;
|
|
2955
|
+
if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
|
|
2956
|
+
throw new _src_core.BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: this.#servingPid ?? process.pid });
|
|
2957
|
+
} finally {
|
|
2958
|
+
await this.#closeProcessPipe(process);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
async #closeProcessPipe(process) {
|
|
2962
|
+
const stream = process?.stderr;
|
|
2963
|
+
if (stream === void 0 || stream === null || stream.closed) return;
|
|
2964
|
+
const closed = (0, node_events.once)(stream, "close");
|
|
2965
|
+
stream.destroy();
|
|
2966
|
+
await closed;
|
|
2534
2967
|
}
|
|
2535
2968
|
#finish() {
|
|
2536
2969
|
this.#unbindTransport();
|
|
@@ -2573,6 +3006,18 @@ exports.BROWSER_PROCESS_EXIT_CAUSE = BROWSER_PROCESS_EXIT_CAUSE;
|
|
|
2573
3006
|
exports.BROWSER_PROFILE_PREFIX = BROWSER_PROFILE_PREFIX;
|
|
2574
3007
|
exports.BROWSER_RUN_DIRECTORY = BROWSER_RUN_DIRECTORY;
|
|
2575
3008
|
exports.BROWSER_RUN_FILE = BROWSER_RUN_FILE;
|
|
3009
|
+
exports.BROWSER_SERVER_CRASH = BROWSER_SERVER_CRASH;
|
|
3010
|
+
exports.BROWSER_SERVER_EXHAUSTED = BROWSER_SERVER_EXHAUSTED;
|
|
3011
|
+
exports.BROWSER_SERVER_LAUNCH = BROWSER_SERVER_LAUNCH;
|
|
3012
|
+
exports.BROWSER_SERVER_OPTIONS = BROWSER_SERVER_OPTIONS;
|
|
3013
|
+
exports.BROWSER_SERVER_POOL_LIMIT = BROWSER_SERVER_POOL_LIMIT;
|
|
3014
|
+
exports.BROWSER_SERVER_POOL_SIZE = BROWSER_SERVER_POOL_SIZE;
|
|
3015
|
+
exports.BROWSER_SERVER_RECORD = BROWSER_SERVER_RECORD;
|
|
3016
|
+
exports.BROWSER_SERVER_RESTARTS = BROWSER_SERVER_RESTARTS;
|
|
3017
|
+
exports.BROWSER_SERVER_SWEEP = BROWSER_SERVER_SWEEP;
|
|
3018
|
+
exports.BROWSER_SERVER_TEARDOWN = BROWSER_SERVER_TEARDOWN;
|
|
3019
|
+
exports.BROWSER_SERVER_UNAVAILABLE = BROWSER_SERVER_UNAVAILABLE;
|
|
3020
|
+
exports.BROWSER_SERVER_UNRESOLVED = BROWSER_SERVER_UNRESOLVED;
|
|
2576
3021
|
exports.BROWSER_STORE_CACHE_DIRS = BROWSER_STORE_CACHE_DIRS;
|
|
2577
3022
|
exports.BROWSER_STORE_DEFAULT_DIRS = BROWSER_STORE_DEFAULT_DIRS;
|
|
2578
3023
|
exports.BROWSER_STORE_ENV_KEY = BROWSER_STORE_ENV_KEY;
|
|
@@ -2601,6 +3046,7 @@ exports.createBrowserWriter = createBrowserWriter;
|
|
|
2601
3046
|
exports.createCDPTransport = createCDPTransport;
|
|
2602
3047
|
exports.createFileBrowserJourneyStore = createFileBrowserJourneyStore;
|
|
2603
3048
|
exports.createFileBrowserRunStore = createFileBrowserRunStore;
|
|
3049
|
+
exports.describeBrowserServerLoss = describeBrowserServerLoss;
|
|
2604
3050
|
exports.fetchCDPTargets = fetchCDPTargets;
|
|
2605
3051
|
exports.findEnvOverrides = findEnvOverrides;
|
|
2606
3052
|
exports.findInstallPaths = findInstallPaths;
|
|
@@ -2614,7 +3060,9 @@ exports.launchBrowserProcess = launchBrowserProcess;
|
|
|
2614
3060
|
exports.normalizeExecutablePath = normalizeExecutablePath;
|
|
2615
3061
|
exports.parseBrowserEngine = parseBrowserEngine;
|
|
2616
3062
|
exports.parseBrowserLockEntry = parseBrowserLockEntry;
|
|
3063
|
+
exports.parseBrowserProfileRecord = parseBrowserProfileRecord;
|
|
2617
3064
|
exports.probePathNames = probePathNames;
|
|
3065
|
+
exports.probeProcess = probeProcess;
|
|
2618
3066
|
exports.readBrowserEndpoint = readBrowserEndpoint;
|
|
2619
3067
|
exports.readFirstLine = readFirstLine;
|
|
2620
3068
|
exports.removeBrowserProfile = removeBrowserProfile;
|