@mind-harness/mh-browser 0.2.5 → 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 +449 -49
- 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/browser.d.ts +11 -5
- package/lib/types/service/control.d.ts +5 -0
- 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/settings-form.d.ts +142 -0
- package/lib/types/settings.d.ts +121 -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
|
});
|
|
@@ -3069,16 +3072,267 @@ function sameOrigin$1(left, right) {
|
|
|
3069
3072
|
}
|
|
3070
3073
|
}
|
|
3071
3074
|
//#endregion
|
|
3072
|
-
//#region lib/types/service/
|
|
3073
|
-
const
|
|
3074
|
-
chrome:
|
|
3075
|
-
|
|
3076
|
-
|
|
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"
|
|
3093
|
+
};
|
|
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"
|
|
3077
3115
|
};
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
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;
|
|
3141
|
+
}
|
|
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
|
+
});
|
|
3081
3333
|
}
|
|
3334
|
+
//#endregion
|
|
3335
|
+
//#region lib/types/service/browser.js
|
|
3082
3336
|
/**
|
|
3083
3337
|
* Playwright appends `--no-sandbox` unless this is true, and branded Chrome
|
|
3084
3338
|
* then warns that stability and security will suffer. An installed browser
|
|
@@ -3089,34 +3343,45 @@ function chromiumSandboxEnabled(browser, platform = process.platform) {
|
|
|
3089
3343
|
return platform !== "linux" || browser !== "chromium";
|
|
3090
3344
|
}
|
|
3091
3345
|
/**
|
|
3092
|
-
* One persistent context for the whole process.
|
|
3093
|
-
*
|
|
3094
|
-
*
|
|
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.
|
|
3095
3351
|
*/
|
|
3096
|
-
async function launchBrowser(options) {
|
|
3352
|
+
async function launchBrowser(options, env = hostLocateEnv()) {
|
|
3097
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;
|
|
3098
3376
|
try {
|
|
3099
|
-
|
|
3100
|
-
headless: options.headless,
|
|
3101
|
-
acceptDownloads: true,
|
|
3102
|
-
chromiumSandbox: chromiumSandboxEnabled(options.browser),
|
|
3103
|
-
...options.headless ? {} : {
|
|
3104
|
-
viewport: null,
|
|
3105
|
-
ignoreDefaultArgs: ["--disable-extensions"],
|
|
3106
|
-
args: [
|
|
3107
|
-
"--enable-unsafe-extension-debugging",
|
|
3108
|
-
"--keep-alive-for-test",
|
|
3109
|
-
"--disable-blink-features=AutomationControlled",
|
|
3110
|
-
"--test-type"
|
|
3111
|
-
]
|
|
3112
|
-
},
|
|
3113
|
-
...options.browser === "chromium" ? {} : { channel: options.browser },
|
|
3114
|
-
...options.executablePath !== void 0 ? { executablePath: options.executablePath } : {}
|
|
3115
|
-
});
|
|
3377
|
+
path = chromium.executablePath();
|
|
3116
3378
|
} catch (error) {
|
|
3117
|
-
|
|
3118
|
-
|
|
3379
|
+
return { missing: [{
|
|
3380
|
+
path: "chromium",
|
|
3381
|
+
reason: error instanceof Error ? error.message : "does not exist"
|
|
3382
|
+
}] };
|
|
3119
3383
|
}
|
|
3384
|
+
return locateBundled(path, access);
|
|
3120
3385
|
}
|
|
3121
3386
|
/**
|
|
3122
3387
|
* Process id of the browser's main process. Playwright gives none for a
|
|
@@ -3179,6 +3444,15 @@ var Control = class {
|
|
|
3179
3444
|
tabs = /* @__PURE__ */ new Map();
|
|
3180
3445
|
since = /* @__PURE__ */ new Map();
|
|
3181
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
|
+
}
|
|
3182
3456
|
/** Whether the person holds any tab, or a handover is waiting. */
|
|
3183
3457
|
of(owner) {
|
|
3184
3458
|
return this.who.get(owner) ?? "model";
|
|
@@ -5318,11 +5592,24 @@ const NO_WORKSPACE_REASON = "this session has no working directory";
|
|
|
5318
5592
|
let MhBrowserService = (() => {
|
|
5319
5593
|
let _classSuper = TypertRemoteService;
|
|
5320
5594
|
let _instanceExtraInitializers = [];
|
|
5595
|
+
let _locateFor_decorators;
|
|
5321
5596
|
let _revealHandover_decorators;
|
|
5322
5597
|
return class MhBrowserService extends _classSuper {
|
|
5323
5598
|
static {
|
|
5324
5599
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
5600
|
+
_locateFor_decorators = [Remote];
|
|
5325
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);
|
|
5326
5613
|
__esDecorate(this, null, _revealHandover_decorators, {
|
|
5327
5614
|
kind: "method",
|
|
5328
5615
|
name: "revealHandover",
|
|
@@ -5341,13 +5628,23 @@ let MhBrowserService = (() => {
|
|
|
5341
5628
|
value: _metadata
|
|
5342
5629
|
});
|
|
5343
5630
|
}
|
|
5344
|
-
static Config = browserConfig;
|
|
5345
5631
|
windows = (__runInitializers(this, _instanceExtraInitializers), new Windows());
|
|
5346
|
-
|
|
5632
|
+
headless;
|
|
5633
|
+
profileDir;
|
|
5634
|
+
browserName;
|
|
5635
|
+
executablePath;
|
|
5636
|
+
/** Tests replace the machine lookup. Unset uses this machine. */
|
|
5637
|
+
locator;
|
|
5347
5638
|
context;
|
|
5348
5639
|
pids = /* @__PURE__ */ new WeakMap();
|
|
5349
5640
|
ready = Promise.resolve();
|
|
5350
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;
|
|
5351
5648
|
/** The browser is not running. The next call that needs a page starts it. */
|
|
5352
5649
|
exited = true;
|
|
5353
5650
|
exits = [];
|
|
@@ -5366,11 +5663,16 @@ let MhBrowserService = (() => {
|
|
|
5366
5663
|
constructor(ctx, config) {
|
|
5367
5664
|
super(ctx, "mhBrowser");
|
|
5368
5665
|
this.maxBytes = config.presence.maxBytes;
|
|
5369
|
-
this.
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
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
|
+
});
|
|
5374
5676
|
this.windows.setPersonInput((owner, tab) => {
|
|
5375
5677
|
const live = tab === void 0 ? void 0 : this.windows.get(owner, tab);
|
|
5376
5678
|
this.control.note(owner, tab);
|
|
@@ -5589,9 +5891,26 @@ let MhBrowserService = (() => {
|
|
|
5589
5891
|
page: (owner, tab) => this.windows.get(owner, tab)?.page,
|
|
5590
5892
|
running: () => this.context !== void 0,
|
|
5591
5893
|
pages: () => this.context?.pages() ?? [],
|
|
5592
|
-
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
|
+
}
|
|
5593
5898
|
};
|
|
5594
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
|
+
}
|
|
5595
5914
|
async act(owner, tab, op, signal) {
|
|
5596
5915
|
return this.exclusive(owner, async () => {
|
|
5597
5916
|
await this.ready;
|
|
@@ -5777,18 +6096,22 @@ let MhBrowserService = (() => {
|
|
|
5777
6096
|
if (!this.control.releaseIfHeld(owner)) return;
|
|
5778
6097
|
this.windows.releaseHandover(owner);
|
|
5779
6098
|
this.windows.markWaiting(owner, false);
|
|
6099
|
+
this.relaunchIfIdle();
|
|
5780
6100
|
}
|
|
5781
6101
|
/**
|
|
5782
6102
|
* The person took over between turns. The next message they send is the
|
|
5783
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.
|
|
5784
6105
|
*/
|
|
5785
6106
|
takePersonHandback(owner) {
|
|
5786
6107
|
const taken = this.control.takePerson(owner);
|
|
5787
6108
|
if (taken === void 0) return void 0;
|
|
6109
|
+
const urls = this.presenceUrls(owner, taken.tabs);
|
|
5788
6110
|
this.windows.releaseHandover(owner);
|
|
6111
|
+
this.relaunchIfIdle();
|
|
5789
6112
|
return {
|
|
5790
6113
|
...taken,
|
|
5791
|
-
urls
|
|
6114
|
+
urls
|
|
5792
6115
|
};
|
|
5793
6116
|
}
|
|
5794
6117
|
/**
|
|
@@ -6672,8 +6995,10 @@ let MhBrowserService = (() => {
|
|
|
6672
6995
|
return started;
|
|
6673
6996
|
}
|
|
6674
6997
|
async start() {
|
|
6675
|
-
const
|
|
6676
|
-
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);
|
|
6677
7002
|
if (pid !== void 0) this.pids.set(context, pid);
|
|
6678
7003
|
if (this.closing) {
|
|
6679
7004
|
await this.closeContext(context);
|
|
@@ -6681,15 +7006,16 @@ let MhBrowserService = (() => {
|
|
|
6681
7006
|
}
|
|
6682
7007
|
this.context = context;
|
|
6683
7008
|
try {
|
|
6684
|
-
await this.windows.attach(context,
|
|
7009
|
+
await this.windows.attach(context, choice.profileDir, { headed: !choice.headless });
|
|
6685
7010
|
} catch (error) {
|
|
6686
7011
|
this.context = void 0;
|
|
6687
7012
|
this.windows.dropPages();
|
|
6688
7013
|
await this.closeContext(context);
|
|
6689
7014
|
throw error;
|
|
6690
7015
|
}
|
|
7016
|
+
if (this.configEpoch === epoch) this.relaunchWanted = false;
|
|
6691
7017
|
context.browser()?.on("disconnected", () => {
|
|
6692
|
-
if (this.closing || this.context !== context) return;
|
|
7018
|
+
if (this.closing || this.replacing || this.context !== context) return;
|
|
6693
7019
|
this.onExit();
|
|
6694
7020
|
});
|
|
6695
7021
|
}
|
|
@@ -6773,9 +7099,69 @@ let MhBrowserService = (() => {
|
|
|
6773
7099
|
this.queues.set(owner, queued.tail);
|
|
6774
7100
|
queued.tail.then(() => {
|
|
6775
7101
|
if (this.queues.get(owner) === queued.tail) this.queues.delete(owner);
|
|
7102
|
+
if (this.queues.size === 0) this.relaunchIfIdle();
|
|
6776
7103
|
});
|
|
6777
7104
|
return queued.run;
|
|
6778
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
|
+
}
|
|
7164
|
+
}
|
|
6779
7165
|
};
|
|
6780
7166
|
})();
|
|
6781
7167
|
const MAX_REDIRECTS = 10;
|
|
@@ -7541,6 +7927,7 @@ const inject = [
|
|
|
7541
7927
|
];
|
|
7542
7928
|
const Config = browserConfig;
|
|
7543
7929
|
async function apply(ctx, config) {
|
|
7930
|
+
const resolved = browserSettings(config);
|
|
7544
7931
|
ctx.plugin({
|
|
7545
7932
|
name: "mh-browser-slot",
|
|
7546
7933
|
inject: ["browserUse", "mhBrowser"],
|
|
@@ -7548,16 +7935,29 @@ async function apply(ctx, config) {
|
|
|
7548
7935
|
inner.browserUse.register("mh-browser");
|
|
7549
7936
|
}
|
|
7550
7937
|
});
|
|
7551
|
-
await ctx.plugin(MhBrowserService,
|
|
7938
|
+
await ctx.plugin(MhBrowserService, resolved);
|
|
7552
7939
|
await ctx.plugin({
|
|
7553
7940
|
name: "mh-browser-runtime",
|
|
7554
7941
|
inject: ["mhBrowser", ...inject],
|
|
7555
7942
|
async apply(inner) {
|
|
7556
7943
|
const reader = await registerProjections(inner);
|
|
7557
|
-
registerBrowserTools(inner, reader, !
|
|
7944
|
+
registerBrowserTools(inner, reader, !resolved.headless);
|
|
7558
7945
|
registerPresence(inner, reader);
|
|
7559
7946
|
}
|
|
7560
7947
|
});
|
|
7561
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
|
+
}
|
|
7562
7962
|
//#endregion
|
|
7563
7963
|
export { Config, MhBrowserError, MhBrowserService, UNKNOWN_TAB, UNKNOWN_TAB_REASON, apply, inject, name, readAgentView, unknownTab };
|