Skip to content

Start singularity-session.target so graphical-session.target actually activates - #6

Open
perlowja wants to merge 6 commits into
singularityos-lab:mainfrom
perlowja:fix/graphical-session-target
Open

perlowja wants to merge 6 commits into
singularityos-lab:mainfrom
perlowja:fix/graphical-session-target

Conversation

@perlowja

@perlowja perlowja commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

graphical-session.target refuses direct manual start (systemd hardening) — it needs a bound unit to pull it in. Nothing did, so every WantedBy=graphical-session.target unit (xdg-desktop-portal-singularity, foot-server) was enabled but never ran.

Adds singularity-session.target (BindsTo=graphical-session.target, matching what labwc's own docs recommend starting) and starts it from the session launcher.

Validated on real O6N: root-caused live, fixed, then confirmed clean from a full session restart — both the portal-mediated Screenshot call and the native singularity-screenshot binary produced real PNGs.

AI assistance: disclosed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b1d95bad61

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread meson.build Outdated
# own xdg-desktop-portal-singularity.service among them) launch at all.
install_data(
'config/systemd-user/singularity-session.target',
install_dir: get_option('prefix') / 'lib' / 'systemd' / 'user',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Install the target into a systemd user-unit search path

For the advertised /opt/local installation, this places the target in /opt/local/lib/systemd/user, so the new systemctl call cannot find it and silently continues without activating the graphical services. Checked with systemd-analyze --user unit-paths: the standard paths include /usr/local/lib/systemd/user and /usr/lib/systemd/user, but not arbitrary ${prefix}/lib/systemd/user; use the configured systemd user-unit directory or otherwise install the file into a searched location.

Useful? React with 👍 / 👎.

# the same role: BindsTo=graphical-session.target,
# Wants=graphical-session-pre.target, so starting it gets the pre-target
# sequencing for free too.
systemctl --user --no-block start singularity-session.target 2>/dev/null || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop the session target when the compositor exits

When the user manager survives logout—such as with lingering enabled or another concurrent login—this start has no matching stop, so singularity-session.target remains active after labwc terminates. Because it keeps the StopWhenUnneeded=yes graphical target needed, graphical-session services such as the portal backend and foot server continue running outside the desktop session; add teardown tied to the launcher/compositor lifetime.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in aec8a46.

The pid-file cleanup that was already on trap ... EXIT becomes _session_cleanup(), and the matching systemctl --user --no-block stop singularity-session.target goes in there alongside it.

EXIT only, deliberately. bash runs the EXIT trap for an untrapped fatal signal as well, whereas an explicit trap ... TERM is deferred until the foreground command returns — and on the logout path the supervisor is blocked in "$_DESKTOP", so it never would. Checked on bash 5.3.9 (and 3.2.57): SIGTERM to the launcher while it supervised a long-lived fake shell issued the stop and removed the pid file.

Added tests/session_target_lifecycle_test.sh (wired into meson as session-target-lifecycle). It drives the launcher to its crash budget behind stub systemctl/pkill/gsettings/xdg-user-dirs-update and asserts the start, the stop, their ordering, and the pid-file removal. Against the pre-fix script it fails with:

launcher exited without stopping singularity-session.target
--user --no-block start singularity-session.target
--user restart --no-block xdg-desktop-portal

Both tests green on aarch64: session-safe-mode OK 4.17s, session-target-lifecycle OK 4.34s.

@mirkobrombin mirkobrombin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please install the target into systemd's configured user unit directory so it works with the /opt/local prefix without depending on runtime links.

@perlowja
perlowja force-pushed the fix/graphical-session-target branch from b1d95ba to cab7acb Compare September 12, 2026 17:37
@perlowja

Copy link
Copy Markdown
Contributor Author

Fixed in cab7acb: the target install now asks systemd for its actual configured user-unit directory (dependency('systemd').get_variable(pkgconfig: 'systemduserunitdir')) instead of deriving the path from our own --prefix. Under a plain build that resolves to /usr/lib/systemd/user regardless of prefix, so --prefix=/opt/local no longer puts the unit somewhere systemd never looks — verified both with the default prefix and with --prefix=/opt/local (destdir installs land the target at /usr/lib/systemd/user in both cases, confirmed on a Fedora box where systemd.pc is present). Falls back to the old prefix-relative path only if systemd.pc isn't available to query, so it degrades gracefully rather than failing the build. Also rebased onto current main to pick up the safe-mode supervisor changes and resolve the conflict in meson.build.

@mirkobrombin mirkobrombin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation now targets systemd correctly, but please reduce the added Meson and launcher comment blocks to the few non-obvious lifecycle facts.

@perlowja

perlowja commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Trimmed both blocks in dc14e66.

meson.build — 7 lines down to 3 for the unit-dir lookup (systemd's user-unit search path does not follow our --prefix, so a prefix-relative dir lands where systemd never looks; the fallback is only for a sysroot without systemd.pc), and 6 lines down to 2 above the install_data (graphical-session.target sets RefuseManualStart, so it can only be reached via a unit that BindsTo= it).

src/singularity-desktop-session — 21 lines down to 4 above the systemctl --user start: the RefuseManualStart fact and the symptom it causes (the target stays inactive, so every WantedBy=graphical-session.target unit silently never runs). Dropped the O6N root-cause narrative, the portals.conf history, and the labwc-session.target comparison — none of that is needed to read the code.

Comment-only, no functional change. Rebuilt and meson test green on aarch64 (1/1), and a DESTDIR install still places the unit in /usr/lib/systemd/user rather than under the project prefix.

@perlowja
perlowja force-pushed the fix/graphical-session-target branch from 5962d54 to dc14e66 Compare September 12, 2026 21:33

@mirkobrombin mirkobrombin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EXIT trap stops the single user-wide target even when another concurrent login is still using it, so please make this lifecycle per-session or reference-counted instead of stopping it unconditionally.

… actually activates

graphical-session.target is systemd-hardened against direct manual
start (RefuseManualStart) -- it needs a unit that BindsTo= it, which
is what labwc's own docs/autostart recommends (start
labwc-session.target). That unit is not installed by this build, so
add our own with the same shape: BindsTo=graphical-session.target,
Wants=graphical-session-pre.target.

Without something starting it, every systemd --user unit declaring
WantedBy=graphical-session.target -- our xdg-desktop-portal-singularity
service among them, plus foot-server -- sits enabled but never runs.
Root-caused on real O6N 2026-08-27: xdg-desktop-portal correctly
routed Screenshot requests to the singularity backend, but the backend
process itself never started because nothing had ever activated
graphical-session.target on this system. Confirmed fixed end-to-end
after a clean session restart (with the matching cix-installer bridge
that puts this target where systemd --user actually looks) -- both the
portal-mediated Screenshot call and the native singularity-screenshot
binary produced real PNGs.
Derive the install path for singularity-session.target from
`dependency('systemd').get_variable(pkgconfig: 'systemduserunitdir')`
instead of hardcoding it relative to our own --prefix.

systemd's user-unit search path doesn't move just because this
project is installed under a non-standard prefix (e.g. /opt/local),
so deriving it from get_option('prefix') silently placed the unit
somewhere systemd never looks, and the systemctl --user start call
in singularity-desktop-session would just quietly fail to find it.
Falls back to the previous prefix-relative path if systemd's
pkg-config data isn't available to query (e.g. a minimal sysroot).

Addresses Mirko's review on this PR asking for the target to be
installed into systemd's configured user unit directory rather than
a path derived from our own prefix.
perlowja added a commit to perlowja/singularity-session that referenced this pull request Sep 14, 2026
The test hardcoded tests/../src/singularity-desktop-session, which is
the .in template's directory without the .in suffix -- that path never
exists; only the meson-generated copy in the build dir does. The
sibling session-safe-mode test already solved this via
SINGULARITY_TEST_DESKTOP_SESSION (set in meson.build, falls back to the
.in template outside meson); apply the same pattern here so the test
actually runs instead of failing with "No such file or directory"
before it ever exercises the launcher.

Assisted-by: Claude Code:claude-sonnet-5
AI-Scope: Diagnosed and fixed the test's launcher-path resolution while landing PR singularityos-lab#6 (session target start/stop); mirrored the existing fix pattern from session_safe_mode_test.sh.
@perlowja
perlowja force-pushed the fix/graphical-session-target branch from aec8a46 to 1d2750a Compare September 14, 2026 15:21
Per review: the Meson and launcher comment blocks added by this PR
restated the code and narrated the investigation. Cut both to the two
facts that are not visible from the code -- graphical-session.target
sets RefuseManualStart so it can only be reached through a unit that
BindsTo= it, and systemd's user-unit search path does not follow our
--prefix, which is why the dir is queried from systemd.pc with a
prefix-relative fallback.

Comment-only; no functional change. Rebuilt and `meson test` green on
aarch64 (1/1), and a DESTDIR install still places the unit in
systemd's own user dir rather than under the project prefix.

Assisted-by: Claude Code:claude-opus-5
AI-Scope: Trimmed the two over-verbose comment blocks in meson.build and src/singularity-desktop-session down to the non-obvious lifecycle facts, per maintainer review, with no functional change.

Signed-off-by: Jason Perlow <jperlow@gmail.com>
The start added in 8f3b9a1 had no matching stop. graphical-session.target is
StopWhenUnneeded=yes, so it only goes away once nothing binds it -- and a user
manager that survives logout (lingering enabled, or a second concurrent login)
keeps singularity-session.target active after the compositor exits, leaving the
portal backend and foot server running outside any desktop session.

Move the existing pid-file cleanup into _session_cleanup() and stop the target
there. EXIT only, deliberately: bash runs the EXIT trap for an untrapped fatal
signal too, whereas an explicit TERM trap is deferred until the supervisor's
foreground child returns, which on the logout path it never does. Confirmed on
bash 5.3.9 by SIGTERMing the launcher while it supervised a long-lived fake
shell -- the stop was issued and the pid file removed.

tests/session_target_lifecycle_test.sh drives the launcher to its crash budget
behind stub systemctl/pkill/gsettings and asserts start, stop, their ordering,
and the pid-file removal. Against the pre-fix script it fails with "launcher
exited without stopping singularity-session.target".

Assisted-by: Claude Code:claude-opus-5
AI-Scope: Authored the cleanup trap, the lifecycle test, and the pre/post-fix
verification runs on the arm64 build host.
The test hardcoded tests/../src/singularity-desktop-session, which is
the .in template's directory without the .in suffix -- that path never
exists; only the meson-generated copy in the build dir does. The
sibling session-safe-mode test already solved this via
SINGULARITY_TEST_DESKTOP_SESSION (set in meson.build, falls back to the
.in template outside meson); apply the same pattern here so the test
actually runs instead of failing with "No such file or directory"
before it ever exercises the launcher.

Assisted-by: Claude Code:claude-sonnet-5
AI-Scope: Diagnosed and fixed the test's launcher-path resolution while landing PR singularityos-lab#6 (session target start/stop); mirrored the existing fix pattern from session_safe_mode_test.sh.
@perlowja
perlowja force-pushed the fix/graphical-session-target branch from 1d2750a to 702451d Compare September 14, 2026 15:28
Mirko (PR singularityos-lab#6 review, on aec8a46): the EXIT trap stopped
singularity-session.target unconditionally, but one systemd --user
manager is shared by every login of the same UID -- a second
concurrent login (a second seat, or console + SSH) runs its own copy
of this launcher under the same manager and the same target, and
would have its portal/audio-autoswitch/etc torn down the moment a
sibling login exited first.

Fix: each running instance drops a marker file under
$XDG_RUNTIME_DIR/singularity-session.d/$$ on start and removes only
its own marker on exit; the target is stopped only when no marker
remains. Verified via a new scenario in
tests/session_target_lifecycle_test.sh: with a sibling marker
present, a full launcher run starts the target but does NOT stop it,
and leaves the sibling's marker untouched.

Assisted-by: Claude Code:claude-sonnet-5
AI-Scope: Implemented the reference-counted stop and the corresponding test scenario in response to Mirko's PR singularityos-lab#6 review comment on commit aec8a46.
@perlowja

Copy link
Copy Markdown
Contributor Author

Addressed all three review threads on this branch (now at 57fe5c2):

  • systemd user-unit dir (b1d95ba): fixed in fcc386a — resolves via systemd_dep.get_variable(pkgconfig: 'systemduserunitdir'), falls back to the prefix-relative path only when systemd.pc isn't available.
  • Comment trim (cab7acb): fixed in 4817210 — cut to the non-obvious lifecycle facts only.
  • Unconditional stop on concurrent login (aec8a46): fixed in 57fe5c2 — each running instance now drops a marker under $XDG_RUNTIME_DIR/singularity-session.d/$$ on start and removes only its own on exit; the target is stopped only when no marker remains, so a sibling login (same UID, same systemd --user manager) keeps its session alive. Verified with a new scenario in session_target_lifecycle_test.sh: with a sibling marker present, a full launcher run starts the target, leaves it running, and leaves the sibling's marker untouched (meson test session-target-lifecycle passes both scenarios).

Assisted-by: Claude Code:claude-sonnet-5

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.

2 participants