package supervisor import ( "bytes" "os" "strconv" "syscall" ) // killGroup signals a whole process group. // // Engines fork worker processes that hold GPU memory. Signalling only the // leader leaves those workers alive, and the next launch then fails to // allocate — with an error that points at the new process rather than the old. func killGroup(pid int, sig syscall.Signal) error { // The negative pid addresses the group. Setpgid at launch made the leader's // pid the group id. if err := syscall.Kill(-pid, sig); err != nil { if err == syscall.ESRCH { return os.ErrProcessDone } return err } return nil } // processAlive reports whether a pid belongs to a process that is still doing // something. // // A zombie is not. It has exited and is only waiting to be reaped, but // kill(pid, 0) still succeeds on it — so a naive check reports a dead engine as // running forever. That happens for real whenever an engine's parent dies // before it does and nothing reaps the orphan: in a container, PID 1 is usually // the workload rather than an init, and orphans simply accumulate. func processAlive(pid int) bool { if pid <= 0 { return false } // Signal 0 performs the permission and existence checks without delivering // anything. if err := syscall.Kill(pid, 0); err != nil { // EPERM means it exists but belongs to someone else, which for our // purposes is still "there". return err == syscall.EPERM } return !isZombie(pid) } // isZombie reports whether a pid has exited but not been reaped. // // Only Linux exposes this; elsewhere there is no procfs and the answer is // false, which degrades to the old behaviour rather than to something worse. func isZombie(pid int) bool { b, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat") if err != nil { return false } // The comm field is parenthesised and may itself contain spaces or // parentheses, so the state character is found from the last ')'. i := bytes.LastIndexByte(b, ')') if i < 0 || i+2 >= len(b) { return false } return b[i+2] == 'Z' }