@mind-harness/mh-browser 0.2.5 → 0.2.7
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/extension/manifest.json +1 -1
- package/extension/welcome.html +4 -0
- package/extension/welcome.js +10 -0
- package/lib/client.cjs +799 -254
- package/lib/index.js +2314 -1461
- package/lib/types/call.d.ts +23 -0
- package/lib/types/card.d.ts +1 -2
- package/lib/types/client.d.ts +20 -17
- package/lib/types/config.d.ts +25 -10
- package/lib/types/index.d.ts +23 -2
- package/lib/types/locales.d.ts +28 -2
- package/lib/types/location.d.ts +14 -0
- package/lib/types/location.js +3 -0
- package/lib/types/routes.d.ts +43 -0
- package/lib/types/row.d.ts +2 -4
- package/lib/types/service/browser.d.ts +16 -5
- package/lib/types/service/control.d.ts +5 -0
- package/lib/types/service/extension.d.ts +7 -0
- package/lib/types/service/launch-plan.d.ts +30 -0
- package/lib/types/service/locate.d.ts +81 -0
- package/lib/types/service/profile.d.ts +11 -0
- package/lib/types/service/service.d.ts +40 -6
- package/lib/types/service/test-hooks.d.ts +3 -0
- package/lib/types/settings-form.d.ts +142 -0
- package/lib/types/settings.d.ts +124 -0
- package/lib/types/typeprobe.d.ts +49 -0
- package/lib/types/types.d.ts +13 -1
- package/package.json +23 -30
- package/THIRD-PARTY-NOTICES +0 -23
- package/lib/typert.host.d.ts +0 -3
- package/lib/typert.host.js +0 -378
- package/lib/typert.remote-client.d.ts +0 -24
- package/lib/typert.remote-client.js +0 -52
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
1
|
+
import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, win32 } from "node:path";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -6,21 +6,25 @@ import { HarnessError, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
|
6
6
|
import { assertSupportedJsonSchema, defineTool, validateJsonSchemaValue } from "@deepseek-ai/dsh-tools";
|
|
7
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import { parse } from "yaml";
|
|
9
|
-
import { constants, readFileSync } from "node:fs";
|
|
10
|
-
import { lstat, mkdir, open, readFile, readlink, realpath, unlink, writeFile } from "node:fs/promises";
|
|
9
|
+
import { accessSync, constants, readFileSync, realpathSync, statSync } from "node:fs";
|
|
10
|
+
import { access, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
11
11
|
import { z as z$1 } from "zod";
|
|
12
|
-
import {
|
|
12
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
13
13
|
import { chromium } from "playwright";
|
|
14
|
+
import { execFile } from "node:child_process";
|
|
15
|
+
import { homedir } from "node:os";
|
|
14
16
|
import { fileURLToPath } from "node:url";
|
|
15
17
|
import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
|
|
16
18
|
//#region lib/types/config.js
|
|
17
19
|
const browserConfig = z.object({
|
|
18
20
|
presence: z.object({ maxBytes: z.number().step(1).min(1).required().description("Most bytes of page text one presence reminder may carry.") }).required().description("Pages kept in context across steps."),
|
|
19
21
|
browser: z.union([
|
|
22
|
+
"auto",
|
|
20
23
|
"chrome",
|
|
21
24
|
"msedge",
|
|
22
25
|
"chromium"
|
|
23
|
-
]).default("
|
|
26
|
+
]).default("auto").volatile().description("Which browser to drive. auto keeps the one that launched last, or Chrome and then Edge. chromium is the one Playwright downloads. A new value restarts the browser once it is idle."),
|
|
27
|
+
executablePath: z.string().volatile().description("Absolute path of the browser executable. `~` is expanded. Leave empty to locate one. A new value restarts the browser once it is idle."),
|
|
24
28
|
profileDir: z.string().description("Browser profile directory, kept between runs. Defaults to mh/profiles/<dsh profile>/browser under the dsh home."),
|
|
25
29
|
headless: z.boolean().default(false).description("Run the browser without a window.")
|
|
26
30
|
});
|
|
@@ -32,16 +36,22 @@ const browserConfig = z.object({
|
|
|
32
36
|
function defaultProfileDir(profile, home = resolveDshHome()) {
|
|
33
37
|
return profile === void 0 ? join(home, "mh", "browser") : join(home, "mh", "profiles", profile.name, "browser");
|
|
34
38
|
}
|
|
39
|
+
/** One subdirectory of the default profile root per browser, so logins do not cross browsers. */
|
|
40
|
+
function profileDirFor(root, browser) {
|
|
41
|
+
return join(root, browser);
|
|
42
|
+
}
|
|
35
43
|
//#endregion
|
|
36
44
|
//#region lib/types/types.js
|
|
37
45
|
var MhBrowserError = class extends Error {
|
|
38
46
|
code;
|
|
39
47
|
page;
|
|
40
|
-
|
|
48
|
+
notFound;
|
|
49
|
+
constructor(code, message, page, notFound) {
|
|
41
50
|
super(message);
|
|
42
51
|
this.name = "MhBrowserError";
|
|
43
52
|
this.code = code;
|
|
44
53
|
if (page !== void 0) this.page = page;
|
|
54
|
+
if (notFound !== void 0) this.notFound = notFound;
|
|
45
55
|
}
|
|
46
56
|
};
|
|
47
57
|
/** Owner of the person's own window. It belongs to no Session. */
|
|
@@ -554,13 +564,13 @@ function pageAttrs(view, screenshot) {
|
|
|
554
564
|
screenshot === void 0 ? "" : attr$1("screenshot", screenshot)
|
|
555
565
|
].join("");
|
|
556
566
|
}
|
|
557
|
-
const BODY_LIMIT = 2e5;
|
|
567
|
+
const BODY_LIMIT$1 = 2e5;
|
|
558
568
|
function pageBody(view) {
|
|
559
569
|
const representation = view.representation;
|
|
560
570
|
if (view.mediaType === void 0 || representation.kind !== "snapshot") return representation.text;
|
|
561
571
|
if (view.body !== void 0) {
|
|
562
|
-
if (view.body.length <= BODY_LIMIT) return view.body;
|
|
563
|
-
return `${view.body.slice(0, BODY_LIMIT)}\nCut at ${String(BODY_LIMIT)} characters; use browser_download to save the whole file into the workspace.`;
|
|
572
|
+
if (view.body.length <= BODY_LIMIT$1) return view.body;
|
|
573
|
+
return `${view.body.slice(0, BODY_LIMIT$1)}\nCut at ${String(BODY_LIMIT$1)} characters; use browser_download to save the whole file into the workspace.`;
|
|
564
574
|
}
|
|
565
575
|
if (view.status !== void 0) return `The site answered HTTP ${String(view.status)}; the ${view.mediaType} body is not shown.`;
|
|
566
576
|
return "Not an HTML page; use browser_download to save it into the workspace.";
|
|
@@ -2367,6 +2377,302 @@ function plainObject(keys) {
|
|
|
2367
2377
|
return z$1.custom((value) => value !== null && typeof value === "object" && keys.every((key) => key in value));
|
|
2368
2378
|
}
|
|
2369
2379
|
//#endregion
|
|
2380
|
+
//#region lib/types/routes.js
|
|
2381
|
+
/** Requests larger than this are refused before the service method runs. */
|
|
2382
|
+
const BODY_LIMIT = 65536;
|
|
2383
|
+
const PREFIX = "/mh-browser/api";
|
|
2384
|
+
var RouteFailure = class extends Error {
|
|
2385
|
+
status;
|
|
2386
|
+
body;
|
|
2387
|
+
constructor(status, body) {
|
|
2388
|
+
super(body.message);
|
|
2389
|
+
this.name = "RouteFailure";
|
|
2390
|
+
this.status = status;
|
|
2391
|
+
this.body = body;
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
function method(spec) {
|
|
2395
|
+
return {
|
|
2396
|
+
verb: spec.verb,
|
|
2397
|
+
input: spec.input,
|
|
2398
|
+
run: (input, signal) => spec.run(input, signal)
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
const locateInput = z$1.object({ browser: z$1.enum([
|
|
2402
|
+
"auto",
|
|
2403
|
+
"chrome",
|
|
2404
|
+
"msedge",
|
|
2405
|
+
"chromium"
|
|
2406
|
+
]) }).strict();
|
|
2407
|
+
const revealInput = z$1.object({ sessionId: z$1.string() }).strict();
|
|
2408
|
+
/**
|
|
2409
|
+
* Routes for the browser client. A composition without `webServer` never
|
|
2410
|
+
* starts this plugin; the browser tools do not depend on it.
|
|
2411
|
+
*/
|
|
2412
|
+
const browserRoutesPlugin = {
|
|
2413
|
+
name: "mh-browser-routes",
|
|
2414
|
+
inject: [
|
|
2415
|
+
"mhBrowser",
|
|
2416
|
+
"webServer",
|
|
2417
|
+
"connection"
|
|
2418
|
+
],
|
|
2419
|
+
apply(inner) {
|
|
2420
|
+
registerBrowserRoutes(inner);
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
function registerBrowserRoutes(ctx) {
|
|
2424
|
+
registerApiRoutes(ctx, {
|
|
2425
|
+
prefix: PREFIX,
|
|
2426
|
+
packageName: "mh-browser",
|
|
2427
|
+
methods: {
|
|
2428
|
+
"locate-for": method({
|
|
2429
|
+
verb: "POST",
|
|
2430
|
+
input: locateInput,
|
|
2431
|
+
run: (input, signal) => ctx.mhBrowser.locateFor(input.browser, signal)
|
|
2432
|
+
}),
|
|
2433
|
+
"reveal-handover": method({
|
|
2434
|
+
verb: "POST",
|
|
2435
|
+
input: revealInput,
|
|
2436
|
+
run: (input, signal) => ctx.mhBrowser.revealHandover(input.sessionId, signal)
|
|
2437
|
+
})
|
|
2438
|
+
},
|
|
2439
|
+
mapError: browserFailure
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
function registerApiRoutes(host, routes) {
|
|
2443
|
+
host.effect(() => host.webServer.register({
|
|
2444
|
+
kind: "prefix",
|
|
2445
|
+
path: routes.prefix,
|
|
2446
|
+
handler: (req, res) => handle(req, res, host, routes)
|
|
2447
|
+
}));
|
|
2448
|
+
}
|
|
2449
|
+
async function handle(req, res, host, routes) {
|
|
2450
|
+
const name = routes.packageName;
|
|
2451
|
+
try {
|
|
2452
|
+
const admission = host.connection.admit({ headers: req.headers });
|
|
2453
|
+
if ("rejection" in admission) {
|
|
2454
|
+
const status = admission.rejection;
|
|
2455
|
+
writeJson(res, status, fail(name, status === 401 ? "unadmitted" : "forbidden", status === 401 ? "sign in required" : "forbidden"), name);
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
const found = methodOf(routes, segmentOf(req.url, routes.prefix));
|
|
2459
|
+
if (found === void 0) {
|
|
2460
|
+
writeJson(res, 404, fail(name, "not-found", "no such route"), name);
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
if (req.method !== found.verb) {
|
|
2464
|
+
writeJson(res, 405, fail(name, "method-not-allowed", "method not allowed"), name, { allow: found.verb });
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
const controller = new AbortController();
|
|
2468
|
+
const onClose = () => {
|
|
2469
|
+
if (!res.writableEnded) controller.abort();
|
|
2470
|
+
};
|
|
2471
|
+
res.on("close", onClose);
|
|
2472
|
+
if (req.destroyed || res.destroyed) controller.abort();
|
|
2473
|
+
try {
|
|
2474
|
+
if (signalAborted(controller, res, name)) return;
|
|
2475
|
+
const input = found.verb === "GET" ? queryInput(req.url) : await readJson(req, name);
|
|
2476
|
+
if (signalAborted(controller, res, name)) return;
|
|
2477
|
+
const parsed = found.input.safeParse(input);
|
|
2478
|
+
if (!parsed.success) {
|
|
2479
|
+
writeJson(res, 400, {
|
|
2480
|
+
ok: false,
|
|
2481
|
+
error: {
|
|
2482
|
+
code: `${name}/invalid-input`,
|
|
2483
|
+
message: "invalid input",
|
|
2484
|
+
details: { issues: parsed.error.issues.map((issue) => ({
|
|
2485
|
+
path: issue.path.map((part) => String(part)).join("."),
|
|
2486
|
+
message: issue.message
|
|
2487
|
+
})) }
|
|
2488
|
+
}
|
|
2489
|
+
}, name);
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
const value = await found.run(parsed.data, controller.signal);
|
|
2493
|
+
writeJson(res, 200, {
|
|
2494
|
+
ok: true,
|
|
2495
|
+
value: value === void 0 ? null : value
|
|
2496
|
+
}, name);
|
|
2497
|
+
} finally {
|
|
2498
|
+
res.removeListener("close", onClose);
|
|
2499
|
+
}
|
|
2500
|
+
} catch (error) {
|
|
2501
|
+
const failed = error instanceof RouteFailure ? {
|
|
2502
|
+
status: error.status,
|
|
2503
|
+
error: error.body
|
|
2504
|
+
} : routes.mapError(error);
|
|
2505
|
+
writeJson(res, failed.status, {
|
|
2506
|
+
ok: false,
|
|
2507
|
+
error: failed.error
|
|
2508
|
+
}, name);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
/** Names on `Object.prototype` are not routes. */
|
|
2512
|
+
function methodOf(routes, segment) {
|
|
2513
|
+
if (segment === void 0 || segment === "" || !Object.hasOwn(routes.methods, segment)) return;
|
|
2514
|
+
return routes.methods[segment];
|
|
2515
|
+
}
|
|
2516
|
+
function signalAborted(controller, res, packageName) {
|
|
2517
|
+
if (!controller.signal.aborted) return false;
|
|
2518
|
+
writeJson(res, 400, fail(packageName, "cancelled", "cancelled"), packageName);
|
|
2519
|
+
return true;
|
|
2520
|
+
}
|
|
2521
|
+
function browserFailure(error) {
|
|
2522
|
+
if (error instanceof MhBrowserError) {
|
|
2523
|
+
if (error.code === "INTERRUPTED") return {
|
|
2524
|
+
status: 400,
|
|
2525
|
+
error: {
|
|
2526
|
+
code: "mh-browser/cancelled",
|
|
2527
|
+
message: error.message
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
return {
|
|
2531
|
+
status: error.code === "INVALID_ARGS" || error.code === "INVALID_INPUT" ? 400 : 500,
|
|
2532
|
+
error: {
|
|
2533
|
+
code: error.code,
|
|
2534
|
+
message: error.message
|
|
2535
|
+
}
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
return {
|
|
2539
|
+
status: 500,
|
|
2540
|
+
error: {
|
|
2541
|
+
code: "mh-browser/failed",
|
|
2542
|
+
message: error instanceof Error ? error.message : "request failed"
|
|
2543
|
+
}
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
function fail(packageName, code, message) {
|
|
2547
|
+
return {
|
|
2548
|
+
ok: false,
|
|
2549
|
+
error: {
|
|
2550
|
+
code: `${packageName}/${code}`,
|
|
2551
|
+
message
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
function segmentOf(url, prefix) {
|
|
2556
|
+
let pathname;
|
|
2557
|
+
try {
|
|
2558
|
+
pathname = new URL(url ?? "/", "http://127.0.0.1").pathname;
|
|
2559
|
+
} catch {
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
if (pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
|
|
2563
|
+
if (pathname === prefix) return "";
|
|
2564
|
+
const head = `${prefix}/`;
|
|
2565
|
+
if (!pathname.startsWith(head)) return void 0;
|
|
2566
|
+
const rest = pathname.slice(head.length);
|
|
2567
|
+
if (rest === "" || rest.includes("/")) return void 0;
|
|
2568
|
+
try {
|
|
2569
|
+
return decodeURIComponent(rest);
|
|
2570
|
+
} catch {
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
function queryInput(url) {
|
|
2575
|
+
const params = new URL(url ?? "/", "http://127.0.0.1").searchParams;
|
|
2576
|
+
const input = {};
|
|
2577
|
+
for (const [key, value] of params) input[key] = value;
|
|
2578
|
+
return input;
|
|
2579
|
+
}
|
|
2580
|
+
async function readJson(req, packageName) {
|
|
2581
|
+
const chunks = await readChunks(req);
|
|
2582
|
+
if (chunks === void 0) throw new RouteFailure(400, {
|
|
2583
|
+
code: `${packageName}/invalid-input`,
|
|
2584
|
+
message: "request is too large"
|
|
2585
|
+
});
|
|
2586
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
2587
|
+
if (text.trim() === "") return {};
|
|
2588
|
+
try {
|
|
2589
|
+
return JSON.parse(text);
|
|
2590
|
+
} catch {
|
|
2591
|
+
throw new RouteFailure(400, {
|
|
2592
|
+
code: `${packageName}/invalid-input`,
|
|
2593
|
+
message: "request is not json"
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
/** `undefined` means the body is over the limit. The socket stays open. */
|
|
2598
|
+
function readChunks(req) {
|
|
2599
|
+
const declared = declaredLength(req);
|
|
2600
|
+
if (declared !== void 0 && declared > BODY_LIMIT) {
|
|
2601
|
+
req.on("error", () => void 0);
|
|
2602
|
+
req.resume();
|
|
2603
|
+
return Promise.resolve(void 0);
|
|
2604
|
+
}
|
|
2605
|
+
const chunks = [];
|
|
2606
|
+
let size = 0;
|
|
2607
|
+
return new Promise((resolve, reject) => {
|
|
2608
|
+
const stop = () => {
|
|
2609
|
+
req.off("data", onData);
|
|
2610
|
+
req.off("end", onEnd);
|
|
2611
|
+
req.off("error", onError);
|
|
2612
|
+
};
|
|
2613
|
+
const onData = (chunk) => {
|
|
2614
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2615
|
+
size += buffer.length;
|
|
2616
|
+
if (size > BODY_LIMIT) {
|
|
2617
|
+
stop();
|
|
2618
|
+
req.on("error", () => void 0);
|
|
2619
|
+
req.pause();
|
|
2620
|
+
req.resume();
|
|
2621
|
+
resolve(void 0);
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
chunks.push(buffer);
|
|
2625
|
+
};
|
|
2626
|
+
const onEnd = () => {
|
|
2627
|
+
stop();
|
|
2628
|
+
resolve(chunks);
|
|
2629
|
+
};
|
|
2630
|
+
const onError = (error) => {
|
|
2631
|
+
stop();
|
|
2632
|
+
reject(error);
|
|
2633
|
+
};
|
|
2634
|
+
req.on("data", onData);
|
|
2635
|
+
req.on("end", onEnd);
|
|
2636
|
+
req.on("error", onError);
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
function declaredLength(req) {
|
|
2640
|
+
const raw = req.headers["content-length"];
|
|
2641
|
+
if (typeof raw !== "string" || !/^\d+$/.test(raw)) return void 0;
|
|
2642
|
+
const length = Number(raw);
|
|
2643
|
+
return Number.isSafeInteger(length) ? length : void 0;
|
|
2644
|
+
}
|
|
2645
|
+
function writeJson(res, status, body, packageName, extra) {
|
|
2646
|
+
if (res.writableEnded || res.destroyed) return;
|
|
2647
|
+
let payload;
|
|
2648
|
+
let nextStatus = status;
|
|
2649
|
+
let nextExtra = extra;
|
|
2650
|
+
try {
|
|
2651
|
+
payload = JSON.stringify(body);
|
|
2652
|
+
} catch {
|
|
2653
|
+
payload = JSON.stringify({
|
|
2654
|
+
ok: false,
|
|
2655
|
+
error: {
|
|
2656
|
+
code: `${packageName}/failed`,
|
|
2657
|
+
message: "request failed"
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
nextStatus = 500;
|
|
2661
|
+
nextExtra = void 0;
|
|
2662
|
+
}
|
|
2663
|
+
const headers = {
|
|
2664
|
+
"content-type": "application/json; charset=utf-8",
|
|
2665
|
+
"cache-control": "no-store"
|
|
2666
|
+
};
|
|
2667
|
+
if (nextExtra !== void 0) Object.assign(headers, nextExtra);
|
|
2668
|
+
try {
|
|
2669
|
+
res.writeHead(nextStatus, headers);
|
|
2670
|
+
res.end(payload);
|
|
2671
|
+
} catch {
|
|
2672
|
+
res.destroy();
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
//#endregion
|
|
2370
2676
|
//#region lib/types/presence/book.js
|
|
2371
2677
|
/** In-memory presence set for one process. The log is the source of a set that outlives the process. */
|
|
2372
2678
|
var PresenceBook = class {
|
|
@@ -3069,16 +3375,309 @@ function sameOrigin$1(left, right) {
|
|
|
3069
3375
|
}
|
|
3070
3376
|
}
|
|
3071
3377
|
//#endregion
|
|
3072
|
-
//#region lib/types/service/
|
|
3073
|
-
const
|
|
3074
|
-
chrome:
|
|
3075
|
-
|
|
3076
|
-
|
|
3378
|
+
//#region lib/types/service/locate.js
|
|
3379
|
+
const MAC_APP = {
|
|
3380
|
+
chrome: [
|
|
3381
|
+
"Google Chrome.app",
|
|
3382
|
+
"Contents",
|
|
3383
|
+
"MacOS",
|
|
3384
|
+
"Google Chrome"
|
|
3385
|
+
],
|
|
3386
|
+
msedge: [
|
|
3387
|
+
"Microsoft Edge.app",
|
|
3388
|
+
"Contents",
|
|
3389
|
+
"MacOS",
|
|
3390
|
+
"Microsoft Edge"
|
|
3391
|
+
]
|
|
3392
|
+
};
|
|
3393
|
+
const LINUX_PATH = {
|
|
3394
|
+
chrome: "/opt/google/chrome/chrome",
|
|
3395
|
+
msedge: "/opt/microsoft/msedge/msedge"
|
|
3396
|
+
};
|
|
3397
|
+
const WIN_SUFFIX = {
|
|
3398
|
+
chrome: "Google\\Chrome\\Application\\chrome.exe",
|
|
3399
|
+
msedge: "Microsoft\\Edge\\Application\\msedge.exe"
|
|
3400
|
+
};
|
|
3401
|
+
const MAC_BUNDLE = {
|
|
3402
|
+
chrome: {
|
|
3403
|
+
id: "com.google.Chrome",
|
|
3404
|
+
binary: "Google Chrome"
|
|
3405
|
+
},
|
|
3406
|
+
msedge: {
|
|
3407
|
+
id: "com.microsoft.edgemac",
|
|
3408
|
+
binary: "Microsoft Edge"
|
|
3409
|
+
}
|
|
3410
|
+
};
|
|
3411
|
+
const LINUX_NAMES = {
|
|
3412
|
+
chrome: ["google-chrome-stable", "google-chrome"],
|
|
3413
|
+
msedge: ["microsoft-edge-stable", "microsoft-edge"]
|
|
3414
|
+
};
|
|
3415
|
+
const WIN_EXE = {
|
|
3416
|
+
chrome: "chrome.exe",
|
|
3417
|
+
msedge: "msedge.exe"
|
|
3418
|
+
};
|
|
3419
|
+
const TITLE = {
|
|
3420
|
+
chrome: "Google Chrome",
|
|
3421
|
+
msedge: "Microsoft Edge"
|
|
3422
|
+
};
|
|
3423
|
+
const DISCOVER_MS = 2e3;
|
|
3424
|
+
/** The fixed paths checked for `browser` on `env`, in the order they are tried. */
|
|
3425
|
+
function candidatePaths(browser, env) {
|
|
3426
|
+
if (env.platform === "darwin") return ["/Applications", posix.join(env.home, "Applications")].map((root) => posix.join(root, ...MAC_APP[browser]));
|
|
3427
|
+
if (env.platform === "linux") return [LINUX_PATH[browser]];
|
|
3428
|
+
if (env.platform !== "win32") return [];
|
|
3429
|
+
const drive = env.env.HOMEDRIVE;
|
|
3430
|
+
const roots = [
|
|
3431
|
+
env.env.LOCALAPPDATA,
|
|
3432
|
+
env.env.PROGRAMFILES,
|
|
3433
|
+
env.env["PROGRAMFILES(X86)"],
|
|
3434
|
+
typeof drive === "string" && drive !== "" ? `${drive}\\Program Files` : void 0,
|
|
3435
|
+
typeof drive === "string" && drive !== "" ? `${drive}\\Program Files (x86)` : void 0
|
|
3436
|
+
];
|
|
3437
|
+
const suffix = WIN_SUFFIX[browser];
|
|
3438
|
+
const paths = [];
|
|
3439
|
+
for (const root of roots) {
|
|
3440
|
+
if (root === void 0 || root === "") continue;
|
|
3441
|
+
paths.push(win32.join(root, suffix));
|
|
3442
|
+
}
|
|
3443
|
+
return paths;
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* `configured` is used as given: a path that cannot be run is a failure, and
|
|
3447
|
+
* nothing else is searched. `~` expands to `env.home`.
|
|
3448
|
+
*/
|
|
3449
|
+
function checkConfigured(configured, env) {
|
|
3450
|
+
const path = expandHome(configured, env.home, env.platform);
|
|
3451
|
+
if (!(env.platform === "win32" ? win32 : posix).isAbsolute(path)) return {
|
|
3452
|
+
ok: false,
|
|
3453
|
+
path,
|
|
3454
|
+
code: "not-absolute"
|
|
3455
|
+
};
|
|
3456
|
+
const access = env.access(path);
|
|
3457
|
+
if (access !== "ok") return {
|
|
3458
|
+
ok: false,
|
|
3459
|
+
path,
|
|
3460
|
+
code: access.code
|
|
3461
|
+
};
|
|
3462
|
+
return {
|
|
3463
|
+
ok: true,
|
|
3464
|
+
path
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
/** Failure for a path the configuration names. */
|
|
3468
|
+
function configuredPathError(path, code) {
|
|
3469
|
+
if (code === "not-absolute") return /* @__PURE__ */ new Error(`The configured browser path must be absolute: ${path}`);
|
|
3470
|
+
return /* @__PURE__ */ new Error(`The configured browser path cannot be run (${code}): ${path}`);
|
|
3471
|
+
}
|
|
3472
|
+
/** Chrome and Edge were both missed. The message asks for Chrome only. */
|
|
3473
|
+
var BrowserNotFoundError = class extends Error {
|
|
3474
|
+
notFound;
|
|
3475
|
+
constructor(misses) {
|
|
3476
|
+
const tried = misses.flatMap((item) => item.missing.map((miss) => ({
|
|
3477
|
+
browser: item.browser,
|
|
3478
|
+
path: miss.path,
|
|
3479
|
+
reason: miss.reason
|
|
3480
|
+
})));
|
|
3481
|
+
const listed = tried.map((item) => `${item.path} (${item.reason})`).join("; ");
|
|
3482
|
+
const detail = listed === "" ? "" : ` Tried: ${listed}.`;
|
|
3483
|
+
super(`Neither Google Chrome nor Microsoft Edge was found.${detail} Ask the person to install Google Chrome, or set the browser's path in the configuration.`);
|
|
3484
|
+
this.name = "BrowserNotFoundError";
|
|
3485
|
+
this.notFound = {
|
|
3486
|
+
browsers: misses.map((item) => item.browser),
|
|
3487
|
+
tried
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3077
3490
|
};
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3491
|
+
/**
|
|
3492
|
+
* The installed browser to launch when the choice is automatic.
|
|
3493
|
+
* The remembered browser wins while it can still be found; otherwise Chrome, then Edge.
|
|
3494
|
+
*/
|
|
3495
|
+
async function resolveAuto(env, remembered, signal) {
|
|
3496
|
+
const order = [];
|
|
3497
|
+
if (remembered === "chrome" || remembered === "msedge") order.push(remembered);
|
|
3498
|
+
for (const name of ["chrome", "msedge"]) if (!order.includes(name)) order.push(name);
|
|
3499
|
+
const misses = [];
|
|
3500
|
+
for (const name of order) {
|
|
3501
|
+
if (signal?.aborted === true) break;
|
|
3502
|
+
const located = await locateInstalled(name, env, signal);
|
|
3503
|
+
if ("found" in located) return {
|
|
3504
|
+
browser: name,
|
|
3505
|
+
path: located.found
|
|
3506
|
+
};
|
|
3507
|
+
misses.push({
|
|
3508
|
+
browser: name,
|
|
3509
|
+
missing: located.missing
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
throw new BrowserNotFoundError(misses);
|
|
3513
|
+
}
|
|
3514
|
+
/** Failure listing every path that was tried. */
|
|
3515
|
+
function browserNotFoundError(browser, missing) {
|
|
3516
|
+
const tried = missing.map((item) => `${item.path} (${item.reason})`).join("; ");
|
|
3517
|
+
const detail = tried === "" ? "" : ` Tried: ${tried}.`;
|
|
3518
|
+
return /* @__PURE__ */ new Error(`${TITLE[browser]} not found.${detail} Install it, or set the browser's path in the configuration.`);
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* The path to launch. A configured path is checked and never replaced.
|
|
3522
|
+
* `chromium` without one returns undefined, so Playwright uses its own build.
|
|
3523
|
+
* An installed browser that cannot be found throws.
|
|
3524
|
+
*/
|
|
3525
|
+
async function resolveExecutable(browser, configured, env, signal) {
|
|
3526
|
+
if (configured !== void 0 && configured !== "") {
|
|
3527
|
+
const checked = checkConfigured(configured, env);
|
|
3528
|
+
if (!checked.ok) throw configuredPathError(checked.path, checked.code);
|
|
3529
|
+
return checked.path;
|
|
3530
|
+
}
|
|
3531
|
+
if (browser === "chromium") return void 0;
|
|
3532
|
+
const located = await locateInstalled(browser, env, signal);
|
|
3533
|
+
if ("found" in located) return located.found;
|
|
3534
|
+
throw browserNotFoundError(browser, located.missing);
|
|
3535
|
+
}
|
|
3536
|
+
/**
|
|
3537
|
+
* Where `browser` is installed when the configuration names no path.
|
|
3538
|
+
* Fixed candidates first, then `env.discover`.
|
|
3539
|
+
*/
|
|
3540
|
+
async function locateInstalled(browser, env, signal) {
|
|
3541
|
+
const missing = [];
|
|
3542
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3543
|
+
const visit = (path) => {
|
|
3544
|
+
if (seen.has(path)) return void 0;
|
|
3545
|
+
seen.add(path);
|
|
3546
|
+
const access = env.access(path);
|
|
3547
|
+
if (access === "ok") return path;
|
|
3548
|
+
missing.push({
|
|
3549
|
+
path,
|
|
3550
|
+
reason: reasonForCode(access.code)
|
|
3551
|
+
});
|
|
3552
|
+
};
|
|
3553
|
+
for (const path of candidatePaths(browser, env)) {
|
|
3554
|
+
const found = visit(path);
|
|
3555
|
+
if (found !== void 0) return { found };
|
|
3556
|
+
}
|
|
3557
|
+
if (signal?.aborted === true) return { missing };
|
|
3558
|
+
for (const path of await env.discover(browser, signal)) {
|
|
3559
|
+
const found = visit(path);
|
|
3560
|
+
if (found !== void 0) return { found };
|
|
3561
|
+
}
|
|
3562
|
+
return { missing };
|
|
3563
|
+
}
|
|
3564
|
+
/** `path` is Playwright's Chromium. Missing or not executable is `missing`. */
|
|
3565
|
+
function locateBundled(path, access) {
|
|
3566
|
+
const result = access(path);
|
|
3567
|
+
if (result === "ok") return { found: path };
|
|
3568
|
+
return { missing: [{
|
|
3569
|
+
path,
|
|
3570
|
+
reason: reasonForCode(result.code)
|
|
3571
|
+
}] };
|
|
3572
|
+
}
|
|
3573
|
+
/** This machine: fixed paths, then Spotlight, App Paths, or `PATH`. */
|
|
3574
|
+
function hostLocateEnv() {
|
|
3575
|
+
const env = {
|
|
3576
|
+
platform: process.platform,
|
|
3577
|
+
env: process.env,
|
|
3578
|
+
home: homedir(),
|
|
3579
|
+
access: accessFile,
|
|
3580
|
+
discover: (browser, signal) => discoverInstalled(browser, env, signal)
|
|
3581
|
+
};
|
|
3582
|
+
return env;
|
|
3583
|
+
}
|
|
3584
|
+
/** A `.app` bundle path reported by Spotlight, turned into its executable. */
|
|
3585
|
+
function executableInApp(reported, binary) {
|
|
3586
|
+
const at = reported.indexOf(".app");
|
|
3587
|
+
if (at < 0) return void 0;
|
|
3588
|
+
return posix.join(reported.slice(0, at + 4), "Contents", "MacOS", binary);
|
|
3589
|
+
}
|
|
3590
|
+
/** The default value from `reg query ... /ve` output. */
|
|
3591
|
+
function regValue(stdout) {
|
|
3592
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
3593
|
+
const value = /\bREG_(?:EXPAND_)?SZ\s+(\S.*?)\s*$/.exec(line)?.[1]?.trim();
|
|
3594
|
+
if (value !== void 0 && value !== "") return value;
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
/** Text for an errno from {@link LocateEnv.access}. */
|
|
3598
|
+
function reasonForCode(code) {
|
|
3599
|
+
if (code === "ENOENT" || code === "ENOTDIR") return "does not exist";
|
|
3600
|
+
if (code === "EISDIR") return "is a directory";
|
|
3601
|
+
return `permission denied: ${code}`;
|
|
3602
|
+
}
|
|
3603
|
+
function expandHome(configured, home, platform) {
|
|
3604
|
+
const slash = platform === "win32" ? win32 : posix;
|
|
3605
|
+
if (configured === "~") return home;
|
|
3606
|
+
if (configured.startsWith("~/") || configured.startsWith("~\\")) return slash.join(home, configured.slice(2));
|
|
3607
|
+
return configured;
|
|
3608
|
+
}
|
|
3609
|
+
function accessFile(path) {
|
|
3610
|
+
try {
|
|
3611
|
+
if (statSync(path).isDirectory()) return { code: "EISDIR" };
|
|
3612
|
+
accessSync(path, constants.X_OK);
|
|
3613
|
+
return "ok";
|
|
3614
|
+
} catch (error) {
|
|
3615
|
+
return { code: error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : "EACCES" };
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
async function discoverInstalled(browser, env, signal) {
|
|
3619
|
+
try {
|
|
3620
|
+
if (env.platform === "darwin") return await discoverMac(browser, signal);
|
|
3621
|
+
if (env.platform === "win32") return await discoverWindows(browser, signal);
|
|
3622
|
+
if (env.platform === "linux") return discoverLinux(browser, env);
|
|
3623
|
+
} catch {}
|
|
3624
|
+
return [];
|
|
3625
|
+
}
|
|
3626
|
+
async function discoverMac(browser, signal) {
|
|
3627
|
+
const bundle = MAC_BUNDLE[browser];
|
|
3628
|
+
const stdout = await command("mdfind", [`kMDItemCFBundleIdentifier == '${bundle.id}'`], signal);
|
|
3629
|
+
const found = [];
|
|
3630
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
3631
|
+
const trimmed = line.trim();
|
|
3632
|
+
if (trimmed === "") continue;
|
|
3633
|
+
const executable = executableInApp(trimmed, bundle.binary);
|
|
3634
|
+
if (executable !== void 0) found.push(executable);
|
|
3635
|
+
}
|
|
3636
|
+
return found;
|
|
3637
|
+
}
|
|
3638
|
+
async function discoverWindows(browser, signal) {
|
|
3639
|
+
const exe = WIN_EXE[browser];
|
|
3640
|
+
const found = [];
|
|
3641
|
+
for (const hive of ["HKCU", "HKLM"]) {
|
|
3642
|
+
const value = regValue(await command("reg", [
|
|
3643
|
+
"query",
|
|
3644
|
+
`${hive}\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${exe}`,
|
|
3645
|
+
"/ve"
|
|
3646
|
+
], signal));
|
|
3647
|
+
if (value !== void 0) found.push(value);
|
|
3648
|
+
}
|
|
3649
|
+
return found;
|
|
3650
|
+
}
|
|
3651
|
+
function discoverLinux(browser, env) {
|
|
3652
|
+
const entries = (env.env.PATH ?? "").split(posix.delimiter);
|
|
3653
|
+
const found = [];
|
|
3654
|
+
for (const name of LINUX_NAMES[browser]) for (const dir of entries) {
|
|
3655
|
+
if (dir === "") continue;
|
|
3656
|
+
const candidate = posix.join(dir, name);
|
|
3657
|
+
if (env.access(candidate) !== "ok") continue;
|
|
3658
|
+
try {
|
|
3659
|
+
found.push(realpathSync(candidate));
|
|
3660
|
+
} catch {
|
|
3661
|
+
found.push(candidate);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
return found;
|
|
3665
|
+
}
|
|
3666
|
+
function command(file, args, signal) {
|
|
3667
|
+
if (signal?.aborted === true) return Promise.resolve("");
|
|
3668
|
+
const timer = AbortSignal.timeout(DISCOVER_MS);
|
|
3669
|
+
const stop = signal === void 0 ? timer : AbortSignal.any([signal, timer]);
|
|
3670
|
+
return new Promise((resolve) => {
|
|
3671
|
+
execFile(file, args, {
|
|
3672
|
+
windowsHide: true,
|
|
3673
|
+
signal: stop
|
|
3674
|
+
}, (error, stdout) => {
|
|
3675
|
+
resolve(error ? "" : String(stdout));
|
|
3676
|
+
});
|
|
3677
|
+
});
|
|
3081
3678
|
}
|
|
3679
|
+
//#endregion
|
|
3680
|
+
//#region lib/types/service/browser.js
|
|
3082
3681
|
/**
|
|
3083
3682
|
* Playwright appends `--no-sandbox` unless this is true, and branded Chrome
|
|
3084
3683
|
* then warns that stability and security will suffer. An installed browser
|
|
@@ -3089,34 +3688,45 @@ function chromiumSandboxEnabled(browser, platform = process.platform) {
|
|
|
3089
3688
|
return platform !== "linux" || browser !== "chromium";
|
|
3090
3689
|
}
|
|
3091
3690
|
/**
|
|
3092
|
-
* One persistent context for the whole process.
|
|
3093
|
-
*
|
|
3094
|
-
*
|
|
3691
|
+
* One persistent context for the whole process. An installed browser is located
|
|
3692
|
+
* here and passed as `executablePath`; `channel` names which install that was.
|
|
3693
|
+
* `chromium` omits `channel` so Playwright uses its own browser unless a path
|
|
3694
|
+
* is configured. A missing browser is reported and not downloaded. A launch
|
|
3695
|
+
* that fails after the executable was found is reported as Playwright said.
|
|
3095
3696
|
*/
|
|
3096
|
-
async function launchBrowser(options) {
|
|
3697
|
+
async function launchBrowser(options, env = hostLocateEnv()) {
|
|
3097
3698
|
await mkdir(options.profileDir, { recursive: true });
|
|
3699
|
+
const executablePath = await resolveExecutable(options.browser, options.executablePath, env);
|
|
3700
|
+
return chromium.launchPersistentContext(options.profileDir, {
|
|
3701
|
+
headless: options.headless,
|
|
3702
|
+
acceptDownloads: true,
|
|
3703
|
+
chromiumSandbox: chromiumSandboxEnabled(options.browser),
|
|
3704
|
+
...options.headless ? {} : {
|
|
3705
|
+
viewport: null,
|
|
3706
|
+
ignoreDefaultArgs: ["--disable-extensions"],
|
|
3707
|
+
args: [
|
|
3708
|
+
"--enable-unsafe-extension-debugging",
|
|
3709
|
+
"--keep-alive-for-test",
|
|
3710
|
+
"--disable-blink-features=AutomationControlled",
|
|
3711
|
+
"--test-type"
|
|
3712
|
+
]
|
|
3713
|
+
},
|
|
3714
|
+
...options.browser !== "chromium" && options.channel !== false ? { channel: options.browser } : {},
|
|
3715
|
+
...executablePath !== void 0 ? { executablePath } : {}
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
/** Playwright's own Chromium, or why that file cannot be run. */
|
|
3719
|
+
function bundledChromiumLocation(access) {
|
|
3720
|
+
let path;
|
|
3098
3721
|
try {
|
|
3099
|
-
|
|
3100
|
-
headless: options.headless,
|
|
3101
|
-
acceptDownloads: true,
|
|
3102
|
-
chromiumSandbox: chromiumSandboxEnabled(options.browser),
|
|
3103
|
-
...options.headless ? {} : {
|
|
3104
|
-
viewport: null,
|
|
3105
|
-
ignoreDefaultArgs: ["--disable-extensions"],
|
|
3106
|
-
args: [
|
|
3107
|
-
"--enable-unsafe-extension-debugging",
|
|
3108
|
-
"--keep-alive-for-test",
|
|
3109
|
-
"--disable-blink-features=AutomationControlled",
|
|
3110
|
-
"--test-type"
|
|
3111
|
-
]
|
|
3112
|
-
},
|
|
3113
|
-
...options.browser === "chromium" ? {} : { channel: options.browser },
|
|
3114
|
-
...options.executablePath !== void 0 ? { executablePath: options.executablePath } : {}
|
|
3115
|
-
});
|
|
3722
|
+
path = chromium.executablePath();
|
|
3116
3723
|
} catch (error) {
|
|
3117
|
-
|
|
3118
|
-
|
|
3724
|
+
return { missing: [{
|
|
3725
|
+
path: "chromium",
|
|
3726
|
+
reason: error instanceof Error ? error.message : "does not exist"
|
|
3727
|
+
}] };
|
|
3119
3728
|
}
|
|
3729
|
+
return locateBundled(path, access);
|
|
3120
3730
|
}
|
|
3121
3731
|
/**
|
|
3122
3732
|
* Process id of the browser's main process. Playwright gives none for a
|
|
@@ -3163,6 +3773,126 @@ async function closeOrKill(close, pid, waitMs = 1e3, kill = (target) => {
|
|
|
3163
3773
|
return "killed";
|
|
3164
3774
|
}
|
|
3165
3775
|
//#endregion
|
|
3776
|
+
//#region lib/types/service/profile.js
|
|
3777
|
+
const LAST_BROWSER = "last-browser.json";
|
|
3778
|
+
/** Subdirectories a split profile root keeps beside the browser that is launching. */
|
|
3779
|
+
const KEPT = /* @__PURE__ */ new Set([
|
|
3780
|
+
"chrome",
|
|
3781
|
+
"msedge",
|
|
3782
|
+
"chromium",
|
|
3783
|
+
"custom",
|
|
3784
|
+
LAST_BROWSER
|
|
3785
|
+
]);
|
|
3786
|
+
/** The browser recorded after the last successful launch, if the file names one. */
|
|
3787
|
+
async function readLastBrowser(root) {
|
|
3788
|
+
try {
|
|
3789
|
+
const browser = JSON.parse(await readFile(join(root, LAST_BROWSER), "utf8")).browser;
|
|
3790
|
+
if (browser === "chrome" || browser === "msedge" || browser === "chromium") return browser;
|
|
3791
|
+
} catch {
|
|
3792
|
+
return;
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
/** Remember which browser actually launched, so automatic selection keeps using it. */
|
|
3796
|
+
async function writeLastBrowser(root, browser) {
|
|
3797
|
+
await mkdir(root, { recursive: true });
|
|
3798
|
+
await writeFile(join(root, LAST_BROWSER), `${JSON.stringify({ browser })}\n`);
|
|
3799
|
+
}
|
|
3800
|
+
/**
|
|
3801
|
+
* Move a profile that older versions kept in the root into `browser`'s subdirectory.
|
|
3802
|
+
* A root without Chrome's `Local State`, or one whose subdirectory already exists, stays as it is.
|
|
3803
|
+
*/
|
|
3804
|
+
async function migrateLegacyProfile(root, browser) {
|
|
3805
|
+
if (!KEPT.has(browser) || browser === LAST_BROWSER) return;
|
|
3806
|
+
const target = join(root, browser);
|
|
3807
|
+
try {
|
|
3808
|
+
await access(join(root, "Local State"));
|
|
3809
|
+
} catch {
|
|
3810
|
+
return;
|
|
3811
|
+
}
|
|
3812
|
+
try {
|
|
3813
|
+
await access(target);
|
|
3814
|
+
return;
|
|
3815
|
+
} catch {}
|
|
3816
|
+
await mkdir(target, { recursive: true });
|
|
3817
|
+
const names = await readdir(root);
|
|
3818
|
+
for (const name of names) {
|
|
3819
|
+
if (KEPT.has(name)) continue;
|
|
3820
|
+
await rename(join(root, name), join(target, name));
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
//#endregion
|
|
3824
|
+
//#region lib/types/service/launch-plan.js
|
|
3825
|
+
/** Subdirectory for a configured path while automatic selection does not name a browser. */
|
|
3826
|
+
const CUSTOM = "custom";
|
|
3827
|
+
/**
|
|
3828
|
+
* Which browser, path, and profile directory to launch.
|
|
3829
|
+
* A configured path is checked and never replaced. Automatic selection keeps the
|
|
3830
|
+
* remembered browser while it can be found, then Chrome, then Edge.
|
|
3831
|
+
* Playwright's Chromium is used only when it is selected, or when it is the remembered one.
|
|
3832
|
+
*/
|
|
3833
|
+
async function planLaunch(input) {
|
|
3834
|
+
const configured = input.configured?.trim() ?? "";
|
|
3835
|
+
if (configured !== "") {
|
|
3836
|
+
const checked = checkConfigured(configured, input.env);
|
|
3837
|
+
if (!checked.ok) throw configuredPathError(checked.path, checked.code);
|
|
3838
|
+
const named = input.choice === "auto" ? void 0 : input.choice;
|
|
3839
|
+
if (named !== void 0) await settle(input, named);
|
|
3840
|
+
return {
|
|
3841
|
+
browser: named ?? "chrome",
|
|
3842
|
+
channel: named !== void 0 && named !== "chromium",
|
|
3843
|
+
profileDir: folder(input, named ?? CUSTOM),
|
|
3844
|
+
executablePath: checked.path,
|
|
3845
|
+
...named !== void 0 ? { remember: named } : {}
|
|
3846
|
+
};
|
|
3847
|
+
}
|
|
3848
|
+
if (input.choice === "auto" && input.remembered === "chromium" && "found" in input.bundled) {
|
|
3849
|
+
await settle(input, "chromium");
|
|
3850
|
+
return {
|
|
3851
|
+
browser: "chromium",
|
|
3852
|
+
channel: false,
|
|
3853
|
+
profileDir: folder(input, "chromium"),
|
|
3854
|
+
remember: "chromium"
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
if (input.choice === "auto") {
|
|
3858
|
+
const remembered = input.remembered === "chromium" ? void 0 : input.remembered;
|
|
3859
|
+
const resolved = await resolveAuto(input.env, remembered, input.signal);
|
|
3860
|
+
await settle(input, resolved.browser);
|
|
3861
|
+
return {
|
|
3862
|
+
browser: resolved.browser,
|
|
3863
|
+
channel: true,
|
|
3864
|
+
profileDir: folder(input, resolved.browser),
|
|
3865
|
+
executablePath: resolved.path,
|
|
3866
|
+
remember: resolved.browser
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
if (input.choice === "chromium") {
|
|
3870
|
+
await settle(input, "chromium");
|
|
3871
|
+
return {
|
|
3872
|
+
browser: "chromium",
|
|
3873
|
+
channel: false,
|
|
3874
|
+
profileDir: folder(input, "chromium"),
|
|
3875
|
+
remember: "chromium"
|
|
3876
|
+
};
|
|
3877
|
+
}
|
|
3878
|
+
const located = await locateInstalled(input.choice, input.env, input.signal);
|
|
3879
|
+
if (!("found" in located)) throw browserNotFoundError(input.choice, located.missing);
|
|
3880
|
+
await settle(input, input.choice);
|
|
3881
|
+
return {
|
|
3882
|
+
browser: input.choice,
|
|
3883
|
+
channel: true,
|
|
3884
|
+
profileDir: folder(input, input.choice),
|
|
3885
|
+
executablePath: located.found,
|
|
3886
|
+
remember: input.choice
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3889
|
+
function folder(input, name) {
|
|
3890
|
+
return input.split ? profileDirFor(input.root, name) : input.root;
|
|
3891
|
+
}
|
|
3892
|
+
async function settle(input, name) {
|
|
3893
|
+
if (input.split) await migrateLegacyProfile(input.root, name);
|
|
3894
|
+
}
|
|
3895
|
+
//#endregion
|
|
3166
3896
|
//#region lib/types/service/control.js
|
|
3167
3897
|
/**
|
|
3168
3898
|
* Who holds each tab of one session. Person input outside a service dispatch
|
|
@@ -3179,6 +3909,15 @@ var Control = class {
|
|
|
3179
3909
|
tabs = /* @__PURE__ */ new Map();
|
|
3180
3910
|
since = /* @__PURE__ */ new Map();
|
|
3181
3911
|
named = /* @__PURE__ */ new Map();
|
|
3912
|
+
/**
|
|
3913
|
+
* A handover is waiting, or the person holds a tab. The browser is not
|
|
3914
|
+
* restarted out from under either.
|
|
3915
|
+
*/
|
|
3916
|
+
busy() {
|
|
3917
|
+
if (this.holding.size > 0) return true;
|
|
3918
|
+
for (const who of this.who.values()) if (who === "person") return true;
|
|
3919
|
+
return false;
|
|
3920
|
+
}
|
|
3182
3921
|
/** Whether the person holds any tab, or a handover is waiting. */
|
|
3183
3922
|
of(owner) {
|
|
3184
3923
|
return this.who.get(owner) ?? "model";
|
|
@@ -4333,16 +5072,18 @@ function extensionDir() {
|
|
|
4333
5072
|
var Extension = class Extension {
|
|
4334
5073
|
context;
|
|
4335
5074
|
id;
|
|
5075
|
+
browserSession;
|
|
4336
5076
|
workerPage;
|
|
4337
|
-
constructor(context, id, worker) {
|
|
5077
|
+
constructor(context, id, browserSession, worker) {
|
|
4338
5078
|
this.context = context;
|
|
4339
5079
|
this.id = id;
|
|
5080
|
+
this.browserSession = browserSession;
|
|
4340
5081
|
this.workerPage = worker;
|
|
4341
5082
|
}
|
|
4342
5083
|
static async load(context, browserSession) {
|
|
4343
5084
|
const loaded = await browserSession.send("Extensions.loadUnpacked", { path: extensionDir() });
|
|
4344
5085
|
const worker = await findWorker(context, loaded.id);
|
|
4345
|
-
return new Extension(context, loaded.id, worker);
|
|
5086
|
+
return new Extension(context, loaded.id, browserSession, worker);
|
|
4346
5087
|
}
|
|
4347
5088
|
/** Address of a page shipped in the extension directory. */
|
|
4348
5089
|
pageUrl(name) {
|
|
@@ -4426,11 +5167,32 @@ var Extension = class Extension {
|
|
|
4426
5167
|
async worker(signal) {
|
|
4427
5168
|
const current = this.workerPage;
|
|
4428
5169
|
if (current !== void 0 && this.context.serviceWorkers().includes(current)) return current;
|
|
4429
|
-
const worker = await
|
|
5170
|
+
const worker = await restoreWorker(this.context, this.id, async () => {
|
|
5171
|
+
await this.browserSession.send("Extensions.loadUnpacked", { path: extensionDir() });
|
|
5172
|
+
}, signal);
|
|
4430
5173
|
this.workerPage = worker;
|
|
4431
5174
|
return worker;
|
|
4432
5175
|
}
|
|
4433
5176
|
};
|
|
5177
|
+
/**
|
|
5178
|
+
* The service worker, reloading the extension once when it is gone.
|
|
5179
|
+
* A worker that comes back on its own during the first wait is used as it is.
|
|
5180
|
+
* After a reload that still has no worker, the extension was turned off.
|
|
5181
|
+
*/
|
|
5182
|
+
async function restoreWorker(context, id, reload, signal, appearMs = 1e3) {
|
|
5183
|
+
try {
|
|
5184
|
+
return await untilAborted(findWorker(context, id, appearMs), signal);
|
|
5185
|
+
} catch (error) {
|
|
5186
|
+
if (isInterrupted(error)) throw error;
|
|
5187
|
+
}
|
|
5188
|
+
try {
|
|
5189
|
+
await reload();
|
|
5190
|
+
return await untilAborted(findWorker(context, id), signal);
|
|
5191
|
+
} catch (error) {
|
|
5192
|
+
if (isInterrupted(error)) throw error;
|
|
5193
|
+
throw new Error("The agent extension in the browser was turned off.", { cause: error });
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
4434
5196
|
/** The extension's service worker. Looks once more after the wait times out. */
|
|
4435
5197
|
async function findWorker(context, id, timeoutMs = WORKER_MS) {
|
|
4436
5198
|
const find = () => context.serviceWorkers().find((worker) => worker.url().includes(id));
|
|
@@ -5248,46 +6010,6 @@ async function writeCounter(profileDir, next) {
|
|
|
5248
6010
|
const testHooks = Symbol("mh-browser test hooks");
|
|
5249
6011
|
//#endregion
|
|
5250
6012
|
//#region lib/types/service/service.js
|
|
5251
|
-
var __runInitializers = function(thisArg, initializers, value) {
|
|
5252
|
-
var useValue = arguments.length > 2;
|
|
5253
|
-
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5254
|
-
return useValue ? value : void 0;
|
|
5255
|
-
};
|
|
5256
|
-
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
5257
|
-
function accept(f) {
|
|
5258
|
-
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
5259
|
-
return f;
|
|
5260
|
-
}
|
|
5261
|
-
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
5262
|
-
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
5263
|
-
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
5264
|
-
var _, done = false;
|
|
5265
|
-
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
5266
|
-
var context = {};
|
|
5267
|
-
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
5268
|
-
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
5269
|
-
context.addInitializer = function(f) {
|
|
5270
|
-
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
5271
|
-
extraInitializers.push(accept(f || null));
|
|
5272
|
-
};
|
|
5273
|
-
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
5274
|
-
get: descriptor.get,
|
|
5275
|
-
set: descriptor.set
|
|
5276
|
-
} : descriptor[key], context);
|
|
5277
|
-
if (kind === "accessor") {
|
|
5278
|
-
if (result === void 0) continue;
|
|
5279
|
-
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
5280
|
-
if (_ = accept(result.get)) descriptor.get = _;
|
|
5281
|
-
if (_ = accept(result.set)) descriptor.set = _;
|
|
5282
|
-
if (_ = accept(result.init)) initializers.unshift(_);
|
|
5283
|
-
} else if (_ = accept(result)) {
|
|
5284
|
-
if (kind === "field") initializers.unshift(_);
|
|
5285
|
-
else descriptor[key] = _;
|
|
5286
|
-
}
|
|
5287
|
-
}
|
|
5288
|
-
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
5289
|
-
done = true;
|
|
5290
|
-
};
|
|
5291
6013
|
/** Navigation and back share this limit (§3.7). */
|
|
5292
6014
|
const NAVIGATION_MS = 1e4;
|
|
5293
6015
|
/** How long an open that started a download waits for the download event. */
|
|
@@ -5315,1469 +6037,1585 @@ const NO_WORKSPACE_REASON = "this session has no working directory";
|
|
|
5315
6037
|
* the session's browser. `act` returns a snapshot, including on a page that
|
|
5316
6038
|
* also has an Agent View.
|
|
5317
6039
|
*/
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
6040
|
+
var MhBrowserService = class extends Service {
|
|
6041
|
+
windows = new Windows();
|
|
6042
|
+
headless;
|
|
6043
|
+
/** Profile root. The default root gets one subdirectory per browser at launch. */
|
|
6044
|
+
profileRoot;
|
|
6045
|
+
/** A configured profile directory is used as given, without a per-browser subdirectory. */
|
|
6046
|
+
splitProfile;
|
|
6047
|
+
browserName;
|
|
6048
|
+
executablePath;
|
|
6049
|
+
/** Tests replace the machine lookup. Unset uses this machine. */
|
|
6050
|
+
locator;
|
|
6051
|
+
context;
|
|
6052
|
+
pids = /* @__PURE__ */ new WeakMap();
|
|
6053
|
+
ready = Promise.resolve();
|
|
6054
|
+
closing = false;
|
|
6055
|
+
/** A browser restart was requested and has not run yet. */
|
|
6056
|
+
relaunchWanted = false;
|
|
6057
|
+
/** Counts browser and path changes. A start that sampled the latest one clears the restart. */
|
|
6058
|
+
configEpoch = 0;
|
|
6059
|
+
/** The running browser is being closed so the new configuration can start. */
|
|
6060
|
+
replacing = false;
|
|
6061
|
+
/** The browser is not running. The next call that needs a page starts it. */
|
|
6062
|
+
exited = true;
|
|
6063
|
+
exits = [];
|
|
6064
|
+
/** One queue per owner: a slow wait in one Session does not hold up another. */
|
|
6065
|
+
queues = /* @__PURE__ */ new Map();
|
|
6066
|
+
shown = /* @__PURE__ */ new Map();
|
|
6067
|
+
book = new PresenceBook();
|
|
6068
|
+
maxBytes;
|
|
6069
|
+
workspaces = /* @__PURE__ */ new Map();
|
|
6070
|
+
control = new Control();
|
|
6071
|
+
personTouch = /* @__PURE__ */ new Set();
|
|
6072
|
+
/** Origins that have served a readable Agent View. Memory only; a restart clears it. */
|
|
6073
|
+
agentViewOrigins = /* @__PURE__ */ new Set();
|
|
6074
|
+
/** The last Agent View read for a page address, so a later read can send If-None-Match. */
|
|
6075
|
+
documents = /* @__PURE__ */ new Map();
|
|
6076
|
+
constructor(ctx, config) {
|
|
6077
|
+
super(ctx, "mhBrowser");
|
|
6078
|
+
this.maxBytes = config.presence.maxBytes;
|
|
6079
|
+
this.headless = config.headless;
|
|
6080
|
+
const configuredDir = config.profileDir;
|
|
6081
|
+
this.splitProfile = configuredDir === void 0 || configuredDir === "";
|
|
6082
|
+
this.profileRoot = this.splitProfile ? defaultProfileDir(ctx.get("profileContext")) : configuredDir;
|
|
6083
|
+
this.browserName = config.browser;
|
|
6084
|
+
this.executablePath = config.executablePath ?? { get: () => void 0 };
|
|
6085
|
+
Object.getPrototypeOf(ctx).on("loader/volatile-update", (paths) => {
|
|
6086
|
+
if (!paths.some((path) => path.includes("browser") || path.includes("executablePath"))) return;
|
|
6087
|
+
this.configEpoch += 1;
|
|
6088
|
+
this.relaunchWanted = true;
|
|
6089
|
+
this.relaunchIfIdle();
|
|
6090
|
+
});
|
|
6091
|
+
this.windows.setPersonInput((owner, tab) => {
|
|
6092
|
+
const live = tab === void 0 ? void 0 : this.windows.get(owner, tab);
|
|
6093
|
+
this.control.note(owner, tab);
|
|
6094
|
+
if (live !== void 0) this.windows.markMayHaveChanged(live);
|
|
6095
|
+
});
|
|
6096
|
+
this.windows.onTabClosed((tab) => {
|
|
6097
|
+
this.shown.delete(tab.id);
|
|
6098
|
+
});
|
|
6099
|
+
this.up().catch(() => void 0);
|
|
6100
|
+
ctx.on("session/disposed", (session) => {
|
|
6101
|
+
const owner = session.id;
|
|
6102
|
+
if (owner === void 0) return;
|
|
6103
|
+
this.exclusive(owner, () => this.windows.closeOwner(owner, AbortSignal.timeout(DISPOSE_MS)), AbortSignal.timeout(DISPOSE_MS)).catch((error) => {
|
|
6104
|
+
ctx.logger.warn("closing the windows of %s failed: %o", owner, error);
|
|
5342
6105
|
});
|
|
6106
|
+
this.book.forget(owner);
|
|
6107
|
+
this.control.forget(owner);
|
|
6108
|
+
this.workspaces.delete(owner);
|
|
6109
|
+
});
|
|
6110
|
+
ctx.effect(() => async () => {
|
|
6111
|
+
this.closing = true;
|
|
6112
|
+
await this.stop();
|
|
6113
|
+
});
|
|
6114
|
+
}
|
|
6115
|
+
/** Remember an origin whose pages offer Agent Views. A missing page does not remove it. */
|
|
6116
|
+
declareAgentViewOrigin(origin) {
|
|
6117
|
+
try {
|
|
6118
|
+
const parsed = new URL(origin);
|
|
6119
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
|
|
6120
|
+
this.agentViewOrigins.add(parsed.origin);
|
|
6121
|
+
} catch {}
|
|
6122
|
+
}
|
|
6123
|
+
async open(owner, items, signal, options) {
|
|
6124
|
+
const reveal = options?.reveal === true;
|
|
6125
|
+
try {
|
|
6126
|
+
return await this.exclusive(owner, async () => {
|
|
6127
|
+
await this.up(owner === PERSON_OWNER);
|
|
6128
|
+
const views = [];
|
|
6129
|
+
for (const item of items) {
|
|
6130
|
+
if (signal?.aborted) {
|
|
6131
|
+
views.push({
|
|
6132
|
+
url: item.url,
|
|
6133
|
+
reason: WAITING_STOPPED
|
|
6134
|
+
});
|
|
6135
|
+
continue;
|
|
6136
|
+
}
|
|
6137
|
+
views.push(await this.openAddress(owner, item, signal, reveal));
|
|
6138
|
+
}
|
|
6139
|
+
await this.enforceCap(owner);
|
|
6140
|
+
return views;
|
|
6141
|
+
}, signal);
|
|
6142
|
+
} catch (error) {
|
|
6143
|
+
if (!isInterrupted(error)) throw error;
|
|
6144
|
+
return items.map((item) => ({
|
|
6145
|
+
url: item.url,
|
|
6146
|
+
reason: WAITING_STOPPED
|
|
6147
|
+
}));
|
|
5343
6148
|
}
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
/** One queue per owner: a slow wait in one Session does not hold up another. */
|
|
5355
|
-
queues = /* @__PURE__ */ new Map();
|
|
5356
|
-
shown = /* @__PURE__ */ new Map();
|
|
5357
|
-
book = new PresenceBook();
|
|
5358
|
-
maxBytes;
|
|
5359
|
-
workspaces = /* @__PURE__ */ new Map();
|
|
5360
|
-
control = new Control();
|
|
5361
|
-
personTouch = /* @__PURE__ */ new Set();
|
|
5362
|
-
/** Origins that have served a readable Agent View. Memory only; a restart clears it. */
|
|
5363
|
-
agentViewOrigins = /* @__PURE__ */ new Set();
|
|
5364
|
-
/** The last Agent View read for a page address, so a later read can send If-None-Match. */
|
|
5365
|
-
documents = /* @__PURE__ */ new Map();
|
|
5366
|
-
constructor(ctx, config) {
|
|
5367
|
-
super(ctx, "mhBrowser");
|
|
5368
|
-
this.maxBytes = config.presence.maxBytes;
|
|
5369
|
-
this.launchOptions = {
|
|
5370
|
-
browser: config.browser,
|
|
5371
|
-
headless: config.headless,
|
|
5372
|
-
profileDir: config.profileDir ?? defaultProfileDir(ctx.get("profileContext"))
|
|
6149
|
+
}
|
|
6150
|
+
async view(owner, tab, signal) {
|
|
6151
|
+
return this.exclusive(owner, async () => {
|
|
6152
|
+
await this.ready;
|
|
6153
|
+
const live = this.windows.get(owner, tab);
|
|
6154
|
+
if (live === void 0) return unknownTab();
|
|
6155
|
+
if (signal?.aborted) return {
|
|
6156
|
+
tab,
|
|
6157
|
+
url: live.url,
|
|
6158
|
+
reason: WAITING_STOPPED
|
|
5373
6159
|
};
|
|
5374
|
-
this.
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
6160
|
+
return this.refreshView(live, signal);
|
|
6161
|
+
}, signal);
|
|
6162
|
+
}
|
|
6163
|
+
/** Pages whose current representation is already in the model's presence. */
|
|
6164
|
+
rememberPresence(owner, urls) {
|
|
6165
|
+
this.book.remember(owner, urls.map((url) => canonical(url)));
|
|
6166
|
+
}
|
|
6167
|
+
presenceList(owner) {
|
|
6168
|
+
return this.book.entries(owner);
|
|
6169
|
+
}
|
|
6170
|
+
presencePut(owner, urls, source) {
|
|
6171
|
+
return this.book.put(owner, urls.map((url) => canonical(url)), source);
|
|
6172
|
+
}
|
|
6173
|
+
presenceRemove(owner, urls) {
|
|
6174
|
+
return this.book.remove(owner, urls.map((url) => canonical(url)));
|
|
6175
|
+
}
|
|
6176
|
+
/** The set the session log folds to; taken once per process. */
|
|
6177
|
+
presenceLoad(owner, folded) {
|
|
6178
|
+
this.book.load(owner, folded);
|
|
6179
|
+
}
|
|
6180
|
+
presenceTakeResume(owner) {
|
|
6181
|
+
return this.book.takeResume(owner);
|
|
6182
|
+
}
|
|
6183
|
+
/**
|
|
6184
|
+
* Re-read presence pages and build the next reminder.
|
|
6185
|
+
* A tab already on the address is read in place. A check that has to open
|
|
6186
|
+
* one closes it before returning. Removals are included, and they cause
|
|
6187
|
+
* every remaining page to be read so a page kept out by the byte budget
|
|
6188
|
+
* can come back. An empty set has nothing to read and does not wait for the
|
|
6189
|
+
* browser; a browser that is down makes every page absent.
|
|
6190
|
+
*/
|
|
6191
|
+
async presenceCheck(owner, which, opts) {
|
|
6192
|
+
if (this.book.entries(owner).length === 0 && this.book.peekRemovals(owner).length === 0) return;
|
|
6193
|
+
return this.exclusive(owner, async () => {
|
|
6194
|
+
const down = await this.up().then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
|
|
6195
|
+
const pages = this.book.entries(owner);
|
|
6196
|
+
const removals = this.book.peekRemovals(owner);
|
|
6197
|
+
const wanted = new Set(which === "all" ? pages.map((page) => page.url) : which.map(canonical));
|
|
6198
|
+
if (removals.length > 0) for (const page of pages) wanted.add(page.url);
|
|
6199
|
+
const observations = [];
|
|
6200
|
+
for (const page of pages) {
|
|
6201
|
+
if (!wanted.has(page.url)) continue;
|
|
6202
|
+
observations.push(down === void 0 ? await this.observePresence(owner, page.url, opts?.signal) : {
|
|
6203
|
+
url: page.url,
|
|
6204
|
+
ok: false,
|
|
6205
|
+
reason: `browser not running: ${down}`,
|
|
6206
|
+
title: "",
|
|
6207
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6208
|
+
bytes: 0
|
|
5388
6209
|
});
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
this.
|
|
5395
|
-
|
|
6210
|
+
}
|
|
6211
|
+
const planned = planReminder({
|
|
6212
|
+
pages,
|
|
6213
|
+
observations,
|
|
6214
|
+
removals,
|
|
6215
|
+
maxBytes: this.maxBytes,
|
|
6216
|
+
baseline: opts?.baseline === true,
|
|
6217
|
+
replaces: opts?.replaces === true,
|
|
6218
|
+
...opts?.force === true ? { force: true } : {}
|
|
5396
6219
|
});
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
url: live.url,
|
|
5441
|
-
reason: WAITING_STOPPED
|
|
5442
|
-
};
|
|
5443
|
-
return this.refreshView(live, signal);
|
|
5444
|
-
}, signal);
|
|
5445
|
-
}
|
|
5446
|
-
/** Pages whose current representation is already in the model's presence. */
|
|
5447
|
-
rememberPresence(owner, urls) {
|
|
5448
|
-
this.book.remember(owner, urls.map((url) => canonical(url)));
|
|
5449
|
-
}
|
|
5450
|
-
presenceList(owner) {
|
|
5451
|
-
return this.book.entries(owner);
|
|
5452
|
-
}
|
|
5453
|
-
presencePut(owner, urls, source) {
|
|
5454
|
-
return this.book.put(owner, urls.map((url) => canonical(url)), source);
|
|
5455
|
-
}
|
|
5456
|
-
presenceRemove(owner, urls) {
|
|
5457
|
-
return this.book.remove(owner, urls.map((url) => canonical(url)));
|
|
5458
|
-
}
|
|
5459
|
-
/** The set the session log folds to; taken once per process. */
|
|
5460
|
-
presenceLoad(owner, folded) {
|
|
5461
|
-
this.book.load(owner, folded);
|
|
5462
|
-
}
|
|
5463
|
-
presenceTakeResume(owner) {
|
|
5464
|
-
return this.book.takeResume(owner);
|
|
5465
|
-
}
|
|
5466
|
-
/**
|
|
5467
|
-
* Re-read presence pages and build the next reminder.
|
|
5468
|
-
* A tab already on the address is read in place. A check that has to open
|
|
5469
|
-
* one closes it before returning. Removals are included, and they cause
|
|
5470
|
-
* every remaining page to be read so a page kept out by the byte budget
|
|
5471
|
-
* can come back. An empty set has nothing to read and does not wait for the
|
|
5472
|
-
* browser; a browser that is down makes every page absent.
|
|
5473
|
-
*/
|
|
5474
|
-
async presenceCheck(owner, which, opts) {
|
|
5475
|
-
if (this.book.entries(owner).length === 0 && this.book.peekRemovals(owner).length === 0) return;
|
|
5476
|
-
return this.exclusive(owner, async () => {
|
|
5477
|
-
const down = await this.up().then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
|
|
5478
|
-
const pages = this.book.entries(owner);
|
|
5479
|
-
const removals = this.book.peekRemovals(owner);
|
|
5480
|
-
const wanted = new Set(which === "all" ? pages.map((page) => page.url) : which.map(canonical));
|
|
5481
|
-
if (removals.length > 0) for (const page of pages) wanted.add(page.url);
|
|
5482
|
-
const observations = [];
|
|
5483
|
-
for (const page of pages) {
|
|
5484
|
-
if (!wanted.has(page.url)) continue;
|
|
5485
|
-
observations.push(down === void 0 ? await this.observePresence(owner, page.url, opts?.signal) : {
|
|
5486
|
-
url: page.url,
|
|
5487
|
-
ok: false,
|
|
5488
|
-
reason: `browser not running: ${down}`,
|
|
5489
|
-
title: "",
|
|
5490
|
-
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5491
|
-
bytes: 0
|
|
5492
|
-
});
|
|
5493
|
-
}
|
|
5494
|
-
const planned = planReminder({
|
|
5495
|
-
pages,
|
|
5496
|
-
observations,
|
|
5497
|
-
removals,
|
|
5498
|
-
maxBytes: this.maxBytes,
|
|
5499
|
-
baseline: opts?.baseline === true,
|
|
5500
|
-
replaces: opts?.replaces === true,
|
|
5501
|
-
...opts?.force === true ? { force: true } : {}
|
|
5502
|
-
});
|
|
5503
|
-
if (planned === void 0) return void 0;
|
|
5504
|
-
this.book.apply(owner, planned.next);
|
|
5505
|
-
this.book.clearRemovals(owner);
|
|
5506
|
-
return {
|
|
5507
|
-
text: planned.text,
|
|
5508
|
-
record: planned.record
|
|
5509
|
-
};
|
|
5510
|
-
}, opts?.signal);
|
|
5511
|
-
}
|
|
5512
|
-
/**
|
|
5513
|
-
* Where a download the page starts may be written: the session's writable
|
|
5514
|
-
* root, or why it may not write at all.
|
|
5515
|
-
*/
|
|
5516
|
-
noteWorkspace(owner, target) {
|
|
5517
|
-
this.workspaces.set(owner, target);
|
|
5518
|
-
}
|
|
5519
|
-
tabs(owner) {
|
|
5520
|
-
return this.windows.list(owner);
|
|
5521
|
-
}
|
|
5522
|
-
takeClosed(owner) {
|
|
5523
|
-
return this.windows.takeClosed(owner);
|
|
5524
|
-
}
|
|
5525
|
-
windowId(owner, tab) {
|
|
5526
|
-
return this.exclusive(owner, async () => {
|
|
5527
|
-
await this.ready;
|
|
5528
|
-
return this.windows.windowId(owner, tab);
|
|
6220
|
+
if (planned === void 0) return void 0;
|
|
6221
|
+
this.book.apply(owner, planned.next);
|
|
6222
|
+
this.book.clearRemovals(owner);
|
|
6223
|
+
return {
|
|
6224
|
+
text: planned.text,
|
|
6225
|
+
record: planned.record
|
|
6226
|
+
};
|
|
6227
|
+
}, opts?.signal);
|
|
6228
|
+
}
|
|
6229
|
+
/**
|
|
6230
|
+
* Where a download the page starts may be written: the session's writable
|
|
6231
|
+
* root, or why it may not write at all.
|
|
6232
|
+
*/
|
|
6233
|
+
noteWorkspace(owner, target) {
|
|
6234
|
+
this.workspaces.set(owner, target);
|
|
6235
|
+
}
|
|
6236
|
+
tabs(owner) {
|
|
6237
|
+
return this.windows.list(owner);
|
|
6238
|
+
}
|
|
6239
|
+
takeClosed(owner) {
|
|
6240
|
+
return this.windows.takeClosed(owner);
|
|
6241
|
+
}
|
|
6242
|
+
windowId(owner, tab) {
|
|
6243
|
+
return this.exclusive(owner, async () => {
|
|
6244
|
+
await this.ready;
|
|
6245
|
+
return this.windows.windowId(owner, tab);
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
6248
|
+
async fetch(url, opts) {
|
|
6249
|
+
try {
|
|
6250
|
+
await this.up();
|
|
6251
|
+
const context = this.context;
|
|
6252
|
+
if (context === void 0) return {
|
|
6253
|
+
kind: "error",
|
|
6254
|
+
reason: "browser is not running"
|
|
6255
|
+
};
|
|
6256
|
+
const headers = {};
|
|
6257
|
+
if (opts?.accept !== void 0) headers.accept = opts.accept;
|
|
6258
|
+
if (opts?.ifNoneMatch !== void 0) headers["if-none-match"] = opts.ifNoneMatch;
|
|
6259
|
+
const response = await context.request.get(url, {
|
|
6260
|
+
headers,
|
|
6261
|
+
failOnStatusCode: false,
|
|
6262
|
+
...opts?.signal !== void 0 ? { signal: opts.signal } : {}
|
|
5529
6263
|
});
|
|
6264
|
+
const headerMap = response.headers();
|
|
6265
|
+
const body = {
|
|
6266
|
+
kind: "response",
|
|
6267
|
+
finalUrl: response.url(),
|
|
6268
|
+
status: response.status(),
|
|
6269
|
+
body: new Uint8Array(await untilAborted(response.body(), capped(opts?.signal, READ_MS)))
|
|
6270
|
+
};
|
|
6271
|
+
const mediaType = headerMap["content-type"];
|
|
6272
|
+
const etag = headerMap.etag;
|
|
6273
|
+
const link = headerMap.link;
|
|
6274
|
+
if (mediaType !== void 0) body.mediaType = mediaType;
|
|
6275
|
+
if (etag !== void 0) body.etag = etag;
|
|
6276
|
+
if (link !== void 0) body.link = link;
|
|
6277
|
+
return body;
|
|
6278
|
+
} catch (error) {
|
|
6279
|
+
return {
|
|
6280
|
+
kind: "error",
|
|
6281
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
6282
|
+
};
|
|
5530
6283
|
}
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
if (link !== void 0) body.link = link;
|
|
5560
|
-
return body;
|
|
5561
|
-
} catch (error) {
|
|
5562
|
-
return {
|
|
5563
|
-
kind: "error",
|
|
5564
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
5565
|
-
};
|
|
6284
|
+
}
|
|
6285
|
+
async pushPath(owner, tab, path) {
|
|
6286
|
+
await this.ready;
|
|
6287
|
+
const live = this.windows.get(owner, tab);
|
|
6288
|
+
if (live === void 0) return;
|
|
6289
|
+
await live.page.evaluate((next) => {
|
|
6290
|
+
history.pushState({}, "", next);
|
|
6291
|
+
}, path);
|
|
6292
|
+
}
|
|
6293
|
+
async started() {
|
|
6294
|
+
await this.ready;
|
|
6295
|
+
}
|
|
6296
|
+
/** The person asked for the browser: start it if it exited, past the exit limit. */
|
|
6297
|
+
startForPerson() {
|
|
6298
|
+
return this.up(true);
|
|
6299
|
+
}
|
|
6300
|
+
[testHooks]() {
|
|
6301
|
+
return {
|
|
6302
|
+
crash: async () => {
|
|
6303
|
+
this.windows.suppressPersonInput();
|
|
6304
|
+
await (this.context?.browser())?.close();
|
|
6305
|
+
},
|
|
6306
|
+
page: (owner, tab) => this.windows.get(owner, tab)?.page,
|
|
6307
|
+
running: () => this.context !== void 0,
|
|
6308
|
+
pages: () => this.context?.pages() ?? [],
|
|
6309
|
+
pid: () => this.context === void 0 ? void 0 : this.pids.get(this.context),
|
|
6310
|
+
useLocator: (env) => {
|
|
6311
|
+
this.locator = env;
|
|
5566
6312
|
}
|
|
6313
|
+
};
|
|
6314
|
+
}
|
|
6315
|
+
/**
|
|
6316
|
+
* Where `browser` would be launched from when no path is configured.
|
|
6317
|
+
* The configured path is not consulted: the answer is what an empty path uses.
|
|
6318
|
+
* `auto` reports the browser automatic selection would launch.
|
|
6319
|
+
*/
|
|
6320
|
+
async locateFor(browser, signal) {
|
|
6321
|
+
const stopped = () => signal?.aborted === true;
|
|
6322
|
+
if (stopped()) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
6323
|
+
if (browser !== "auto" && browser !== "chrome" && browser !== "msedge" && browser !== "chromium") throw new MhBrowserError("INVALID_ARGS", "browser must be auto, chrome, msedge, or chromium");
|
|
6324
|
+
const env = this.locateEnv();
|
|
6325
|
+
if (browser === "auto") return this.locateAuto(env, signal);
|
|
6326
|
+
if (browser === "chromium") return bundledChromiumLocation(env.access);
|
|
6327
|
+
const located = await locateInstalled(browser, env, signal);
|
|
6328
|
+
if (stopped()) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
6329
|
+
return located;
|
|
6330
|
+
}
|
|
6331
|
+
async locateAuto(env, signal) {
|
|
6332
|
+
const stopped = () => signal?.aborted === true;
|
|
6333
|
+
const remembered = await readLastBrowser(this.profileRoot);
|
|
6334
|
+
if (remembered === "chromium") {
|
|
6335
|
+
const bundled = bundledChromiumLocation(env.access);
|
|
6336
|
+
if ("found" in bundled) return {
|
|
6337
|
+
found: bundled.found,
|
|
6338
|
+
browser: "chromium"
|
|
6339
|
+
};
|
|
5567
6340
|
}
|
|
5568
|
-
|
|
5569
|
-
await
|
|
5570
|
-
|
|
5571
|
-
if (live === void 0) return;
|
|
5572
|
-
await live.page.evaluate((next) => {
|
|
5573
|
-
history.pushState({}, "", next);
|
|
5574
|
-
}, path);
|
|
5575
|
-
}
|
|
5576
|
-
async started() {
|
|
5577
|
-
await this.ready;
|
|
5578
|
-
}
|
|
5579
|
-
/** The person asked for the browser: start it if it exited, past the exit limit. */
|
|
5580
|
-
startForPerson() {
|
|
5581
|
-
return this.up(true);
|
|
5582
|
-
}
|
|
5583
|
-
[testHooks]() {
|
|
6341
|
+
try {
|
|
6342
|
+
const resolved = await resolveAuto(env, remembered === "chromium" ? void 0 : remembered, signal);
|
|
6343
|
+
if (stopped()) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
5584
6344
|
return {
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
await (this.context?.browser())?.close();
|
|
5588
|
-
},
|
|
5589
|
-
page: (owner, tab) => this.windows.get(owner, tab)?.page,
|
|
5590
|
-
running: () => this.context !== void 0,
|
|
5591
|
-
pages: () => this.context?.pages() ?? [],
|
|
5592
|
-
pid: () => this.context === void 0 ? void 0 : this.pids.get(this.context)
|
|
6345
|
+
found: resolved.path,
|
|
6346
|
+
browser: resolved.browser
|
|
5593
6347
|
};
|
|
6348
|
+
} catch (error) {
|
|
6349
|
+
if (stopped()) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
6350
|
+
if (error instanceof BrowserNotFoundError) return { missing: error.notFound.tried };
|
|
6351
|
+
throw error;
|
|
5594
6352
|
}
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
if (live === void 0) throw new MhBrowserError(UNKNOWN_TAB, UNKNOWN_TAB_REASON);
|
|
5601
|
-
if (signal?.aborted) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
5602
|
-
const linked = this.control.link(owner, signal, tab);
|
|
5603
|
-
try {
|
|
5604
|
-
if (live.dialogHeld && op.kind !== "handle_dialog") throw new MhBrowserError("MODAL_STATE", "a dialog is open; use browser_handle_dialog", await this.blockedView(live));
|
|
5605
|
-
if (live.fileChooser !== void 0 && op.kind !== "file_upload") {
|
|
5606
|
-
const blocked = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }, signal);
|
|
5607
|
-
blocked.dialog = {
|
|
5608
|
-
type: "filechooser",
|
|
5609
|
-
message: live.fileChooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
|
|
5610
|
-
};
|
|
5611
|
-
throw new MhBrowserError("MODAL_STATE", "a file chooser is open; use browser_file_upload", blocked);
|
|
5612
|
-
}
|
|
5613
|
-
const outcome = await this.perform(live, op, linked.signal);
|
|
5614
|
-
if (op.kind === "fill_form" || op.kind === "select_option" || op.kind === "type" || op.kind === "file_upload") {
|
|
5615
|
-
if (outcome.navigated) this.windows.onNavigated(live);
|
|
5616
|
-
else this.windows.holdInput(live);
|
|
5617
|
-
} else if (outcome.navigated) this.windows.onNavigated(live);
|
|
5618
|
-
this.windows.touch(live);
|
|
5619
|
-
const shown = outcome.navigated ? await this.discover(live, live.page.url(), null, signal) : await this.refreshSnapshot(live, signal);
|
|
5620
|
-
const scoped = op.kind === "snapshot" && (op.target !== void 0 || op.depth !== void 0 || op.boxes === true) ? await this.scopedSnapshot(live, op, signal) : void 0;
|
|
5621
|
-
this.shown.set(live.id, shown);
|
|
5622
|
-
this.windows.noteUrl(live, live.page.url());
|
|
5623
|
-
const view = await this.pageFromShown(live, shown, {
|
|
5624
|
-
surface: "snapshot",
|
|
5625
|
-
...outcome.dialog !== void 0 ? { dialog: outcome.dialog } : {},
|
|
5626
|
-
...outcome.incomplete !== void 0 ? { incomplete: outcome.incomplete } : {},
|
|
5627
|
-
...outcome.waited !== void 0 ? { waited: outcome.waited } : {}
|
|
5628
|
-
}, signal);
|
|
5629
|
-
if (scoped !== void 0 && view.representation.kind === "snapshot") view.representation = {
|
|
5630
|
-
...view.representation,
|
|
5631
|
-
text: scoped
|
|
5632
|
-
};
|
|
5633
|
-
if (!showsSnapshot(op)) {
|
|
5634
|
-
const summary = outcome.summary ?? summaryOf(op);
|
|
5635
|
-
if (summary !== void 0) view.summary = summary;
|
|
5636
|
-
if (outcome.detail !== void 0 && outcome.detail !== "") view.detail = outcome.detail;
|
|
5637
|
-
}
|
|
5638
|
-
if (op.kind === "take_screenshot") view.summary = "";
|
|
5639
|
-
if (outcome.shot !== void 0) view.shot = outcome.shot;
|
|
5640
|
-
if (outcome.writes !== void 0) view.writes = outcome.writes;
|
|
5641
|
-
return this.attachDownload(live, view, false);
|
|
5642
|
-
} catch (error) {
|
|
5643
|
-
if (error instanceof MhBrowserError && error.code === "TAKEN_OVER") throw error;
|
|
5644
|
-
if (this.control.ofTab(owner, tab) === "person") throw await this.takenOver(live);
|
|
5645
|
-
if (error instanceof MhBrowserError && error.code === "STALE_REF" && error.page === void 0) throw new MhBrowserError(error.code, error.message, await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }, signal));
|
|
5646
|
-
throw error;
|
|
5647
|
-
} finally {
|
|
5648
|
-
linked.release();
|
|
5649
|
-
}
|
|
5650
|
-
}, signal);
|
|
5651
|
-
}
|
|
5652
|
-
holdHandover(owner, tab) {
|
|
6353
|
+
}
|
|
6354
|
+
async act(owner, tab, op, signal) {
|
|
6355
|
+
return this.exclusive(owner, async () => {
|
|
6356
|
+
await this.ready;
|
|
6357
|
+
this.refuseIfPerson(owner, tab);
|
|
5653
6358
|
const live = this.windows.get(owner, tab);
|
|
5654
6359
|
if (live === void 0) throw new MhBrowserError(UNKNOWN_TAB, UNKNOWN_TAB_REASON);
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
if (live === void 0 || live.dialogHeld) return void 0;
|
|
5667
|
-
try {
|
|
5668
|
-
const limit = capped(signal, READ_MS);
|
|
5669
|
-
const bytes = await untilAborted(live.page.screenshot({
|
|
5670
|
-
type: "png",
|
|
5671
|
-
signal: limit
|
|
5672
|
-
}), limit);
|
|
5673
|
-
return new Uint8Array(bytes);
|
|
5674
|
-
} catch {
|
|
5675
|
-
return;
|
|
6360
|
+
if (signal?.aborted) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
6361
|
+
const linked = this.control.link(owner, signal, tab);
|
|
6362
|
+
try {
|
|
6363
|
+
if (live.dialogHeld && op.kind !== "handle_dialog") throw new MhBrowserError("MODAL_STATE", "a dialog is open; use browser_handle_dialog", await this.blockedView(live));
|
|
6364
|
+
if (live.fileChooser !== void 0 && op.kind !== "file_upload") {
|
|
6365
|
+
const blocked = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }, signal);
|
|
6366
|
+
blocked.dialog = {
|
|
6367
|
+
type: "filechooser",
|
|
6368
|
+
message: live.fileChooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
|
|
6369
|
+
};
|
|
6370
|
+
throw new MhBrowserError("MODAL_STATE", "a file chooser is open; use browser_file_upload", blocked);
|
|
5676
6371
|
}
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
const
|
|
5684
|
-
const
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
}
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
await this.up();
|
|
5702
|
-
return this.saveResponse(url, root, signal);
|
|
5703
|
-
}
|
|
5704
|
-
controlOf(owner) {
|
|
5705
|
-
return this.control.of(owner);
|
|
5706
|
-
}
|
|
5707
|
-
/** Presence registers this. A hand-back calls it with the touched presence pages. */
|
|
5708
|
-
bindPersonTouch(fn) {
|
|
5709
|
-
this.personTouch.add(fn);
|
|
5710
|
-
return () => {
|
|
5711
|
-
this.personTouch.delete(fn);
|
|
5712
|
-
};
|
|
5713
|
-
}
|
|
5714
|
-
/**
|
|
5715
|
-
* Give the browser to the person. The named tab stays in use until hand-back.
|
|
5716
|
-
* This does not wait; the tool waits on the question card.
|
|
5717
|
-
*/
|
|
5718
|
-
handover(owner, req, signal) {
|
|
5719
|
-
return this.exclusive(owner, async () => {
|
|
5720
|
-
await this.up();
|
|
5721
|
-
if (signal?.aborted === true) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
5722
|
-
if (req.tab !== void 0) {
|
|
5723
|
-
const live = this.windows.get(owner, req.tab);
|
|
5724
|
-
if (live === void 0) throw new MhBrowserError(UNKNOWN_TAB, UNKNOWN_TAB_REASON);
|
|
5725
|
-
this.windows.holdHandover(live);
|
|
5726
|
-
await this.windows.activate(live, signal).catch(() => void 0);
|
|
6372
|
+
const outcome = await this.perform(live, op, linked.signal);
|
|
6373
|
+
if (op.kind === "fill_form" || op.kind === "select_option" || op.kind === "type" || op.kind === "file_upload") {
|
|
6374
|
+
if (outcome.navigated) this.windows.onNavigated(live);
|
|
6375
|
+
else this.windows.holdInput(live);
|
|
6376
|
+
} else if (outcome.navigated) this.windows.onNavigated(live);
|
|
6377
|
+
this.windows.touch(live);
|
|
6378
|
+
const shown = outcome.navigated ? await this.discover(live, live.page.url(), null, signal) : await this.refreshSnapshot(live, signal);
|
|
6379
|
+
const scoped = op.kind === "snapshot" && (op.target !== void 0 || op.depth !== void 0 || op.boxes === true) ? await this.scopedSnapshot(live, op, signal) : void 0;
|
|
6380
|
+
this.shown.set(live.id, shown);
|
|
6381
|
+
this.windows.noteUrl(live, live.page.url());
|
|
6382
|
+
const view = await this.pageFromShown(live, shown, {
|
|
6383
|
+
surface: "snapshot",
|
|
6384
|
+
...outcome.dialog !== void 0 ? { dialog: outcome.dialog } : {},
|
|
6385
|
+
...outcome.incomplete !== void 0 ? { incomplete: outcome.incomplete } : {},
|
|
6386
|
+
...outcome.waited !== void 0 ? { waited: outcome.waited } : {}
|
|
6387
|
+
}, signal);
|
|
6388
|
+
if (scoped !== void 0 && view.representation.kind === "snapshot") view.representation = {
|
|
6389
|
+
...view.representation,
|
|
6390
|
+
text: scoped
|
|
6391
|
+
};
|
|
6392
|
+
if (!showsSnapshot(op)) {
|
|
6393
|
+
const summary = outcome.summary ?? summaryOf(op);
|
|
6394
|
+
if (summary !== void 0) view.summary = summary;
|
|
6395
|
+
if (outcome.detail !== void 0 && outcome.detail !== "") view.detail = outcome.detail;
|
|
5727
6396
|
}
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
6397
|
+
if (op.kind === "take_screenshot") view.summary = "";
|
|
6398
|
+
if (outcome.shot !== void 0) view.shot = outcome.shot;
|
|
6399
|
+
if (outcome.writes !== void 0) view.writes = outcome.writes;
|
|
6400
|
+
return this.attachDownload(live, view, false);
|
|
6401
|
+
} catch (error) {
|
|
6402
|
+
if (error instanceof MhBrowserError && error.code === "TAKEN_OVER") throw error;
|
|
6403
|
+
if (this.control.ofTab(owner, tab) === "person") throw await this.takenOver(live);
|
|
6404
|
+
if (error instanceof MhBrowserError && error.code === "STALE_REF" && error.page === void 0) throw new MhBrowserError(error.code, error.message, await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }, signal));
|
|
6405
|
+
throw error;
|
|
6406
|
+
} finally {
|
|
6407
|
+
linked.release();
|
|
6408
|
+
}
|
|
6409
|
+
}, signal);
|
|
6410
|
+
}
|
|
6411
|
+
holdHandover(owner, tab) {
|
|
6412
|
+
const live = this.windows.get(owner, tab);
|
|
6413
|
+
if (live === void 0) throw new MhBrowserError(UNKNOWN_TAB, UNKNOWN_TAB_REASON);
|
|
6414
|
+
this.windows.holdHandover(live);
|
|
6415
|
+
}
|
|
6416
|
+
releaseHandover(owner) {
|
|
6417
|
+
this.windows.releaseHandover(owner);
|
|
6418
|
+
this.windows.markWaiting(owner, false);
|
|
6419
|
+
}
|
|
6420
|
+
/** `undefined` when no screenshot can be taken now: a dialog is open, or the page refused. */
|
|
6421
|
+
capture(owner, tab, signal) {
|
|
6422
|
+
return this.exclusive(owner, async () => {
|
|
5738
6423
|
await this.ready;
|
|
5739
|
-
const live = this.windows.
|
|
5740
|
-
if (live === void 0) return
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
6424
|
+
const live = this.windows.get(owner, tab);
|
|
6425
|
+
if (live === void 0 || live.dialogHeld) return void 0;
|
|
6426
|
+
try {
|
|
6427
|
+
const limit = capped(signal, READ_MS);
|
|
6428
|
+
const bytes = await untilAborted(live.page.screenshot({
|
|
6429
|
+
type: "png",
|
|
6430
|
+
signal: limit
|
|
6431
|
+
}), limit);
|
|
6432
|
+
return new Uint8Array(bytes);
|
|
6433
|
+
} catch {
|
|
6434
|
+
return;
|
|
6435
|
+
}
|
|
6436
|
+
}, signal);
|
|
6437
|
+
}
|
|
6438
|
+
invoke(owner, _page, call, opts) {
|
|
6439
|
+
return this.exclusive(owner, async () => {
|
|
6440
|
+
await this.up();
|
|
6441
|
+
if (opts?.signal?.aborted) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
6442
|
+
const signal = opts?.signal;
|
|
6443
|
+
const sent = await runInvoke({
|
|
6444
|
+
open: (url) => this.openAddress(owner, { url }, signal),
|
|
6445
|
+
refreshIdle: (url, except) => this.refreshIdle(owner, url, except, signal),
|
|
6446
|
+
send: (request) => this.sendAction(request, signal),
|
|
6447
|
+
readFile: opts?.readFile ?? (async () => {
|
|
6448
|
+
throw new MhBrowserError("NO_WORKSPACE", NO_WORKSPACE_REASON);
|
|
6449
|
+
})
|
|
6450
|
+
}, call);
|
|
6451
|
+
await this.enforceCap(owner);
|
|
6452
|
+
return sent;
|
|
6453
|
+
}, opts?.signal);
|
|
6454
|
+
}
|
|
6455
|
+
/**
|
|
6456
|
+
* Not queued: it opens no tab, and a long body must not hold up the
|
|
6457
|
+
* Session's other calls. `root` is the directory the session may write.
|
|
6458
|
+
*/
|
|
6459
|
+
async download(_owner, url, root, signal) {
|
|
6460
|
+
await this.up();
|
|
6461
|
+
return this.saveResponse(url, root, signal);
|
|
6462
|
+
}
|
|
6463
|
+
controlOf(owner) {
|
|
6464
|
+
return this.control.of(owner);
|
|
6465
|
+
}
|
|
6466
|
+
/** Presence registers this. A hand-back calls it with the touched presence pages. */
|
|
6467
|
+
bindPersonTouch(fn) {
|
|
6468
|
+
this.personTouch.add(fn);
|
|
6469
|
+
return () => {
|
|
6470
|
+
this.personTouch.delete(fn);
|
|
6471
|
+
};
|
|
6472
|
+
}
|
|
6473
|
+
/**
|
|
6474
|
+
* Give the browser to the person. The named tab stays in use until hand-back.
|
|
6475
|
+
* This does not wait; the tool waits on the question card.
|
|
6476
|
+
*/
|
|
6477
|
+
handover(owner, req, signal) {
|
|
6478
|
+
return this.exclusive(owner, async () => {
|
|
6479
|
+
await this.up();
|
|
6480
|
+
if (signal?.aborted === true) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
6481
|
+
if (req.tab !== void 0) {
|
|
6482
|
+
const live = this.windows.get(owner, req.tab);
|
|
6483
|
+
if (live === void 0) throw new MhBrowserError(UNKNOWN_TAB, UNKNOWN_TAB_REASON);
|
|
6484
|
+
this.windows.holdHandover(live);
|
|
6485
|
+
await this.windows.activate(live, signal).catch(() => void 0);
|
|
6486
|
+
}
|
|
6487
|
+
this.control.handover(owner, req.tab);
|
|
6488
|
+
this.windows.markWaiting(owner, true);
|
|
6489
|
+
}, signal);
|
|
6490
|
+
}
|
|
6491
|
+
/** Remember the Session title for its tab group. Empty when the host has none. */
|
|
6492
|
+
noteSessionTitle(owner, title) {
|
|
6493
|
+
this.windows.setTitle(owner, title);
|
|
6494
|
+
}
|
|
6495
|
+
/** Raise the window of this owner's latest tab. False when it has no tab. */
|
|
6496
|
+
async bringToFront(owner) {
|
|
6497
|
+
await this.ready;
|
|
6498
|
+
const live = this.windows.latest(owner);
|
|
6499
|
+
if (live === void 0) return false;
|
|
6500
|
+
await untilAborted(live.page.bringToFront(), AbortSignal.timeout(READ_MS));
|
|
6501
|
+
return true;
|
|
6502
|
+
}
|
|
6503
|
+
/** The person asked to see the page this Session handed over. */
|
|
6504
|
+
async revealHandover(sessionId, signal) {
|
|
6505
|
+
await this.ready;
|
|
6506
|
+
if (signal?.aborted === true) return { status: "not-handing-over" };
|
|
6507
|
+
if (!this.control.holds(sessionId)) return { status: "not-handing-over" };
|
|
6508
|
+
const named = this.control.namedTab(sessionId);
|
|
6509
|
+
const live = (named === void 0 ? void 0 : this.windows.get(sessionId, named)) ?? this.windows.latest(sessionId);
|
|
6510
|
+
if (live === void 0) return { status: "no-tab" };
|
|
6511
|
+
await untilAborted(live.page.bringToFront(), capped(signal, READ_MS));
|
|
6512
|
+
return { status: "revealed" };
|
|
6513
|
+
}
|
|
6514
|
+
/** The person answered. Returns the note, the operation count, and the named tab. */
|
|
6515
|
+
async handBack(owner, note) {
|
|
6516
|
+
const signal = capped(void 0, READ_MS);
|
|
6517
|
+
return this.exclusive(owner, async () => {
|
|
5746
6518
|
await this.ready;
|
|
5747
|
-
|
|
5748
|
-
if (
|
|
5749
|
-
const named = this.control.namedTab(sessionId);
|
|
5750
|
-
const live = (named === void 0 ? void 0 : this.windows.get(sessionId, named)) ?? this.windows.latest(sessionId);
|
|
5751
|
-
if (live === void 0) return { status: "no-tab" };
|
|
5752
|
-
await untilAborted(live.page.bringToFront(), capped(signal, READ_MS));
|
|
5753
|
-
return { status: "revealed" };
|
|
5754
|
-
}
|
|
5755
|
-
/** The person answered. Returns the note, the operation count, and the named tab. */
|
|
5756
|
-
async handBack(owner, note) {
|
|
5757
|
-
const signal = capped(void 0, READ_MS);
|
|
5758
|
-
return this.exclusive(owner, async () => {
|
|
5759
|
-
await this.ready;
|
|
5760
|
-
const taken = this.control.finish(owner);
|
|
5761
|
-
if (taken === void 0) throw new MhBrowserError("INVALID_ARGS", "the browser is not with the person");
|
|
5762
|
-
this.windows.releaseHandover(owner);
|
|
5763
|
-
this.windows.markWaiting(owner, false);
|
|
5764
|
-
const urls = this.presenceUrls(owner, taken.tabs);
|
|
5765
|
-
for (const touch of this.personTouch) touch(owner, urls);
|
|
5766
|
-
const page = await this.snapshotNamed(owner, taken.tab, signal);
|
|
5767
|
-
return {
|
|
5768
|
-
...taken,
|
|
5769
|
-
urls,
|
|
5770
|
-
...note !== void 0 && note !== "" ? { note } : {},
|
|
5771
|
-
...page !== void 0 ? { page } : {}
|
|
5772
|
-
};
|
|
5773
|
-
}, signal);
|
|
5774
|
-
}
|
|
5775
|
-
/** A stopped handover gives control back. A finished hand-back is left alone. */
|
|
5776
|
-
releaseIfHeld(owner) {
|
|
5777
|
-
if (!this.control.releaseIfHeld(owner)) return;
|
|
6519
|
+
const taken = this.control.finish(owner);
|
|
6520
|
+
if (taken === void 0) throw new MhBrowserError("INVALID_ARGS", "the browser is not with the person");
|
|
5778
6521
|
this.windows.releaseHandover(owner);
|
|
5779
6522
|
this.windows.markWaiting(owner, false);
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
* hand-back. A handover that is still waiting on its card is not.
|
|
5784
|
-
*/
|
|
5785
|
-
takePersonHandback(owner) {
|
|
5786
|
-
const taken = this.control.takePerson(owner);
|
|
5787
|
-
if (taken === void 0) return void 0;
|
|
5788
|
-
this.windows.releaseHandover(owner);
|
|
6523
|
+
const urls = this.presenceUrls(owner, taken.tabs);
|
|
6524
|
+
for (const touch of this.personTouch) touch(owner, urls);
|
|
6525
|
+
const page = await this.snapshotNamed(owner, taken.tab, signal);
|
|
5789
6526
|
return {
|
|
5790
6527
|
...taken,
|
|
5791
|
-
urls
|
|
6528
|
+
urls,
|
|
6529
|
+
...note !== void 0 && note !== "" ? { note } : {},
|
|
6530
|
+
...page !== void 0 ? { page } : {}
|
|
5792
6531
|
};
|
|
5793
|
-
}
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
6532
|
+
}, signal);
|
|
6533
|
+
}
|
|
6534
|
+
/** A stopped handover gives control back. A finished hand-back is left alone. */
|
|
6535
|
+
releaseIfHeld(owner) {
|
|
6536
|
+
if (!this.control.releaseIfHeld(owner)) return;
|
|
6537
|
+
this.windows.releaseHandover(owner);
|
|
6538
|
+
this.windows.markWaiting(owner, false);
|
|
6539
|
+
this.relaunchIfIdle();
|
|
6540
|
+
}
|
|
6541
|
+
/**
|
|
6542
|
+
* The person took over between turns. The next message they send is the
|
|
6543
|
+
* hand-back. A handover that is still waiting on its card is not.
|
|
6544
|
+
* The addresses are read before a waiting restart closes the pages.
|
|
6545
|
+
*/
|
|
6546
|
+
takePersonHandback(owner) {
|
|
6547
|
+
const taken = this.control.takePerson(owner);
|
|
6548
|
+
if (taken === void 0) return void 0;
|
|
6549
|
+
const urls = this.presenceUrls(owner, taken.tabs);
|
|
6550
|
+
this.windows.releaseHandover(owner);
|
|
6551
|
+
this.relaunchIfIdle();
|
|
6552
|
+
return {
|
|
6553
|
+
...taken,
|
|
6554
|
+
urls
|
|
6555
|
+
};
|
|
6556
|
+
}
|
|
6557
|
+
/**
|
|
6558
|
+
* Read one address. A known origin is tried on the request channel first.
|
|
6559
|
+
* A readable Agent View found on a tab this call opened is returned without
|
|
6560
|
+
* the tab, and that tab is not listed as closed.
|
|
6561
|
+
*/
|
|
6562
|
+
async openAddress(owner, item, signal, reveal = false) {
|
|
6563
|
+
if (!openableAddress(item.url)) return {
|
|
6564
|
+
url: item.url,
|
|
6565
|
+
reason: OPEN_ADDRESS_REASON
|
|
6566
|
+
};
|
|
6567
|
+
const wantsTab = owner === "person" || item.snapshot === true || item.waitFor !== void 0;
|
|
6568
|
+
if (!wantsTab && this.knownOrigin(item.url)) {
|
|
6569
|
+
const via = await this.readKnown(owner, item.url, signal);
|
|
6570
|
+
if (via === STOPPED) return {
|
|
5801
6571
|
url: item.url,
|
|
5802
|
-
reason:
|
|
6572
|
+
reason: WAITING_STOPPED
|
|
5803
6573
|
};
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
const opened = await this.openOne(owner, item.url, item.waitFor, signal, item.snapshot === true, reveal);
|
|
5814
|
-
this.keepAgentView(opened);
|
|
5815
|
-
if (opened.live !== void 0 && !wantsTab && opened.fresh && this.closable(owner, opened.live) && "representation" in opened.view && opened.view.representation.kind === "agent-view") {
|
|
5816
|
-
await this.windows.closeTab(opened.live);
|
|
5817
|
-
const view = { ...opened.view };
|
|
5818
|
-
delete view.tab;
|
|
5819
|
-
return view;
|
|
5820
|
-
}
|
|
5821
|
-
return opened.view;
|
|
6574
|
+
if (via !== void 0) return via;
|
|
6575
|
+
}
|
|
6576
|
+
const opened = await this.openOne(owner, item.url, item.waitFor, signal, item.snapshot === true, reveal);
|
|
6577
|
+
this.keepAgentView(opened);
|
|
6578
|
+
if (opened.live !== void 0 && !wantsTab && opened.fresh && this.closable(owner, opened.live) && "representation" in opened.view && opened.view.representation.kind === "agent-view") {
|
|
6579
|
+
await this.windows.closeTab(opened.live);
|
|
6580
|
+
const view = { ...opened.view };
|
|
6581
|
+
delete view.tab;
|
|
6582
|
+
return view;
|
|
5822
6583
|
}
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
6584
|
+
return opened.view;
|
|
6585
|
+
}
|
|
6586
|
+
/** A tab this call opened can be closed again unless the person took it or it holds input. */
|
|
6587
|
+
closable(owner, live) {
|
|
6588
|
+
return this.control.ofTab(owner, live.id) !== "person" && !live.occupied;
|
|
6589
|
+
}
|
|
6590
|
+
knownOrigin(url) {
|
|
6591
|
+
try {
|
|
6592
|
+
return this.agentViewOrigins.has(new URL(url).origin);
|
|
6593
|
+
} catch {
|
|
6594
|
+
return false;
|
|
5826
6595
|
}
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
6596
|
+
}
|
|
6597
|
+
rememberOrigin(url) {
|
|
6598
|
+
try {
|
|
6599
|
+
this.agentViewOrigins.add(new URL(url).origin);
|
|
6600
|
+
} catch {}
|
|
6601
|
+
}
|
|
6602
|
+
/** A readable Agent View puts its origin on the list and keeps the document for the next read. */
|
|
6603
|
+
keepAgentView(opened) {
|
|
6604
|
+
if (opened.live === void 0) return;
|
|
6605
|
+
if (!("representation" in opened.view)) return;
|
|
6606
|
+
const representation = opened.view.representation;
|
|
6607
|
+
if (!(representation.kind === "agent-view" || representation.kind === "snapshot" && representation.agentView !== void 0)) return;
|
|
6608
|
+
this.rememberOrigin(opened.view.url);
|
|
6609
|
+
const shown = this.shown.get(opened.live.id);
|
|
6610
|
+
if (shown?.kind === "agent-view") this.rememberDocument(shown.pageUrl, shown);
|
|
6611
|
+
}
|
|
6612
|
+
/**
|
|
6613
|
+
* Read a known origin without a tab. `undefined` means this page has to be
|
|
6614
|
+
* opened in a tab: nothing declared, the declaration could not be read, or
|
|
6615
|
+
* the request failed. The origin stays on the list either way. A stop
|
|
6616
|
+
* ends the read without falling back to a tab.
|
|
6617
|
+
*/
|
|
6618
|
+
async readKnown(owner, url, signal) {
|
|
6619
|
+
const key = canonical(url);
|
|
6620
|
+
const previous = this.documents.get(key);
|
|
6621
|
+
const servedAtPage = previous?.kind === "agent-view" && previous.href !== void 0 && canonical(previous.href) === canonical(previous.pageUrl);
|
|
6622
|
+
let response;
|
|
6623
|
+
try {
|
|
6624
|
+
const got = await this.fetchSameOrigin(url, PAGE_ACCEPT, servedAtPage ? previous.etag : void 0, signal);
|
|
6625
|
+
if (signal?.aborted === true) return STOPPED;
|
|
6626
|
+
if (got === void 0) return void 0;
|
|
6627
|
+
response = got;
|
|
6628
|
+
} catch {
|
|
6629
|
+
return signal?.aborted === true ? STOPPED : void 0;
|
|
6630
|
+
}
|
|
6631
|
+
if (response.status === 304 && previous?.kind === "agent-view") return this.pageOfShown(owner, previous);
|
|
6632
|
+
const text = new TextDecoder().decode(response.body);
|
|
6633
|
+
const pageUrl = canonical(response.finalUrl === "" ? url : response.finalUrl);
|
|
6634
|
+
const cached = this.documents.get(pageUrl);
|
|
6635
|
+
const shown = await loadShown({
|
|
6636
|
+
pageUrl,
|
|
6637
|
+
...optionalFragment(response.finalUrl === "" ? url : response.finalUrl),
|
|
6638
|
+
snapshot: "",
|
|
6639
|
+
main: {
|
|
6640
|
+
status: response.status,
|
|
6641
|
+
...response.mediaType !== void 0 ? { mediaType: response.mediaType } : {},
|
|
6642
|
+
finalUrl: response.finalUrl === "" ? url : response.finalUrl,
|
|
6643
|
+
...response.link !== void 0 ? { link: response.link } : {},
|
|
6644
|
+
...response.etag !== void 0 ? { etag: response.etag } : {},
|
|
6645
|
+
body: text
|
|
6646
|
+
},
|
|
6647
|
+
domLinks: linksInHtml(text, response.finalUrl === "" ? url : response.finalUrl),
|
|
6648
|
+
...cached !== void 0 ? { previous: cached } : {},
|
|
6649
|
+
relocate: false,
|
|
6650
|
+
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
6651
|
+
});
|
|
6652
|
+
if (stopped(signal)) return STOPPED;
|
|
6653
|
+
if (shown.kind !== "agent-view") return void 0;
|
|
6654
|
+
this.rememberDocument(pageUrl, shown);
|
|
6655
|
+
this.rememberOrigin(pageUrl);
|
|
6656
|
+
return this.pageOfShown(owner, shown);
|
|
6657
|
+
}
|
|
6658
|
+
/** Keep the newest Agent Views. A dropped one is read in full the next time. */
|
|
6659
|
+
rememberDocument(url, shown) {
|
|
6660
|
+
const key = canonical(url);
|
|
6661
|
+
this.documents.delete(key);
|
|
6662
|
+
this.documents.set(key, shown);
|
|
6663
|
+
while (this.documents.size > DOCUMENT_CAP) {
|
|
6664
|
+
const oldest = this.documents.keys().next().value;
|
|
6665
|
+
if (oldest === void 0) return;
|
|
6666
|
+
this.documents.delete(oldest);
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
6669
|
+
/** One page from a document, with no tab. */
|
|
6670
|
+
pageOfShown(owner, shown) {
|
|
6671
|
+
const view = {
|
|
6672
|
+
url: shown.pageUrl,
|
|
6673
|
+
title: "",
|
|
6674
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6675
|
+
representation: shown.kind === "agent-view" ? {
|
|
6676
|
+
kind: "agent-view",
|
|
6677
|
+
text: shown.text,
|
|
6678
|
+
...shown.version !== void 0 ? { version: shown.version } : {},
|
|
6679
|
+
...shown.href !== void 0 ? { href: shown.href } : {}
|
|
6680
|
+
} : {
|
|
6681
|
+
kind: "snapshot",
|
|
6682
|
+
text: shown.snapshot,
|
|
6683
|
+
...shown.agentViewError !== void 0 ? { agentViewError: shown.agentViewError } : {}
|
|
6684
|
+
},
|
|
6685
|
+
inPresence: this.book.isShown(owner, canonical(shown.pageUrl))
|
|
6686
|
+
};
|
|
6687
|
+
if (shown.fragment !== void 0) view.fragment = shown.fragment;
|
|
6688
|
+
if (shown.status !== void 0) view.status = shown.status;
|
|
6689
|
+
if (shown.mediaType !== void 0) view.mediaType = shown.mediaType;
|
|
6690
|
+
if (shown.body !== void 0) view.body = shown.body;
|
|
6691
|
+
return view;
|
|
6692
|
+
}
|
|
6693
|
+
/**
|
|
6694
|
+
* GET that follows same-origin redirects only. `undefined` when it cannot.
|
|
6695
|
+
* A body that is neither text nor an Agent View is not read: the page then
|
|
6696
|
+
* goes to a tab, which loads it anyway.
|
|
6697
|
+
*/
|
|
6698
|
+
async fetchSameOrigin(url, accept, ifNoneMatch, signal) {
|
|
6699
|
+
const context = this.context;
|
|
6700
|
+
if (context === void 0) return void 0;
|
|
6701
|
+
let current = url;
|
|
6702
|
+
let origin;
|
|
6703
|
+
try {
|
|
6704
|
+
origin = new URL(url).origin;
|
|
6705
|
+
} catch {
|
|
6706
|
+
return;
|
|
5833
6707
|
}
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
if (!("representation" in opened.view)) return;
|
|
5843
|
-
const representation = opened.view.representation;
|
|
5844
|
-
if (!(representation.kind === "agent-view" || representation.kind === "snapshot" && representation.agentView !== void 0)) return;
|
|
5845
|
-
this.rememberOrigin(opened.view.url);
|
|
5846
|
-
const shown = this.shown.get(opened.live.id);
|
|
5847
|
-
if (shown?.kind === "agent-view") this.rememberDocument(shown.pageUrl, shown);
|
|
5848
|
-
}
|
|
5849
|
-
/**
|
|
5850
|
-
* Read a known origin without a tab. `undefined` means this page has to be
|
|
5851
|
-
* opened in a tab: nothing declared, the declaration could not be read, or
|
|
5852
|
-
* the request failed. The origin stays on the list either way. A stop
|
|
5853
|
-
* ends the read without falling back to a tab.
|
|
5854
|
-
*/
|
|
5855
|
-
async readKnown(owner, url, signal) {
|
|
5856
|
-
const key = canonical(url);
|
|
5857
|
-
const previous = this.documents.get(key);
|
|
5858
|
-
const servedAtPage = previous?.kind === "agent-view" && previous.href !== void 0 && canonical(previous.href) === canonical(previous.pageUrl);
|
|
5859
|
-
let response;
|
|
5860
|
-
try {
|
|
5861
|
-
const got = await this.fetchSameOrigin(url, PAGE_ACCEPT, servedAtPage ? previous.etag : void 0, signal);
|
|
5862
|
-
if (signal?.aborted === true) return STOPPED;
|
|
5863
|
-
if (got === void 0) return void 0;
|
|
5864
|
-
response = got;
|
|
5865
|
-
} catch {
|
|
5866
|
-
return signal?.aborted === true ? STOPPED : void 0;
|
|
5867
|
-
}
|
|
5868
|
-
if (response.status === 304 && previous?.kind === "agent-view") return this.pageOfShown(owner, previous);
|
|
5869
|
-
const text = new TextDecoder().decode(response.body);
|
|
5870
|
-
const pageUrl = canonical(response.finalUrl === "" ? url : response.finalUrl);
|
|
5871
|
-
const cached = this.documents.get(pageUrl);
|
|
5872
|
-
const shown = await loadShown({
|
|
5873
|
-
pageUrl,
|
|
5874
|
-
...optionalFragment(response.finalUrl === "" ? url : response.finalUrl),
|
|
5875
|
-
snapshot: "",
|
|
5876
|
-
main: {
|
|
5877
|
-
status: response.status,
|
|
5878
|
-
...response.mediaType !== void 0 ? { mediaType: response.mediaType } : {},
|
|
5879
|
-
finalUrl: response.finalUrl === "" ? url : response.finalUrl,
|
|
5880
|
-
...response.link !== void 0 ? { link: response.link } : {},
|
|
5881
|
-
...response.etag !== void 0 ? { etag: response.etag } : {},
|
|
5882
|
-
body: text
|
|
5883
|
-
},
|
|
5884
|
-
domLinks: linksInHtml(text, response.finalUrl === "" ? url : response.finalUrl),
|
|
5885
|
-
...cached !== void 0 ? { previous: cached } : {},
|
|
5886
|
-
relocate: false,
|
|
5887
|
-
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
6708
|
+
for (let hop = 0; hop < 10; hop += 1) {
|
|
6709
|
+
const headers = { accept };
|
|
6710
|
+
if (ifNoneMatch !== void 0 && hop === 0) headers["if-none-match"] = ifNoneMatch;
|
|
6711
|
+
const response = await context.request.get(current, {
|
|
6712
|
+
headers,
|
|
6713
|
+
maxRedirects: 0,
|
|
6714
|
+
failOnStatusCode: false,
|
|
6715
|
+
...signal !== void 0 ? { signal } : {}
|
|
5888
6716
|
});
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
const oldest = this.documents.keys().next().value;
|
|
5902
|
-
if (oldest === void 0) return;
|
|
5903
|
-
this.documents.delete(oldest);
|
|
6717
|
+
const status = response.status();
|
|
6718
|
+
const location = response.headers().location;
|
|
6719
|
+
if (status >= 300 && status < 400 && location !== void 0 && location !== "") {
|
|
6720
|
+
let next;
|
|
6721
|
+
try {
|
|
6722
|
+
next = new URL(location, current);
|
|
6723
|
+
} catch {
|
|
6724
|
+
return;
|
|
6725
|
+
}
|
|
6726
|
+
if (next.origin !== origin) return void 0;
|
|
6727
|
+
current = next.href;
|
|
6728
|
+
continue;
|
|
5904
6729
|
}
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
6730
|
+
const headerMap = response.headers();
|
|
6731
|
+
const mediaType = headerMap["content-type"];
|
|
6732
|
+
const body = {
|
|
6733
|
+
kind: "response",
|
|
6734
|
+
finalUrl: current,
|
|
6735
|
+
status,
|
|
6736
|
+
body: readsAsText(mediaType) ? new Uint8Array(await untilAborted(response.body(), capped(signal, READ_MS))) : /* @__PURE__ */ new Uint8Array()
|
|
6737
|
+
};
|
|
6738
|
+
const etag = headerMap.etag;
|
|
6739
|
+
const link = headerMap.link;
|
|
6740
|
+
if (mediaType !== void 0) body.mediaType = mediaType;
|
|
6741
|
+
if (etag !== void 0) body.etag = etag;
|
|
6742
|
+
if (link !== void 0) body.link = link;
|
|
6743
|
+
return body;
|
|
6744
|
+
}
|
|
6745
|
+
}
|
|
6746
|
+
async openOne(owner, url, waitFor, signal, snapshot = false, reveal = false) {
|
|
6747
|
+
assertWaitFor(waitFor);
|
|
6748
|
+
const reused = this.idleTabOn(owner, url);
|
|
6749
|
+
const fresh = reused === void 0;
|
|
6750
|
+
let live;
|
|
6751
|
+
try {
|
|
6752
|
+
live = reused ?? await this.windows.openBlank(owner, signal);
|
|
6753
|
+
} catch (error) {
|
|
6754
|
+
if (signal?.aborted === true || isInterrupted(error)) return {
|
|
6755
|
+
view: {
|
|
6756
|
+
url,
|
|
6757
|
+
reason: WAITING_STOPPED
|
|
5921
6758
|
},
|
|
5922
|
-
|
|
6759
|
+
fresh
|
|
5923
6760
|
};
|
|
5924
|
-
|
|
5925
|
-
if (shown.status !== void 0) view.status = shown.status;
|
|
5926
|
-
if (shown.mediaType !== void 0) view.mediaType = shown.mediaType;
|
|
5927
|
-
if (shown.body !== void 0) view.body = shown.body;
|
|
5928
|
-
return view;
|
|
5929
|
-
}
|
|
5930
|
-
/**
|
|
5931
|
-
* GET that follows same-origin redirects only. `undefined` when it cannot.
|
|
5932
|
-
* A body that is neither text nor an Agent View is not read: the page then
|
|
5933
|
-
* goes to a tab, which loads it anyway.
|
|
5934
|
-
*/
|
|
5935
|
-
async fetchSameOrigin(url, accept, ifNoneMatch, signal) {
|
|
5936
|
-
const context = this.context;
|
|
5937
|
-
if (context === void 0) return void 0;
|
|
5938
|
-
let current = url;
|
|
5939
|
-
let origin;
|
|
5940
|
-
try {
|
|
5941
|
-
origin = new URL(url).origin;
|
|
5942
|
-
} catch {
|
|
5943
|
-
return;
|
|
5944
|
-
}
|
|
5945
|
-
for (let hop = 0; hop < 10; hop += 1) {
|
|
5946
|
-
const headers = { accept };
|
|
5947
|
-
if (ifNoneMatch !== void 0 && hop === 0) headers["if-none-match"] = ifNoneMatch;
|
|
5948
|
-
const response = await context.request.get(current, {
|
|
5949
|
-
headers,
|
|
5950
|
-
maxRedirects: 0,
|
|
5951
|
-
failOnStatusCode: false,
|
|
5952
|
-
...signal !== void 0 ? { signal } : {}
|
|
5953
|
-
});
|
|
5954
|
-
const status = response.status();
|
|
5955
|
-
const location = response.headers().location;
|
|
5956
|
-
if (status >= 300 && status < 400 && location !== void 0 && location !== "") {
|
|
5957
|
-
let next;
|
|
5958
|
-
try {
|
|
5959
|
-
next = new URL(location, current);
|
|
5960
|
-
} catch {
|
|
5961
|
-
return;
|
|
5962
|
-
}
|
|
5963
|
-
if (next.origin !== origin) return void 0;
|
|
5964
|
-
current = next.href;
|
|
5965
|
-
continue;
|
|
5966
|
-
}
|
|
5967
|
-
const headerMap = response.headers();
|
|
5968
|
-
const mediaType = headerMap["content-type"];
|
|
5969
|
-
const body = {
|
|
5970
|
-
kind: "response",
|
|
5971
|
-
finalUrl: current,
|
|
5972
|
-
status,
|
|
5973
|
-
body: readsAsText(mediaType) ? new Uint8Array(await untilAborted(response.body(), capped(signal, READ_MS))) : /* @__PURE__ */ new Uint8Array()
|
|
5974
|
-
};
|
|
5975
|
-
const etag = headerMap.etag;
|
|
5976
|
-
const link = headerMap.link;
|
|
5977
|
-
if (mediaType !== void 0) body.mediaType = mediaType;
|
|
5978
|
-
if (etag !== void 0) body.etag = etag;
|
|
5979
|
-
if (link !== void 0) body.link = link;
|
|
5980
|
-
return body;
|
|
5981
|
-
}
|
|
5982
|
-
}
|
|
5983
|
-
async openOne(owner, url, waitFor, signal, snapshot = false, reveal = false) {
|
|
5984
|
-
assertWaitFor(waitFor);
|
|
5985
|
-
const reused = this.idleTabOn(owner, url);
|
|
5986
|
-
const fresh = reused === void 0;
|
|
5987
|
-
let live;
|
|
5988
|
-
try {
|
|
5989
|
-
live = reused ?? await this.windows.openBlank(owner, signal);
|
|
5990
|
-
} catch (error) {
|
|
5991
|
-
if (signal?.aborted === true || isInterrupted(error)) return {
|
|
5992
|
-
view: {
|
|
5993
|
-
url,
|
|
5994
|
-
reason: WAITING_STOPPED
|
|
5995
|
-
},
|
|
5996
|
-
fresh
|
|
5997
|
-
};
|
|
5998
|
-
throw error;
|
|
5999
|
-
}
|
|
6000
|
-
const linked = this.control.link(owner, signal, live.id);
|
|
6001
|
-
if (reveal && !linked.signal.aborted) await live.page.bringToFront().catch(() => void 0);
|
|
6002
|
-
const nextDialog = this.windows.nextDialog(live);
|
|
6003
|
-
try {
|
|
6004
|
-
let seenDialog;
|
|
6005
|
-
const dialogDuringGoto = nextDialog.dialog.then((dialog) => {
|
|
6006
|
-
seenDialog = dialog;
|
|
6007
|
-
return dialog;
|
|
6008
|
-
});
|
|
6009
|
-
let navigationError;
|
|
6010
|
-
let response = null;
|
|
6011
|
-
const raced = this.goto(live.page, url, linked.signal).then((value) => {
|
|
6012
|
-
response = value;
|
|
6013
|
-
return "navigated";
|
|
6014
|
-
}, (error) => {
|
|
6015
|
-
navigationError = error;
|
|
6016
|
-
return "failed";
|
|
6017
|
-
});
|
|
6018
|
-
const winner = await Promise.race([raced, dialogDuringGoto.then((dialog) => ({ dialog }))]);
|
|
6019
|
-
nextDialog.cancel();
|
|
6020
|
-
if (winner === "failed" && seenDialog === void 0) {
|
|
6021
|
-
if (isDownloadStart(navigationError) || live.pendingDownload !== void 0) {
|
|
6022
|
-
const view = await this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
6023
|
-
if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
|
|
6024
|
-
this.applyDocumentType(live, view);
|
|
6025
|
-
return {
|
|
6026
|
-
view: await this.attachDownload(live, view, true),
|
|
6027
|
-
fresh,
|
|
6028
|
-
live
|
|
6029
|
-
};
|
|
6030
|
-
}
|
|
6031
|
-
throw navigationError;
|
|
6032
|
-
}
|
|
6033
|
-
if (linked.signal.aborted) return {
|
|
6034
|
-
view: {
|
|
6035
|
-
tab: live.id,
|
|
6036
|
-
url,
|
|
6037
|
-
reason: WAITING_STOPPED
|
|
6038
|
-
},
|
|
6039
|
-
fresh,
|
|
6040
|
-
live
|
|
6041
|
-
};
|
|
6042
|
-
const current = live.page.url();
|
|
6043
|
-
const address = current === "about:blank" ? url : current;
|
|
6044
|
-
this.windows.noteUrl(live, address);
|
|
6045
|
-
const held = seenDialog ?? (typeof winner === "object" ? winner.dialog : void 0);
|
|
6046
|
-
const dialog = held === void 0 ? void 0 : {
|
|
6047
|
-
type: held.type(),
|
|
6048
|
-
message: held.message()
|
|
6049
|
-
};
|
|
6050
|
-
if (held !== void 0) this.windows.holdDialog(live, held);
|
|
6051
|
-
else this.windows.onNavigated(live);
|
|
6052
|
-
const load = dialog !== void 0 ? {
|
|
6053
|
-
load: false,
|
|
6054
|
-
pendingRequests: 0,
|
|
6055
|
-
dialog
|
|
6056
|
-
} : await waitForLoad(live.page, linked.signal);
|
|
6057
|
-
if (load.dialog !== void 0) this.windows.holdDialog(live);
|
|
6058
|
-
let waited;
|
|
6059
|
-
if (waitFor !== void 0 && load.dialog === void 0) {
|
|
6060
|
-
const text = await waitForText(live.page, waitFor, linked.signal);
|
|
6061
|
-
waited = {
|
|
6062
|
-
met: text.met,
|
|
6063
|
-
seconds: text.seconds
|
|
6064
|
-
};
|
|
6065
|
-
if (text.dialog !== void 0) this.windows.holdDialog(live);
|
|
6066
|
-
}
|
|
6067
|
-
this.windows.touch(live);
|
|
6068
|
-
const shown = load.dialog !== void 0 ? await loadShown({
|
|
6069
|
-
pageUrl: canonical(address),
|
|
6070
|
-
...optionalFragment(address),
|
|
6071
|
-
snapshot: "",
|
|
6072
|
-
relocate: false,
|
|
6073
|
-
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
6074
|
-
}) : await this.discover(live, address, response, signal);
|
|
6075
|
-
this.shown.set(live.id, shown);
|
|
6076
|
-
const view = await this.pageFromShown(live, shown, {
|
|
6077
|
-
...snapshot ? { surface: "snapshot" } : {},
|
|
6078
|
-
...load.load ? {} : { incomplete: {
|
|
6079
|
-
load: load.load,
|
|
6080
|
-
pendingRequests: load.pendingRequests
|
|
6081
|
-
} },
|
|
6082
|
-
...load.dialog !== void 0 ? { dialog: load.dialog } : {},
|
|
6083
|
-
...waited !== void 0 ? { waited } : {}
|
|
6084
|
-
}, signal);
|
|
6085
|
-
return {
|
|
6086
|
-
view: await this.attachDownload(live, view, false),
|
|
6087
|
-
fresh,
|
|
6088
|
-
live
|
|
6089
|
-
};
|
|
6090
|
-
} catch (error) {
|
|
6091
|
-
nextDialog.cancel();
|
|
6092
|
-
if (this.control.ofTab(owner, live.id) === "person") throw await this.takenOver(live);
|
|
6093
|
-
if (linked.signal.aborted || error instanceof MhBrowserError && error.code === "INTERRUPTED") return {
|
|
6094
|
-
view: {
|
|
6095
|
-
tab: live.id,
|
|
6096
|
-
url,
|
|
6097
|
-
reason: WAITING_STOPPED
|
|
6098
|
-
},
|
|
6099
|
-
fresh,
|
|
6100
|
-
live
|
|
6101
|
-
};
|
|
6102
|
-
return {
|
|
6103
|
-
view: {
|
|
6104
|
-
tab: live.id,
|
|
6105
|
-
url,
|
|
6106
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
6107
|
-
},
|
|
6108
|
-
fresh,
|
|
6109
|
-
live
|
|
6110
|
-
};
|
|
6111
|
-
} finally {
|
|
6112
|
-
linked.release();
|
|
6113
|
-
}
|
|
6761
|
+
throw error;
|
|
6114
6762
|
}
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
};
|
|
6141
|
-
}
|
|
6142
|
-
if (op.kind === "handle_dialog") {
|
|
6143
|
-
const dialog = live.pendingDialog;
|
|
6144
|
-
if (dialog === void 0) throw new MhBrowserError("INVALID_ARGS", "this page has no dialog");
|
|
6145
|
-
const before = live.page.url();
|
|
6146
|
-
if (op.accept) await (op.promptText !== void 0 ? dialog.accept(op.promptText) : dialog.accept());
|
|
6147
|
-
else await dialog.dismiss();
|
|
6148
|
-
this.windows.clearDialog(live);
|
|
6149
|
-
const navigated = live.page.url() !== before;
|
|
6150
|
-
return {
|
|
6151
|
-
navigated,
|
|
6152
|
-
...settled(await settleAction(live.page, navigated, void 0, signal))
|
|
6153
|
-
};
|
|
6154
|
-
}
|
|
6155
|
-
const requests = trackActionRequests(live.page);
|
|
6156
|
-
const nextDialog = this.windows.nextDialog(live);
|
|
6157
|
-
const chooser = watchFileChooser(live.page);
|
|
6158
|
-
const snapshot = this.shown.get(live.id)?.snapshot ?? "";
|
|
6159
|
-
this.windows.beginDispatch(live.page);
|
|
6160
|
-
try {
|
|
6161
|
-
const finished = runAction(live.page, op, snapshot, signal).then((navigated) => ({
|
|
6162
|
-
kind: "done",
|
|
6163
|
-
navigated
|
|
6164
|
-
}));
|
|
6165
|
-
const appeared = nextDialog.dialog.then((dialog) => ({
|
|
6166
|
-
kind: "dialog",
|
|
6167
|
-
dialog
|
|
6168
|
-
}));
|
|
6169
|
-
const chose = chooser.opened.then((opened) => ({
|
|
6170
|
-
kind: "chooser",
|
|
6171
|
-
opened
|
|
6172
|
-
}));
|
|
6173
|
-
const winner = await Promise.race([
|
|
6174
|
-
finished,
|
|
6175
|
-
appeared,
|
|
6176
|
-
chose
|
|
6177
|
-
]).finally(() => {
|
|
6178
|
-
this.windows.endDispatch(live.page);
|
|
6179
|
-
});
|
|
6180
|
-
if (winner.kind === "dialog") {
|
|
6181
|
-
this.windows.holdDialog(live, winner.dialog);
|
|
6182
|
-
finished.catch(() => void 0);
|
|
6183
|
-
return {
|
|
6184
|
-
navigated: false,
|
|
6185
|
-
dialog: {
|
|
6186
|
-
type: winner.dialog.type(),
|
|
6187
|
-
message: winner.dialog.message()
|
|
6188
|
-
}
|
|
6189
|
-
};
|
|
6190
|
-
}
|
|
6191
|
-
if (winner.kind === "chooser") {
|
|
6192
|
-
finished.catch(() => void 0);
|
|
6763
|
+
const linked = this.control.link(owner, signal, live.id);
|
|
6764
|
+
if (reveal && !linked.signal.aborted) await live.page.bringToFront().catch(() => void 0);
|
|
6765
|
+
const nextDialog = this.windows.nextDialog(live);
|
|
6766
|
+
try {
|
|
6767
|
+
let seenDialog;
|
|
6768
|
+
const dialogDuringGoto = nextDialog.dialog.then((dialog) => {
|
|
6769
|
+
seenDialog = dialog;
|
|
6770
|
+
return dialog;
|
|
6771
|
+
});
|
|
6772
|
+
let navigationError;
|
|
6773
|
+
let response = null;
|
|
6774
|
+
const raced = this.goto(live.page, url, linked.signal).then((value) => {
|
|
6775
|
+
response = value;
|
|
6776
|
+
return "navigated";
|
|
6777
|
+
}, (error) => {
|
|
6778
|
+
navigationError = error;
|
|
6779
|
+
return "failed";
|
|
6780
|
+
});
|
|
6781
|
+
const winner = await Promise.race([raced, dialogDuringGoto.then((dialog) => ({ dialog }))]);
|
|
6782
|
+
nextDialog.cancel();
|
|
6783
|
+
if (winner === "failed" && seenDialog === void 0) {
|
|
6784
|
+
if (isDownloadStart(navigationError) || live.pendingDownload !== void 0) {
|
|
6785
|
+
const view = await this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
6786
|
+
if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
|
|
6787
|
+
this.applyDocumentType(live, view);
|
|
6193
6788
|
return {
|
|
6194
|
-
|
|
6195
|
-
|
|
6789
|
+
view: await this.attachDownload(live, view, true),
|
|
6790
|
+
fresh,
|
|
6791
|
+
live
|
|
6196
6792
|
};
|
|
6197
6793
|
}
|
|
6198
|
-
|
|
6199
|
-
load: true,
|
|
6200
|
-
pendingRequests: 0,
|
|
6201
|
-
dialog: this.chooserHeld(live, opened)
|
|
6202
|
-
}))]);
|
|
6203
|
-
return {
|
|
6204
|
-
navigated: winner.navigated,
|
|
6205
|
-
...settled(settledLoad)
|
|
6206
|
-
};
|
|
6207
|
-
} finally {
|
|
6208
|
-
requests.stop();
|
|
6209
|
-
nextDialog.cancel();
|
|
6210
|
-
chooser.stop();
|
|
6794
|
+
throw navigationError;
|
|
6211
6795
|
}
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
const onNav = (frame) => {
|
|
6221
|
-
if (frame.parentFrame() === null) navigated = true;
|
|
6796
|
+
if (linked.signal.aborted) return {
|
|
6797
|
+
view: {
|
|
6798
|
+
tab: live.id,
|
|
6799
|
+
url,
|
|
6800
|
+
reason: WAITING_STOPPED
|
|
6801
|
+
},
|
|
6802
|
+
fresh,
|
|
6803
|
+
live
|
|
6222
6804
|
};
|
|
6223
|
-
live.page.
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
if (this.control.ofTab(live.owner, live.id) === "person") throw await this.takenOver(live, note);
|
|
6231
|
-
throw new MhBrowserError(INTERRUPTED, `${WAITING_STOPPED}${note}`);
|
|
6232
|
-
}
|
|
6233
|
-
if (evaluated.kind === "dialog") {
|
|
6234
|
-
this.windows.holdDialog(live, evaluated.dialog);
|
|
6235
|
-
return {
|
|
6236
|
-
navigated: false,
|
|
6237
|
-
dialog: {
|
|
6238
|
-
type: evaluated.dialog.type(),
|
|
6239
|
-
message: evaluated.dialog.message()
|
|
6240
|
-
},
|
|
6241
|
-
summary: "",
|
|
6242
|
-
detail: writes,
|
|
6243
|
-
...counted
|
|
6244
|
-
};
|
|
6245
|
-
}
|
|
6246
|
-
if (evaluated.kind === "navigated") return {
|
|
6247
|
-
navigated: true,
|
|
6248
|
-
summary: NAVIGATED_DURING_EVALUATE,
|
|
6249
|
-
detail: writes,
|
|
6250
|
-
...counted
|
|
6251
|
-
};
|
|
6252
|
-
return {
|
|
6253
|
-
navigated,
|
|
6254
|
-
summary: evaluated.value,
|
|
6255
|
-
detail: writes,
|
|
6256
|
-
...counted
|
|
6257
|
-
};
|
|
6258
|
-
} finally {
|
|
6259
|
-
live.page.off("framenavigated", onNav);
|
|
6260
|
-
nextDialog.cancel();
|
|
6261
|
-
}
|
|
6262
|
-
}
|
|
6263
|
-
async uploadHeld(live, op, signal) {
|
|
6264
|
-
const chooser = live.fileChooser;
|
|
6265
|
-
if (chooser === void 0) throw new MhBrowserError("INVALID_ARGS", "No file chooser visible");
|
|
6266
|
-
const paths = op.paths;
|
|
6267
|
-
if (paths === void 0 || paths.length === 0) {
|
|
6268
|
-
live.fileChooser = void 0;
|
|
6269
|
-
return { navigated: false };
|
|
6270
|
-
}
|
|
6271
|
-
const requests = trackActionRequests(live.page);
|
|
6272
|
-
let navigated = false;
|
|
6273
|
-
const onNav = (frame) => {
|
|
6274
|
-
if (frame.parentFrame() === null) navigated = true;
|
|
6805
|
+
const current = live.page.url();
|
|
6806
|
+
const address = current === "about:blank" ? url : current;
|
|
6807
|
+
this.windows.noteUrl(live, address);
|
|
6808
|
+
const held = seenDialog ?? (typeof winner === "object" ? winner.dialog : void 0);
|
|
6809
|
+
const dialog = held === void 0 ? void 0 : {
|
|
6810
|
+
type: held.type(),
|
|
6811
|
+
message: held.message()
|
|
6275
6812
|
};
|
|
6276
|
-
|
|
6277
|
-
this.windows.
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6813
|
+
if (held !== void 0) this.windows.holdDialog(live, held);
|
|
6814
|
+
else this.windows.onNavigated(live);
|
|
6815
|
+
const load = dialog !== void 0 ? {
|
|
6816
|
+
load: false,
|
|
6817
|
+
pendingRequests: 0,
|
|
6818
|
+
dialog
|
|
6819
|
+
} : await waitForLoad(live.page, linked.signal);
|
|
6820
|
+
if (load.dialog !== void 0) this.windows.holdDialog(live);
|
|
6821
|
+
let waited;
|
|
6822
|
+
if (waitFor !== void 0 && load.dialog === void 0) {
|
|
6823
|
+
const text = await waitForText(live.page, waitFor, linked.signal);
|
|
6824
|
+
waited = {
|
|
6825
|
+
met: text.met,
|
|
6826
|
+
seconds: text.seconds
|
|
6827
|
+
};
|
|
6828
|
+
if (text.dialog !== void 0) this.windows.holdDialog(live);
|
|
6287
6829
|
}
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
...
|
|
6299
|
-
...
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
bytes: new Uint8Array(bytes)
|
|
6307
|
-
}
|
|
6308
|
-
};
|
|
6309
|
-
}
|
|
6310
|
-
/** A snapshot limited to a target, a depth, or boxes. Refs still resolve against the full one. */
|
|
6311
|
-
async scopedSnapshot(tab, op, signal) {
|
|
6312
|
-
const options = {
|
|
6313
|
-
mode: "ai",
|
|
6314
|
-
...op.depth !== void 0 ? { depth: op.depth } : {},
|
|
6315
|
-
...op.boxes === true ? { boxes: true } : {}
|
|
6316
|
-
};
|
|
6317
|
-
const snapshot = this.shown.get(tab.id)?.snapshot ?? "";
|
|
6318
|
-
const limit = capped(signal, READ_MS);
|
|
6319
|
-
if (op.target !== void 0) return untilAborted((await locate(tab.page, op.target, snapshot)).ariaSnapshot(options), limit);
|
|
6320
|
-
return untilAborted(tab.page.ariaSnapshot(options), limit);
|
|
6321
|
-
}
|
|
6322
|
-
/** A file chooser the action opened. The tab stays in use until it navigates. */
|
|
6323
|
-
chooserHeld(live, chooser) {
|
|
6324
|
-
live.fileChooser = chooser;
|
|
6325
|
-
this.windows.holdInput(live);
|
|
6830
|
+
this.windows.touch(live);
|
|
6831
|
+
const shown = load.dialog !== void 0 ? await loadShown({
|
|
6832
|
+
pageUrl: canonical(address),
|
|
6833
|
+
...optionalFragment(address),
|
|
6834
|
+
snapshot: "",
|
|
6835
|
+
relocate: false,
|
|
6836
|
+
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
6837
|
+
}) : await this.discover(live, address, response, signal);
|
|
6838
|
+
this.shown.set(live.id, shown);
|
|
6839
|
+
const view = await this.pageFromShown(live, shown, {
|
|
6840
|
+
...snapshot ? { surface: "snapshot" } : {},
|
|
6841
|
+
...load.load ? {} : { incomplete: {
|
|
6842
|
+
load: load.load,
|
|
6843
|
+
pendingRequests: load.pendingRequests
|
|
6844
|
+
} },
|
|
6845
|
+
...load.dialog !== void 0 ? { dialog: load.dialog } : {},
|
|
6846
|
+
...waited !== void 0 ? { waited } : {}
|
|
6847
|
+
}, signal);
|
|
6326
6848
|
return {
|
|
6327
|
-
|
|
6328
|
-
|
|
6849
|
+
view: await this.attachDownload(live, view, false),
|
|
6850
|
+
fresh,
|
|
6851
|
+
live
|
|
6329
6852
|
};
|
|
6330
|
-
}
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6853
|
+
} catch (error) {
|
|
6854
|
+
nextDialog.cancel();
|
|
6855
|
+
if (this.control.ofTab(owner, live.id) === "person") throw await this.takenOver(live);
|
|
6856
|
+
if (linked.signal.aborted || error instanceof MhBrowserError && error.code === "INTERRUPTED") return {
|
|
6857
|
+
view: {
|
|
6858
|
+
tab: live.id,
|
|
6859
|
+
url,
|
|
6860
|
+
reason: WAITING_STOPPED
|
|
6861
|
+
},
|
|
6862
|
+
fresh,
|
|
6863
|
+
live
|
|
6340
6864
|
};
|
|
6341
6865
|
return {
|
|
6342
|
-
|
|
6343
|
-
|
|
6866
|
+
view: {
|
|
6867
|
+
tab: live.id,
|
|
6868
|
+
url,
|
|
6869
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
6870
|
+
},
|
|
6871
|
+
fresh,
|
|
6872
|
+
live
|
|
6344
6873
|
};
|
|
6874
|
+
} finally {
|
|
6875
|
+
linked.release();
|
|
6345
6876
|
}
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
const relocated = main !== void 0 && !samePage(main.finalUrl, address);
|
|
6371
|
-
const domLinks = relocated ? [] : await readDomLinks(tab.page, signal);
|
|
6372
|
-
const previous = this.shown.get(tab.id);
|
|
6373
|
-
return loadShown({
|
|
6374
|
-
pageUrl: canonical(address),
|
|
6375
|
-
...optionalFragment(address),
|
|
6376
|
-
snapshot: await this.aria(tab, signal),
|
|
6377
|
-
...main !== void 0 && !relocated ? { main } : {},
|
|
6378
|
-
...domLinks.length > 0 ? { domLinks } : {},
|
|
6379
|
-
...previous !== void 0 ? { previous } : {},
|
|
6380
|
-
relocate: relocated,
|
|
6381
|
-
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
6382
|
-
});
|
|
6383
|
-
}
|
|
6384
|
-
fetchForRead(url, accept, ifNoneMatch, signal) {
|
|
6385
|
-
return this.fetch(url, {
|
|
6386
|
-
accept,
|
|
6387
|
-
...ifNoneMatch !== void 0 ? { ifNoneMatch } : {},
|
|
6388
|
-
...signal !== void 0 ? { signal } : {}
|
|
6389
|
-
});
|
|
6390
|
-
}
|
|
6391
|
-
async aria(tab, signal) {
|
|
6392
|
-
return untilAborted(tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
|
|
6393
|
-
this.ctx.logger.debug("aria snapshot failed: %s", errorText(error));
|
|
6394
|
-
return "";
|
|
6395
|
-
}), capped(signal, READ_MS));
|
|
6396
|
-
}
|
|
6397
|
-
async pageFromShown(tab, shown, extra, signal) {
|
|
6398
|
-
const current = shown ?? {
|
|
6399
|
-
pageUrl: canonical(tab.url),
|
|
6400
|
-
digest: "",
|
|
6401
|
-
kind: "snapshot",
|
|
6402
|
-
text: "",
|
|
6403
|
-
snapshot: ""
|
|
6404
|
-
};
|
|
6405
|
-
const title = tab.dialogHeld ? "" : await untilAborted(tab.page.title().catch((error) => {
|
|
6406
|
-
this.ctx.logger.debug("page title failed: %s", errorText(error));
|
|
6407
|
-
return "";
|
|
6408
|
-
}), capped(signal, READ_MS));
|
|
6409
|
-
this.windows.noteTitle(tab, title);
|
|
6410
|
-
const view = {
|
|
6411
|
-
tab: tab.id,
|
|
6412
|
-
url: current.pageUrl,
|
|
6413
|
-
title,
|
|
6414
|
-
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6415
|
-
representation: current.kind === "agent-view" ? {
|
|
6416
|
-
kind: "agent-view",
|
|
6417
|
-
text: current.text,
|
|
6418
|
-
...current.version !== void 0 ? { version: current.version } : {},
|
|
6419
|
-
...current.href !== void 0 ? { href: current.href } : {}
|
|
6420
|
-
} : {
|
|
6421
|
-
kind: "snapshot",
|
|
6422
|
-
text: current.snapshot,
|
|
6423
|
-
...current.agentViewError !== void 0 ? { agentViewError: current.agentViewError } : {}
|
|
6877
|
+
}
|
|
6878
|
+
async perform(live, op, signal) {
|
|
6879
|
+
if (op.kind === "snapshot") return { navigated: false };
|
|
6880
|
+
if (op.kind === "find") return {
|
|
6881
|
+
navigated: false,
|
|
6882
|
+
summary: findInSnapshot(await this.aria(live, signal), op)
|
|
6883
|
+
};
|
|
6884
|
+
if (op.kind === "evaluate") {
|
|
6885
|
+
const snapshot = this.shown.get(live.id)?.snapshot ?? "";
|
|
6886
|
+
const target = op.target === void 0 ? void 0 : await locate(live.page, op.target, snapshot);
|
|
6887
|
+
return this.evaluateOn(live, op, target, signal);
|
|
6888
|
+
}
|
|
6889
|
+
if (op.kind === "take_screenshot") return this.shoot(live, op, signal);
|
|
6890
|
+
if (op.kind === "file_upload") return this.uploadHeld(live, op, signal);
|
|
6891
|
+
if (op.kind === "wait_for") {
|
|
6892
|
+
const waitFor = waitFrom(op);
|
|
6893
|
+
assertWaitFor(waitFor);
|
|
6894
|
+
const text = await waitForText(live.page, waitFor, signal);
|
|
6895
|
+
if (text.dialog !== void 0) this.windows.holdDialog(live);
|
|
6896
|
+
return {
|
|
6897
|
+
navigated: false,
|
|
6898
|
+
waited: {
|
|
6899
|
+
met: text.met,
|
|
6900
|
+
seconds: text.seconds
|
|
6424
6901
|
},
|
|
6425
|
-
|
|
6902
|
+
...text.dialog !== void 0 ? { dialog: text.dialog } : {}
|
|
6426
6903
|
};
|
|
6427
|
-
tab.inPresence = view.inPresence;
|
|
6428
|
-
if (extra?.surface === "snapshot") view.representation = snapshotSurface(current);
|
|
6429
|
-
if (current.fragment !== void 0) view.fragment = current.fragment;
|
|
6430
|
-
if (current.status !== void 0) view.status = current.status;
|
|
6431
|
-
if (current.mediaType !== void 0) view.mediaType = current.mediaType;
|
|
6432
|
-
if (current.body !== void 0) view.body = current.body;
|
|
6433
|
-
if (extra?.dialog !== void 0) view.dialog = extra.dialog;
|
|
6434
|
-
if (extra?.incomplete !== void 0) view.incomplete = extra.incomplete;
|
|
6435
|
-
if (extra?.waited !== void 0) view.waited = extra.waited;
|
|
6436
|
-
return view;
|
|
6437
|
-
}
|
|
6438
|
-
/**
|
|
6439
|
-
* Read one presence page. A known origin is read on the request channel,
|
|
6440
|
-
* with If-None-Match when the document was read before. Anything else uses
|
|
6441
|
-
* a tab already on the address, or opens one and closes it again.
|
|
6442
|
-
*/
|
|
6443
|
-
async observePresence(owner, url, signal) {
|
|
6444
|
-
if (this.knownOrigin(url)) {
|
|
6445
|
-
const via = await this.readKnown(owner, url, signal);
|
|
6446
|
-
if (via === STOPPED) return observationOf(url, {
|
|
6447
|
-
url,
|
|
6448
|
-
reason: WAITING_STOPPED
|
|
6449
|
-
});
|
|
6450
|
-
if (via !== void 0) return observationOf(url, via);
|
|
6451
|
-
}
|
|
6452
|
-
const existing = this.tabOn(owner, url);
|
|
6453
|
-
if (existing !== void 0) return observationOf(url, await this.refreshView(existing, signal));
|
|
6454
|
-
const opened = await this.openOne(owner, url, void 0, signal, false);
|
|
6455
|
-
this.keepAgentView(opened);
|
|
6456
|
-
const view = opened.view;
|
|
6457
|
-
if (opened.fresh && "tab" in view && view.tab !== void 0) {
|
|
6458
|
-
const live = this.windows.get(owner, view.tab);
|
|
6459
|
-
if (live !== void 0 && this.closable(owner, live)) {
|
|
6460
|
-
const agent = "representation" in view && view.representation.kind === "agent-view";
|
|
6461
|
-
await this.windows.closeTab(live, agent ? void 0 : `${live.id} (idle)`);
|
|
6462
|
-
if (agent) {
|
|
6463
|
-
const shown = { ...view };
|
|
6464
|
-
delete shown.tab;
|
|
6465
|
-
return observationOf(url, shown);
|
|
6466
|
-
}
|
|
6467
|
-
}
|
|
6468
|
-
}
|
|
6469
|
-
return observationOf(url, view);
|
|
6470
6904
|
}
|
|
6471
|
-
|
|
6472
|
-
const
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6905
|
+
if (op.kind === "handle_dialog") {
|
|
6906
|
+
const dialog = live.pendingDialog;
|
|
6907
|
+
if (dialog === void 0) throw new MhBrowserError("INVALID_ARGS", "this page has no dialog");
|
|
6908
|
+
const before = live.page.url();
|
|
6909
|
+
if (op.accept) await (op.promptText !== void 0 ? dialog.accept(op.promptText) : dialog.accept());
|
|
6910
|
+
else await dialog.dismiss();
|
|
6911
|
+
this.windows.clearDialog(live);
|
|
6912
|
+
const navigated = live.page.url() !== before;
|
|
6913
|
+
return {
|
|
6914
|
+
navigated,
|
|
6915
|
+
...settled(await settleAction(live.page, navigated, void 0, signal))
|
|
6916
|
+
};
|
|
6476
6917
|
}
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6918
|
+
const requests = trackActionRequests(live.page);
|
|
6919
|
+
const nextDialog = this.windows.nextDialog(live);
|
|
6920
|
+
const chooser = watchFileChooser(live.page);
|
|
6921
|
+
const snapshot = this.shown.get(live.id)?.snapshot ?? "";
|
|
6922
|
+
this.windows.beginDispatch(live.page);
|
|
6923
|
+
try {
|
|
6924
|
+
const finished = runAction(live.page, op, snapshot, signal).then((navigated) => ({
|
|
6925
|
+
kind: "done",
|
|
6926
|
+
navigated
|
|
6927
|
+
}));
|
|
6928
|
+
const appeared = nextDialog.dialog.then((dialog) => ({
|
|
6929
|
+
kind: "dialog",
|
|
6930
|
+
dialog
|
|
6931
|
+
}));
|
|
6932
|
+
const chose = chooser.opened.then((opened) => ({
|
|
6933
|
+
kind: "chooser",
|
|
6934
|
+
opened
|
|
6935
|
+
}));
|
|
6936
|
+
const winner = await Promise.race([
|
|
6937
|
+
finished,
|
|
6938
|
+
appeared,
|
|
6939
|
+
chose
|
|
6940
|
+
]).finally(() => {
|
|
6941
|
+
this.windows.endDispatch(live.page);
|
|
6489
6942
|
});
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
}
|
|
6501
|
-
/** Only Session windows are capped; the person's window is theirs to close. */
|
|
6502
|
-
async enforceCap(owner) {
|
|
6503
|
-
if (owner === "person") return;
|
|
6504
|
-
await this.windows.enforceCap(owner);
|
|
6505
|
-
}
|
|
6506
|
-
/** Other tabs on this address: idle ones reload, occupied or taken ones are marked. */
|
|
6507
|
-
async refreshIdle(owner, url, except, signal) {
|
|
6508
|
-
const pageUrl = canonical(url);
|
|
6509
|
-
const tabs = this.windows.list(owner).map((info) => this.windows.get(owner, info.tab)).filter((item) => item !== void 0 && canonical(item.url) === pageUrl);
|
|
6510
|
-
for (const item of tabs) {
|
|
6511
|
-
if (item.id === except) continue;
|
|
6512
|
-
if (item.occupied || this.control.ofTab(owner, item.id) === "person") this.windows.markMayHaveChanged(item);
|
|
6513
|
-
else await this.navigateTab(item, item.page.url(), signal);
|
|
6943
|
+
if (winner.kind === "dialog") {
|
|
6944
|
+
this.windows.holdDialog(live, winner.dialog);
|
|
6945
|
+
finished.catch(() => void 0);
|
|
6946
|
+
return {
|
|
6947
|
+
navigated: false,
|
|
6948
|
+
dialog: {
|
|
6949
|
+
type: winner.dialog.type(),
|
|
6950
|
+
message: winner.dialog.message()
|
|
6951
|
+
}
|
|
6952
|
+
};
|
|
6514
6953
|
}
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
try {
|
|
6518
|
-
const response = await this.goto(live.page, url, signal);
|
|
6519
|
-
this.windows.onNavigated(live);
|
|
6520
|
-
const address = live.page.url();
|
|
6521
|
-
this.windows.noteUrl(live, address === "about:blank" ? url : address);
|
|
6522
|
-
const load = await waitForLoad(live.page, signal);
|
|
6523
|
-
if (load.dialog !== void 0) this.windows.holdDialog(live);
|
|
6524
|
-
this.windows.touch(live);
|
|
6525
|
-
const shown = await this.discover(live, live.page.url(), response, signal);
|
|
6526
|
-
this.shown.set(live.id, shown);
|
|
6527
|
-
const view = await this.pageFromShown(live, shown, {
|
|
6528
|
-
...load.load ? {} : { incomplete: {
|
|
6529
|
-
load: load.load,
|
|
6530
|
-
pendingRequests: load.pendingRequests
|
|
6531
|
-
} },
|
|
6532
|
-
...load.dialog !== void 0 ? { dialog: load.dialog } : {}
|
|
6533
|
-
}, signal);
|
|
6534
|
-
return this.attachDownload(live, view, false);
|
|
6535
|
-
} catch (error) {
|
|
6536
|
-
if (isDownloadStart(error) || live.pendingDownload !== void 0) {
|
|
6537
|
-
const view = await this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
6538
|
-
if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
|
|
6539
|
-
this.applyDocumentType(live, view);
|
|
6540
|
-
return this.attachDownload(live, view, true);
|
|
6541
|
-
}
|
|
6954
|
+
if (winner.kind === "chooser") {
|
|
6955
|
+
finished.catch(() => void 0);
|
|
6542
6956
|
return {
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
6957
|
+
navigated: false,
|
|
6958
|
+
dialog: this.chooserHeld(live, winner.opened)
|
|
6546
6959
|
};
|
|
6547
6960
|
}
|
|
6961
|
+
const settledLoad = await Promise.race([settleAction(live.page, winner.navigated, requests, signal), chooser.opened.then((opened) => ({
|
|
6962
|
+
load: true,
|
|
6963
|
+
pendingRequests: 0,
|
|
6964
|
+
dialog: this.chooserHeld(live, opened)
|
|
6965
|
+
}))]);
|
|
6966
|
+
return {
|
|
6967
|
+
navigated: winner.navigated,
|
|
6968
|
+
...settled(settledLoad)
|
|
6969
|
+
};
|
|
6970
|
+
} finally {
|
|
6971
|
+
requests.stop();
|
|
6972
|
+
nextDialog.cancel();
|
|
6973
|
+
chooser.stop();
|
|
6548
6974
|
}
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6975
|
+
}
|
|
6976
|
+
/**
|
|
6977
|
+
* `browser_evaluate`. A dialog the function opens is held like any other; a
|
|
6978
|
+
* stop or a takeover ends the wait and still reports the writes already sent.
|
|
6979
|
+
*/
|
|
6980
|
+
async evaluateOn(live, op, target, signal) {
|
|
6981
|
+
const nextDialog = this.windows.nextDialog(live);
|
|
6982
|
+
let navigated = false;
|
|
6983
|
+
const onNav = (frame) => {
|
|
6984
|
+
if (frame.parentFrame() === null) navigated = true;
|
|
6985
|
+
};
|
|
6986
|
+
live.page.on("framenavigated", onNav);
|
|
6987
|
+
try {
|
|
6988
|
+
const evaluated = await evaluateMarked(live.page, op.function, target, op.callId, signal, nextDialog.dialog);
|
|
6989
|
+
const writes = formatWrites(evaluated.writes);
|
|
6990
|
+
const counted = evaluated.writes.length === 0 ? {} : { writes: evaluated.writes };
|
|
6991
|
+
if (evaluated.kind === "stopped") {
|
|
6992
|
+
const note = writes === "" ? "" : `\n${writes}`;
|
|
6993
|
+
if (this.control.ofTab(live.owner, live.id) === "person") throw await this.takenOver(live, note);
|
|
6994
|
+
throw new MhBrowserError(INTERRUPTED, `${WAITING_STOPPED}${note}`);
|
|
6995
|
+
}
|
|
6996
|
+
if (evaluated.kind === "dialog") {
|
|
6997
|
+
this.windows.holdDialog(live, evaluated.dialog);
|
|
6566
6998
|
return {
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6999
|
+
navigated: false,
|
|
7000
|
+
dialog: {
|
|
7001
|
+
type: evaluated.dialog.type(),
|
|
7002
|
+
message: evaluated.dialog.message()
|
|
7003
|
+
},
|
|
7004
|
+
summary: "",
|
|
7005
|
+
detail: writes,
|
|
7006
|
+
...counted
|
|
6572
7007
|
};
|
|
6573
|
-
} catch (error) {
|
|
6574
|
-
if (signal?.aborted === true) throw new MhBrowserError(INTERRUPTED, "stopped after sending; it may have taken effect");
|
|
6575
|
-
this.ctx.logger.debug("declared action was not answered: %s", errorText(error));
|
|
6576
|
-
return { kind: "lost" };
|
|
6577
7008
|
}
|
|
7009
|
+
if (evaluated.kind === "navigated") return {
|
|
7010
|
+
navigated: true,
|
|
7011
|
+
summary: NAVIGATED_DURING_EVALUATE,
|
|
7012
|
+
detail: writes,
|
|
7013
|
+
...counted
|
|
7014
|
+
};
|
|
7015
|
+
return {
|
|
7016
|
+
navigated,
|
|
7017
|
+
summary: evaluated.value,
|
|
7018
|
+
detail: writes,
|
|
7019
|
+
...counted
|
|
7020
|
+
};
|
|
7021
|
+
} finally {
|
|
7022
|
+
live.page.off("framenavigated", onNav);
|
|
7023
|
+
nextDialog.cancel();
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
async uploadHeld(live, op, signal) {
|
|
7027
|
+
const chooser = live.fileChooser;
|
|
7028
|
+
if (chooser === void 0) throw new MhBrowserError("INVALID_ARGS", "No file chooser visible");
|
|
7029
|
+
const paths = op.paths;
|
|
7030
|
+
if (paths === void 0 || paths.length === 0) {
|
|
7031
|
+
live.fileChooser = void 0;
|
|
7032
|
+
return { navigated: false };
|
|
7033
|
+
}
|
|
7034
|
+
const requests = trackActionRequests(live.page);
|
|
7035
|
+
let navigated = false;
|
|
7036
|
+
const onNav = (frame) => {
|
|
7037
|
+
if (frame.parentFrame() === null) navigated = true;
|
|
7038
|
+
};
|
|
7039
|
+
live.page.on("framenavigated", onNav);
|
|
7040
|
+
this.windows.beginDispatch(live.page);
|
|
7041
|
+
try {
|
|
7042
|
+
await chooser.setFiles(paths, signal === void 0 ? {} : { signal });
|
|
7043
|
+
live.fileChooser = void 0;
|
|
7044
|
+
await settleAction(live.page, navigated, requests, signal);
|
|
7045
|
+
return { navigated };
|
|
7046
|
+
} finally {
|
|
7047
|
+
live.page.off("framenavigated", onNav);
|
|
7048
|
+
this.windows.endDispatch(live.page);
|
|
7049
|
+
requests.stop();
|
|
6578
7050
|
}
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
7051
|
+
}
|
|
7052
|
+
async shoot(live, op, signal) {
|
|
7053
|
+
if (op.fullPage === true && op.target !== void 0) throw new MhBrowserError("INVALID_ARGS", "fullPage cannot be used with element screenshots.");
|
|
7054
|
+
const type = op.type ?? "png";
|
|
7055
|
+
const snapshot = this.shown.get(live.id)?.snapshot ?? "";
|
|
7056
|
+
const locator = op.target === void 0 ? void 0 : await locate(live.page, op.target, snapshot);
|
|
7057
|
+
const options = {
|
|
7058
|
+
type,
|
|
7059
|
+
...type === "jpeg" ? { quality: 90 } : {},
|
|
7060
|
+
...op.scale !== void 0 ? { scale: op.scale } : {},
|
|
7061
|
+
...op.fullPage === true ? { fullPage: true } : {},
|
|
7062
|
+
...signal !== void 0 ? { signal } : {}
|
|
7063
|
+
};
|
|
7064
|
+
const bytes = locator === void 0 ? await live.page.screenshot(options) : await locator.screenshot(options);
|
|
7065
|
+
return {
|
|
7066
|
+
navigated: false,
|
|
7067
|
+
shot: {
|
|
7068
|
+
mediaType: `image/${type}`,
|
|
7069
|
+
bytes: new Uint8Array(bytes)
|
|
7070
|
+
}
|
|
7071
|
+
};
|
|
7072
|
+
}
|
|
7073
|
+
/** A snapshot limited to a target, a depth, or boxes. Refs still resolve against the full one. */
|
|
7074
|
+
async scopedSnapshot(tab, op, signal) {
|
|
7075
|
+
const options = {
|
|
7076
|
+
mode: "ai",
|
|
7077
|
+
...op.depth !== void 0 ? { depth: op.depth } : {},
|
|
7078
|
+
...op.boxes === true ? { boxes: true } : {}
|
|
7079
|
+
};
|
|
7080
|
+
const snapshot = this.shown.get(tab.id)?.snapshot ?? "";
|
|
7081
|
+
const limit = capped(signal, READ_MS);
|
|
7082
|
+
if (op.target !== void 0) return untilAborted((await locate(tab.page, op.target, snapshot)).ariaSnapshot(options), limit);
|
|
7083
|
+
return untilAborted(tab.page.ariaSnapshot(options), limit);
|
|
7084
|
+
}
|
|
7085
|
+
/** A file chooser the action opened. The tab stays in use until it navigates. */
|
|
7086
|
+
chooserHeld(live, chooser) {
|
|
7087
|
+
live.fileChooser = chooser;
|
|
7088
|
+
this.windows.holdInput(live);
|
|
7089
|
+
return {
|
|
7090
|
+
type: "filechooser",
|
|
7091
|
+
message: chooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
|
|
7092
|
+
};
|
|
7093
|
+
}
|
|
7094
|
+
async refreshSnapshot(tab, signal) {
|
|
7095
|
+
const previous = this.shown.get(tab.id);
|
|
7096
|
+
const snapshot = tab.dialogHeld ? "" : await this.aria(tab, signal);
|
|
7097
|
+
if (previous === void 0) return {
|
|
7098
|
+
pageUrl: canonical(tab.page.url()),
|
|
7099
|
+
digest: "",
|
|
7100
|
+
kind: "snapshot",
|
|
7101
|
+
text: snapshot,
|
|
7102
|
+
snapshot
|
|
7103
|
+
};
|
|
7104
|
+
return {
|
|
7105
|
+
...previous,
|
|
7106
|
+
snapshot
|
|
7107
|
+
};
|
|
7108
|
+
}
|
|
7109
|
+
async blockedView(tab) {
|
|
7110
|
+
const dialog = tab.pendingDialog;
|
|
7111
|
+
return this.pageFromShown(tab, this.shown.get(tab.id), {
|
|
7112
|
+
surface: "snapshot",
|
|
7113
|
+
...dialog !== void 0 ? { dialog: {
|
|
7114
|
+
type: dialog.type(),
|
|
7115
|
+
message: dialog.message()
|
|
7116
|
+
} } : {}
|
|
7117
|
+
});
|
|
7118
|
+
}
|
|
7119
|
+
async goto(page, url, signal) {
|
|
7120
|
+
this.windows.beginDispatch(page);
|
|
7121
|
+
try {
|
|
7122
|
+
return await page.goto(url, {
|
|
7123
|
+
waitUntil: "domcontentloaded",
|
|
7124
|
+
timeout: NAVIGATION_MS,
|
|
7125
|
+
...signal !== void 0 ? { signal } : {}
|
|
7126
|
+
});
|
|
7127
|
+
} finally {
|
|
7128
|
+
this.windows.endDispatch(page);
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
async discover(tab, address, response, signal) {
|
|
7132
|
+
const main = response === null ? void 0 : await mainDocument(response, signal);
|
|
7133
|
+
const relocated = main !== void 0 && !samePage(main.finalUrl, address);
|
|
7134
|
+
const domLinks = relocated ? [] : await readDomLinks(tab.page, signal);
|
|
7135
|
+
const previous = this.shown.get(tab.id);
|
|
7136
|
+
return loadShown({
|
|
7137
|
+
pageUrl: canonical(address),
|
|
7138
|
+
...optionalFragment(address),
|
|
7139
|
+
snapshot: await this.aria(tab, signal),
|
|
7140
|
+
...main !== void 0 && !relocated ? { main } : {},
|
|
7141
|
+
...domLinks.length > 0 ? { domLinks } : {},
|
|
7142
|
+
...previous !== void 0 ? { previous } : {},
|
|
7143
|
+
relocate: relocated,
|
|
7144
|
+
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
7145
|
+
});
|
|
7146
|
+
}
|
|
7147
|
+
fetchForRead(url, accept, ifNoneMatch, signal) {
|
|
7148
|
+
return this.fetch(url, {
|
|
7149
|
+
accept,
|
|
7150
|
+
...ifNoneMatch !== void 0 ? { ifNoneMatch } : {},
|
|
7151
|
+
...signal !== void 0 ? { signal } : {}
|
|
7152
|
+
});
|
|
7153
|
+
}
|
|
7154
|
+
async aria(tab, signal) {
|
|
7155
|
+
return untilAborted(tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
|
|
7156
|
+
this.ctx.logger.debug("aria snapshot failed: %s", errorText(error));
|
|
7157
|
+
return "";
|
|
7158
|
+
}), capped(signal, READ_MS));
|
|
7159
|
+
}
|
|
7160
|
+
async pageFromShown(tab, shown, extra, signal) {
|
|
7161
|
+
const current = shown ?? {
|
|
7162
|
+
pageUrl: canonical(tab.url),
|
|
7163
|
+
digest: "",
|
|
7164
|
+
kind: "snapshot",
|
|
7165
|
+
text: "",
|
|
7166
|
+
snapshot: ""
|
|
7167
|
+
};
|
|
7168
|
+
const title = tab.dialogHeld ? "" : await untilAborted(tab.page.title().catch((error) => {
|
|
7169
|
+
this.ctx.logger.debug("page title failed: %s", errorText(error));
|
|
7170
|
+
return "";
|
|
7171
|
+
}), capped(signal, READ_MS));
|
|
7172
|
+
this.windows.noteTitle(tab, title);
|
|
7173
|
+
const view = {
|
|
7174
|
+
tab: tab.id,
|
|
7175
|
+
url: current.pageUrl,
|
|
7176
|
+
title,
|
|
7177
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7178
|
+
representation: current.kind === "agent-view" ? {
|
|
7179
|
+
kind: "agent-view",
|
|
7180
|
+
text: current.text,
|
|
7181
|
+
...current.version !== void 0 ? { version: current.version } : {},
|
|
7182
|
+
...current.href !== void 0 ? { href: current.href } : {}
|
|
7183
|
+
} : {
|
|
7184
|
+
kind: "snapshot",
|
|
7185
|
+
text: current.snapshot,
|
|
7186
|
+
...current.agentViewError !== void 0 ? { agentViewError: current.agentViewError } : {}
|
|
7187
|
+
},
|
|
7188
|
+
inPresence: this.book.isShown(tab.owner, canonical(current.pageUrl))
|
|
7189
|
+
};
|
|
7190
|
+
tab.inPresence = view.inPresence;
|
|
7191
|
+
if (extra?.surface === "snapshot") view.representation = snapshotSurface(current);
|
|
7192
|
+
if (current.fragment !== void 0) view.fragment = current.fragment;
|
|
7193
|
+
if (current.status !== void 0) view.status = current.status;
|
|
7194
|
+
if (current.mediaType !== void 0) view.mediaType = current.mediaType;
|
|
7195
|
+
if (current.body !== void 0) view.body = current.body;
|
|
7196
|
+
if (extra?.dialog !== void 0) view.dialog = extra.dialog;
|
|
7197
|
+
if (extra?.incomplete !== void 0) view.incomplete = extra.incomplete;
|
|
7198
|
+
if (extra?.waited !== void 0) view.waited = extra.waited;
|
|
7199
|
+
return view;
|
|
7200
|
+
}
|
|
7201
|
+
/**
|
|
7202
|
+
* Read one presence page. A known origin is read on the request channel,
|
|
7203
|
+
* with If-None-Match when the document was read before. Anything else uses
|
|
7204
|
+
* a tab already on the address, or opens one and closes it again.
|
|
7205
|
+
*/
|
|
7206
|
+
async observePresence(owner, url, signal) {
|
|
7207
|
+
if (this.knownOrigin(url)) {
|
|
7208
|
+
const via = await this.readKnown(owner, url, signal);
|
|
7209
|
+
if (via === STOPPED) return observationOf(url, {
|
|
6583
7210
|
url,
|
|
6584
|
-
reason:
|
|
6585
|
-
};
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
7211
|
+
reason: WAITING_STOPPED
|
|
7212
|
+
});
|
|
7213
|
+
if (via !== void 0) return observationOf(url, via);
|
|
7214
|
+
}
|
|
7215
|
+
const existing = this.tabOn(owner, url);
|
|
7216
|
+
if (existing !== void 0) return observationOf(url, await this.refreshView(existing, signal));
|
|
7217
|
+
const opened = await this.openOne(owner, url, void 0, signal, false);
|
|
7218
|
+
this.keepAgentView(opened);
|
|
7219
|
+
const view = opened.view;
|
|
7220
|
+
if (opened.fresh && "tab" in view && view.tab !== void 0) {
|
|
7221
|
+
const live = this.windows.get(owner, view.tab);
|
|
7222
|
+
if (live !== void 0 && this.closable(owner, live)) {
|
|
7223
|
+
const agent = "representation" in view && view.representation.kind === "agent-view";
|
|
7224
|
+
await this.windows.closeTab(live, agent ? void 0 : `${live.id} (idle)`);
|
|
7225
|
+
if (agent) {
|
|
7226
|
+
const shown = { ...view };
|
|
7227
|
+
delete shown.tab;
|
|
7228
|
+
return observationOf(url, shown);
|
|
6598
7229
|
}
|
|
6599
|
-
const written = await writeDownload(root, filenameFrom(response.headers.get("content-disposition") ?? void 0, finalUrl), response.body ?? emptyBody());
|
|
6600
|
-
return {
|
|
6601
|
-
kind: "saved",
|
|
6602
|
-
path: written.path,
|
|
6603
|
-
url: finalUrl,
|
|
6604
|
-
bytes: written.bytes,
|
|
6605
|
-
sha256: written.sha256,
|
|
6606
|
-
observed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6607
|
-
...mediaType === void 0 ? {} : { mediaType }
|
|
6608
|
-
};
|
|
6609
|
-
} catch (error) {
|
|
6610
|
-
if (error instanceof MhBrowserError) throw error;
|
|
6611
|
-
return {
|
|
6612
|
-
kind: "error",
|
|
6613
|
-
url,
|
|
6614
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
6615
|
-
};
|
|
6616
7230
|
}
|
|
6617
7231
|
}
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
7232
|
+
return observationOf(url, view);
|
|
7233
|
+
}
|
|
7234
|
+
tabOn(owner, url) {
|
|
7235
|
+
const pageUrl = canonical(url);
|
|
7236
|
+
const tabs = this.windows.list(owner).map((info) => this.windows.get(owner, info.tab)).filter((tab) => tab !== void 0 && canonical(tab.url) === pageUrl);
|
|
7237
|
+
tabs.sort((left, right) => right.touchedAt - left.touchedAt);
|
|
7238
|
+
return tabs[0];
|
|
7239
|
+
}
|
|
7240
|
+
async refreshView(live, signal) {
|
|
7241
|
+
this.windows.touch(live);
|
|
7242
|
+
if (live.dialogHeld) return this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
7243
|
+
const address = live.page.url();
|
|
7244
|
+
const previous = this.shown.get(live.id);
|
|
7245
|
+
const shown = await loadShown({
|
|
7246
|
+
pageUrl: canonical(address),
|
|
7247
|
+
...optionalFragment(address),
|
|
7248
|
+
snapshot: await this.aria(live, signal),
|
|
7249
|
+
...previous !== void 0 ? { previous } : {},
|
|
7250
|
+
relocate: previous !== void 0 && !samePage(previous.pageUrl, address),
|
|
7251
|
+
fetch: (url, accept, ifNoneMatch) => this.fetchForRead(url, accept, ifNoneMatch, signal)
|
|
7252
|
+
});
|
|
7253
|
+
this.shown.set(live.id, shown);
|
|
7254
|
+
this.windows.noteUrl(live, address);
|
|
7255
|
+
return this.pageFromShown(live, shown, void 0, signal);
|
|
7256
|
+
}
|
|
7257
|
+
/** The most recently touched idle tab on this address. A tab the person holds is left alone. */
|
|
7258
|
+
idleTabOn(owner, url) {
|
|
7259
|
+
const pageUrl = canonical(url);
|
|
7260
|
+
const tabs = this.windows.list(owner).map((info) => this.windows.get(owner, info.tab)).filter((tab) => tab !== void 0 && canonical(tab.url) === pageUrl && !tab.occupied && this.control.ofTab(owner, tab.id) !== "person");
|
|
7261
|
+
tabs.sort((left, right) => right.touchedAt - left.touchedAt);
|
|
7262
|
+
return tabs[0];
|
|
7263
|
+
}
|
|
7264
|
+
/** Only Session windows are capped; the person's window is theirs to close. */
|
|
7265
|
+
async enforceCap(owner) {
|
|
7266
|
+
if (owner === "person") return;
|
|
7267
|
+
await this.windows.enforceCap(owner);
|
|
7268
|
+
}
|
|
7269
|
+
/** Other tabs on this address: idle ones reload, occupied or taken ones are marked. */
|
|
7270
|
+
async refreshIdle(owner, url, except, signal) {
|
|
7271
|
+
const pageUrl = canonical(url);
|
|
7272
|
+
const tabs = this.windows.list(owner).map((info) => this.windows.get(owner, info.tab)).filter((item) => item !== void 0 && canonical(item.url) === pageUrl);
|
|
7273
|
+
for (const item of tabs) {
|
|
7274
|
+
if (item.id === except) continue;
|
|
7275
|
+
if (item.occupied || this.control.ofTab(owner, item.id) === "person") this.windows.markMayHaveChanged(item);
|
|
7276
|
+
else await this.navigateTab(item, item.page.url(), signal);
|
|
6622
7277
|
}
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
live
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
7278
|
+
}
|
|
7279
|
+
async navigateTab(live, url, signal) {
|
|
7280
|
+
try {
|
|
7281
|
+
const response = await this.goto(live.page, url, signal);
|
|
7282
|
+
this.windows.onNavigated(live);
|
|
7283
|
+
const address = live.page.url();
|
|
7284
|
+
this.windows.noteUrl(live, address === "about:blank" ? url : address);
|
|
7285
|
+
const load = await waitForLoad(live.page, signal);
|
|
7286
|
+
if (load.dialog !== void 0) this.windows.holdDialog(live);
|
|
7287
|
+
this.windows.touch(live);
|
|
7288
|
+
const shown = await this.discover(live, live.page.url(), response, signal);
|
|
7289
|
+
this.shown.set(live.id, shown);
|
|
7290
|
+
const view = await this.pageFromShown(live, shown, {
|
|
7291
|
+
...load.load ? {} : { incomplete: {
|
|
7292
|
+
load: load.load,
|
|
7293
|
+
pendingRequests: load.pendingRequests
|
|
7294
|
+
} },
|
|
7295
|
+
...load.dialog !== void 0 ? { dialog: load.dialog } : {}
|
|
7296
|
+
}, signal);
|
|
7297
|
+
return this.attachDownload(live, view, false);
|
|
7298
|
+
} catch (error) {
|
|
7299
|
+
if (isDownloadStart(error) || live.pendingDownload !== void 0) {
|
|
7300
|
+
const view = await this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
7301
|
+
if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
|
|
7302
|
+
this.applyDocumentType(live, view);
|
|
7303
|
+
return this.attachDownload(live, view, true);
|
|
7304
|
+
}
|
|
7305
|
+
return {
|
|
7306
|
+
tab: live.id,
|
|
6635
7307
|
url,
|
|
6636
|
-
reason:
|
|
7308
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
7309
|
+
};
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
7312
|
+
async sendAction(request, signal) {
|
|
7313
|
+
const context = this.context;
|
|
7314
|
+
if (context === void 0) return { kind: "lost" };
|
|
7315
|
+
try {
|
|
7316
|
+
const response = await context.request.fetch(request.url, {
|
|
7317
|
+
method: request.method,
|
|
7318
|
+
headers: request.headers,
|
|
7319
|
+
maxRedirects: 0,
|
|
7320
|
+
maxRetries: 0,
|
|
7321
|
+
failOnStatusCode: false,
|
|
7322
|
+
...signal !== void 0 ? { signal } : {},
|
|
7323
|
+
...request.multipart !== void 0 ? { multipart: request.multipart } : {},
|
|
7324
|
+
...request.data !== void 0 ? { data: request.data } : {}
|
|
7325
|
+
});
|
|
7326
|
+
const headers = response.headers();
|
|
7327
|
+
const location = headers.location;
|
|
7328
|
+
const mediaType = headers["content-type"];
|
|
7329
|
+
return {
|
|
7330
|
+
kind: "response",
|
|
7331
|
+
status: response.status(),
|
|
7332
|
+
body: new Uint8Array(await untilAborted(response.body(), capped(signal, READ_MS))),
|
|
7333
|
+
...location === void 0 || location === "" ? {} : { location },
|
|
7334
|
+
...mediaType === void 0 ? {} : { mediaType }
|
|
6637
7335
|
};
|
|
6638
|
-
|
|
6639
|
-
|
|
7336
|
+
} catch (error) {
|
|
7337
|
+
if (signal?.aborted === true) throw new MhBrowserError(INTERRUPTED, "stopped after sending; it may have taken effect");
|
|
7338
|
+
this.ctx.logger.debug("declared action was not answered: %s", errorText(error));
|
|
7339
|
+
return { kind: "lost" };
|
|
7340
|
+
}
|
|
7341
|
+
}
|
|
7342
|
+
async saveResponse(url, root, signal) {
|
|
7343
|
+
const context = this.context;
|
|
7344
|
+
if (context === void 0) return {
|
|
7345
|
+
kind: "error",
|
|
7346
|
+
url,
|
|
7347
|
+
reason: "browser is not running"
|
|
7348
|
+
};
|
|
7349
|
+
try {
|
|
7350
|
+
const { response, finalUrl } = await getWithCookies(context, url, signal);
|
|
7351
|
+
const status = response.status;
|
|
7352
|
+
const mediaType = response.headers.get("content-type") ?? void 0;
|
|
7353
|
+
if (status < 200 || status >= 300) {
|
|
7354
|
+
await response.body?.cancel().catch(() => void 0);
|
|
6640
7355
|
return {
|
|
6641
7356
|
kind: "error",
|
|
6642
|
-
url,
|
|
6643
|
-
|
|
7357
|
+
url: finalUrl,
|
|
7358
|
+
status,
|
|
7359
|
+
reason: `HTTP ${String(status)}`
|
|
6644
7360
|
};
|
|
6645
7361
|
}
|
|
6646
|
-
const written = await writeDownload(
|
|
7362
|
+
const written = await writeDownload(root, filenameFrom(response.headers.get("content-disposition") ?? void 0, finalUrl), response.body ?? emptyBody());
|
|
6647
7363
|
return {
|
|
6648
7364
|
kind: "saved",
|
|
6649
7365
|
path: written.path,
|
|
6650
|
-
url,
|
|
7366
|
+
url: finalUrl,
|
|
6651
7367
|
bytes: written.bytes,
|
|
6652
7368
|
sha256: written.sha256,
|
|
6653
|
-
observed: (/* @__PURE__ */ new Date()).toISOString()
|
|
7369
|
+
observed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7370
|
+
...mediaType === void 0 ? {} : { mediaType }
|
|
7371
|
+
};
|
|
7372
|
+
} catch (error) {
|
|
7373
|
+
if (error instanceof MhBrowserError) throw error;
|
|
7374
|
+
return {
|
|
7375
|
+
kind: "error",
|
|
7376
|
+
url,
|
|
7377
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
6654
7378
|
};
|
|
6655
7379
|
}
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
}
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
await this.windows.attach(context, this.launchOptions.profileDir, { headed: !this.launchOptions.headless });
|
|
6685
|
-
} catch (error) {
|
|
6686
|
-
this.context = void 0;
|
|
6687
|
-
this.windows.dropPages();
|
|
6688
|
-
await this.closeContext(context);
|
|
6689
|
-
throw error;
|
|
6690
|
-
}
|
|
6691
|
-
context.browser()?.on("disconnected", () => {
|
|
6692
|
-
if (this.closing || this.context !== context) return;
|
|
6693
|
-
this.onExit();
|
|
6694
|
-
});
|
|
7380
|
+
}
|
|
7381
|
+
async attachDownload(live, view, wait) {
|
|
7382
|
+
const saved = await this.takePendingDownload(live, wait);
|
|
7383
|
+
if (saved !== void 0) view.download = saved;
|
|
7384
|
+
return view;
|
|
7385
|
+
}
|
|
7386
|
+
async takePendingDownload(live, wait) {
|
|
7387
|
+
if (live.pendingDownload === void 0 && wait) await live.page.waitForEvent("download", { timeout: DOWNLOAD_EVENT_MS }).catch(() => void 0);
|
|
7388
|
+
const download = live.pendingDownload;
|
|
7389
|
+
if (download === void 0) return void 0;
|
|
7390
|
+
live.pendingDownload = void 0;
|
|
7391
|
+
return this.savePlaywrightDownload(live.owner, download);
|
|
7392
|
+
}
|
|
7393
|
+
async savePlaywrightDownload(owner, download) {
|
|
7394
|
+
const target = this.workspaces.get(owner);
|
|
7395
|
+
const url = download.url();
|
|
7396
|
+
if (target === void 0) return {
|
|
7397
|
+
kind: "error",
|
|
7398
|
+
url,
|
|
7399
|
+
reason: NO_WORKSPACE_REASON
|
|
7400
|
+
};
|
|
7401
|
+
if ("denied" in target) {
|
|
7402
|
+
await download.cancel().catch(() => void 0);
|
|
7403
|
+
return {
|
|
7404
|
+
kind: "error",
|
|
7405
|
+
url,
|
|
7406
|
+
reason: target.denied
|
|
7407
|
+
};
|
|
6695
7408
|
}
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
}
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
this.
|
|
6719
|
-
const
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
7409
|
+
const written = await writeDownload(target.root, download.suggestedFilename(), await download.createReadStream());
|
|
7410
|
+
return {
|
|
7411
|
+
kind: "saved",
|
|
7412
|
+
path: written.path,
|
|
7413
|
+
url,
|
|
7414
|
+
bytes: written.bytes,
|
|
7415
|
+
sha256: written.sha256,
|
|
7416
|
+
observed: (/* @__PURE__ */ new Date()).toISOString()
|
|
7417
|
+
};
|
|
7418
|
+
}
|
|
7419
|
+
applyDocumentType(live, view) {
|
|
7420
|
+
const type = live.documentType;
|
|
7421
|
+
if (view.mediaType === void 0 && type !== void 0) {
|
|
7422
|
+
const bare = type.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
7423
|
+
if (bare !== "" && bare !== "text/html") view.mediaType = type;
|
|
7424
|
+
}
|
|
7425
|
+
if (view.status === void 0 && live.documentStatus !== void 0 && live.documentStatus >= 300) view.status = live.documentStatus;
|
|
7426
|
+
}
|
|
7427
|
+
/** A failed launch is logged here; calls that need the browser see it as `BROWSER_UNAVAILABLE`. */
|
|
7428
|
+
launch() {
|
|
7429
|
+
const started = this.start().catch((error) => {
|
|
7430
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
7431
|
+
this.ctx.logger.warn("browser did not start: %s", reason);
|
|
7432
|
+
const notFound = error instanceof BrowserNotFoundError ? error.notFound : void 0;
|
|
7433
|
+
throw new MhBrowserError(BROWSER_UNAVAILABLE, `the browser did not start: ${reason}`, void 0, notFound);
|
|
7434
|
+
});
|
|
7435
|
+
started.catch(() => void 0);
|
|
7436
|
+
return started;
|
|
7437
|
+
}
|
|
7438
|
+
async start() {
|
|
7439
|
+
const env = this.locateEnv();
|
|
7440
|
+
const remembered = await readLastBrowser(this.profileRoot);
|
|
7441
|
+
const configured = this.executablePath.get();
|
|
7442
|
+
const plan = await planLaunch({
|
|
7443
|
+
choice: this.browserName.get(),
|
|
7444
|
+
configured,
|
|
7445
|
+
root: this.profileRoot,
|
|
7446
|
+
split: this.splitProfile,
|
|
7447
|
+
env,
|
|
7448
|
+
remembered,
|
|
7449
|
+
bundled: bundledChromiumLocation(env.access)
|
|
7450
|
+
});
|
|
7451
|
+
const epoch = this.configEpoch;
|
|
7452
|
+
const context = await launchBrowser({
|
|
7453
|
+
browser: plan.browser,
|
|
7454
|
+
headless: this.headless,
|
|
7455
|
+
profileDir: plan.profileDir,
|
|
7456
|
+
...plan.channel ? {} : { channel: false },
|
|
7457
|
+
...plan.executablePath !== void 0 ? { executablePath: plan.executablePath } : {}
|
|
7458
|
+
}, env);
|
|
7459
|
+
const pid = await browserPid(context, plan.profileDir);
|
|
7460
|
+
if (pid !== void 0) this.pids.set(context, pid);
|
|
7461
|
+
if (this.closing) {
|
|
7462
|
+
await this.closeContext(context);
|
|
7463
|
+
return;
|
|
6726
7464
|
}
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
await
|
|
6730
|
-
|
|
7465
|
+
this.context = context;
|
|
7466
|
+
try {
|
|
7467
|
+
await this.windows.attach(context, plan.profileDir, { headed: !this.headless });
|
|
7468
|
+
} catch (error) {
|
|
6731
7469
|
this.context = void 0;
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
async closeContext(context) {
|
|
6736
|
-
await closeOrKill(() => context.close(), this.pids.get(context));
|
|
6737
|
-
this.pids.delete(context);
|
|
7470
|
+
this.windows.dropPages();
|
|
7471
|
+
await this.closeContext(context);
|
|
7472
|
+
throw error;
|
|
6738
7473
|
}
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
7474
|
+
if (plan.remember !== void 0) await writeLastBrowser(this.profileRoot, plan.remember).catch((error) => {
|
|
7475
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
7476
|
+
this.ctx.logger.warn("browser choice was not recorded: %s", reason);
|
|
7477
|
+
});
|
|
7478
|
+
if (this.configEpoch === epoch) this.relaunchWanted = false;
|
|
7479
|
+
context.browser()?.on("disconnected", () => {
|
|
7480
|
+
if (this.closing || this.replacing || this.context !== context) return;
|
|
7481
|
+
this.onExit();
|
|
7482
|
+
});
|
|
7483
|
+
}
|
|
7484
|
+
/** No window opens on exit: the person may have quit it. Calls on old tabs find none. */
|
|
7485
|
+
onExit() {
|
|
7486
|
+
this.windows.suppressPersonInput();
|
|
7487
|
+
this.windows.dropPages();
|
|
7488
|
+
this.context = void 0;
|
|
7489
|
+
this.exits.push(Date.now());
|
|
7490
|
+
this.exited = true;
|
|
7491
|
+
this.ready = Promise.resolve();
|
|
7492
|
+
}
|
|
7493
|
+
/**
|
|
7494
|
+
* The browser for a call that needs a page. One that is not running starts
|
|
7495
|
+
* again here. The exit limit stops unattended restarts; a person's request
|
|
7496
|
+
* is one start per click and skips it.
|
|
7497
|
+
*/
|
|
7498
|
+
up(byPerson = false) {
|
|
7499
|
+
if (!this.exited || this.closing) return this.ready;
|
|
7500
|
+
const now = Date.now();
|
|
7501
|
+
while (this.exits.length > 0 && now - (this.exits[0] ?? now) > RELAUNCH_WINDOW_MS) this.exits.shift();
|
|
7502
|
+
if (!byPerson && this.exits.length > RELAUNCH_LIMIT) {
|
|
7503
|
+
this.ctx.logger.warn("browser keeps stopping; not starting it again yet");
|
|
7504
|
+
return Promise.reject(new MhBrowserError(BROWSER_UNAVAILABLE, `the browser stopped or did not start ${String(this.exits.length)} times within ${String(RELAUNCH_WINDOW_MS / 1e3)} s; try again in a minute`));
|
|
7505
|
+
}
|
|
7506
|
+
this.exited = false;
|
|
7507
|
+
const started = this.launch();
|
|
7508
|
+
started.then(() => this.windows.resumePersonInput(), () => {
|
|
7509
|
+
this.exited = true;
|
|
7510
|
+
if (!this.closing) this.exits.push(Date.now());
|
|
7511
|
+
});
|
|
7512
|
+
this.ready = started;
|
|
7513
|
+
return started;
|
|
7514
|
+
}
|
|
7515
|
+
async stop() {
|
|
7516
|
+
this.windows.suppressPersonInput();
|
|
7517
|
+
await Promise.race([this.ready.catch(() => void 0), new Promise((resolve) => setTimeout(resolve, STOP_WAIT_MS))]);
|
|
7518
|
+
const context = this.context;
|
|
7519
|
+
this.context = void 0;
|
|
7520
|
+
if (context !== void 0) await this.closeContext(context);
|
|
7521
|
+
}
|
|
7522
|
+
/** Close a context the service is done with, and kill it if close stalls. */
|
|
7523
|
+
async closeContext(context) {
|
|
7524
|
+
await closeOrKill(() => context.close(), this.pids.get(context));
|
|
7525
|
+
this.pids.delete(context);
|
|
7526
|
+
}
|
|
7527
|
+
refuseIfPerson(owner, tab) {
|
|
7528
|
+
if (this.control.ofTab(owner, tab) !== "person") return;
|
|
7529
|
+
throw new MhBrowserError("PERSON_IN_CONTROL", "the person is using this tab; call browser_handover with this tab to wait until they hand it back, or work in another tab");
|
|
7530
|
+
}
|
|
7531
|
+
async takenOver(live, note = "") {
|
|
7532
|
+
try {
|
|
7533
|
+
const page = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" });
|
|
7534
|
+
return new MhBrowserError("TAKEN_OVER", `${MAY_HAVE_TAKEN_EFFECT}${note}`, page);
|
|
7535
|
+
} catch {
|
|
7536
|
+
return new MhBrowserError("TAKEN_OVER", `${MAY_HAVE_TAKEN_EFFECT}${note}`);
|
|
6742
7537
|
}
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
7538
|
+
}
|
|
7539
|
+
presenceUrls(owner, tabs) {
|
|
7540
|
+
const known = new Set(this.book.entries(owner).map((page) => page.url));
|
|
7541
|
+
const urls = [];
|
|
7542
|
+
for (const id of tabs) {
|
|
7543
|
+
const address = this.windows.urlOf(owner, id);
|
|
7544
|
+
if (address === void 0) continue;
|
|
7545
|
+
const url = canonical(address);
|
|
7546
|
+
if (!known.has(url) || urls.includes(url)) continue;
|
|
7547
|
+
urls.push(url);
|
|
6750
7548
|
}
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
7549
|
+
return urls;
|
|
7550
|
+
}
|
|
7551
|
+
async snapshotNamed(owner, tab, signal) {
|
|
7552
|
+
if (tab === void 0) return void 0;
|
|
7553
|
+
const live = this.windows.get(owner, tab);
|
|
7554
|
+
if (live === void 0) return void 0;
|
|
7555
|
+
const shown = await this.refreshSnapshot(live, signal);
|
|
7556
|
+
this.shown.set(live.id, shown);
|
|
7557
|
+
return this.pageFromShown(live, shown, { surface: "snapshot" }, signal);
|
|
7558
|
+
}
|
|
7559
|
+
exclusive(owner, op, signal) {
|
|
7560
|
+
const queued = enqueue(this.queues.get(owner) ?? Promise.resolve(), op, signal);
|
|
7561
|
+
this.queues.set(owner, queued.tail);
|
|
7562
|
+
queued.tail.then(() => {
|
|
7563
|
+
if (this.queues.get(owner) === queued.tail) this.queues.delete(owner);
|
|
7564
|
+
if (this.queues.size === 0) this.relaunchIfIdle();
|
|
7565
|
+
});
|
|
7566
|
+
return queued.run;
|
|
7567
|
+
}
|
|
7568
|
+
locateEnv() {
|
|
7569
|
+
return this.locator ?? hostLocateEnv();
|
|
7570
|
+
}
|
|
7571
|
+
idle() {
|
|
7572
|
+
return this.queues.size === 0 && !this.control.busy();
|
|
7573
|
+
}
|
|
7574
|
+
/**
|
|
7575
|
+
* Close and start again when a configuration change is waiting and nothing
|
|
7576
|
+
* is using the browser. A browser that is not running keeps the new values
|
|
7577
|
+
* for the next start. The close is not counted as a crash.
|
|
7578
|
+
*/
|
|
7579
|
+
relaunchIfIdle() {
|
|
7580
|
+
if (!this.relaunchWanted || this.closing || this.replacing) return;
|
|
7581
|
+
if (!this.idle()) return;
|
|
7582
|
+
if (this.context === void 0) {
|
|
7583
|
+
if (this.exited) {
|
|
7584
|
+
this.relaunchWanted = false;
|
|
7585
|
+
return;
|
|
6760
7586
|
}
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
async snapshotNamed(owner, tab, signal) {
|
|
6764
|
-
if (tab === void 0) return void 0;
|
|
6765
|
-
const live = this.windows.get(owner, tab);
|
|
6766
|
-
if (live === void 0) return void 0;
|
|
6767
|
-
const shown = await this.refreshSnapshot(live, signal);
|
|
6768
|
-
this.shown.set(live.id, shown);
|
|
6769
|
-
return this.pageFromShown(live, shown, { surface: "snapshot" }, signal);
|
|
6770
|
-
}
|
|
6771
|
-
exclusive(owner, op, signal) {
|
|
6772
|
-
const queued = enqueue(this.queues.get(owner) ?? Promise.resolve(), op, signal);
|
|
6773
|
-
this.queues.set(owner, queued.tail);
|
|
6774
|
-
queued.tail.then(() => {
|
|
6775
|
-
if (this.queues.get(owner) === queued.tail) this.queues.delete(owner);
|
|
7587
|
+
this.ready.then(() => this.relaunchIfIdle(), () => {
|
|
7588
|
+
this.relaunchWanted = false;
|
|
6776
7589
|
});
|
|
6777
|
-
return
|
|
7590
|
+
return;
|
|
6778
7591
|
}
|
|
6779
|
-
|
|
6780
|
-
|
|
7592
|
+
this.relaunchWanted = false;
|
|
7593
|
+
this.replacing = true;
|
|
7594
|
+
const run = this.replaceBrowser();
|
|
7595
|
+
this.ready = run;
|
|
7596
|
+
const finish = () => {
|
|
7597
|
+
this.replacing = false;
|
|
7598
|
+
if (this.relaunchWanted) this.relaunchIfIdle();
|
|
7599
|
+
};
|
|
7600
|
+
run.then(finish, finish);
|
|
7601
|
+
}
|
|
7602
|
+
async replaceBrowser() {
|
|
7603
|
+
this.windows.suppressPersonInput();
|
|
7604
|
+
const context = this.context;
|
|
7605
|
+
this.context = void 0;
|
|
7606
|
+
this.windows.dropPages();
|
|
7607
|
+
if (context !== void 0) await this.closeContext(context);
|
|
7608
|
+
if (this.closing) return;
|
|
7609
|
+
this.exited = false;
|
|
7610
|
+
try {
|
|
7611
|
+
await this.launch();
|
|
7612
|
+
this.windows.resumePersonInput();
|
|
7613
|
+
} catch (error) {
|
|
7614
|
+
this.exited = true;
|
|
7615
|
+
throw error;
|
|
7616
|
+
}
|
|
7617
|
+
}
|
|
7618
|
+
};
|
|
6781
7619
|
const MAX_REDIRECTS = 10;
|
|
6782
7620
|
/** GET with the profile's cookies for each hop, so the body can be streamed. */
|
|
6783
7621
|
async function getWithCookies(context, url, signal) {
|
|
@@ -7541,6 +8379,7 @@ const inject = [
|
|
|
7541
8379
|
];
|
|
7542
8380
|
const Config = browserConfig;
|
|
7543
8381
|
async function apply(ctx, config) {
|
|
8382
|
+
const resolved = browserSettings(config);
|
|
7544
8383
|
ctx.plugin({
|
|
7545
8384
|
name: "mh-browser-slot",
|
|
7546
8385
|
inject: ["browserUse", "mhBrowser"],
|
|
@@ -7548,16 +8387,30 @@ async function apply(ctx, config) {
|
|
|
7548
8387
|
inner.browserUse.register("mh-browser");
|
|
7549
8388
|
}
|
|
7550
8389
|
});
|
|
7551
|
-
await ctx.plugin(MhBrowserService,
|
|
8390
|
+
await ctx.plugin(MhBrowserService, resolved);
|
|
8391
|
+
ctx.plugin(browserRoutesPlugin);
|
|
7552
8392
|
await ctx.plugin({
|
|
7553
8393
|
name: "mh-browser-runtime",
|
|
7554
8394
|
inject: ["mhBrowser", ...inject],
|
|
7555
8395
|
async apply(inner) {
|
|
7556
8396
|
const reader = await registerProjections(inner);
|
|
7557
|
-
registerBrowserTools(inner, reader, !
|
|
8397
|
+
registerBrowserTools(inner, reader, !resolved.headless);
|
|
7558
8398
|
registerPresence(inner, reader);
|
|
7559
8399
|
}
|
|
7560
8400
|
});
|
|
7561
8401
|
}
|
|
8402
|
+
/**
|
|
8403
|
+
* The loader passes a config it has already resolved, including the volatile
|
|
8404
|
+
* fields. A direct caller passes the raw values, which are resolved here.
|
|
8405
|
+
* Resolving twice rejects a volatile value as if it were the wrong type.
|
|
8406
|
+
*/
|
|
8407
|
+
function browserSettings(config) {
|
|
8408
|
+
if (isResolved(config)) return config;
|
|
8409
|
+
return browserConfig(config);
|
|
8410
|
+
}
|
|
8411
|
+
function isResolved(config) {
|
|
8412
|
+
const browser = config.browser;
|
|
8413
|
+
return typeof browser === "object" && browser !== null && "get" in browser;
|
|
8414
|
+
}
|
|
7562
8415
|
//#endregion
|
|
7563
8416
|
export { Config, MhBrowserError, MhBrowserService, UNKNOWN_TAB, UNKNOWN_TAB_REASON, apply, inject, name, readAgentView, unknownTab };
|