docs/sdk/go/execution.mdx
Run commands inside a running sandbox, collect their output, or stream events live. See Commands for usage examples.
The exec API mirrors os/exec conventions: a non-zero exit code is not a Go error. Transport, timeout, and spawn-failure paths return an error; a program that ran and exited non-zero is a normal *ExecOutput result, inspect Success() or ExitCode().
import m "github.com/superradcompany/microsandbox/sdk/go"
out, err := sb.Exec(ctx, "python3", []string{"-c", "print(1 + 1)"})
if err != nil {
return err // transport / timeout / spawn failure
}
if !out.Success() {
log.Printf("exited %d: %s", out.ExitCode(), out.Stderr())
}
fmt.Print(out.Stdout())
The exec entry points live on *Sandbox.
func (s *Sandbox) Exec(ctx context.Context, cmd string, args []string, opts ...ExecOption) (*ExecOutput, error)
Run a command in the sandbox and return its collected output. Blocks until the command exits. The returned error is non-nil only on transport or runtime failures; a non-zero exit code is reported via ExecOutput.ExitCode, not as an error.
out, err := sb.Exec(ctx, "make", []string{"build"},
m.WithExecCwd("/app"),
m.WithExecEnv(map[string]string{"DEBUG": "1"}),
)
func (s *Sandbox) Shell(ctx context.Context, command string, opts ...ExecOption) (*ExecOutput, error)
Run /bin/sh -c command in the sandbox and collect its output. Blocks until the command exits. A convenience wrapper over Exec for shell one-liners.
out, err := sb.Shell(ctx, "echo $HOME && ls /tmp")
if err != nil {
return err
}
fmt.Print(out.Stdout())
func (s *Sandbox) ExecStream(ctx context.Context, cmd string, args []string, opts ...ExecOption) (*ExecHandle, error)
Start a streaming exec session and return an *ExecHandle. The handle MUST be closed with Close when the stream is no longer needed. Nonblocking: the handle returns immediately and the stream starts in the background.
ctx controls only the start handshake; individual Recv calls take their own ctx. Non-zero exit codes are not errors, inspect ExecEventExited.
h, err := sb.ExecStream(ctx, "cat", nil, m.WithExecStdinPipe())
if err != nil {
return err
}
defer h.Close()
func (s *Sandbox) ShellStream(ctx context.Context, command string, opts ...ExecOption) (*ExecHandle, error)
Run /bin/sh -c command with streaming output. A convenience wrapper over ExecStream. Nonblocking: the handle returns immediately and the stream starts in the background.
h, err := sb.ShellStream(ctx, "tail -f /var/log/app.log")
if err != nil {
return err
}
defer h.Close()
for {
ev, err := h.Recv(ctx)
if err != nil {
return err
}
switch ev.Kind {
case m.ExecEventStdout:
os.Stdout.Write(ev.Data)
case m.ExecEventDone:
return nil
}
}
A live streaming exec session returned by ExecStream and ShellStream. Not safe for concurrent use from multiple goroutines.
func (h *ExecHandle) ID() (string, error)
Return the unique identifier for this exec session, assigned by the guest agent. Useful for correlating log entries or referencing the session from external tooling.
<p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><span className="msb-type">string</span></div> <div className="msb-param-desc">Session correlation id.</div> </div> </div>func (h *ExecHandle) TakeStdin() *ExecSink
Return the stdin sink for this exec session. Single-take: returns nil if the session was not started with WithExecStdinPipe, or if TakeStdin was already called on this handle (matching the Node and Python SDKs). The caller is responsible for closing the sink when done writing; closing the sink without closing the exec handle is fine, they own different Rust-side resources.
sink := h.TakeStdin()
sink.Write([]byte("hello\n"))
sink.Close()
func (h *ExecHandle) Recv(ctx context.Context) (*ExecEvent, error)
Block until the next event arrives or the stream ends. Returns an event with Kind == ExecEventDone when all events have been consumed. ctx controls the wait; cancellation causes Recv to return ctx.Err() immediately. The underlying Rust call may continue to completion in the background.
for {
ev, err := h.Recv(ctx)
if err != nil {
return err
}
switch ev.Kind {
case m.ExecEventStarted:
fmt.Printf("started pid=%d\n", ev.PID)
case m.ExecEventStdout:
os.Stdout.Write(ev.Data)
case m.ExecEventStderr:
os.Stderr.Write(ev.Data)
case m.ExecEventExited:
fmt.Printf("exited code=%d\n", ev.ExitCode)
case m.ExecEventDone:
return nil
}
}
func (h *ExecHandle) Collect(ctx context.Context) (*ExecOutput, error)
Drain the stream, accumulate all output, and return it as an *ExecOutput. Equivalent to calling Recv in a loop and assembling the result. The handle should be closed after Collect returns.
out, err := h.Collect(ctx)
if err != nil {
return err
}
fmt.Print(out.Stdout())
func (h *ExecHandle) Wait(ctx context.Context) (int, error)
Block until the process exits and return its exit code. Unlike Collect, stdout and stderr are discarded. The handle should be closed after Wait returns.
func (h *ExecHandle) Kill(ctx context.Context) error
Send SIGKILL to the running process.
<p className="msb-label">Parameters</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><code>ctx</code><span className="msb-type">context.Context</span></div> <div className="msb-param-desc">Cancels the call.</div> </div> </div>func (h *ExecHandle) Signal(ctx context.Context, signal int) error
Send a Unix signal to the running process. Pass values from syscall (e.g. int(syscall.SIGTERM)).
import "syscall"
if err := h.Signal(ctx, int(syscall.SIGTERM)); err != nil {
return err
}
func (h *ExecHandle) Close() error
Release the Rust-side exec handle. Does not kill the running process; call Signal or Kill first if you need to terminate it. Safe to call after ExecEventDone has been received.
defer h.Close()
The collected result of a completed command execution, returned by Exec, Shell, and Collect.
func (e *ExecOutput) Stdout() string
Captured standard output as a string.
<p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><span className="msb-type">string</span></div> <div className="msb-param-desc">Collected stdout.</div> </div> </div>func (e *ExecOutput) StdoutBytes() []byte
Captured standard output as raw bytes. Use when the output may not be valid UTF-8.
<p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><span className="msb-type">[]byte</span></div> <div className="msb-param-desc">Raw stdout bytes.</div> </div> </div>func (e *ExecOutput) Stderr() string
Captured standard error as a string.
<p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><span className="msb-type">string</span></div> <div className="msb-param-desc">Collected stderr.</div> </div> </div>func (e *ExecOutput) StderrBytes() []byte
Captured standard error as raw bytes.
<p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><span className="msb-type">[]byte</span></div> <div className="msb-param-desc">Raw stderr bytes.</div> </div> </div>func (e *ExecOutput) ExitCode() int
The process's exit code, or -1 if the guest did not report one (e.g. the process was killed by a signal).
func (e *ExecOutput) Success() bool
Reports whether the command exited with code 0.
A write-only pipe to a running process's stdin, obtained from ExecHandle.TakeStdin. Implements io.WriteCloser. Write and Close use context.Background() under the hood; for caller-controlled cancellation use WriteCtx, or tear the session down via ExecHandle.Kill / Close.
func (sk *ExecSink) Write(p []byte) (int, error)
Send data to the process stdin. Implements io.Writer. Uses context.Background() internally, there is no way to cancel a stuck write through this method alone.
h, err := sb.ExecStream(ctx, "cat", nil, m.WithExecStdinPipe())
if err != nil {
return err
}
defer h.Close()
sink := h.TakeStdin()
sink.Write([]byte("hello\n"))
sink.Close()
out, _ := h.Collect(ctx)
fmt.Print(out.Stdout()) // "hello\n"
func (sk *ExecSink) WriteCtx(ctx context.Context, p []byte) (int, error)
Like Write, but with a caller-controlled context, so a stuck stdin write can be cancelled.
func (sk *ExecSink) Close() error
Close the stdin pipe, sending EOF to the process. Implements io.Closer.
sink.Close()
The collected result of a command execution. A non-zero ExitCode is not treated as a Go error, callers inspect Success or ExitCode explicitly, matching how os/exec.Cmd.Output works against a script that exits non-zero.
| Method | Returns | Description |
|---|---|---|
Stdout() | string | Captured stdout as a string |
StdoutBytes() | []byte | Raw stdout bytes |
Stderr() | string | Captured stderr as a string |
StderrBytes() | []byte | Raw stderr bytes |
ExitCode() | int | Exit code, or -1 if the guest did not report one |
Success() | bool | true if ExitCode() is 0 |
A live streaming exec session. Must be closed with Close when done to release Rust-side resources. Not safe for concurrent use from multiple goroutines.
| Method | Returns | Description |
|---|---|---|
ID() | (string, error) | Session correlation id assigned by the guest agent |
TakeStdin() | *ExecSink | Take the stdin writer (single-take; only with WithExecStdinPipe) |
Recv(ctx) | (*ExecEvent, error) | Block until the next event arrives |
Collect(ctx) | (*ExecOutput, error) | Drain remaining output into an ExecOutput |
Wait(ctx) | (int, error) | Wait for exit, discarding output; returns the exit code |
Kill(ctx) | error | Send SIGKILL |
Signal(ctx, signal) | error | Send a Unix signal |
Close() | error | Release the Rust-side handle |
type ExecSink = ffi.ExecSink
A write-only pipe to a running process's stdin. Implements io.WriteCloser.
| Method | Returns | Description |
|---|---|---|
Write(p) | (int, error) | Implements io.Writer |
WriteCtx(ctx, p) | (int, error) | Write with explicit context |
Close() | error | Send EOF and finalize |
One event from a streaming exec session. Kind identifies which fields are populated.
| Field | Type | Description |
|---|---|---|
Kind | ExecEventKind | Identifies which fields are populated |
PID | uint32 | Guest process id, set on ExecEventStarted |
Data | []byte | Chunk of stdout or stderr, set on ExecEventStdout / ExecEventStderr |
ExitCode | int | Process exit code, set on ExecEventExited |
Failure | *ExecFailure | Failure detail, set on ExecEventFailed and ExecEventStdinError |
type ExecEventKind = ffi.ExecEventKind
Identifies what an ExecEvent carries.
| Constant | Description |
|---|---|
ExecEventStarted | Sent once when the guest process starts; PID is valid |
ExecEventStdout | A chunk of stdout; Data is valid |
ExecEventStderr | A chunk of stderr; Data is valid |
ExecEventExited | The process exited; ExitCode is valid |
ExecEventFailed | The user program never started (binary missing, permission denied, ...); Failure is valid and ExitCode is not meaningful |
ExecEventStdinError | A stdin write failed; Failure is valid. Non-terminal: the process may still exit normally |
ExecEventDone | All events have been consumed |
type ExecFailure = ffi.ExecFailure
Structured detail about a failed-to-start exec, populated on ExecEventFailed and ExecEventStdinError. See Error Handling for the kinds it carries (not_found, permission_denied, etc.) and how to branch on them.
| Field | Type | Description |
|---|---|---|
Kind | string | Failure category, e.g. not_found, permission_denied |
Errno | *int | Underlying errno, if known |
ErrnoName | string | Symbolic errno name, if known |
Message | string | Human-readable failure message |
Path | string | Offending path, if applicable |
Configures a single Exec or ExecStream call. Most callers set fields through the WithExec* functional options; ExecConfig is exported for parity with the other SDKs' config types.
| Field | Type | Description |
|---|---|---|
Cwd | string | Working directory inside the guest |
Timeout | time.Duration | Kill the process after this duration; sub-second precision is rounded up |
StdinPipe | bool | Enable a stdin pipe; required for TakeStdin |
User | string | Guest user (UID or name) |
Env | map[string]string | Per-command environment variables |
type ExecOption func(*ExecConfig)
A functional option that mutates an ExecConfig. Construct them with the WithExec* functions below.
func WithExecCwd(path string) ExecOption
Set the working directory for a single command.
<p className="msb-label">Parameters</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><code>path</code><span className="msb-type">string</span></div> <div className="msb-param-desc">Absolute path inside the guest.</div> </div> </div>func WithExecTimeout(d time.Duration) ExecOption
Set a per-command timeout. When exceeded, the guest terminates the process and the call returns an error with Kind == ErrExecTimeout. Sub-second precision rounds up to whole seconds; pass at least 1 second.
out, err := sb.Shell(ctx, "long-running-task",
m.WithExecTimeout(30*time.Second))
if m.IsKind(err, m.ErrExecTimeout) {
log.Println("timed out")
}
func WithExecStdinPipe() ExecOption
Enable a stdin pipe for the exec session, allowing data to be written via ExecHandle.TakeStdin.
func WithExecUser(user string) ExecOption
Run the command as the given guest user (UID or name).
<p className="msb-label">Parameters</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><code>user</code><span className="msb-type">string</span></div> <div className="msb-param-desc">Guest user, as a UID or name.</div> </div> </div>func WithExecEnv(env map[string]string) ExecOption
Add per-command environment variables. Called repeatedly, maps merge; later keys overwrite earlier ones.
<p className="msb-label">Parameters</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><code>env</code><span className="msb-type">map[string]string</span></div> <div className="msb-param-desc">Environment variables for this command.</div> </div> </div> <Accordion title="Example">out, err := sb.Exec(ctx, "make", []string{"build"},
m.WithExecCwd("/app"),
m.WithExecEnv(map[string]string{"DEBUG": "1"}),
)