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)