{"record":{"id":"0dd35abb2458e5ab","repo":"wavetermdev/waveterm","slug":"error-sending-signal-w","errorCode":null,"errorMessage":"error sending signal: %w","messagePattern":"error sending signal: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/jobmanager/jobcmd.go","lineNumber":205,"sourceCode":"\n\tif len(data.InputData64) > 0 {\n\t\tinputBuf := make([]byte, base64.StdEncoding.DecodedLen(len(data.InputData64)))\n\t\tnw, err := base64.StdEncoding.Decode(inputBuf, []byte(data.InputData64))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error decoding input data: %w\", err)\n\t\t}\n\t\t_, err = jm.cmdPty.Write(inputBuf[:nw])\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error writing to pty: %w\", err)\n\t\t}\n\t}\n\n\tif data.SigName != \"\" {\n\t\tsig := unixutil.ParseSignal(data.SigName)\n\t\tif sig != nil && jm.cmd.Process != nil {\n\t\t\terr := jm.cmd.Process.Signal(sig)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error sending signal: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tif data.TermSize != nil {\n\t\terr := jm.setTermSize_withlock(*data.TermSize)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (jm *JobCmd) TerminateByClosingPtyMaster() {\n\tjm.lock.Lock()\n\tdefer jm.lock.Unlock()\n\tif jm.ptyClosed {","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/jobmanager/jobcmd.go#L187-L223","documentation":"HandleInput delivers a signal (SigName) to the job's process via cmd.Process.Signal. This error wraps the failure of that syscall, typically 'os: process already finished' since the job exited before the signal was sent.","triggerScenarios":"Sending a kill/interrupt signal to a job that has already exited; the process becoming a zombie or being reaped before Signal is called; platform rejecting the signal number.","commonSituations":"User clicks 'stop job' twice; a timeout fires a kill at the same moment the process exits normally; SIGWINCH/resize-signal arrives after job completion.","solutions":["Ignore 'os: process already finished' errors — the desired outcome (process stopped) already happened","Check jm.cmd.ProcessState is nil before signaling, and skip if the process has exited","Use a done-channel/WaitGroup so signals are only sent while the job is live","If the signal is rejected on another platform, guard signal-sending behind a GOOS check"],"exampleFix":"// before\nerr := jm.cmd.Process.Signal(sig)\nif err != nil {\n    return fmt.Errorf(\"error sending signal: %w\", err)\n}\n// after\nif jm.cmd.ProcessState != nil {\n    return nil // process already exited\n}\nerr := jm.cmd.Process.Signal(sig)\nif err != nil && !strings.Contains(err.Error(), \"process already finished\") {\n    return fmt.Errorf(\"error sending signal: %w\", err)\n}","handlingStrategy":"try-catch","validationCode":"if jm.Cmd == nil || jm.Cmd.Process == nil || jm.Cmd.ProcessState != nil {\n    return // cannot signal a finished/nonexistent process\n}","typeGuard":null,"tryCatchPattern":"if err := jm.HandleInput(sigData); err != nil {\n    var oe *os.PathError\n    if errors.As(err, &oe) && strings.Contains(err.Error(), \"process already finished\") {\n        return nil // benign race; job already exited\n    }\n    return err\n}","preventionTips":["Track a done channel from cmd.Wait and gate signals on it","Ignore 'process already finished' errors on stop paths","Debounce duplicate stop/kill requests in the UI","Guard signal sends behind the same lock the waiter uses"],"tags":["signal","process","race-condition"],"backgroundTag":"process-already-finished","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}