mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 16:02:45 +00:00
fix(daemon): release terminal attach cancellation listeners (#23191)
* fix(daemon): release terminal attach cancellation listeners * fix(daemon): preserve attach wait settlement ordering * fix(i18n): register existing diff note fallback strings --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { waitForTerminalAttachOperation } from './terminal-attach-cancellation'
|
||||
|
||||
describe('terminal attach cancellation', () => {
|
||||
it('removes cancellation listeners when the operation settles first', async () => {
|
||||
const controller = new AbortController()
|
||||
const removeListener = vi.spyOn(controller.signal, 'removeEventListener')
|
||||
|
||||
await expect(
|
||||
waitForTerminalAttachOperation(Promise.resolve('ready'), controller.signal, 'session-1')
|
||||
).resolves.toBe('ready')
|
||||
|
||||
expect(removeListener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('preserves operation-first ordering when settlement and abort share a turn', async () => {
|
||||
const controller = new AbortController()
|
||||
const operation = Promise.withResolvers<string>()
|
||||
const waiting = waitForTerminalAttachOperation(
|
||||
operation.promise,
|
||||
controller.signal,
|
||||
'session-3'
|
||||
)
|
||||
operation.resolve('ready')
|
||||
controller.abort()
|
||||
await expect(waiting).resolves.toBe('ready')
|
||||
})
|
||||
|
||||
it('rejects promptly on cancellation while the operation remains pending', async () => {
|
||||
const controller = new AbortController()
|
||||
const operation = new Promise<void>(() => {})
|
||||
const waiting = waitForTerminalAttachOperation(operation, controller.signal, 'session-2')
|
||||
|
||||
controller.abort()
|
||||
|
||||
await expect(waiting).rejects.toMatchObject({ name: 'TerminalAttachCanceledError' })
|
||||
})
|
||||
})
|
||||
@@ -1,17 +1,17 @@
|
||||
import { TerminalAttachCanceledError } from './daemon-errors'
|
||||
import { PromiseSettlementWaiters } from '../../shared/promise-settlement-waiters'
|
||||
|
||||
/** Never resolves; only rejects, so it can bound a wait without settling it. */
|
||||
export function rejectOnAbort(signal: AbortSignal | undefined, sessionId: string): Promise<never> {
|
||||
export function waitForTerminalAttachOperation<T>(
|
||||
operation: Promise<T>,
|
||||
signal: AbortSignal | undefined,
|
||||
sessionId: string
|
||||
): Promise<T> {
|
||||
if (!signal) {
|
||||
return new Promise<never>(() => {})
|
||||
return operation
|
||||
}
|
||||
return new Promise<never>((_resolve, reject) => {
|
||||
if (signal.aborted) {
|
||||
reject(new TerminalAttachCanceledError(sessionId))
|
||||
return
|
||||
}
|
||||
signal.addEventListener('abort', () => reject(new TerminalAttachCanceledError(sessionId)), {
|
||||
once: true
|
||||
})
|
||||
return new PromiseSettlementWaiters(operation).wait({
|
||||
signal,
|
||||
abortInMicrotask: true,
|
||||
createAbortError: () => new TerminalAttachCanceledError(sessionId)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { TerminalHostTombstones } from './terminal-host-tombstones'
|
||||
import type { TerminalSessionTeardown } from './terminal-session-teardown'
|
||||
import { resolveDaemonSessionScrollbackRows } from './daemon-session-scrollback-window'
|
||||
import { TerminalAttachCanceledError } from './daemon-errors'
|
||||
import { rejectOnAbort } from './terminal-attach-cancellation'
|
||||
import { waitForTerminalAttachOperation } from './terminal-attach-cancellation'
|
||||
import { SessionNotFoundError } from './types'
|
||||
import { resolveWslSessionContext } from './wsl-session-context'
|
||||
|
||||
@@ -47,10 +47,11 @@ export async function createOrAttachTerminalSession(
|
||||
// reaches this a beat after the attach that retired it, and refusing surfaced the raw
|
||||
// SessionNotFoundError to the user. Windows makes it the common case, where the plain-shell
|
||||
// sweep holds the claim across an OS identity probe and taskkill (#18046).
|
||||
await Promise.race([
|
||||
await waitForTerminalAttachOperation(
|
||||
deps.sessionTeardown.settle(opts.sessionId),
|
||||
rejectOnAbort(opts.cancelSignal, opts.sessionId)
|
||||
])
|
||||
opts.cancelSignal,
|
||||
opts.sessionId
|
||||
)
|
||||
deps.assertCreateAllowed()
|
||||
existing = deps.sessions.get(opts.sessionId)
|
||||
// Unkillable child, or a fresh teardown claimed it while we waited: still nobody's to recreate.
|
||||
|
||||
@@ -21,7 +21,7 @@ import { TerminalHostTombstones } from './terminal-host-tombstones'
|
||||
import { listLiveTerminalHostSessions } from './terminal-host-session-listing'
|
||||
import { createOrAttachTerminalSession } from './terminal-host-session-create'
|
||||
import { TerminalAttachCanceledError } from './daemon-errors'
|
||||
import { rejectOnAbort } from './terminal-attach-cancellation'
|
||||
import { waitForTerminalAttachOperation } from './terminal-attach-cancellation'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { pruneRetiredPtyIncarnations } from '../../shared/retired-pty-incarnations'
|
||||
import {
|
||||
@@ -84,7 +84,7 @@ export class TerminalHost {
|
||||
// Why: the create ahead of us can be stuck on an unreachable share for
|
||||
// minutes. Waiting unconditionally is what let one dead path strand every
|
||||
// later create and attach for the session, so a canceled caller leaves.
|
||||
await Promise.race([inFlight, rejectOnAbort(opts.cancelSignal, opts.sessionId)])
|
||||
await waitForTerminalAttachOperation(inFlight, opts.cancelSignal, opts.sessionId)
|
||||
this.assertCreateOrAttachAllowed(opts)
|
||||
}
|
||||
this.assertCreateOrAttachAllowed(opts)
|
||||
|
||||
Reference in New Issue
Block a user