@orkestrel/browser 0.0.12 → 0.0.14
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/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +99 -25
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +41 -10
- package/dist/src/server/index.d.ts +41 -10
- package/dist/src/server/index.js +99 -26
- package/dist/src/server/index.js.map +1 -1
- package/package.json +18 -14
|
@@ -353,12 +353,35 @@ function probeAllPathNames(names, platform) {
|
|
|
353
353
|
for (const name of names) {
|
|
354
354
|
const result = (0, node_child_process.spawnSync)(finder, [name], { stdio: "pipe" });
|
|
355
355
|
if (result.status === 0) {
|
|
356
|
-
const
|
|
357
|
-
if (
|
|
356
|
+
const executable = readFirstLine(result.stdout.toString("utf-8"));
|
|
357
|
+
if (executable !== void 0) found.push(executable);
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
360
|
return found;
|
|
361
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Returns the first non-empty line of a command's output, without its
|
|
364
|
+
* surrounding whitespace.
|
|
365
|
+
*
|
|
366
|
+
* @remarks
|
|
367
|
+
* `where` reports every PATH match on its own CRLF-terminated line, so a
|
|
368
|
+
* carriage return left on the first line would travel into `existsSync` and
|
|
369
|
+
* `spawn` as part of the executable path.
|
|
370
|
+
*
|
|
371
|
+
* @param output - Raw decoded standard output of a command
|
|
372
|
+
* @returns The first line carrying text, or undefined when the output has none
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```ts
|
|
376
|
+
* readFirstLine('C:\\bin\\chrome.exe\r\nC:\\other\\chrome.exe\r\n') // 'C:\\bin\\chrome.exe'
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
function readFirstLine(output) {
|
|
380
|
+
for (const line of output.split(/\r?\n/)) {
|
|
381
|
+
const text = line.trim();
|
|
382
|
+
if (text.length > 0) return text;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
362
385
|
/** Build the default Playwright browser store base directories to search for a managed Chromium. */
|
|
363
386
|
function defaultStoreBases(env, platform) {
|
|
364
387
|
const bases = [];
|
|
@@ -399,6 +422,11 @@ function findAllInStore(base, platform) {
|
|
|
399
422
|
* @remarks
|
|
400
423
|
* POSIX launches own an isolated process group so lifecycle teardown can
|
|
401
424
|
* signal and await every Chromium subprocess without affecting the caller.
|
|
425
|
+
* Windows launches are not detached and own no group, so teardown there
|
|
426
|
+
* signals one process by identifier: the spawned process, or the process the
|
|
427
|
+
* launcher handed the endpoint to when that spawned process re-executed the
|
|
428
|
+
* browser and exited. Terminating a Chromium browser process takes its own
|
|
429
|
+
* subprocesses with it, so that single signal drains the tree.
|
|
402
430
|
*
|
|
403
431
|
* @param executable - Absolute path to the browser executable
|
|
404
432
|
* @param port - Port the browser exposes its CDP endpoint on
|
|
@@ -743,6 +771,7 @@ var Browser = class {
|
|
|
743
771
|
#endpoint;
|
|
744
772
|
#client;
|
|
745
773
|
#process;
|
|
774
|
+
#servingPid;
|
|
746
775
|
#profile;
|
|
747
776
|
#contexts = [];
|
|
748
777
|
#destroyed = false;
|
|
@@ -784,7 +813,7 @@ var Browser = class {
|
|
|
784
813
|
return this.#status === "connected";
|
|
785
814
|
}
|
|
786
815
|
get pid() {
|
|
787
|
-
return this.#process?.pid;
|
|
816
|
+
return this.#servingPid ?? this.#process?.pid;
|
|
788
817
|
}
|
|
789
818
|
async discover() {
|
|
790
819
|
const result = await this.#discoverCDP();
|
|
@@ -980,6 +1009,7 @@ var Browser = class {
|
|
|
980
1009
|
this.#unbindTransport();
|
|
981
1010
|
this.#unbindProcess();
|
|
982
1011
|
this.#process = void 0;
|
|
1012
|
+
this.#servingPid = void 0;
|
|
983
1013
|
const cleanup = this.#cleanupExitedProcess(process, contexts, client);
|
|
984
1014
|
this.#exitCleanup = cleanup;
|
|
985
1015
|
cleanup.catch((error) => this.#emitter.emit("error", error));
|
|
@@ -997,27 +1027,19 @@ var Browser = class {
|
|
|
997
1027
|
}
|
|
998
1028
|
#handleTransportLoss() {
|
|
999
1029
|
if (this.#destroyed || this.#status !== "connected") return;
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
this.#handleProcessExit();
|
|
1030
|
+
if (this.#process === void 0) {
|
|
1031
|
+
this.#confirmTransportLoss();
|
|
1003
1032
|
return;
|
|
1004
1033
|
}
|
|
1005
|
-
if (
|
|
1006
|
-
|
|
1034
|
+
if (!this.#alive()) {
|
|
1035
|
+
this.#handleProcessExit();
|
|
1007
1036
|
return;
|
|
1008
1037
|
}
|
|
1009
|
-
this.#confirmTransportLoss();
|
|
1038
|
+
setTimeout(() => this.#confirmTransportLoss(), 50);
|
|
1010
1039
|
}
|
|
1011
1040
|
#confirmTransportLoss() {
|
|
1012
1041
|
if (this.#destroyed || this.#status !== "connected") return;
|
|
1013
|
-
|
|
1014
|
-
if (process !== void 0 && (process.exitCode !== null || process.signalCode !== null)) {
|
|
1015
|
-
this.#handleProcessExit();
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
if (process?.pid !== void 0) try {
|
|
1019
|
-
process.kill(0);
|
|
1020
|
-
} catch {
|
|
1042
|
+
if (this.#process !== void 0 && !this.#alive()) {
|
|
1021
1043
|
this.#handleProcessExit();
|
|
1022
1044
|
return;
|
|
1023
1045
|
}
|
|
@@ -1054,6 +1076,7 @@ var Browser = class {
|
|
|
1054
1076
|
}
|
|
1055
1077
|
#bindProcess(process) {
|
|
1056
1078
|
this.#unbindProcess();
|
|
1079
|
+
if (this.#servingPid !== void 0) return;
|
|
1057
1080
|
process.once("exit", this.#onProcessExit);
|
|
1058
1081
|
if (process.exitCode !== null || process.signalCode !== null) queueMicrotask(this.#onProcessExit);
|
|
1059
1082
|
}
|
|
@@ -1178,6 +1201,7 @@ var Browser = class {
|
|
|
1178
1201
|
timeout: this.#timeout()
|
|
1179
1202
|
});
|
|
1180
1203
|
await this.#raceAbort(client.connect());
|
|
1204
|
+
await this.#takeEndpointOwner(process, client);
|
|
1181
1205
|
const connection = this.#options.profile === void 0 ? "launch" : "persistent";
|
|
1182
1206
|
this.#client = client;
|
|
1183
1207
|
this.#endpoint = endpoint;
|
|
@@ -1196,12 +1220,49 @@ var Browser = class {
|
|
|
1196
1220
|
await this.#terminate(process);
|
|
1197
1221
|
await this.#releaseProfile();
|
|
1198
1222
|
this.#process = void 0;
|
|
1223
|
+
this.#servingPid = void 0;
|
|
1199
1224
|
this.#client = void 0;
|
|
1200
1225
|
this.#endpoint = void 0;
|
|
1201
1226
|
this.#owned = void 0;
|
|
1202
1227
|
throw error;
|
|
1203
1228
|
}
|
|
1204
1229
|
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Takes over the process serving the CDP endpoint when the spawned child is
|
|
1232
|
+
* no longer it.
|
|
1233
|
+
*
|
|
1234
|
+
* @remarks
|
|
1235
|
+
* A Windows launcher — Microsoft Edge — re-executes the browser with the
|
|
1236
|
+
* same `--remote-debugging-port` and exits 0 before the endpoint answers, so
|
|
1237
|
+
* the child this instance spawned parents the real tree without being part
|
|
1238
|
+
* of it. Chromium names the process behind the endpoint as the `browser`
|
|
1239
|
+
* entry of `SystemInfo.getProcessInfo`, which identifies it without a
|
|
1240
|
+
* platform branch. An endpoint that names no such process cannot be owned,
|
|
1241
|
+
* so the launch closes it and fails rather than leaking it.
|
|
1242
|
+
*/
|
|
1243
|
+
async #takeEndpointOwner(process, client) {
|
|
1244
|
+
if (process.exitCode === null && process.signalCode === null) return;
|
|
1245
|
+
let result;
|
|
1246
|
+
try {
|
|
1247
|
+
result = await client.send("SystemInfo.getProcessInfo");
|
|
1248
|
+
} catch {
|
|
1249
|
+
result = void 0;
|
|
1250
|
+
}
|
|
1251
|
+
let pid;
|
|
1252
|
+
if ((0, _orkestrel_contract.isRecord)(result) && (0, _orkestrel_contract.isArray)(result["processInfo"])) for (const entry of result["processInfo"]) {
|
|
1253
|
+
if (!(0, _orkestrel_contract.isRecord)(entry) || entry["type"] !== "browser" || !(0, _orkestrel_contract.isInteger)(entry["id"])) continue;
|
|
1254
|
+
pid = entry["id"];
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
if (pid === void 0) {
|
|
1258
|
+
await this.#closeRemote(client);
|
|
1259
|
+
throw new BrowserConnectionError("The browser launcher exited without naming the process serving its CDP endpoint", {
|
|
1260
|
+
executable: this.#options.executable,
|
|
1261
|
+
pid: process.pid
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
this.#servingPid = pid;
|
|
1265
|
+
}
|
|
1205
1266
|
async #waitForLaunch(process, executable, args) {
|
|
1206
1267
|
const context = {
|
|
1207
1268
|
executable,
|
|
@@ -1213,6 +1274,7 @@ var Browser = class {
|
|
|
1213
1274
|
const exited = (0, node_events.once)(process, "exit", { signal }).then((values) => {
|
|
1214
1275
|
const code = (0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null;
|
|
1215
1276
|
const exitSignal = (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null;
|
|
1277
|
+
if (code === 0 && exitSignal === null) return ready;
|
|
1216
1278
|
throw new BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
|
|
1217
1279
|
});
|
|
1218
1280
|
try {
|
|
@@ -1275,6 +1337,7 @@ var Browser = class {
|
|
|
1275
1337
|
this.#contexts = [];
|
|
1276
1338
|
await this.#terminate(this.#process);
|
|
1277
1339
|
this.#process = void 0;
|
|
1340
|
+
this.#servingPid = void 0;
|
|
1278
1341
|
terminated = true;
|
|
1279
1342
|
} finally {
|
|
1280
1343
|
const contexts = this.#contexts;
|
|
@@ -1304,6 +1367,7 @@ var Browser = class {
|
|
|
1304
1367
|
if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
|
|
1305
1368
|
}
|
|
1306
1369
|
this.#process = void 0;
|
|
1370
|
+
this.#servingPid = void 0;
|
|
1307
1371
|
terminated = true;
|
|
1308
1372
|
} finally {
|
|
1309
1373
|
const contexts = this.#contexts;
|
|
@@ -1415,31 +1479,39 @@ var Browser = class {
|
|
|
1415
1479
|
const pid = process.pid;
|
|
1416
1480
|
return globalThis.process.platform !== "win32" && pid !== void 0 && pid > 0;
|
|
1417
1481
|
}
|
|
1418
|
-
#
|
|
1419
|
-
|
|
1482
|
+
#alive() {
|
|
1483
|
+
const process = this.#process;
|
|
1484
|
+
if (process === void 0) return false;
|
|
1485
|
+
if (this.#servingPid !== void 0) return this.#signalProcess(process, 0) !== false;
|
|
1486
|
+
return process.exitCode === null && process.signalCode === null;
|
|
1487
|
+
}
|
|
1488
|
+
#inspectRemainder(process) {
|
|
1489
|
+
if (!this.#hasProcessGroup(process) && this.#servingPid === void 0) return false;
|
|
1420
1490
|
return this.#signalProcess(process, 0);
|
|
1421
1491
|
}
|
|
1422
1492
|
#signalProcess(process, signal) {
|
|
1423
1493
|
const pid = process.pid;
|
|
1494
|
+
const serving = this.#servingPid;
|
|
1424
1495
|
try {
|
|
1425
1496
|
if (this.#hasProcessGroup(process) && pid !== void 0) return globalThis.process.kill(-pid, signal);
|
|
1497
|
+
if (serving !== void 0) return globalThis.process.kill(serving, signal);
|
|
1426
1498
|
return process.kill(signal);
|
|
1427
1499
|
} catch (error) {
|
|
1428
1500
|
const code = (0, _orkestrel_contract.isError)(error) && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : void 0;
|
|
1429
1501
|
if (code === "ESRCH") return false;
|
|
1430
1502
|
if (signal === 0) return code === "EPERM" ? true : void 0;
|
|
1431
1503
|
throw new BrowserConnectionError("Failed to signal the browser process", {
|
|
1432
|
-
pid,
|
|
1504
|
+
pid: serving ?? pid,
|
|
1433
1505
|
signal,
|
|
1434
1506
|
cause: error
|
|
1435
1507
|
});
|
|
1436
1508
|
}
|
|
1437
1509
|
}
|
|
1438
|
-
async #
|
|
1510
|
+
async #waitForRemainderWithin(process, timeout) {
|
|
1439
1511
|
const deadline = performance.now() + timeout;
|
|
1440
1512
|
let confirmed = false;
|
|
1441
1513
|
while (true) {
|
|
1442
|
-
const alive = this.#
|
|
1514
|
+
const alive = this.#inspectRemainder(process);
|
|
1443
1515
|
if (alive === false) return true;
|
|
1444
1516
|
if (alive === true) confirmed = true;
|
|
1445
1517
|
const remaining = deadline - performance.now();
|
|
@@ -1449,17 +1521,17 @@ var Browser = class {
|
|
|
1449
1521
|
}
|
|
1450
1522
|
async #waitForTerminationWithin(process, timeout, final = false) {
|
|
1451
1523
|
const exited = this.#waitForProcessWithin(process, timeout);
|
|
1452
|
-
const drained = this.#
|
|
1524
|
+
const drained = this.#waitForRemainderWithin(process, timeout);
|
|
1453
1525
|
const results = await Promise.all([exited, drained]);
|
|
1454
1526
|
return results[0] && (results[1] === true || final && results[1] === void 0);
|
|
1455
1527
|
}
|
|
1456
1528
|
async #terminate(process) {
|
|
1457
|
-
if (process === void 0 || (process.exitCode !== null || process.signalCode !== null) && this.#
|
|
1529
|
+
if (process === void 0 || (process.exitCode !== null || process.signalCode !== null) && this.#inspectRemainder(process) === false) return;
|
|
1458
1530
|
if (this.#signalProcess(process, "SIGTERM") === false) return;
|
|
1459
1531
|
if (await this.#waitForTerminationWithin(process, 3e3)) return;
|
|
1460
1532
|
if (this.#signalProcess(process, "SIGKILL") === false) return;
|
|
1461
1533
|
if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
|
|
1462
|
-
throw new BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: process.pid });
|
|
1534
|
+
throw new BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: this.#servingPid ?? process.pid });
|
|
1463
1535
|
}
|
|
1464
1536
|
#finish() {
|
|
1465
1537
|
this.#unbindTransport();
|
|
@@ -1470,6 +1542,7 @@ var Browser = class {
|
|
|
1470
1542
|
this.#endpoint = void 0;
|
|
1471
1543
|
this.#client = void 0;
|
|
1472
1544
|
this.#process = void 0;
|
|
1545
|
+
this.#servingPid = void 0;
|
|
1473
1546
|
this.#exitCleanup = void 0;
|
|
1474
1547
|
this.#contexts = [];
|
|
1475
1548
|
this.#emitter.emit("destroy");
|
|
@@ -1530,6 +1603,7 @@ exports.normalizeExecutablePath = normalizeExecutablePath;
|
|
|
1530
1603
|
exports.parseBrowserEngine = parseBrowserEngine;
|
|
1531
1604
|
exports.probeAllPathNames = probeAllPathNames;
|
|
1532
1605
|
exports.probePathNames = probePathNames;
|
|
1606
|
+
exports.readFirstLine = readFirstLine;
|
|
1533
1607
|
exports.removeBrowserProfile = removeBrowserProfile;
|
|
1534
1608
|
exports.waitForCDPReady = waitForCDPReady;
|
|
1535
1609
|
exports.windowsRoots = windowsRoots;
|