wavetermdev/waveterm · error

failed to get stdout pipe: %w

Error message

failed to get stdout pipe: %w

What it means

After a process controller is created, RunSimpleCommand calls proc.StdoutPipe() to obtain a reader for the child's stdout. This error wraps a failure from that call. For exec-based controllers StdoutPipe rarely fails, but for SSH-session-backed controllers it returns the underlying ssh.Session error — most commonly 'session already started' or a closed/broken session — meaning stdout capture cannot be set up and the command is aborted.

Source

Thrown at pkg/genconn/genconn.go:74

	Start() error
	Wait() error
	Kill()

	// these are not required to be called, if they are not called, the impl will set to discard output
	StdinPipe() (io.WriteCloser, error)
	StdoutPipe() (io.Reader, error)
	StderrPipe() (io.Reader, error)
}

func RunSimpleCommand(ctx context.Context, client ShellClient, spec CommandSpec) (string, string, error) {
	proc, err := client.MakeProcessController(spec)
	if err != nil {
		return "", "", fmt.Errorf("failed to create process controller: %w", err)
	}

	stdout, err := proc.StdoutPipe()
	if err != nil {
		return "", "", fmt.Errorf("failed to get stdout pipe: %w", err)
	}
	stderr, err := proc.StderrPipe()
	if err != nil {
		return "", "", fmt.Errorf("failed to get stderr pipe: %w", err)
	}

	if err := proc.Start(); err != nil {
		return "", "", fmt.Errorf("failed to start process: %w", err)
	}

	stdoutBuf := syncbuf.MakeSyncBuffer()
	stderrBuf := syncbuf.MakeSyncBuffer()
	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		io.Copy(stdoutBuf, stdout)

View on GitHub (pinned to a4447c1563)

Solutions

  1. Ensure your ShellProcessController implementation only starts the process in Start(), never in MakeProcessController — pipes must be requested before Start().
  2. Check the wrapped error; ssh 'session already started' means pipe ordering is wrong in the controller implementation.
  3. Recreate the controller from a fresh/verified connection if the session was closed.
  4. Do not reuse a ShellProcessController across multiple runs; create a new one per command.
  5. If using your own client, conform to genconn.ShellProcessController semantics exactly (pipes before Start).

Example fix

// before (bad controller impl)
func (c *MyController) MakeProcessController(spec genconn.CommandSpec) (genconn.ShellProcessController, error) {
    cmd := exec.Command("sh", "-c", spec.Cmd)
    cmd.Start() // WRONG: starts before pipes are set
    return &MyProc{cmd}, nil
}
// after
func (c *MyController) MakeProcessController(spec genconn.CommandSpec) (genconn.ShellProcessController, error) {
    cmd := exec.Command("sh", "-c", spec.Cmd)
    return &MyProc{cmd}, nil // only Start() starts it; pipes requested first
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure controller contract: pipes must be taken before Start.
// In custom controllers, assert process not started:
if p.started {
    return fmt.Errorf("cannot take stdout pipe after start")
}

Try / catch

stdout, err := proc.StdoutPipe()
if err != nil {
    if strings.Contains(err.Error(), "already started") || strings.Contains(err.Error(), "closed") {
        return fmt.Errorf("controller in bad state, rebuild required: %w", err)
    }
    return fmt.Errorf("stdout pipe: %w", err)
}

Prevention

When it happens

Trigger: proc.StdoutPipe() errors: calling pipes after Start() on some implementations (SSH sessions reject pipe setup after start), the ssh session was closed between MakeProcessController and StdoutPipe, or the local exec.Cmd was misconfigured by the client.

Common situations: A custom ShellProcessController implementation that starts the process inside MakeProcessController (violating the interface contract that Start() comes later); a stale SSH connection; reusing one controller for multiple commands.

Related errors


AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01). Data as JSON: /api/errors/47c3b867c543e4fc. Report an issue: GitHub.