{"record":{"id":"d187c355e1ef587c","repo":"wavetermdev/waveterm","slug":"failed-to-get-pgid-w","errorCode":null,"errorMessage":"failed to get pgid: %w","messagePattern":"failed to get pgid: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/jobmanager/jobcmd.go","lineNumber":129,"sourceCode":"\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{\n\t\tJobId:      WshCmdJobManager.JobId,\n\t\tExitCode:   jm.exitCode,\n\t\tExitSignal: jm.exitSignal,\n\t\tExitTs:     jm.exitTs,","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/jobmanager/jobcmd.go#L111-L147","documentation":"GetPGID resolves the process group via unixutil.GetProcessGroupId(jm.cmd.Process.Pid). If that syscall-level lookup fails (permission problem or the process vanished mid-call), the underlying error is wrapped as 'failed to get pgid'.","triggerScenarios":"The process exited between the processExited check and the GetProcessGroupId call (procfs/sysctl entry gone), or the caller lacks permission to read the other process's group.","commonSituations":"Tight races with fast-exiting processes; hardened /proc (hidepid) mount options; sandboxed environments restricting process metadata access.","solutions":["Unwrap to inspect the OS error (ESRCH vs EPERM) and handle each accordingly.","Treat ESRCH as 'process exited' and fall back to GetExitInfo.","Retry once immediately — the race window is tiny and usually resolves to a definitive exit.","Fix environment permissions (hidepid / sandbox) if EPERM is the cause."],"exampleFix":"// before\npgid, err := jobCmd.GetPGID()\n// after\npgid, err := jobCmd.GetPGID()\nif err != nil && errors.Is(err, unix.ESRCH) {\n    exited, _ := jobCmd.GetExitInfo()\n    _ = exited // process gone; handle as exited\n}","handlingStrategy":"retry","validationCode":"exited, _ := jobCmd.GetExitInfo()\nif exited {\n    return errors.New(\"skip pgid lookup: process exited\")\n}","typeGuard":"func canLookupPGID(jm *jobmanager.JobCmd) bool {\n    exited, _ := jm.GetExitInfo()\n    return !exited\n}","tryCatchPattern":"pgid, err := jobCmd.GetPGID()\nif err != nil && strings.Contains(err.Error(), \"failed to get pgid\") {\n    time.Sleep(10 * time.Millisecond)\n    pgid, err = jobCmd.GetPGID() // one retry; usually resolves the ESRCH race\n    if err != nil {\n        exited, _ := jobCmd.GetExitInfo()\n        if exited { return nil } // process gone; handle as exit\n        return err\n    }\n}","preventionTips":["Expect races with fast-exiting processes and code a single retry","Distinguish ESRCH (gone) from EPERM (permissions) after unwrapping","Avoid hidepid / sandbox configs that block /proc metadata reads"],"tags":["process","unix","pgid","race"],"backgroundTag":"process-group-lookup-failed","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}