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:
| TypeScript | Python | Default | What it does |
|---|---|---|---|
cwd | cwd | the user's home | Working directory for the command. |
user | user | the template's user | Linux user to run as — any user that exists in the template. base has user, with home /home/user, and root. |
envs | envs | {} | Environment variables for this command. They override the ones given to the sandbox. |
onStdout, onStderr | on_stdout, on_stderr | none | Callbacks that receive output chunks as they arrive. See Stream output. |
stdin | stdin | false | Keep stdin open so you can write to it. See Send input. |
background | background | false | Return a handle at once instead of waiting. See Run a command in the background. |
timeoutMs | timeout | 60000 ms / 60 s | How 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
- Interactive terminal for a shell you drive keystroke by keystroke.
- Filesystem to read and write the files a command produces.