fix(pty): preserve persistent bash exit status
This commit is contained in:
@@ -43,6 +43,7 @@ interface RetainedOutput {
|
|||||||
interface CapturedOutput {
|
interface CapturedOutput {
|
||||||
text: string
|
text: string
|
||||||
incomplete: boolean
|
incomplete: boolean
|
||||||
|
exitCode?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PersistentShells {
|
interface PersistentShells {
|
||||||
@@ -94,11 +95,13 @@ function commandOutput(
|
|||||||
): CapturedOutput {
|
): CapturedOutput {
|
||||||
const text = snapshot.text
|
const text = snapshot.text
|
||||||
const end = text.lastIndexOf(marker.end)
|
const end = text.lastIndexOf(marker.end)
|
||||||
|
const exitCode = Number.parseInt(text.slice(end + marker.end.length), 10)
|
||||||
const startMarker = text.lastIndexOf(marker.start, end)
|
const startMarker = text.lastIndexOf(marker.start, end)
|
||||||
const start = startMarker < 0 ? 0 : startMarker + marker.start.length
|
const start = startMarker < 0 ? 0 : startMarker + marker.start.length
|
||||||
return {
|
return {
|
||||||
text: stripPrompt(text.slice(start, end).replace(/^\r?\n/, '')),
|
text: stripPrompt(text.slice(start, end).replace(/^\r?\n/, '')),
|
||||||
incomplete: startMarker < 0,
|
incomplete: startMarker < 0,
|
||||||
|
exitCode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,9 +168,24 @@ function retainedScrollback(
|
|||||||
|
|
||||||
function renderCaptured(output: CapturedOutput, maxOutputChars: number): string {
|
function renderCaptured(output: CapturedOutput, maxOutputChars: number): string {
|
||||||
const rendered = maybeTruncate(output.text, maxOutputChars, output.incomplete)
|
const rendered = maybeTruncate(output.text, maxOutputChars, output.incomplete)
|
||||||
return output.incomplete && output.text.length > 0
|
const withPrefix = output.incomplete && output.text.length > 0
|
||||||
? LOST_PREFIX_MESSAGE + rendered
|
? LOST_PREFIX_MESSAGE + rendered
|
||||||
: rendered
|
: rendered
|
||||||
|
return renderExitStatus(withPrefix, output.exitCode ?? 0, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExitStatus(
|
||||||
|
content: string,
|
||||||
|
exitCode: number | null,
|
||||||
|
signal: NodeJS.Signals | null,
|
||||||
|
): string {
|
||||||
|
const marker = signal !== null
|
||||||
|
? `[killed by signal: ${signal}]`
|
||||||
|
: exitCode !== null && exitCode !== 0
|
||||||
|
? `[exit code: ${exitCode}]`
|
||||||
|
: undefined
|
||||||
|
if (marker === undefined) return content
|
||||||
|
return content.length === 0 ? marker : `${content}\n${marker}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistentShells(ctx: Context, config: ResolvedConfig): PersistentShells {
|
function persistentShells(ctx: Context, config: ResolvedConfig): PersistentShells {
|
||||||
@@ -299,7 +317,11 @@ async function executeCommand(
|
|||||||
const snapshot = retainedScrollback(ctx, owner, id, latest)
|
const snapshot = retainedScrollback(ctx, owner, id, latest)
|
||||||
await shells.reset(owner, 'persistent bash shell exited')
|
await shells.reset(owner, 'persistent bash shell exited')
|
||||||
return [
|
return [
|
||||||
renderCaptured(partialOutput(snapshot, marker, fallback, fallbackTruncated), config.maxOutputChars),
|
renderExitStatus(
|
||||||
|
renderCaptured(partialOutput(snapshot, marker, fallback, fallbackTruncated), config.maxOutputChars),
|
||||||
|
result.sessionStatus.exitCode,
|
||||||
|
result.sessionStatus.signal,
|
||||||
|
),
|
||||||
SHELL_RESET_MESSAGE,
|
SHELL_RESET_MESSAGE,
|
||||||
].filter(part => part.length > 0).join('\n')
|
].filter(part => part.length > 0).join('\n')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,9 +78,11 @@ type StubMode =
|
|||||||
| 'empty-read'
|
| 'empty-read'
|
||||||
| 'stalled-read'
|
| 'stalled-read'
|
||||||
| 'exit'
|
| 'exit'
|
||||||
|
| 'signal-exit'
|
||||||
| 'wait-for-abort'
|
| 'wait-for-abort'
|
||||||
| 'idle-then-normal'
|
| 'idle-then-normal'
|
||||||
| 'large'
|
| 'large'
|
||||||
|
| 'nonzero'
|
||||||
| 'end-only'
|
| 'end-only'
|
||||||
| 'init-exit'
|
| 'init-exit'
|
||||||
| 'init-timeout'
|
| 'init-timeout'
|
||||||
@@ -157,13 +159,18 @@ class StubPtySession implements PtyBackendSession {
|
|||||||
this.scrollback += output
|
this.scrollback += output
|
||||||
return this.operation(Promise.resolve(this.result(output, 'stdin_read')))
|
return this.operation(Promise.resolve(this.result(output, 'stdin_read')))
|
||||||
}
|
}
|
||||||
const commandOutput = this.mode === 'large' ? 'x'.repeat(100) : 'hello from stub'
|
const commandOutput = this.mode === 'large'
|
||||||
const output = `${start ?? ''}\n${commandOutput}\n${end ?? ''}0\n${this.motd}`
|
? 'x'.repeat(100)
|
||||||
|
: this.mode === 'nonzero' ? '' : 'hello from stub'
|
||||||
|
const exitCode = this.mode === 'nonzero' ? 7 : 0
|
||||||
|
const output = `${start ?? ''}\n${commandOutput}\n${end ?? ''}${exitCode}\n${this.motd}`
|
||||||
this.scrollback += output
|
this.scrollback += output
|
||||||
if (this.mode === 'exit') {
|
if (this.mode === 'exit' || this.mode === 'signal-exit') {
|
||||||
const exitedOutput = `${start ?? ''}\nhello from stub\n`
|
const exitedOutput = `${start ?? ''}\nhello from stub\n`
|
||||||
this.scrollback = this.scrollback.slice(0, -output.length) + exitedOutput
|
this.scrollback = this.scrollback.slice(0, -output.length) + exitedOutput
|
||||||
this.statusValue = { kind: 'exited', exitCode: 0, signal: null }
|
this.statusValue = this.mode === 'signal-exit'
|
||||||
|
? { kind: 'exited', exitCode: null, signal: 'SIGTERM' }
|
||||||
|
: { kind: 'exited', exitCode: 9, signal: null }
|
||||||
return this.operation(Promise.resolve(this.result(exitedOutput, 'session_exit')))
|
return this.operation(Promise.resolve(this.result(exitedOutput, 'session_exit')))
|
||||||
}
|
}
|
||||||
return this.operation(Promise.resolve(this.result(output, 'stdin_read')))
|
return this.operation(Promise.resolve(this.result(output, 'stdin_read')))
|
||||||
@@ -303,19 +310,29 @@ describe('tool-bash-persistent', () => {
|
|||||||
session.mode = 'large'
|
session.mode = 'large'
|
||||||
expect(text(await call(ctx, owner, 'large'))).toContain('<response clipped>')
|
expect(text(await call(ctx, owner, 'large'))).toContain('<response clipped>')
|
||||||
|
|
||||||
|
session.mode = 'nonzero'
|
||||||
|
expect(text(await call(ctx, owner, 'false'))).toBe('[exit code: 7]')
|
||||||
|
|
||||||
session.mode = 'exit'
|
session.mode = 'exit'
|
||||||
const exited = text(await call(ctx, owner, 'exit'))
|
const exited = text(await call(ctx, owner, 'exit'))
|
||||||
expect(exited).toContain('hello from')
|
expect(exited).toContain('hello from')
|
||||||
|
expect(exited).toContain('[exit code: 9]')
|
||||||
expect(exited).toContain('next bash call starts from the workspace')
|
expect(exited).toContain('next bash call starts from the workspace')
|
||||||
expect(session.closed).toContain('persistent bash shell exited')
|
expect(session.closed).toContain('persistent bash shell exited')
|
||||||
|
|
||||||
await call(ctx, owner, 'new shell')
|
await call(ctx, owner, 'new shell')
|
||||||
expect(stub.sessions).toHaveLength(2)
|
expect(stub.sessions).toHaveLength(2)
|
||||||
|
const replacement = stub.sessions[1]!
|
||||||
|
replacement.mode = 'signal-exit'
|
||||||
|
expect(text(await call(ctx, owner, 'kill shell'))).toContain('[killed by signal: SIGTERM]')
|
||||||
|
|
||||||
|
await call(ctx, owner, 'another shell')
|
||||||
|
expect(stub.sessions).toHaveLength(3)
|
||||||
const externallyClosed = ctx.pty.list(owner)[0]?.sessionId
|
const externallyClosed = ctx.pty.list(owner)[0]?.sessionId
|
||||||
expect(externallyClosed).toBeDefined()
|
expect(externallyClosed).toBeDefined()
|
||||||
await ctx.pty.kill(owner, externallyClosed!, 'external cleanup')
|
await ctx.pty.kill(owner, externallyClosed!, 'external cleanup')
|
||||||
await fiber.dispose()
|
await fiber.dispose()
|
||||||
expect(stub.sessions[1]?.closed).toEqual(['external cleanup'])
|
expect(stub.sessions[2]?.closed).toEqual(['external cleanup'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('marks a short missing-prefix result and tolerates exhausted scrollback pages', async () => {
|
it('marks a short missing-prefix result and tolerates exhausted scrollback pages', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user