/** * @file * * Desktop CDP transport — evaluates expressions via Chrome DevTools Protocol * over WebSocket and manages vaults via Electron IPC. * * Two modes: * - **Owned (default)**: the transport launches and owns an isolated Obsidian * instance against a temporary `--user-data-directory` on a free `--remote-debugging-port`, * never touching the user's Obsidian. Supports version pinning via the user-data asar. * - **Attach**: when an explicit CDP port is configured, the transport connects * to an already-running Obsidian on that port. * * It connects to page targets, sends `Runtime.evaluate` commands, and routes to * the correct vault target using `getBasePath()` probing. * * Requirements: Node.js 22+ (uses built-in `WebSocket` and `fetch` globals). */ import type { AsarFallback } from './asar-fallback-detection.mjs'; import type { CaptureScreenshotParams } from './capture-screenshot.mjs'; import type { ElectronCompatibility } from './electron-compatibility.mjs'; import type { InstallerCompatibility } from './installer-compatibility.mjs'; import type { ObsidianTransport, TransportEvalOptions } from './transport.mjs'; /** * Configuration for the CDP transport. */ export interface DesktopCdpTransportConfig { /** * CDP host. Defaults to `'localhost'`. */ cdpHost?: string; /** * CDP port for **attach** mode (the `--remote-debugging-port` the running * Obsidian was launched with). In owned-instance mode this is ignored — a * free port is chosen at launch. */ cdpPort?: number; /** * Timeout in milliseconds for individual CDP commands. * * A test's closure travels as one `Runtime.evaluate` command, so this is also * the desktop per-eval cap. The default is `DEFAULT_EVAL_CAP_IN_MILLISECONDS`, * exported from the package root — import it rather than restating the number, * so a closure sized against the cap follows it if it ever moves. * * @default `30000` */ commandTimeoutInMilliseconds?: number; /** * The vault's config folder override (Obsidian's *Override config folder*), * e.g. `'.obsidian-desktop'`. When set, the owned instance boots to the * starter screen, writes `-config` into that renderer's * `localStorage`, and only then opens the vault over the `vault-open` IPC — * the override is read once, during the vault renderer's own setup, so it has * to exist before that window is created. The opened vault's actual * `app.vault.configDir` is then read back, and a mismatch throws * {@link ConfigDirectoryFallbackError}. Only meaningful in owned mode. * * @default `undefined` (Obsidian's own default, `.obsidian`) */ configDirectory?: string; /** * Grace window in milliseconds for fast-failing a dead boot of the owned * instance (empty `` with no `window.app` after the renderer reached * `document.readyState` `'complete'`). Defaults to * {@link DEFAULT_DEAD_BOOT_GRACE_IN_MILLISECONDS}. `0` disables fast-fail. */ deadBootGraceInMilliseconds?: number; /** * When attaching (i.e. {@link cdpPort} is set), marks the target as a * **harness-owned, already-prepared** instance. Suppresses the user-scope * vault-registration preflight, since the owned instance's vault lives in an * isolated user-data config rather than the user-scope registry. */ isHarnessOwnedInstance?: boolean; /** * Whether the owned Obsidian window is shown on screen. When `false` (the * default), the owned instance is launched with keep-alive Chromium flags and * its window is moved off-screen after launch. Only meaningful in owned mode; * attach mode never touches the (user's) window. * * @default `true` */ isObsidianAppVisible?: boolean; /** * When set, the transport launches and owns an isolated Obsidian instance * instead of attaching to a running one. This is the default desktop mode. */ ownedInstance?: OwnedInstanceConfig; /** * Whether to launch the owned instance with Chromium's sandbox disabled * (`--no-sandbox`). Needed to boot on Linux without a correctly-configured * setuid `chrome-sandbox` helper (e.g. an extracted portable shell, or CI as a * non-root user); harmless on Windows/macOS. Only meaningful in owned mode. * * @default `false` */ shouldDisableSandbox?: boolean; /** * Whether a post-boot **silent asar fallback** (the running app version differs * from the swapped-in pin) fails fast with {@link SilentAsarFallbackError}. The * verdict is always computed and surfaced via * {@link DesktopCdpTransport.getAsarFallback}; this gates only the throw. Only * meaningful in owned mode running a swapped-in asar. * * @default `true` */ shouldThrowOnSilentAsarFallback?: boolean; /** * Whether the post-boot runtime-Electron compatibility nag warning is emitted * when the live Electron is below the app's recommended minimum. The verdict is * always computed and surfaced via {@link DesktopCdpTransport.getElectronCompatibility}; * this only gates the log. Only meaningful in owned mode. * * @default `true` */ shouldWarnOnCompatibilityIssues?: boolean; } /** * An asar to provision into a harness-owned instance's user-data directory before launch. */ export interface OwnedInstanceAsar { /** Absolute path to the cached/source asar file. */ readonly path: string; /** The asar's `x.y.z` version. */ readonly version: string; } /** * Configuration for a harness-owned, isolated Obsidian instance. * * When present, the transport launches and owns its own Obsidian process * against an isolated user-data directory instead of attaching to a running instance. */ export interface OwnedInstanceConfig { /** Optional asar to provision into {@link userDataDirectory} before launch. */ readonly asar?: OwnedInstanceAsar | undefined; /** * The resolved installer↔app compatibility verdict, when it could be determined * (an asar-swap onto a known shell version). Surfaced by * {@link DesktopCdpTransport.getCompatibility}. An `'unrunnable'` verdict reaches * this surface only when the proactive throw is disabled * (`shouldThrowOnIncompatibleInstaller: false`); otherwise it throws * `IncompatibleInstallerVersionError` before the config is built. */ readonly compatibility?: InstallerCompatibility | undefined; /** Absolute path to the Obsidian shell executable to launch. */ readonly exePath: string; /** * Absolute path to the isolated user-data directory. Created and owned by the * transport, and deleted on dispose. */ readonly userDataDirectory: string; } /** * The CDP endpoint of a launched, harness-owned instance. */ export interface OwnedInstanceEndpoint { /** CDP host (e.g. `'localhost'`). */ readonly host: string; /** The free CDP port the owned instance was launched on. */ readonly port: number; } /** * Transport that communicates with Desktop Obsidian via Chrome DevTools Protocol. * * Connects to Obsidian's CDP WebSocket endpoint, sends `Runtime.evaluate` * commands, and routes expressions to the correct vault target. */ export declare class DesktopCdpTransport implements ObsidianTransport { /** * Indicates whether this transport is for a mobile platform. Always `false` for this transport. */ readonly isMobile = false; private activeVaultPath; private asarFallback; private readonly cdpHost; private cdpPort; private cdpUrl; private readonly commandTimeoutInMilliseconds; private readonly configDirectory; private readonly deadBootGraceInMilliseconds; private electronCompatibility; private readonly isHarnessOwnedInstance; private readonly isObsidianAppVisible; private messageId; private readonly ownedConfig; private ownedInstance; /** * The in-flight commands of every socket this transport has opened, keyed by * the CDP message id each is waiting for. One entry per command, added by * {@link sendCommand} and removed by whichever of the reply and the timeout * comes first — see {@link getPendingCommands} for why this exists at all. * * Weak by socket so a closed temporary connection is collectable with its * pending map; the dispatcher listener is the socket's own, so nothing here * keeps it alive. */ private readonly pendingCommandsBySocket; /** * Vault paths THIS transport opened via {@link registerVault}, normalized by * {@link normalizeVaultPathForComparison}. A worker attached to a * harness-owned instance may tear down only these — every other vault in that * instance belongs to the global setup that launched it (see * {@link shouldTearDownVaultWindow}). */ private readonly selfRegisteredVaultPaths; private readonly shouldDisableSandbox; private readonly shouldThrowOnSilentAsarFallback; private readonly shouldWarnOnCompatibilityIssues; private ws; /** * Creates a new CDP transport. * * @param config - CDP connection configuration. */ constructor(config?: DesktopCdpTransportConfig); /** * Captures a PNG screenshot of the Obsidian window showing the given vault. * * When a size is requested the viewport is pinned to it with * `Emulation.setDeviceMetricsOverride` for the duration of the capture, so the * emitted PNG is exactly that size whatever size the window happens to be — * and the override is always cleared afterwards, including when the capture * throws, so a failed capture cannot leave the window stuck at the override. * * @param params - Which vault window to capture, and the exact size to capture it at. * @returns The raw PNG bytes. * @throws Error if CDP returns no image data, or data that is not a PNG. */ captureScreenshot(params: CaptureScreenshotParams): Promise; /** * Disposes of the active WebSocket connection and, in owned-instance mode, * kills the owned Obsidian process and removes its isolated user-data directory. * * The removal is retried because Windows briefly holds the just-killed * process's file handles, which would otherwise fail `rmSync` with `EPERM`. */ dispose(): Promise; /** * Synchronous disposal — kills the owned instance and makes a best-effort * removal of its user-data directory. Safe to call from a process `exit` handler * (where async retries are impossible; a leftover temp directory is acceptable). */ disposeSync(): void; /** * Evaluates a JavaScript expression inside Obsidian via CDP `Runtime.evaluate`. * * Routes to the correct vault target based on `options.cwd`. * * @param expression - The JavaScript expression to evaluate. * @param options - Evaluation options. * @returns The normalized result string. */ evaluate(expression: string, options: TransportEvalOptions): Promise; /** * Returns the silent-asar-fallback verdict for this owned instance — whether the * app version it is actually running matches the swapped-in pin, or the installer * silently reverted to its own bundled asar (read live post-boot). Returns * `undefined` when this is not an owned instance, the instance has not booted * yet, or the verdict could not be determined (no asar was swapped, or the live * version was unreadable). A `'fallback'` verdict reaches this surface only when * the throw is disabled ({@link DesktopCdpTransportConfig.shouldThrowOnSilentAsarFallback} * `false`); otherwise it throws `SilentAsarFallbackError`. * * @returns The silent-asar-fallback verdict, or `undefined`. */ getAsarFallback(): AsarFallback | undefined; /** * Returns the resolved installer↔app compatibility verdict for this owned * instance, so callers can assert on it. Returns `undefined` when this is not * an owned instance, or the verdict could not be determined (e.g. an * undetectable shell version, or the app version is absent from the table). * * @returns The compatibility verdict, or `undefined`. */ getCompatibility(): InstallerCompatibility | undefined; /** * Returns the runtime Electron compatibility verdict for this owned instance — * whether the Electron version it is actually running is new enough for the * running app version (read live post-boot; see {@link ObsidianVersionMetadata.minRecommendedElectronVersion}). * Returns `undefined` when this is not an owned instance, the instance has not * booted yet, or the verdict could not be determined (the live version was * unreadable, or the app version carries no recommended Electron version). * * @returns The runtime Electron compatibility verdict, or `undefined`. */ getElectronCompatibility(): ElectronCompatibility | undefined; /** * Returns the CDP endpoint of the owned, launched instance so the global setup * can hand it to test workers (which then **attach** to it instead of * launching their own). Returns `undefined` when this transport is not an * owned instance, or its instance has not been launched yet. * * @returns The owned instance's CDP host and port, or `undefined`. */ getOwnedInstanceEndpoint(): OwnedInstanceEndpoint | undefined; /** * Verifies that the CDP endpoint is reachable and has Obsidian targets. * * If Obsidian is not running, attempts to auto-start it via URI protocol * and polls until CDP becomes available. * * @param vaultPath - The vault path (used for vault registration check). */ preflightCheck(vaultPath: string): Promise; /** * Registers a vault via Electron IPC evaluated through CDP. * * Uses an existing Obsidian target to send the `vault-open` IPC command, * then polls until the new vault's target appears. * * @param vaultPath - The absolute path to the vault folder. */ registerVault(vaultPath: string): Promise; /** * Unregisters a vault by destroying its window and removing it from the registry. * * Refuses when this transport does not own what it would be tearing down — * see {@link shouldTearDownVaultWindow} for which of the two ownership flags * means what, and why they are not interchangeable. * * @param vaultPath - The absolute path to the vault folder. */ unregisterVault(vaultPath: string): Promise; /** * Computes and stores the runtime-Electron compatibility verdict (on * {@link getElectronCompatibility}) from an already-read version pair, warning * when the live Electron is below the app's recommended minimum. Never throws; a * boot whose running app version was unreadable is skipped (nothing to judge). * * @param appVersion - The live running app version, or `undefined` when unreadable. * @param actualElectronVersion - The live Electron version, or `undefined` when unreadable. */ private applyElectronCompatibility; /** * Points the owned instance's renderer at this process's liveness socket so it * destroys itself if we die without running teardown (see `parent-liveness.ts`). * * Best-effort by design. A renderer without Node access reports `'unavailable'` * and an evaluation failure is logged and swallowed: the watchdog is a backstop * for an already-abnormal exit, never a reason to fail an otherwise good launch. * * @param vaultPath - The vault path to evaluate in. * @param parentLivenessPort - The loopback port the renderer should connect back to. */ private armParentLivenessWatchdog; /** * Verifies the ready vault is actually using the requested config folder, * throwing {@link ConfigDirectoryFallbackError} when it is not. * * Obsidian applies the override with `setConfigDir`, which substitutes * `.obsidian` for any value its own `validateConfigDir` rejects — without a * word, and without failing the boot. The resulting vault opens, reaches * layout-ready, and reports success; what it lacks is the settings and enabled * plugins of the folder that was asked for. Reading the live * `app.vault.configDir` back is the only way to tell the two apart, and it * covers every cause at once: a name Obsidian rejected (including its * unrecoverable character blacklist, which `assertValidConfigDirectory` deliberately * does not guess at), and a write that never reached the vault's renderer. * * A no-op when no override was requested. * * @param vaultPath - The absolute path to the vault folder. */ private assertRequestedConfigDirectory; /** * Builds the error for a harness-owned instance that is no longer serving CDP, * from the best evidence this process has. * * Two processes see two different halves of the death. The one that launched * the instance holds the child and can read the exit code straight off it; a * test worker holds nothing but the port, and reads the same facts from the * exit marker the owner wrote (`owned-instance-exit-marker.ts`). With neither * — the marker lost, or a kill so abrupt nothing recorded it — the error still * names the instance as the cause, which is the whole point of it. * * @returns The error to throw in place of a refused connection. */ private buildOwnedInstanceExitedError; /** * Verifies the running app (asar) version matches the swapped-in pin, storing the * verdict on {@link getAsarFallback}. On a **silent fallback** (the installer ran * its own bundled asar instead of the pin) it throws {@link SilentAsarFallbackError} * when the throw is enabled, otherwise warns (when warnings are on) and lets the * boot proceed. A boot with no swapped-in asar, or an unreadable running version, * is `'unknown'` — nothing is thrown or warned. This is the healthy-UI companion to * the black-screen {@link RendererFailedToInitializeError} dead-boot fast-fail. * * @param runningApiVersion - The live running app version, or `undefined` when unreadable. */ private checkRuntimeAsarFallback; /** * Runs the post-boot runtime compatibility checks for an owned instance, once the * vault is ready. Reads the live running app version and Electron version **once** * from the booted renderer's main process, then: (1) verifies the running app * version matches the swapped-in pin — throwing {@link SilentAsarFallbackError} on * a silent fallback (when enabled); and (2) runs the best-effort runtime-Electron * nag. Only the asar-fallback check can throw, so this must run outside the * readiness poll's try/catch (a swallowed throw would loop until timeout). * * Both are read at the renderer top level: `ipcRenderer.sendSync('version')` (the * running app version, truthful even under a silent asar fallback) and * `process.versions.electron` (the live shell Electron); neither uses * `require('obsidian')`, which resolves only inside a plugin-load context. A read * failure leaves both unknown (logged, non-fatal) — an unreadable running version * cannot be judged a fallback, so the boot is not broken by a flaky read. * * @param vaultPath - The vault path to evaluate in. */ private checkRuntimeCompatibility; /** * Connects to a CDP target's WebSocket endpoint. * * @param target - The CDP target to connect to. * @returns The open WebSocket connection. */ private connectToTarget; /** * Disconnects the active WebSocket connection. */ private disconnect; /** * Dismisses the "Do you trust the author of this vault?" dialog if present. * * Acts as a safety net when `enable-plugin-` is written in one renderer * but not yet visible to the newly-opened vault's renderer (race observed * in Obsidian 1.13.0). * * @param vaultPath - The vault path to evaluate in. */ private dismissTrustDialog; /** * Sets `enable-plugin-` in Obsidian's localStorage to prevent * the "Do you trust the author of this vault?" dialog from appearing * when a vault with community plugins is opened for the first time. * * Must be called after the `vault-open` IPC (so the vault ID exists in * `obsidian.json`) and before the new vault window finishes loading. * * @param ws - An open WebSocket to an existing Obsidian target. * @param vaultPath - The absolute path to the vault folder. */ private enablePluginsInLocalStorage; /** * Ensures there is an active WebSocket connection to the correct vault target. * * Reuses the existing connection if it targets the requested vault. * Otherwise, finds the correct target and reconnects. * * @param vaultPath - The vault path to target. * @returns The active WebSocket connection. */ private ensureConnection; /** * Launches Obsidian with `--remote-debugging-port` and polls until CDP becomes available. */ private ensureObsidianRunning; /** * Finds the CDP target that has the given vault open. * * Probes every target by evaluating `getBasePath()` and returns the one whose * base path matches `vaultPath` (via {@link areVaultPathsMatching}, tolerant of * separator/case differences). A single target is **not** returned blindly: with * more than one vault open (attach mode's shared instance), the sole-target * shortcut would return whichever window happens to be open regardless of which * vault was requested — the exact mis-routing this method must avoid. A target * whose probe throws is treated as not-ready and skipped (the caller's readiness * poll retries); a target whose probe succeeds but does not match is never * returned. When nothing matches, throw so the caller keeps polling. * * @param vaultPath - The vault path to match. * @returns The matching CDP target. */ private findTargetForVault; /** * Fetches the list of page-type targets from the CDP `/json` endpoint. * * @returns The list of page targets. * @throws {Error} When no CDP endpoint is configured yet. */ private getPageTargets; /** * Returns a socket's in-flight command map, installing its reply dispatcher on * first use. * * ONE `message` listener per socket, for the socket's whole life — not one per * command. A listener per in-flight command makes the count scale with how many * evaluations a suite has in flight at once, so a consumer that fires eleven * concurrent `evalInObsidian` calls crosses Node's default `maxListeners` of 10 * and is told its transport is leaking memory, which it is not: measured * 2026-09-15, fifteen concurrent evaluations registered fifteen listeners on the * one long-lived socket and the count fell back to zero the moment they settled. * A warning that fires on correct use is worse than no warning, because the real * signal — an entry that is added and never removed — is then indistinguishable * from it. With one dispatcher the listener count is a constant, and a genuine * leak shows instead as a pending map that never empties. * * The dispatcher is deliberately forgiving about what arrives: a CDP **event** * carries no `id` and belongs to no command, and a frame that is not JSON at all * belongs to nothing this transport sent. Both are ignored. The old per-command * handler parsed unguarded, so one frame that is not JSON threw once per * in-flight command, out of the event dispatch, where nothing could catch it. * * @param ws - The socket whose replies are being awaited. * @returns The socket's map of in-flight commands, keyed by CDP message id. */ private getPendingCommands; /** * Kills the currently-running owned instance (if any) and waits for it to exit, * so the next launch gets a pristine single-window instance. * * Relaunching over a live instance is forwarded by Electron's single-instance * lock on the shared user-data directory and surfaces the vault picker, and opening a * second window via IPC leaves stale windows that break vault-target routing — * hence a full kill + wait-for-exit (releasing the lock) between launches. A * no-op on the first attempt, when no instance is running yet. */ private killRunningOwnedInstance; /** * Moves the owned instance's window off-screen so a hidden run never steals * focus. Uses Electron's remote bridge (`window.electron.remote`) — the only * cross-platform way to reposition the window — placing it just beyond the * right edge of all displays. The window stays "visible" to Chromium (so * timers, `requestAnimationFrame`, `:hover`, and trusted input keep working), * unlike minimizing, which would freeze `requestAnimationFrame`. * * The remote bridge is not available the instant CDP starts serving, so this * polls until the move succeeds. Best-effort: if the window cannot be moved * within the timeout, the keep-alive launch flags still apply and the run * proceeds (worst case the window is briefly visible). */ private moveOwnedWindowOffscreen; /** * Writes the config-folder override into the starter screen's `localStorage` * and then opens the vault from there over the `vault-open` IPC. * * Obsidian reads the override exactly once, in the vault's own renderer, during * `Vault.setup` — `getOverrideConfigDir(appId)` is `localStorage.getItem(appId + * '-config')`. So there is no writing it into the vault's window: by the time * that window exists the read has happened. The starter screen is the earlier * renderer the write needs, and it qualifies because it has `localStorage` and * `window.electron.ipcRenderer` but no `app` (verified on 1.13.7). * * That missing `app` is also why this talks raw CDP rather than going through * {@link ensureNamespaceBootstrapped} / the `ipcSendSync` helper as * {@link DesktopCdpTransport.openVaultInRunningInstance} does: every one of * those paths probes `app.vault.adapter` to identify its target, and would fail * on the very renderer this needs. * * The key name is known ahead of the write because the harness registered the * vault under an id it generated itself, and `vault-open` reuses that entry * rather than minting a new one. * * @param vaultPath - The absolute path to the vault folder. * @param vaultId - The harness-generated vault id the vault is registered under. * @param configDirectory - The config folder to override to. */ private openVaultFromStarterScreen; /** * Opens a vault in an already-running Obsidian instance via the `vault-open` * Electron IPC (evaluated through CDP on an existing target), then polls until * the new vault's window target appears, is layout-ready, and its trust dialog * (if any) has been dismissed. * * Shared by the attach-mode {@link registerVault} and the owned-mode * "instance already launched" path, so opening an additional vault never * relaunches the process (which would surface the vault picker). * * @param vaultPath - The absolute path to the vault folder. */ private openVaultInRunningInstance; /** * Samples the vault renderer's bootstrap state — whether the document is * `complete`, whether `window.app` exists, and the `` child count — from * the first page target, for dead-boot detection. This works even when the app * never bootstrapped (the renderer page target still exists), which is exactly * the state it must observe. * * @returns The sampled observation, or `undefined` when no target is reachable * or the probe failed (so the caller keeps polling rather than fast-failing). */ private probeRendererBootState; /** * Probes a target to discover which vault path it has open. * * Creates a temporary WebSocket connection, evaluates `getBasePath()`, * and returns the result. * * @param target - The CDP target to probe. * @returns The vault's base path. */ private probeVaultPath; /** * Launches and connects to a harness-owned, isolated Obsidian instance for a * vault: provisions the asar (if any), pre-seeds the isolated `obsidian.json` * so the vault opens directly, launches the instance on a free CDP port, then * waits until the vault window is ready and dismisses the trust dialog. * * @param vaultPath - The absolute path to the vault folder. */ private registerVaultInOwnedInstance; /** * Sends a CDP command over WebSocket and waits for the response. * * @param ws - The WebSocket connection. * @param method - The CDP method name. * @param params - The CDP method parameters. * @returns The CDP response. */ private sendCommand; /** * Polls a killed owned instance's CDP endpoint until it stops responding, * confirming the process has exited and released Electron's single-instance * lock on the shared user-data directory before a fresh instance is launched into it. * * @param cdpUrl - The CDP URL of the instance that was just killed. */ private waitForInstanceExit; /** * Waits for the vault's `app.workspace` to reach layout-ready state. * * `findTargetForVault` returns as soon as `app.vault.adapter.getBasePath()` * matches — which is true shortly after the `App` constructor runs, before * `plugins.initialize()` (and any trust dialog) has executed. Bootstrapping * the namespace and calling `pollVaultBasePath()` awaits `layoutReady`, so * by the time this returns the dialog has either rendered or is not going * to render. * * @param vaultPath - The vault path to evaluate in. */ private waitForLayoutReady; /** * Polls the owned instance until the vault target exists, layout is ready, and * the trust dialog (if any) has been dismissed. * * Between readiness attempts it also checks for a **dead boot** — the renderer * loaded (`document.readyState` `'complete'`) but the app never bootstrapped * (empty ``, no `window.app`), the terminal state when the asar cannot * run on the launched Electron shell. Once that state has held for the * configured grace window it throws a {@link RendererFailedToInitializeError} * immediately instead of waiting out the full readiness timeout. A grace of * `0` disables the fast-fail. The grace clock starts when the renderer first * reports `complete`, so a slow load before then is never counted against it. * * @param vaultPath - The absolute path to the vault folder. */ private waitForOwnedVaultReady; /** * Polls until the starter screen's renderer can accept the config-folder * override — that is, until it exposes both `localStorage` and * `window.electron.ipcRenderer.sendSync`. * * A page target exists as soon as the launch resolves, so this is not waiting * for a window; it is waiting for that window's own scripts to finish. Writing * before then would either throw or, worse, land in a renderer that is about to * be replaced. * * @returns The starter screen's CDP target. * @throws {Error} When no renderer becomes writable within the timeout. */ private waitForStarterScreenTarget; }