Skip to content
GigadriveDocs

Commands

Run commands in a microVM and wait for them, or detach them, stream their output, send input and signals, and open an interactive terminal.

A command is a process you start in a running microVM. Wait for it when you need its result before moving on, such as npm install, and detach it when it keeps running, such as a dev server.

Run a command and wait

vm.run starts a command and resolves when it exits. A non-zero exit code is a normal result; pass throwOnError: true to reject with CommandFailedError instead. Over REST, POST /microvms/{id}/commands waits by default and answers with the result.

import { MicroVM } from '@gigadrive/sdk';

await using vm = await MicroVM.create({ image: 'node22' });

const install = await vm.run('npm', ['install', 'left-pad'], {
  cwd: '/tmp',
  env: { CI: '1' },
  timeout: '2m',
  onStdout: (text) => process.stdout.write(text),
  onStderr: (text) => process.stderr.write(text),
});

if (install.exitCode !== 0) {
  throw new Error(`npm install failed: ${install.stderr}`);
}

With args, the command runs directly and is looked up on PATH, with no shell involved. Without args, a cmd that contains a space runs through /bin/sh -c, so pipes, redirects and && work:

await vm.run('ls -la | wc -l');

Waiting has a time limit

A command you wait for over REST runs for at most 60 seconds unless you set timeoutMs, and for at most 10 minutes whatever you set. The response carries at most 1 MiB of each of standard output and standard error, and sets truncated: true when it left output out.

vm.run makes that same waiting request unless you pass onStdout, onStderr, stdin or signal. With any of those, it starts the command detached and streams its output instead, so neither limit applies. For a job that can run longer than 10 minutes, pass an output callback, or start it with vm.spawn as described in Detached commands.

Options

TypeScriptREST fieldDefaultEffect
cwdcwdThe image's working directoryDirectory the command starts in
envenvNoneVariables added on top of the image's and the microVM's environment
useruserThe image's user, else rootA user name from the image, root, or a numeric user ID
sudononefalseRun as root. Shorthand for user: 'root'
timeouttimeoutMsNone, see the previous noteKill the command with SIGKILL after this long
onStdoutnoneNoneCalled with each chunk of standard output as text, as it arrives
onStderrnoneNoneCalled with each chunk of standard error as text, as it arrives
stdinstdinNoneSee Standard input
ptyptyNoneRun in a terminal of cols by rows characters. vm.spawn only
throwOnErrornonefalseReject with CommandFailedError on a non-zero exit code. vm.run only
signalnoneNoneAn AbortSignal. Aborting kills the command with SIGKILL. vm.run only
nonewaittruefalse answers 201 as soon as the command starts

In TypeScript, timeout takes milliseconds or a string such as '30s', '10m' or '2h'.

Commands run as the image's configured user unless you pass user. root is allowed, because the microVM itself is the isolation boundary.

Exit codes and timeouts

A finished command reports exitCode, and signal when a signal ended it. A process killed by a signal has exitCode: null. A command that reaches its timeout is killed with SIGKILL and reports signal: "SIGKILL" and timedOut: true.

A command that runs past the microVM's own timeout ends when the microVM stops. Extend the microVM first for long jobs; see Limits and pricing.

Detached commands

vm.spawn starts a command and returns a Command without waiting. Over REST, send "wait": false and the response is 201 with the command.

// An app you uploaded to /srv/app earlier, see Files.
const server = await vm.spawn('npm', ['run', 'dev'], { cwd: '/srv/app' });

await vm.waitForPort(3000);

// Later: stop the server and wait for it to exit.
await server.kill();
const finished = await server.wait();

console.log(finished.exitCode, finished.signal);

A detached command belongs to the microVM, not to your connection. Closing your process, or losing the network, does not stop it. Reattach to one later with vm.commands.get(commandId), or list them with vm.commands.list().

Over REST, list a microVM's commands with GET /microvms/{id}/commands, read one with GET /microvms/{id}/commands/{commandId}, and wait for one with POST /microvms/{id}/commands/{commandId}/wait. A single wait call returns after at most 60 seconds, with status: "running" if the command has not exited yet; call it again to keep waiting.

The Command object carries id, cmd, args, cwd, user, status (running or exited), exitCode, signal, timedOut, pid, pty, startedAt and finishedAt.

Stream output

command.logs() streams a command's output as it is written, starting with what it has printed so far. Each chunk carries stream (stdout or stderr), text and offset. command.output() collects everything until the command exits.

for await (const { stream, text } of server.logs()) {
  (stream === 'stdout' ? process.stdout : process.stderr).write(text);
}

Over REST, GET /microvms/{id}/commands/{commandId}/logs is a Server-Sent Events stream:

curl -N \
  -H "Authorization: Bearer $GIGADRIVE_TOKEN" \
  -H "Accept: text/event-stream" \
  "https://api.gigadrive.network/microvms/0199a7c4-5b1e-7c3d-9f20-333333333333/commands/cmd_0k3x9q2m7a1bz4yd/logs"
EventDataMeaning
stdout{ data, offset }A chunk of standard output. data is base64
stderr{ data, offset }A chunk of standard error. data is base64
truncated{}Part of the requested output is no longer kept
exit{ exitCode, signal, timedOut, finishedAt }The command exited. The stream ends after this event
end{ reason: "snapshot" }With follow=false, the command is still running and the stream ends
error{ retryable: true }The stream failed partway. Reconnect from the last offsets

offset is the position of the chunk's first byte in its stream. To resume after a dropped connection, pass the next byte's position as stdoutOffset and stderrOffset. follow=false sends what is available now and closes instead of waiting for the command to exit. The SDK reconnects on its own.

The number of log streams open at once is limited per credential and per organization. Past the limit, the call answers 429 with a retry-after header.

Output retention

Each command keeps the last 8 MiB of standard output and the last 8 MiB of standard error. Older output is discarded, so a stream that starts late, or an offset that is too old, begins at the oldest byte still kept.

A microVM keeps its 256 most recent commands. When it is full, the oldest exited command is forgotten to make room. If all 256 are still running, starting another one fails with 413.

Standard input

Pass stdin: true to vm.spawn to keep the command's input open, then write to it with write(). end() closes the input, which most programs read as the end of their data. For vm.run, stdin takes the data itself, writes it and closes the input.

const sort = await vm.spawn('sort', [], { stdin: true });

await sort.write('banana\napple\ncherry\n');
await sort.end();

console.log(await sort.stdout()); // "apple\nbanana\ncherry\n"

Over REST, start the command with "stdin": true and "wait": false, then send raw bytes to POST /microvms/{id}/commands/{commandId}/stdin with Content-Type: application/octet-stream. Add ?eof=true to close the input after those bytes. A command started without stdin reads an empty input.

Signals

command.kill() sends SIGTERM, or the signal you pass it. Over REST, POST /microvms/{id}/commands/{commandId}/signal takes { "signal": "SIGINT" }. The signal goes to the command's whole process group, so the processes it started receive it too.

Allowed signals: SIGTERM, SIGKILL, SIGINT, SIGHUP, SIGQUIT, SIGUSR1 and SIGUSR2.

Interactive terminals

Pass pty to run the command in a terminal, for example to drive a shell or a program that only works interactively. Output then arrives on standard output only, and input written to the command is typed into the terminal. In TypeScript, pty: true opens an 80 by 24 terminal, and command.resize(cols, rows) changes its size.

curl -X POST https://api.gigadrive.network/microvms/0199a7c4-5b1e-7c3d-9f20-333333333333/commands \
  -H "Authorization: Bearer $GIGADRIVE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"cmd":"bash","pty":{"cols":120,"rows":30},"wait":false}'

When the terminal window changes size, send { "cols": 160, "rows": 40 } to POST /microvms/{id}/commands/{commandId}/resize. Both values range from 1 to 1000. From your own terminal, gigadrive microvms shell opens one for you; see gigadrive microvms.

Errors

Every error response is { error, code }, where error explains the problem and code is stable.

StatuscodeWhen
400invalid_request, guest_invalid_argumentAn invalid option, or a resize of a command that has no terminal
400guest_command_not_foundThe program does not exist or is not on PATH
403guest_permission_deniedThe user may not run the program or enter the working directory
404not_found, guest_not_foundThe microVM or the command does not exist
409microvm_not_runningThe microVM is not running
413guest_too_large256 commands are already running
429microvm_busyToo many operations run on this microVM at once. Retry shortly
502microvm_unreachableThe microVM could not be reached. Retry shortly