DocsRun commands

Run commands

Run a shell command inside a sandbox and read its result. You can also stream its output as it arrives, keep it running in the background, feed it stdin, and handle a non-zero exit.

Commands run over the sandbox's own connection, not the public REST API, so there is no REST endpoint and no curl example here. Use the TypeScript or Python SDK. Every example reads IMPELLO_API_KEY from the environment; get a key at dashboard.impello.ai/keys.

Run a command

sandbox.commands.run() starts the command, waits for it to exit and returns its stdout, stderr and exit code. Every command runs through /bin/bash -l -c, so pipes, globs, && and shell variables work as they do in a terminal.

import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

const result = await sandbox.commands.run('echo hello && uname -a')
console.log(result.stdout)
console.log(result.exitCode) // 0

await sandbox.kill()

The result has four fields: stdout, stderr, exitCode (exit_code in Python) and an optional error message. Both streams are decoded as UTF-8 and returned as strings.

Options

run() takes these options:

TypeScriptPythonDefaultWhat it does
cwdcwdthe user's homeWorking directory for the command.
useruserthe template's userLinux user to run as — any user that exists in the template. base has user, with home /home/user, and root.
envsenvs{}Environment variables for this command. They override the ones given to the sandbox.
onStdout, onStderron_stdout, on_stderrnoneCallbacks that receive output chunks as they arrive. See Stream output.
stdinstdinfalseKeep stdin open so you can write to it. See Send input.
backgroundbackgroundfalseReturn a handle at once instead of waiting. See Run a command in the background.
timeoutMstimeout60000 ms / 60 sHow long the command may run. When it passes, the sandbox kills the command. 0 removes the limit. See Set a command timeout.

Stream output

Pass onStdout and onStderr to receive output while the command is still running. The callbacks get each chunk as a string. The full output is still collected in the returned result.

import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

await sandbox.commands.run('for i in 1 2 3; do echo $i; sleep 1; done', {
  onStdout: (data) => {
    process.stdout.write(data)
  },
  onStderr: (data) => {
    process.stderr.write(data)
  },
})

await sandbox.kill()

In the Python sync SDK a background handle is also iterable. Each item is a (stdout, stderr, pty) tuple with one field set and the other two None:

handle = sandbox.commands.run("ls -la /", background=True)

for stdout, stderr, _ in handle:
    if stdout is not None:
        print(stdout, end="")

In the sync SDK the on_stdout and on_stderr arguments to run() apply to foreground commands only. For a background command, iterate the handle or pass them to handle.wait(on_stdout=..., on_stderr=...).

With AsyncSandbox, pass on_stdout and on_stderr to run() for both foreground and background commands. AsyncCommandHandle.wait() takes no callbacks.

Run a command in the background

Set background: true and run() returns a CommandHandle immediately. Use the handle to wait for the exit, read the output so far, or kill the process.

import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

const handle = await sandbox.commands.run('sleep 2 && echo done', {
  background: true,
})
console.log(handle.pid)
console.log(handle.exitCode) // undefined while running

const result = await handle.wait()
console.log(result.stdout) // "done\n"

await sandbox.kill()

In TypeScript the handle exposes pid, stdout, stderr, exitCode and error. exitCode is undefined until the command exits. The Python sync handle exposes pid and the methods below; the async handle adds stdout, stderr, exit_code and error, with exit_code None while running.

handle.disconnect() stops the SDK listening without killing the command. To pick it up again, call sandbox.commands.connect(pid) from any client connected to the same sandbox. The new handle's wait() returns the result when the command exits. A disconnected command is still killed when the timeoutMs it was started with passes.

const handle = await sandbox.commands.run('sleep 30 && echo done', {
  background: true,
})
await handle.disconnect()

const again = await sandbox.commands.connect(handle.pid, {
  onStdout: (data) => console.log(data),
})
await again.wait()

Set a command timeout

A command may run for 60 seconds by default. When the limit passes, the sandbox kills the command and run() or wait() throws TimeoutError (TimeoutException in Python). To run a longer command, raise the limit with timeoutMs (timeout in Python, in seconds) to cover the longest run you expect. background: true does not change this. A background command is killed at the same limit, even if nothing is waiting on it, so pass the option there too.

const result = await sandbox.commands.run('npm install', {
  timeoutMs: 10 * 60 * 1000,
})

timeoutMs: 0 in TypeScript and timeout=0 in Python both remove the limit. The command then runs until it exits or the sandbox ends.

connect() also has a 60 second default and takes the same option. On connect() the value bounds only how long the new handle listens. When it passes, wait() throws TimeoutError and the command keeps running.

The command timeout is separate from the sandbox timeout. A sandbox that reaches its own limit is killed with everything running inside it, unless its lifecycle pauses it instead. A wait() still pending on one of its commands then throws TimeoutError. See Timeouts.

Send input

Start the command with stdin: true, write to it with sendStdin(), and call closeStdin() to send EOF when you are done. Both methods also exist on sandbox.commands taking a pid, for a command you did not start from this handle.

import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

const handle = await sandbox.commands.run('cat', {
  background: true,
  stdin: true,
})
await handle.sendStdin('hello from stdin\n')
await handle.closeStdin()

const result = await handle.wait()
console.log(result.stdout) // "hello from stdin\n"

await sandbox.kill()

The data may be a string or raw bytes — a Uint8Array in TypeScript, bytes in Python. A handle for a pseudo-terminal supports neither method; use the Interactive terminal module for that instead.

List and kill processes

sandbox.commands.list() returns every running command and PTY session as a ProcessInfo with pid, cmd, args, envs, and optional cwd and tag. Because every command runs through bash, cmd is /bin/bash and your command line is the last element of args.

sandbox.commands.kill(pid) sends SIGKILL. It returns true when the signal was sent and false when no process had that pid. handle.kill() does the same for the handle's own process.

import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

await sandbox.commands.run('sleep 600', { background: true })

for (const proc of await sandbox.commands.list()) {
  console.log(proc.pid, proc.args.at(-1))
  if (proc.args.at(-1) === 'sleep 600') {
    console.log(await sandbox.commands.kill(proc.pid)) // true
  }
}

await sandbox.kill()

Handle a non-zero exit

A command that exits with anything other than 0 makes run() and wait() throw CommandExitError (CommandExitException in Python). The error carries the same exitCode, stdout and stderr fields as a result, so you can read what went wrong.

import { CommandExitError, Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

try {
  await sandbox.commands.run('ls /does-not-exist')
} catch (err) {
  if (err instanceof CommandExitError) {
    console.log(err.exitCode) // 2
    console.log(err.stderr)
  } else {
    throw err
  }
}

await sandbox.kill()

In Python, str(e) reads Command exited with code 2 and error: followed by stderr. The other classes a command can raise are listed on the Errors page.

Next steps