{"record":{"id":"47c3b867c543e4fc","repo":"wavetermdev/waveterm","slug":"failed-to-get-stdout-pipe-w","errorCode":null,"errorMessage":"failed to get stdout pipe: %w","messagePattern":"failed to get stdout pipe: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/genconn/genconn.go","lineNumber":74,"sourceCode":"\tStart() error\n\tWait() error\n\tKill()\n\n\t// these are not required to be called, if they are not called, the impl will set to discard output\n\tStdinPipe() (io.WriteCloser, error)\n\tStdoutPipe() (io.Reader, error)\n\tStderrPipe() (io.Reader, error)\n}\n\nfunc RunSimpleCommand(ctx context.Context, client ShellClient, spec CommandSpec) (string, string, error) {\n\tproc, err := client.MakeProcessController(spec)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"failed to create process controller: %w\", err)\n\t}\n\n\tstdout, err := proc.StdoutPipe()\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"failed to get stdout pipe: %w\", err)\n\t}\n\tstderr, err := proc.StderrPipe()\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"failed to get stderr pipe: %w\", err)\n\t}\n\n\tif err := proc.Start(); err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"failed to start process: %w\", err)\n\t}\n\n\tstdoutBuf := syncbuf.MakeSyncBuffer()\n\tstderrBuf := syncbuf.MakeSyncBuffer()\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tio.Copy(stdoutBuf, stdout)","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/genconn/genconn.go#L56-L92","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure your ShellProcessController implementation only starts the process in Start(), never in MakeProcessController — pipes must be requested before Start().","Check the wrapped error; ssh 'session already started' means pipe ordering is wrong in the controller implementation.","Recreate the controller from a fresh/verified connection if the session was closed.","Do not reuse a ShellProcessController across multiple runs; create a new one per command.","If using your own client, conform to genconn.ShellProcessController semantics exactly (pipes before Start)."],"exampleFix":"// before (bad controller impl)\nfunc (c *MyController) MakeProcessController(spec genconn.CommandSpec) (genconn.ShellProcessController, error) {\n    cmd := exec.Command(\"sh\", \"-c\", spec.Cmd)\n    cmd.Start() // WRONG: starts before pipes are set\n    return &MyProc{cmd}, nil\n}\n// after\nfunc (c *MyController) MakeProcessController(spec genconn.CommandSpec) (genconn.ShellProcessController, error) {\n    cmd := exec.Command(\"sh\", \"-c\", spec.Cmd)\n    return &MyProc{cmd}, nil // only Start() starts it; pipes requested first\n}","handlingStrategy":"try-catch","validationCode":"// Ensure controller contract: pipes must be taken before Start.\n// In custom controllers, assert process not started:\nif p.started {\n    return fmt.Errorf(\"cannot take stdout pipe after start\")\n}","typeGuard":null,"tryCatchPattern":"stdout, err := proc.StdoutPipe()\nif err != nil {\n    if strings.Contains(err.Error(), \"already started\") || strings.Contains(err.Error(), \"closed\") {\n        return fmt.Errorf(\"controller in bad state, rebuild required: %w\", err)\n    }\n    return fmt.Errorf(\"stdout pipe: %w\", err)\n}","preventionTips":["Always call StdoutPipe before Start()","Take each pipe exactly once per controller","Do not reuse controllers across commands","Rebuild the controller from a fresh session if any pipe call fails","Implement ShellProcessController contract faithfully in custom clients"],"tags":["process","pipe","ssh","stdout"],"backgroundTag":"process-spawn-failed","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}