Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Prepared Processes and Lifetime

Prepare a single-use process, stream live pipes, supervise lifetime, and close resources safely.

developer

Executor.PrepareProcess is the asynchronous counterpart to RunArgv. Preparation validates the command and directory, checks command admission, redeems and consumes supplied grants, reserves path handles and proxy credentials, and compiles the transient backend spec without spawning. The returned PreparedProcess is single-use. Start transfers the reservation capsule to a background supervisor; Close releases an unstarted preparation.

Prepare, start, wait

package example

import (
	"context"
	"fmt"
	"io"

	"github.com/looprig/sandbox"
)

func stream(executor *sandbox.Executor, workspace string) error {
	prepared, err := executor.PrepareProcess(context.Background(), sandbox.ProcessOptions{
		Directory: workspace,
		Command:   "printf 'hello\\n'",
	})
	if err != nil {
		return err
	}
	defer prepared.Close() // no-op after Start consumes it

	process, err := prepared.Start(context.Background())
	if err != nil {
		return err
	}
	defer process.Close(context.Background())

	output, err := io.ReadAll(process.Stdout())
	if err != nil {
		return err
	}
	result, err := process.Wait(context.Background())
	if err != nil {
		return err
	}
	fmt.Printf("exit=%d output=%q lifetime=%v\\n",
		result.ExitCode, output, process.LifetimeContainment())
	return nil
}

Stdout() and Stderr() are distinct pipes for pipe mode. Stdin() is a concurrent-safe writer whose Close delivers EOF and makes later writes return ErrProcessStdinClosed. Wait starts the one real wait lazily, caches its result, and lets a canceled wait context stop only that caller. It does not kill the process. The background supervisor also calls Wait, so a caller that forgets to wait does not strand the process cleanup.

Process access is frozen

PreparedProcess.EffectiveAccess() returns a defensive immutable description: ProcessAccessReadOnly, ProcessAccessScopedWrite with canonical write paths/trees, or ProcessAccessBroadWrite. A later grant or workspace lease cannot widen that preparation. This is useful to Harness or a Tool runtime that needs to classify workspace invalidation after the process completes.

TTY and signals

Set ProcessOptions.TTY only when a real terminal is required. Unix can provide a PTY and Windows can provide ConPTY on supported hosts, but a selected backend without terminal wiring returns ErrProcessTTYUnsupported rather than silently returning pipes. StreamMode() reports ProcessStreamModePipes or ProcessStreamModePTY.

Signal accepts ProcessSignalInterrupt, ProcessSignalTerminate, or ProcessSignalKill. Terminate has one grace period and at most one escalation. A pipe process can fail closed with ErrProcessSignalUnsupported when a platform/backend has no signal adapter. LifetimeContainment() is the actual process-tree teardown contract: Enforced, BestEffort, or Unspecified. Darwin Seatbelt can be best effort; Linux namespace/cgroup and Windows Job paths can be enforced. Do not collapse those values into a generic “process killed” claim.

Source

Proof

← back to documentation