From 38067726c20969defa26c321dfbbdab721612303 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Mon, 28 Sep 2026 11:11:01 +0100 Subject: [PATCH] Activate the graphical session and refresh portal services at login --- docs/installation.md | 6 ++- packaging/ferese-session-shell | 42 +++++++++++++-- packaging/systemd/ferese-session.target | 6 +++ scripts/install-session.sh | 6 +++ scripts/install.sh | 2 +- scripts/tests/test_session_startup.py | 72 +++++++++++++++++++++++++ 6 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 packaging/systemd/ferese-session.target create mode 100644 scripts/tests/test_session_startup.py diff --git a/docs/installation.md b/docs/installation.md index a55827f..e9c989f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -14,7 +14,11 @@ desktop instead of direct access to the graphics device. Build with **Rust 1.95 or newer**, a C/C++ toolchain, CMake, pkg-config, and the native development libraries below. The installer builds Ferese, its shell, Settings, command-line tools, locker, polkit agent, and ScreenCast portal together. It does not install system -packages or enable services for you. +packages or enable background services at boot. On systemd systems, the installed +session helper activates `ferese-session.target` after exporting the compositor's +Wayland display, and stops it when the shell exits. This keeps +`graphical-session.target` active so desktop portals can start. Nested previews +do not alter the host's activation environment or session targets. ### Fedora diff --git a/packaging/ferese-session-shell b/packaging/ferese-session-shell index 4013d7e..c3d6629 100755 --- a/packaging/ferese-session-shell +++ b/packaging/ferese-session-shell @@ -1,12 +1,47 @@ #!/usr/bin/env bash set -euo pipefail +session_target_started=0 +shell_pid= +agent_pid= +cleanup() { + trap - EXIT INT TERM + if [[ -n $shell_pid ]]; then kill "$shell_pid" 2>/dev/null || true; fi + if [[ -n $agent_pid ]]; then + kill "$agent_pid" 2>/dev/null || true + wait "$agent_pid" 2>/dev/null || true + fi + if ((session_target_started)); then + systemctl --user stop ferese-session.target || true + fi +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + # The compositor supplies its new Wayland socket before this helper starts. # Nested previews must never replace the host session's activation environment. if [[ ${FERESE_SESSION_IMPORT_ENV:-0} == 1 ]]; then - dbus-update-activation-environment --systemd \ + # Shell control uses a private socket FD. Activation must use the public + # socket so portal and other desktop services connect as ordinary clients. + public_display=${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}} + env WAYLAND_DISPLAY="$public_display" dbus-update-activation-environment --systemd \ WAYLAND_DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE || \ - dbus-update-activation-environment \ + env WAYLAND_DISPLAY="$public_display" dbus-update-activation-environment \ WAYLAND_DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE || true + # User buses can outlive logout and may predate a newly installed service directory. + if command -v dbus-send >/dev/null; then + dbus-send --session --type=method_call --print-reply \ + --dest=org.freedesktop.DBus /org/freedesktop/DBus \ + org.freedesktop.DBus.ReloadConfig >/dev/null || true + fi + if command -v systemctl >/dev/null; then + systemctl --user daemon-reload || true + if systemctl --user start ferese-session.target; then + session_target_started=1 + else + echo 'Ferese: graphical session target could not start; portal activation may be unavailable.' >&2 + fi + fi fi release_dir=$(dirname -- "$(readlink -f -- "$0")") env -u WAYLAND_SOCKET WAYLAND_DISPLAY="${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}}" \ @@ -14,12 +49,9 @@ env -u WAYLAND_SOCKET WAYLAND_DISPLAY="${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAN agent_pid=$! "$release_dir/ferese-shell" & shell_pid=$! -trap 'kill "$shell_pid" "$agent_pid" 2>/dev/null || true' EXIT INT TERM if wait "$shell_pid"; then status=0 else status=$? fi -kill "$agent_pid" 2>/dev/null || true -wait "$agent_pid" 2>/dev/null || true exit "$status" diff --git a/packaging/systemd/ferese-session.target b/packaging/systemd/ferese-session.target new file mode 100644 index 0000000..519ce42 --- /dev/null +++ b/packaging/systemd/ferese-session.target @@ -0,0 +1,6 @@ +[Unit] +Description=Ferese graphical session +BindsTo=graphical-session.target +Before=graphical-session.target +# Ferese Shell owns the notification service for this session. +Conflicts=swaync.service diff --git a/scripts/install-session.sh b/scripts/install-session.sh index f08cee5..e922536 100644 --- a/scripts/install-session.sh +++ b/scripts/install-session.sh @@ -73,6 +73,11 @@ if [[ -e $service ]] && ! cmp -s "$repo_dir/packaging/portal/$(basename "$servic echo "Unmanaged portal service exists: $service" >&2; exit 1 fi +session_target=/usr/local/lib/systemd/user/ferese-session.target +if [[ -e $session_target ]] && ! cmp -s "$repo_dir/packaging/systemd/ferese-session.target" "$session_target"; then + echo "Unmanaged session target exists: $session_target" >&2; exit 1 +fi + desktop-file-validate "$repo_dir/packaging/ferese.desktop" desktop-file-validate "$repo_dir/packaging/dev.ferese.Settings.desktop" @@ -85,6 +90,7 @@ for name in ferese ferese-shell ferese-settings feresectl ferese-lock ferese-pol done install -m 0755 -- "$repo_dir/packaging/ferese-session" "$release_dir/ferese-session" install -m 0755 -- "$repo_dir/packaging/ferese-session-shell" "$release_dir/ferese-session-shell" +install -D -m 0644 -- "$repo_dir/packaging/systemd/ferese-session.target" "$session_target" install -m 0755 -- "$repo_dir/packaging/ferese-screenshot" "$release_dir/ferese-screenshot" install -m 0644 -- "$repo_dir/packaging/config.kdl" "$release_dir/config.example.kdl" install -D -m 0644 -- "$repo_dir/assets/fonts/Comfortaa-LICENSE.txt" "$release_dir/licenses/Comfortaa-LICENSE.txt" diff --git a/scripts/install.sh b/scripts/install.sh index 5e3928d..c199f76 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -75,7 +75,7 @@ fi cd -- "$repo_dir" if ! $skip_build; then "${build[@]}"; fi -for name in ferese ferese-shell ferese-settings feresectl ferese-lock ferese-polkit-agent; do +for name in ferese ferese-shell ferese-settings feresectl ferese-lock ferese-polkit-agent xdg-desktop-portal-ferese ferese-record; do [[ -x $repo_dir/target/release/$name ]] || fail "missing release binary: $name" done "${installer[@]}" diff --git a/scripts/tests/test_session_startup.py b/scripts/tests/test_session_startup.py new file mode 100644 index 0000000..71ca7ff --- /dev/null +++ b/scripts/tests/test_session_startup.py @@ -0,0 +1,72 @@ +"""Session startup regressions using fake services; never touches the host bus.""" +import os +from pathlib import Path +import shutil +import signal +import subprocess +import tempfile +import time +import unittest + +SCRIPT = Path(__file__).resolve().parents[2] / 'packaging/ferese-session-shell' + + +class SessionStartupTest(unittest.TestCase): + def run_session(self, direct=True, fail_target=False, shell_status=0, terminate=False): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + helper = root / 'ferese-session-shell' + shutil.copyfile(SCRIPT, helper) + programs = { + 'dbus-update-activation-environment': 'printf "import:%s\\n" "$WAYLAND_DISPLAY" >> "$TEST_ROOT/events"\n', + 'dbus-send': 'echo reload-bus >> "$TEST_ROOT/events"\n', + 'systemctl': 'echo "$*" >> "$TEST_ROOT/events"\nif [[ $* == "--user start ferese-session.target" && $TEST_FAIL == 1 ]]; then exit 1; fi\n', + 'ferese-polkit-agent': 'exec sleep 30\n', + 'ferese-shell': 'printf "shell:%s\\n" "$WAYLAND_SOCKET" >> "$TEST_ROOT/events"\nif [[ $TEST_WAIT == 1 ]]; then exec sleep 30; fi\nexit "$TEST_STATUS"\n', + } + for name, body in programs.items(): + path = root / name + path.write_text('#!/bin/bash\n' + body) + path.chmod(0o755) + env = dict(os.environ, PATH=f'{root}:{os.environ["PATH"]}', TEST_ROOT=directory, + FERESE_SESSION_IMPORT_ENV=str(int(direct)), WAYLAND_DISPLAY='private-display', + FERESE_PUBLIC_WAYLAND_DISPLAY='public-display', WAYLAND_SOCKET='77', + TEST_FAIL=str(int(fail_target)), TEST_STATUS=str(shell_status), TEST_WAIT=str(int(terminate))) + child = subprocess.Popen(['bash', str(helper)], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if terminate: + deadline = time.monotonic() + 3 + while not (root / 'events').exists() or 'shell:77' not in (root / 'events').read_text(): + if time.monotonic() > deadline: + child.kill() + self.fail('Session shell did not start') + time.sleep(.01) + child.send_signal(signal.SIGTERM) + child.communicate(timeout=4) + return child.returncode, (root / 'events').read_text().splitlines() + + def test_direct_session_imports_public_display_before_activation_and_cleans_up(self): + status, events = self.run_session(shell_status=7) + self.assertEqual(status, 7) + self.assertEqual(events, ['import:public-display', 'reload-bus', '--user daemon-reload', + '--user start ferese-session.target', 'shell:77', + '--user stop ferese-session.target']) + + def test_nested_session_never_changes_host_activation(self): + status, events = self.run_session(direct=False) + self.assertEqual(status, 0) + self.assertEqual(events, ['shell:77']) + + def test_failed_target_does_not_prevent_shell_or_stop_unowned_target(self): + status, events = self.run_session(fail_target=True) + self.assertEqual(status, 0) + self.assertIn('shell:77', events) + self.assertNotIn('--user stop ferese-session.target', events) + + def test_termination_stops_the_session_target(self): + status, events = self.run_session(terminate=True) + self.assertEqual(status, 143) + self.assertEqual(events[-1], '--user stop ferese-session.target') + + +if __name__ == '__main__': + unittest.main()