Skip to content

Linux: the engine an app starts outlives the app's systemd unit - #1391

Draft
alexandre-vl wants to merge 1 commit into
zeronsh:zeron/zgxfrom
alexandre-vl:engine-own-systemd-scope
Draft

alexandre-vl wants to merge 1 commit into
zeronsh:zeron/zgxfrom
alexandre-vl:engine-own-systemd-scope

Conversation

@alexandre-vl

@alexandre-vl alexandre-vl commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Under a systemd user manager an app runs in a unit of its own: uwsm starts each app as a service (app-Hyprland-zeron@….service), GNOME and KDE as a scope.
    • The engine the app started stayed in that unit's cgroup even with a session of its own.
    • A service stops as soon as its main process exits, and systemd kills everything left in it.
    • So on Hyprland under uwsm, quitting Zeron took its engine down, and every session it ran. crates/engine/src/managed.rs promises those keep running.
  • On Linux, a process started detached from within a user unit is now started through systemd-run --user --scope.
    • It gets a transient scope of its own in the app's slice, so it still ends with the graphical session.
    • It keeps the same process and environment.
    • spawn_detached hands it back only once systemd-run has become it, which happens once the scope exists, so an app that exits straight after doesn't take it down either. This costs about 3 ms.
  • Fallbacks:
    • Not in a user unit (a login session's scope, a system service, a container, cgroup v1): unchanged.
    • systemd-run missing: the spawn fails, and the command is started as before.
    • systemd-run unable to create the scope: it exits before running the command, still named systemd-run, which is detected, and the command is started as before.
    • A slow manager: the process is handed back after 2 s.
  • Why systemd-run (process spawning, please review closely):
    • It is how a launcher gives a process its own scope (GLib and KDE do the same over D-Bus).
    • It comes with systemd, so there's no new dependency. zbus would put a D-Bus stack into the engine and the CLI.
    • --slice= names the app's own slice rather than --slice-inherit (systemd ≥ 251), so older systemd works. --collect needs systemd ≥ 236.
  • Applies to every spawn_detached caller:
    • launch(), used by the zgx app, the GPUI app and the CLI.
    • The managed engine's relaunch_self: its successor gets a new scope in the same slice.
    • crates/update's own spawn_detached is unchanged.
  • Windows already leaves the app's job object for the same reason.

Test plan

  • cargo test --locked -p zeron-launch: 9 passed.
    • cgroup parsing:
      • A slice is found for a uwsm service, a GNOME scope, and a hybrid hierarchy.
      • None is found for a login session, a system service, the manager itself, a delegated cgroup, a container's root, or cgroup v1 only.
    • The systemd-run command line, environment and directory carried over.
    • The hand-over, with stand-in systemd-run scripts that become the command or give up.
  • Live, needs a systemd user manager (cargo test -p zeron-launch -- --ignored outlives): passes on CachyOS (systemd 262, Hyprland under uwsm). With the scope step removed it fails: "the detached process went down with the service".
  • Reproduced by hand: a setsid child of a transient user service is killed when the service's main process exits. Started through systemd-run --scope, it survives.
  • cargo clippy --locked -p zeron-launch --all-targets, cargo check -p zeron-engine -p zeron-cli.
  • Not checked: GNOME or KDE sessions, a distro with systemd < 251 (e.g. Ubuntu 22.04), Flatpak.
  • Not compiled for macOS or Windows here (no targets installed). On Unix, the existing setsid hook moved unchanged into new_session. The new code is #[cfg(target_os = "linux")], and the Windows branch is untouched.

Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

Under a systemd user manager an app runs in a unit of its own: uwsm
starts each app as a service (app-Hyprland-zeron@….service), GNOME and
KDE as a scope. The engine the app started stayed in that unit's
cgroup, its own session notwithstanding, and a service stops as soon as
its main process exits, systemd killing everything left in it: on
Hyprland under uwsm, quitting Zeron took its engine down, and every
session it ran, which crates/engine's managed.rs promises keep running.

On Linux a process started detached from within a user unit is now
started through `systemd-run --user --scope`: a transient scope of its
own in the app's slice (so it still ends with the graphical session),
the same process and environment. spawn_detached hands it back once
systemd-run has become it, which it does once the scope exists, so an
app that exits straight after doesn't take it down either. Where
systemd-run gives up (no manager to reach, an option it doesn't know)
or isn't there, the command is started as before. Windows leaves the
app's job object for the same reason.

a_detached_process_outlives_the_unit_that_started_it (ignored: it needs
a systemd user manager) starts a process detached from inside a
transient service that then ends, and fails without it.
@alexandre-vl

alexandre-vl commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

The red checks come from zeron/zgx itself, not this change.

As for this change:

  • zeron-launch built on Windows and macOS without an error or a warning, which settles the test plan's open item about those two platforms.
  • linux-core / core-tests ran its five systemd_scope tests, and all five passed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant