aboutsummaryrefslogtreecommitdiff
path: root/src/profile/launch.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/profile/launch.ts')
-rw-r--r--src/profile/launch.ts44
1 files changed, 4 insertions, 40 deletions
diff --git a/src/profile/launch.ts b/src/profile/launch.ts
index 0088b6d..a54906d 100644
--- a/src/profile/launch.ts
+++ b/src/profile/launch.ts
@@ -1,19 +1,5 @@
-/**
- * Opening a browser window for a chosen profile.
- *
- * CDP cannot do this: `Target.createTarget` rejects another profile's
- * browserContextId ("Failed to find browser context with id"), and a bare
- * createTarget lands in `defaultBrowserContextId`, which follows window focus.
- * The only way in is the browser's own command line — Chromium's
- * ProcessSingleton hands a second launch's argv to the running browser, which
- * "does what it would have done", i.e. opens a window in `--profile-directory`.
- *
- * The window is then identified by a unique `file://` sentinel page. That
- * matters: a bare `about:blank#token` argument is discarded by Chrome (it opens
- * chrome://newtab instead), so the token has to ride on a real URL. Polling for
- * the sentinel URL — rather than diffing the target list — also keeps
- * identification correct when the user opens tabs at the same moment.
- */
+// CDP cannot open a window in another profile, so this goes through the command line: ProcessSingleton hands the argv to the running browser.
+// The sentinel must ride on a real `file://` URL — Chrome discards a bare `about:blank#token` argument and opens chrome://newtab instead.
import { spawn, type ChildProcess } from "node:child_process";
import { randomUUID } from "node:crypto";
@@ -24,23 +10,13 @@ import { pathToFileURL } from "node:url";
import { type Result, err, ok } from "../util/result";
export type ProfileWindowRequest = {
- /** Browser binary to invoke. */
readonly exePath: string;
- /** Profile subdirectory, e.g. "Profile 1". */
readonly profileDir: string;
- /**
- * Only set when the RUNNING browser was launched with an explicit
- * --user-data-dir. ProcessSingleton keys on this path, so passing one that
- * differs by case or normalisation would start a second browser instead of
- * delegating to the running one.
- */
readonly explicitUserDataDir?: string;
};
export type ProfileWindowHandle = {
- /** file:// URL that will appear as the new window's page target. */
readonly sentinelUrl: string;
- /** Remove the sentinel file. Safe to call more than once. */
readonly cleanup: () => Promise<void>;
readonly kill: () => void;
};
@@ -49,10 +25,6 @@ const SENTINEL_HTML = (token: string): string =>
`<!doctype html><meta charset="utf-8"><title>${token}</title>` +
`<body style="font:14px system-ui;padding:2rem;color:#555">pi browser harness — opening this profile…</body>`;
-/**
- * Ask the running browser to open a window in `profileDir`, marked with a
- * unique sentinel page the caller can then find over CDP.
- */
export const openProfileWindow = async (
req: ProfileWindowRequest,
): Promise<Result<ProfileWindowHandle, string>> => {
@@ -71,26 +43,18 @@ export const openProfileWindow = async (
let child: ChildProcess;
try {
- // No shell: arguments are passed as an array, so profile names and paths
- // containing spaces survive intact on every platform.
child = spawn(req.exePath, args, { detached: true, stdio: "ignore" });
} catch (e) {
await rm(file, { force: true }).catch(() => {});
return err(`could not launch ${req.exePath}: ${e instanceof Error ? e.message : String(e)}`);
}
- // spawn() reports a missing or non-executable binary ASYNCHRONOUSLY, as an
- // 'error' event — it does not throw. Ignoring that event made an ENOENT
- // indistinguishable from a browser that simply never opened the window, so
- // the caller's 15s sentinel poll reported a timeout for what was really a bad
- // executable path. Node guarantees exactly one of 'spawn' or 'error', so
- // waiting for it costs nothing and turns the failure into a named cause.
+ // spawn() reports a bad binary asynchronously as an 'error' event rather than throwing; unhandled, an ENOENT looked like the 15s sentinel timeout.
const spawnFailure = await new Promise<string | undefined>((resolve) => {
child.once("spawn", () => resolve(undefined));
child.once("error", (e: Error) => resolve(e.message));
});
- // A later 'error' event with no listener would throw; keep one attached for
- // the lifetime of the detached launcher.
+ // A later 'error' event with no listener would throw, so keep one attached for the detached launcher's lifetime.
child.on("error", () => {});
child.unref();