Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions packages/cli/src/commands/sandbox/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
* Execute a command in a running sandbox.
*/

import { Sandbox, CommandExitError, SandboxNotFoundError } from '@abox-dev/sdk'
import {
Sandbox,
CommandExitError,
CommandNotFoundError,
SandboxNotFoundError,
} from '@abox-dev/sdk'
import * as commander from 'commander'

import { ensureAPIKey } from '../../api'
Expand Down Expand Up @@ -182,7 +187,10 @@ async function sendStdin(sandbox: Sandbox, pid: number): Promise<void> {
try {
await sandbox.commands.sendStdin(pid, chunk)
} catch (err) {
if (err instanceof SandboxNotFoundError) {
if (
err instanceof CommandNotFoundError ||
err instanceof SandboxNotFoundError
) {
processExited = true
console.error(
'agentbox: Remote command exited before stdin could be delivered.'
Expand All @@ -203,7 +211,10 @@ async function sendStdin(sandbox: Sandbox, pid: number): Promise<void> {
try {
await sandbox.commands.closeStdin(pid)
} catch (err) {
if (err instanceof SandboxNotFoundError) {
if (
err instanceof CommandNotFoundError ||
err instanceof SandboxNotFoundError
) {
// Process already exited — EOF is moot.
return
}
Expand Down
14 changes: 8 additions & 6 deletions packages/cli/tests/commands/sandbox/exec_close_stdin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ vi.mock('@abox-dev/sdk', () => {
}

class SandboxNotFoundError extends Error {}
class CommandNotFoundError extends Error {}

return {
Sandbox: {
connect: mocks.connect,
},
CommandExitError,
SandboxNotFoundError,
CommandNotFoundError,
}
})

Expand Down Expand Up @@ -131,10 +133,10 @@ describe('sandbox exec closeStdin handling', () => {
expect(exitSpy).toHaveBeenCalledWith(1)
})

test('keeps SandboxNotFoundError from closeStdin non-fatal', async () => {
const { SandboxNotFoundError } = await import('@abox-dev/sdk')
test('keeps CommandNotFoundError from closeStdin non-fatal', async () => {
const { CommandNotFoundError } = await import('@abox-dev/sdk')
mocks.closeStdin.mockRejectedValue(
new SandboxNotFoundError('already exited')
new CommandNotFoundError('already exited')
)

const exitSpy = vi
Expand All @@ -153,10 +155,10 @@ describe('sandbox exec closeStdin handling', () => {
expect(exitSpy).toHaveBeenCalledWith(0)
})

test('stops stdin streaming after SandboxNotFoundError from sendStdin', async () => {
const { SandboxNotFoundError } = await import('@abox-dev/sdk')
test('stops stdin streaming after CommandNotFoundError from sendStdin', async () => {
const { CommandNotFoundError } = await import('@abox-dev/sdk')
mocks.sendStdin.mockRejectedValueOnce(
new SandboxNotFoundError('already exited')
new CommandNotFoundError('already exited')
)
mocks.streamStdinChunks.mockImplementation(
async (
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/tests/commands/sandbox/exec_passthrough.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ vi.mock('@abox-dev/sdk', () => {
}

class SandboxNotFoundError extends Error {}
class CommandNotFoundError extends Error {}

return {
Sandbox: {
connect: mocks.connect,
},
CommandExitError,
SandboxNotFoundError,
CommandNotFoundError,
}
})

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/tests/commands/sandbox/exec_stdin_flag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ vi.mock('@abox-dev/sdk', () => {
}

class SandboxNotFoundError extends Error {}
class CommandNotFoundError extends Error {}

return {
Sandbox: {
connect: mocks.connect,
},
CommandExitError,
SandboxNotFoundError,
CommandNotFoundError,
}
})

Expand Down
10 changes: 10 additions & 0 deletions packages/js-sdk/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ export class SandboxNotFoundError extends SandboxError {
}
}

/**
* Thrown when a command process has already exited or its PID does not exist.
*/
export class CommandNotFoundError extends SandboxError {
constructor(message: string) {
super(message)
this.name = 'CommandNotFoundError'
}
}

/**
* Thrown when authentication fails.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/js-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {
NotEnoughSpaceError,
SandboxError,
SandboxNotFoundError,
CommandNotFoundError,
TemplateError,
TimeoutError,
RateLimitError,
Expand Down
10 changes: 7 additions & 3 deletions packages/js-sdk/src/sandbox/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
SandboxHealthCheck,
} from '../../envd/rpc'
import { ENVD_COMMANDS_STDIN, ENVD_ENVD_CLOSE } from '../../envd/versions'
import { SandboxError } from '../../errors'
import { CommandNotFoundError, SandboxError } from '../../errors'
import { CommandHandle, CommandResult } from './commandHandle'
export { Pty } from './pty'

Expand Down Expand Up @@ -228,7 +228,9 @@ export class Commands {
}
)
} catch (err) {
throw await handleRpcErrorWithHealthCheck(err, this.checkHealth)
throw await handleRpcErrorWithHealthCheck(err, this.checkHealth, {
[Code.NotFound]: (message) => new CommandNotFoundError(message),
})
}
}

Expand Down Expand Up @@ -265,7 +267,9 @@ export class Commands {
}
)
} catch (err) {
throw await handleRpcErrorWithHealthCheck(err, this.checkHealth)
throw await handleRpcErrorWithHealthCheck(err, this.checkHealth, {
[Code.NotFound]: (message) => new CommandNotFoundError(message),
})
}
}

Expand Down
32 changes: 32 additions & 0 deletions packages/js-sdk/tests/envd/commandProcessNotFound.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Code, ConnectError } from '@connectrpc/connect'
import { describe, expect, it, vi } from 'vitest'

import { CommandNotFoundError } from '../../src/errors'
import { Commands } from '../../src/sandbox/commands'

function commandsWithMissingProcess(): Commands {
const missing = () =>
Promise.reject(new ConnectError('process not found', Code.NotFound))
const commands = Object.create(Commands.prototype) as Commands
Object.assign(commands, {
rpc: { sendInput: vi.fn(missing), closeStdin: vi.fn(missing) },
envdVersion: '0.6.15',
connectionConfig: { getSignal: () => undefined },
checkHealth: async () => true,
})
return commands
}

describe('command process disappearance', () => {
it('identifies a missing PID while sending stdin', async () => {
await expect(
commandsWithMissingProcess().sendStdin(1234, 'input')
).rejects.toBeInstanceOf(CommandNotFoundError)
})

it('identifies a missing PID while closing stdin', async () => {
await expect(
commandsWithMissingProcess().closeStdin(1234)
).rejects.toBeInstanceOf(CommandNotFoundError)
})
})
Loading