all repos — sz-chrome-remote-cdp @ master

pi-browser-harness, patched to drive a Chrome on another host over CDP, with castle_cdp_search web search

src/cdp/target-factory.ts (view raw)

 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
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
// `window.open` with userGesture is the only CDP-reachable way into a non-default browser context: createTarget rejects a foreign browserContextId and `openerId` does not inherit the opener's context.

import { randomUUID } from "node:crypto";
import type { BrowserClient } from "../client";
import { type Result, err, ok } from "../util/result";
import { type CdpError, cdpError } from "./errors";

const SPAWN_DEADLINE_MS = 8_000;
const SPAWN_POLL_MS = 100;

export type HarnessWindow = {
  readonly targetId: string;
  readonly freshlyCreated: boolean;
};

type PageTarget = {
  readonly targetId: string;
  readonly type: string;
  readonly url: string;
  readonly browserContextId?: string;
};

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

const sessionForTarget = async (client: BrowserClient, targetId: string): Promise<Result<string, CdpError>> => {
  const current = client.current();
  if (current?.targetId === targetId) return ok(current.sessionId);
  return client.session().attach(targetId);
};

const spawnTabViaOpener = async (
  client: BrowserClient,
  openerTargetId: string,
): Promise<Result<string, CdpError>> => {
  const session = await sessionForTarget(client, openerTargetId);
  if (!session.success) return session;

  const token = `pi-${randomUUID()}`;
  // userGesture: true is what gets this past the popup blocker.
  const opened = await client.session().callOnTarget(
    "Runtime.evaluate",
    {
      expression: `!!window.open('about:blank#${token}', '_blank')`,
      returnByValue: true,
      userGesture: true,
    },
    session.data,
  );
  if (!opened.success) return opened;
  if (opened.data.result.value !== true) {
    return err(cdpError(
      "invalid_response",
      "the browser blocked window.open in the pinned profile's window — the page may have navigated away; retry, or run /browser-profile to re-seed",
      "Runtime.evaluate",
    ));
  }

  const deadline = Date.now() + SPAWN_DEADLINE_MS;
  while (Date.now() < deadline) {
    const targets = await listPageTargets(client);
    if (targets.success) {
      const spawned = targets.data.find((t) => t.url.includes(token));
      if (spawned) return ok(spawned.targetId);
    }
    await new Promise((r) => setTimeout(r, SPAWN_POLL_MS));
  }
  return err(cdpError("timeout", `the tab opened in the pinned profile did not report back within ${SPAWN_DEADLINE_MS}ms`));
};

const inFlight = new WeakMap<BrowserClient, Promise<Result<HarnessWindow, CdpError>>>();

export const ensureHarnessWindow = async (client: BrowserClient): Promise<Result<HarnessWindow, CdpError>> => {
  const pending = inFlight.get(client);
  if (pending) {
    const shared = await pending;
    // `freshlyCreated` is a claim on the seed tab that only the caller which started the creation may hold, or two callers would drive the same tab.
    return shared.success ? ok({ targetId: shared.data.targetId, freshlyCreated: false }) : shared;
  }
  const attempt = ensureHarnessWindowUncoordinated(client).finally(() => inFlight.delete(client));
  inFlight.set(client, attempt);
  return attempt;
};

const ensureHarnessWindowUncoordinated = async (
  client: BrowserClient,
): Promise<Result<HarnessWindow, CdpError>> => {
  const targets = await listPageTargets(client);
  if (!targets.success) return targets;
  const live = new Set(targets.data.map((t) => t.targetId));

  const ownership = client.ownership();
  const pinnedCtx = client.profileContextId();
  // With a pin set but no context resolved yet, a recorded tab's profile cannot be verified, so seed a new window rather than risk adopting one from the wrong profile.
  const canReuse = client.profilePin() === null || pinnedCtx !== undefined;
  const inPinnedProfile = (targetId: string): boolean => {
    if (!pinnedCtx) return true;
    const info = targets.data.find((t) => t.targetId === targetId);
    return info?.browserContextId === pinnedCtx;
  };

  if (canReuse) {
    const harnessWindow = ownership.harnessWindow();
    if (harnessWindow && live.has(harnessWindow) && inPinnedProfile(harnessWindow)) {
      return ok({ targetId: harnessWindow, freshlyCreated: false });
    }
    const survivor = ownership.list().find((id) => live.has(id) && inPinnedProfile(id));
    if (survivor) {
      ownership.setHarnessWindow(survivor);
      return ok({ targetId: survivor, freshlyCreated: false });
    }
  }

  const seeded = client.profilePin()
    ? await client.seedPinnedProfileWindow()
    : await createDedicatedWindow(client);
  if (!seeded.success) return seeded;
  return ok({ targetId: seeded.data, freshlyCreated: true });
};

const createDedicatedWindow = async (client: BrowserClient): Promise<Result<string, CdpError>> => {
  const created = await client.session().callBrowser("Target.createTarget", { url: "about:blank", newWindow: true });
  if (!created.success) return created;
  const { targetId } = created.data;
  const ownership = client.ownership();
  ownership.setHarnessWindow(targetId);
  ownership.add(targetId);
  const win = await client.session().windowId(targetId);
  if (win.success) ownership.setHarnessWindowId(win.data);
  return ok(targetId);
};

export const openHarnessTab = async (
  client: BrowserClient,
  openerTargetId: string,
): Promise<Result<string, CdpError>> => {
  const spawned = client.profileContextId()
    ? await spawnTabViaOpener(client, openerTargetId)
    : await (async (): Promise<Result<string, CdpError>> => {
        const created = await client.session().callBrowser("Target.createTarget", {
          url: "about:blank",
          openerId: openerTargetId,
        });
        if (!created.success) return created;
        return ok(created.data.targetId);
      })();
  if (!spawned.success) return spawned;
  client.ownership().add(spawned.data);
  return ok(spawned.data);
};