From 97a42315f299c4d4e3bb084dea8290eaa84aefe9 Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Sat, 4 Jul 2026 21:10:52 -0400 Subject: [PATCH] fix: replace per-user skel autostart with gated system-wide autostart The wizard's autostart entry was installed to /etc/skel/.config/autostart/, so every user ever created (live-session users, installer-created users, accounts added years later) received a private copy that launched the wizard unconditionally on first login. Suppression was purely reactive: the app deleted the per-user copy only when that user completed the flow, and live media had to manually rm the file from skel. Mechanism change: - Install a single autostart entry to /etc/xdg/autostart/ (XDG-standard, never copied per-user). Its Exec passes --autostart. - Gate at startup: with --autostart, main() exits 0 before any GTK/resource loading when the per-user completion marker ($XDG_CONFIG_HOME/snow-first-setup.done) exists or when running a live session (existing backend.is_live_session(), snow-linux.live=1). - On completion, write the marker instead of deleting a file: the remove-autostart-file script becomes complete-setup, which touches the marker and also rm -f's the legacy skel-copied per-user entry as upgrade cleanup (rm -f, since new users never have it). - Upgrade safety: pre-0.2.16 users keep a per-user copy with the same filename, which shadows the /etc/xdg/autostart entry per the XDG autostart spec, so no double launch; GApplication single-instance (org.frostyard.FirstSetup) guards any residual race. A dpkg maintscript removes the now-obsolete skel conffile on package upgrade. The OEM snow-user path (Session=firstsetup) and explicit launches are unaffected: the gate only applies to --autostart invocations. Fixes frostyard/snosi#355 Co-Authored-By: Claude Fable 5 --- data/meson.build | 21 ++++++++++------ ....frostyard.FirstSetup.autostart.desktop.in | 9 +++++++ debian/snow-first-setup.maintscript | 1 + snow_first_setup/application.py | 8 +++++++ snow_first_setup/core/backend.py | 17 +++++++++++-- snow_first_setup/main.py | 9 +++++++ snow_first_setup/scripts/complete-setup | 24 +++++++++++++++++++ snow_first_setup/scripts/meson.build | 2 +- .../scripts/remove-autostart-file | 13 ---------- snow_first_setup/views/progress.py | 6 ++--- 10 files changed, 84 insertions(+), 26 deletions(-) create mode 100644 data/org.frostyard.FirstSetup.autostart.desktop.in create mode 100644 debian/snow-first-setup.maintscript create mode 100755 snow_first_setup/scripts/complete-setup delete mode 100755 snow_first_setup/scripts/remove-autostart-file diff --git a/data/meson.build b/data/meson.build index ea7db46..22e446a 100644 --- a/data/meson.build +++ b/data/meson.build @@ -41,14 +41,18 @@ desktop_file = i18n.merge_file( install_dir: join_paths(get_option('datadir'), 'applications'), ) -custom_target( - 'install-autostart-desktop', - input: desktop_file, +# System-wide autostart entry (XDG autostart, not copied per-user via skel). +# The app self-gates on launch: with --autostart it exits immediately when the +# per-user completion marker exists or when running in a live session. A +# per-user file of the same name in ~/.config/autostart (from pre-0.2.16 skel +# copies) shadows this entry per the XDG autostart spec. +autostart_file = i18n.merge_file( + input: 'org.frostyard.FirstSetup.autostart.desktop.in', output: 'org.frostyard.FirstSetup.autostart.desktop', - command: ['cp', '@INPUT@', '@OUTPUT@'], - install_dir: join_paths(get_option('sysconfdir'), 'skel', '.config', 'autostart'), - build_by_default: true, - install : true, + type: 'desktop', + po_dir: '../po', + install: true, + install_dir: join_paths(get_option('sysconfdir'), 'xdg', 'autostart'), ) desktop_utils = find_program('desktop-file-validate', required: false) @@ -56,6 +60,9 @@ if desktop_utils.found() test('Validate desktop file', desktop_utils, args: [desktop_file] ) + test('Validate autostart desktop file', desktop_utils, + args: [autostart_file] + ) endif install_data( diff --git a/data/org.frostyard.FirstSetup.autostart.desktop.in b/data/org.frostyard.FirstSetup.autostart.desktop.in new file mode 100644 index 0000000..0e04ec7 --- /dev/null +++ b/data/org.frostyard.FirstSetup.autostart.desktop.in @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=SNOW First Setup +Exec=snow-first-setup --autostart +Icon=org.frostyard.FirstSetup +Terminal=false +Type=Application +Categories=System;GTK; +StartupNotify=true +NoDisplay=true diff --git a/debian/snow-first-setup.maintscript b/debian/snow-first-setup.maintscript new file mode 100644 index 0000000..918d724 --- /dev/null +++ b/debian/snow-first-setup.maintscript @@ -0,0 +1 @@ +rm_conffile /etc/skel/.config/autostart/org.frostyard.FirstSetup.autostart.desktop 0.2.16~ diff --git a/snow_first_setup/application.py b/snow_first_setup/application.py index 9b7af6c..bbb406d 100644 --- a/snow_first_setup/application.py +++ b/snow_first_setup/application.py @@ -100,6 +100,14 @@ def __register_arguments(self): _("Force the regular mode, independant of group."), None, ) + self.add_main_option( + "autostart", + ord("a"), + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _("Launched by the system autostart entry; exits early if setup is already complete (handled in main)."), + None, + ) self.add_main_option( "oem-mode", ord("o"), diff --git a/snow_first_setup/core/backend.py b/snow_first_setup/core/backend.py index 64b00b1..1a6dbef 100644 --- a/snow_first_setup/core/backend.py +++ b/snow_first_setup/core/backend.py @@ -46,6 +46,19 @@ def set_hostname(hostname: str): def set_theme(theme: str) -> str|None: return run_script("theme", [theme]) +def setup_complete_marker_path() -> str: + """Path of the per-user marker written when first setup completes. + + The system-wide autostart entry (installed to /etc/xdg/autostart) launches + the app with --autostart, which exits immediately when this marker exists. + """ + config_home = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config") + return os.path.join(config_home, "snow-first-setup.done") + +def is_setup_complete() -> bool: + """Check whether the current user already completed first setup.""" + return os.path.exists(setup_complete_marker_path()) + def is_live_session() -> bool: """Check if the system is running in live session mode. @@ -83,8 +96,8 @@ def remove_first_setup_user(): def oem_complete(): return run_script("oemcomplete", [], root=True) -def remove_autostart_file(): - return run_script("remove-autostart-file", []) +def mark_setup_complete(): + return run_script("complete-setup", []) def _setup_system(): return run_script("setup-system", []) diff --git a/snow_first_setup/main.py b/snow_first_setup/main.py index 77459fa..ce124ff 100644 --- a/snow_first_setup/main.py +++ b/snow_first_setup/main.py @@ -29,6 +29,15 @@ def main(version, moduledir: str, localedir: str): sys.exit(1) return + # When launched via the system-wide autostart entry (/etc/xdg/autostart), + # exit immediately if this user already completed first setup or if this + # is a live session (the live media runs the installer instead). Checked + # before any GTK/resource loading so the gate is essentially free. + if "--autostart" in sys.argv: + import snow_first_setup.core.backend as early_backend + if early_backend.is_setup_complete() or early_backend.is_live_session(): + return 0 + signal.signal(signal.SIGINT, signal.SIG_DFL) locale.bindtextdomain('snow-first-setup', localedir) locale.textdomain('snow-first-setup') diff --git a/snow_first_setup/scripts/complete-setup b/snow_first_setup/scripts/complete-setup new file mode 100755 index 0000000..f7ee20a --- /dev/null +++ b/snow_first_setup/scripts/complete-setup @@ -0,0 +1,24 @@ +#!/bin/bash +if ! [ -z "$1" ]; then + echo "usage:" + echo "complete-setup" + exit 5 +fi + +if [ "$UID" == "0" ]; then + echo "this script must be run as a regular user" + exit 7 +fi + +config_home="${XDG_CONFIG_HOME:-$HOME/.config}" + +# Record per-user completion. The system-wide autostart entry in +# /etc/xdg/autostart launches the app with --autostart, which exits +# immediately when this marker exists. +mkdir -p "$config_home" +touch "$config_home/snow-first-setup.done" + +# Legacy cleanup: users provisioned before v0.2.16 received a per-user +# autostart copy via /etc/skel; remove it if present so it no longer +# shadows (or duplicates) the system-wide entry. +rm -f "$config_home/autostart/org.frostyard.FirstSetup.autostart.desktop" diff --git a/snow_first_setup/scripts/meson.build b/snow_first_setup/scripts/meson.build index b489d32..e392180 100755 --- a/snow_first_setup/scripts/meson.build +++ b/snow_first_setup/scripts/meson.build @@ -18,7 +18,7 @@ sources = [ 'setup-system', 'user', 'remove-first-setup-user', - 'remove-autostart-file', + 'complete-setup', 'install-to-disk', 'enroll-key', 'recovery-key', diff --git a/snow_first_setup/scripts/remove-autostart-file b/snow_first_setup/scripts/remove-autostart-file deleted file mode 100755 index c282901..0000000 --- a/snow_first_setup/scripts/remove-autostart-file +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -if ! [ -z "$1" ]; then - echo "usage:" - echo "remove-autostart-file" - exit 5 -fi - -if [ "$UID" == "0" ]; then - echo "this script must be run as a regular user" - exit 7 -fi - -rm ~/.config/autostart/org.frostyard.FirstSetup.autostart.desktop diff --git a/snow_first_setup/views/progress.py b/snow_first_setup/views/progress.py index b62966a..5a0a13a 100644 --- a/snow_first_setup/views/progress.py +++ b/snow_first_setup/views/progress.py @@ -33,7 +33,7 @@ class VanillaProgress(Adw.Bin): __not_started = True __finished = False __already_skipped = False - __already_removed_autostart_file = False + __already_marked_complete = False def __init__(self, window, **kwargs): super().__init__(**kwargs) @@ -52,8 +52,8 @@ def set_page_inactive(self): return def finish(self): - if not self.__already_removed_autostart_file: - self.__already_removed_autostart_file = backend.remove_autostart_file() + if not self.__already_marked_complete: + self.__already_marked_complete = backend.mark_setup_complete() return True def __on_items_changed_thread(self, id: str, uid: str, state: backend.ProgressState, info: dict):