|
| 1 | +// browserDesktopHelper.mjs - which desktop a launched browser's windows live on, and the Windows helper that starts a |
| 2 | +// browser on a desktop of its own. Windows gives each desktop its own foreground window, so no window of a browser on a |
| 3 | +// private desktop can take the foreground or the keyboard of the desktop the user works on (a headed browser started |
| 4 | +// normally activates its window even while the user types elsewhere). The browser is still headed there: it renders, |
| 5 | +// lays out, and answers input and screenshots as it would on screen, with the identity of a headed browser. The helper |
| 6 | +// comes with the win_amd64 platform wheel, pinned by SHA-256 in the wheel's host descriptor; without the wheel (a source |
| 7 | +// checkout, the npm package) PYPROC_BROWSER_DESKTOP_HELPER names one built with cargo from nativeHelper. |
| 8 | +import { createHash } from "node:crypto"; |
| 9 | +import { existsSync, readFileSync } from "node:fs"; |
| 10 | +import { dirname, isAbsolute, join, resolve } from "node:path"; |
| 11 | +import { fileURLToPath } from "node:url"; |
| 12 | + |
| 13 | +const HERE = dirname(fileURLToPath(import.meta.url)); |
| 14 | +const PACKAGE_ROOT = resolve(HERE, "..", "..", ".."); |
| 15 | +export const BROWSER_DESKTOP_SOURCE_ROOT = join(HERE, "nativeHelper"); |
| 16 | +export const BROWSER_DESKTOP_HELPER_FILE = "pyproc-browser-desktop.exe"; |
| 17 | +// `user`: the desktop the user works on (the default). `private`: a desktop created for this one browser. |
| 18 | +export const BROWSER_DESKTOPS = Object.freeze(["user", "private"]); |
| 19 | +// The helper's own failure (it never ran the browser), distinct from any browser exit code. |
| 20 | +export const BROWSER_DESKTOP_HELPER_FAILED = 0x70d; |
| 21 | + |
| 22 | +let helperPath = null; |
| 23 | + |
| 24 | +function unavailable(message) { |
| 25 | + return Object.assign(new Error(message), { code: "BROWSER_DESKTOP_UNAVAILABLE" }); |
| 26 | +} |
| 27 | + |
| 28 | +/** The desktop a launch asks for (`opts.desktop`, else PYPROC_BROWSER_DESKTOP, else `user`), checked for this host. */ |
| 29 | +export function browserDesktopOf({ desktop, headed } = {}, env = process.env) { |
| 30 | + const value = desktop ?? (env.PYPROC_BROWSER_DESKTOP || "user"); |
| 31 | + if (!BROWSER_DESKTOPS.includes(value)) { |
| 32 | + throw new TypeError(`browser desktop must be one of ${BROWSER_DESKTOPS.join(", ")}`); |
| 33 | + } |
| 34 | + if (value === "private") { |
| 35 | + if (process.platform !== "win32") throw new TypeError("a private browser desktop is Windows-only"); |
| 36 | + if (!(headed === true || env.PYPROC_HEADED === "1")) { |
| 37 | + throw new TypeError("a private browser desktop is for a headed browser; a headless one has no windows"); |
| 38 | + } |
| 39 | + } |
| 40 | + return value; |
| 41 | +} |
| 42 | + |
| 43 | +function wheelHelper() { |
| 44 | + const descriptorPath = join(PACKAGE_ROOT, "..", "host.json"); |
| 45 | + if (!existsSync(descriptorPath)) return null; |
| 46 | + const descriptor = JSON.parse(readFileSync(descriptorPath, "utf8")); |
| 47 | + const { version } = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")); |
| 48 | + const entry = descriptor?.browserDesktop; |
| 49 | + if (descriptor?.package?.name !== "pyproc" || descriptor.package.version !== version || !entry) return null; |
| 50 | + const path = join(dirname(descriptorPath), entry.path); |
| 51 | + const digest = createHash("sha256").update(readFileSync(path)).digest("hex"); |
| 52 | + if (digest !== entry.sha256) throw unavailable(`the platform wheel's browser desktop helper ${path} does not match its SHA-256`); |
| 53 | + return path; |
| 54 | +} |
| 55 | + |
| 56 | +/** The helper to start a browser on a private desktop: the platform wheel's, else the one PYPROC_BROWSER_DESKTOP_HELPER |
| 57 | + * names. */ |
| 58 | +export function browserDesktopHelper(env = process.env) { |
| 59 | + if (helperPath) return helperPath; |
| 60 | + const named = env.PYPROC_BROWSER_DESKTOP_HELPER || ""; |
| 61 | + if (named && (!isAbsolute(named) || !existsSync(named))) { |
| 62 | + throw unavailable(`PYPROC_BROWSER_DESKTOP_HELPER must name an existing absolute file: ${named}`); |
| 63 | + } |
| 64 | + const found = wheelHelper() || named; |
| 65 | + if (!found) { |
| 66 | + throw unavailable("a private browser desktop needs pyproc's browser desktop helper: install the pyproc-control " |
| 67 | + + "win_amd64 platform wheel, or build it (cargo build --release --manifest-path " |
| 68 | + + `${join(BROWSER_DESKTOP_SOURCE_ROOT, "Cargo.toml")} --target-dir <outside the checkout>) and set ` |
| 69 | + + "PYPROC_BROWSER_DESKTOP_HELPER to the built pyproc-browser-desktop.exe"); |
| 70 | + } |
| 71 | + helperPath = found; |
| 72 | + return found; |
| 73 | +} |
0 commit comments