NixOS could not detect a graphical session at all — and the module was never checked by CI #158
Merged
enricobuehler
merged 5 commits from 2026-08-10 18:37:36 +00:00
worktree-nixos-wrapped-comm-detection into main
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
61dfc3dadc |
Merge remote-tracking branch 'origin/main' into worktree-nixos-wrapped-comm-detection
ci / bun-nix (pull_request) Successful in 40s
ci / web (pull_request) Successful in 1m19s
ci / docs-site (pull_request) Successful in 1m30s
ci / rust-arm64 (pull_request) Successful in 1m46s
android / android (pull_request) Successful in 5m22s
ci / rust (pull_request) Successful in 6m47s
apple / swift (pull_request) Failing after 13m41s
apple / screenshots (pull_request) Skipped
nix / flake (pull_request) Successful in 13m48s
|
||
|
|
1befa8a2c4 |
docs(nix): bring the Nix docs in line with the module, and fix a stale claim they shared
There are three places Nix is documented — the public docs-site, packaging/nix/
README.md, and packaging/README.md — plus the changelog. All had drifted.
STALE CLAIM, and not only for Nix. install.md said the plugin runner's "user unit
ships **disabled** — enable it once you have" something to run. That is true only
of Arch and source installs: the deb postinst and RPM %post both
`systemctl --global enable punktfunk-scripting.service`, and the Bazzite sysext
bakes in a default.target.wants symlink (build-sysext.sh:113). bazzite.md carried
the same claim about its own image. Both corrected, per channel, with the reason
the default flipped — the library scanners are plugins, so a host without the
runner can come up with an empty library — and the `mask`-not-`disable` opt-out
the sysext's own comment documents.
docs-site:
* install.md NixOS — `desktopSession` in the example and explained, the runner
no longer needs enabling, and the host/console line says what autoStart does.
* running-as-a-service.md — "Restart the host with your desktop" documented the
drop-in for packaged installs only; NixOS gets its one-liner beside it.
* bazzite.md — the runner is started for you, not "isn't started".
packaging/nix/README.md:
* option tables gain `desktopSession`, `gamescopeHdr`, `gamescopePackage`, and
the `punktfunk` group next to `input` (both are required — the udev rule
chgrp's the vhci nodes and fails outright if the group was never created).
* "what the module configures" gains the security.wrappers entry, and a note on
why the capability sits on the encode worker and never on the host: a wrapper
raises it into the ambient set, which lands it in the permitted set and fails
KWin's /proc/<pid>/exe readlink identically to a file capability.
* the appliance snippet no longer tells you to put pkgs.gamescope on PATH —
gamescopeHdr does that with the patched build, and desktopSession is called
out as the thing to leave off there.
* a caveat recording that `nix flake check` does not check the module, and the
two rules for editing module-check.nix (assertions stay pure Nix; assert
list-valued unit fields on the lists, not the rendered text).
packaging/README.md: the flake ships five packages, not "host + client".
CHANGELOG.md v0.27.0: a NixOS section covering the comm/session-detection fix, the
module changes including the scripting default flip as an explicit behaviour
change, and the flake-check gap — plus the documentation bullets above.
|
||
|
|
f8cde0adaf |
feat(nix): actually check the NixOS module in CI, and close the sweep's open issues
THE CI GAP. `nix flake check` does not check `nixosModules`. It forces the value
and asserts it is a lambda taking an open attribute set — nothing more; nix's own
source carries `// FIXME: if we have a 'nixpkgs' input, use it to check the
module.` Measured: a flake whose module sets a nonexistent OPTION, references a
nonexistent `pkgs` attribute AND calls a nonexistent `lib` function passes clean,
printing `checking NixOS module 'nixosModules.default'... all checks passed!`.
nix.yml's header claimed that leg covered the module; it never did, for the
module's whole life — on a flake whose history is Nix regressions reaching main
invisibly.
Closed with `checks.<system>.nixos-module` (packaging/nix/module-check.nix): it
evaluates the module against real nixpkgs in four scenarios (desktop, appliance,
native-only, client-only) and asserts on the rendered systemd units. The
assertions are PURE NIX so instantiating the check runs them — which means the
eval-only `--no-build` leg CI already runs is sufficient, and no Rust is built.
Stub fake-derivation packages keep it independent of punktfunk-host/-client and
the from-source gamescope; crane and bun2nix are provably not needed (they are
`throw`s in the wiring test and it still instantiates).
17 checks, including regression guards for every divergence the sweep found and
for the KWin identification trap (host ExecStart must stay on the plain store
path, never a capability wrapper, while the encode worker points AT the wrapper).
Mutation-tested: 8 mutants, each re-introducing one real defect, all 8 rejected,
baseline green. The suite already earned it once — its first run failed a correct
module because systemd renders `After=` as one space-separated line, so those
assertions now read the evaluated lists instead of the text.
Also closed from the sweep:
* services.punktfunk.host.desktopSession (new, default false) — binds the host
to graphical-session.target, the declarative form of the
punktfunk-host-desktop-session.conf drop-in. Without it a Plasma/GNOME
restart leaves the host holding a Wayland socket and portal D-Bus connection
that died with the old compositor: it still listens, still answers, and every
session it then serves fails at capture. Off by default because an appliance
may never reach that target and would be left permanently stopped.
* scripting.autoStart now defaults ON, matching the deb postinst and RPM %post,
which both `systemctl --global enable` the runner. It was opt-in here on the
reasoning that the runner is inert until you add automation — which stopped
being true when the game-library scanners became plugins. A NixOS host came up
with an empty library and no obvious reason why. The module and README carried
the superseded rationale verbatim; both updated.
* A warning when the host is enabled and xdg.portal is not. A warning rather
than `xdg.portal.enable = mkDefault true`, because enabling the portal service
with no `extraPortals` backend is its own broken state and only the operator
knows which backend their compositor needs.
* punktfunk-gamescope gets a `build-gamescope` dispatch input. It is on the
critical path of every host build (`gamescopeHdr` defaults true) yet nothing
compiled it; it tracks nixpkgs' gamescope, so a flake.lock bump — not a change
of ours — is what breaks it, and the first to find out would be an operator
whose system rebuild fails.
All .nix files reformatted with the flake's own declared formatter
(nixfmt-rfc-style from the PINNED nixpkgs, not a channel's).
|
||
|
|
159bbdbfc2 |
fix(nix): port three NixOS-module divergences from the shipped systemd units
A sweep of the Nix packaging against the units the deb/rpm actually install found three decisions that were made, documented and deliberate everywhere else, and simply not carried into packaging/nix/nixos-module.nix. punktfunk-web — StartLimitIntervalSec=0. The unit's EnvironmentFile for the mgmt token is mandatory ON PURPOSE, so the console genuinely fails until the host's first `serve` writes it. systemd's default rate limit (5 starts / 10 s) against RestartSec=2 then gives up permanently after ~10 s — which on an appliance is exactly the window before the host is ready, so a console enabled before the host's first run stayed dead until someone restarted it by hand. scripts/punktfunk-web.service has carried the override since that defect was found; the Nix module omitted it while its own comment went on promising "Restart retries until the host has created it". punktfunk-web — Restart=always, not on-failure. A console that exits 0 has still stopped serving, and on-failure leaves it down. Matches the shipped unit and web-run.cmd on Windows, both of which relaunch bun on ANY exit. An explicit `systemctl --user stop` is unaffected. punktfunk-scripting — the sandbox was missing entirely. The shipped unit confines the runner with NoNewPrivileges, ProtectSystem= strict, ReadWritePaths=%h /tmp and an AF_UNIX/AF_INET/AF_INET6 address-family restriction, plus PrivateTmp=no (a field report: a private /tmp hides /tmp/vhclient and /tmp/.X11-unix, so a plugin launches its vendor binary and then cannot reach the daemon behind it). The NixOS unit had none of it — so the one unit here that executes arbitrary operator TypeScript by design ran strictly LESS confined on NixOS than on every other channel. Verified by evaluating the module against the pinned nixpkgs and rendering the units: assertions clean, cap_sys_nice=ep on the encode-worker wrapper, firewall 47984/47989/47990/47992/47993/48010, and each unit carrying exactly the directives above. That evaluation is NOT something CI does — measured: `nix flake check` passes a nixosModule containing a nonexistent option, a nonexistent pkgs attribute and a nonexistent lib function, printing "checking NixOS module ... all checks passed!" while never evaluating it against nixpkgs. nix.yml's header claims that leg covers the module. It does not; tracked separately. |
||
|
|
002702bcec |
fix(pf-vdisplay): NixOS sessions were undetectable — comm is the WRAPPER's name
The session probe decided "is a desktop live?" by reading /proc/<pid>/comm for
every process of our uid and exact-matching it against "kwin_wayland" /
"gamescope" / "gnome-shell" / "Hyprland". comm is the kernel's name for the
executed FILE, truncated to 15 bytes — not argv[0].
nixpkgs wraps essentially every graphical binary: wrapProgram moves the real
ELF aside to `.<name>-wrapped` and installs a wrapper under the original name,
which then `exec -a "$0"`s the hidden file. So on NixOS the kernel reports
`.kwin_wayland-w` (15 bytes of `.kwin_wayland-wrapped`) while ps/pgrep -a show
a perfectly ordinary `kwin_wayland`, because they read argv. Measured against a
live kernel: `.kwin_wayland-w`, `.kwin_wayland_w` (KWin's own
kwin_wayland_wrapper), `.gamescope-wrap`, all 15 bytes.
Nothing downstream could recover from that one string comparison:
- detect_active_session returned ActiveKind::None on a *running* KDE desktop;
- wayland_display is only resolved for a detected kind, so the connect log
reported wayland="-" even though WAYLAND_DISPLAY was correct;
- pick_compositor's Auto arm returns the DETECTED backend, so a live, fully
working KWin sitting in available() was never chosen — every connect died
"no usable compositor";
- and PUNKTFUNK_COMPOSITOR could not rescue it: pinned_at_a_dead_session
consults the same probe, turning the miss into a hard error instead.
No environment variable reached the comparison — the XDG_CURRENT_DESKTOP
fallback in detect() is only on the pinned path. Capture itself was never at
fault: a decoy process merely NAMED kwin_wayland satisfied the probe and the
stream came up against the real KWin.
Resolve the name through /proc/<pid>/exe (the full, untruncated file name) and
strip the nixpkgs decoration. Both the leading `.` and the trailing `-wrapped`
are required before anything is stripped, so KWin's own real
`kwin_wayland_wrapper` binary keeps its name rather than collapsing into
`kwin_wayland` and handing the probe the parent's PID. The comm fast path is
kept for every ordinary distro — one read, no readlink, and no name that
matched before can stop matching.
Also applied to foreign_gamescope_running, which had the same defect: nixpkgs
wraps gamescope too, so the attach-vs-spawn ladder saw no foreign session.
Tests are fixture-driven rather than spawn-driven on purpose: a stand-in has to
be a real ELF that tolerates being renamed, and /bin/sleep is not one — modern
coreutils is a multi-call binary that dispatches on the executable's own name,
so a copy called `.kwin_wayland-wrapped` exits instantly and /proc/<pid>/exe is
gone before it can be read. That failure looks exactly like this resolver being
broken; it cost one debugging round here and the same trap is already recorded
in punktfunk-host's /proc matcher.
|