mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
fix(pty): clean up failed post-spawn setup
This commit is contained in:
@@ -6129,13 +6129,22 @@ describe('registerPtyHandlers', () => {
|
||||
// awaits a promise that never settles and the tab hangs forever.
|
||||
registerPtyHandlers(mainWindow as never)
|
||||
const leafId = '44444444-4444-4444-8444-444444444444'
|
||||
const spawnArgs = { cols: 80, rows: 24, tabId: 'tab-reservation', leafId }
|
||||
const paneKey = makePaneKey('tab-reservation', leafId)
|
||||
const spawnArgs = {
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
tabId: 'tab-reservation',
|
||||
leafId,
|
||||
env: { ORCA_PANE_KEY: paneKey, ORCA_TAB_ID: 'tab-reservation' }
|
||||
}
|
||||
|
||||
registerPtyMock.mockImplementationOnce(() => {
|
||||
throw new Error('boom: post-spawn registration failed')
|
||||
})
|
||||
|
||||
await expect(handlers.get('pty:spawn')!(null, spawnArgs)).rejects.toThrow('boom')
|
||||
const orphanedPtyId = (registerPtyMock.mock.calls[0]![0] as { ptyId: string }).ptyId
|
||||
expect(unregisterPtyMock).toHaveBeenCalledWith(orphanedPtyId)
|
||||
|
||||
// A second spawn for the same pane must run a fresh spawn rather than await
|
||||
// the leaked (never-settled) reservation promise.
|
||||
@@ -6175,12 +6184,16 @@ describe('registerPtyHandlers', () => {
|
||||
}
|
||||
let spawnCount = 0
|
||||
const providerSpawn = vi.fn(async () => ({ id: `pty-${++spawnCount}` }))
|
||||
const shutdown = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('native shutdown failed'))
|
||||
.mockResolvedValue(undefined)
|
||||
setLocalPtyProvider({
|
||||
spawn: providerSpawn,
|
||||
write: vi.fn(),
|
||||
resize: vi.fn(),
|
||||
kill: vi.fn(),
|
||||
shutdown: vi.fn(),
|
||||
shutdown,
|
||||
sendSignal: vi.fn(),
|
||||
getCwd: vi.fn(),
|
||||
getInitialCwd: vi.fn(),
|
||||
@@ -6199,7 +6212,9 @@ describe('registerPtyHandlers', () => {
|
||||
getProfiles: vi.fn()
|
||||
} as never)
|
||||
const store = {
|
||||
persistPtyBinding: vi.fn()
|
||||
persistPtyBinding: vi.fn(),
|
||||
upsertPendingLocalPtyShutdown: vi.fn(),
|
||||
removePendingLocalPtyShutdown: vi.fn()
|
||||
}
|
||||
let controller: RuntimeSpawnController | null = null
|
||||
const runtime = {
|
||||
@@ -6240,6 +6255,21 @@ describe('registerPtyHandlers', () => {
|
||||
}
|
||||
|
||||
await expect(spawnController.spawn(spawnArgs)).rejects.toThrow('boom')
|
||||
expect(shutdown).toHaveBeenCalledWith('pty-1', {
|
||||
immediate: true,
|
||||
expectedPaneKey: paneKey,
|
||||
expectedTabId: 'tab-runtime-reservation'
|
||||
})
|
||||
expect(store.upsertPendingLocalPtyShutdown).toHaveBeenCalledWith({
|
||||
ptyId: 'pty-1',
|
||||
expectedPaneKey: paneKey,
|
||||
expectedTabId: 'tab-runtime-reservation',
|
||||
requestedAt: expect.any(Number)
|
||||
})
|
||||
expect(store.removePendingLocalPtyShutdown).not.toHaveBeenCalled()
|
||||
await vi.waitFor(() => expect(shutdown).toHaveBeenCalledTimes(2))
|
||||
expect(store.removePendingLocalPtyShutdown).toHaveBeenCalledWith('pty-1')
|
||||
expect(unregisterPtyMock).toHaveBeenCalledWith('pty-1')
|
||||
|
||||
// The reservation must be gone, so a second materialization runs a fresh
|
||||
// provider.spawn instead of awaiting the leaked promise.
|
||||
|
||||
+36
-2
@@ -3732,7 +3732,8 @@ export function registerPtyHandlers(
|
||||
const paneSpawnReservation = materializedPaneKey
|
||||
? reservePaneSpawn(materializedPaneKey)
|
||||
: null
|
||||
let result: PtySpawnResult
|
||||
let result: PtySpawnResult | undefined
|
||||
let failedSpawnCleanupAttempted = false
|
||||
try {
|
||||
try {
|
||||
if (args.preAllocatedHandle) {
|
||||
@@ -3829,6 +3830,7 @@ export function registerPtyHandlers(
|
||||
// writes, but the pre-existing provider session remains owned.
|
||||
deletePtyOwnership(result.id)
|
||||
} else {
|
||||
failedSpawnCleanupAttempted = true
|
||||
if (args.connectionId) {
|
||||
// Why: the binding failed, so the SSH lease is the only durable
|
||||
// identity that can carry cleanup across an app restart.
|
||||
@@ -3911,6 +3913,21 @@ export function registerPtyHandlers(
|
||||
const response = { id: result.id }
|
||||
return resolvePaneSpawnReservation(materializedPaneKey, paneSpawnReservation, response)
|
||||
} catch (err) {
|
||||
if (result?.isReattach) {
|
||||
// Why: this request published only routing ownership for an existing
|
||||
// provider session; failed setup must not kill that session.
|
||||
deletePtyOwnership(result.id)
|
||||
} else if (result && !failedSpawnCleanupAttempted) {
|
||||
try {
|
||||
await retainFailedSpawnCleanup(result.id, args.connectionId, provider, {
|
||||
immediate: true,
|
||||
...(metadataPaneKey ? { expectedPaneKey: metadataPaneKey } : {}),
|
||||
...(typeof args.tabId === 'string' ? { expectedTabId: args.tabId } : {})
|
||||
})
|
||||
} catch (shutdownErr) {
|
||||
console.warn('[pty] failed to clean up PTY after post-spawn failure:', shutdownErr)
|
||||
}
|
||||
}
|
||||
// Why: once the reservation is created, any later throw — spawn
|
||||
// failure, persist failure, or a post-spawn helper such as
|
||||
// registerPty/rememberPaneKeyForPty/track — must settle it. Otherwise
|
||||
@@ -4642,7 +4659,8 @@ export function registerPtyHandlers(
|
||||
if (preSpawnHiddenMarkId !== null) {
|
||||
markHiddenRendererPty(preSpawnHiddenMarkId)
|
||||
}
|
||||
let result: PtySpawnResult
|
||||
let result: PtySpawnResult | undefined
|
||||
let failedSpawnCleanupAttempted = false
|
||||
try {
|
||||
try {
|
||||
if (preAllocatedHandle) {
|
||||
@@ -4826,6 +4844,7 @@ export function registerPtyHandlers(
|
||||
} catch (err) {
|
||||
console.error('[pty] failed to persist PTY binding after spawn:', err)
|
||||
if (!result.isReattach) {
|
||||
failedSpawnCleanupAttempted = true
|
||||
try {
|
||||
await retainFailedSpawnCleanup(result.id, args.connectionId, provider, {
|
||||
immediate: true,
|
||||
@@ -5030,6 +5049,21 @@ export function registerPtyHandlers(
|
||||
}
|
||||
return resolvePaneSpawnReservation(reservationPaneKey, paneSpawnReservation, response)
|
||||
} catch (err) {
|
||||
if (result?.isReattach) {
|
||||
// Why: this request published only routing ownership for an existing
|
||||
// provider session; failed setup must not kill that session.
|
||||
deletePtyOwnership(result.id)
|
||||
} else if (result && !failedSpawnCleanupAttempted) {
|
||||
try {
|
||||
await retainFailedSpawnCleanup(result.id, args.connectionId, provider, {
|
||||
immediate: true,
|
||||
...(validatedPaneKey ? { expectedPaneKey: validatedPaneKey } : {}),
|
||||
...(typeof args.tabId === 'string' ? { expectedTabId: args.tabId } : {})
|
||||
})
|
||||
} catch (shutdownErr) {
|
||||
console.warn('[pty] failed to clean up PTY after post-spawn failure:', shutdownErr)
|
||||
}
|
||||
}
|
||||
// Why: once the reservation is created, any later throw —
|
||||
// spawn failure, persist failure, or a post-spawn helper such as
|
||||
// seedHeadlessTerminal/registerPty/track — must settle it. Otherwise
|
||||
|
||||
Reference in New Issue
Block a user