Activate the graphical session and refresh portal services at login

This commit is contained in:
Samuel Onoja committed 2026-09-28 11:11:01 +01:00
1 parent 41d63f6b18
commit 38067726c2
6 files changed
+127 -7

No files matched your search

+5 -1
View File
@@ -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 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, 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 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 ### Fedora
+37 -5
View File
@@ -1,12 +1,47 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail 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. # The compositor supplies its new Wayland socket before this helper starts.
# Nested previews must never replace the host session's activation environment. # Nested previews must never replace the host session's activation environment.
if [[ ${FERESE_SESSION_IMPORT_ENV:-0} == 1 ]]; then 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 || \ 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 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 fi
release_dir=$(dirname -- "$(readlink -f -- "$0")") release_dir=$(dirname -- "$(readlink -f -- "$0")")
env -u WAYLAND_SOCKET WAYLAND_DISPLAY="${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}}" \ 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=$! agent_pid=$!
"$release_dir/ferese-shell" & "$release_dir/ferese-shell" &
shell_pid=$! shell_pid=$!
trap 'kill "$shell_pid" "$agent_pid" 2>/dev/null || true' EXIT INT TERM
if wait "$shell_pid"; then if wait "$shell_pid"; then
status=0 status=0
else else
status=$? status=$?
fi fi
kill "$agent_pid" 2>/dev/null || true
wait "$agent_pid" 2>/dev/null || true
exit "$status" exit "$status"
+6
View File
@@ -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
+6
View File
@@ -73,6 +73,11 @@ if [[ -e $service ]] && ! cmp -s "$repo_dir/packaging/portal/$(basename "$servic
echo "Unmanaged portal service exists: $service" >&2; exit 1 echo "Unmanaged portal service exists: $service" >&2; exit 1
fi 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/ferese.desktop"
desktop-file-validate "$repo_dir/packaging/dev.ferese.Settings.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 done
install -m 0755 -- "$repo_dir/packaging/ferese-session" "$release_dir/ferese-session" 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 -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 0755 -- "$repo_dir/packaging/ferese-screenshot" "$release_dir/ferese-screenshot"
install -m 0644 -- "$repo_dir/packaging/config.kdl" "$release_dir/config.example.kdl" 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" install -D -m 0644 -- "$repo_dir/assets/fonts/Comfortaa-LICENSE.txt" "$release_dir/licenses/Comfortaa-LICENSE.txt"
+1 -1
View File
@@ -75,7 +75,7 @@ fi
cd -- "$repo_dir" cd -- "$repo_dir"
if ! $skip_build; then "${build[@]}"; fi 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" [[ -x $repo_dir/target/release/$name ]] || fail "missing release binary: $name"
done done
"${installer[@]}" "${installer[@]}"
+72
View File
@@ -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()