refactor(runtime): remove duplicate terminal ownership state

This commit is contained in:
Tianyi Cui
2026-07-30 05:19:53 +08:00
parent 87b2c5d93f
commit c1d08edd83
3 changed files with 9 additions and 13 deletions

View File

@@ -167,7 +167,7 @@ export class LocalPtySession implements PtyBackendSession {
private activeDeadlineTimer: NodeJS.Timeout | undefined private activeDeadlineTimer: NodeJS.Timeout | undefined
private activeAbort: (() => void) | undefined private activeAbort: (() => void) | undefined
private interrupting: LocalSendOperation | undefined private interrupting: LocalSendOperation | undefined
private activeWrite: { operation: LocalSendOperation; settled: Promise<boolean> } | undefined private activeWrite: Promise<boolean> | undefined
private pollingReady: LocalSendOperation | undefined private pollingReady: LocalSendOperation | undefined
private polling = false private polling = false
private promptSeen = false private promptSeen = false
@@ -238,7 +238,7 @@ export class LocalPtySession implements PtyBackendSession {
} }
this.activeDeadlineTimer = setTimeout(() => { this.activeDeadlineTimer = setTimeout(() => {
if (this.active === operation) { if (this.active === operation) {
this.settleActive('timeout', this.activeWrite?.operation === operation || this.interrupting === operation) this.settleActive('timeout', this.activeWrite !== undefined || this.interrupting === operation)
} }
}, this.config.timeoutMs) }, this.config.timeoutMs)
void this.beginSend(operation, request) void this.beginSend(operation, request)
@@ -254,11 +254,7 @@ export class LocalPtySession implements PtyBackendSession {
if (input.length > 0 && !operation.cancelRequested) { if (input.length > 0 && !operation.cancelRequested) {
this.resetReadinessEvidence() this.resetReadinessEvidence()
const write = this.terminal.write(input) const write = this.terminal.write(input)
const activeWrite = { this.activeWrite = write.then(() => true, () => false)
operation,
settled: write.then(() => true, () => false),
}
this.activeWrite = activeWrite
try { try {
await write await write
} finally { } finally {
@@ -272,7 +268,7 @@ export class LocalPtySession implements PtyBackendSession {
return return
} }
// Closing can race the awaited provider write even though static analysis sees only local assignments. // Closing can race the awaited provider write even though static analysis sees only local assignments.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // oxlint-disable-next-line typescript/no-unnecessary-condition -- awaited provider writes can close the session.
if (this.active === operation && !this.closing) { if (this.active === operation && !this.closing) {
this.pollingReady = operation this.pollingReady = operation
this.schedulePoll(operation) this.schedulePoll(operation)
@@ -448,7 +444,7 @@ export class LocalPtySession implements PtyBackendSession {
this.polling = false this.polling = false
const active = this.active const active = this.active
// Awaited provider inspection can clear or replace the active send despite static analysis. // Awaited provider inspection can clear or replace the active send despite static analysis.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // oxlint-disable-next-line typescript/no-unnecessary-condition -- awaited inspection can replace the active send.
if (active !== undefined && this.pollingReady === active) this.schedulePoll(active) if (active !== undefined && this.pollingReady === active) this.schedulePoll(active)
} }
} }
@@ -506,7 +502,7 @@ export class LocalPtySession implements PtyBackendSession {
private async interruptOnce(operation: LocalSendOperation): Promise<void> { private async interruptOnce(operation: LocalSendOperation): Promise<void> {
try { try {
const activeWrite = this.activeWrite const activeWrite = this.activeWrite
if (activeWrite?.operation === operation && !await activeWrite.settled) return if (activeWrite !== undefined && !await activeWrite) return
await this.terminal.signalForeground('SIGINT') await this.terminal.signalForeground('SIGINT')
} catch (error: unknown) { } catch (error: unknown) {
if (this.active === operation && !this.closing) this.onTransportFailure(error) if (this.active === operation && !this.closing) this.onTransportFailure(error)

View File

@@ -58,19 +58,18 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
exitCode: exitSignal === undefined || exitSignal === 0 ? exitCode : null, exitCode: exitSignal === undefined || exitSignal === 0 ? exitCode : null,
signal: signalName(exitSignal), signal: signalName(exitSignal),
}) })
void this.terminate().catch(() => {})
}) })
} }
// node-pty writes synchronously; the seam returns a promise for remote transports. // node-pty writes synchronously; the seam returns a promise for remote transports.
// eslint-disable-next-line @typescript-eslint/require-await // oxlint-disable-next-line typescript/require-await -- Preserve promise rejection semantics at the async provider seam.
async write(data: string): Promise<void> { async write(data: string): Promise<void> {
if (this.exited) throw new Error('terminal process has exited') if (this.exited) throw new Error('terminal process has exited')
this.terminal.write(data) this.terminal.write(data)
} }
// Local inspection is synchronous; the seam returns a promise for remote transports. // Local inspection is synchronous; the seam returns a promise for remote transports.
// eslint-disable-next-line @typescript-eslint/require-await // oxlint-disable-next-line typescript/require-await -- Preserve promise rejection semantics at the async provider seam.
async inspectForeground(): Promise<SubprocessTerminalForeground | undefined> { async inspectForeground(): Promise<SubprocessTerminalForeground | undefined> {
this.descendants() this.descendants()
const processGroupId = this.inspector.foregroundPgid(this.pid) const processGroupId = this.inspector.foregroundPgid(this.pid)

View File

@@ -128,6 +128,7 @@ describe('LocalTerminalHandle', () => {
const handle = new LocalTerminalHandle(pty.asPty(), inspector, 20) const handle = new LocalTerminalHandle(pty.asPty(), inspector, 20)
const quiescent = handle.terminate() const quiescent = handle.terminate()
expect(handle.terminate()).toBe(quiescent)
await vi.advanceTimersByTimeAsync(20) await vi.advanceTimersByTimeAsync(20)
expect(inspector.processes).toContainEqual([124, 'SIGKILL']) expect(inspector.processes).toContainEqual([124, 'SIGKILL'])
expect(pty.kills).toEqual([]) expect(pty.kills).toEqual([])