From d02aa35f60b589bc73fa31361ce72b312c7e7ad7 Mon Sep 17 00:00:00 2001 From: Jason Perlow Date: Thu, 27 Aug 2026 15:21:46 -0400 Subject: [PATCH 1/6] session: start singularity-session.target so graphical-session.target 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. --- .../systemd-user/singularity-session.target | 5 +++++ meson.build | 11 ++++++++++ src/singularity-desktop-session.in | 22 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 config/systemd-user/singularity-session.target diff --git a/config/systemd-user/singularity-session.target b/config/systemd-user/singularity-session.target new file mode 100644 index 0000000..13cc26f --- /dev/null +++ b/config/systemd-user/singularity-session.target @@ -0,0 +1,5 @@ +[Unit] +Description=Singularity Desktop session +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target diff --git a/meson.build b/meson.build index 8a56409..9271654 100644 --- a/meson.build +++ b/meson.build @@ -45,3 +45,14 @@ test('session-safe-mode', session_test_bash, 'SINGULARITY_TEST_DESKTOP_SESSION': meson.current_build_dir() / 'singularity-desktop-session', }) + +# BindsTo=graphical-session.target so starting it pulls the target in as a +# side effect -- graphical-session.target itself is systemd-hardened against +# direct manual start (RefuseManualStart), by design: it must be reached via +# a bound unit. singularity-desktop-session starts this unit; that is the +# only thing that makes every WantedBy=graphical-session.target unit (our +# 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', +) diff --git a/src/singularity-desktop-session.in b/src/singularity-desktop-session.in index 61a88aa..a3c179e 100755 --- a/src/singularity-desktop-session.in +++ b/src/singularity-desktop-session.in @@ -130,6 +130,28 @@ nohup "$(singularity_helper singularity-polkit-agent)" >> "$_STATE/polkit.log" 2 if [ -x "$BIN/ush-broker" ]; then nohup "$BIN/ush-broker" >> "$_STATE/ush-broker.log" 2>&1 & fi +# Pull in graphical-session.target so every systemd --user unit declaring +# WantedBy=graphical-session.target actually launches: our own +# xdg-desktop-portal-singularity.service among them, plus foot-server. +# Without this the target sits permanently inactive and those units never +# start at all, regardless of anything the portal ROUTER does below -- +# confirmed live on O6N 2026-08-27: xdg-desktop-portal correctly resolved +# and routed Screenshot requests to the singularity backend (portals.conf +# staging from 2026-08-25 was working fine), but the backend process +# itself never started because nothing had ever started +# graphical-session.target on this system. +# +# graphical-session.target itself refuses direct manual start +# (systemd-hardened, RefuseManualStart) -- it must be reached via a unit +# that BindsTo= it. labwc ships labwc-session.target for exactly this +# (its own docs/autostart recommends starting it), but that unit is not +# actually installed by this build, so singularity-session.target +# (config/systemd-user/, installed by this repo's own meson.build) plays +# 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 + # Restart the portal so it picks up the live session environment, but do NOT # block on it: xdg-desktop-portal can hang ~25s on its settings-proxy timeout # waiting for our portal backend, which would delay the shell launch below. From fcc386a06a30e908d9e2aca4eb4d47816d840cdb Mon Sep 17 00:00:00 2001 From: Jason Perlow Date: Sat, 12 Sep 2026 13:36:59 -0400 Subject: [PATCH 2/6] fix: install session target via systemd's own user-unit dir 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. --- meson.build | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 9271654..6ec6094 100644 --- a/meson.build +++ b/meson.build @@ -18,6 +18,20 @@ configure_file( install_mode: 'rwxr-xr-x', ) +# Ask systemd itself where it expects user units, instead of guessing from +# our own --prefix. This is what makes the install correct under a +# non-standard prefix (e.g. /opt/local): systemd's search path doesn't move +# just because this project's prefix does, so deriving the unit dir from +# get_option('prefix') silently installs to a directory systemd never reads. +# Fall back to the old prefix-relative path only if systemd's pkg-config +# file isn't available to ask (e.g. a minimal cross sysroot). +systemd_dep = dependency('systemd', required: false) +if systemd_dep.found() + systemd_user_unit_dir = systemd_dep.get_variable(pkgconfig: 'systemduserunitdir') +else + systemd_user_unit_dir = get_option('prefix') / 'lib' / 'systemd' / 'user' +endif + install_data( 'src/singularity-labwc-session', install_dir: get_option('bindir'), @@ -54,5 +68,5 @@ test('session-safe-mode', session_test_bash, # 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', + install_dir: systemd_user_unit_dir, ) From 481721078cb8d8b1d72fe335d5a6f60360cf6c60 Mon Sep 17 00:00:00 2001 From: Jason Perlow Date: Sat, 12 Sep 2026 17:17:01 -0400 Subject: [PATCH 3/6] fix: trim session-target comments to the non-obvious facts 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 --- meson.build | 18 +++++------------- src/singularity-desktop-session.in | 24 ++++-------------------- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/meson.build b/meson.build index 6ec6094..e1e426b 100644 --- a/meson.build +++ b/meson.build @@ -18,13 +18,9 @@ configure_file( install_mode: 'rwxr-xr-x', ) -# Ask systemd itself where it expects user units, instead of guessing from -# our own --prefix. This is what makes the install correct under a -# non-standard prefix (e.g. /opt/local): systemd's search path doesn't move -# just because this project's prefix does, so deriving the unit dir from -# get_option('prefix') silently installs to a directory systemd never reads. -# Fall back to the old prefix-relative path only if systemd's pkg-config -# file isn't available to ask (e.g. a minimal cross sysroot). +# systemd's user-unit search path does not follow our --prefix, so a +# prefix-relative install dir lands where systemd never looks. Fall back to +# one only when systemd.pc isn't available to ask (e.g. a minimal sysroot). systemd_dep = dependency('systemd', required: false) if systemd_dep.found() systemd_user_unit_dir = systemd_dep.get_variable(pkgconfig: 'systemduserunitdir') @@ -60,12 +56,8 @@ test('session-safe-mode', session_test_bash, meson.current_build_dir() / 'singularity-desktop-session', }) -# BindsTo=graphical-session.target so starting it pulls the target in as a -# side effect -- graphical-session.target itself is systemd-hardened against -# direct manual start (RefuseManualStart), by design: it must be reached via -# a bound unit. singularity-desktop-session starts this unit; that is the -# only thing that makes every WantedBy=graphical-session.target unit (our -# own xdg-desktop-portal-singularity.service among them) launch at all. +# graphical-session.target sets RefuseManualStart, so it can only be reached +# via a unit that BindsTo= it. This target is that unit. install_data( 'config/systemd-user/singularity-session.target', install_dir: systemd_user_unit_dir, diff --git a/src/singularity-desktop-session.in b/src/singularity-desktop-session.in index a3c179e..dc447c2 100755 --- a/src/singularity-desktop-session.in +++ b/src/singularity-desktop-session.in @@ -130,26 +130,10 @@ nohup "$(singularity_helper singularity-polkit-agent)" >> "$_STATE/polkit.log" 2 if [ -x "$BIN/ush-broker" ]; then nohup "$BIN/ush-broker" >> "$_STATE/ush-broker.log" 2>&1 & fi -# Pull in graphical-session.target so every systemd --user unit declaring -# WantedBy=graphical-session.target actually launches: our own -# xdg-desktop-portal-singularity.service among them, plus foot-server. -# Without this the target sits permanently inactive and those units never -# start at all, regardless of anything the portal ROUTER does below -- -# confirmed live on O6N 2026-08-27: xdg-desktop-portal correctly resolved -# and routed Screenshot requests to the singularity backend (portals.conf -# staging from 2026-08-25 was working fine), but the backend process -# itself never started because nothing had ever started -# graphical-session.target on this system. -# -# graphical-session.target itself refuses direct manual start -# (systemd-hardened, RefuseManualStart) -- it must be reached via a unit -# that BindsTo= it. labwc ships labwc-session.target for exactly this -# (its own docs/autostart recommends starting it), but that unit is not -# actually installed by this build, so singularity-session.target -# (config/systemd-user/, installed by this repo's own meson.build) plays -# the same role: BindsTo=graphical-session.target, -# Wants=graphical-session-pre.target, so starting it gets the pre-target -# sequencing for free too. +# graphical-session.target sets RefuseManualStart and can only be reached via +# a unit that BindsTo= it. Without this start, the target stays inactive and +# every WantedBy=graphical-session.target unit -- our own +# xdg-desktop-portal-singularity.service, foot-server -- silently never runs. systemctl --user --no-block start singularity-session.target 2>/dev/null || true # Restart the portal so it picks up the live session environment, but do NOT From b9ef1dfc8c16c59164b78b445797ff442badc0a0 Mon Sep 17 00:00:00 2001 From: Jason Perlow Date: Sat, 12 Sep 2026 17:49:36 -0400 Subject: [PATCH 4/6] session: stop singularity-session.target when the session ends 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. --- meson.build | 2 + src/singularity-desktop-session.in | 14 ++++- tests/session_target_lifecycle_test.sh | 76 ++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100755 tests/session_target_lifecycle_test.sh diff --git a/meson.build b/meson.build index e1e426b..d0dac2a 100644 --- a/meson.build +++ b/meson.build @@ -55,6 +55,8 @@ test('session-safe-mode', session_test_bash, 'SINGULARITY_TEST_DESKTOP_SESSION': meson.current_build_dir() / 'singularity-desktop-session', }) +test('session-target-lifecycle', session_test_bash, + args: files('tests/session_target_lifecycle_test.sh')) # graphical-session.target sets RefuseManualStart, so it can only be reached # via a unit that BindsTo= it. This target is that unit. diff --git a/src/singularity-desktop-session.in b/src/singularity-desktop-session.in index dc447c2..7b2ee26 100755 --- a/src/singularity-desktop-session.in +++ b/src/singularity-desktop-session.in @@ -120,7 +120,19 @@ if [ -f "$_SPID" ]; then fi fi echo $$ > "$_SPID" -trap "rm -f $_SPID" EXIT +_session_cleanup() { + rm -f "$_SPID" + # Nothing else ever stops the target we start below. graphical-session.target + # is StopWhenUnneeded=yes, so it only goes away once nothing binds it, and a + # user manager that outlives the session -- lingering enabled, or a second + # concurrent login -- keeps ours active after the compositor is gone, and + # with it every WantedBy=graphical-session.target service beneath it. + systemctl --user --no-block stop singularity-session.target 2>/dev/null || true +} +# EXIT only, deliberately: bash runs the EXIT trap for an untrapped fatal signal +# as well, while an explicit TERM trap would be deferred until the supervisor's +# foreground child returns -- which on the logout path it never does. +trap _session_cleanup EXIT pkill -TERM -x singularity-desktop 2>/dev/null || true pkill -x singularity-polkit-agent 2>/dev/null || true diff --git a/tests/session_target_lifecycle_test.sh b/tests/session_target_lifecycle_test.sh new file mode 100755 index 0000000..6b7cb12 --- /dev/null +++ b/tests/session_target_lifecycle_test.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# singularity-session.target is started by the desktop-session launcher and +# stopped by nothing else. graphical-session.target is StopWhenUnneeded=yes, so +# it only goes away once nothing binds it -- and a user manager that outlives +# the session (lingering enabled, or a second concurrent login) keeps ours +# active after the compositor exits, and with it the portal backend and foot +# server underneath. This asserts the launcher's exit path issues the matching +# stop, and that the older pid-file cleanup still runs alongside it. +set -eu + +# Re-entrant stub: the launcher runs this same file as singularity-desktop, and +# a non-zero exit drives the supervisor to its crash budget so the script exits. +if [ "${SINGULARITY_TEST_FAKE_DESKTOP:-0}" = "1" ]; then + exit 1 +fi + +TEST_DIR=$(mktemp -d "${TMPDIR:-/tmp}/singularity-session-target-test.XXXXXX") +trap 'rm -rf "$TEST_DIR"' EXIT +mkdir -p "$TEST_DIR/state" "$TEST_DIR/runtime" "$TEST_DIR/bin" +chmod 700 "$TEST_DIR/runtime" + +SYSTEMCTL_LOG="$TEST_DIR/systemctl.log" +: > "$SYSTEMCTL_LOG" + +cat > "$TEST_DIR/bin/systemctl" <> "$SYSTEMCTL_LOG" +EOF +# Keep the launcher's environment probing off the machine running the test. +for stub in pkill xdg-user-dirs-update dbus-update-activation-environment; do + printf '#!/bin/sh\nexit 0\n' > "$TEST_DIR/bin/$stub" +done +printf '#!/bin/sh\nprintf "false\\n"\n' > "$TEST_DIR/bin/gsettings" +chmod +x "$TEST_DIR"/bin/* + +export PATH="$TEST_DIR/bin:$PATH" +export XDG_STATE_HOME="$TEST_DIR/state" +export XDG_RUNTIME_DIR="$TEST_DIR/runtime" +export DBUS_SESSION_BUS_ADDRESS="test-bus" +export SINGULARITY_SESSION_BUILD_ID="session-target-test-build" +export SINGULARITY_DESKTOP_BINARY="$0" +export SINGULARITY_TEST_FAKE_DESKTOP=1 + +LAUNCHER="$(dirname "$0")/../src/singularity-desktop-session" +# The supervisor kills $PPID once it gives up, so run the launcher under a +# wrapper that absorbs that signal instead of the test process itself. +set +e +bash -c 'trap "" TERM; bash "$1" & child=$!; wait "$child"' _ "$LAUNCHER" +set -e + +grep -Fq -- "--user --no-block start singularity-session.target" "$SYSTEMCTL_LOG" || { + echo "launcher never started singularity-session.target" >&2 + cat "$SYSTEMCTL_LOG" >&2 + exit 1 +} +grep -Fq -- "--user --no-block stop singularity-session.target" "$SYSTEMCTL_LOG" || { + echo "launcher exited without stopping singularity-session.target" >&2 + cat "$SYSTEMCTL_LOG" >&2 + exit 1 +} + +# Order matters: a stop that raced ahead of the start would leave the target up. +START_LINE=$(grep -Fn -- "start singularity-session.target" "$SYSTEMCTL_LOG" | head -1 | cut -d: -f1) +STOP_LINE=$(grep -Fn -- "stop singularity-session.target" "$SYSTEMCTL_LOG" | head -1 | cut -d: -f1) +[ "$STOP_LINE" -gt "$START_LINE" ] || { + echo "stop (line $STOP_LINE) did not follow start (line $START_LINE)" >&2 + cat "$SYSTEMCTL_LOG" >&2 + exit 1 +} + +# The pre-existing pid-file cleanup has to survive being moved into the +# trap function that now also stops the target. +[ ! -e "$XDG_RUNTIME_DIR/singularity-desktop-session.pid" ] || { + echo "launcher left its pid file behind" >&2 + exit 1 +} From 702451dab970342bb16513c292567062ef6697d1 Mon Sep 17 00:00:00 2001 From: Jason Perlow Date: Mon, 14 Sep 2026 11:21:05 -0400 Subject: [PATCH 5/6] fix: point session-target-lifecycle test at the built launcher 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 #6 (session target start/stop); mirrored the existing fix pattern from session_safe_mode_test.sh. --- meson.build | 6 +++++- tests/session_target_lifecycle_test.sh | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index d0dac2a..e50a45c 100644 --- a/meson.build +++ b/meson.build @@ -56,7 +56,11 @@ test('session-safe-mode', session_test_bash, meson.current_build_dir() / 'singularity-desktop-session', }) test('session-target-lifecycle', session_test_bash, - args: files('tests/session_target_lifecycle_test.sh')) + args: files('tests/session_target_lifecycle_test.sh'), + env: { + 'SINGULARITY_TEST_DESKTOP_SESSION': + meson.current_build_dir() / 'singularity-desktop-session', + }) # graphical-session.target sets RefuseManualStart, so it can only be reached # via a unit that BindsTo= it. This target is that unit. diff --git a/tests/session_target_lifecycle_test.sh b/tests/session_target_lifecycle_test.sh index 6b7cb12..9835cb4 100755 --- a/tests/session_target_lifecycle_test.sh +++ b/tests/session_target_lifecycle_test.sh @@ -41,7 +41,10 @@ export SINGULARITY_SESSION_BUILD_ID="session-target-test-build" export SINGULARITY_DESKTOP_BINARY="$0" export SINGULARITY_TEST_FAKE_DESKTOP=1 -LAUNCHER="$(dirname "$0")/../src/singularity-desktop-session" +# The launcher under test is generated, so point the test at the configured +# copy in the build dir (SINGULARITY_TEST_DESKTOP_SESSION, set in meson.build). +# Outside meson, fall back to the raw .in template. +LAUNCHER="${SINGULARITY_TEST_DESKTOP_SESSION:-$(dirname "$0")/../src/singularity-desktop-session.in}" # The supervisor kills $PPID once it gives up, so run the launcher under a # wrapper that absorbs that signal instead of the test process itself. set +e From 57fe5c2cff935895fe0b9bd1bc16764cad221141 Mon Sep 17 00:00:00 2001 From: Jason Perlow Date: Mon, 14 Sep 2026 11:36:59 -0400 Subject: [PATCH 6/6] session: reference-count singularity-session.target stop Mirko (PR #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 #6 review comment on commit aec8a46. --- src/singularity-desktop-session.in | 21 ++++++++++++----- tests/session_target_lifecycle_test.sh | 32 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/singularity-desktop-session.in b/src/singularity-desktop-session.in index 7b2ee26..f4674dd 100755 --- a/src/singularity-desktop-session.in +++ b/src/singularity-desktop-session.in @@ -120,13 +120,22 @@ if [ -f "$_SPID" ]; then fi fi echo $$ > "$_SPID" +# One systemd --user manager is shared by every login of the same UID, so a +# second concurrent login (a second seat, or console + SSH) runs its own copy +# of this launcher under the SAME manager and the SAME singularity-session.target. +# A marker per running instance turns the unconditional stop below into a +# reference count: only the last instance to exit actually stops the target. +_SESSION_MARKERS="${XDG_RUNTIME_DIR:-/tmp}/singularity-session.d" +mkdir -p "$_SESSION_MARKERS" +_SESSION_MARKER="$_SESSION_MARKERS/$$" +: > "$_SESSION_MARKER" _session_cleanup() { - rm -f "$_SPID" - # Nothing else ever stops the target we start below. graphical-session.target - # is StopWhenUnneeded=yes, so it only goes away once nothing binds it, and a - # user manager that outlives the session -- lingering enabled, or a second - # concurrent login -- keeps ours active after the compositor is gone, and - # with it every WantedBy=graphical-session.target service beneath it. + rm -f "$_SPID" "$_SESSION_MARKER" + # graphical-session.target is StopWhenUnneeded=yes, so it only goes away + # once nothing binds it -- stopping singularity-session.target while a + # sibling login's marker is still present would tear down that login's + # portal/audio-autoswitch/etc out from under it. + [ -z "$(ls -A "$_SESSION_MARKERS" 2>/dev/null)" ] || return 0 systemctl --user --no-block stop singularity-session.target 2>/dev/null || true } # EXIT only, deliberately: bash runs the EXIT trap for an untrapped fatal signal diff --git a/tests/session_target_lifecycle_test.sh b/tests/session_target_lifecycle_test.sh index 9835cb4..4c31d02 100755 --- a/tests/session_target_lifecycle_test.sh +++ b/tests/session_target_lifecycle_test.sh @@ -77,3 +77,35 @@ STOP_LINE=$(grep -Fn -- "stop singularity-session.target" "$SYSTEMCTL_LOG" | hea echo "launcher left its pid file behind" >&2 exit 1 } + +# Scenario 2: a sibling login (same UID, same XDG_RUNTIME_DIR, same systemd +# --user manager) is still running when this login exits. Simulated by +# planting a session marker the launcher did not create itself -- it must +# leave the target running for the sibling rather than stopping it. +: > "$SYSTEMCTL_LOG" +SIBLING_MARKER="$XDG_RUNTIME_DIR/singularity-session.d/sibling-fake-pid" +mkdir -p "$(dirname "$SIBLING_MARKER")" +: > "$SIBLING_MARKER" + +set +e +bash -c 'trap "" TERM; bash "$1" & child=$!; wait "$child"' _ "$LAUNCHER" +set -e + +grep -Fq -- "--user --no-block start singularity-session.target" "$SYSTEMCTL_LOG" || { + echo "launcher never started singularity-session.target on the second run" >&2 + cat "$SYSTEMCTL_LOG" >&2 + exit 1 +} +if grep -Fq -- "--user --no-block stop singularity-session.target" "$SYSTEMCTL_LOG"; then + echo "launcher stopped singularity-session.target while a sibling login's marker was still present" >&2 + cat "$SYSTEMCTL_LOG" >&2 + exit 1 +fi +[ -e "$SIBLING_MARKER" ] || { + echo "launcher removed a marker it did not create" >&2 + exit 1 +} +[ ! -e "$XDG_RUNTIME_DIR/singularity-desktop-session.pid" ] || { + echo "launcher left its pid file behind on the second run" >&2 + exit 1 +}