/** * @file * * A second, independent Chrome DevTools Protocol connection to Obsidian Mobile's WebView, used to inject * **trusted** input on Android (see **L39**). * * It is deliberately separate from the Appium session rather than layered on it. `AppiumTransport.evaluate` * runs the whole `evalInObsidian` closure inside one W3C *Execute Script*, which awaits the returned * promise — so while a closure sits waiting on `lib.clickElement(...)`, the WebDriver session is busy and * cannot be asked to do anything else. CDP is id-multiplexed and Chromium accepts multiple debugger * clients, so this socket stays free exactly when the Appium one does not. That is what lets the renderer * call back out to the host mid-closure. * * Reaching the WebView's debugger is a two-step: the app publishes an abstract unix socket named * `webview_devtools_remote_`, `adb forward` maps it to a local TCP port, and the usual `/json` * endpoint then lists the page targets. */ import type { CdpInputCommand } from './mobile-input.mjs'; /** * One entry of the debugger's `/json` target list. */ export interface CdpTarget { readonly title?: string; readonly type?: string; readonly url?: string; readonly webSocketDebuggerUrl?: string; } /** * Parameters for {@link connectToWebViewCdp}. */ export interface ConnectToWebViewCdpParams { /** * The Android application id whose WebView to attach to, e.g. `md.obsidian`. */ readonly appId: string; /** * The adb device id (`emulator-5554`, a serial) to run adb against. */ readonly deviceId: string; } /** * Handles a CDP event. */ type CdpEventHandler = (params: Record) => void; /** * An open CDP connection to a WebView. * * Commands are id-multiplexed, so a command issued from inside an event handler completes while an earlier * `Runtime.evaluate` is still awaiting its promise — which is the whole reason this class exists. */ export declare class WebViewCdpConnection { private readonly webSocket; private readonly deviceId; private readonly port; /** * Whether the socket is still usable. * * Restarting the app tears its WebView down and takes the debugger target with it, so a cached * connection has to be re-checked rather than assumed live. * * @returns `true` while the socket is open. */ get isOpen(): boolean; private readonly eventHandlers; private messageId; /** * The commands still waiting for a reply, keyed by the CDP message id each was * sent with. Emptied by whichever of the reply and the timeout comes first. */ private readonly pendingCommands; /** * Wraps an already-open debugger socket, plus what is needed to tear its port forward down. * * ONE `message` listener for the socket's whole life, routing both halves of what * arrives — CDP events to their subscribed handler, command replies to the * {@link send} call waiting on the id. A listener per in-flight command instead * would make the count scale with concurrency, so eleven commands in flight at * once would cross Node's default `maxListeners` of 10 and report a memory leak * that is not one (see the desktop transport's `getPendingCommands`, where the * same shape was measured). * * The parse is guarded because a frame this connection cannot read belongs to * nothing it sent: throwing here would throw out of the event dispatch, where * nothing can catch it. * * @param webSocket - The open debugger socket. * @param deviceId - The adb device id, needed to drop the port forward on dispose. * @param port - The forwarded local port. */ constructor(webSocket: WebSocket, deviceId: string, port: number); /** * Closes the socket and drops the adb port forward. */ dispose(): Promise; /** * Synchronous disposal, for `process.on('exit')` handlers where async work cannot run. * * Without this an abrupt worker exit leaves the port forward behind — the async {@link dispose} above * never gets a turn. */ disposeSync(): void; /** * Subscribes to a CDP event. One handler per method; subscribing again replaces it. * * @param method - The CDP event name, e.g. `Runtime.bindingCalled`. * @param handler - Called with the event parameters. */ on(method: string, handler: CdpEventHandler): void; /** * Sends a CDP command and waits for its response. * * @param method - The CDP method name. * @param params - The CDP method parameters. * @returns The command result. */ send(method: string, params?: Record): Promise>; /** * Sends an ordered command sequence, one command at a time. * * Each command is awaited before the next is sent, which is the only ordering guarantee an input * sequence needs — a long-press holds itself for its own `duration` rather than asking the host to * sleep between two touches, so there is no inter-command delay to honor. * * @param commands - The commands to send, in order. */ sendAll(commands: readonly CdpInputCommand[]): Promise; /** * Closes the debugger socket and stops routing its events. */ private closeSocket; } /** * Opens a CDP connection to the app's WebView. * * @param params - Which app on which device. * @returns The open connection. Dispose it to close the socket and drop the adb port forward. */ export declare function connectToWebViewCdp(params: ConnectToWebViewCdpParams): Promise; /** * Finds a port already forwarded to this device socket, so repeated runs reuse one forward. * * Without this every run allocates a fresh port, and any run that exits without the async * {@link WebViewCdpConnection.dispose} — an abrupt worker exit, where only the synchronous teardown * path runs — leaves its forward behind forever. Reuse bounds that to a single forward per socket * rather than one per run. * * @param forwardList - The output of `adb forward --list`. * @param deviceId - The adb device id to match. * @param socketName - The abstract socket name to match. * @returns The already-forwarded local port, or `undefined` when there is none. */ export declare function parseForwardedPort(forwardList: string, deviceId: string, socketName: string): number | undefined; /** * Picks the WebView's devtools socket name out of `/proc/net/unix`. * * A device can host several debuggable WebViews (other apps, a leftover process), so the app's own pid is * what disambiguates them. When no socket matches that pid the sole remaining candidate is used — a * WebView whose socket is named after a child process rather than the one `pidof` reported still has to be * reachable — but two unattributable candidates is ambiguous enough to refuse. * * @param procNetUnix - The contents of `/proc/net/unix`. * @param pid - The application's process id. * @returns The socket name, e.g. `webview_devtools_remote_18056`. */ export declare function parseWebViewDevtoolsSocketName(procNetUnix: string, pid: string): string; /** * Picks the Obsidian page target out of the debugger's `/json` list. * * @param targets - The `/json` response. * @returns The page target to attach to. */ export declare function selectWebViewPageTarget(targets: readonly CdpTarget[]): CdpTarget; export {};