1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
|
//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()
}
|