@mind-harness/mh-browser 0.2.4 → 0.2.6
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/lib/client.cjs +772 -51
- package/lib/index.js +944 -316
- package/lib/typert.host.js +88 -4
- package/lib/typert.remote-client.d.ts +4 -0
- package/lib/typert.remote-client.js +45 -1
- package/lib/types/client.d.ts +15 -1
- package/lib/types/config.d.ts +23 -10
- package/lib/types/index.d.ts +23 -2
- package/lib/types/locales.d.ts +22 -0
- package/lib/types/location.d.ts +12 -0
- package/lib/types/location.js +3 -0
- package/lib/types/service/abortable.d.ts +24 -0
- package/lib/types/service/browser.d.ts +11 -5
- package/lib/types/service/control.d.ts +5 -0
- package/lib/types/service/extension.d.ts +7 -6
- package/lib/types/service/locate.d.ts +65 -0
- package/lib/types/service/service.d.ts +34 -3
- package/lib/types/service/test-hooks.d.ts +3 -0
- package/lib/types/service/windows.d.ts +10 -3
- package/lib/types/settings-form.d.ts +142 -0
- package/lib/types/settings.d.ts +121 -0
- package/lib/types/tools/given.d.ts +5 -0
- package/lib/types/typeprobe.d.ts +49 -0
- package/package.json +10 -2
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,11 +6,13 @@ 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";
|
|
9
|
+
import { accessSync, constants, readFileSync, realpathSync, statSync } from "node:fs";
|
|
10
10
|
import { lstat, mkdir, open, readFile, readlink, realpath, unlink, writeFile } from "node:fs/promises";
|
|
11
11
|
import { z as z$1 } from "zod";
|
|
12
12
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
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
|
|
@@ -20,7 +22,8 @@ const browserConfig = z.object({
|
|
|
20
22
|
"chrome",
|
|
21
23
|
"msedge",
|
|
22
24
|
"chromium"
|
|
23
|
-
]).default("chrome").description("Which
|
|
25
|
+
]).default("chrome").volatile().description("Which browser to drive. chromium is the one Playwright downloads. A new value restarts the browser once it is idle."),
|
|
26
|
+
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
27
|
profileDir: z.string().description("Browser profile directory, kept between runs. Defaults to mh/profiles/<dsh profile>/browser under the dsh home."),
|
|
25
28
|
headless: z.boolean().default(false).description("Run the browser without a window.")
|
|
26
29
|
});
|
|
@@ -33,6 +36,146 @@ function defaultProfileDir(profile, home = resolveDshHome()) {
|
|
|
33
36
|
return profile === void 0 ? join(home, "mh", "browser") : join(home, "mh", "profiles", profile.name, "browser");
|
|
34
37
|
}
|
|
35
38
|
//#endregion
|
|
39
|
+
//#region lib/types/types.js
|
|
40
|
+
var MhBrowserError = class extends Error {
|
|
41
|
+
code;
|
|
42
|
+
page;
|
|
43
|
+
constructor(code, message, page) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "MhBrowserError";
|
|
46
|
+
this.code = code;
|
|
47
|
+
if (page !== void 0) this.page = page;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/** Owner of the person's own window. It belongs to no Session. */
|
|
51
|
+
const PERSON_OWNER = "person";
|
|
52
|
+
const UNKNOWN_TAB = "UNKNOWN_TAB";
|
|
53
|
+
const UNKNOWN_TAB_REASON = "no such tab in this session";
|
|
54
|
+
/**
|
|
55
|
+
* Internal: a wait ended because its signal aborted. A tool never reports it:
|
|
56
|
+
* a stopped call ends as the tool runtime's abort, a takeover as `TAKEN_OVER`.
|
|
57
|
+
*/
|
|
58
|
+
const INTERRUPTED = "INTERRUPTED";
|
|
59
|
+
/** The browser did not start or could not be restarted. */
|
|
60
|
+
const BROWSER_UNAVAILABLE = "BROWSER_UNAVAILABLE";
|
|
61
|
+
function unknownTab() {
|
|
62
|
+
return {
|
|
63
|
+
url: "",
|
|
64
|
+
reason: UNKNOWN_TAB_REASON
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region lib/types/service/abortable.js
|
|
69
|
+
const WAITING_STOPPED$1 = "stopped while waiting";
|
|
70
|
+
/** A wait ended because its signal aborted, including a deadline synthesized here. */
|
|
71
|
+
function stopped$1() {
|
|
72
|
+
return new MhBrowserError(INTERRUPTED, WAITING_STOPPED$1);
|
|
73
|
+
}
|
|
74
|
+
function isInterrupted(error) {
|
|
75
|
+
return error instanceof MhBrowserError && error.code === "INTERRUPTED";
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `promise`, or a rejection when `signal` aborts. The original promise is
|
|
79
|
+
* always observed, so abandoning it leaves no unhandled rejection.
|
|
80
|
+
*/
|
|
81
|
+
function untilAborted(promise, signal) {
|
|
82
|
+
const settled = promise.then((value) => ({
|
|
83
|
+
ok: true,
|
|
84
|
+
value
|
|
85
|
+
}), (error) => ({
|
|
86
|
+
ok: false,
|
|
87
|
+
error
|
|
88
|
+
}));
|
|
89
|
+
if (signal === void 0) return unwrap(settled);
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
let done = false;
|
|
92
|
+
const finish = (run) => {
|
|
93
|
+
if (done) return;
|
|
94
|
+
done = true;
|
|
95
|
+
signal.removeEventListener("abort", onAbort);
|
|
96
|
+
run();
|
|
97
|
+
};
|
|
98
|
+
const onAbort = () => {
|
|
99
|
+
finish(() => reject(stopped$1()));
|
|
100
|
+
};
|
|
101
|
+
if (signal.aborted) {
|
|
102
|
+
finish(() => reject(stopped$1()));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
106
|
+
settled.then((result) => {
|
|
107
|
+
finish(() => {
|
|
108
|
+
if (result.ok) resolve(result.value);
|
|
109
|
+
else reject(result.error);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** `signal`, a deadline of `ms`, or whichever of the two aborts first. */
|
|
115
|
+
function capped(signal, ms) {
|
|
116
|
+
const limit = AbortSignal.timeout(ms);
|
|
117
|
+
if (signal === void 0) return limit;
|
|
118
|
+
return AbortSignal.any([signal, limit]);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Run `op` once `previous` settles. Aborting `signal` while waiting rejects
|
|
122
|
+
* `run` and does not call `op`. `tail` still follows `previous`, and `op`
|
|
123
|
+
* when `op` has started, so a later caller cannot overlap the work.
|
|
124
|
+
*/
|
|
125
|
+
function enqueue(previous, op, signal) {
|
|
126
|
+
let started = Promise.resolve();
|
|
127
|
+
let began = false;
|
|
128
|
+
return {
|
|
129
|
+
run: new Promise((resolve, reject) => {
|
|
130
|
+
let settled = false;
|
|
131
|
+
const fail = () => {
|
|
132
|
+
if (settled) return;
|
|
133
|
+
settled = true;
|
|
134
|
+
reject(stopped$1());
|
|
135
|
+
};
|
|
136
|
+
const onAbort = () => {
|
|
137
|
+
if (began) return;
|
|
138
|
+
fail();
|
|
139
|
+
};
|
|
140
|
+
const start = () => {
|
|
141
|
+
signal?.removeEventListener("abort", onAbort);
|
|
142
|
+
if (settled || signal?.aborted === true) {
|
|
143
|
+
fail();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
let work;
|
|
147
|
+
try {
|
|
148
|
+
began = true;
|
|
149
|
+
work = op();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
started = Promise.reject(error);
|
|
152
|
+
started.catch(() => void 0);
|
|
153
|
+
if (!settled) {
|
|
154
|
+
settled = true;
|
|
155
|
+
reject(error);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
started = work;
|
|
160
|
+
resolve(work);
|
|
161
|
+
};
|
|
162
|
+
if (signal?.aborted === true) {
|
|
163
|
+
fail();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (signal !== void 0) signal.addEventListener("abort", onAbort, { once: true });
|
|
167
|
+
previous.then(start, start);
|
|
168
|
+
}),
|
|
169
|
+
tail: previous.then(() => started, () => started).then(() => void 0, () => void 0)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function unwrap(settled) {
|
|
173
|
+
return settled.then((result) => {
|
|
174
|
+
if (result.ok) return result.value;
|
|
175
|
+
throw result.error;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
36
179
|
//#region lib/types/representation/agent-view.js
|
|
37
180
|
const AGENT_VIEW_VERSION = "0.1";
|
|
38
181
|
const AGENT_VIEW_ACCEPT = "text/markdown; variant=agent-view";
|
|
@@ -446,35 +589,6 @@ function formatSeconds(seconds) {
|
|
|
446
589
|
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
447
590
|
}
|
|
448
591
|
//#endregion
|
|
449
|
-
//#region lib/types/types.js
|
|
450
|
-
var MhBrowserError = class extends Error {
|
|
451
|
-
code;
|
|
452
|
-
page;
|
|
453
|
-
constructor(code, message, page) {
|
|
454
|
-
super(message);
|
|
455
|
-
this.name = "MhBrowserError";
|
|
456
|
-
this.code = code;
|
|
457
|
-
if (page !== void 0) this.page = page;
|
|
458
|
-
}
|
|
459
|
-
};
|
|
460
|
-
/** Owner of the person's own window. It belongs to no Session. */
|
|
461
|
-
const PERSON_OWNER = "person";
|
|
462
|
-
const UNKNOWN_TAB = "UNKNOWN_TAB";
|
|
463
|
-
const UNKNOWN_TAB_REASON = "no such tab in this session";
|
|
464
|
-
/**
|
|
465
|
-
* Internal: a wait ended because its signal aborted. A tool never reports it:
|
|
466
|
-
* a stopped call ends as the tool runtime's abort, a takeover as `TAKEN_OVER`.
|
|
467
|
-
*/
|
|
468
|
-
const INTERRUPTED = "INTERRUPTED";
|
|
469
|
-
/** The browser did not start or could not be restarted. */
|
|
470
|
-
const BROWSER_UNAVAILABLE = "BROWSER_UNAVAILABLE";
|
|
471
|
-
function unknownTab() {
|
|
472
|
-
return {
|
|
473
|
-
url: "",
|
|
474
|
-
reason: UNKNOWN_TAB_REASON
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
//#endregion
|
|
478
592
|
//#region lib/types/tools/session.js
|
|
479
593
|
/** dsh shows the model only the message, so the message starts with the code. */
|
|
480
594
|
var BrowserToolError = class extends HarnessError {
|
|
@@ -694,6 +808,18 @@ function visit(value, depth, found) {
|
|
|
694
808
|
for (const item of Object.values(row)) visit(item, depth + 1, found);
|
|
695
809
|
}
|
|
696
810
|
//#endregion
|
|
811
|
+
//#region lib/types/tools/given.js
|
|
812
|
+
/** A string the caller gave. Blank is the same as omitting it; the text itself is kept. */
|
|
813
|
+
function givenString(value) {
|
|
814
|
+
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
815
|
+
return value;
|
|
816
|
+
}
|
|
817
|
+
/** Seconds to wait. Zero is the same as omitting it; any other number is kept. */
|
|
818
|
+
function givenTime(value) {
|
|
819
|
+
if (typeof value !== "number" || value === 0) return void 0;
|
|
820
|
+
return value;
|
|
821
|
+
}
|
|
822
|
+
//#endregion
|
|
697
823
|
//#region lib/types/tools/delivery.js
|
|
698
824
|
/** Every mh-browser tool result's `meta.by`; a result without it is not a delivery. */
|
|
699
825
|
const META_BY = "mh-browser";
|
|
@@ -1370,9 +1496,9 @@ const OPS = [
|
|
|
1370
1496
|
},
|
|
1371
1497
|
build: async (args) => ({
|
|
1372
1498
|
kind: "wait_for",
|
|
1373
|
-
...
|
|
1374
|
-
...
|
|
1375
|
-
...
|
|
1499
|
+
...stringField("text", args.text),
|
|
1500
|
+
...stringField("textGone", args.textGone),
|
|
1501
|
+
...timeField(args.time)
|
|
1376
1502
|
})
|
|
1377
1503
|
},
|
|
1378
1504
|
{
|
|
@@ -1399,7 +1525,7 @@ const OPS = [
|
|
|
1399
1525
|
filename: true,
|
|
1400
1526
|
build: async (args) => ({
|
|
1401
1527
|
kind: "snapshot",
|
|
1402
|
-
...
|
|
1528
|
+
...stringField("target", args.target),
|
|
1403
1529
|
...typeof args.depth === "number" ? { depth: args.depth } : {},
|
|
1404
1530
|
...args.boxes === true ? { boxes: true } : {}
|
|
1405
1531
|
})
|
|
@@ -1428,8 +1554,8 @@ const OPS = [
|
|
|
1428
1554
|
filename: true,
|
|
1429
1555
|
build: async (args) => ({
|
|
1430
1556
|
kind: "find",
|
|
1431
|
-
...
|
|
1432
|
-
...
|
|
1557
|
+
...stringField("text", args.text),
|
|
1558
|
+
...stringField("regex", args.regex),
|
|
1433
1559
|
...typeof args.maxResults === "number" ? { maxResults: args.maxResults } : {}
|
|
1434
1560
|
})
|
|
1435
1561
|
},
|
|
@@ -1473,7 +1599,7 @@ const OPS = [
|
|
|
1473
1599
|
build: async (args) => ({
|
|
1474
1600
|
kind: "take_screenshot",
|
|
1475
1601
|
...typeof args.element === "string" ? { element: args.element } : {},
|
|
1476
|
-
...
|
|
1602
|
+
...stringField("target", args.target),
|
|
1477
1603
|
...args.type === "png" || args.type === "jpeg" || args.type === "webp" ? { type: args.type } : {},
|
|
1478
1604
|
...args.fullPage === true ? { fullPage: true } : {},
|
|
1479
1605
|
...args.scale === "css" || args.scale === "device" ? { scale: args.scale } : {}
|
|
@@ -1507,7 +1633,7 @@ const OPS = [
|
|
|
1507
1633
|
function: string(args.function),
|
|
1508
1634
|
callId: callIdOf(exec),
|
|
1509
1635
|
...typeof args.element === "string" ? { element: args.element } : {},
|
|
1510
|
-
...
|
|
1636
|
+
...stringField("target", args.target)
|
|
1511
1637
|
})
|
|
1512
1638
|
}
|
|
1513
1639
|
];
|
|
@@ -1542,7 +1668,7 @@ function defineOperation(ctx, reader, spec) {
|
|
|
1542
1668
|
const target = writeTarget(ctx, session);
|
|
1543
1669
|
ctx.mhBrowser.noteWorkspace(session.id, target);
|
|
1544
1670
|
const input = args;
|
|
1545
|
-
const filename = spec.filename === true
|
|
1671
|
+
const filename = spec.filename === true ? givenString(input.filename) : void 0;
|
|
1546
1672
|
if (filename !== void 0) await assertInside(target, filename);
|
|
1547
1673
|
try {
|
|
1548
1674
|
const tab = tabOf(reader, session, args.tab);
|
|
@@ -1600,6 +1726,18 @@ function string(value) {
|
|
|
1600
1726
|
function stringList$1(value) {
|
|
1601
1727
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1602
1728
|
}
|
|
1729
|
+
function stringField(key, value) {
|
|
1730
|
+
const given = givenString(value);
|
|
1731
|
+
if (given === void 0) return {};
|
|
1732
|
+
if (key === "target") return { target: given };
|
|
1733
|
+
if (key === "text") return { text: given };
|
|
1734
|
+
if (key === "textGone") return { textGone: given };
|
|
1735
|
+
return { regex: given };
|
|
1736
|
+
}
|
|
1737
|
+
function timeField(value) {
|
|
1738
|
+
const given = givenTime(value);
|
|
1739
|
+
return given === void 0 ? {} : { time: given };
|
|
1740
|
+
}
|
|
1603
1741
|
function optionalString(args, key) {
|
|
1604
1742
|
return typeof args[key] === "string" ? { element: args[key] } : {};
|
|
1605
1743
|
}
|
|
@@ -1911,6 +2049,8 @@ function takeoverText(taken) {
|
|
|
1911
2049
|
}
|
|
1912
2050
|
//#endregion
|
|
1913
2051
|
//#region lib/types/presence/register.js
|
|
2052
|
+
/** A presence read that stops answering. One stuck page must not hold the next step. */
|
|
2053
|
+
const CHECK_MS = 6e4;
|
|
1914
2054
|
const BROWSER_TOOLS = /* @__PURE__ */ new Set([
|
|
1915
2055
|
"browser_open",
|
|
1916
2056
|
"browser_invoke",
|
|
@@ -1931,14 +2071,17 @@ function registerPresence(ctx, reader) {
|
|
|
1931
2071
|
ctx.effect(() => () => {
|
|
1932
2072
|
lifetime.abort();
|
|
1933
2073
|
});
|
|
1934
|
-
const waitForChecks = async (agent) => {
|
|
2074
|
+
const waitForChecks = async (agent, signal) => {
|
|
1935
2075
|
let pending;
|
|
1936
|
-
while ((pending = tails.get(agent)) !== void 0)
|
|
2076
|
+
while ((pending = tails.get(agent)) !== void 0) {
|
|
2077
|
+
await untilAborted(pending, signal).catch(() => void 0);
|
|
2078
|
+
if (signal.aborted) return;
|
|
2079
|
+
}
|
|
1937
2080
|
};
|
|
1938
2081
|
const queue = (agent, urls) => {
|
|
1939
2082
|
const current = (tails.get(agent) ?? Promise.resolve()).then(async () => {
|
|
1940
2083
|
if (lifetime.signal.aborted) return;
|
|
1941
|
-
const reminder = await ctx.mhBrowser.presenceCheck(agent.session.id, urls, { signal: lifetime.signal });
|
|
2084
|
+
const reminder = await ctx.mhBrowser.presenceCheck(agent.session.id, urls, { signal: AbortSignal.any([lifetime.signal, AbortSignal.timeout(CHECK_MS)]) });
|
|
1942
2085
|
if (reminder === void 0 || lifetime.signal.aborted) return;
|
|
1943
2086
|
syncInbox(agent, reader.presence(agent.session), [], reminderMessage(reminder));
|
|
1944
2087
|
}).catch((error) => {
|
|
@@ -2015,7 +2158,7 @@ function registerPresence(ctx, reader) {
|
|
|
2015
2158
|
agents.set(agent.session.id, agent);
|
|
2016
2159
|
const handed = messages.some((message) => message.source.kind === "user") ? ctx.mhBrowser.takePersonHandback(agent.session.id) : void 0;
|
|
2017
2160
|
if (handed !== void 0 && handed.urls.length > 0) queue(agent, handed.urls);
|
|
2018
|
-
await waitForChecks(agent);
|
|
2161
|
+
await waitForChecks(agent, signal);
|
|
2019
2162
|
signal.throwIfAborted();
|
|
2020
2163
|
const desired = await compose(ctx.mhBrowser, agent, reader.presence(agent.session), messages, signal);
|
|
2021
2164
|
signal.throwIfAborted();
|
|
@@ -2159,11 +2302,10 @@ function record(value) {
|
|
|
2159
2302
|
return {};
|
|
2160
2303
|
}
|
|
2161
2304
|
function checkOptions(signal, extra) {
|
|
2162
|
-
const options = {};
|
|
2305
|
+
const options = { signal: capped(signal, CHECK_MS) };
|
|
2163
2306
|
if (extra?.baseline === true) options.baseline = true;
|
|
2164
2307
|
if (extra?.replaces === true) options.replaces = true;
|
|
2165
2308
|
if (extra?.force === true) options.force = true;
|
|
2166
|
-
if (signal !== void 0) options.signal = signal;
|
|
2167
2309
|
return options;
|
|
2168
2310
|
}
|
|
2169
2311
|
function canonical$1(url) {
|
|
@@ -2930,17 +3072,268 @@ function sameOrigin$1(left, right) {
|
|
|
2930
3072
|
}
|
|
2931
3073
|
}
|
|
2932
3074
|
//#endregion
|
|
2933
|
-
//#region lib/types/service/
|
|
2934
|
-
const
|
|
2935
|
-
chrome:
|
|
2936
|
-
|
|
2937
|
-
|
|
3075
|
+
//#region lib/types/service/locate.js
|
|
3076
|
+
const MAC_APP = {
|
|
3077
|
+
chrome: [
|
|
3078
|
+
"Google Chrome.app",
|
|
3079
|
+
"Contents",
|
|
3080
|
+
"MacOS",
|
|
3081
|
+
"Google Chrome"
|
|
3082
|
+
],
|
|
3083
|
+
msedge: [
|
|
3084
|
+
"Microsoft Edge.app",
|
|
3085
|
+
"Contents",
|
|
3086
|
+
"MacOS",
|
|
3087
|
+
"Microsoft Edge"
|
|
3088
|
+
]
|
|
3089
|
+
};
|
|
3090
|
+
const LINUX_PATH = {
|
|
3091
|
+
chrome: "/opt/google/chrome/chrome",
|
|
3092
|
+
msedge: "/opt/microsoft/msedge/msedge"
|
|
2938
3093
|
};
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
3094
|
+
const WIN_SUFFIX = {
|
|
3095
|
+
chrome: "Google\\Chrome\\Application\\chrome.exe",
|
|
3096
|
+
msedge: "Microsoft\\Edge\\Application\\msedge.exe"
|
|
3097
|
+
};
|
|
3098
|
+
const MAC_BUNDLE = {
|
|
3099
|
+
chrome: {
|
|
3100
|
+
id: "com.google.Chrome",
|
|
3101
|
+
binary: "Google Chrome"
|
|
3102
|
+
},
|
|
3103
|
+
msedge: {
|
|
3104
|
+
id: "com.microsoft.edgemac",
|
|
3105
|
+
binary: "Microsoft Edge"
|
|
3106
|
+
}
|
|
3107
|
+
};
|
|
3108
|
+
const LINUX_NAMES = {
|
|
3109
|
+
chrome: ["google-chrome-stable", "google-chrome"],
|
|
3110
|
+
msedge: ["microsoft-edge-stable", "microsoft-edge"]
|
|
3111
|
+
};
|
|
3112
|
+
const WIN_EXE = {
|
|
3113
|
+
chrome: "chrome.exe",
|
|
3114
|
+
msedge: "msedge.exe"
|
|
3115
|
+
};
|
|
3116
|
+
const TITLE = {
|
|
3117
|
+
chrome: "Google Chrome",
|
|
3118
|
+
msedge: "Microsoft Edge"
|
|
3119
|
+
};
|
|
3120
|
+
const DISCOVER_MS = 2e3;
|
|
3121
|
+
/** The fixed paths checked for `browser` on `env`, in the order they are tried. */
|
|
3122
|
+
function candidatePaths(browser, env) {
|
|
3123
|
+
if (env.platform === "darwin") return ["/Applications", posix.join(env.home, "Applications")].map((root) => posix.join(root, ...MAC_APP[browser]));
|
|
3124
|
+
if (env.platform === "linux") return [LINUX_PATH[browser]];
|
|
3125
|
+
if (env.platform !== "win32") return [];
|
|
3126
|
+
const drive = env.env.HOMEDRIVE;
|
|
3127
|
+
const roots = [
|
|
3128
|
+
env.env.LOCALAPPDATA,
|
|
3129
|
+
env.env.PROGRAMFILES,
|
|
3130
|
+
env.env["PROGRAMFILES(X86)"],
|
|
3131
|
+
typeof drive === "string" && drive !== "" ? `${drive}\\Program Files` : void 0,
|
|
3132
|
+
typeof drive === "string" && drive !== "" ? `${drive}\\Program Files (x86)` : void 0
|
|
3133
|
+
];
|
|
3134
|
+
const suffix = WIN_SUFFIX[browser];
|
|
3135
|
+
const paths = [];
|
|
3136
|
+
for (const root of roots) {
|
|
3137
|
+
if (root === void 0 || root === "") continue;
|
|
3138
|
+
paths.push(win32.join(root, suffix));
|
|
3139
|
+
}
|
|
3140
|
+
return paths;
|
|
2942
3141
|
}
|
|
2943
3142
|
/**
|
|
3143
|
+
* `configured` is used as given: a path that cannot be run is a failure, and
|
|
3144
|
+
* nothing else is searched. `~` expands to `env.home`.
|
|
3145
|
+
*/
|
|
3146
|
+
function checkConfigured(configured, env) {
|
|
3147
|
+
const path = expandHome(configured, env.home, env.platform);
|
|
3148
|
+
if (!(env.platform === "win32" ? win32 : posix).isAbsolute(path)) return {
|
|
3149
|
+
ok: false,
|
|
3150
|
+
path,
|
|
3151
|
+
code: "not-absolute"
|
|
3152
|
+
};
|
|
3153
|
+
const access = env.access(path);
|
|
3154
|
+
if (access !== "ok") return {
|
|
3155
|
+
ok: false,
|
|
3156
|
+
path,
|
|
3157
|
+
code: access.code
|
|
3158
|
+
};
|
|
3159
|
+
return {
|
|
3160
|
+
ok: true,
|
|
3161
|
+
path
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
/** Failure for a path the configuration names. */
|
|
3165
|
+
function configuredPathError(path, code) {
|
|
3166
|
+
if (code === "not-absolute") return /* @__PURE__ */ new Error(`The configured browser path must be absolute: ${path}`);
|
|
3167
|
+
return /* @__PURE__ */ new Error(`The configured browser path cannot be run (${code}): ${path}`);
|
|
3168
|
+
}
|
|
3169
|
+
/** Failure listing every path that was tried. */
|
|
3170
|
+
function browserNotFoundError(browser, missing) {
|
|
3171
|
+
const tried = missing.map((item) => `${item.path} (${item.reason})`).join("; ");
|
|
3172
|
+
const detail = tried === "" ? "" : ` Tried: ${tried}.`;
|
|
3173
|
+
return /* @__PURE__ */ new Error(`${TITLE[browser]} not found.${detail} Install it, or set the browser's path in the configuration.`);
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* The path to launch. A configured path is checked and never replaced.
|
|
3177
|
+
* `chromium` without one returns undefined, so Playwright uses its own build.
|
|
3178
|
+
* An installed browser that cannot be found throws.
|
|
3179
|
+
*/
|
|
3180
|
+
async function resolveExecutable(browser, configured, env, signal) {
|
|
3181
|
+
if (configured !== void 0 && configured !== "") {
|
|
3182
|
+
const checked = checkConfigured(configured, env);
|
|
3183
|
+
if (!checked.ok) throw configuredPathError(checked.path, checked.code);
|
|
3184
|
+
return checked.path;
|
|
3185
|
+
}
|
|
3186
|
+
if (browser === "chromium") return void 0;
|
|
3187
|
+
const located = await locateInstalled(browser, env, signal);
|
|
3188
|
+
if ("found" in located) return located.found;
|
|
3189
|
+
throw browserNotFoundError(browser, located.missing);
|
|
3190
|
+
}
|
|
3191
|
+
/**
|
|
3192
|
+
* Where `browser` is installed when the configuration names no path.
|
|
3193
|
+
* Fixed candidates first, then `env.discover`.
|
|
3194
|
+
*/
|
|
3195
|
+
async function locateInstalled(browser, env, signal) {
|
|
3196
|
+
const missing = [];
|
|
3197
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3198
|
+
const visit = (path) => {
|
|
3199
|
+
if (seen.has(path)) return void 0;
|
|
3200
|
+
seen.add(path);
|
|
3201
|
+
const access = env.access(path);
|
|
3202
|
+
if (access === "ok") return path;
|
|
3203
|
+
missing.push({
|
|
3204
|
+
path,
|
|
3205
|
+
reason: reasonForCode(access.code)
|
|
3206
|
+
});
|
|
3207
|
+
};
|
|
3208
|
+
for (const path of candidatePaths(browser, env)) {
|
|
3209
|
+
const found = visit(path);
|
|
3210
|
+
if (found !== void 0) return { found };
|
|
3211
|
+
}
|
|
3212
|
+
if (signal?.aborted === true) return { missing };
|
|
3213
|
+
for (const path of await env.discover(browser, signal)) {
|
|
3214
|
+
const found = visit(path);
|
|
3215
|
+
if (found !== void 0) return { found };
|
|
3216
|
+
}
|
|
3217
|
+
return { missing };
|
|
3218
|
+
}
|
|
3219
|
+
/** `path` is Playwright's Chromium. Missing or not executable is `missing`. */
|
|
3220
|
+
function locateBundled(path, access) {
|
|
3221
|
+
const result = access(path);
|
|
3222
|
+
if (result === "ok") return { found: path };
|
|
3223
|
+
return { missing: [{
|
|
3224
|
+
path,
|
|
3225
|
+
reason: reasonForCode(result.code)
|
|
3226
|
+
}] };
|
|
3227
|
+
}
|
|
3228
|
+
/** This machine: fixed paths, then Spotlight, App Paths, or `PATH`. */
|
|
3229
|
+
function hostLocateEnv() {
|
|
3230
|
+
const env = {
|
|
3231
|
+
platform: process.platform,
|
|
3232
|
+
env: process.env,
|
|
3233
|
+
home: homedir(),
|
|
3234
|
+
access: accessFile,
|
|
3235
|
+
discover: (browser, signal) => discoverInstalled(browser, env, signal)
|
|
3236
|
+
};
|
|
3237
|
+
return env;
|
|
3238
|
+
}
|
|
3239
|
+
/** A `.app` bundle path reported by Spotlight, turned into its executable. */
|
|
3240
|
+
function executableInApp(reported, binary) {
|
|
3241
|
+
const at = reported.indexOf(".app");
|
|
3242
|
+
if (at < 0) return void 0;
|
|
3243
|
+
return posix.join(reported.slice(0, at + 4), "Contents", "MacOS", binary);
|
|
3244
|
+
}
|
|
3245
|
+
/** The default value from `reg query ... /ve` output. */
|
|
3246
|
+
function regValue(stdout) {
|
|
3247
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
3248
|
+
const value = /\bREG_(?:EXPAND_)?SZ\s+(\S.*?)\s*$/.exec(line)?.[1]?.trim();
|
|
3249
|
+
if (value !== void 0 && value !== "") return value;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
/** Text for an errno from {@link LocateEnv.access}. */
|
|
3253
|
+
function reasonForCode(code) {
|
|
3254
|
+
if (code === "ENOENT" || code === "ENOTDIR") return "does not exist";
|
|
3255
|
+
if (code === "EISDIR") return "is a directory";
|
|
3256
|
+
return `permission denied: ${code}`;
|
|
3257
|
+
}
|
|
3258
|
+
function expandHome(configured, home, platform) {
|
|
3259
|
+
const slash = platform === "win32" ? win32 : posix;
|
|
3260
|
+
if (configured === "~") return home;
|
|
3261
|
+
if (configured.startsWith("~/") || configured.startsWith("~\\")) return slash.join(home, configured.slice(2));
|
|
3262
|
+
return configured;
|
|
3263
|
+
}
|
|
3264
|
+
function accessFile(path) {
|
|
3265
|
+
try {
|
|
3266
|
+
if (statSync(path).isDirectory()) return { code: "EISDIR" };
|
|
3267
|
+
accessSync(path, constants.X_OK);
|
|
3268
|
+
return "ok";
|
|
3269
|
+
} catch (error) {
|
|
3270
|
+
return { code: error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : "EACCES" };
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
async function discoverInstalled(browser, env, signal) {
|
|
3274
|
+
try {
|
|
3275
|
+
if (env.platform === "darwin") return await discoverMac(browser, signal);
|
|
3276
|
+
if (env.platform === "win32") return await discoverWindows(browser, signal);
|
|
3277
|
+
if (env.platform === "linux") return discoverLinux(browser, env);
|
|
3278
|
+
} catch {}
|
|
3279
|
+
return [];
|
|
3280
|
+
}
|
|
3281
|
+
async function discoverMac(browser, signal) {
|
|
3282
|
+
const bundle = MAC_BUNDLE[browser];
|
|
3283
|
+
const stdout = await command("mdfind", [`kMDItemCFBundleIdentifier == '${bundle.id}'`], signal);
|
|
3284
|
+
const found = [];
|
|
3285
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
3286
|
+
const trimmed = line.trim();
|
|
3287
|
+
if (trimmed === "") continue;
|
|
3288
|
+
const executable = executableInApp(trimmed, bundle.binary);
|
|
3289
|
+
if (executable !== void 0) found.push(executable);
|
|
3290
|
+
}
|
|
3291
|
+
return found;
|
|
3292
|
+
}
|
|
3293
|
+
async function discoverWindows(browser, signal) {
|
|
3294
|
+
const exe = WIN_EXE[browser];
|
|
3295
|
+
const found = [];
|
|
3296
|
+
for (const hive of ["HKCU", "HKLM"]) {
|
|
3297
|
+
const value = regValue(await command("reg", [
|
|
3298
|
+
"query",
|
|
3299
|
+
`${hive}\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${exe}`,
|
|
3300
|
+
"/ve"
|
|
3301
|
+
], signal));
|
|
3302
|
+
if (value !== void 0) found.push(value);
|
|
3303
|
+
}
|
|
3304
|
+
return found;
|
|
3305
|
+
}
|
|
3306
|
+
function discoverLinux(browser, env) {
|
|
3307
|
+
const entries = (env.env.PATH ?? "").split(posix.delimiter);
|
|
3308
|
+
const found = [];
|
|
3309
|
+
for (const name of LINUX_NAMES[browser]) for (const dir of entries) {
|
|
3310
|
+
if (dir === "") continue;
|
|
3311
|
+
const candidate = posix.join(dir, name);
|
|
3312
|
+
if (env.access(candidate) !== "ok") continue;
|
|
3313
|
+
try {
|
|
3314
|
+
found.push(realpathSync(candidate));
|
|
3315
|
+
} catch {
|
|
3316
|
+
found.push(candidate);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
return found;
|
|
3320
|
+
}
|
|
3321
|
+
function command(file, args, signal) {
|
|
3322
|
+
if (signal?.aborted === true) return Promise.resolve("");
|
|
3323
|
+
const timer = AbortSignal.timeout(DISCOVER_MS);
|
|
3324
|
+
const stop = signal === void 0 ? timer : AbortSignal.any([signal, timer]);
|
|
3325
|
+
return new Promise((resolve) => {
|
|
3326
|
+
execFile(file, args, {
|
|
3327
|
+
windowsHide: true,
|
|
3328
|
+
signal: stop
|
|
3329
|
+
}, (error, stdout) => {
|
|
3330
|
+
resolve(error ? "" : String(stdout));
|
|
3331
|
+
});
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
//#endregion
|
|
3335
|
+
//#region lib/types/service/browser.js
|
|
3336
|
+
/**
|
|
2944
3337
|
* Playwright appends `--no-sandbox` unless this is true, and branded Chrome
|
|
2945
3338
|
* then warns that stability and security will suffer. An installed browser
|
|
2946
3339
|
* sandboxes on every platform. Playwright's own Chromium does too, except on
|
|
@@ -2950,34 +3343,45 @@ function chromiumSandboxEnabled(browser, platform = process.platform) {
|
|
|
2950
3343
|
return platform !== "linux" || browser !== "chromium";
|
|
2951
3344
|
}
|
|
2952
3345
|
/**
|
|
2953
|
-
* One persistent context for the whole process.
|
|
2954
|
-
*
|
|
2955
|
-
*
|
|
3346
|
+
* One persistent context for the whole process. An installed browser is located
|
|
3347
|
+
* here and passed as `executablePath`; `channel` names which install that was.
|
|
3348
|
+
* `chromium` omits `channel` so Playwright uses its own browser unless a path
|
|
3349
|
+
* is configured. A missing browser is reported and not downloaded. A launch
|
|
3350
|
+
* that fails after the executable was found is reported as Playwright said.
|
|
2956
3351
|
*/
|
|
2957
|
-
async function launchBrowser(options) {
|
|
3352
|
+
async function launchBrowser(options, env = hostLocateEnv()) {
|
|
2958
3353
|
await mkdir(options.profileDir, { recursive: true });
|
|
3354
|
+
const executablePath = await resolveExecutable(options.browser, options.executablePath, env);
|
|
3355
|
+
return chromium.launchPersistentContext(options.profileDir, {
|
|
3356
|
+
headless: options.headless,
|
|
3357
|
+
acceptDownloads: true,
|
|
3358
|
+
chromiumSandbox: chromiumSandboxEnabled(options.browser),
|
|
3359
|
+
...options.headless ? {} : {
|
|
3360
|
+
viewport: null,
|
|
3361
|
+
ignoreDefaultArgs: ["--disable-extensions"],
|
|
3362
|
+
args: [
|
|
3363
|
+
"--enable-unsafe-extension-debugging",
|
|
3364
|
+
"--keep-alive-for-test",
|
|
3365
|
+
"--disable-blink-features=AutomationControlled",
|
|
3366
|
+
"--test-type"
|
|
3367
|
+
]
|
|
3368
|
+
},
|
|
3369
|
+
...options.browser === "chromium" ? {} : { channel: options.browser },
|
|
3370
|
+
...executablePath !== void 0 ? { executablePath } : {}
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
/** Playwright's own Chromium, or why that file cannot be run. */
|
|
3374
|
+
function bundledChromiumLocation(access) {
|
|
3375
|
+
let path;
|
|
2959
3376
|
try {
|
|
2960
|
-
|
|
2961
|
-
headless: options.headless,
|
|
2962
|
-
acceptDownloads: true,
|
|
2963
|
-
chromiumSandbox: chromiumSandboxEnabled(options.browser),
|
|
2964
|
-
...options.headless ? {} : {
|
|
2965
|
-
viewport: null,
|
|
2966
|
-
ignoreDefaultArgs: ["--disable-extensions"],
|
|
2967
|
-
args: [
|
|
2968
|
-
"--enable-unsafe-extension-debugging",
|
|
2969
|
-
"--keep-alive-for-test",
|
|
2970
|
-
"--disable-blink-features=AutomationControlled",
|
|
2971
|
-
"--test-type"
|
|
2972
|
-
]
|
|
2973
|
-
},
|
|
2974
|
-
...options.browser === "chromium" ? {} : { channel: options.browser },
|
|
2975
|
-
...options.executablePath !== void 0 ? { executablePath: options.executablePath } : {}
|
|
2976
|
-
});
|
|
3377
|
+
path = chromium.executablePath();
|
|
2977
3378
|
} catch (error) {
|
|
2978
|
-
|
|
2979
|
-
|
|
3379
|
+
return { missing: [{
|
|
3380
|
+
path: "chromium",
|
|
3381
|
+
reason: error instanceof Error ? error.message : "does not exist"
|
|
3382
|
+
}] };
|
|
2980
3383
|
}
|
|
3384
|
+
return locateBundled(path, access);
|
|
2981
3385
|
}
|
|
2982
3386
|
/**
|
|
2983
3387
|
* Process id of the browser's main process. Playwright gives none for a
|
|
@@ -3040,6 +3444,15 @@ var Control = class {
|
|
|
3040
3444
|
tabs = /* @__PURE__ */ new Map();
|
|
3041
3445
|
since = /* @__PURE__ */ new Map();
|
|
3042
3446
|
named = /* @__PURE__ */ new Map();
|
|
3447
|
+
/**
|
|
3448
|
+
* A handover is waiting, or the person holds a tab. The browser is not
|
|
3449
|
+
* restarted out from under either.
|
|
3450
|
+
*/
|
|
3451
|
+
busy() {
|
|
3452
|
+
if (this.holding.size > 0) return true;
|
|
3453
|
+
for (const who of this.who.values()) if (who === "person") return true;
|
|
3454
|
+
return false;
|
|
3455
|
+
}
|
|
3043
3456
|
/** Whether the person holds any tab, or a handover is waiting. */
|
|
3044
3457
|
of(owner) {
|
|
3045
3458
|
return this.who.get(owner) ?? "model";
|
|
@@ -3684,6 +4097,8 @@ async function dispatch(page, op, snapshot, signal) {
|
|
|
3684
4097
|
//#region lib/types/service/evaluate.js
|
|
3685
4098
|
/** How long requests the function started are still collected after it returns. */
|
|
3686
4099
|
const SETTLE_MS$1 = 50;
|
|
4100
|
+
/** A debugger call or a detach the page no longer answers. */
|
|
4101
|
+
const CALL_MS$2 = 15e3;
|
|
3687
4102
|
/**
|
|
3688
4103
|
* Run the expression in the page and collect write requests.
|
|
3689
4104
|
* A request is from this call when its initiator stack, including parents,
|
|
@@ -3693,7 +4108,8 @@ const SETTLE_MS$1 = 50;
|
|
|
3693
4108
|
*/
|
|
3694
4109
|
async function evaluateMarked(page, source, target, callId, signal, dialog) {
|
|
3695
4110
|
const marker = `mh-evaluate-${callId}-${randomUUID().slice(0, 8)}`;
|
|
3696
|
-
const
|
|
4111
|
+
const limit = capped(signal, CALL_MS$2);
|
|
4112
|
+
const client = await untilAborted(page.context().newCDPSession(page), limit);
|
|
3697
4113
|
const notes = /* @__PURE__ */ new Map();
|
|
3698
4114
|
const onRequest = (event) => {
|
|
3699
4115
|
const method = event.request?.method?.toUpperCase();
|
|
@@ -3713,10 +4129,10 @@ async function evaluateMarked(page, source, target, callId, signal, dialog) {
|
|
|
3713
4129
|
const stop = whenAborted(signal);
|
|
3714
4130
|
let dialogOpen = false;
|
|
3715
4131
|
try {
|
|
3716
|
-
await client.send("Network.enable");
|
|
3717
|
-
await client.send("Debugger.enable");
|
|
3718
|
-
await client.send("Debugger.setSkipAllPauses", { skip: true });
|
|
3719
|
-
await client.send("Debugger.setAsyncCallStackDepth", { maxDepth: 32 });
|
|
4132
|
+
await untilAborted(client.send("Network.enable"), limit);
|
|
4133
|
+
await untilAborted(client.send("Debugger.enable"), limit);
|
|
4134
|
+
await untilAborted(client.send("Debugger.setSkipAllPauses", { skip: true }), limit);
|
|
4135
|
+
await untilAborted(client.send("Debugger.setAsyncCallStackDepth", { maxDepth: 32 }), limit);
|
|
3720
4136
|
const evaluation = (target === void 0 ? page.evaluate(runSource, {
|
|
3721
4137
|
expr: source,
|
|
3722
4138
|
marker
|
|
@@ -3768,7 +4184,7 @@ async function evaluateMarked(page, source, target, callId, signal, dialog) {
|
|
|
3768
4184
|
stop.dispose();
|
|
3769
4185
|
client.off("Network.requestWillBeSent", onRequest);
|
|
3770
4186
|
const detached = client.detach().catch(() => void 0);
|
|
3771
|
-
if (!dialogOpen) await detached;
|
|
4187
|
+
if (!dialogOpen) await untilAborted(detached, capped(signal, CALL_MS$2)).catch(() => void 0);
|
|
3772
4188
|
}
|
|
3773
4189
|
}
|
|
3774
4190
|
/** One line per group: `POST https://site × 2 (this call)`. */
|
|
@@ -4166,6 +4582,8 @@ const REPLIED = "继续";
|
|
|
4166
4582
|
//#region lib/types/service/extension.js
|
|
4167
4583
|
const PACKAGE = "@mind-harness/mh-browser";
|
|
4168
4584
|
const WORKER_MS = 15e3;
|
|
4585
|
+
/** A service-worker call that does not answer. The caller's signal can end it sooner. */
|
|
4586
|
+
const CALL_MS$1 = 15e3;
|
|
4169
4587
|
/**
|
|
4170
4588
|
* Directory of the unpacked extension shipped with this package.
|
|
4171
4589
|
* Walks up from this module: sources live in `src/`, the bundle in `lib/`.
|
|
@@ -4205,8 +4623,9 @@ var Extension = class Extension {
|
|
|
4205
4623
|
return `chrome-extension://${this.id}/${name}`;
|
|
4206
4624
|
}
|
|
4207
4625
|
/** `chrome.tabs.create` in the named window. Returns the extension's tab id. */
|
|
4208
|
-
async createTab(windowId, url) {
|
|
4209
|
-
|
|
4626
|
+
async createTab(windowId, url, signal) {
|
|
4627
|
+
const worker = await this.worker(signal);
|
|
4628
|
+
return this.answer(worker.evaluate(async (arg) => {
|
|
4210
4629
|
const tab = await globalThis.chrome.tabs.create({
|
|
4211
4630
|
windowId: arg.windowId,
|
|
4212
4631
|
url: arg.url,
|
|
@@ -4217,23 +4636,25 @@ var Extension = class Extension {
|
|
|
4217
4636
|
}, {
|
|
4218
4637
|
windowId,
|
|
4219
4638
|
url
|
|
4220
|
-
});
|
|
4639
|
+
}), signal);
|
|
4221
4640
|
}
|
|
4222
|
-
async tabIdsIn(windowId) {
|
|
4223
|
-
|
|
4641
|
+
async tabIdsIn(windowId, signal) {
|
|
4642
|
+
const worker = await this.worker(signal);
|
|
4643
|
+
return this.answer(worker.evaluate(async (target) => {
|
|
4224
4644
|
return (await globalThis.chrome.tabs.query({ windowId: target })).flatMap((tab) => tab.id === void 0 ? [] : [tab.id]);
|
|
4225
|
-
}, windowId);
|
|
4645
|
+
}, windowId), signal);
|
|
4226
4646
|
}
|
|
4227
|
-
async activate(tabId) {
|
|
4228
|
-
|
|
4647
|
+
async activate(tabId, signal) {
|
|
4648
|
+
const worker = await this.worker(signal);
|
|
4649
|
+
await this.answer(worker.evaluate(async (id) => {
|
|
4229
4650
|
await globalThis.chrome.tabs.update(id, { active: true });
|
|
4230
|
-
}, tabId);
|
|
4651
|
+
}, tabId), signal);
|
|
4231
4652
|
}
|
|
4232
|
-
async group(tabId, target) {
|
|
4233
|
-
const worker = await this.worker();
|
|
4653
|
+
async group(tabId, target, signal) {
|
|
4654
|
+
const worker = await this.worker(signal);
|
|
4234
4655
|
if ("groupId" in target) {
|
|
4235
4656
|
const groupId = target.groupId;
|
|
4236
|
-
return worker.evaluate(async (arg) => {
|
|
4657
|
+
return this.answer(worker.evaluate(async (arg) => {
|
|
4237
4658
|
return globalThis.chrome.tabs.group({
|
|
4238
4659
|
tabIds: [arg.tabId],
|
|
4239
4660
|
groupId: arg.groupId
|
|
@@ -4241,10 +4662,10 @@ var Extension = class Extension {
|
|
|
4241
4662
|
}, {
|
|
4242
4663
|
tabId,
|
|
4243
4664
|
groupId
|
|
4244
|
-
});
|
|
4665
|
+
}), signal);
|
|
4245
4666
|
}
|
|
4246
4667
|
const windowId = target.windowId;
|
|
4247
|
-
return worker.evaluate(async (arg) => {
|
|
4668
|
+
return this.answer(worker.evaluate(async (arg) => {
|
|
4248
4669
|
return globalThis.chrome.tabs.group({
|
|
4249
4670
|
tabIds: [arg.tabId],
|
|
4250
4671
|
createProperties: { windowId: arg.windowId }
|
|
@@ -4252,10 +4673,11 @@ var Extension = class Extension {
|
|
|
4252
4673
|
}, {
|
|
4253
4674
|
tabId,
|
|
4254
4675
|
windowId
|
|
4255
|
-
});
|
|
4676
|
+
}), signal);
|
|
4256
4677
|
}
|
|
4257
|
-
async styleGroup(groupId, title, color) {
|
|
4258
|
-
|
|
4678
|
+
async styleGroup(groupId, title, color, signal) {
|
|
4679
|
+
const worker = await this.worker(signal);
|
|
4680
|
+
await this.answer(worker.evaluate(async (arg) => {
|
|
4259
4681
|
await globalThis.chrome.tabGroups.update(arg.groupId, {
|
|
4260
4682
|
title: arg.title,
|
|
4261
4683
|
color: arg.color
|
|
@@ -4264,17 +4686,21 @@ var Extension = class Extension {
|
|
|
4264
4686
|
groupId,
|
|
4265
4687
|
title,
|
|
4266
4688
|
color
|
|
4267
|
-
});
|
|
4689
|
+
}), signal);
|
|
4268
4690
|
}
|
|
4269
|
-
async ungroup(tabId) {
|
|
4270
|
-
|
|
4691
|
+
async ungroup(tabId, signal) {
|
|
4692
|
+
const worker = await this.worker(signal);
|
|
4693
|
+
await this.answer(worker.evaluate(async (id) => {
|
|
4271
4694
|
await globalThis.chrome.tabs.ungroup(id);
|
|
4272
|
-
}, tabId);
|
|
4695
|
+
}, tabId), signal);
|
|
4696
|
+
}
|
|
4697
|
+
answer(pending, signal) {
|
|
4698
|
+
return untilAborted(pending, capped(signal, CALL_MS$1));
|
|
4273
4699
|
}
|
|
4274
|
-
async worker() {
|
|
4700
|
+
async worker(signal) {
|
|
4275
4701
|
const current = this.workerPage;
|
|
4276
4702
|
if (current !== void 0 && this.context.serviceWorkers().includes(current)) return current;
|
|
4277
|
-
const worker = await findWorker(this.context, this.id);
|
|
4703
|
+
const worker = await untilAborted(findWorker(this.context, this.id), capped(signal, CALL_MS$1));
|
|
4278
4704
|
this.workerPage = worker;
|
|
4279
4705
|
return worker;
|
|
4280
4706
|
}
|
|
@@ -4300,6 +4726,10 @@ async function findWorker(context, id, timeoutMs = WORKER_MS) {
|
|
|
4300
4726
|
const TAB_CAP = 8;
|
|
4301
4727
|
/** How long a new window or tab may take to appear. */
|
|
4302
4728
|
const TARGET_MS = 5e3;
|
|
4729
|
+
/** A CDP or extension call that does not answer. The caller's signal can end it sooner. */
|
|
4730
|
+
const CALL_MS = 15e3;
|
|
4731
|
+
/** Closing a page the renderer no longer answers. */
|
|
4732
|
+
const CLOSE_MS = 5e3;
|
|
4303
4733
|
const WORLD = "mh-browser";
|
|
4304
4734
|
const BINDING = "mhBrowserInput";
|
|
4305
4735
|
const LISTENER = `(() => {
|
|
@@ -4521,10 +4951,10 @@ var Windows = class {
|
|
|
4521
4951
|
return open.reduce((best, tab) => tab.touchedAt >= best.touchedAt ? tab : best);
|
|
4522
4952
|
}
|
|
4523
4953
|
/** Make this tab the current one in its window. Does not raise the browser. */
|
|
4524
|
-
async activate(tab) {
|
|
4954
|
+
async activate(tab, signal) {
|
|
4525
4955
|
const extension = this.extension;
|
|
4526
4956
|
if (extension === void 0 || tab.chromeTabId === void 0) return;
|
|
4527
|
-
await extension.activate(tab.chromeTabId);
|
|
4957
|
+
await extension.activate(tab.chromeTabId, signal);
|
|
4528
4958
|
}
|
|
4529
4959
|
/** Group title for the person. Empty until a title is known. */
|
|
4530
4960
|
setTitle(owner, title) {
|
|
@@ -4551,54 +4981,70 @@ var Windows = class {
|
|
|
4551
4981
|
* use the extension. Without a window, later pages use `window.open` from
|
|
4552
4982
|
* an isolated world so they stay in that window.
|
|
4553
4983
|
*/
|
|
4554
|
-
openBlank(owner) {
|
|
4984
|
+
openBlank(owner, signal) {
|
|
4555
4985
|
this.pendingCreates.set(owner, (this.pendingCreates.get(owner) ?? 0) + 1);
|
|
4556
|
-
const
|
|
4557
|
-
const run =
|
|
4986
|
+
const queued = enqueue(this.creating, () => this.createTab(owner, signal), signal);
|
|
4987
|
+
const run = queued.run.finally(() => {
|
|
4558
4988
|
const left = (this.pendingCreates.get(owner) ?? 1) - 1;
|
|
4559
4989
|
if (left <= 0) this.pendingCreates.delete(owner);
|
|
4560
4990
|
else this.pendingCreates.set(owner, left);
|
|
4561
4991
|
});
|
|
4562
|
-
this.creating =
|
|
4992
|
+
this.creating = queued.tail;
|
|
4563
4993
|
return run;
|
|
4564
4994
|
}
|
|
4565
|
-
async createTab(owner) {
|
|
4995
|
+
async createTab(owner, signal) {
|
|
4566
4996
|
const extension = this.extension;
|
|
4567
4997
|
if (extension === void 0) {
|
|
4568
|
-
const opener = await this.openerFor(owner);
|
|
4569
|
-
const page = await this.openTabIn(opener);
|
|
4570
|
-
return this.keep(owner, page, void 0);
|
|
4998
|
+
const opener = await this.openerFor(owner, signal);
|
|
4999
|
+
const page = await this.openTabIn(opener, signal);
|
|
5000
|
+
return this.keep(owner, page, void 0, void 0, signal);
|
|
4571
5001
|
}
|
|
4572
5002
|
const open = this.owned(owner);
|
|
4573
5003
|
if (open.length === 0) {
|
|
4574
|
-
const page = await this.openWindow();
|
|
4575
|
-
const windowId = await this.windowIdOf(page);
|
|
4576
|
-
const chromeTabId = await this.extensionTabIn(extension, windowId);
|
|
4577
|
-
const tab = await this.keep(owner, page, chromeTabId);
|
|
4578
|
-
this.groupTab(tab);
|
|
5004
|
+
const page = await this.openWindow(signal);
|
|
5005
|
+
const windowId = await this.windowIdOf(page, signal);
|
|
5006
|
+
const chromeTabId = await this.extensionTabIn(extension, windowId, signal);
|
|
5007
|
+
const tab = await this.keep(owner, page, chromeTabId, void 0, signal);
|
|
5008
|
+
this.groupTab(tab, signal);
|
|
4579
5009
|
return tab;
|
|
4580
5010
|
}
|
|
4581
5011
|
const anchor = open.reduce((best, tab) => tab.touchedAt >= best.touchedAt ? tab : best);
|
|
4582
|
-
const opened = await this.openTabViaExtension(anchor.windowId);
|
|
4583
|
-
const tab = await this.keep(owner, opened.page, opened.chromeTabId, anchor.windowId);
|
|
4584
|
-
this.groupTab(tab);
|
|
5012
|
+
const opened = await this.openTabViaExtension(anchor.windowId, signal);
|
|
5013
|
+
const tab = await this.keep(owner, opened.page, opened.chromeTabId, anchor.windowId, signal);
|
|
5014
|
+
this.groupTab(tab, signal);
|
|
4585
5015
|
return tab;
|
|
4586
5016
|
}
|
|
4587
|
-
async keep(owner, page, chromeTabId, windowId) {
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
5017
|
+
async keep(owner, page, chromeTabId, windowId, signal) {
|
|
5018
|
+
let tab;
|
|
5019
|
+
try {
|
|
5020
|
+
tab = {
|
|
5021
|
+
id: await this.allocate(),
|
|
5022
|
+
owner,
|
|
5023
|
+
page,
|
|
5024
|
+
targetId: await this.targetIdOf(page, signal),
|
|
5025
|
+
windowId: windowId ?? await this.windowIdOf(page, signal),
|
|
5026
|
+
chromeTabId,
|
|
5027
|
+
url: page.url(),
|
|
5028
|
+
touchedAt: Date.now(),
|
|
5029
|
+
...idleTab()
|
|
5030
|
+
};
|
|
5031
|
+
this.remember(tab);
|
|
5032
|
+
await this.arm(page, owner, tab.id, signal);
|
|
5033
|
+
return tab;
|
|
5034
|
+
} catch (error) {
|
|
5035
|
+
if (tab !== void 0 && this.byPage.get(page) === tab) await this.closePage(tab);
|
|
5036
|
+
else await this.shutdown(page);
|
|
5037
|
+
throw error;
|
|
5038
|
+
}
|
|
5039
|
+
}
|
|
5040
|
+
/** The page from a create, or close it when this wait is abandoned. */
|
|
5041
|
+
async claimedPage(pending, signal) {
|
|
5042
|
+
try {
|
|
5043
|
+
return await untilAborted(pending, capped(signal, TARGET_MS));
|
|
5044
|
+
} catch (error) {
|
|
5045
|
+
pending.then((page) => this.shutdown(page)).catch(() => void 0);
|
|
5046
|
+
throw error;
|
|
5047
|
+
}
|
|
4602
5048
|
}
|
|
4603
5049
|
/**
|
|
4604
5050
|
* Close one tab. `note` is listed in the next `<tabs>` block. A tab the
|
|
@@ -4612,9 +5058,12 @@ var Windows = class {
|
|
|
4612
5058
|
}
|
|
4613
5059
|
await this.closePage(tab);
|
|
4614
5060
|
}
|
|
4615
|
-
async closeOwner(owner) {
|
|
4616
|
-
for (const tab of this.owned(owner))
|
|
4617
|
-
|
|
5061
|
+
async closeOwner(owner, signal) {
|
|
5062
|
+
for (const tab of this.owned(owner)) {
|
|
5063
|
+
if (signal?.aborted === true) break;
|
|
5064
|
+
await this.closePage(tab);
|
|
5065
|
+
}
|
|
5066
|
+
if (signal?.aborted !== true) await this.closeOpener(owner);
|
|
4618
5067
|
this.closedNotes.delete(owner);
|
|
4619
5068
|
for (const [id, last] of this.lastUrls) if (last.owner === owner) this.lastUrls.delete(id);
|
|
4620
5069
|
this.groups.delete(owner);
|
|
@@ -4632,7 +5081,7 @@ var Windows = class {
|
|
|
4632
5081
|
}
|
|
4633
5082
|
this.serviceClose.add(tab.page);
|
|
4634
5083
|
this.forget(tab);
|
|
4635
|
-
if (!tab.page.isClosed()) await tab.page
|
|
5084
|
+
if (!tab.page.isClosed()) await this.shutdown(tab.page);
|
|
4636
5085
|
}
|
|
4637
5086
|
/** A tab leaves the registry. The window closes with its last tab. */
|
|
4638
5087
|
forget(tab) {
|
|
@@ -4660,7 +5109,7 @@ var Windows = class {
|
|
|
4660
5109
|
session?.detach().catch(() => void 0);
|
|
4661
5110
|
if (opener.isClosed()) return;
|
|
4662
5111
|
this.serviceClose.add(opener);
|
|
4663
|
-
await
|
|
5112
|
+
await this.shutdown(opener);
|
|
4664
5113
|
}
|
|
4665
5114
|
/** Close least-recently touched idle tabs until the owner is within the cap. */
|
|
4666
5115
|
async enforceCap(owner) {
|
|
@@ -4707,57 +5156,58 @@ var Windows = class {
|
|
|
4707
5156
|
await writeCounter(this.profileDir, this.nextId);
|
|
4708
5157
|
return id;
|
|
4709
5158
|
}
|
|
4710
|
-
async openerFor(owner) {
|
|
5159
|
+
async openerFor(owner, signal) {
|
|
4711
5160
|
const existing = this.openers.get(owner);
|
|
4712
5161
|
if (existing !== void 0 && !existing.isClosed()) return existing;
|
|
4713
|
-
const opener = await this.openWindow();
|
|
5162
|
+
const opener = await this.openWindow(signal);
|
|
4714
5163
|
this.openers.set(owner, opener);
|
|
4715
|
-
await this.arm(opener, owner, void 0);
|
|
5164
|
+
await this.arm(opener, owner, void 0, signal);
|
|
4716
5165
|
return opener;
|
|
4717
5166
|
}
|
|
4718
|
-
async openWindow() {
|
|
5167
|
+
async openWindow(signal) {
|
|
4719
5168
|
const session = this.browserSession;
|
|
4720
5169
|
if (session === void 0) throw new Error("browser is not running");
|
|
4721
|
-
const created = await this.createWindowTarget(session);
|
|
4722
|
-
|
|
5170
|
+
const created = await this.createWindowTarget(session, signal);
|
|
5171
|
+
const pending = this.waitForTarget(created.targetId);
|
|
5172
|
+
return this.claimedPage(pending, signal);
|
|
4723
5173
|
}
|
|
4724
5174
|
/** Headed windows pass `focus: false`. A browser that rejects it is opened without the flag. */
|
|
4725
|
-
async createWindowTarget(session) {
|
|
5175
|
+
async createWindowTarget(session, signal) {
|
|
4726
5176
|
const base = {
|
|
4727
5177
|
url: "about:blank",
|
|
4728
5178
|
newWindow: true
|
|
4729
5179
|
};
|
|
4730
|
-
if (!this.headed) return
|
|
5180
|
+
if (!this.headed) return this.cdp(session, "Target.createTarget", base, signal);
|
|
4731
5181
|
try {
|
|
4732
|
-
return await
|
|
5182
|
+
return await this.cdp(session, "Target.createTarget", {
|
|
4733
5183
|
...base,
|
|
4734
5184
|
focus: false
|
|
4735
|
-
});
|
|
4736
|
-
} catch {
|
|
4737
|
-
|
|
5185
|
+
}, signal);
|
|
5186
|
+
} catch (error) {
|
|
5187
|
+
if (signal?.aborted === true || isInterrupted(error)) throw error;
|
|
5188
|
+
return this.cdp(session, "Target.createTarget", base, signal);
|
|
4738
5189
|
}
|
|
4739
5190
|
}
|
|
4740
|
-
async openTabIn(anchor) {
|
|
4741
|
-
|
|
4742
|
-
if (context === void 0) throw new Error("browser is not running");
|
|
5191
|
+
async openTabIn(anchor, signal) {
|
|
5192
|
+
if (this.context === void 0) throw new Error("browser is not running");
|
|
4743
5193
|
const claim = this.claimBlank();
|
|
4744
5194
|
try {
|
|
4745
|
-
const session = await
|
|
5195
|
+
const session = await this.pageSession(anchor, signal);
|
|
4746
5196
|
try {
|
|
4747
|
-
const tree = await
|
|
4748
|
-
const world = await
|
|
5197
|
+
const tree = await this.cdp(session, "Page.getFrameTree", void 0, signal);
|
|
5198
|
+
const world = await this.cdp(session, "Page.createIsolatedWorld", {
|
|
4749
5199
|
frameId: tree.frameTree.frame.id,
|
|
4750
5200
|
worldName: WORLD,
|
|
4751
5201
|
grantUniveralAccess: true
|
|
4752
|
-
});
|
|
4753
|
-
await
|
|
5202
|
+
}, signal);
|
|
5203
|
+
await this.cdp(session, "Runtime.evaluate", {
|
|
4754
5204
|
expression: `window.open(${JSON.stringify(claim.url)},'_blank','noopener,noreferrer')`,
|
|
4755
5205
|
contextId: world.executionContextId
|
|
4756
|
-
});
|
|
5206
|
+
}, signal);
|
|
4757
5207
|
} finally {
|
|
4758
|
-
await
|
|
5208
|
+
await this.shutdownSession(session);
|
|
4759
5209
|
}
|
|
4760
|
-
return await claim.page;
|
|
5210
|
+
return await this.claimedPage(claim.page, signal);
|
|
4761
5211
|
} finally {
|
|
4762
5212
|
claim.drop();
|
|
4763
5213
|
}
|
|
@@ -4800,22 +5250,23 @@ var Windows = class {
|
|
|
4800
5250
|
};
|
|
4801
5251
|
}
|
|
4802
5252
|
/** The extension's id for the only tab in a window just opened. */
|
|
4803
|
-
async extensionTabIn(extension, windowId) {
|
|
5253
|
+
async extensionTabIn(extension, windowId, signal) {
|
|
4804
5254
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
4805
|
-
|
|
5255
|
+
if (signal?.aborted === true) return void 0;
|
|
5256
|
+
const id = (await extension.tabIdsIn(windowId, signal))[0];
|
|
4806
5257
|
if (id !== void 0) return id;
|
|
4807
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5258
|
+
await untilAborted(new Promise((resolve) => setTimeout(resolve, 100)), signal).catch(() => void 0);
|
|
4808
5259
|
}
|
|
4809
5260
|
}
|
|
4810
5261
|
/** Ask the extension to open a blank tab in `windowId`, and claim it. */
|
|
4811
|
-
async openTabViaExtension(windowId) {
|
|
5262
|
+
async openTabViaExtension(windowId, signal) {
|
|
4812
5263
|
const extension = this.extension;
|
|
4813
5264
|
if (extension === void 0) throw new Error("browser is not running");
|
|
4814
5265
|
const claim = this.claimBlank();
|
|
4815
5266
|
try {
|
|
4816
|
-
const chromeTabId = await extension.createTab(windowId, claim.url);
|
|
5267
|
+
const chromeTabId = await extension.createTab(windowId, claim.url, signal);
|
|
4817
5268
|
return {
|
|
4818
|
-
page: await claim.page,
|
|
5269
|
+
page: await this.claimedPage(claim.page, signal),
|
|
4819
5270
|
chromeTabId
|
|
4820
5271
|
};
|
|
4821
5272
|
} finally {
|
|
@@ -4823,28 +5274,29 @@ var Windows = class {
|
|
|
4823
5274
|
}
|
|
4824
5275
|
}
|
|
4825
5276
|
/** Put a Session's tab in its group. The person's window is not a Session and stays ungrouped. */
|
|
4826
|
-
async groupTab(tab) {
|
|
5277
|
+
async groupTab(tab, signal) {
|
|
4827
5278
|
const extension = this.extension;
|
|
4828
5279
|
const chromeTabId = tab.chromeTabId;
|
|
4829
5280
|
if (extension === void 0 || chromeTabId === void 0 || tab.owner === "person") return;
|
|
4830
5281
|
try {
|
|
4831
5282
|
const known = this.groups.get(tab.owner);
|
|
4832
|
-
if (known === void 0) this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }));
|
|
5283
|
+
if (known === void 0) this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }, signal));
|
|
4833
5284
|
else try {
|
|
4834
|
-
await extension.group(chromeTabId, { groupId: known });
|
|
4835
|
-
} catch {
|
|
4836
|
-
|
|
5285
|
+
await extension.group(chromeTabId, { groupId: known }, signal);
|
|
5286
|
+
} catch (error) {
|
|
5287
|
+
if (isInterrupted(error)) return;
|
|
5288
|
+
this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }, signal));
|
|
4837
5289
|
}
|
|
4838
|
-
await this.applyGroupStyle(tab.owner);
|
|
5290
|
+
await this.applyGroupStyle(tab.owner, signal);
|
|
4839
5291
|
} catch {}
|
|
4840
5292
|
}
|
|
4841
|
-
async applyGroupStyle(owner) {
|
|
5293
|
+
async applyGroupStyle(owner, signal) {
|
|
4842
5294
|
const extension = this.extension;
|
|
4843
5295
|
const groupId = this.groups.get(owner);
|
|
4844
5296
|
if (extension === void 0 || groupId === void 0) return;
|
|
4845
5297
|
const title = this.titles.get(owner) ?? "";
|
|
4846
5298
|
const waiting = this.waiting.has(owner);
|
|
4847
|
-
await extension.styleGroup(groupId, waiting ? `${WAITING_TITLE}${title}` : title, waiting ? "red" : "blue");
|
|
5299
|
+
await extension.styleGroup(groupId, waiting ? `${WAITING_TITLE}${title}` : title, waiting ? "red" : "blue", signal);
|
|
4848
5300
|
}
|
|
4849
5301
|
async ingest(page) {
|
|
4850
5302
|
if (this.byPage.has(page)) return;
|
|
@@ -4866,6 +5318,10 @@ var Windows = class {
|
|
|
4866
5318
|
slot.accept(page);
|
|
4867
5319
|
return;
|
|
4868
5320
|
}
|
|
5321
|
+
if (page.url().startsWith("about:blank#mh-")) {
|
|
5322
|
+
await this.shutdown(page);
|
|
5323
|
+
return;
|
|
5324
|
+
}
|
|
4869
5325
|
const opener = await page.opener().catch(() => null);
|
|
4870
5326
|
const fromOpener = opener === null ? void 0 : this.byPage.get(opener);
|
|
4871
5327
|
if (fromOpener !== void 0) {
|
|
@@ -4907,10 +5363,9 @@ var Windows = class {
|
|
|
4907
5363
|
* or a close the service did not start counts as well. A page the content
|
|
4908
5364
|
* tab opened itself is not counted for its first navigation or its close.
|
|
4909
5365
|
*/
|
|
4910
|
-
async arm(page, owner, tab) {
|
|
5366
|
+
async arm(page, owner, tab, signal) {
|
|
4911
5367
|
if (this.armed.has(page)) return;
|
|
4912
|
-
|
|
4913
|
-
if (context === void 0) return;
|
|
5368
|
+
if (this.context === void 0) return;
|
|
4914
5369
|
this.armed.add(page);
|
|
4915
5370
|
page.on("close", () => {
|
|
4916
5371
|
const live = this.context;
|
|
@@ -4928,19 +5383,19 @@ var Windows = class {
|
|
|
4928
5383
|
});
|
|
4929
5384
|
let session;
|
|
4930
5385
|
try {
|
|
4931
|
-
session = await
|
|
5386
|
+
session = await this.pageSession(page, signal);
|
|
4932
5387
|
this.sessions.set(page, session);
|
|
4933
|
-
await
|
|
4934
|
-
await
|
|
4935
|
-
await
|
|
5388
|
+
await this.cdp(session, "Runtime.enable", void 0, signal);
|
|
5389
|
+
await this.cdp(session, "Page.enable", void 0, signal);
|
|
5390
|
+
await this.cdp(session, "Runtime.addBinding", {
|
|
4936
5391
|
name: BINDING,
|
|
4937
5392
|
executionContextName: WORLD
|
|
4938
|
-
});
|
|
4939
|
-
await
|
|
5393
|
+
}, signal);
|
|
5394
|
+
await this.cdp(session, "Page.addScriptToEvaluateOnNewDocument", {
|
|
4940
5395
|
source: LISTENER,
|
|
4941
5396
|
worldName: WORLD,
|
|
4942
5397
|
runImmediately: true
|
|
4943
|
-
});
|
|
5398
|
+
}, signal);
|
|
4944
5399
|
} catch (error) {
|
|
4945
5400
|
if (closedTarget(error)) {
|
|
4946
5401
|
const known = this.byPage.get(page);
|
|
@@ -4997,21 +5452,35 @@ var Windows = class {
|
|
|
4997
5452
|
this.sessions.delete(page);
|
|
4998
5453
|
session?.detach().catch(() => void 0);
|
|
4999
5454
|
}
|
|
5000
|
-
async targetIdOf(page) {
|
|
5001
|
-
const
|
|
5002
|
-
if (context === void 0) throw new Error("browser is not running");
|
|
5003
|
-
const session = await context.newCDPSession(page);
|
|
5455
|
+
async targetIdOf(page, signal) {
|
|
5456
|
+
const session = await this.pageSession(page, signal);
|
|
5004
5457
|
try {
|
|
5005
|
-
return (await
|
|
5458
|
+
return (await this.cdp(session, "Target.getTargetInfo", void 0, signal)).targetInfo.targetId;
|
|
5006
5459
|
} finally {
|
|
5007
|
-
await
|
|
5460
|
+
await this.shutdownSession(session);
|
|
5008
5461
|
}
|
|
5009
5462
|
}
|
|
5010
|
-
async windowIdOf(page) {
|
|
5463
|
+
async windowIdOf(page, signal) {
|
|
5011
5464
|
const session = this.browserSession;
|
|
5012
5465
|
if (session === void 0) throw new Error("browser is not running");
|
|
5013
|
-
const targetId = await this.targetIdOf(page);
|
|
5014
|
-
return (await
|
|
5466
|
+
const targetId = await this.targetIdOf(page, signal);
|
|
5467
|
+
return (await this.cdp(session, "Browser.getWindowForTarget", { targetId }, signal)).windowId;
|
|
5468
|
+
}
|
|
5469
|
+
pageSession(page, signal) {
|
|
5470
|
+
const context = this.context;
|
|
5471
|
+
if (context === void 0) return Promise.reject(/* @__PURE__ */ new Error("browser is not running"));
|
|
5472
|
+
return untilAborted(context.newCDPSession(page), capped(signal, CALL_MS));
|
|
5473
|
+
}
|
|
5474
|
+
cdp(session, method, params, signal) {
|
|
5475
|
+
const send = session.send.bind(session);
|
|
5476
|
+
return untilAborted(params === void 0 ? send(method) : send(method, params), capped(signal, CALL_MS));
|
|
5477
|
+
}
|
|
5478
|
+
/** Close a page that may no longer answer. A stall ends at `CLOSE_MS`. */
|
|
5479
|
+
shutdown(page) {
|
|
5480
|
+
return untilAborted(page.close(), AbortSignal.timeout(CLOSE_MS)).catch(() => void 0);
|
|
5481
|
+
}
|
|
5482
|
+
shutdownSession(session) {
|
|
5483
|
+
return untilAborted(session.detach(), AbortSignal.timeout(CLOSE_MS)).catch(() => void 0);
|
|
5015
5484
|
}
|
|
5016
5485
|
};
|
|
5017
5486
|
/** Playwright's error once the page, its context, or the browser has gone. */
|
|
@@ -5104,6 +5573,10 @@ const RELAUNCH_WINDOW_MS = 6e4;
|
|
|
5104
5573
|
const DOCUMENT_CAP = 64;
|
|
5105
5574
|
const OPEN_ADDRESS_REASON = "only http and https addresses are opened";
|
|
5106
5575
|
const STOP_WAIT_MS = 5e3;
|
|
5576
|
+
/** A page read the renderer no longer answers. The caller's signal can end it sooner. */
|
|
5577
|
+
const READ_MS = 15e3;
|
|
5578
|
+
/** Closing one Session's windows must not wait on a renderer that stopped answering. */
|
|
5579
|
+
const DISPOSE_MS = 3e4;
|
|
5107
5580
|
const WAITING_STOPPED = "stopped while waiting";
|
|
5108
5581
|
const STOPPED = Symbol("stopped");
|
|
5109
5582
|
const NAVIGATED_DURING_EVALUATE = "The page navigated before the function returned.";
|
|
@@ -5119,11 +5592,24 @@ const NO_WORKSPACE_REASON = "this session has no working directory";
|
|
|
5119
5592
|
let MhBrowserService = (() => {
|
|
5120
5593
|
let _classSuper = TypertRemoteService;
|
|
5121
5594
|
let _instanceExtraInitializers = [];
|
|
5595
|
+
let _locateFor_decorators;
|
|
5122
5596
|
let _revealHandover_decorators;
|
|
5123
5597
|
return class MhBrowserService extends _classSuper {
|
|
5124
5598
|
static {
|
|
5125
5599
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
5600
|
+
_locateFor_decorators = [Remote];
|
|
5126
5601
|
_revealHandover_decorators = [Remote];
|
|
5602
|
+
__esDecorate(this, null, _locateFor_decorators, {
|
|
5603
|
+
kind: "method",
|
|
5604
|
+
name: "locateFor",
|
|
5605
|
+
static: false,
|
|
5606
|
+
private: false,
|
|
5607
|
+
access: {
|
|
5608
|
+
has: (obj) => "locateFor" in obj,
|
|
5609
|
+
get: (obj) => obj.locateFor
|
|
5610
|
+
},
|
|
5611
|
+
metadata: _metadata
|
|
5612
|
+
}, null, _instanceExtraInitializers);
|
|
5127
5613
|
__esDecorate(this, null, _revealHandover_decorators, {
|
|
5128
5614
|
kind: "method",
|
|
5129
5615
|
name: "revealHandover",
|
|
@@ -5142,13 +5628,23 @@ let MhBrowserService = (() => {
|
|
|
5142
5628
|
value: _metadata
|
|
5143
5629
|
});
|
|
5144
5630
|
}
|
|
5145
|
-
static Config = browserConfig;
|
|
5146
5631
|
windows = (__runInitializers(this, _instanceExtraInitializers), new Windows());
|
|
5147
|
-
|
|
5632
|
+
headless;
|
|
5633
|
+
profileDir;
|
|
5634
|
+
browserName;
|
|
5635
|
+
executablePath;
|
|
5636
|
+
/** Tests replace the machine lookup. Unset uses this machine. */
|
|
5637
|
+
locator;
|
|
5148
5638
|
context;
|
|
5149
5639
|
pids = /* @__PURE__ */ new WeakMap();
|
|
5150
5640
|
ready = Promise.resolve();
|
|
5151
5641
|
closing = false;
|
|
5642
|
+
/** A browser restart was requested and has not run yet. */
|
|
5643
|
+
relaunchWanted = false;
|
|
5644
|
+
/** Counts browser and path changes. A start that sampled the latest one clears the restart. */
|
|
5645
|
+
configEpoch = 0;
|
|
5646
|
+
/** The running browser is being closed so the new configuration can start. */
|
|
5647
|
+
replacing = false;
|
|
5152
5648
|
/** The browser is not running. The next call that needs a page starts it. */
|
|
5153
5649
|
exited = true;
|
|
5154
5650
|
exits = [];
|
|
@@ -5167,11 +5663,16 @@ let MhBrowserService = (() => {
|
|
|
5167
5663
|
constructor(ctx, config) {
|
|
5168
5664
|
super(ctx, "mhBrowser");
|
|
5169
5665
|
this.maxBytes = config.presence.maxBytes;
|
|
5170
|
-
this.
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5666
|
+
this.headless = config.headless;
|
|
5667
|
+
this.profileDir = config.profileDir ?? defaultProfileDir(ctx.get("profileContext"));
|
|
5668
|
+
this.browserName = config.browser;
|
|
5669
|
+
this.executablePath = config.executablePath ?? { get: () => void 0 };
|
|
5670
|
+
Object.getPrototypeOf(ctx).on("loader/volatile-update", (paths) => {
|
|
5671
|
+
if (!paths.some((path) => path.includes("browser") || path.includes("executablePath"))) return;
|
|
5672
|
+
this.configEpoch += 1;
|
|
5673
|
+
this.relaunchWanted = true;
|
|
5674
|
+
this.relaunchIfIdle();
|
|
5675
|
+
});
|
|
5175
5676
|
this.windows.setPersonInput((owner, tab) => {
|
|
5176
5677
|
const live = tab === void 0 ? void 0 : this.windows.get(owner, tab);
|
|
5177
5678
|
this.control.note(owner, tab);
|
|
@@ -5184,7 +5685,7 @@ let MhBrowserService = (() => {
|
|
|
5184
5685
|
ctx.on("session/disposed", (session) => {
|
|
5185
5686
|
const owner = session.id;
|
|
5186
5687
|
if (owner === void 0) return;
|
|
5187
|
-
this.exclusive(owner, () => this.windows.closeOwner(owner)).catch((error) => {
|
|
5688
|
+
this.exclusive(owner, () => this.windows.closeOwner(owner, AbortSignal.timeout(DISPOSE_MS)), AbortSignal.timeout(DISPOSE_MS)).catch((error) => {
|
|
5188
5689
|
ctx.logger.warn("closing the windows of %s failed: %o", owner, error);
|
|
5189
5690
|
});
|
|
5190
5691
|
this.book.forget(owner);
|
|
@@ -5206,22 +5707,30 @@ let MhBrowserService = (() => {
|
|
|
5206
5707
|
}
|
|
5207
5708
|
async open(owner, items, signal, options) {
|
|
5208
5709
|
const reveal = options?.reveal === true;
|
|
5209
|
-
|
|
5210
|
-
await this.
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5710
|
+
try {
|
|
5711
|
+
return await this.exclusive(owner, async () => {
|
|
5712
|
+
await this.up(owner === PERSON_OWNER);
|
|
5713
|
+
const views = [];
|
|
5714
|
+
for (const item of items) {
|
|
5715
|
+
if (signal?.aborted) {
|
|
5716
|
+
views.push({
|
|
5717
|
+
url: item.url,
|
|
5718
|
+
reason: WAITING_STOPPED
|
|
5719
|
+
});
|
|
5720
|
+
continue;
|
|
5721
|
+
}
|
|
5722
|
+
views.push(await this.openAddress(owner, item, signal, reveal));
|
|
5219
5723
|
}
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5724
|
+
await this.enforceCap(owner);
|
|
5725
|
+
return views;
|
|
5726
|
+
}, signal);
|
|
5727
|
+
} catch (error) {
|
|
5728
|
+
if (!isInterrupted(error)) throw error;
|
|
5729
|
+
return items.map((item) => ({
|
|
5730
|
+
url: item.url,
|
|
5731
|
+
reason: WAITING_STOPPED
|
|
5732
|
+
}));
|
|
5733
|
+
}
|
|
5225
5734
|
}
|
|
5226
5735
|
async view(owner, tab, signal) {
|
|
5227
5736
|
return this.exclusive(owner, async () => {
|
|
@@ -5233,8 +5742,8 @@ let MhBrowserService = (() => {
|
|
|
5233
5742
|
url: live.url,
|
|
5234
5743
|
reason: WAITING_STOPPED
|
|
5235
5744
|
};
|
|
5236
|
-
return this.refreshView(live);
|
|
5237
|
-
});
|
|
5745
|
+
return this.refreshView(live, signal);
|
|
5746
|
+
}, signal);
|
|
5238
5747
|
}
|
|
5239
5748
|
/** Pages whose current representation is already in the model's presence. */
|
|
5240
5749
|
rememberPresence(owner, urls) {
|
|
@@ -5300,7 +5809,7 @@ let MhBrowserService = (() => {
|
|
|
5300
5809
|
text: planned.text,
|
|
5301
5810
|
record: planned.record
|
|
5302
5811
|
};
|
|
5303
|
-
});
|
|
5812
|
+
}, opts?.signal);
|
|
5304
5813
|
}
|
|
5305
5814
|
/**
|
|
5306
5815
|
* Where a download the page starts may be written: the session's writable
|
|
@@ -5342,7 +5851,7 @@ let MhBrowserService = (() => {
|
|
|
5342
5851
|
kind: "response",
|
|
5343
5852
|
finalUrl: response.url(),
|
|
5344
5853
|
status: response.status(),
|
|
5345
|
-
body: new Uint8Array(await response.body())
|
|
5854
|
+
body: new Uint8Array(await untilAborted(response.body(), capped(opts?.signal, READ_MS)))
|
|
5346
5855
|
};
|
|
5347
5856
|
const mediaType = headerMap["content-type"];
|
|
5348
5857
|
const etag = headerMap.etag;
|
|
@@ -5382,9 +5891,26 @@ let MhBrowserService = (() => {
|
|
|
5382
5891
|
page: (owner, tab) => this.windows.get(owner, tab)?.page,
|
|
5383
5892
|
running: () => this.context !== void 0,
|
|
5384
5893
|
pages: () => this.context?.pages() ?? [],
|
|
5385
|
-
pid: () => this.context === void 0 ? void 0 : this.pids.get(this.context)
|
|
5894
|
+
pid: () => this.context === void 0 ? void 0 : this.pids.get(this.context),
|
|
5895
|
+
useLocator: (env) => {
|
|
5896
|
+
this.locator = env;
|
|
5897
|
+
}
|
|
5386
5898
|
};
|
|
5387
5899
|
}
|
|
5900
|
+
/**
|
|
5901
|
+
* Where `browser` would be launched from when no path is configured.
|
|
5902
|
+
* The configured path is not consulted: the answer is what an empty path uses.
|
|
5903
|
+
*/
|
|
5904
|
+
async locateFor(browser, signal) {
|
|
5905
|
+
const stopped = () => signal?.aborted === true;
|
|
5906
|
+
if (stopped()) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
5907
|
+
if (browser !== "chrome" && browser !== "msedge" && browser !== "chromium") throw new MhBrowserError("INVALID_ARGS", "browser must be chrome, msedge, or chromium");
|
|
5908
|
+
const env = this.locateEnv();
|
|
5909
|
+
if (browser === "chromium") return bundledChromiumLocation(env.access);
|
|
5910
|
+
const located = await locateInstalled(browser, env, signal);
|
|
5911
|
+
if (stopped()) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
5912
|
+
return located;
|
|
5913
|
+
}
|
|
5388
5914
|
async act(owner, tab, op, signal) {
|
|
5389
5915
|
return this.exclusive(owner, async () => {
|
|
5390
5916
|
await this.ready;
|
|
@@ -5396,7 +5922,7 @@ let MhBrowserService = (() => {
|
|
|
5396
5922
|
try {
|
|
5397
5923
|
if (live.dialogHeld && op.kind !== "handle_dialog") throw new MhBrowserError("MODAL_STATE", "a dialog is open; use browser_handle_dialog", await this.blockedView(live));
|
|
5398
5924
|
if (live.fileChooser !== void 0 && op.kind !== "file_upload") {
|
|
5399
|
-
const blocked = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" });
|
|
5925
|
+
const blocked = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }, signal);
|
|
5400
5926
|
blocked.dialog = {
|
|
5401
5927
|
type: "filechooser",
|
|
5402
5928
|
message: live.fileChooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
|
|
@@ -5409,8 +5935,8 @@ let MhBrowserService = (() => {
|
|
|
5409
5935
|
else this.windows.holdInput(live);
|
|
5410
5936
|
} else if (outcome.navigated) this.windows.onNavigated(live);
|
|
5411
5937
|
this.windows.touch(live);
|
|
5412
|
-
const shown = outcome.navigated ? await this.discover(live, live.page.url(), null) : await this.refreshSnapshot(live);
|
|
5413
|
-
const scoped = op.kind === "snapshot" && (op.target !== void 0 || op.depth !== void 0 || op.boxes === true) ? await this.scopedSnapshot(live, op) : void 0;
|
|
5938
|
+
const shown = outcome.navigated ? await this.discover(live, live.page.url(), null, signal) : await this.refreshSnapshot(live, signal);
|
|
5939
|
+
const scoped = op.kind === "snapshot" && (op.target !== void 0 || op.depth !== void 0 || op.boxes === true) ? await this.scopedSnapshot(live, op, signal) : void 0;
|
|
5414
5940
|
this.shown.set(live.id, shown);
|
|
5415
5941
|
this.windows.noteUrl(live, live.page.url());
|
|
5416
5942
|
const view = await this.pageFromShown(live, shown, {
|
|
@@ -5418,7 +5944,7 @@ let MhBrowserService = (() => {
|
|
|
5418
5944
|
...outcome.dialog !== void 0 ? { dialog: outcome.dialog } : {},
|
|
5419
5945
|
...outcome.incomplete !== void 0 ? { incomplete: outcome.incomplete } : {},
|
|
5420
5946
|
...outcome.waited !== void 0 ? { waited: outcome.waited } : {}
|
|
5421
|
-
});
|
|
5947
|
+
}, signal);
|
|
5422
5948
|
if (scoped !== void 0 && view.representation.kind === "snapshot") view.representation = {
|
|
5423
5949
|
...view.representation,
|
|
5424
5950
|
text: scoped
|
|
@@ -5435,12 +5961,12 @@ let MhBrowserService = (() => {
|
|
|
5435
5961
|
} catch (error) {
|
|
5436
5962
|
if (error instanceof MhBrowserError && error.code === "TAKEN_OVER") throw error;
|
|
5437
5963
|
if (this.control.ofTab(owner, tab) === "person") throw await this.takenOver(live);
|
|
5438
|
-
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" }));
|
|
5964
|
+
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));
|
|
5439
5965
|
throw error;
|
|
5440
5966
|
} finally {
|
|
5441
5967
|
linked.release();
|
|
5442
5968
|
}
|
|
5443
|
-
});
|
|
5969
|
+
}, signal);
|
|
5444
5970
|
}
|
|
5445
5971
|
holdHandover(owner, tab) {
|
|
5446
5972
|
const live = this.windows.get(owner, tab);
|
|
@@ -5458,15 +5984,16 @@ let MhBrowserService = (() => {
|
|
|
5458
5984
|
const live = this.windows.get(owner, tab);
|
|
5459
5985
|
if (live === void 0 || live.dialogHeld) return void 0;
|
|
5460
5986
|
try {
|
|
5461
|
-
const
|
|
5987
|
+
const limit = capped(signal, READ_MS);
|
|
5988
|
+
const bytes = await untilAborted(live.page.screenshot({
|
|
5462
5989
|
type: "png",
|
|
5463
|
-
|
|
5464
|
-
});
|
|
5990
|
+
signal: limit
|
|
5991
|
+
}), limit);
|
|
5465
5992
|
return new Uint8Array(bytes);
|
|
5466
5993
|
} catch {
|
|
5467
5994
|
return;
|
|
5468
5995
|
}
|
|
5469
|
-
});
|
|
5996
|
+
}, signal);
|
|
5470
5997
|
}
|
|
5471
5998
|
invoke(owner, _page, call, opts) {
|
|
5472
5999
|
return this.exclusive(owner, async () => {
|
|
@@ -5483,7 +6010,7 @@ let MhBrowserService = (() => {
|
|
|
5483
6010
|
}, call);
|
|
5484
6011
|
await this.enforceCap(owner);
|
|
5485
6012
|
return sent;
|
|
5486
|
-
});
|
|
6013
|
+
}, opts?.signal);
|
|
5487
6014
|
}
|
|
5488
6015
|
/**
|
|
5489
6016
|
* Not queued: it opens no tab, and a long body must not hold up the
|
|
@@ -5515,11 +6042,11 @@ let MhBrowserService = (() => {
|
|
|
5515
6042
|
const live = this.windows.get(owner, req.tab);
|
|
5516
6043
|
if (live === void 0) throw new MhBrowserError(UNKNOWN_TAB, UNKNOWN_TAB_REASON);
|
|
5517
6044
|
this.windows.holdHandover(live);
|
|
5518
|
-
await this.windows.activate(live).catch(() => void 0);
|
|
6045
|
+
await this.windows.activate(live, signal).catch(() => void 0);
|
|
5519
6046
|
}
|
|
5520
6047
|
this.control.handover(owner, req.tab);
|
|
5521
6048
|
this.windows.markWaiting(owner, true);
|
|
5522
|
-
});
|
|
6049
|
+
}, signal);
|
|
5523
6050
|
}
|
|
5524
6051
|
/** Remember the Session title for its tab group. Empty when the host has none. */
|
|
5525
6052
|
noteSessionTitle(owner, title) {
|
|
@@ -5530,7 +6057,7 @@ let MhBrowserService = (() => {
|
|
|
5530
6057
|
await this.ready;
|
|
5531
6058
|
const live = this.windows.latest(owner);
|
|
5532
6059
|
if (live === void 0) return false;
|
|
5533
|
-
await live.page.bringToFront();
|
|
6060
|
+
await untilAborted(live.page.bringToFront(), AbortSignal.timeout(READ_MS));
|
|
5534
6061
|
return true;
|
|
5535
6062
|
}
|
|
5536
6063
|
/** The person asked to see the page this Session handed over. */
|
|
@@ -5541,11 +6068,12 @@ let MhBrowserService = (() => {
|
|
|
5541
6068
|
const named = this.control.namedTab(sessionId);
|
|
5542
6069
|
const live = (named === void 0 ? void 0 : this.windows.get(sessionId, named)) ?? this.windows.latest(sessionId);
|
|
5543
6070
|
if (live === void 0) return { status: "no-tab" };
|
|
5544
|
-
await live.page.bringToFront();
|
|
6071
|
+
await untilAborted(live.page.bringToFront(), capped(signal, READ_MS));
|
|
5545
6072
|
return { status: "revealed" };
|
|
5546
6073
|
}
|
|
5547
6074
|
/** The person answered. Returns the note, the operation count, and the named tab. */
|
|
5548
6075
|
async handBack(owner, note) {
|
|
6076
|
+
const signal = capped(void 0, READ_MS);
|
|
5549
6077
|
return this.exclusive(owner, async () => {
|
|
5550
6078
|
await this.ready;
|
|
5551
6079
|
const taken = this.control.finish(owner);
|
|
@@ -5554,32 +6082,36 @@ let MhBrowserService = (() => {
|
|
|
5554
6082
|
this.windows.markWaiting(owner, false);
|
|
5555
6083
|
const urls = this.presenceUrls(owner, taken.tabs);
|
|
5556
6084
|
for (const touch of this.personTouch) touch(owner, urls);
|
|
5557
|
-
const page = await this.snapshotNamed(owner, taken.tab);
|
|
6085
|
+
const page = await this.snapshotNamed(owner, taken.tab, signal);
|
|
5558
6086
|
return {
|
|
5559
6087
|
...taken,
|
|
5560
6088
|
urls,
|
|
5561
6089
|
...note !== void 0 && note !== "" ? { note } : {},
|
|
5562
6090
|
...page !== void 0 ? { page } : {}
|
|
5563
6091
|
};
|
|
5564
|
-
});
|
|
6092
|
+
}, signal);
|
|
5565
6093
|
}
|
|
5566
6094
|
/** A stopped handover gives control back. A finished hand-back is left alone. */
|
|
5567
6095
|
releaseIfHeld(owner) {
|
|
5568
6096
|
if (!this.control.releaseIfHeld(owner)) return;
|
|
5569
6097
|
this.windows.releaseHandover(owner);
|
|
5570
6098
|
this.windows.markWaiting(owner, false);
|
|
6099
|
+
this.relaunchIfIdle();
|
|
5571
6100
|
}
|
|
5572
6101
|
/**
|
|
5573
6102
|
* The person took over between turns. The next message they send is the
|
|
5574
6103
|
* hand-back. A handover that is still waiting on its card is not.
|
|
6104
|
+
* The addresses are read before a waiting restart closes the pages.
|
|
5575
6105
|
*/
|
|
5576
6106
|
takePersonHandback(owner) {
|
|
5577
6107
|
const taken = this.control.takePerson(owner);
|
|
5578
6108
|
if (taken === void 0) return void 0;
|
|
6109
|
+
const urls = this.presenceUrls(owner, taken.tabs);
|
|
5579
6110
|
this.windows.releaseHandover(owner);
|
|
6111
|
+
this.relaunchIfIdle();
|
|
5580
6112
|
return {
|
|
5581
6113
|
...taken,
|
|
5582
|
-
urls
|
|
6114
|
+
urls
|
|
5583
6115
|
};
|
|
5584
6116
|
}
|
|
5585
6117
|
/**
|
|
@@ -5603,7 +6135,7 @@ let MhBrowserService = (() => {
|
|
|
5603
6135
|
}
|
|
5604
6136
|
const opened = await this.openOne(owner, item.url, item.waitFor, signal, item.snapshot === true, reveal);
|
|
5605
6137
|
this.keepAgentView(opened);
|
|
5606
|
-
if (!wantsTab && opened.fresh && this.closable(owner, opened.live) && "representation" in opened.view && opened.view.representation.kind === "agent-view") {
|
|
6138
|
+
if (opened.live !== void 0 && !wantsTab && opened.fresh && this.closable(owner, opened.live) && "representation" in opened.view && opened.view.representation.kind === "agent-view") {
|
|
5607
6139
|
await this.windows.closeTab(opened.live);
|
|
5608
6140
|
const view = { ...opened.view };
|
|
5609
6141
|
delete view.tab;
|
|
@@ -5629,6 +6161,7 @@ let MhBrowserService = (() => {
|
|
|
5629
6161
|
}
|
|
5630
6162
|
/** A readable Agent View puts its origin on the list and keeps the document for the next read. */
|
|
5631
6163
|
keepAgentView(opened) {
|
|
6164
|
+
if (opened.live === void 0) return;
|
|
5632
6165
|
if (!("representation" in opened.view)) return;
|
|
5633
6166
|
const representation = opened.view.representation;
|
|
5634
6167
|
if (!(representation.kind === "agent-view" || representation.kind === "snapshot" && representation.agentView !== void 0)) return;
|
|
@@ -5760,7 +6293,7 @@ let MhBrowserService = (() => {
|
|
|
5760
6293
|
kind: "response",
|
|
5761
6294
|
finalUrl: current,
|
|
5762
6295
|
status,
|
|
5763
|
-
body: readsAsText(mediaType) ? new Uint8Array(await response.body()) : /* @__PURE__ */ new Uint8Array()
|
|
6296
|
+
body: readsAsText(mediaType) ? new Uint8Array(await untilAborted(response.body(), capped(signal, READ_MS))) : /* @__PURE__ */ new Uint8Array()
|
|
5764
6297
|
};
|
|
5765
6298
|
const etag = headerMap.etag;
|
|
5766
6299
|
const link = headerMap.link;
|
|
@@ -5773,8 +6306,20 @@ let MhBrowserService = (() => {
|
|
|
5773
6306
|
async openOne(owner, url, waitFor, signal, snapshot = false, reveal = false) {
|
|
5774
6307
|
assertWaitFor(waitFor);
|
|
5775
6308
|
const reused = this.idleTabOn(owner, url);
|
|
5776
|
-
const live = reused ?? await this.windows.openBlank(owner);
|
|
5777
6309
|
const fresh = reused === void 0;
|
|
6310
|
+
let live;
|
|
6311
|
+
try {
|
|
6312
|
+
live = reused ?? await this.windows.openBlank(owner, signal);
|
|
6313
|
+
} catch (error) {
|
|
6314
|
+
if (signal?.aborted === true || isInterrupted(error)) return {
|
|
6315
|
+
view: {
|
|
6316
|
+
url,
|
|
6317
|
+
reason: WAITING_STOPPED
|
|
6318
|
+
},
|
|
6319
|
+
fresh
|
|
6320
|
+
};
|
|
6321
|
+
throw error;
|
|
6322
|
+
}
|
|
5778
6323
|
const linked = this.control.link(owner, signal, live.id);
|
|
5779
6324
|
if (reveal && !linked.signal.aborted) await live.page.bringToFront().catch(() => void 0);
|
|
5780
6325
|
const nextDialog = this.windows.nextDialog(live);
|
|
@@ -5797,7 +6342,7 @@ let MhBrowserService = (() => {
|
|
|
5797
6342
|
nextDialog.cancel();
|
|
5798
6343
|
if (winner === "failed" && seenDialog === void 0) {
|
|
5799
6344
|
if (isDownloadStart(navigationError) || live.pendingDownload !== void 0) {
|
|
5800
|
-
const view = await this.pageFromShown(live, this.shown.get(live.id));
|
|
6345
|
+
const view = await this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
5801
6346
|
if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
|
|
5802
6347
|
this.applyDocumentType(live, view);
|
|
5803
6348
|
return {
|
|
@@ -5848,8 +6393,8 @@ let MhBrowserService = (() => {
|
|
|
5848
6393
|
...optionalFragment(address),
|
|
5849
6394
|
snapshot: "",
|
|
5850
6395
|
relocate: false,
|
|
5851
|
-
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch)
|
|
5852
|
-
}) : await this.discover(live, address, response);
|
|
6396
|
+
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
6397
|
+
}) : await this.discover(live, address, response, signal);
|
|
5853
6398
|
this.shown.set(live.id, shown);
|
|
5854
6399
|
const view = await this.pageFromShown(live, shown, {
|
|
5855
6400
|
...snapshot ? { surface: "snapshot" } : {},
|
|
@@ -5859,7 +6404,7 @@ let MhBrowserService = (() => {
|
|
|
5859
6404
|
} },
|
|
5860
6405
|
...load.dialog !== void 0 ? { dialog: load.dialog } : {},
|
|
5861
6406
|
...waited !== void 0 ? { waited } : {}
|
|
5862
|
-
});
|
|
6407
|
+
}, signal);
|
|
5863
6408
|
return {
|
|
5864
6409
|
view: await this.attachDownload(live, view, false),
|
|
5865
6410
|
fresh,
|
|
@@ -5894,7 +6439,7 @@ let MhBrowserService = (() => {
|
|
|
5894
6439
|
if (op.kind === "snapshot") return { navigated: false };
|
|
5895
6440
|
if (op.kind === "find") return {
|
|
5896
6441
|
navigated: false,
|
|
5897
|
-
summary: findInSnapshot(await this.aria(live), op)
|
|
6442
|
+
summary: findInSnapshot(await this.aria(live, signal), op)
|
|
5898
6443
|
};
|
|
5899
6444
|
if (op.kind === "evaluate") {
|
|
5900
6445
|
const snapshot = this.shown.get(live.id)?.snapshot ?? "";
|
|
@@ -6086,15 +6631,16 @@ let MhBrowserService = (() => {
|
|
|
6086
6631
|
};
|
|
6087
6632
|
}
|
|
6088
6633
|
/** A snapshot limited to a target, a depth, or boxes. Refs still resolve against the full one. */
|
|
6089
|
-
async scopedSnapshot(tab, op) {
|
|
6634
|
+
async scopedSnapshot(tab, op, signal) {
|
|
6090
6635
|
const options = {
|
|
6091
6636
|
mode: "ai",
|
|
6092
6637
|
...op.depth !== void 0 ? { depth: op.depth } : {},
|
|
6093
6638
|
...op.boxes === true ? { boxes: true } : {}
|
|
6094
6639
|
};
|
|
6095
6640
|
const snapshot = this.shown.get(tab.id)?.snapshot ?? "";
|
|
6096
|
-
|
|
6097
|
-
return tab.page.ariaSnapshot(options);
|
|
6641
|
+
const limit = capped(signal, READ_MS);
|
|
6642
|
+
if (op.target !== void 0) return untilAborted((await locate(tab.page, op.target, snapshot)).ariaSnapshot(options), limit);
|
|
6643
|
+
return untilAborted(tab.page.ariaSnapshot(options), limit);
|
|
6098
6644
|
}
|
|
6099
6645
|
/** A file chooser the action opened. The tab stays in use until it navigates. */
|
|
6100
6646
|
chooserHeld(live, chooser) {
|
|
@@ -6105,9 +6651,9 @@ let MhBrowserService = (() => {
|
|
|
6105
6651
|
message: chooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
|
|
6106
6652
|
};
|
|
6107
6653
|
}
|
|
6108
|
-
async refreshSnapshot(tab) {
|
|
6654
|
+
async refreshSnapshot(tab, signal) {
|
|
6109
6655
|
const previous = this.shown.get(tab.id);
|
|
6110
|
-
const snapshot = tab.dialogHeld ? "" : await this.aria(tab);
|
|
6656
|
+
const snapshot = tab.dialogHeld ? "" : await this.aria(tab, signal);
|
|
6111
6657
|
if (previous === void 0) return {
|
|
6112
6658
|
pageUrl: canonical(tab.page.url()),
|
|
6113
6659
|
digest: "",
|
|
@@ -6142,20 +6688,20 @@ let MhBrowserService = (() => {
|
|
|
6142
6688
|
this.windows.endDispatch(page);
|
|
6143
6689
|
}
|
|
6144
6690
|
}
|
|
6145
|
-
async discover(tab, address, response) {
|
|
6146
|
-
const main = response === null ? void 0 : await mainDocument(response);
|
|
6691
|
+
async discover(tab, address, response, signal) {
|
|
6692
|
+
const main = response === null ? void 0 : await mainDocument(response, signal);
|
|
6147
6693
|
const relocated = main !== void 0 && !samePage(main.finalUrl, address);
|
|
6148
|
-
const domLinks = relocated ? [] : await readDomLinks(tab.page);
|
|
6694
|
+
const domLinks = relocated ? [] : await readDomLinks(tab.page, signal);
|
|
6149
6695
|
const previous = this.shown.get(tab.id);
|
|
6150
6696
|
return loadShown({
|
|
6151
6697
|
pageUrl: canonical(address),
|
|
6152
6698
|
...optionalFragment(address),
|
|
6153
|
-
snapshot: await this.aria(tab),
|
|
6699
|
+
snapshot: await this.aria(tab, signal),
|
|
6154
6700
|
...main !== void 0 && !relocated ? { main } : {},
|
|
6155
6701
|
...domLinks.length > 0 ? { domLinks } : {},
|
|
6156
6702
|
...previous !== void 0 ? { previous } : {},
|
|
6157
6703
|
relocate: relocated,
|
|
6158
|
-
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch)
|
|
6704
|
+
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
6159
6705
|
});
|
|
6160
6706
|
}
|
|
6161
6707
|
fetchForRead(url, accept, ifNoneMatch, signal) {
|
|
@@ -6165,13 +6711,13 @@ let MhBrowserService = (() => {
|
|
|
6165
6711
|
...signal !== void 0 ? { signal } : {}
|
|
6166
6712
|
});
|
|
6167
6713
|
}
|
|
6168
|
-
async aria(tab) {
|
|
6169
|
-
return tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
|
|
6714
|
+
async aria(tab, signal) {
|
|
6715
|
+
return untilAborted(tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
|
|
6170
6716
|
this.ctx.logger.debug("aria snapshot failed: %s", errorText(error));
|
|
6171
6717
|
return "";
|
|
6172
|
-
});
|
|
6718
|
+
}), capped(signal, READ_MS));
|
|
6173
6719
|
}
|
|
6174
|
-
async pageFromShown(tab, shown, extra) {
|
|
6720
|
+
async pageFromShown(tab, shown, extra, signal) {
|
|
6175
6721
|
const current = shown ?? {
|
|
6176
6722
|
pageUrl: canonical(tab.url),
|
|
6177
6723
|
digest: "",
|
|
@@ -6179,10 +6725,10 @@ let MhBrowserService = (() => {
|
|
|
6179
6725
|
text: "",
|
|
6180
6726
|
snapshot: ""
|
|
6181
6727
|
};
|
|
6182
|
-
const title = tab.dialogHeld ? "" : await tab.page.title().catch((error) => {
|
|
6728
|
+
const title = tab.dialogHeld ? "" : await untilAborted(tab.page.title().catch((error) => {
|
|
6183
6729
|
this.ctx.logger.debug("page title failed: %s", errorText(error));
|
|
6184
6730
|
return "";
|
|
6185
|
-
});
|
|
6731
|
+
}), capped(signal, READ_MS));
|
|
6186
6732
|
this.windows.noteTitle(tab, title);
|
|
6187
6733
|
const view = {
|
|
6188
6734
|
tab: tab.id,
|
|
@@ -6227,7 +6773,7 @@ let MhBrowserService = (() => {
|
|
|
6227
6773
|
if (via !== void 0) return observationOf(url, via);
|
|
6228
6774
|
}
|
|
6229
6775
|
const existing = this.tabOn(owner, url);
|
|
6230
|
-
if (existing !== void 0) return observationOf(url, await this.refreshView(existing));
|
|
6776
|
+
if (existing !== void 0) return observationOf(url, await this.refreshView(existing, signal));
|
|
6231
6777
|
const opened = await this.openOne(owner, url, void 0, signal, false);
|
|
6232
6778
|
this.keepAgentView(opened);
|
|
6233
6779
|
const view = opened.view;
|
|
@@ -6251,22 +6797,22 @@ let MhBrowserService = (() => {
|
|
|
6251
6797
|
tabs.sort((left, right) => right.touchedAt - left.touchedAt);
|
|
6252
6798
|
return tabs[0];
|
|
6253
6799
|
}
|
|
6254
|
-
async refreshView(live) {
|
|
6800
|
+
async refreshView(live, signal) {
|
|
6255
6801
|
this.windows.touch(live);
|
|
6256
|
-
if (live.dialogHeld) return this.pageFromShown(live, this.shown.get(live.id));
|
|
6802
|
+
if (live.dialogHeld) return this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
6257
6803
|
const address = live.page.url();
|
|
6258
6804
|
const previous = this.shown.get(live.id);
|
|
6259
6805
|
const shown = await loadShown({
|
|
6260
6806
|
pageUrl: canonical(address),
|
|
6261
6807
|
...optionalFragment(address),
|
|
6262
|
-
snapshot: await this.aria(live),
|
|
6808
|
+
snapshot: await this.aria(live, signal),
|
|
6263
6809
|
...previous !== void 0 ? { previous } : {},
|
|
6264
6810
|
relocate: previous !== void 0 && !samePage(previous.pageUrl, address),
|
|
6265
|
-
fetch: (url, accept, ifNoneMatch) => this.fetchForRead(url, accept, ifNoneMatch)
|
|
6811
|
+
fetch: (url, accept, ifNoneMatch) => this.fetchForRead(url, accept, ifNoneMatch, signal)
|
|
6266
6812
|
});
|
|
6267
6813
|
this.shown.set(live.id, shown);
|
|
6268
6814
|
this.windows.noteUrl(live, address);
|
|
6269
|
-
return this.pageFromShown(live, shown);
|
|
6815
|
+
return this.pageFromShown(live, shown, void 0, signal);
|
|
6270
6816
|
}
|
|
6271
6817
|
/** The most recently touched idle tab on this address. A tab the person holds is left alone. */
|
|
6272
6818
|
idleTabOn(owner, url) {
|
|
@@ -6299,7 +6845,7 @@ let MhBrowserService = (() => {
|
|
|
6299
6845
|
const load = await waitForLoad(live.page, signal);
|
|
6300
6846
|
if (load.dialog !== void 0) this.windows.holdDialog(live);
|
|
6301
6847
|
this.windows.touch(live);
|
|
6302
|
-
const shown = await this.discover(live, live.page.url(), response);
|
|
6848
|
+
const shown = await this.discover(live, live.page.url(), response, signal);
|
|
6303
6849
|
this.shown.set(live.id, shown);
|
|
6304
6850
|
const view = await this.pageFromShown(live, shown, {
|
|
6305
6851
|
...load.load ? {} : { incomplete: {
|
|
@@ -6307,11 +6853,11 @@ let MhBrowserService = (() => {
|
|
|
6307
6853
|
pendingRequests: load.pendingRequests
|
|
6308
6854
|
} },
|
|
6309
6855
|
...load.dialog !== void 0 ? { dialog: load.dialog } : {}
|
|
6310
|
-
});
|
|
6856
|
+
}, signal);
|
|
6311
6857
|
return this.attachDownload(live, view, false);
|
|
6312
6858
|
} catch (error) {
|
|
6313
6859
|
if (isDownloadStart(error) || live.pendingDownload !== void 0) {
|
|
6314
|
-
const view = await this.pageFromShown(live, this.shown.get(live.id));
|
|
6860
|
+
const view = await this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
6315
6861
|
if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
|
|
6316
6862
|
this.applyDocumentType(live, view);
|
|
6317
6863
|
return this.attachDownload(live, view, true);
|
|
@@ -6343,7 +6889,7 @@ let MhBrowserService = (() => {
|
|
|
6343
6889
|
return {
|
|
6344
6890
|
kind: "response",
|
|
6345
6891
|
status: response.status(),
|
|
6346
|
-
body: new Uint8Array(await response.body()),
|
|
6892
|
+
body: new Uint8Array(await untilAborted(response.body(), capped(signal, READ_MS))),
|
|
6347
6893
|
...location === void 0 || location === "" ? {} : { location },
|
|
6348
6894
|
...mediaType === void 0 ? {} : { mediaType }
|
|
6349
6895
|
};
|
|
@@ -6449,8 +6995,10 @@ let MhBrowserService = (() => {
|
|
|
6449
6995
|
return started;
|
|
6450
6996
|
}
|
|
6451
6997
|
async start() {
|
|
6452
|
-
const
|
|
6453
|
-
const
|
|
6998
|
+
const choice = this.launchChoice();
|
|
6999
|
+
const epoch = this.configEpoch;
|
|
7000
|
+
const context = await launchBrowser(choice, this.locateEnv());
|
|
7001
|
+
const pid = await browserPid(context, choice.profileDir);
|
|
6454
7002
|
if (pid !== void 0) this.pids.set(context, pid);
|
|
6455
7003
|
if (this.closing) {
|
|
6456
7004
|
await this.closeContext(context);
|
|
@@ -6458,15 +7006,16 @@ let MhBrowserService = (() => {
|
|
|
6458
7006
|
}
|
|
6459
7007
|
this.context = context;
|
|
6460
7008
|
try {
|
|
6461
|
-
await this.windows.attach(context,
|
|
7009
|
+
await this.windows.attach(context, choice.profileDir, { headed: !choice.headless });
|
|
6462
7010
|
} catch (error) {
|
|
6463
7011
|
this.context = void 0;
|
|
6464
7012
|
this.windows.dropPages();
|
|
6465
7013
|
await this.closeContext(context);
|
|
6466
7014
|
throw error;
|
|
6467
7015
|
}
|
|
7016
|
+
if (this.configEpoch === epoch) this.relaunchWanted = false;
|
|
6468
7017
|
context.browser()?.on("disconnected", () => {
|
|
6469
|
-
if (this.closing || this.context !== context) return;
|
|
7018
|
+
if (this.closing || this.replacing || this.context !== context) return;
|
|
6470
7019
|
this.onExit();
|
|
6471
7020
|
});
|
|
6472
7021
|
}
|
|
@@ -6537,22 +7086,81 @@ let MhBrowserService = (() => {
|
|
|
6537
7086
|
}
|
|
6538
7087
|
return urls;
|
|
6539
7088
|
}
|
|
6540
|
-
async snapshotNamed(owner, tab) {
|
|
7089
|
+
async snapshotNamed(owner, tab, signal) {
|
|
6541
7090
|
if (tab === void 0) return void 0;
|
|
6542
7091
|
const live = this.windows.get(owner, tab);
|
|
6543
7092
|
if (live === void 0) return void 0;
|
|
6544
|
-
const shown = await this.refreshSnapshot(live);
|
|
7093
|
+
const shown = await this.refreshSnapshot(live, signal);
|
|
6545
7094
|
this.shown.set(live.id, shown);
|
|
6546
|
-
return this.pageFromShown(live, shown, { surface: "snapshot" });
|
|
6547
|
-
}
|
|
6548
|
-
exclusive(owner, op) {
|
|
6549
|
-
const
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
if (this.queues.
|
|
7095
|
+
return this.pageFromShown(live, shown, { surface: "snapshot" }, signal);
|
|
7096
|
+
}
|
|
7097
|
+
exclusive(owner, op, signal) {
|
|
7098
|
+
const queued = enqueue(this.queues.get(owner) ?? Promise.resolve(), op, signal);
|
|
7099
|
+
this.queues.set(owner, queued.tail);
|
|
7100
|
+
queued.tail.then(() => {
|
|
7101
|
+
if (this.queues.get(owner) === queued.tail) this.queues.delete(owner);
|
|
7102
|
+
if (this.queues.size === 0) this.relaunchIfIdle();
|
|
6554
7103
|
});
|
|
6555
|
-
return run;
|
|
7104
|
+
return queued.run;
|
|
7105
|
+
}
|
|
7106
|
+
launchChoice() {
|
|
7107
|
+
const configured = this.executablePath.get();
|
|
7108
|
+
return {
|
|
7109
|
+
browser: this.browserName.get(),
|
|
7110
|
+
headless: this.headless,
|
|
7111
|
+
profileDir: this.profileDir,
|
|
7112
|
+
...configured !== void 0 && configured !== "" ? { executablePath: configured } : {}
|
|
7113
|
+
};
|
|
7114
|
+
}
|
|
7115
|
+
locateEnv() {
|
|
7116
|
+
return this.locator ?? hostLocateEnv();
|
|
7117
|
+
}
|
|
7118
|
+
idle() {
|
|
7119
|
+
return this.queues.size === 0 && !this.control.busy();
|
|
7120
|
+
}
|
|
7121
|
+
/**
|
|
7122
|
+
* Close and start again when a configuration change is waiting and nothing
|
|
7123
|
+
* is using the browser. A browser that is not running keeps the new values
|
|
7124
|
+
* for the next start. The close is not counted as a crash.
|
|
7125
|
+
*/
|
|
7126
|
+
relaunchIfIdle() {
|
|
7127
|
+
if (!this.relaunchWanted || this.closing || this.replacing) return;
|
|
7128
|
+
if (!this.idle()) return;
|
|
7129
|
+
if (this.context === void 0) {
|
|
7130
|
+
if (this.exited) {
|
|
7131
|
+
this.relaunchWanted = false;
|
|
7132
|
+
return;
|
|
7133
|
+
}
|
|
7134
|
+
this.ready.then(() => this.relaunchIfIdle(), () => {
|
|
7135
|
+
this.relaunchWanted = false;
|
|
7136
|
+
});
|
|
7137
|
+
return;
|
|
7138
|
+
}
|
|
7139
|
+
this.relaunchWanted = false;
|
|
7140
|
+
this.replacing = true;
|
|
7141
|
+
const run = this.replaceBrowser();
|
|
7142
|
+
this.ready = run;
|
|
7143
|
+
const finish = () => {
|
|
7144
|
+
this.replacing = false;
|
|
7145
|
+
if (this.relaunchWanted) this.relaunchIfIdle();
|
|
7146
|
+
};
|
|
7147
|
+
run.then(finish, finish);
|
|
7148
|
+
}
|
|
7149
|
+
async replaceBrowser() {
|
|
7150
|
+
this.windows.suppressPersonInput();
|
|
7151
|
+
const context = this.context;
|
|
7152
|
+
this.context = void 0;
|
|
7153
|
+
this.windows.dropPages();
|
|
7154
|
+
if (context !== void 0) await this.closeContext(context);
|
|
7155
|
+
if (this.closing) return;
|
|
7156
|
+
this.exited = false;
|
|
7157
|
+
try {
|
|
7158
|
+
await this.launch();
|
|
7159
|
+
this.windows.resumePersonInput();
|
|
7160
|
+
} catch (error) {
|
|
7161
|
+
this.exited = true;
|
|
7162
|
+
throw error;
|
|
7163
|
+
}
|
|
6556
7164
|
}
|
|
6557
7165
|
};
|
|
6558
7166
|
})();
|
|
@@ -6600,24 +7208,24 @@ function fragmentOf(url) {
|
|
|
6600
7208
|
return;
|
|
6601
7209
|
}
|
|
6602
7210
|
}
|
|
6603
|
-
async function mainDocument(response) {
|
|
7211
|
+
async function mainDocument(response, signal) {
|
|
6604
7212
|
const headers = response.headers();
|
|
6605
7213
|
const mediaType = headers["content-type"];
|
|
6606
7214
|
const link = headers.link;
|
|
6607
7215
|
return {
|
|
6608
7216
|
status: response.status(),
|
|
6609
7217
|
finalUrl: response.url(),
|
|
6610
|
-
body: await response.text(),
|
|
7218
|
+
body: await untilAborted(response.text(), capped(signal, READ_MS)),
|
|
6611
7219
|
...mediaType !== void 0 ? { mediaType } : {},
|
|
6612
7220
|
...link !== void 0 ? { link } : {}
|
|
6613
7221
|
};
|
|
6614
7222
|
}
|
|
6615
|
-
async function readDomLinks(page) {
|
|
6616
|
-
return (await page.locator("link").evaluateAll((elements) => elements.map((element) => ({
|
|
7223
|
+
async function readDomLinks(page, signal) {
|
|
7224
|
+
return (await untilAborted(page.locator("link").evaluateAll((elements) => elements.map((element) => ({
|
|
6617
7225
|
rel: element.getAttribute("rel") ?? "",
|
|
6618
7226
|
href: element.getAttribute("href") ?? "",
|
|
6619
7227
|
title: element.getAttribute("title") ?? ""
|
|
6620
|
-
}))).catch(() => [])).filter((row) => row.rel !== "" && row.href !== "").map((row) => ({
|
|
7228
|
+
}))).catch(() => []), capped(signal, READ_MS))).filter((row) => row.rel !== "" && row.href !== "").map((row) => ({
|
|
6621
7229
|
rel: row.rel,
|
|
6622
7230
|
href: row.href,
|
|
6623
7231
|
...row.title !== "" ? { title: row.title } : {}
|
|
@@ -6890,9 +7498,10 @@ function browserHandoverTool(ctx, reader, headed = true) {
|
|
|
6890
7498
|
if (exec.signal.aborted) return quiet();
|
|
6891
7499
|
const session = sessionFor(ctx, exec);
|
|
6892
7500
|
try {
|
|
7501
|
+
const tab = givenString(args.tab);
|
|
6893
7502
|
await ctx.mhBrowser.handover(session.id, {
|
|
6894
7503
|
reason: args.reason,
|
|
6895
|
-
...
|
|
7504
|
+
...tab === void 0 ? {} : { tab }
|
|
6896
7505
|
}, exec.signal);
|
|
6897
7506
|
const answer = await askUntilAnswered(ctx, args.reason, exec, headed);
|
|
6898
7507
|
if (answer === ABORT || exec.signal.aborted) return quiet();
|
|
@@ -7036,6 +7645,7 @@ function browserInvokeTool(ctx, reader) {
|
|
|
7036
7645
|
if (resolved === void 0) throw new BrowserToolError("NOT_DELIVERED", `${NOT_DELIVERED_REASON}: ${args.page}`);
|
|
7037
7646
|
if ("ambiguous" in resolved) throw new BrowserToolError("INVALID_ARGS", `${args.page} is the id of more than one page shown to you; give the url of one: ${resolved.ambiguous.join(", ")}`);
|
|
7038
7647
|
const input = args.input === void 0 ? void 0 : asObject(args.input);
|
|
7648
|
+
const item = givenString(args.item);
|
|
7039
7649
|
try {
|
|
7040
7650
|
const sent = await ctx.mhBrowser.invoke(session.id, resolved.url, {
|
|
7041
7651
|
name: args.name,
|
|
@@ -7043,7 +7653,7 @@ function browserInvokeTool(ctx, reader) {
|
|
|
7043
7653
|
url: resolved.url,
|
|
7044
7654
|
text: resolved.text
|
|
7045
7655
|
},
|
|
7046
|
-
...
|
|
7656
|
+
...item === void 0 ? {} : { item },
|
|
7047
7657
|
...input === void 0 ? {} : { input }
|
|
7048
7658
|
}, {
|
|
7049
7659
|
signal: exec.signal,
|
|
@@ -7168,18 +7778,19 @@ function browserOpenTool(ctx, reader) {
|
|
|
7168
7778
|
}),
|
|
7169
7779
|
async execute(args, exec) {
|
|
7170
7780
|
if (args.pages.length < 1 || args.pages.length > 8) throw new BrowserToolError("INVALID_ARGS", "pages takes 1 to 8 entries");
|
|
7171
|
-
|
|
7172
|
-
const waitFor = page.waitFor;
|
|
7781
|
+
const items = args.pages.map((page) => {
|
|
7782
|
+
const waitFor = page.waitFor === void 0 ? void 0 : compactWait(page.waitFor);
|
|
7173
7783
|
if (waitFor !== void 0 && waitFor.text === void 0 && waitFor.textGone === void 0 && waitFor.time === void 0) throw new BrowserToolError("INVALID_ARGS", "waitFor needs text, textGone, or time");
|
|
7174
|
-
|
|
7784
|
+
return {
|
|
7785
|
+
url: page.url,
|
|
7786
|
+
...page.snapshot === true ? { snapshot: true } : {},
|
|
7787
|
+
...waitFor === void 0 ? {} : { waitFor }
|
|
7788
|
+
};
|
|
7789
|
+
});
|
|
7175
7790
|
const session = sessionFor(ctx, exec);
|
|
7176
7791
|
ctx.mhBrowser.noteWorkspace(session.id, writeTarget(ctx, session));
|
|
7177
7792
|
try {
|
|
7178
|
-
const pages = await ctx.mhBrowser.open(session.id,
|
|
7179
|
-
url: page.url,
|
|
7180
|
-
...page.snapshot === true ? { snapshot: true } : {},
|
|
7181
|
-
...page.waitFor === void 0 ? {} : { waitFor: compactWait(page.waitFor) }
|
|
7182
|
-
})), exec.signal);
|
|
7793
|
+
const pages = await ctx.mhBrowser.open(session.id, items, exec.signal);
|
|
7183
7794
|
const block = tabBlock(ctx, reader, session);
|
|
7184
7795
|
return {
|
|
7185
7796
|
text: `${renderOutcome(pages, void 0, true)}${block.suffix}`,
|
|
@@ -7196,10 +7807,13 @@ function browserOpenTool(ctx, reader) {
|
|
|
7196
7807
|
});
|
|
7197
7808
|
}
|
|
7198
7809
|
function compactWait(waitFor) {
|
|
7810
|
+
const text = givenString(waitFor.text);
|
|
7811
|
+
const textGone = givenString(waitFor.textGone);
|
|
7812
|
+
const time = givenTime(waitFor.time);
|
|
7199
7813
|
return {
|
|
7200
|
-
...
|
|
7201
|
-
...
|
|
7202
|
-
...
|
|
7814
|
+
...text === void 0 ? {} : { text },
|
|
7815
|
+
...textGone === void 0 ? {} : { textGone },
|
|
7816
|
+
...time === void 0 ? {} : { time }
|
|
7203
7817
|
};
|
|
7204
7818
|
}
|
|
7205
7819
|
function titleOf(name, url) {
|
|
@@ -7313,6 +7927,7 @@ const inject = [
|
|
|
7313
7927
|
];
|
|
7314
7928
|
const Config = browserConfig;
|
|
7315
7929
|
async function apply(ctx, config) {
|
|
7930
|
+
const resolved = browserSettings(config);
|
|
7316
7931
|
ctx.plugin({
|
|
7317
7932
|
name: "mh-browser-slot",
|
|
7318
7933
|
inject: ["browserUse", "mhBrowser"],
|
|
@@ -7320,16 +7935,29 @@ async function apply(ctx, config) {
|
|
|
7320
7935
|
inner.browserUse.register("mh-browser");
|
|
7321
7936
|
}
|
|
7322
7937
|
});
|
|
7323
|
-
await ctx.plugin(MhBrowserService,
|
|
7938
|
+
await ctx.plugin(MhBrowserService, resolved);
|
|
7324
7939
|
await ctx.plugin({
|
|
7325
7940
|
name: "mh-browser-runtime",
|
|
7326
7941
|
inject: ["mhBrowser", ...inject],
|
|
7327
7942
|
async apply(inner) {
|
|
7328
7943
|
const reader = await registerProjections(inner);
|
|
7329
|
-
registerBrowserTools(inner, reader, !
|
|
7944
|
+
registerBrowserTools(inner, reader, !resolved.headless);
|
|
7330
7945
|
registerPresence(inner, reader);
|
|
7331
7946
|
}
|
|
7332
7947
|
});
|
|
7333
7948
|
}
|
|
7949
|
+
/**
|
|
7950
|
+
* The loader passes a config it has already resolved, including the volatile
|
|
7951
|
+
* fields. A direct caller passes the raw values, which are resolved here.
|
|
7952
|
+
* Resolving twice rejects a volatile value as if it were the wrong type.
|
|
7953
|
+
*/
|
|
7954
|
+
function browserSettings(config) {
|
|
7955
|
+
if (isResolved(config)) return config;
|
|
7956
|
+
return browserConfig(config);
|
|
7957
|
+
}
|
|
7958
|
+
function isResolved(config) {
|
|
7959
|
+
const browser = config.browser;
|
|
7960
|
+
return typeof browser === "object" && browser !== null && "get" in browser;
|
|
7961
|
+
}
|
|
7334
7962
|
//#endregion
|
|
7335
7963
|
export { Config, MhBrowserError, MhBrowserService, UNKNOWN_TAB, UNKNOWN_TAB_REASON, apply, inject, name, readAgentView, unknownTab };
|