fix(pty): detach cancellation before close drain
This commit is contained in:
@@ -518,13 +518,16 @@ export class LocalPtySession implements PtyBackendSession {
|
|||||||
if (!quiescent) {
|
if (!quiescent) {
|
||||||
throw new Error(`PTY cleanup failed (${reason}); terminal session did not reach quiescence`)
|
throw new Error(`PTY cleanup failed (${reason}); terminal session did not reach quiescence`)
|
||||||
}
|
}
|
||||||
|
// Quiescence is the active send's terminal outcome. Detach its abort
|
||||||
|
// listener before snapshotting provider operations so no late interrupt
|
||||||
|
// can enter the owned set after the drain starts.
|
||||||
|
this.settleActive('session_exit')
|
||||||
await Promise.all(this.terminalOperations)
|
await Promise.all(this.terminalOperations)
|
||||||
// Whole-session cleanup can fail before the top-level process exits. Wait
|
// Whole-session cleanup can fail before the top-level process exits. Wait
|
||||||
// for it first so that failure is reported instead of blocking forever on
|
// for it first so that failure is reported instead of blocking forever on
|
||||||
// `done`; successful quiescence guarantees `done` can now settle status and
|
// `done`; successful quiescence guarantees `done` can now settle status and
|
||||||
// drain the terminal output.
|
// drain the terminal output.
|
||||||
await this.completion
|
await this.completion
|
||||||
this.settleActive('session_exit')
|
|
||||||
this.terminal.output.off('data', this.onTerminalData)
|
this.terminal.output.off('data', this.onTerminalData)
|
||||||
this.terminal.output.off('end', this.onTerminalEnd)
|
this.terminal.output.off('end', this.onTerminalEnd)
|
||||||
this.terminal.output.off('error', this.onTerminalError)
|
this.terminal.output.off('error', this.onTerminalError)
|
||||||
|
|||||||
@@ -954,4 +954,26 @@ describe('LocalPtySession bounds, signals, and teardown', () => {
|
|||||||
expect((await operation.done).waitReason).toBe('session_exit')
|
expect((await operation.done).waitReason).toBe('session_exit')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('detaches active cancellation before draining terminal operations', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
const terminal = new FakeTerminal()
|
||||||
|
const session = new LocalPtySession(terminal, config())
|
||||||
|
await initialize(session, terminal)
|
||||||
|
|
||||||
|
const inspection = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
|
||||||
|
terminal.inspectForeground = async () => await inspection.promise
|
||||||
|
const signalForeground = vi.spyOn(terminal, 'signalForeground')
|
||||||
|
const controller = new AbortController()
|
||||||
|
const operation = session.startSend({ text: 'pending inspection', submit: true, signal: controller.signal })
|
||||||
|
const closing = session.close('pending cancellation')
|
||||||
|
await Promise.resolve()
|
||||||
|
await Promise.resolve()
|
||||||
|
|
||||||
|
controller.abort('late cancellation')
|
||||||
|
expect(signalForeground).not.toHaveBeenCalled()
|
||||||
|
inspection.resolve({ processGroupId: 456, inputWaiting: false })
|
||||||
|
await closing
|
||||||
|
expect((await operation.done).waitReason).toBe('session_exit')
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user