{"record":{"id":"59b5a038353c3151","repo":"wavetermdev/waveterm","slug":"process-already-exited","errorCode":null,"errorMessage":"process already exited","messagePattern":"process already exited","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/jobmanager/jobcmd.go","lineNumber":125,"sourceCode":"\tlog.Printf(\"process exited: exitcode=%s, signal=%s, err=%v\\n\", exitCodeStr, jm.exitSignal, jm.exitErr)\n\n\tgo WshCmdJobManager.sendJobExited()\n}\n\nfunc (jm *JobCmd) GetCmd() (*exec.Cmd, pty.Pty) {\n\tjm.lock.Lock()\n\tdefer jm.lock.Unlock()\n\treturn jm.cmd, jm.cmdPty\n}\n\nfunc (jm *JobCmd) GetPGID() (int, error) {\n\tjm.lock.Lock()\n\tdefer jm.lock.Unlock()\n\tif jm.cmd == nil || jm.cmd.Process == nil {\n\t\treturn 0, fmt.Errorf(\"no active process\")\n\t}\n\tif jm.processExited {\n\t\treturn 0, fmt.Errorf(\"process already exited\")\n\t}\n\tpgid, err := unixutil.GetProcessGroupId(jm.cmd.Process.Pid)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to get pgid: %w\", err)\n\t}\n\tif pgid <= 0 {\n\t\treturn 0, fmt.Errorf(\"invalid pgid returned: %d\", pgid)\n\t}\n\treturn pgid, nil\n}\n\nfunc (jm *JobCmd) GetExitInfo() (bool, *wshrpc.CommandJobCmdExitedData) {\n\tjm.lock.Lock()\n\tdefer jm.lock.Unlock()\n\tif !jm.processExited {\n\t\treturn false, nil\n\t}\n\texitData := &wshrpc.CommandJobCmdExitedData{","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/jobmanager/jobcmd.go#L107-L143","documentation":"GetPGID tracks job termination via the processExited flag (set by the exit-waiter). Once the process has exited, its pgid is no longer meaningful/available, so the method refuses the lookup with this error.","triggerScenarios":"Calling GetPGID after the job's process has terminated — e.g. querying the process group to send a signal but the command already finished (or was killed) and the exit watcher flipped processExited.","commonSituations":"Sending SIGTERM to a job that already completed; a UI still calling process-management RPCs on a finished block; race between process exit and a cleanup routine fetching the pgid.","solutions":["Check GetExitInfo() before calling GetPGID and skip if exited.","Listen for the job-exited event instead of polling the pgid.","Treat this error as a benign 'already done' condition in callers.","Re-fetch fresh job state after restart if the pgid is genuinely needed."],"exampleFix":"// before\npgid, err := jobCmd.GetPGID()\n// after\nexited, _ := jobCmd.GetExitInfo()\nif exited {\n    return nil // job already finished\n}\npgid, err := jobCmd.GetPGID()","handlingStrategy":"validation","validationCode":"exited, exitData := jobCmd.GetExitInfo()\nif exited {\n    return fmt.Errorf(\"job exited (code %v); pgid unavailable\", exitData)\n}","typeGuard":"func isRunning(jm *jobmanager.JobCmd) bool {\n    exited, _ := jm.GetExitInfo()\n    return !exited\n}","tryCatchPattern":"pgid, err := jobCmd.GetPGID()\nif err != nil {\n    if err.Error() == \"process already exited\" {\n        return nil // benign: treat as completed job\n    }\n    return err\n}","preventionTips":["Subscribe to the job-exited event instead of polling the pgid","Check GetExitInfo before any signal/pgid operation","Treat 'process already exited' as a success path in cleanup logic"],"tags":["process","lifecycle","exit"],"backgroundTag":"process-already-exited","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}