//go:build linux // Package for handling processes and PIDs. package pidhandle import ( "encoding/hex" "fmt" "os" "strconv" "strings" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) type pidfdHandle struct { pidfd int normalHandle pidHandle } // Store the "unix." methods in variables so we can mock them // in the unit-tests and test out different return value. var ( pidfdOpen = unix.PidfdOpen newFileHandle = unix.NewFileHandle openByHandleAt = unix.OpenByHandleAt nameToHandleAt = unix.NameToHandleAt pidfdSendSignal = unix.PidfdSendSignal ) // The pidData prefix used when the pidfd and name_to_handle is supported // when creating the PIDHandle to uniquely identify the process. const nameToHandlePrefix = "name-to-handle:" // Creates new PIDHandle for a given process pid. // // Note that there still can be a race condition if the process terminates // *before* the PIDHandle is created. It is a caller's responsibility // to ensure that this either cannot happen or accept this risk. func NewPIDHandle(pid int) (PIDHandle, error) { // Use the pidfd to obtain the file-descriptor pointing to the process. pidData := "" pidfd, err := pidfdOpen(pid, 0) if err != nil { switch err { case unix.ENOSYS: // Do not fail if PidFdOpen is not supported, we will // fallback to process start-time later. case unix.ESRCH: // The process does not exist, so any future call of Kill // or IsAlive should return unix.ESRCH, even if the pid is // recycled in the future. Let's note it in the pidData. pidData = noSuchProcessID case unix.EINVAL: // The PidfdOpen returns EINVAL if pid is invalid or if it refers // to a thread and not to process. This is not a valid PID for // PIDHandle and it most likely means the pid has been recycled // (or there is a programming error). We therefore store // noSuchProcessID into pidData to return unix.ESRCH in // the future Kill or IsAlive calls. pidData = noSuchProcessID default: return nil, fmt.Errorf("pidfdOpen failed: %w", err) } } h := pidfdHandle{ pidfd: pidfd, normalHandle: pidHandle{pid: pid, pidData: pidData}, } pidData, err = h.String() if err != nil { return nil, err } h.normalHandle.pidData = pidData return &h, nil } // Creates new PIDHandle for a given process pid using the pidData // originally obtained from PIDHandle.String(). func NewPIDHandleFromString(pid int, pidData string) (PIDHandle, error) { h := pidfdHandle{ pidfd: -1, normalHandle: pidHandle{pid: pid, pidData: pidData}, } // Open the pidfd encoded in pidData. data, found := strings.CutPrefix(pidData, nameToHandlePrefix) if found { // Split the data. parts := strings.SplitN(data, " ", 2) if len(parts) != 2 { return nil, fmt.Errorf("invalid format, expected 2 parts") } // Parse fhType. fhTypeInt, err := strconv.Atoi(parts[0]) if err != nil { return nil, err } fhType := int32(fhTypeInt) // Decode hex string to bytes. bytes, err := hex.DecodeString(parts[1]) if err != nil { return nil, err } // Create FileHandle and open it. fh := newFileHandle(fhType, bytes) fd, err := pidfdOpen(os.Getpid(), 0) if err != nil { return nil, err } defer unix.Close(fd) pidfd, err := openByHandleAt(fd, fh, 0) if err != nil { if err == unix.ESTALE { h.normalHandle.pidData = noSuchProcessID return &h, nil } return nil, fmt.Errorf("openByHandleAt failed: %w", err) } h.pidfd = pidfd return &h, nil } return &h, nil } // Returns the PID associated with this PIDHandle. func (h *pidfdHandle) PID() int { return h.normalHandle.PID() } // Close releases the pidfd resource. func (h *pidfdHandle) Close() error { if h.pidfd != 0 { err := unix.Close(h.pidfd) if err != nil { return fmt.Errorf("failed to close pidfd: %w", err) } h.pidfd = 0 } return h.normalHandle.Close() } // Sends the signal to process. func (h *pidfdHandle) Kill(signal unix.Signal) error { if h.pidfd > -1 { return pidfdSendSignal(h.pidfd, signal, nil, 0) } return h.normalHandle.Kill(signal) } // Returns true in case the process is still alive. func (h *pidfdHandle) IsAlive() (bool, error) { err := h.Kill(0) if err != nil { if err == unix.ESRCH { return false, nil } return false, err } return true, nil } // Returns a serialized representation of the PIDHandle. // This string can be passed to NewPIDHandleFromString to recreate // a PIDHandle that reliably refers to the same process as the original. func (h *pidfdHandle) String() (string, error) { if len(h.normalHandle.pidData) != 0 { return h.normalHandle.pidData, nil } // Serialize the pidfd to string if possible. if h.pidfd > -1 { fh, _, err := nameToHandleAt(h.pidfd, "", unix.AT_EMPTY_PATH) if err != nil { // Do not fail if NameToHandleAt is not supported, we will // fallback to process start-time later. if err == unix.ENOTSUP { logrus.Debugf("NameToHandleAt(%d) failed: %v", h.pidfd, err) } else { return "", err } } else { hexStr := hex.EncodeToString(fh.Bytes()) return nameToHandlePrefix + strconv.Itoa(int(fh.Type())) + " " + hexStr, nil } } // Fallback to default String(). return h.normalHandle.String() }