diff --git a/libpod/container_exec.go b/libpod/container_exec.go index aec429b0eb..fa5020f1c8 100644 --- a/libpod/container_exec.go +++ b/libpod/container_exec.go @@ -16,6 +16,7 @@ import ( "github.com/containers/common/pkg/util" "github.com/containers/podman/v5/libpod/define" "github.com/containers/podman/v5/libpod/events" + "github.com/containers/podman/v5/pkg/pidhandle" "github.com/containers/storage/pkg/stringid" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" @@ -101,6 +102,10 @@ type ExecSession struct { // Config is the configuration of this exec session. // Cannot be empty. Config *ExecConfig `json:"config"` + + // PIDData is a string uniquely identifying the PID. The value is platform + // specific. It is generated by pidhandle package and used by isSessionAlive. + PIDData string `json:"pidData,omitempty"` } // ID returns the ID of an exec session. @@ -251,6 +256,27 @@ func (c *Container) ExecCreate(config *ExecConfig) (string, error) { return session.Id, nil } +// Returns a serialized representation of the pid using the PIDHandle package. +// This string can be passed to NewPIDHandleFromString to recreate a PIDHandle +// that reliably refers to the same process as the original. +// +// NOTE that there is a still race in generating the PIDData on start but this +// is as good as we can do currently. Long term we could change conmon to +// send pidfd back directly. +func getPidData(pid int) string { + pidHandle, err := pidhandle.NewPIDHandle(pid) + if err != nil { + logrus.Debugf("getting the PID handle for pid %d: %v", pid, err) + return "" + } + defer pidHandle.Close() + pidData, err := pidHandle.String() + if err != nil { + logrus.Debugf("generating PIDData (%d) failed: %v", pid, err) + } + return pidData +} + // ExecStart starts an exec session in the container, but does not attach to it. // Returns immediately upon starting the exec session, unlike other ExecStart // functions, which will only return when the exec session exits. @@ -296,6 +322,7 @@ func (c *Container) ExecStart(sessionID string) error { // Update and save session to reflect PID/running session.PID = pid session.State = define.ExecStateRunning + session.PIDData = getPidData(pid) return c.save() } @@ -354,6 +381,7 @@ func (c *Container) execStartAndAttach(sessionID string, streams *define.AttachS // Update and save session to reflect PID/running session.PID = pid session.State = define.ExecStateRunning + session.PIDData = getPidData(pid) if err := c.save(); err != nil { lastErr = err @@ -535,6 +563,7 @@ func (c *Container) ExecHTTPStartAndAttach(sessionID string, r *http.Request, w session.PID = pid session.State = define.ExecStateRunning + session.PIDData = getPidData(pid) if err := c.save(); err != nil { lastErr = err @@ -1057,17 +1086,17 @@ func (c *Container) readExecExitCode(sessionID string) (int, error) { } // getExecSessionPID gets the PID of an active exec session -func (c *Container) getExecSessionPID(sessionID string) (int, error) { +func (c *Container) getExecSessionPID(sessionID string) (int, string, error) { session, ok := c.state.ExecSessions[sessionID] if ok { - return session.PID, nil + return session.PID, session.PIDData, nil } oldSession, ok := c.state.LegacyExecSessions[sessionID] if ok { - return oldSession.PID, nil + return oldSession.PID, "", nil } - return -1, fmt.Errorf("no exec session with ID %s found in container %s: %w", sessionID, c.ID(), define.ErrNoSuchExecSession) + return -1, "", fmt.Errorf("no exec session with ID %s found in container %s: %w", sessionID, c.ID(), define.ErrNoSuchExecSession) } // getKnownExecSessions gets a list of all exec sessions we think are running, @@ -1128,6 +1157,7 @@ func (c *Container) getActiveExecSessions() ([]string, error) { } session.ExitCode = exitCode session.PID = 0 + session.PIDData = "" session.State = define.ExecStateStopped c.newExecDiedEvent(session.ID(), exitCode) @@ -1262,6 +1292,7 @@ func justWriteExecExitCode(c *Container, sessionID string, exitCode int, emitEve session.State = define.ExecStateStopped session.ExitCode = exitCode session.PID = 0 + session.PIDData = "" // Finally, save our changes. return c.save() diff --git a/libpod/oci_conmon_exec_common.go b/libpod/oci_conmon_exec_common.go index a65e22fd79..4f09a3ba16 100644 --- a/libpod/oci_conmon_exec_common.go +++ b/libpod/oci_conmon_exec_common.go @@ -21,6 +21,7 @@ import ( "github.com/containers/podman/v5/libpod/define" "github.com/containers/podman/v5/pkg/errorhandling" "github.com/containers/podman/v5/pkg/lookup" + "github.com/containers/podman/v5/pkg/pidhandle" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" @@ -214,26 +215,32 @@ func (r *ConmonOCIRuntime) ExecAttachResize(ctr *Container, sessionID string, ne // ExecStopContainer stops a given exec session in a running container. func (r *ConmonOCIRuntime) ExecStopContainer(ctr *Container, sessionID string, timeout uint) error { - pid, err := ctr.getExecSessionPID(sessionID) + pid, pidData, err := ctr.getExecSessionPID(sessionID) if err != nil { return err } logrus.Debugf("Going to stop container %s exec session %s", ctr.ID(), sessionID) + pidHandle, err := pidhandle.NewPIDHandleFromString(pid, pidData) + if err != nil { + return fmt.Errorf("getting the PID handle for pid %d from '%s': %w", pid, pidData, err) + } + defer pidHandle.Close() + // Is the session dead? - // Ping the PID with signal 0 to see if it still exists. - if err := unix.Kill(pid, 0); err != nil { - if err == unix.ESRCH { - return nil - } - return fmt.Errorf("pinging container %s exec session %s PID %d with signal 0: %w", ctr.ID(), sessionID, pid, err) + sessionAlive, err := pidHandle.IsAlive() + if err != nil { + return fmt.Errorf("getting the process status for pid %d: %w", pid, err) + } + if !sessionAlive { + return nil } if timeout > 0 { // Use SIGTERM by default, then SIGSTOP after timeout. logrus.Debugf("Killing exec session %s (PID %d) of container %s with SIGTERM", sessionID, pid, ctr.ID()) - if err := unix.Kill(pid, unix.SIGTERM); err != nil { + if err := pidHandle.Kill(unix.SIGTERM); err != nil { if err == unix.ESRCH { return nil } @@ -251,7 +258,7 @@ func (r *ConmonOCIRuntime) ExecStopContainer(ctr *Container, sessionID string, t // SIGTERM did not work. On to SIGKILL. logrus.Debugf("Killing exec session %s (PID %d) of container %s with SIGKILL", sessionID, pid, ctr.ID()) - if err := unix.Kill(pid, unix.SIGTERM); err != nil { + if err := pidHandle.Kill(unix.SIGTERM); err != nil { if err == unix.ESRCH { return nil } @@ -268,23 +275,26 @@ func (r *ConmonOCIRuntime) ExecStopContainer(ctr *Container, sessionID string, t // ExecUpdateStatus checks if the given exec session is still running. func (r *ConmonOCIRuntime) ExecUpdateStatus(ctr *Container, sessionID string) (bool, error) { - pid, err := ctr.getExecSessionPID(sessionID) + pid, pidData, err := ctr.getExecSessionPID(sessionID) if err != nil { return false, err } logrus.Debugf("Checking status of container %s exec session %s", ctr.ID(), sessionID) + pidHandle, err := pidhandle.NewPIDHandleFromString(pid, pidData) + if err != nil { + return false, fmt.Errorf("getting the PID handle for pid %d from '%s': %w", pid, pidData, err) + } + defer pidHandle.Close() + // Is the session dead? - // Ping the PID with signal 0 to see if it still exists. - if err := unix.Kill(pid, 0); err != nil { - if err == unix.ESRCH { - return false, nil - } - return false, fmt.Errorf("pinging container %s exec session %s PID %d with signal 0: %w", ctr.ID(), sessionID, pid, err) + sessionAlive, err := pidHandle.IsAlive() + if err != nil { + return false, fmt.Errorf("getting the process status for pid %d: %w", pid, err) } - return true, nil + return sessionAlive, nil } // ExecAttachSocketPath is the path to a container's exec session attach socket. diff --git a/pkg/pidhandle/pidhandle.go b/pkg/pidhandle/pidhandle.go new file mode 100644 index 0000000000..db93dcc6d4 --- /dev/null +++ b/pkg/pidhandle/pidhandle.go @@ -0,0 +1,140 @@ +//go:build !windows + +// Package for handling processes and PIDs. +package pidhandle + +import ( + "strconv" + "strings" + + "github.com/shirou/gopsutil/v4/process" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +// PIDHandle defines an interface for working with operating system processes +// in a reliable way. OS-specific implementations include additional logic +// to try to ensure that operations (e.g., sending signals) are performed +// on the exact same process that was originally referenced when the PIDHandle +// was created via NewPIDHandle or NewPIDHandleFromString. +// +// This prevents accidental interaction with a different process in scenarios +// where the original process has exited and its PID has been reused by +// the system for an unrelated process. +type PIDHandle interface { + // Returns the PID associated with this PIDHandle. + PID() int + // Releases the PIDHandle resources. + Close() error + // Sends the signal to process. + Kill(signal unix.Signal) error + // Returns true in case the process is still alive. + IsAlive() (bool, error) + // 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. + String() (string, error) +} + +// The pidData value used when no process with this PID exists when creating +// the PIDHandle. +const noSuchProcessID = "no-proc" + +// The pidData prefix used when only the process start time (creation time) +// is supported when creating the PIDHandle to uniquely identify the process. +const startTimePrefix = "start-time:" + +type pidHandle struct { + pid int + pidData string +} + +// Returns the PID. +func (h *pidHandle) PID() int { + return h.pid +} + +// Close releases the PIDHandle resource. +func (h *pidHandle) Close() error { + // No resources for the default PIDHandle implementation. + return nil +} + +// Sends the signal to process. +func (h *pidHandle) Kill(signal unix.Signal) error { + if h.pidData == noSuchProcessID { + // The process did not exist when we created the PIDHandle, so return + // ESRCH error. + return unix.ESRCH + } + + // Get the start-time of the process and check if it is the same as + // the one we store in pidData. If it is not, we know that the PID + // has been recycled and return ESRCH error. + startTime, found := strings.CutPrefix(h.pidData, startTimePrefix) + if found { + p, err := process.NewProcess(int32(h.pid)) + if err != nil { + if err == process.ErrorProcessNotRunning { + return unix.ESRCH + } + return err + } + + ctime, err := p.CreateTime() + if err != nil { + return err + } + + if strconv.FormatInt(ctime, 10) != startTime { + return unix.ESRCH + } + } + + return unix.Kill(h.pid, signal) +} + +// Returns true in case the process is still alive. +func (h *pidHandle) 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 *pidHandle) String() (string, error) { + if len(h.pidData) != 0 { + return h.pidData, nil + } + + // Get the start-time of the process and return it as string. + p, err := process.NewProcess(int32(h.pid)) + if err != nil { + if err == process.ErrorProcessNotRunning { + return noSuchProcessID, nil + } + return "", err + } + + ctime, err := p.CreateTime() + if err != nil { + // The process existed, but we cannot get its start-time. There is + // either an issue with getting it, or the process terminated in the + // mean-time. We have no way to find out what actually happened, so + // in this case, we just fallback to an empty string. This will mean + // that Kill or IsAlive might kill wrong process in rare situation + // when CreateTime() failed for different reason than the process + // terminated... + logrus.Debugf("Getting CreateTime for process (%d) failed: %v", h.pid, err) + return "", nil + } + + return startTimePrefix + strconv.FormatInt(ctime, 10), nil +} diff --git a/pkg/pidhandle/pidhandle_linux.go b/pkg/pidhandle/pidhandle_linux.go new file mode 100644 index 0000000000..2420ed86c4 --- /dev/null +++ b/pkg/pidhandle/pidhandle_linux.go @@ -0,0 +1,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() +} diff --git a/pkg/pidhandle/pidhandle_linux_test.go b/pkg/pidhandle/pidhandle_linux_test.go new file mode 100644 index 0000000000..82f7c41343 --- /dev/null +++ b/pkg/pidhandle/pidhandle_linux_test.go @@ -0,0 +1,235 @@ +//go:build linux + +package pidhandle + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/sys/unix" +) + +func TestNewPIDHandle(t *testing.T) { + // Mock the pidfdOpen + original := pidfdOpen + defer func() { pidfdOpen = original }() + pidfdOpen = func(pid int, flags int) (int, error) { + return 255, nil + } + + // Mock the nameToHandleAt + original_nameToHandleAt := nameToHandleAt + defer func() { nameToHandleAt = original_nameToHandleAt }() + nameToHandleAt = func(dirfd int, path string, flags int) (handle unix.FileHandle, mountID int, err error) { + return newFileHandle(254, []byte("test")), 1, nil + } + + h, err := NewPIDHandle(os.Getpid()) + assert.NoError(t, err) + defer h.Close() + + pidData, err := h.String() + assert.NoError(t, err) + assert.Equal(t, nameToHandlePrefix+"254 74657374", pidData) + assert.Equal(t, h.PID(), os.Getpid()) +} + +func TestNewPIDHandlepidfdOpenNotSupported(t *testing.T) { + // Mock the pidfdOpen + original := pidfdOpen + defer func() { pidfdOpen = original }() + pidfdOpen = func(pid int, flags int) (int, error) { + return -1, unix.ENOSYS + } + + h, err := NewPIDHandle(os.Getpid()) + assert.NoError(t, err) + defer h.Close() + + pidData, err := h.String() + assert.NoError(t, err) + assert.Contains(t, pidData, startTimePrefix) + assert.Equal(t, h.PID(), os.Getpid()) +} + +func TestPIDHandleStringnameToHandleAtNotSupported(t *testing.T) { + // Mock the pidfdOpen + original := pidfdOpen + defer func() { pidfdOpen = original }() + pidfdOpen = func(pid int, flags int) (int, error) { + return 254, nil + } + + // Mock the nameToHandleAt + original_nameToHandleAt := nameToHandleAt + defer func() { nameToHandleAt = original_nameToHandleAt }() + nameToHandleAt = func(dirfd int, path string, flags int) (handle unix.FileHandle, mountID int, err error) { + return newFileHandle(-1, []byte("")), 1, unix.ENOTSUP + } + + h, err := NewPIDHandle(os.Getpid()) + assert.NoError(t, err) + defer h.Close() + + pidData, err := h.String() + assert.NoError(t, err) + assert.Contains(t, pidData, startTimePrefix) +} + +func TestNewPIDHandleFromString(t *testing.T) { + // Mock the pidfdOpen + original := pidfdOpen + defer func() { pidfdOpen = original }() + pidfdOpen = func(pid int, flags int) (int, error) { + return 254, nil + } + + // Mock the newFileHandle + original_newFileHandle := newFileHandle + defer func() { newFileHandle = original_newFileHandle }() + newFileHandle = func(fhType int32, bytes []byte) unix.FileHandle { + return unix.NewFileHandle(254, []byte("test")) + } + + // Mock the openByHandleAt + original_openByHandleAt := openByHandleAt + defer func() { openByHandleAt = original_openByHandleAt }() + openByHandleAt = func(mountFD int, handle unix.FileHandle, flags int) (fd int, err error) { + return 255, nil + } + + // Mock the nameToHandleAt + original_nameToHandleAt := nameToHandleAt + defer func() { nameToHandleAt = original_nameToHandleAt }() + nameToHandleAt = func(dirfd int, path string, flags int) (handle unix.FileHandle, mountID int, err error) { + return newFileHandle(255, []byte("test")), 1, nil + } + + h, err := NewPIDHandleFromString(os.Getpid(), nameToHandlePrefix+"254 74657374") + assert.NoError(t, err) + defer h.Close() + + pidData, err := h.String() + assert.NoError(t, err) + assert.Equal(t, nameToHandlePrefix+"254 74657374", pidData) + assert.Equal(t, h.PID(), os.Getpid()) +} + +func TestNewPIDHandleFromStringWrongPidData(t *testing.T) { + // Mock the pidfdOpen + original := pidfdOpen + defer func() { pidfdOpen = original }() + pidfdOpen = func(pid int, flags int) (int, error) { + return 254, nil + } + + // Mock the newFileHandle + original_newFileHandle := newFileHandle + defer func() { newFileHandle = original_newFileHandle }() + newFileHandle = func(fhType int32, bytes []byte) unix.FileHandle { + return unix.NewFileHandle(254, []byte("test")) + } + + // Mock the openByHandleAt + original_openByHandleAt := openByHandleAt + defer func() { openByHandleAt = original_openByHandleAt }() + openByHandleAt = func(mountFD int, handle unix.FileHandle, flags int) (fd int, err error) { + return 255, nil + } + + values := []string{ + nameToHandlePrefix + "foo", + nameToHandlePrefix + "254 foo", + nameToHandlePrefix + "254 foo bar", + nameToHandlePrefix + "foo 1245", + } + + for _, s := range values { + _, err := NewPIDHandleFromString(os.Getpid(), s) + assert.Error(t, err) + } +} + +func TestPIDHandlePidfdStartTime(t *testing.T) { + h, err := NewPIDHandleFromString(os.Getpid(), "start-time:1234567890") + assert.NoError(t, err) + defer h.Close() +} + +func TestPIDHandleKill(t *testing.T) { + // Mock the pidfdOpen + original := pidfdOpen + defer func() { pidfdOpen = original }() + pidfdOpen = func(pid int, flags int) (int, error) { + return 254, nil + } + + // Mock the pidfdSendSignal + original_pidfdSendSignal := pidfdSendSignal + defer func() { pidfdSendSignal = original_pidfdSendSignal }() + pidfdSendSignal = func(pidfd int, sig unix.Signal, info *unix.Siginfo, flags int) (err error) { + return unix.ESRCH + } + + // Mock the nameToHandleAt + original_nameToHandleAt := nameToHandleAt + defer func() { nameToHandleAt = original_nameToHandleAt }() + nameToHandleAt = func(dirfd int, path string, flags int) (handle unix.FileHandle, mountID int, err error) { + return newFileHandle(254, []byte("test")), 1, nil + } + + h, err := NewPIDHandle(os.Getpid()) + assert.NoError(t, err) + defer h.Close() + + err = h.Kill(0) + assert.ErrorIs(t, err, unix.ESRCH) +} + +func TestPIDHandleKillPidfdNotSupported(t *testing.T) { + // Mock the pidfdOpen + original := pidfdOpen + defer func() { pidfdOpen = original }() + pidfdOpen = func(pid int, flags int) (int, error) { + return -1, unix.ENOSYS + } + + h, err := NewPIDHandle(os.Getpid()) + assert.NoError(t, err) + defer h.Close() + + pidData, err := h.String() + assert.NoError(t, err) + h, err = NewPIDHandleFromString(os.Getpid(), pidData) + assert.NoError(t, err) + + err = h.Kill(0) + assert.NoError(t, err) + isAlive, err := h.IsAlive() + assert.NoError(t, err) + assert.True(t, isAlive) +} + +func TestPIDHandleKillPidfdNotSupportedStartTimeNotMatch(t *testing.T) { + // Mock the pidfdOpen + original := pidfdOpen + defer func() { pidfdOpen = original }() + pidfdOpen = func(pid int, flags int) (int, error) { + return -1, unix.ENOSYS + } + + h, err := NewPIDHandle(os.Getpid()) + assert.NoError(t, err) + defer h.Close() + + h, err = NewPIDHandleFromString(os.Getpid(), "start-time:1234567890") + assert.NoError(t, err) + + err = h.Kill(0) + assert.ErrorIs(t, err, unix.ESRCH) + + isAlive, err := h.IsAlive() + assert.NoError(t, err) + assert.False(t, isAlive) +} diff --git a/pkg/pidhandle/pidhandle_unix.go b/pkg/pidhandle/pidhandle_unix.go new file mode 100644 index 0000000000..da9f79cf69 --- /dev/null +++ b/pkg/pidhandle/pidhandle_unix.go @@ -0,0 +1,24 @@ +//go:build !linux && !windows + +// Package for handling processes and PIDs. +package pidhandle + +// Creates new PIDHandle for a given process pid. +func NewPIDHandle(pid int) (PIDHandle, error) { + h := pidHandle{pid: pid} + pidData, err := h.String() + if err != nil { + return nil, err + } + h.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) { + return &pidHandle{ + pid: pid, + pidData: pidData, + }, nil +} diff --git a/winmake.ps1 b/winmake.ps1 index ad032168ed..f757d08eac 100644 --- a/winmake.ps1 +++ b/winmake.ps1 @@ -66,7 +66,8 @@ function Local-Unit { Build-Ginkgo $skippackages="hack,internal\domain\infra\abi,internal\domain\infra\tunnel,libpod\lock\shm,pkg\api\handlers\libpod,pkg\api\handlers\utils,pkg\bindings," $skippackages+="pkg\domain\infra\abi,pkg\emulation,pkg\machine\apple,pkg\machine\applehv,pkg\machine\e2e,pkg\machine\libkrun," - $skippackages+="pkg\machine\provider,pkg\machine\proxyenv,pkg\machine\qemu,pkg\specgen\generate,pkg\systemd,test\e2e,test\utils,cmd\rootlessport" + $skippackages+="pkg\machine\provider,pkg\machine\proxyenv,pkg\machine\qemu,pkg\specgen\generate,pkg\systemd,test\e2e,test\utils,cmd\rootlessport," + $skippackages+="pkg\pidhandle" Run-Command "./bin/ginkgo.exe -vv -r --tags `"$remotetags`" --timeout=15m --trace --no-color --skip-package `"$skippackages`"" }