From 36957bce3b2f8a6299c69a47d228205dad3c1da4 Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 19 Oct 2026 01:10:54 -0700 Subject: [PATCH] refactor(browser): one page gate and one owner for a host command's hold The revocation gate, the input cancel list and the hold's lifecycle each lived in two places. Sinks now call one gate, held input is recorded at the CDP send (and the one native key path), and runWithRemoteCliPageAdmission owns retire-on-revoke, release and error conversion. --- ...ent-browser-bridge-host-revocation.test.ts | 29 +++-- .../browser/agent-browser-bridge-queue.ts | 52 +++----- src/main/browser/agent-input-injection.ts | 32 +---- src/main/browser/cdp-debugger-channel.ts | 7 -- src/main/browser/cdp-page-input-commands.ts | 6 +- .../cdp-ws-proxy-host-admission.test.ts | 2 +- src/main/browser/cdp-ws-proxy.ts | 4 +- src/main/browser/guest-cdp-command.ts | 28 ++--- src/main/browser/guest-direct-key-input.ts | 32 +++-- src/main/browser/remote-cli-page-admission.ts | 116 +++++++++++------- .../remote-cli-browser-scope.ts | 8 +- 11 files changed, 138 insertions(+), 178 deletions(-) diff --git a/src/main/browser/agent-browser-bridge-host-revocation.test.ts b/src/main/browser/agent-browser-bridge-host-revocation.test.ts index f75234734848..c1a929244313 100644 --- a/src/main/browser/agent-browser-bridge-host-revocation.test.ts +++ b/src/main/browser/agent-browser-bridge-host-revocation.test.ts @@ -43,7 +43,10 @@ import { captureRemoteCliBrowserScope, runInRemoteCliBrowserScope } from '../runtime/desktop-browser-bridge/remote-cli-browser-scope' -import { assertRemoteCliPageAdmitted } from './remote-cli-page-admission' +import { + assertRemoteCliPageAccess, + runUnderRemoteCliPageAdmission +} from './remote-cli-page-admission' import { createFakeAgentBrowserChild, mockBrowserManager, @@ -57,6 +60,11 @@ overrideBridgeWebContentsLookup(AgentBrowserBridge.prototype, webContentsFromIdM const WORKSPACE = 'repo-host::/srv/app' +/** A helper step reaching the page's CDP proxy now. */ +function assertHelperStepAdmitted(): void { + runUnderRemoteCliPageAdmission(100, () => assertRemoteCliPageAccess(100)) +} + function dispatchedMouseEvents(wc: ReturnType): unknown[] { return wc.debugger.sendCommand.mock.calls.filter((call) => call[0] === 'Input.dispatchMouseEvent') } @@ -126,24 +134,21 @@ describe('AgentBrowserBridge under a host revocation', () => { revoked.signal ) await vi.waitFor(() => expect(helperCallbacks).toHaveLength(1)) - expect(() => assertRemoteCliPageAdmitted(100)).not.toThrow() + expect(() => assertHelperStepAdmitted()).not.toThrow() // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every instance the hoisted mock constructs has a vi.fn stop. const proxy = CdpWsProxyMock.instances.at(-1) as { stop: ReturnType } let heldWhenRetired = false proxy.stop.mockImplementation(async () => { - heldWhenRetired = (() => { - try { - assertRemoteCliPageAdmitted(100) - return false - } catch { - return true - } - })() + try { + assertHelperStepAdmitted() + } catch { + heldWhenRetired = true + } }) revoked.abort() // The helper's next CDP step reaches the page's proxy and is refused there. - expect(() => assertRemoteCliPageAdmitted(100)).toThrow('stopped sharing') + expect(() => assertHelperStepAdmitted()).toThrow('stopped sharing') // Retired on the revocation itself, not when the helper's client times out. await vi.waitFor(() => expect(proxy.stop).toHaveBeenCalled()) helperCallbacks[0]!( @@ -153,7 +158,7 @@ describe('AgentBrowserBridge under a host revocation', () => { ) await expect(click).rejects.toMatchObject({ code: 'desktop_browser_bridge_unavailable' }) - expect(() => assertRemoteCliPageAdmitted(100)).not.toThrow() + expect(() => assertHelperStepAdmitted()).not.toThrow() // The helper's daemon outlives its client, so it is retired before the page is let go. expect(heldWhenRetired).toBe(true) expect(execFileMock).toHaveBeenCalledWith( diff --git a/src/main/browser/agent-browser-bridge-queue.ts b/src/main/browser/agent-browser-bridge-queue.ts index e63b2842e446..910ffba8f073 100644 --- a/src/main/browser/agent-browser-bridge-queue.ts +++ b/src/main/browser/agent-browser-bridge-queue.ts @@ -6,14 +6,12 @@ import type { EnqueueTargetedCommandOptions, ResolvedBrowserCommandTarget } from './agent-browser-bridge-types' -import { isAdmissionRevoked, runWithRemoteCliPageAdmission } from './remote-cli-page-admission' +import { runWithRemoteCliPageAdmission } from './remote-cli-page-admission' import { AGENT_BROWSER_CLEANUP_TIMEOUT_MS } from './agent-browser-bridge-types' import { releaseHeldPageInput } from './guest-direct-key-input' import { captureRemoteCliBrowserRevocation, - captureRemoteCliBrowserRevokedSignal, - captureRemoteCliBrowserScope, - runOutsideRemoteCliBrowserScope + captureRemoteCliBrowserScope } from '../runtime/desktop-browser-bridge/remote-cli-browser-scope' export abstract class AgentBrowserBridgeQueue extends AgentBrowserBridgeShutdown { @@ -95,7 +93,7 @@ export abstract class AgentBrowserBridgeQueue extends AgentBrowserBridgeShutdown const gate = (): void => assertNotRevoked(target.webContentsId) gate() return origin.bridged - ? this.runAdmitted(session, target.webContentsId, gate, () => + ? this.runHostCommand(session, target.webContentsId, gate, origin.revoked, () => execute(session, target) ) : execute(session, target) @@ -127,53 +125,31 @@ export abstract class AgentBrowserBridgeQueue extends AgentBrowserBridgeShutdown return execute(sessionName, target) } - /** - * Holds the page for a host's command: every later page-affecting step, including the helper's - * through the CDP proxy, is refused once the host is revoked, and the command reports that. - */ - private async runAdmitted( + /** Holds the page for a host's command, so a revocation stops every later step of it. */ + private runHostCommand( sessionName: string, webContentsId: number, gate: () => void, + revoked: AbortSignal | undefined, execute: () => Promise ): Promise { - // Why: the helper's daemon outlives its killed client and keeps retrying; retire it while the - // page is still held, or its next step lands once the hold is gone. Retiring on the signal also - // frees the page's queue now instead of at the helper's exec timeout. - let retiring: Promise | null = null - const retire = (): Promise => - (retiring ??= runOutsideRemoteCliBrowserScope(() => + return runWithRemoteCliPageAdmission(webContentsId, gate, execute, { + revoked, + // Why keep the queue: commands waiting on the page still run, on a fresh helper session. + retire: () => this.destroySession(sessionName, { closeTimeoutMs: AGENT_BROWSER_CLEANUP_TIMEOUT_MS, keepQueuedCommands: true - }) - )) - const signal = captureRemoteCliBrowserRevokedSignal() - const onRevoked = (): void => void retire() - signal?.addEventListener('abort', onRevoked, { once: true }) - const held = async (): Promise => { - try { - return await execute() - } finally { - signal?.removeEventListener('abort', onRevoked) - if (retiring || isAdmissionRevoked(gate)) { - await retire() - } - } - } - try { - return await runWithRemoteCliPageAdmission(webContentsId, gate, held, (release) => { + }), + releaseHeld: (release) => { const wc = this.getWebContents(webContentsId) return wc ? releaseHeldPageInput(wc, release, () => this.browserManager.guardUserFocusForAgentInput(webContentsId) ) : Promise.resolve() - }) - } catch (error) { - gate() - throw error - } + } + }) } protected async processQueue(sessionName: string): Promise { diff --git a/src/main/browser/agent-input-injection.ts b/src/main/browser/agent-input-injection.ts index 658d06bc7f27..dd6a3fc0d1ce 100644 --- a/src/main/browser/agent-input-injection.ts +++ b/src/main/browser/agent-input-injection.ts @@ -1,10 +1,5 @@ import type { WebContents } from 'electron' -import { - assertRemoteCliPageAdmitted, - isInputRelease, - noteRemoteCliInput -} from './remote-cli-page-admission' -import { assertRemoteCliBrowserAccess } from '../runtime/desktop-browser-bridge/remote-cli-browser-scope' +import { assertRemoteCliPageAccess } from './remote-cli-page-admission' // Why: Orca's guest shortcut and grab handlers act on keys the page receives, and an unhandled key // can reach the app menu. Keys an agent injects must do neither (#20595), whichever path sends them. @@ -14,38 +9,19 @@ export function isAgentInjectingInput(webContents: WebContents): boolean { return (injectionDepth.get(webContents) ?? 0) > 0 } -/** The CDP input event being delivered, so a revocation can release what a host pressed. */ -export type AgentInputEvent = { method: string; params: Record } - -/** Refuses agent input that a revoked SSH host's command sends, directly or through its helper. */ -export function assertAgentInputAdmitted(webContents: WebContents): void { - assertRemoteCliBrowserAccess(webContents.id) - assertRemoteCliPageAdmitted(webContents.id) -} - /** Marks input sent during `run` as the agent's: Orca shortcuts and app menu items ignore it. */ export async function withAgentInputInjection( webContents: WebContents, - run: () => Promise, - input?: AgentInputEvent + run: () => Promise ): Promise { - assertAgentInputAdmitted(webContents) - // Why: a press counts as held once it may have landed; a release only once it surely did. - const releasing = input !== undefined && isInputRelease(input.params) - if (input && !releasing) { - noteRemoteCliInput(webContents.id, input.method, input.params) - } + assertRemoteCliPageAccess(webContents.id) const depth = injectionDepth.get(webContents) ?? 0 injectionDepth.set(webContents, depth + 1) if (depth === 0) { webContents.setIgnoreMenuShortcuts(true) } try { - const result = await run() - if (input && releasing) { - noteRemoteCliInput(webContents.id, input.method, input.params) - } - return result + return await run() } finally { const remaining = (injectionDepth.get(webContents) ?? 1) - 1 if (remaining > 0) { diff --git a/src/main/browser/cdp-debugger-channel.ts b/src/main/browser/cdp-debugger-channel.ts index 82da834ca1db..18620aa6c6d3 100644 --- a/src/main/browser/cdp-debugger-channel.ts +++ b/src/main/browser/cdp-debugger-channel.ts @@ -1,7 +1,6 @@ import { WebSocket } from 'ws' import type { WebContents } from 'electron' import { acquireElectronDebugger, type ElectronDebuggerLease } from './electron-debugger-lease' -import { assertRemoteCliPageAdmitted } from './remote-cli-page-admission' import type { CdpClientResponseWriter } from './cdp-client-response-writer' import type { CdpSyntheticSessionRegistry } from './cdp-synthetic-session-registry' import { sendGuestCdpCommand } from './guest-cdp-command' @@ -94,12 +93,6 @@ export class CdpDebuggerChannel { params: Record, sessionId?: string ): Promise { - // Why: the proxy's queued input outlives the message's admission; a revoked host's must not land. - try { - assertRemoteCliPageAdmitted(this.webContents.id) - } catch (error) { - return Promise.reject(error) - } return sessionId ? sendGuestCdpCommand(this.webContents, method, params, sessionId) : sendGuestCdpCommand(this.webContents, method, params) diff --git a/src/main/browser/cdp-page-input-commands.ts b/src/main/browser/cdp-page-input-commands.ts index 57e0bf15fe5d..89da2e803fd1 100644 --- a/src/main/browser/cdp-page-input-commands.ts +++ b/src/main/browser/cdp-page-input-commands.ts @@ -85,10 +85,8 @@ export class CdpPageInputCommands { return await this.debuggerChannel.sendDebuggerCommand(method, params, sessionId) } const send = (): Promise => - withAgentInputInjection( - this.webContents, - () => this.debuggerChannel.sendDebuggerCommand(method, params, sessionId), - { method, params } + withAgentInputInjection(this.webContents, () => + this.debuggerChannel.sendDebuggerCommand(method, params, sessionId) ) const routedByFocus = !sessionId && isFocusRoutedCdpInput(method) if (!routedByFocus && !isFocusTakingCdpInput(method, params)) { diff --git a/src/main/browser/cdp-ws-proxy-host-admission.test.ts b/src/main/browser/cdp-ws-proxy-host-admission.test.ts index ef43cdc249ef..8a18a963eafb 100644 --- a/src/main/browser/cdp-ws-proxy-host-admission.test.ts +++ b/src/main/browser/cdp-ws-proxy-host-admission.test.ts @@ -104,7 +104,7 @@ describe('CdpWsProxy under a host command', () => { } }, () => new Promise((resolve) => (finish = resolve)), - (release) => releaseHeldPageInput(page, release, () => guardFocus()) + { releaseHeld: (release) => releaseHeldPageInput(page, release, () => guardFocus()) } ) return { revoke: () => (revoked = true), diff --git a/src/main/browser/cdp-ws-proxy.ts b/src/main/browser/cdp-ws-proxy.ts index 163e857c01d0..127ad59f3f20 100644 --- a/src/main/browser/cdp-ws-proxy.ts +++ b/src/main/browser/cdp-ws-proxy.ts @@ -13,7 +13,7 @@ import type { AgentInputFocusGuard } from './agent-input-focus-guard' import { CdpPageInputCommands } from './cdp-page-input-commands' import { isTrustedCdpLoopbackRequest } from './cdp-loopback-request-guard' import { - assertRemoteCliPageAdmitted, + assertRemoteCliPageAccess, runUnderRemoteCliPageAdmission } from './remote-cli-page-admission' @@ -186,7 +186,7 @@ export class CdpWsProxy { this.responder.recordRequestSessionId(client, clientId, msg) // Why here too: a revoked host's helper is refused even its read-only steps, so it stops at once. try { - assertRemoteCliPageAdmitted(this.webContents.id) + assertRemoteCliPageAccess(this.webContents.id) } catch (error) { this.responder.sendError( clientId, diff --git a/src/main/browser/guest-cdp-command.ts b/src/main/browser/guest-cdp-command.ts index f3845e552a93..d71bca59b8ea 100644 --- a/src/main/browser/guest-cdp-command.ts +++ b/src/main/browser/guest-cdp-command.ts @@ -1,7 +1,6 @@ import type { WebContents } from 'electron' import { BrowserError } from './browser-error' -import { assertRemoteCliBrowserAccess } from '../runtime/desktop-browser-bridge/remote-cli-browser-scope' -import { isInputRelease, noteRemoteCliInput } from './remote-cli-page-admission' +import { assertRemoteCliPageAccess, trackRemoteCliInput } from './remote-cli-page-admission' // Why: Chromium resizes the page's view for these without checking the view still exists // (WebContentsImpl::SetDeviceEmulationSize). A crashed renderer takes its view with it until the @@ -28,8 +27,8 @@ export function sendGuestCdpCommand( sessionId?: string ): Promise { try { - // Why: only a host's own command runs in its scope; desktop-owned sends are never refused here. - assertRemoteCliBrowserAccess(guest.id) + // Why: only a host's own work carries its scope; desktop-owned sends are never refused here. + assertRemoteCliPageAccess(guest.id) } catch (error) { return Promise.reject(error) } @@ -43,20 +42,11 @@ export function sendGuestCdpCommand( ) ) } - // Why: a press counts as held once it may have landed; a release only once it surely did. - const releasing = params !== undefined && isInputRelease(params) - if (!releasing) { - noteRemoteCliInput(guest.id, method, params) - } - const sent = Promise.resolve( - sessionId === undefined - ? guest.debugger.sendCommand(method, params) - : guest.debugger.sendCommand(method, params, sessionId) + return trackRemoteCliInput(guest.id, method, params, () => + Promise.resolve( + sessionId === undefined + ? guest.debugger.sendCommand(method, params) + : guest.debugger.sendCommand(method, params, sessionId) + ) ) - return releasing - ? sent.then((result: unknown) => { - noteRemoteCliInput(guest.id, method, params) - return result - }) - : sent } diff --git a/src/main/browser/guest-direct-key-input.ts b/src/main/browser/guest-direct-key-input.ts index 08e9d2649abf..a33b34dd299c 100644 --- a/src/main/browser/guest-direct-key-input.ts +++ b/src/main/browser/guest-direct-key-input.ts @@ -1,6 +1,6 @@ import type { WebContents } from 'electron' import { enablePageFocusEmulation, type AgentInputFocusGuard } from './agent-input-focus-guard' -import { assertAgentInputAdmitted, withAgentInputInjection } from './agent-input-injection' +import { withAgentInputInjection } from './agent-input-injection' import { resolveFocusedFrameRoute, settleWithin, @@ -12,7 +12,11 @@ import { sendGuestCdpCommand } from './guest-cdp-command' import { acquireElectronDebugger } from './electron-debugger-lease' import { normalizeCdpPointerButton } from './agent-browser-bridge-mouse' import { cdpPointerStateFor, releaseCdpPointerButton } from './cdp-pointer-input' -import type { HeldInputRelease } from './remote-cli-page-admission' +import { + assertRemoteCliPageAccess, + trackRemoteCliInput, + type HeldInputRelease +} from './remote-cli-page-admission' // Why: Chromium routes main-session CDP key and text input on a guest by the // window's focus, so it lands in whatever the user is typing into (#20595). Events sent to @@ -43,7 +47,7 @@ async function waitForPageToHandleInput(send: GuestCdpSender): Promise { function runEditCommand(webContents: WebContents, command: GuestEditCommand): void { // Why: it runs after awaiting the page, so a host's access may have been revoked meanwhile. - assertAgentInputAdmitted(webContents) + assertRemoteCliPageAccess(webContents.id) switch (command) { case 'selectAll': webContents.selectAll() @@ -91,10 +95,8 @@ async function sendToFrameSession( method: string, params: Record ): Promise { - return await withAgentInputInjection( - webContents, - () => sendFrameSessionInput(webContents, send, sessionId, method, params), - { method, params } + return await withAgentInputInjection(webContents, () => + sendFrameSessionInput(webContents, send, sessionId, method, params) ) } @@ -111,7 +113,6 @@ export async function dispatchPageKeyEvent( if (!isEmbeddedGuest(webContents)) { return await send('Input.dispatchKeyEvent', params) } - const keyInput = { method: 'Input.dispatchKeyEvent', params } const route = await resolveFocusedFrameRoute(webContents, send) if (route.kind === 'session') { // Why: Tab from a frame's last field can move focus out of the guest into Orca's window. @@ -137,9 +138,9 @@ export async function dispatchPageKeyEvent( ? await guardFocus() : () => {} try { - await withAgentInputInjection( - webContents, - async () => { + await withAgentInputInjection(webContents, () => + // Why: widget events skip the CDP send, so this key is recorded on the page's hold here. + trackRemoteCliInput(webContents.id, 'Input.dispatchKeyEvent', params, async () => { for (const event of plan.events) { webContents.sendInputEvent(event) } @@ -149,8 +150,7 @@ export async function dispatchPageKeyEvent( for (const command of plan.commands) { runEditCommand(webContents, command) } - }, - keyInput + }) ) } finally { release() @@ -159,11 +159,7 @@ export async function dispatchPageKeyEvent( } const release = await focusGuestForCdpInput(webContents, send, guardFocus) try { - return await withAgentInputInjection( - webContents, - () => send('Input.dispatchKeyEvent', params), - keyInput - ) + return await withAgentInputInjection(webContents, () => send('Input.dispatchKeyEvent', params)) } finally { release() } diff --git a/src/main/browser/remote-cli-page-admission.ts b/src/main/browser/remote-cli-page-admission.ts index c80979150835..e19476f28f97 100644 --- a/src/main/browser/remote-cli-page-admission.ts +++ b/src/main/browser/remote-cli-page-admission.ts @@ -1,78 +1,114 @@ /** - * The pages an SSH host's command is driving right now, each with the check that refuses its next - * step once that host is revoked. Every page-affecting sink (guest CDP sends, native input events, - * edit commands) consults it, so no queued or deferred step of a running command outlives the - * revocation. + * One SSH host command's hold on a page. While it runs, every page sink (guest CDP sends, native + * input, edit commands, the CDP proxy's intake) refuses work attributed to it once the host is + * revoked. Work is attributed by async context: the command's own through its bridged scope, its + * out-of-process helper's through the proxy message it arrived on. The hold also owns the + * command's input cancel list: what it pressed and has not released, let go of on revocation. */ import { AsyncLocalStorage } from 'node:async_hooks' -import { runOutsideRemoteCliBrowserScope } from '../runtime/desktop-browser-bridge/remote-cli-browser-scope' +import { + assertRemoteCliBrowserAccess, + runOutsideRemoteCliBrowserScope +} from '../runtime/desktop-browser-bridge/remote-cli-browser-scope' /** The CDP input event that lets go of something a command pressed. */ export type HeldInputRelease = { method: string; params: Record } +export type PageHoldHooks = { + /** Fires when the host is revoked; `retire` then runs at once, while the page is still held. */ + revoked?: AbortSignal + /** Stops the command's helper, whose daemon outlives its killed client and keeps retrying. */ + retire?: () => Promise + releaseHeld?: (release: HeldInputRelease) => Promise +} + type Admission = { admit: () => void - /** What this command pressed and has not released, keyed by button, key or touch. */ + /** Keyed by button, key or touch. */ held: Map } const admissions = new Map() -/** The admission a proxy message arrived under; its deferred steps keep it after the hold ends. */ +/** The hold a proxy message arrived under; its deferred steps keep it after the hold ends. */ const messageAdmission = new AsyncLocalStorage<() => void>() /** - * Runs a host's command holding the page. If the host is revoked mid-press, the command's own - * release is refused like every other step, so the desktop lets go of what it left held once the - * command ends: from outside the host's scope, with routing probes no longer refused. + * Runs a host's command holding the page. On revocation every later step is refused, releases + * included, so once the command ends the desktop lets go of what it left held, from outside the + * host's scope. A failure after revocation reports the revocation. */ export async function runWithRemoteCliPageAdmission( webContentsId: number, admit: () => void, run: () => Promise, - releaseHeld?: (release: HeldInputRelease) => Promise + hooks: PageHoldHooks = {} ): Promise { const admission: Admission = { admit, held: new Map() } + let retiring: Promise | undefined + const retire = (): Promise | undefined => + hooks.retire && (retiring ??= runOutsideRemoteCliBrowserScope(hooks.retire)) + const onRevoked = (): void => void retire() + hooks.revoked?.addEventListener('abort', onRevoked, { once: true }) admissions.set(webContentsId, admission) try { - return await run() - } finally { - if (admissions.get(webContentsId) === admission) { - admissions.delete(webContentsId) - } - if (releaseHeld && admission.held.size > 0 && isAdmissionRevoked(admit)) { - await runOutsideRemoteCliBrowserScope(() => releaseAll(admission.held, releaseHeld)) + try { + return await run() + } finally { + hooks.revoked?.removeEventListener('abort', onRevoked) + if (isRevoked(admit)) { + await retire() + } + if (admissions.get(webContentsId) === admission) { + admissions.delete(webContentsId) + } + const { releaseHeld } = hooks + if (releaseHeld && admission.held.size > 0 && isRevoked(admit)) { + await runOutsideRemoteCliBrowserScope(() => releaseAll(admission.held, releaseHeld)) + } } + } catch (error) { + admit() + throw error } } -/** Throws while a revoked host's command still holds this page; a no-op otherwise. */ -export function assertRemoteCliPageAdmitted(webContentsId: number | undefined): void { - if (webContentsId !== undefined) { - admissions.get(webContentsId)?.admit() - } - messageAdmission.getStore()?.() -} - /** Runs a proxy message's handling, deferred steps included, under the page's current hold. */ export function runUnderRemoteCliPageAdmission(webContentsId: number, handle: () => T): T { const admission = admissions.get(webContentsId) return admission ? messageAdmission.run(admission.admit, handle) : handle() } -/** Records input that reached a page a host's command holds, so a revocation can release it. */ -export function noteRemoteCliInput( +/** The page sinks' gate: throws for work of a revoked host's command; a no-op for the desktop's. */ +export function assertRemoteCliPageAccess(webContentsId?: number): void { + assertRemoteCliBrowserAccess(webContentsId) + messageAdmission.getStore()?.() +} + +/** + * Delivers one input event, recording it on the page's hold: a press counts as held once it may + * have landed, a release only once it surely did. + */ +export function trackRemoteCliInput( webContentsId: number | undefined, method: string, - params?: Record -): void { + params: Record | undefined, + deliver: () => Promise +): Promise { const admission = webContentsId === undefined ? undefined : admissions.get(webContentsId) - if (admission) { + if (!admission || !params) { + return deliver() + } + if (!['mouseReleased', 'keyUp', 'touchEnd', 'touchCancel'].includes(String(params.type))) { trackInput(admission, method, params) + return deliver() } + return deliver().then((result) => { + trackInput(admission, method, params) + return result + }) } -/** True once the host behind this admission check was revoked. */ -export function isAdmissionRevoked(admit: () => void): boolean { +function isRevoked(admit: () => void): boolean { try { admit() return false @@ -94,17 +130,9 @@ async function releaseAll( } } -export function isInputRelease(params: Record): boolean { - return ['mouseReleased', 'keyUp', 'touchEnd', 'touchCancel'].includes(String(params.type)) -} - -function trackInput( - admission: Admission, - method: string | undefined, - params: Record | undefined -): void { - const type = params?.type - if (!params || typeof type !== 'string') { +function trackInput(admission: Admission, method: string, params: Record): void { + const type = params.type + if (typeof type !== 'string') { return } if ( diff --git a/src/main/runtime/desktop-browser-bridge/remote-cli-browser-scope.ts b/src/main/runtime/desktop-browser-bridge/remote-cli-browser-scope.ts index 469f27d07e7a..60cb60147c4b 100644 --- a/src/main/runtime/desktop-browser-bridge/remote-cli-browser-scope.ts +++ b/src/main/runtime/desktop-browser-bridge/remote-cli-browser-scope.ts @@ -61,20 +61,18 @@ export function captureRemoteCliBrowserRevocation(): (webContentsId?: number) => */ export function captureRemoteCliBrowserScope(): { bridged: boolean + /** Fires when the host behind the command is revoked. */ + revoked?: AbortSignal run(work: () => T): T } { const active = scope.getStore() return { bridged: active !== undefined, + revoked: active?.revoked, run: (work) => (active ? scope.run(active, work) : scope.exit(work)) } } -/** The signal that fires when the host behind the current command is revoked, if any. */ -export function captureRemoteCliBrowserRevokedSignal(): AbortSignal | undefined { - return scope.getStore()?.revoked -} - /** Runs `work` in no host's scope, so long-lived resources it creates never inherit one. */ export function runOutsideRemoteCliBrowserScope(work: () => T): T { return scope.exit(work)