{"record":{"id":"980c2e3c10d79445","repo":"wavetermdev/waveterm","slug":"no-shell-input-chan","errorCode":null,"errorMessage":"no shell input chan","messagePattern":"no shell input chan","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/blockcontroller/shellcontroller.go","lineNumber":150,"sourceCode":"func (sc *ShellController) GetRuntimeStatus() *BlockControllerRuntimeStatus {\n\tvar rtn BlockControllerRuntimeStatus\n\tsc.WithLock(func() {\n\t\trtn = sc.getRuntimeStatus_nolock()\n\t})\n\treturn &rtn\n}\n\nfunc (sc *ShellController) GetConnName() string {\n\treturn sc.ConnName\n}\n\nfunc (sc *ShellController) SendInput(inputUnion *BlockInputUnion) error {\n\tvar shellInputCh chan *BlockInputUnion\n\tsc.WithLock(func() {\n\t\tshellInputCh = sc.ShellInputCh\n\t})\n\tif shellInputCh == nil {\n\t\treturn fmt.Errorf(\"no shell input chan\")\n\t}\n\tshellInputCh <- inputUnion\n\treturn nil\n}\n\nfunc (sc *ShellController) WithLock(f func()) {\n\tsc.Lock.Lock()\n\tdefer sc.Lock.Unlock()\n\tf()\n}\n\ntype RunShellOpts struct {\n\tTermSize waveobj.TermSize `json:\"termsize,omitempty\"`\n}\n\n// only call when holding the lock\nfunc (sc *ShellController) sendUpdate_nolock() {\n\trtStatus := sc.getRuntimeStatus_nolock()","sourceCodeStart":132,"sourceCodeEnd":168,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/blockcontroller/shellcontroller.go#L132-L168","documentation":"ShellController buffers terminal input through ShellInputCh, which is created during the shell startup sequence. SendInput refuses to send when that channel is nil, meaning the shell process has not started yet, already exited, or the channel was torn down. This prevents a blocking send (or panic) on a dead channel.","triggerScenarios":"Calling SendInput before Start has created the channel, after the shell process exits and the channel is closed/nil-ed, or when Start failed midway during setupAndStartShellProcess.","commonSituations":"User types into a terminal block while the shell is still starting or right after it exits; a crashed shell leaves the block present but input unattached; sending input from automation without waiting for readiness.","solutions":["Wait for the shell/terminal 'running' state before sending input.","Restart the block (Start) if the shell exited, then send input.","Check process state via the block controller status before writing.","Add a readiness signal/guard in the calling UI around SendInput."],"exampleFix":"// before\nsc.SendInput(union) // no channel yet — error\n// after\nselect {\ncase <-sc.WaitStarted(): // readiness signal from the controller\n    return sc.SendInput(union)\ncase <-time.After(5 * time.Second):\n    return fmt.Errorf(\"shell not ready\")\n}","handlingStrategy":"type-guard","validationCode":"var ch chan *BlockInputUnion\nsc.WithLock(func() { ch = sc.ShellInputCh })\nif ch == nil {\n    return fmt.Errorf(\"shell input not ready\")\n}","typeGuard":"func (sc *ShellController) InputReady() bool {\n    var ok bool\n    sc.WithLock(func() { ok = sc.ShellInputCh != nil })\n    return ok\n}","tryCatchPattern":"if err := sc.SendInput(union); err != nil && err.Error() == \"no shell input chan\" {\n    // queue the input and flush once the shell reports running\n    pendingInput = append(pendingInput, union)\n    return nil\n}","preventionTips":["Gate input UI on shell-running status, not block existence","Rebuild the channel lifecycle on shell restart","Flush queued input only after SendInput readiness is confirmed"],"tags":["state","input","lifecycle","channel"],"backgroundTag":"shell-not-running","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}