aboutsummaryrefslogtreecommitdiff
path: root/src/profile/bind.ts
blob: 95cffaa90fd27e2a23813e14b3f04cd4a7ec776e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import type { BrowserClient } from "../client";
import type { ResultOf } from "../cdp/commands";
import { type CdpError, cdpError } from "../cdp/errors";
import { type Result, err, ok } from "../util/result";
import { detectRunningBrowser, resolveBrowserExecutable } from "./browser-process";
import { openProfileWindow } from "./launch";
import type { ProfilePin } from "./store";

const SEED_DEADLINE_MS = 15_000;
const SEED_POLL_MS = 500;

type PageTarget = ResultOf<"Target.getTargets">["targetInfos"][number];

const pageTargets = async (client: BrowserClient): Promise<ReadonlyArray<PageTarget>> => {
  const r = await client.session().callBrowser("Target.getTargets");
  if (!r.success) return [];
  return r.data.targetInfos.filter((t) => t.type === "page");
};

const manualFallbackHint = (pin: ProfilePin): string =>
  `open a window for "${pin.label}" yourself (browser profile menu → ${pin.profileDir}), then retry. ` +
  `Run /browser-profile to pick a different profile, or clear the selection to use whichever window is focused.`;

export const seedProfileWindow = async (
  client: BrowserClient,
  pin: ProfilePin,
): Promise<Result<string, CdpError>> => {
  const connectedDir = client.userDataDir();
  if (connectedDir && connectedDir !== pin.userDataDir) {
    return err(cdpError(
      "discovery_failed",
      `the selected profile "${pin.label}" belongs to ${pin.userDataDir}, but the harness is connected to ${connectedDir}. ` +
      `Run /browser-profile to choose a profile in the browser you're using.`,
    ));
  }

  const targetUserDataDir = connectedDir ?? pin.userDataDir;
  const running = await detectRunningBrowser(targetUserDataDir);
  const exePath = await resolveBrowserExecutable(running);
  if (!exePath) {
    return err(cdpError(
      "discovery_failed",
      `could not locate the browser executable needed to open "${pin.label}" — ${manualFallbackHint(pin)}`,
    ));
  }

  const launched = await openProfileWindow({
    exePath,
    profileDir: pin.profileDir,
    // ProcessSingleton keys on the user-data-dir path, so forward only the value the running browser reports: a reconstructed one may differ by case and start a SECOND browser.
    ...(running.explicitUserDataDir ? { explicitUserDataDir: running.explicitUserDataDir } : {}),
  });
  if (!launched.success) {
    return err(cdpError("discovery_failed", `${launched.error} — ${manualFallbackHint(pin)}`));
  }

  const { sentinelUrl, cleanup, kill } = launched.data;
  const deadline = Date.now() + SEED_DEADLINE_MS;
  let seed: PageTarget | undefined;
  while (Date.now() < deadline && !seed) {
    await new Promise((r) => setTimeout(r, SEED_POLL_MS));
    seed = (await pageTargets(client)).find((t) => t.url === sentinelUrl);
  }

  if (!seed) {
    kill();
    await cleanup();
    return err(cdpError(
      "timeout",
      `couldn't open a window in "${pin.label}" automatically — ${manualFallbackHint(pin)}`,
    ));
  }

  // The sentinel's context is the profile's context — the only way to learn it.
  if (seed.browserContextId) client.setProfileContextId(seed.browserContextId);

  const ownership = client.ownership();
  ownership.setHarnessWindow(seed.targetId);
  ownership.add(seed.targetId);
  const win = await client.session().windowId(seed.targetId);
  if (win.success) ownership.setHarnessWindowId(win.data);

  const attached = await client.session().attach(seed.targetId);
  if (attached.success) {
    await client.session().callOnTarget("Page.navigate", { url: "about:blank" }, attached.data);
  }
  await cleanup();

  return ok(seed.targetId);
};