pidfd 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pidfd/__init__.py +41 -0
- pidfd/_syscall.py +163 -0
- pidfd/errors.py +10 -0
- pidfd/pidfd.py +222 -0
- pidfd/py.typed +0 -0
- pidfd-0.1.0.dist-info/METADATA +79 -0
- pidfd-0.1.0.dist-info/RECORD +9 -0
- pidfd-0.1.0.dist-info/WHEEL +4 -0
- pidfd-0.1.0.dist-info/licenses/LICENSE +12 -0
pidfd/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# SPDX-License-Identifier: 0BSD
|
|
2
|
+
"""Python bindings for Linux pidfds.
|
|
3
|
+
|
|
4
|
+
A pidfd is a file descriptor referring to a process. It is a stable
|
|
5
|
+
handle: signaling through it can never hit a recycled pid, it becomes
|
|
6
|
+
pollable-readable when the task exits, and it can be waited on with
|
|
7
|
+
waitid(2) or used to duplicate the task's file descriptors with
|
|
8
|
+
pidfd_getfd(2). Everything goes through raw syscalls via ctypes. There
|
|
9
|
+
are no runtime dependencies.
|
|
10
|
+
|
|
11
|
+
Kernel references: pidfd_open(2), pidfd_send_signal(2), pidfd_getfd(2),
|
|
12
|
+
waitid(2).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .errors import PidFdError, UnsupportedError
|
|
16
|
+
from .pidfd import (
|
|
17
|
+
OpenFlag,
|
|
18
|
+
PidFd,
|
|
19
|
+
SigInfo,
|
|
20
|
+
SignalFlag,
|
|
21
|
+
WaitCode,
|
|
22
|
+
WaitFlag,
|
|
23
|
+
WaitResult,
|
|
24
|
+
pidfd_send_signal,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"OpenFlag",
|
|
31
|
+
"PidFd",
|
|
32
|
+
"PidFdError",
|
|
33
|
+
"SigInfo",
|
|
34
|
+
"SignalFlag",
|
|
35
|
+
"UnsupportedError",
|
|
36
|
+
"WaitCode",
|
|
37
|
+
"WaitFlag",
|
|
38
|
+
"WaitResult",
|
|
39
|
+
"__version__",
|
|
40
|
+
"pidfd_send_signal",
|
|
41
|
+
]
|
pidfd/_syscall.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# SPDX-License-Identifier: 0BSD
|
|
2
|
+
"""Raw ctypes bindings for the pidfd syscalls and waitid(2).
|
|
3
|
+
|
|
4
|
+
glibc has no wrappers for pidfd_open, pidfd_send_signal or pidfd_getfd,
|
|
5
|
+
so all four calls go through libc's syscall(2). The numbers differ per
|
|
6
|
+
architecture for waitid only. The three pidfd calls share the common
|
|
7
|
+
numbering on every supported arch.
|
|
8
|
+
|
|
9
|
+
Kernel references: pidfd_open(2), pidfd_send_signal(2), pidfd_getfd(2),
|
|
10
|
+
waitid(2), asm/unistd*.h, asm-generic/unistd.h.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import ctypes
|
|
14
|
+
import ctypes.util
|
|
15
|
+
import errno
|
|
16
|
+
import os
|
|
17
|
+
import platform
|
|
18
|
+
import sys
|
|
19
|
+
from typing import NoReturn
|
|
20
|
+
|
|
21
|
+
from .errors import PidFdError, UnsupportedError
|
|
22
|
+
|
|
23
|
+
P_PIDFD = 3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _SigChld(ctypes.Structure):
|
|
27
|
+
"""The _sifields._sigchld member of the kernel siginfo_t."""
|
|
28
|
+
|
|
29
|
+
_fields_ = [
|
|
30
|
+
("si_pid", ctypes.c_int32),
|
|
31
|
+
("si_uid", ctypes.c_uint32),
|
|
32
|
+
("si_status", ctypes.c_int32),
|
|
33
|
+
("si_utime", ctypes.c_long),
|
|
34
|
+
("si_stime", ctypes.c_long),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _SiFields(ctypes.Union):
|
|
39
|
+
"""union __sifields, padded so the whole siginfo stays writable."""
|
|
40
|
+
|
|
41
|
+
_fields_ = [
|
|
42
|
+
("sigchld", _SigChld),
|
|
43
|
+
("pad", ctypes.c_byte * 128),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SigInfo(ctypes.Structure):
|
|
48
|
+
"""The kernel siginfo_t used by waitid(2) and pidfd_send_signal(2).
|
|
49
|
+
|
|
50
|
+
Only the SIGCHLD member of union __sifields is decoded. The rest is
|
|
51
|
+
padding. The buffer is deliberately larger than the kernel's
|
|
52
|
+
128-byte siginfo_t so waitid's user access check always succeeds.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
_fields_ = [
|
|
56
|
+
("si_signo", ctypes.c_int),
|
|
57
|
+
("si_errno", ctypes.c_int),
|
|
58
|
+
("si_code", ctypes.c_int),
|
|
59
|
+
("_sifields", _SiFields),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def si_pid(self) -> int:
|
|
64
|
+
return int(self._sifields.sigchld.si_pid)
|
|
65
|
+
|
|
66
|
+
@si_pid.setter
|
|
67
|
+
def si_pid(self, value: int) -> None:
|
|
68
|
+
self._sifields.sigchld.si_pid = value
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def si_uid(self) -> int:
|
|
72
|
+
return int(self._sifields.sigchld.si_uid)
|
|
73
|
+
|
|
74
|
+
@si_uid.setter
|
|
75
|
+
def si_uid(self, value: int) -> None:
|
|
76
|
+
self._sifields.sigchld.si_uid = value
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def si_status(self) -> int:
|
|
80
|
+
return int(self._sifields.sigchld.si_status)
|
|
81
|
+
|
|
82
|
+
@si_status.setter
|
|
83
|
+
def si_status(self, value: int) -> None:
|
|
84
|
+
self._sifields.sigchld.si_status = value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
_libc: ctypes.CDLL | None = None
|
|
88
|
+
_numbers: tuple[int, int, int, int] | None = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _get_libc() -> ctypes.CDLL:
|
|
92
|
+
global _libc
|
|
93
|
+
if _libc is None:
|
|
94
|
+
if sys.platform != "linux":
|
|
95
|
+
raise UnsupportedError("pidfds are only available on Linux")
|
|
96
|
+
name = ctypes.util.find_library("c")
|
|
97
|
+
_libc = ctypes.CDLL(name or None, use_errno=True)
|
|
98
|
+
_libc.syscall.restype = ctypes.c_long
|
|
99
|
+
return _libc
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _syscall_numbers() -> tuple[int, int, int, int]:
|
|
103
|
+
"""Return (pidfd_open, pidfd_send_signal, pidfd_getfd, waitid)."""
|
|
104
|
+
global _numbers
|
|
105
|
+
if _numbers is not None:
|
|
106
|
+
return _numbers
|
|
107
|
+
machine = platform.machine().lower()
|
|
108
|
+
if machine in ("x86_64", "amd64"):
|
|
109
|
+
if ctypes.sizeof(ctypes.c_void_p) == 4:
|
|
110
|
+
# The x32 ABI carries different numbers and a differently
|
|
111
|
+
# aligned siginfo_t. Reject it rather than call the wrong
|
|
112
|
+
# syscall.
|
|
113
|
+
raise UnsupportedError("the x32 ABI is not supported")
|
|
114
|
+
_numbers = (434, 424, 438, 247)
|
|
115
|
+
elif machine in ("i386", "i486", "i586", "i686", "x86"):
|
|
116
|
+
_numbers = (434, 424, 438, 284)
|
|
117
|
+
elif machine in ("aarch64", "arm64", "riscv32", "riscv64", "loongarch64"):
|
|
118
|
+
_numbers = (434, 424, 438, 95)
|
|
119
|
+
elif machine in ("armv6l", "armv7l", "arm"):
|
|
120
|
+
_numbers = (434, 424, 438, 280)
|
|
121
|
+
elif machine.startswith("ppc"):
|
|
122
|
+
_numbers = (434, 424, 438, 272)
|
|
123
|
+
elif machine.startswith("s390"):
|
|
124
|
+
_numbers = (434, 424, 438, 281)
|
|
125
|
+
else:
|
|
126
|
+
raise UnsupportedError(f"no pidfd syscall numbers for architecture {machine}")
|
|
127
|
+
return _numbers
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _raise_errno(err: int) -> NoReturn:
|
|
131
|
+
if err == errno.ENOSYS:
|
|
132
|
+
raise UnsupportedError(err, os.strerror(err))
|
|
133
|
+
raise PidFdError(err, os.strerror(err))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _call(nr: int, *args: object) -> int:
|
|
137
|
+
ret = int(_get_libc().syscall(nr, *args))
|
|
138
|
+
if ret == -1:
|
|
139
|
+
_raise_errno(ctypes.get_errno())
|
|
140
|
+
return ret
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def pidfd_open(pid: int, flags: int = 0) -> int:
|
|
144
|
+
"""Open a pidfd for pid and return the new file descriptor."""
|
|
145
|
+
return _call(_syscall_numbers()[0], pid, flags)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def pidfd_send_signal(
|
|
149
|
+
pidfd: int, sig: int, info: SigInfo | None, flags: int = 0
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Send sig to the process the pidfd refers to."""
|
|
152
|
+
ref = ctypes.byref(info) if info is not None else None
|
|
153
|
+
_call(_syscall_numbers()[1], pidfd, sig, ref, flags)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def pidfd_getfd(pidfd: int, targetfd: int, flags: int = 0) -> int:
|
|
157
|
+
"""Duplicate targetfd from the pidfd's process into our fd table."""
|
|
158
|
+
return _call(_syscall_numbers()[2], pidfd, targetfd, flags)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def waitid(idtype: int, upid: int, info: SigInfo, options: int) -> int:
|
|
162
|
+
"""waitid(2) filling info; the rusage argument stays NULL."""
|
|
163
|
+
return _call(_syscall_numbers()[3], idtype, upid, ctypes.byref(info), options, 0)
|
pidfd/errors.py
ADDED
pidfd/pidfd.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# SPDX-License-Identifier: 0BSD
|
|
2
|
+
"""PidFd: a file descriptor that refers to a process.
|
|
3
|
+
|
|
4
|
+
A pidfd is a stable reference to a task: unlike a raw pid it cannot be
|
|
5
|
+
recycled, so signaling through it cannot hit an unrelated process. The
|
|
6
|
+
descriptor becomes readable (POLLIN) once the task exits and turns into
|
|
7
|
+
a zombie, and reports POLLHUP once it is reaped. waitid(2) with idtype
|
|
8
|
+
P_PIDFD reaps a child through it.
|
|
9
|
+
|
|
10
|
+
Kernel references: pidfd_open(2), pidfd_send_signal(2), pidfd_getfd(2),
|
|
11
|
+
waitid(2), linux/pidfd.h.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
import os
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from enum import IntEnum, IntFlag
|
|
20
|
+
from types import TracebackType
|
|
21
|
+
|
|
22
|
+
from . import _syscall
|
|
23
|
+
from ._syscall import SigInfo
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"OpenFlag",
|
|
27
|
+
"PidFd",
|
|
28
|
+
"SigInfo",
|
|
29
|
+
"SignalFlag",
|
|
30
|
+
"WaitCode",
|
|
31
|
+
"WaitFlag",
|
|
32
|
+
"WaitResult",
|
|
33
|
+
"pidfd_send_signal",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OpenFlag(IntFlag):
|
|
38
|
+
"""Flags for pidfd_open(2), mirroring linux/pidfd.h.
|
|
39
|
+
|
|
40
|
+
NONBLOCK makes wait() fail with EAGAIN instead of blocking while the
|
|
41
|
+
task is still running (Linux 5.10+). THREAD pins the fd to a single
|
|
42
|
+
thread instead of the thread-group leader (Linux 6.9+).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
NONBLOCK = 0o4000 # O_NONBLOCK
|
|
46
|
+
THREAD = 0o200 # O_EXCL
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SignalFlag(IntFlag):
|
|
50
|
+
"""Scope flags for pidfd_send_signal(2), Linux 6.9+."""
|
|
51
|
+
|
|
52
|
+
THREAD = 1 << 0
|
|
53
|
+
THREAD_GROUP = 1 << 1
|
|
54
|
+
PROCESS_GROUP = 1 << 2
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class WaitFlag(IntFlag):
|
|
58
|
+
"""Option bits for waitid(2), mirroring linux/wait.h.
|
|
59
|
+
|
|
60
|
+
At least one of WEXITED, WSTOPPED or WCONTINUED is required.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
WNOHANG = 0x00000001
|
|
64
|
+
WSTOPPED = 0x00000002
|
|
65
|
+
WEXITED = 0x00000004
|
|
66
|
+
WCONTINUED = 0x00000008
|
|
67
|
+
WNOWAIT = 0x01000000
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class WaitCode(IntEnum):
|
|
71
|
+
"""si_code values the kernel reports in a SIGCHLD siginfo (CLD_*)."""
|
|
72
|
+
|
|
73
|
+
CLD_EXITED = 1
|
|
74
|
+
CLD_KILLED = 2
|
|
75
|
+
CLD_DUMPED = 3
|
|
76
|
+
CLD_TRAPPED = 4
|
|
77
|
+
CLD_STOPPED = 5
|
|
78
|
+
CLD_CONTINUED = 6
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class WaitResult:
|
|
83
|
+
"""The fields waitid(2) fills into the siginfo_t for a child.
|
|
84
|
+
|
|
85
|
+
code holds a WaitCode (CLD_*) value. Its meaning for status is that
|
|
86
|
+
of waitpid(2): an exit status when code is CLD_EXITED, a signal
|
|
87
|
+
number for CLD_KILLED/CLD_DUMPED/CLD_STOPPED/CLD_CONTINUED. A result
|
|
88
|
+
with signo 0 means the call returned without an event, which only
|
|
89
|
+
happens when WNOHANG was requested.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
pid: int
|
|
93
|
+
uid: int
|
|
94
|
+
signo: int
|
|
95
|
+
code: int
|
|
96
|
+
status: int
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class PidFd:
|
|
100
|
+
"""A pidfd file descriptor referring to a process.
|
|
101
|
+
|
|
102
|
+
Obtain one with open() or wrap an existing descriptor in the
|
|
103
|
+
constructor. Instances own their fd: close() is idempotent and the
|
|
104
|
+
context manager closes on exit.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, fd: int) -> None:
|
|
108
|
+
self._fd = -1 # keeps __del__ safe when validation fails
|
|
109
|
+
if fd < 0:
|
|
110
|
+
raise ValueError(f"fd must not be negative: {fd}")
|
|
111
|
+
self._fd = fd
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def open(cls, pid: int, flags: OpenFlag | int = 0) -> PidFd:
|
|
115
|
+
"""Open a pidfd for pid via pidfd_open(2) (Linux 5.3+)."""
|
|
116
|
+
return cls(_syscall.pidfd_open(int(pid), int(flags)))
|
|
117
|
+
|
|
118
|
+
def fileno(self) -> int:
|
|
119
|
+
"""Return the underlying descriptor, or -1 once closed."""
|
|
120
|
+
return self._fd
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def closed(self) -> bool:
|
|
124
|
+
"""Whether close() has released the descriptor."""
|
|
125
|
+
return self._fd < 0
|
|
126
|
+
|
|
127
|
+
def _check_open(self) -> int:
|
|
128
|
+
if self._fd < 0:
|
|
129
|
+
raise ValueError("pidfd is closed")
|
|
130
|
+
return self._fd
|
|
131
|
+
|
|
132
|
+
def close(self) -> None:
|
|
133
|
+
"""Close the descriptor. Safe to call more than once."""
|
|
134
|
+
if self._fd >= 0:
|
|
135
|
+
fd, self._fd = self._fd, -1
|
|
136
|
+
os.close(fd)
|
|
137
|
+
|
|
138
|
+
def detach(self) -> int:
|
|
139
|
+
"""Release the descriptor without closing it.
|
|
140
|
+
|
|
141
|
+
Returns the raw fd and marks the PidFd closed, transferring
|
|
142
|
+
ownership of the descriptor to the caller.
|
|
143
|
+
"""
|
|
144
|
+
fd = self._check_open()
|
|
145
|
+
self._fd = -1
|
|
146
|
+
return fd
|
|
147
|
+
|
|
148
|
+
def signal(
|
|
149
|
+
self,
|
|
150
|
+
sig: int,
|
|
151
|
+
siginfo: SigInfo | None = None,
|
|
152
|
+
flags: SignalFlag | int = 0,
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Send sig to the referenced process via pidfd_send_signal(2).
|
|
155
|
+
|
|
156
|
+
siginfo=None selects the same implicit fields kill(2) supplies.
|
|
157
|
+
Pass a populated SigInfo for rt_sigqueueinfo(2)-style delivery,
|
|
158
|
+
where si_code must be a negative SI_* value such as SI_QUEUE
|
|
159
|
+
when the target is not the caller. flags accepts the SignalFlag
|
|
160
|
+
bits on Linux 6.9+. 0 is the only value older kernels allow.
|
|
161
|
+
ESRCH means the task is already gone
|
|
162
|
+
and reaped, never that the pid was recycled.
|
|
163
|
+
"""
|
|
164
|
+
_syscall.pidfd_send_signal(self._check_open(), int(sig), siginfo, int(flags))
|
|
165
|
+
|
|
166
|
+
def wait(self, flags: WaitFlag | int = WaitFlag.WEXITED) -> WaitResult:
|
|
167
|
+
"""waitid(P_PIDFD) on the referenced child (Linux 5.4+).
|
|
168
|
+
|
|
169
|
+
The pidfd must refer to a child of the caller, otherwise the
|
|
170
|
+
kernel answers ECHILD. A pidfd opened with OpenFlag.NONBLOCK
|
|
171
|
+
fails with EAGAIN while the child is still running. On a
|
|
172
|
+
blocking pidfd add WaitFlag.WNOHANG for a poll-style call that
|
|
173
|
+
returns a WaitResult with signo 0.
|
|
174
|
+
"""
|
|
175
|
+
info = SigInfo()
|
|
176
|
+
_syscall.waitid(_syscall.P_PIDFD, self._check_open(), info, int(flags))
|
|
177
|
+
return WaitResult(
|
|
178
|
+
pid=info.si_pid,
|
|
179
|
+
uid=info.si_uid,
|
|
180
|
+
signo=info.si_signo,
|
|
181
|
+
code=info.si_code,
|
|
182
|
+
status=info.si_status,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def getfd(self, targetfd: int, flags: int = 0) -> int:
|
|
186
|
+
"""Duplicate targetfd of the referenced process into our fd table.
|
|
187
|
+
|
|
188
|
+
The returned fd shares the open file description with the
|
|
189
|
+
target's fd and has close-on-exec set. Access is governed by a
|
|
190
|
+
PTRACE_MODE_ATTACH_REALCREDS ptrace check (see ptrace(2)): under
|
|
191
|
+
yama ptrace_scope that typically means the target must be a
|
|
192
|
+
descendant, or the caller needs CAP_SYS_PTRACE.
|
|
193
|
+
"""
|
|
194
|
+
return _syscall.pidfd_getfd(self._check_open(), int(targetfd), int(flags))
|
|
195
|
+
|
|
196
|
+
def __enter__(self) -> PidFd:
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
def __exit__(
|
|
200
|
+
self,
|
|
201
|
+
exc_type: type[BaseException] | None,
|
|
202
|
+
exc_value: BaseException | None,
|
|
203
|
+
traceback: TracebackType | None,
|
|
204
|
+
) -> None:
|
|
205
|
+
self.close()
|
|
206
|
+
|
|
207
|
+
def __index__(self) -> int:
|
|
208
|
+
return self._check_open()
|
|
209
|
+
|
|
210
|
+
def __repr__(self) -> str:
|
|
211
|
+
state = self._fd if self._fd >= 0 else "closed"
|
|
212
|
+
return f"PidFd(fd={state})"
|
|
213
|
+
|
|
214
|
+
def __del__(self) -> None:
|
|
215
|
+
with contextlib.suppress(OSError):
|
|
216
|
+
self.close()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def pidfd_send_signal(fd: int | PidFd, sig: int, flags: SignalFlag | int = 0) -> None:
|
|
220
|
+
"""Send sig through a pidfd without wrapping it in PidFd first."""
|
|
221
|
+
raw = fd.fileno() if isinstance(fd, PidFd) else int(fd)
|
|
222
|
+
_syscall.pidfd_send_signal(raw, int(sig), None, int(flags))
|
pidfd/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pidfd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python bindings for Linux pidfds
|
|
5
|
+
Project-URL: Homepage, https://quad4.io
|
|
6
|
+
Project-URL: Repository, https://github.com/Quad4-Software/pidfd
|
|
7
|
+
Project-URL: Issues, https://github.com/Quad4-Software/pidfd/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Quad4-Software/pidfd/blob/master/CHANGELOG.md
|
|
9
|
+
Author: Quad4
|
|
10
|
+
License-Expression: 0BSD
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: linux,pidfd,process,signals
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Security
|
|
24
|
+
Classifier: Topic :: System :: Operating System Kernels :: Linux
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# pidfd
|
|
30
|
+
|
|
31
|
+
[](https://github.com/Quad4-Software/pidfd/actions/workflows/ci.yml)
|
|
32
|
+
[](https://github.com/Quad4-Software/pidfd/actions/workflows/codeql.yml)
|
|
33
|
+
[](https://securityscorecards.dev/viewer/?uri=github.com/Quad4-Software/pidfd)
|
|
34
|
+
[](https://pypi.org/project/pidfd/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
Dependency-free Python bindings for Linux pidfds. A pidfd is a file
|
|
38
|
+
descriptor referring to a process: a stable handle that can never hit a
|
|
39
|
+
recycled pid, becomes readable when the task exits, can be waited on
|
|
40
|
+
with waitid(2), and can duplicate the task's file descriptors with
|
|
41
|
+
pidfd_getfd(2). Everything goes through pidfd_open(2),
|
|
42
|
+
pidfd_send_signal(2), pidfd_getfd(2) and waitid(2) via ctypes. There
|
|
43
|
+
are no runtime dependencies.
|
|
44
|
+
|
|
45
|
+
Requires Python 3.10+ and Linux.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
pip install pidfd
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import select, signal, subprocess
|
|
55
|
+
from pidfd import PidFd, WaitCode
|
|
56
|
+
|
|
57
|
+
proc = subprocess.Popen(["sleep", "30"])
|
|
58
|
+
with PidFd.open(proc.pid) as pf:
|
|
59
|
+
# poll(2) reports the fd readable once the task is a zombie
|
|
60
|
+
pf.signal(signal.SIGKILL)
|
|
61
|
+
|
|
62
|
+
poller = select.poll()
|
|
63
|
+
poller.register(pf.fileno(), select.POLLIN)
|
|
64
|
+
poller.poll()
|
|
65
|
+
|
|
66
|
+
result = pf.wait() # waitid(P_PIDFD)
|
|
67
|
+
assert result.code == WaitCode.CLD_KILLED
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`PidFd.open(pid, OpenFlag.NONBLOCK)` makes `wait()` fail with EAGAIN
|
|
71
|
+
instead of blocking. `pf.getfd(fd)` duplicates one of the target's open
|
|
72
|
+
fds (ptrace access rules apply).
|
|
73
|
+
|
|
74
|
+
## Development
|
|
75
|
+
|
|
76
|
+
uv sync --group dev
|
|
77
|
+
make check
|
|
78
|
+
|
|
79
|
+
License: 0BSD. Quad4 Software, https://quad4.io
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pidfd/__init__.py,sha256=7QQYwDE6IBcBBRbLIdsQo9POpSfTzgf19H52Qk200HU,958
|
|
2
|
+
pidfd/_syscall.py,sha256=m8jK6DrmLy7uqN7W2dLir1o441Z3NaHeHwH1QjAy3o8,5071
|
|
3
|
+
pidfd/errors.py,sha256=aKqnr7V60Y-DDLsYzMQfBvPH_Xkh56F9PmL-lPqBcUA,252
|
|
4
|
+
pidfd/pidfd.py,sha256=l7GTn4L88B6-R2jonBLuYdA-uZo0QSu-qrZLUb-g7CE,6892
|
|
5
|
+
pidfd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
pidfd-0.1.0.dist-info/METADATA,sha256=D6SZpssZhfYI8bfuzjTTjGXBeWV-j_vp_YTYxEBUEPw,3015
|
|
7
|
+
pidfd-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
pidfd-0.1.0.dist-info/licenses/LICENSE,sha256=3Hnwsz5EXuTC9iTlGjzA171dKQtIVsgjFoF5DEMRl48,633
|
|
9
|
+
pidfd-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright (c) 2026 Quad4
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose
|
|
4
|
+
with or without fee is hereby granted.
|
|
5
|
+
|
|
6
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
7
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
8
|
+
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
9
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
10
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
11
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
12
|
+
PERFORMANCE OF THIS SOFTWARE.
|