wavetermdev/waveterm · error

failed to create stdout pipe

Error message

failed to create stdout pipe: %w

What it means

runBuilderApp launches a builder subprocess via os/exec and wires up its I/O. This error wraps the failure from cmd.StdoutPipe(), which Go's exec package returns when the process has already been started or the OS cannot allocate a pipe. It is thrown before the builder process starts, so no child process exists yet.

Solutions

  1. Ensure cmd.Start() is called only once and only after all pipes are created
  2. Check the wrapped error (%w) — if 'too many open files', fix fd leaks or raise ulimit
  3. Re-run the build; transient fd exhaustion clears once other handles are released

Example fix

// before
cmd.Start()
stdoutPipe, err := cmd.StdoutPipe() // fails: process already started
// after
stdoutPipe, err := cmd.StdoutPipe()
if err != nil { return nil, fmt.Errorf("failed to create stdout pipe: %w", err) }
err = cmd.Start()
Defensive patterns

Strategy: try-catch

Validate before calling

if cmd.Process != nil { panic("cmd already started") } // create pipes only on a fresh exec.Cmd

Try / catch

process, err := bc.runBuilderApp(ctx, ...)
if err != nil {
    if strings.Contains(err.Error(), "failed to create stdout pipe") {
        // fd exhaustion or double-start; inspect wrapped cause with errors.Unwrap
    }
    return err
}

Prevention

When it happens

Trigger: cmd.StdoutPipe() returns non-nil err — practically only when cmd.Start() was already called on the same Cmd, or on rare OS pipe-allocation failure (fd exhaustion).

Common situations: Code accidentally starting the command twice; extreme fd exhaustion (ulimit -n hit) after leaking pipes/files in a long-running server; reusing a BuilderController Cmd struct incorrectly.

Related errors


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

Appendix: source

Thrown at pkg/buildercontroller/buildercontroller.go:340

	}
	for k, v := range secretEnv {
		builderEnv[k] = v
	}

	cmd := exec.Command(appBinPath)
	cmd.Env = append(os.Environ(), "TSUNAMI_CLOSEONSTDIN=1")

	if wavebase.IsDevMode() {
		cmd.Env = append(cmd.Env, "TSUNAMI_CORS="+tsunamiutil.DevModeCorsOrigins)
	}

	for key, value := range builderEnv {
		cmd.Env = append(cmd.Env, key+"="+value)
	}

	stdoutPipe, err := cmd.StdoutPipe()
	if err != nil {
		return nil, fmt.Errorf("failed to create stdout pipe: %w", err)
	}

	stderrPipe, err := cmd.StderrPipe()
	if err != nil {
		return nil, fmt.Errorf("failed to create stderr pipe: %w", err)
	}

	stdinPipe, err := cmd.StdinPipe()
	if err != nil {
		return nil, fmt.Errorf("failed to create stdin pipe: %w", err)
	}

	portChan := make(chan int, 1)
	portFound := false

	bc.outputBuffer.SetLineCallback(func(line string) {
		if !portFound {
			if port := build.ParseTsunamiPort(line); port > 0 {

View on GitHub (pinned to a4447c1563)