> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vpod.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Process I/O

> Bound, interrupt, stream, and feed input to commands while they run.

`commands.run()` resolves with everything at once when the command ends, which is all a quick command needs. Commands that run longer are worth bounding, watching, or feeding input to while they run. These options cover that; they layer onto the same [`run()`](/sdk/typescript/sandbox) call and none of them change its return value.

## Timeouts

Both methods take a `timeout` in seconds. When it is reached the guest work is
interrupted and the call returns rather than waiting.

```ts theme={null}
await sandbox.code.run("import time; time.sleep(30)", { timeout: 3 });

const result = await sandbox.commands.run("sleep 30", { timeout: 3 });
console.log(result.exitCode); // 124
```

## Interrupting a command

A command no longer has to run to its timeout. `interrupt()` stops whatever is in
the foreground, and `commands.run()` takes an `AbortSignal` for callers who would
rather set the deadline up front.

```ts theme={null}
const running = sandbox.commands.run("sleep 300");
await sandbox.commands.interrupt();

const result = await running;
console.log(result.exitCode); // 130
```

```ts theme={null}
const controller = new AbortController();
const promise = sandbox.commands.run("sleep 300", { signal: controller.signal });
controller.abort();

// or set a deadline without wiring up a controller
await sandbox.commands.run("sleep 300", { signal: AbortSignal.timeout(5000) });
```

The two report differently, on purpose. `interrupt()` resolves with an ordinary
result carrying exit code `130`, because a terminal wants the exit code. A
`signal` rejects with `signal.reason`, so your own controller throws `AbortError`
and `AbortSignal.timeout()` throws `TimeoutError`.

The command really stops. This is not the caller walking away while the guest
keeps working, so the sandbox is yours again immediately.

<Note>
  `code.run()` cannot be interrupted yet. It accepts `signal` because it shares an
  options type with `commands.run()`, but ignores it, so use `timeout` there.

  A command that ignores the interrupt runs to its own deadline and is recovered
  the same way a timeout is.
</Note>

## Streaming output

`commands.run()` hands back everything at once, when the command ends. For a
build or a test run that is a long wait followed by a wall of text, so it also
takes `onStdout` and `onStderr`, called with each piece of output as it arrives:

```ts theme={null}
const result = await sandbox.commands.run(
    "for i in 1 2 3; do echo step $i; sleep 1; done",
    {
        timeout: 60,
        onStdout: (chunk) => process.stdout.write(chunk),
    },
);

console.log(result.exitCode); // 0
```

The return value does not change. What the callbacks receive concatenates to
exactly the `stdout` you get without them, so you can add one to existing code
and nothing else moves.

Chunks arrive as the guest produces output, not on a fixed schedule. A command
printing two hundred lines with no pauses delivers them in one chunk; one
printing a line a second delivers a chunk per line. A command that finishes
quickly delivers a single chunk, which is indistinguishable from not streaming,
and that is correct: there was nothing to stream.

Streaming composes with `interrupt()` and `signal`. The chunks that arrived
before the stop are kept, and `interrupt()` still resolves with exit code `130`.

<Note>
  `code.run()` does not stream. It accepts the options because it shares an
  options type with `commands.run()`, but ignores them.
</Note>

## Sending input

`stdin` gives the command something to read. A string or a `Uint8Array` becomes
that command's entire input, ending with a real EOF:

```ts theme={null}
const result = await sandbox.commands.run("sort", { stdin: "banana\napple\ncherry\n" });
console.log(result.stdout); // apple\nbanana\ncherry
```

Input is byte-exact, so binary works the same way:

```ts theme={null}
const png = await readFile("logo.png");
const { stdout } = await sandbox.commands.run("sha256sum", { stdin: png });
```

Without `stdin` a command's input is closed, which is why `cat` with no `stdin`
returns at once rather than hanging.

## Streaming input

Pass a `ReadableStream` or an `AsyncIterable` when what you send next depends on
what came back. That puts the command on a terminal, so you write as it runs and
`writer.close()` ends its input:

```ts theme={null}
const { readable, writable } = new TransformStream<string>();
const writer = writable.getWriter();

let seen = "";
let step = 0;
const running = sandbox.commands.run("python3", {
    stdin: readable,
    timeout: 60,
    onStdout: (chunk) => {
        seen += chunk;
        if (step === 0 && seen.includes(">>>")) {
            step = 1;
            void writer.write("print(40 + 2)\n");
        } else if (step === 1 && seen.includes("42")) {
            step = 2;
            void writer.close();
        }
    },
});

const result = await running;   // exit code 0
```

`stdin` accepts a `string`, a `Uint8Array`, an `AsyncIterable`, or a
`ReadableStream`. Match on the output so far rather than the chunk you were
handed, and step through once: the prompt comes back after every answer.

Use `tty: true` for a terminal with no input of its own, and `timeout: 0` for no
deadline, which is what an interactive session wants.

<Warning>
  Close the stream only once the command has read what you sent it, by writing in
  response to output as above. Closing early is ignored and the command keeps
  waiting. If you have all the input up front, pass a string instead.
</Warning>
