Repository navigation
Conversation
pystemd.run:
- wait=True / wait_for_activation=True could block forever: systemd only
emits unit PropertiesChanged signals while some client has called
Manager.Subscribe(), and run() only eavesdrops via BecomeMonitor. On a
desktop something else is usually subscribed; on a minimal host
(e.g. a Fedora 44 cloud VM) nobody is. Subscribe before starting the unit.
- Drain every message sd-bus has buffered on each wakeup. One socket read
can bring several signals, and select() does not fire again for the ones
already read, so processing one message per wakeup could miss the state
change we are waiting for.
pystemd.futures:
- Python 3.14 made forkserver the default start method on Linux, which
broke TransientUnitPoolExecutor ("SemLock created in a fork context is
being shared with a process in a spawn context"). Workers now subclass
ForkProcess, which enter_unit relies on anyway.
- Start the anchor unit with Type=exec. With Type=simple the unit is
reported running right after fork, while systemd >= 255's executor is
still root, so enter_unit copied uid 0 and User= was ignored.
- The pool's anchor process is now `tail --pid=<parent>` instead of
`sleep infinity` (as TransientUnitProcess already does), so the unit
does not leak when the parent dies without reaching __exit__.
Tested on Fedora 44 (systemd 259, kernel 6.19, aarch64, Python 3.14):
futures.run with User=nobody + PrivateNetwork, CPUQuota and MemoryMax;
examples/future_cpucap_{method,process,pool}.py; unit tests pass.
Without them pystemd.run(extra={...}) raises KeyError for these
properties. Types are what systemd 259 exposes on
org.freedesktop.systemd1.Service:
- PrivatePIDs: "s" ("yes"/"no"; systemd >= 257), not a boolean
- ProtectHostnameEx: "(ss)", e.g. ("private", "myhostname")
- SystemCallErrorNumber: "i", e.g. errno.EPERM so SystemCallFilter=
denies with an error instead of killing the process
Tested on Fedora 44 (systemd 259): a transient unit with
PrivatePIDs="yes", ProtectHostnameEx=("private", "pycon-unit"),
SystemCallFilter=(False, ["socket"]) and SystemCallErrorNumber=EPERM
runs as PID 1 with its own hostname, and socket() fails with EPERM.
wait_for_activation=True only stopped waiting when it saw SubState "running" for the start job. A Type=notify or Type=exec unit that fails during startup never gets there (it goes straight to "failed"), so pystemd.run() blocked forever. Also stop once the start job is over and the unit is in an exit substate (exited/failed/dead), the same condition wait=True uses; with raise_on_fail the caller then gets PystemdRunError as usual. Tested on systemd 259: Type=notify `sh -c "exit 3"` and Type=exec `/nonexistent` now return in ~20ms as failed/failed (both hung before); a Type=notify unit that sends READY=1 still waits for it.
Needed to use the file descriptor store from pystemd.run(extra={...})
(otherwise KeyError). Types as exposed by systemd 259 on
org.freedesktop.systemd1.Service: FileDescriptorStoreMax "u",
FileDescriptorStorePreserve "s" ("no" / "yes" / "restart").
Tested: a Type=notify service with FileDescriptorStoreMax=1 stores a
memfd (FDSTORE=1, FDNAME=db) and gets it back via LISTEN_FDS across
four restarts; Service.DumpFileDescriptorStore() lists it.
aleivag
force-pushed
the
fix-futures-systemd259-py314
branch
3 times, most recently
from
October 29, 2026 22:32
1509dfe to
aa8eaa4
Compare
- notify(unset_environment, *states, fds=None, **kwstates): when `fds`
is given, call sd_pid_notify_with_fds() so the fds travel with the
message; without it (or with an empty list), sd_notify() exactly as
before. `fds` is keyword-only, and there is no FDS= notify state for
it to shadow. It takes a single fd or a list of them; each fd is an int
or an object with fileno(). fds=0 sends fd 0 (it is not "no fds"), and
str/bytes are rejected with TypeError: bytes would otherwise iterate
as ints and silently send the wrong fds.
e.g. notify(False, fds=[memfd], fdstore=1, fdname="db") hands the
memfd to systemd's file descriptor store.
- listen_fds_with_names(unset_environment=0): sd_listen_fds_with_names(),
returned as {name: [fd, ...]}; this is how a restarted service gets
its stored fds back (and how named socket activation fds are found).
Names are not unique (a socket unit with several ListenStream= passes
all its fds under one name; FDNAME= can repeat), hence the lists.
Tested on systemd 259: a Type=notify service with FileDescriptorStoreMax=1
stores a memfd holding a SQLite database with notify(fds=...) and gets
it back with listen_fds_with_names()["db"][0] across four restarts;
fds=<int> lands in the store too, and two fds stored under the same
FDNAME both come back (LISTEN_FDNAMES=same:same).
aleivag
force-pushed
the
fix-futures-systemd259-py314
branch
2 times, most recently
from
October 29, 2026 23:35
92cb60f to
1963192
Compare
Helpers for systemd's file descriptor store, on top of notify(fds=...):
- store_fd(name, fds, unset_environment=False, **kwstates): hand a single
fd or a list of fds to the store under `name`; the same as
notify(unset_environment, fds=fds, fdstore=1, fdname=name, **kwstates).
- store_fds({name: fds, ...}, unset_environment=False, **kwstates): several
names at once. FDNAME= covers a whole notify message, so this sends one
message per name (not atomic); unset_environment and the extra states
(e.g. ready=1) only go with the last message, since unsetting
$NOTIFY_SOCKET earlier would make the remaining sends no-ops. Returns
the lowest notify() result.
unset_environment can be passed positionally (like notify()) or as a
keyword, for readability: store_fds({...}, unset_environment=True).
Both validate names first (1-255 printable ASCII characters, no ':', as
LISTEN_FDNAMES is ':'-separated) and raise ValueError: systemd ignores an
invalid FDNAME= silently, so the fds would just never be stored.
Together with listen_fds_with_names() -> {name: [fd, ...]}, storing and
getting fds back is a dict both ways.
Tested on systemd 259: store_fds({"a": fd, "b": [fd1, fd2], "c": fd},
unset_environment=True, ready=1, status="stored") stores all four fds under three names and
activates the unit with the status set; store_fd() is used by a memfd,
chat-server (listening + client sockets), pty and pidfd example that all
get their fds back across restarts.
aleivag
force-pushed
the
fix-futures-systemd259-py314
branch
from
October 29, 2026 23:37
1963192 to
3744d5a
Compare
pystemd/utils.py becomes pystemd/utils/, one module per kind of helper:
- utils/x2.py: x2char_star, str2cmd, strlist2cmd, x2cmdlist (the old
file, moved, so its history follows)
- utils/unit.py: random_unit_name
- utils/typing.py: T, unwrap
utils/__init__.py re-exports all of them, so existing imports such as
`from pystemd.utils import x2char_star` and `pystemd.utils.unwrap(...)`
keep working unchanged (including mock.patch("pystemd.utils.random_unit_name")
in the tests). setup.py lists packages explicitly, so pystemd.utils is
added there.
No behaviour change; this makes room for new helper modules (e.g.
namespaces) without growing a single catch-all file.
join_namespaces(pid, namespaces=None) moves the calling process into the
namespaces of `pid` (like `nsenter --target pid --all`) and returns the
names it joined. `namespaces` is an iterable of names ("net", "mnt", ...;
NAMESPACES lists them), all by default.
- Namespaces already shared with `pid` are skipped (compared by inode
under /proc/*/ns/), so asking for "all" works when we share e.g. the
user namespace, which setns() would otherwise reject with EINVAL.
- First tries a single setns() on a pidfd (Python >= 3.12, Linux >= 5.8),
which joins everything atomically; otherwise (Python 3.11, older kernels)
joins one namespace at a time in nsenter's order, closing each fd.
- Failures raise OSError instead of being ignored.
pystemd.futures.enter_unit() used to open every /proc/<pid>/ns/* file,
call setns() on it, ignore the result and never close the fd. It now calls
join_namespaces(main_pid): no fd leak (one per namespace, in every worker),
and a namespace that can't be joined (e.g. PrivateNetwork=) is an error
rather than a worker that silently runs outside the sandbox.
Tested on Linux 6.19 / Python 3.14: joining a PrivateNetwork= +
ProtectHostnameEx= unit gives ['uts', 'net', 'mnt'] with only `lo` and the
unit's hostname, through both the pidfd path and the forced fallback, with
no fds leaked; futures.run() with User=nobody + PrivateNetwork, CPUQuota
and MemoryMax behaves as before.
aleivag
force-pushed
the
fix-futures-systemd259-py314
branch
from
October 30, 2026 00:01
2db3f13 to
c80ed8e
Compare
pystemd.run(cmd, delegate=True) sets Delegate= on the transient unit, a
shortcut for extra={"Delegate": True}. Delegation is what lets
Service.AttachProcesses move processes into the unit. None (the
default) sends nothing and keeps systemd's default; extra= still
overrides it.
Adds tests/test_run.py, the first unit tests for pystemd.run, with
the D-Bus side mocked.
Needed to set them from pystemd.run(extra={...}) (otherwise KeyError).
Types as exposed by systemd 259 on org.freedesktop.systemd1.Service:
OOMPolicy "s", OOMScoreAdjust "i", ManagedOOMSwap "s",
ManagedOOMMemoryPressure "s", ManagedOOMMemoryPressureLimit "u",
ManagedOOMMemoryPressureDurationUSec "t", ManagedOOMPreference "s".
Tested: stress-ng --vm under MemoryMax=50M with OOMPolicy=continue and
OOMScoreAdjust=500 keeps running while memory.events oom_kill climbs;
with the default policy (stop) the unit fails with result oom-kill.
peturingi
approved these changes
Nov 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
pystemd.run:
pystemd.futures:
tail --pid=<parent>instead ofsleep infinity(as TransientUnitProcess already does), so the unit does not leak when the parent dies without reaching exit.Tested on Fedora 44 (systemd 259, kernel 6.19, aarch64, Python 3.14): futures.run with User=nobody + PrivateNetwork, CPUQuota and MemoryMax; examples/future_cpucap_{method,process,pool}.py; unit tests pass.