From 168e7a89afb3bd4733af324c1cd4582c41b52962 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 2 Oct 2026 18:07:44 +0200 Subject: [PATCH 01/11] Add support for sysupdate --- src/Application.vala | 2 + .../SystemUpdate/SystemDSystemUpdate.vala | 163 ++++++++++++++++++ src/DBus/Sysupdate.vala | 54 ++++++ src/Utils/SysupdateJob.vala | 33 ++++ src/Utils/SysupdateJobManager.vala | 57 ++++++ src/Utils/SysupdateTarget.vala | 93 ++++++++++ src/meson.build | 5 + 7 files changed, 407 insertions(+) create mode 100644 src/Backends/SystemUpdate/SystemDSystemUpdate.vala create mode 100644 src/DBus/Sysupdate.vala create mode 100644 src/Utils/SysupdateJob.vala create mode 100644 src/Utils/SysupdateJobManager.vala create mode 100644 src/Utils/SysupdateTarget.vala diff --git a/src/Application.vala b/src/Application.vala index 2e3adea7..32159805 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -86,6 +86,8 @@ public sealed class SettingsDaemon.Application : Gtk.Application { #if UBUNTU_DRIVERS connection.register_object (object_path, new Backends.UbuntuDrivers ()); #endif + } else { + connection.register_object (object_path, new Backends.SystemDSystemUpdate ()); } return true; diff --git a/src/Backends/SystemUpdate/SystemDSystemUpdate.vala b/src/Backends/SystemUpdate/SystemDSystemUpdate.vala new file mode 100644 index 00000000..55cc4544 --- /dev/null +++ b/src/Backends/SystemUpdate/SystemDSystemUpdate.vala @@ -0,0 +1,163 @@ +/* + * Copyright 2023 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +[DBus (name="io.elementary.settings_daemon.SystemUpdate")] +public class SettingsDaemon.Backends.SystemDSystemUpdate : Object { + public struct UpdateDetails { + string[] packages; + uint64 size; + Pk.Info[] info; + } + + private const string NOTIFICATION_ID = "system-update"; + + public signal void state_changed (); + + private PkUtils.CurrentState current_state; + private UpdateDetails update_details; + + private Utils.SysupdateTarget target; + private Cancellable? current_cancellable; + + construct { + current_state = { + UP_TO_DATE, + "", + 0, + 0 + }; + + update_details = { + {}, + 0, + {} + }; + + target = new Utils.SysupdateTarget (Sysupdate.Target.HOST_PATH); + } + + public async void check_for_updates (bool force, bool notify) throws DBusError, IOError { + if (SettingsDaemon.Utils.is_running_in_demo_mode () && !force) { + return; + } + + if (current_state.state != UP_TO_DATE && current_state.state != AVAILABLE && !force) { + return; + } + + update_state (CHECKING); + + /* First check if there is an even newer version than anything we have installed */ + string? new_version = null; + try { + new_version = yield target.check_new (); + } catch (Error e) { + critical ("Failed to check for updates: %s", e.message); + update_state (UP_TO_DATE); + return; + } + + if (new_version != null) { + update_details = { + { new_version }, + 0, + { Pk.Info.IMPORTANT } + }; + + update_state (AVAILABLE); + return; + } + + /* Then check if we maybe have a newer version already installed but not booted into */ + try { + if (yield target.has_newer_unapplied_version ()) { + update_state (RESTART_REQUIRED); + return; + } + } catch (Error e) { + critical ("Failed to check for newer unapplied version: %s", e.message); + } + + update_state (UP_TO_DATE); + } + + public async void update () throws DBusError, IOError { + if (current_state.state != AVAILABLE) { + return; + } + + update_state (DOWNLOADING); + + current_cancellable = new Cancellable (); + + try { + yield target.update (NEWEST, current_cancellable, progress_callback); + } catch (Error e) { + critical ("Failed to update: %s", e.message); + send_error (e.message); + return; + } + + update_state (RESTART_REQUIRED); + } + + public void cancel () throws DBusError, IOError { + if (current_state.state != DOWNLOADING) { + return; + } + + current_cancellable.cancel (); + } + + private void progress_callback (uint percentage) { + update_state ( + DOWNLOADING, + _("Downloading the latest version"), + percentage, + 0 + ); + } + + private void send_error (string message) { + var notification = new Notification (_("System updates couldn't be installed")); + notification.set_body (_("An error occurred while trying to update your system")); + notification.set_icon (new ThemedIcon ("dialog-error")); + notification.set_default_action (Application.ACTION_PREFIX + Application.SHOW_UPDATES_ACTION); + + GLib.Application.get_default ().send_notification (NOTIFICATION_ID, notification); + + update_state (ERROR, message); + } + + private void update_state ( + PkUtils.State state, + string message = "", + uint percentage = 0, + uint64 download_size_remaining = 0 + ) { + current_state = { + state, + message, + percentage, + download_size_remaining + }; + + state_changed (); + } + + public async PkUtils.CurrentState get_current_state () throws DBusError, IOError { + return current_state; + } + + public async UpdateDetails get_update_details () throws DBusError, IOError { + return update_details; + } + + public async int64 get_last_refresh_time () throws DBusError, IOError { + return 0; + } +} diff --git a/src/DBus/Sysupdate.vala b/src/DBus/Sysupdate.vala new file mode 100644 index 00000000..a6bb47a2 --- /dev/null +++ b/src/DBus/Sysupdate.vala @@ -0,0 +1,54 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +/** + * Some of the DBus interfaces here have manually implemented methods. + * The reason for that is that most of the methods are authenticated via polkit therefore + * we need to call them with ALLOW_INTERACTIVE_AUTHORIZATION which currently isn't possible + * via the normal vala way. Therefore all methods that are authenticated via polkit + * (even if they are by default allowed without privileges e.g. check_new) are manually + * implemented. + */ +namespace Sysupdate { + public const string BUS_NAME = "org.freedesktop.sysupdate1"; +} + +[DBus (name = "org.freedesktop.sysupdate1.Manager")] +private interface Sysupdate.Manager : Object { + public const string PATH = "/org/freedesktop/sysupdate1"; + + public signal void job_removed (uint64 job_id, ObjectPath object_path, int status); +} + +[DBus (name = "org.freedesktop.sysupdate1.Target")] +private interface Sysupdate.Target : DBusProxy { + public const string HOST_PATH = "/org/freedesktop/sysupdate1/target/host"; + + public async string check_new () throws Error { + var result = yield call ("CheckNew", null, ALLOW_INTERACTIVE_AUTHORIZATION, -1); + return (string) result.get_child_value (0); + } + + public abstract async string get_version () throws DBusError, IOError; + + public async void update (string new_version, uint64 flags, out string used_version, out uint64 job_id, out ObjectPath job_path) throws Error { + var parameters = new Variant.tuple ({ new_version, flags }); + + var result = yield call ("Update", parameters, ALLOW_INTERACTIVE_AUTHORIZATION, -1); + + used_version = (string) result.get_child_value (0); + job_id = (uint64) result.get_child_value (1); + job_path = (ObjectPath) result.get_child_value (2); + } +} + +[DBus (name = "org.freedesktop.sysupdate1.Job")] +private interface Sysupdate.Job : DBusProxy { + public async void cancel () throws Error { + yield call ("Cancel", null, ALLOW_INTERACTIVE_AUTHORIZATION, -1); + } +} diff --git a/src/Utils/SysupdateJob.vala b/src/Utils/SysupdateJob.vala new file mode 100644 index 00000000..de1b5226 --- /dev/null +++ b/src/Utils/SysupdateJob.vala @@ -0,0 +1,33 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class SettingsDaemon.Utils.SysupdateJob : Object { + private Cancellable cancellable; + private SysupdateTarget.ProgressCallback progress_callback; + private Sysupdate.Job job; + + /** + * Starts observing the given job, calling the progress callback with progess + * and cancelling it if the cancellable is triggered. + */ + public async SysupdateJob (ObjectPath job_path, Cancellable cancellable, SysupdateTarget.ProgressCallback progress_callback) throws Error { + this.cancellable = cancellable; + cancellable.cancelled.connect (cancel_job); + + this.progress_callback = progress_callback; + + this.job = yield Bus.get_proxy (SYSTEM, Sysupdate.BUS_NAME, job_path, NONE, cancellable); + } + + private async void cancel_job () requires (job != null) { + try { + yield job.cancel (); + } catch (Error e) { + warning ("Failed to cancel job: %s", e.message); + } + } +} diff --git a/src/Utils/SysupdateJobManager.vala b/src/Utils/SysupdateJobManager.vala new file mode 100644 index 00000000..bf2f9490 --- /dev/null +++ b/src/Utils/SysupdateJobManager.vala @@ -0,0 +1,57 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class SettingsDaemon.Utils.SysupdateJobManager : Object { + private Sysupdate.Manager manager; + + private HashTable received_object_paths; + + public async SysupdateJobManager (Cancellable cancellable) throws Error { + manager = yield Bus.get_proxy (SYSTEM, Sysupdate.BUS_NAME, Sysupdate.Manager.PATH, NONE, cancellable); + manager.job_removed.connect (on_job_removed); + } + + construct { + received_object_paths = new HashTable (str_hash, str_equal); + } + + private void on_job_removed (uint64 finished_job_id, string finished_object_path, int status) { + received_object_paths[finished_object_path] = status; + } + + /** + * Waits for the job to complete or returns immediately if the job has already completed. + * If the job has failed an error will be thrown. + * Note that for this to reliably work without races the this has to have been created before + * the job was started. + */ + public async void wait_for_job (string object_path) throws Error { + if (object_path in received_object_paths) { + check_status (object_path); + return; + } + + var signal_id = manager.job_removed.connect ((job_id, job_path) => { + if (job_path == object_path) { + wait_for_job.callback (); + } + }); + + yield; + + manager.disconnect (signal_id); + + check_status (object_path); + } + + private void check_status (string object_path) throws Error { + var status = received_object_paths[object_path]; + if (status != 0) { + throw new IOError.FAILED ("Job failed with status: %d".printf (status)); + } + } +} diff --git a/src/Utils/SysupdateTarget.vala b/src/Utils/SysupdateTarget.vala new file mode 100644 index 00000000..d653bd3c --- /dev/null +++ b/src/Utils/SysupdateTarget.vala @@ -0,0 +1,93 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class SettingsDaemon.Utils.SysupdateTarget : Object { + public enum UpdateVersion { + NEWEST, + CURRENT, + } + + public delegate void ProgressCallback (uint percentage); + + public string path { get; construct; } + + private GenericSet jobs; + + private Sysupdate.Target? target; + + public SysupdateTarget (string path) { + Object (path: path); + } + + construct { + jobs = new GenericSet (null, null); + } + + private async void ensure_connected () throws Error { + if (target != null) { + return; + } + + target = yield Bus.get_proxy (SYSTEM, Sysupdate.BUS_NAME, path, NONE, null); + } + + public async string? check_new () throws Error { + yield ensure_connected (); + + var new_version = yield target.check_new (); + + if (new_version == "") { + /* Docs say "" means no new version is available so make it clearer by returning null */ + return null; + } + + return new_version; + } + + /** + * Runs an update. This is both used to actually update the system to a newer version + * but also to apply newly selected or deselected features. + * You can specify what version you want to use with {@link version}. + */ + public async void update (UpdateVersion version, Cancellable cancellable, ProgressCallback progress_callback) throws Error { + yield ensure_connected (); + + /* "" means newest available so we use it when version is UpdateVersion.NEWEST */ + string version_string = ""; + + if (version == CURRENT) { + version_string = yield target.get_version (); + } + + var manager = yield new SysupdateJobManager (cancellable); + + string used_version; + uint64 job_id; + ObjectPath job_path; + yield target.update (version_string, 0, out used_version, out job_id, out job_path); + + var job = yield new SysupdateJob (job_path, cancellable, progress_callback); + jobs.add (job); + + yield manager.wait_for_job (job_path); + + jobs.remove (job); + } + + /** + * Checks if there is a new version already downloaded and installed but not booted into. + * Most commonly this means we ran update but haven't rebooted into the new version yet. + */ + public async bool has_newer_unapplied_version () throws Error { + yield ensure_connected (); + + var newest_installed_version = yield target.get_version (); + var booted_version = Environment.get_os_info ("IMAGE_VERSION"); + + return newest_installed_version != booted_version; + } +} diff --git a/src/meson.build b/src/meson.build index c0fd3128..196c8c8a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -11,13 +11,18 @@ sources = files( 'Backends/PowerProfilesSync.vala', 'Backends/PrefersColorSchemeSettings.vala', 'Backends/SystemUpdate/PackageKit.vala', + 'Backends/SystemUpdate/SystemDSystemUpdate.vala', 'Backends/SystemUpdate/SystemUpdate.vala', 'Backends/SystemUpdate/SystemUpdateProvider.vala', 'DBus/DesktopIntegration.vala', 'DBus/ShellKeyGrabber.vala', + 'DBus/Sysupdate.vala', 'Utils/PkUtils.vala', 'Utils/SessionUtils.vala', 'Utils/SunriseSunsetCalculator.vala', + 'Utils/SysupdateJob.vala', + 'Utils/SysupdateJobManager.vala', + 'Utils/SysupdateTarget.vala', ) args = [] From 112d48ca73c208115711d15629aa62af21db77de Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Mon, 5 Oct 2026 19:12:56 +0200 Subject: [PATCH 02/11] Better error handling --- src/Utils/SysupdateJobManager.vala | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Utils/SysupdateJobManager.vala b/src/Utils/SysupdateJobManager.vala index bf2f9490..0bcae833 100644 --- a/src/Utils/SysupdateJobManager.vala +++ b/src/Utils/SysupdateJobManager.vala @@ -50,8 +50,17 @@ public class SettingsDaemon.Utils.SysupdateJobManager : Object { private void check_status (string object_path) throws Error { var status = received_object_paths[object_path]; - if (status != 0) { - throw new IOError.FAILED ("Job failed with status: %d".printf (status)); + + if (status == 0) { + return; + } + + if (status > 0) { + throw new IOError.FAILED ("Job failed with exit code: %d".printf (status)); + } + + if (status < 0) { + throw IOError.from_errno (-status); } } } From a123e652b5e09983003452a6ac6ab12ea2020b13 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sun, 4 Oct 2026 20:11:10 +0200 Subject: [PATCH 03/11] Add some more logging --- src/Backends/SystemUpdate/SystemDSystemUpdate.vala | 6 +++--- src/Utils/SysupdateTarget.vala | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Backends/SystemUpdate/SystemDSystemUpdate.vala b/src/Backends/SystemUpdate/SystemDSystemUpdate.vala index 55cc4544..415861df 100644 --- a/src/Backends/SystemUpdate/SystemDSystemUpdate.vala +++ b/src/Backends/SystemUpdate/SystemDSystemUpdate.vala @@ -49,7 +49,7 @@ public class SettingsDaemon.Backends.SystemDSystemUpdate : Object { return; } - update_state (CHECKING); + update_state (CHECKING, _("Checking for updates")); /* First check if there is an even newer version than anything we have installed */ string? new_version = null; @@ -113,10 +113,10 @@ public class SettingsDaemon.Backends.SystemDSystemUpdate : Object { current_cancellable.cancel (); } - private void progress_callback (uint percentage) { + private void progress_callback (string message, uint percentage) { update_state ( DOWNLOADING, - _("Downloading the latest version"), + message, percentage, 0 ); diff --git a/src/Utils/SysupdateTarget.vala b/src/Utils/SysupdateTarget.vala index d653bd3c..4a52d3f5 100644 --- a/src/Utils/SysupdateTarget.vala +++ b/src/Utils/SysupdateTarget.vala @@ -11,7 +11,7 @@ public class SettingsDaemon.Utils.SysupdateTarget : Object { CURRENT, } - public delegate void ProgressCallback (uint percentage); + public delegate void ProgressCallback (string message, uint percentage); public string path { get; construct; } @@ -54,6 +54,8 @@ public class SettingsDaemon.Utils.SysupdateTarget : Object { * You can specify what version you want to use with {@link version}. */ public async void update (UpdateVersion version, Cancellable cancellable, ProgressCallback progress_callback) throws Error { + progress_callback (_("Preparing update"), 0); + yield ensure_connected (); /* "" means newest available so we use it when version is UpdateVersion.NEWEST */ @@ -65,6 +67,8 @@ public class SettingsDaemon.Utils.SysupdateTarget : Object { var manager = yield new SysupdateJobManager (cancellable); + progress_callback (_("Starting update"), 0); + string used_version; uint64 job_id; ObjectPath job_path; From 98e785fe42322c2154958d729f5c6d09119f4469 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Mon, 5 Oct 2026 18:41:20 +0200 Subject: [PATCH 04/11] Add progress report --- src/DBus/Sysupdate.vala | 2 ++ src/Utils/SysupdateJob.vala | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/DBus/Sysupdate.vala b/src/DBus/Sysupdate.vala index a6bb47a2..55ba280b 100644 --- a/src/DBus/Sysupdate.vala +++ b/src/DBus/Sysupdate.vala @@ -48,6 +48,8 @@ private interface Sysupdate.Target : DBusProxy { [DBus (name = "org.freedesktop.sysupdate1.Job")] private interface Sysupdate.Job : DBusProxy { + public abstract uint progress { get; } + public async void cancel () throws Error { yield call ("Cancel", null, ALLOW_INTERACTIVE_AUTHORIZATION, -1); } diff --git a/src/Utils/SysupdateJob.vala b/src/Utils/SysupdateJob.vala index de1b5226..34da8230 100644 --- a/src/Utils/SysupdateJob.vala +++ b/src/Utils/SysupdateJob.vala @@ -21,6 +21,8 @@ public class SettingsDaemon.Utils.SysupdateJob : Object { this.progress_callback = progress_callback; this.job = yield Bus.get_proxy (SYSTEM, Sysupdate.BUS_NAME, job_path, NONE, cancellable); + + job.g_properties_changed.connect (on_properties_changed); } private async void cancel_job () requires (job != null) { @@ -30,4 +32,8 @@ public class SettingsDaemon.Utils.SysupdateJob : Object { warning ("Failed to cancel job: %s", e.message); } } + + private void on_properties_changed () { + progress_callback (_("Downloading new image"), job.progress); + } } From 9ea712fc99cebecd908e81cddfeb0262f3735d0c Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Mon, 5 Oct 2026 19:50:23 +0200 Subject: [PATCH 05/11] Fix leaking jobs --- src/Utils/SysupdateJob.vala | 36 +++++++++++++--------------------- src/Utils/SysupdateTarget.vala | 28 ++++++++++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/Utils/SysupdateJob.vala b/src/Utils/SysupdateJob.vala index 34da8230..448b39b4 100644 --- a/src/Utils/SysupdateJob.vala +++ b/src/Utils/SysupdateJob.vala @@ -6,34 +6,26 @@ */ public class SettingsDaemon.Utils.SysupdateJob : Object { - private Cancellable cancellable; - private SysupdateTarget.ProgressCallback progress_callback; - private Sysupdate.Job job; - - /** - * Starts observing the given job, calling the progress callback with progess - * and cancelling it if the cancellable is triggered. - */ - public async SysupdateJob (ObjectPath job_path, Cancellable cancellable, SysupdateTarget.ProgressCallback progress_callback) throws Error { - this.cancellable = cancellable; - cancellable.cancelled.connect (cancel_job); - - this.progress_callback = progress_callback; + public signal void progress_changed (string message, uint progress); - this.job = yield Bus.get_proxy (SYSTEM, Sysupdate.BUS_NAME, job_path, NONE, cancellable); + private Sysupdate.Job job; + public async SysupdateJob (ObjectPath job_path) throws Error { + job = yield Bus.get_proxy (SYSTEM, Sysupdate.BUS_NAME, job_path, NONE); job.g_properties_changed.connect (on_properties_changed); } - private async void cancel_job () requires (job != null) { - try { - yield job.cancel (); - } catch (Error e) { - warning ("Failed to cancel job: %s", e.message); - } + private void on_properties_changed () { + progress_changed (_("Downloading new image"), job.progress); } - private void on_properties_changed () { - progress_callback (_("Downloading new image"), job.progress); + public void cancel () { + job.cancel.begin ((obj, res) => { + try { + job.cancel.end (res); + } catch (Error e) { + warning ("Failed to cancel job: %s", e.message); + } + }); } } diff --git a/src/Utils/SysupdateTarget.vala b/src/Utils/SysupdateTarget.vala index 4a52d3f5..10ee9cd9 100644 --- a/src/Utils/SysupdateTarget.vala +++ b/src/Utils/SysupdateTarget.vala @@ -15,18 +15,12 @@ public class SettingsDaemon.Utils.SysupdateTarget : Object { public string path { get; construct; } - private GenericSet jobs; - private Sysupdate.Target? target; public SysupdateTarget (string path) { Object (path: path); } - construct { - jobs = new GenericSet (null, null); - } - private async void ensure_connected () throws Error { if (target != null) { return; @@ -74,12 +68,26 @@ public class SettingsDaemon.Utils.SysupdateTarget : Object { ObjectPath job_path; yield target.update (version_string, 0, out used_version, out job_id, out job_path); - var job = yield new SysupdateJob (job_path, cancellable, progress_callback); - jobs.add (job); + var job = yield new SysupdateJob (job_path); + job.progress_changed.connect (progress_callback); + + cancellable.cancelled.connect (job.cancel); - yield manager.wait_for_job (job_path); + if (cancellable.is_cancelled ()) { + job.cancel (); - jobs.remove (job); + /* Don't return we will get the actual result from wait_for_job + (e.g. cancelling takes a while, fails, etc.) */ + } + + try { + yield manager.wait_for_job (job_path); + } catch (Error e) { + throw e; + } finally { + job.progress_changed.disconnect (progress_callback); + cancellable.cancelled.disconnect (job.cancel); + } } /** From 61e2d2251b3f94b61ec3b5ef314f768cf6a08256 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Mon, 5 Oct 2026 19:53:00 +0200 Subject: [PATCH 06/11] Simplify --- src/Utils/SysupdateJob.vala | 16 +++++++++++++++- src/Utils/SysupdateTarget.vala | 14 ++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Utils/SysupdateJob.vala b/src/Utils/SysupdateJob.vala index 448b39b4..57978c1a 100644 --- a/src/Utils/SysupdateJob.vala +++ b/src/Utils/SysupdateJob.vala @@ -19,7 +19,7 @@ public class SettingsDaemon.Utils.SysupdateJob : Object { progress_changed (_("Downloading new image"), job.progress); } - public void cancel () { + private void cancel () { job.cancel.begin ((obj, res) => { try { job.cancel.end (res); @@ -28,4 +28,18 @@ public class SettingsDaemon.Utils.SysupdateJob : Object { } }); } + + public void start_observing (Cancellable cancellable, SysupdateTarget.ProgressCallback progress_callback) { + cancellable.cancelled.connect (cancel); + progress_changed.connect (progress_callback); + + if (cancellable.is_cancelled ()) { + cancel (); + } + } + + public void stop_observing (Cancellable cancellable, SysupdateTarget.ProgressCallback progress_callback) { + cancellable.cancelled.disconnect (cancel); + progress_changed.disconnect (progress_callback); + } } diff --git a/src/Utils/SysupdateTarget.vala b/src/Utils/SysupdateTarget.vala index 10ee9cd9..8b9608b7 100644 --- a/src/Utils/SysupdateTarget.vala +++ b/src/Utils/SysupdateTarget.vala @@ -69,24 +69,14 @@ public class SettingsDaemon.Utils.SysupdateTarget : Object { yield target.update (version_string, 0, out used_version, out job_id, out job_path); var job = yield new SysupdateJob (job_path); - job.progress_changed.connect (progress_callback); - - cancellable.cancelled.connect (job.cancel); - - if (cancellable.is_cancelled ()) { - job.cancel (); - - /* Don't return we will get the actual result from wait_for_job - (e.g. cancelling takes a while, fails, etc.) */ - } + job.start_observing (cancellable, progress_callback); try { yield manager.wait_for_job (job_path); } catch (Error e) { throw e; } finally { - job.progress_changed.disconnect (progress_callback); - cancellable.cancelled.disconnect (job.cancel); + job.stop_observing (cancellable, progress_callback); } } From b2de5729e8c2ce48aec6a2c12bf0e04ad73346d7 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Wed, 7 Oct 2026 12:45:49 +0200 Subject: [PATCH 07/11] Cleaner job handling --- src/Utils/SysupdateJob.vala | 47 ++++++++++++++++++------------ src/Utils/SysupdateJobManager.vala | 16 +++++----- src/Utils/SysupdateTarget.vala | 13 ++------- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/Utils/SysupdateJob.vala b/src/Utils/SysupdateJob.vala index 57978c1a..e0221359 100644 --- a/src/Utils/SysupdateJob.vala +++ b/src/Utils/SysupdateJob.vala @@ -6,20 +6,39 @@ */ public class SettingsDaemon.Utils.SysupdateJob : Object { - public signal void progress_changed (string message, uint progress); + public ObjectPath path { get; construct; } - private Sysupdate.Job job; + private Cancellable cancellable; + private SysupdateTarget.ProgressCallback progress_callback; - public async SysupdateJob (ObjectPath job_path) throws Error { - job = yield Bus.get_proxy (SYSTEM, Sysupdate.BUS_NAME, job_path, NONE); + private Sysupdate.Job? job; + + /** + * Starts observing the job at the given path, cancelling it when the cancellable is triggered + * and calling the progress callback with progress updates. + */ + public async SysupdateJob (ObjectPath path, Cancellable cancellable, SysupdateTarget.ProgressCallback progress_callback) throws Error { + Object (path: path); + + this.cancellable = cancellable; + this.progress_callback = progress_callback; + + job = yield Bus.get_proxy (SYSTEM, Sysupdate.BUS_NAME, path, NONE); job.g_properties_changed.connect (on_properties_changed); + + cancellable.cancelled.connect (cancel); + + if (cancellable.is_cancelled ()) { + /* The user tried to cancel while the job was being started */ + cancel (); + } } - private void on_properties_changed () { - progress_changed (_("Downloading new image"), job.progress); + private void on_properties_changed () requires (job != null) { + progress_callback (_("Downloading new image"), job.progress); } - private void cancel () { + private void cancel () requires (job != null) { job.cancel.begin ((obj, res) => { try { job.cancel.end (res); @@ -29,17 +48,7 @@ public class SettingsDaemon.Utils.SysupdateJob : Object { }); } - public void start_observing (Cancellable cancellable, SysupdateTarget.ProgressCallback progress_callback) { - cancellable.cancelled.connect (cancel); - progress_changed.connect (progress_callback); - - if (cancellable.is_cancelled ()) { - cancel (); - } - } - - public void stop_observing (Cancellable cancellable, SysupdateTarget.ProgressCallback progress_callback) { - cancellable.cancelled.disconnect (cancel); - progress_changed.disconnect (progress_callback); + internal void notify_completed () { + job = null; } } diff --git a/src/Utils/SysupdateJobManager.vala b/src/Utils/SysupdateJobManager.vala index 0bcae833..36c1358a 100644 --- a/src/Utils/SysupdateJobManager.vala +++ b/src/Utils/SysupdateJobManager.vala @@ -29,14 +29,14 @@ public class SettingsDaemon.Utils.SysupdateJobManager : Object { * Note that for this to reliably work without races the this has to have been created before * the job was started. */ - public async void wait_for_job (string object_path) throws Error { - if (object_path in received_object_paths) { - check_status (object_path); + public async void wait_for_job (SysupdateJob job) throws Error { + if (job.path in received_object_paths) { + check_status (job); return; } var signal_id = manager.job_removed.connect ((job_id, job_path) => { - if (job_path == object_path) { + if (job_path == job.path) { wait_for_job.callback (); } }); @@ -45,11 +45,13 @@ public class SettingsDaemon.Utils.SysupdateJobManager : Object { manager.disconnect (signal_id); - check_status (object_path); + check_status (job); } - private void check_status (string object_path) throws Error { - var status = received_object_paths[object_path]; + private void check_status (SysupdateJob job) throws Error { + job.notify_completed (); + + var status = received_object_paths[job.path]; if (status == 0) { return; diff --git a/src/Utils/SysupdateTarget.vala b/src/Utils/SysupdateTarget.vala index 8b9608b7..95e68e21 100644 --- a/src/Utils/SysupdateTarget.vala +++ b/src/Utils/SysupdateTarget.vala @@ -68,16 +68,9 @@ public class SettingsDaemon.Utils.SysupdateTarget : Object { ObjectPath job_path; yield target.update (version_string, 0, out used_version, out job_id, out job_path); - var job = yield new SysupdateJob (job_path); - job.start_observing (cancellable, progress_callback); - - try { - yield manager.wait_for_job (job_path); - } catch (Error e) { - throw e; - } finally { - job.stop_observing (cancellable, progress_callback); - } + var job = yield new SysupdateJob (job_path, cancellable, progress_callback); + + yield manager.wait_for_job (job); } /** From 207c447adda2bd9e87374de285624a6963149f1e Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Wed, 7 Oct 2026 13:03:05 +0200 Subject: [PATCH 08/11] Unowned progress callback --- src/Utils/SysupdateJob.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utils/SysupdateJob.vala b/src/Utils/SysupdateJob.vala index e0221359..e0108d3a 100644 --- a/src/Utils/SysupdateJob.vala +++ b/src/Utils/SysupdateJob.vala @@ -9,7 +9,7 @@ public class SettingsDaemon.Utils.SysupdateJob : Object { public ObjectPath path { get; construct; } private Cancellable cancellable; - private SysupdateTarget.ProgressCallback progress_callback; + private unowned SysupdateTarget.ProgressCallback progress_callback; private Sysupdate.Job? job; From ad4462d8007d7f288c005310dc13f1b708823a89 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 9 Oct 2026 12:26:54 +0200 Subject: [PATCH 09/11] Adjust to new SystemUpdateProvider interface --- src/Application.vala | 2 -- src/Backends/SystemUpdate/SystemDSystemUpdate.vala | 11 +---------- src/Backends/SystemUpdate/SystemUpdate.vala | 5 +++-- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 32159805..2e3adea7 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -86,8 +86,6 @@ public sealed class SettingsDaemon.Application : Gtk.Application { #if UBUNTU_DRIVERS connection.register_object (object_path, new Backends.UbuntuDrivers ()); #endif - } else { - connection.register_object (object_path, new Backends.SystemDSystemUpdate ()); } return true; diff --git a/src/Backends/SystemUpdate/SystemDSystemUpdate.vala b/src/Backends/SystemUpdate/SystemDSystemUpdate.vala index 415861df..67859b79 100644 --- a/src/Backends/SystemUpdate/SystemDSystemUpdate.vala +++ b/src/Backends/SystemUpdate/SystemDSystemUpdate.vala @@ -5,18 +5,9 @@ * Authored by: Leonhard Kargl */ -[DBus (name="io.elementary.settings_daemon.SystemUpdate")] -public class SettingsDaemon.Backends.SystemDSystemUpdate : Object { - public struct UpdateDetails { - string[] packages; - uint64 size; - Pk.Info[] info; - } - +public class SettingsDaemon.Backends.SystemDSystemUpdate : Object, SystemUpdateProvider { private const string NOTIFICATION_ID = "system-update"; - public signal void state_changed (); - private PkUtils.CurrentState current_state; private UpdateDetails update_details; diff --git a/src/Backends/SystemUpdate/SystemUpdate.vala b/src/Backends/SystemUpdate/SystemUpdate.vala index 4dcc838e..6b9f2843 100644 --- a/src/Backends/SystemUpdate/SystemUpdate.vala +++ b/src/Backends/SystemUpdate/SystemUpdate.vala @@ -13,10 +13,11 @@ public class SettingsDaemon.Backends.SystemUpdate : Object { construct { if (SettingsDaemon.Utils.is_sysupdate ()) { - return; + provider = new SystemDSystemUpdate (); + } else { + provider = new PackageKit (); } - provider = new PackageKit (); provider.state_changed.connect (() => state_changed ()); } From 97d457d85813f3dd413f9363b327752f9ae3b2fb Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 9 Oct 2026 12:27:58 +0200 Subject: [PATCH 10/11] Fix copyright year --- src/Backends/SystemUpdate/SystemDSystemUpdate.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Backends/SystemUpdate/SystemDSystemUpdate.vala b/src/Backends/SystemUpdate/SystemDSystemUpdate.vala index 67859b79..4bc2f1ee 100644 --- a/src/Backends/SystemUpdate/SystemDSystemUpdate.vala +++ b/src/Backends/SystemUpdate/SystemDSystemUpdate.vala @@ -1,5 +1,5 @@ /* - * Copyright 2023 elementary, Inc. (https://elementary.io) + * Copyright 2026 elementary, Inc. (https://elementary.io) * SPDX-License-Identifier: GPL-3.0-or-later * * Authored by: Leonhard Kargl From 8971c0251e3bb457f50432ddc90e9123d58c5e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 13 Oct 2026 10:06:02 -0700 Subject: [PATCH 11/11] Update POTFILES --- po/POTFILES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/po/POTFILES b/po/POTFILES index 064d75e7..c51f8465 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -2,6 +2,9 @@ data/settings-daemon.metainfo.xml.in src/Application.vala src/Backends/UbuntuDrivers.vala src/Backends/SystemUpdate/PackageKit.vala +src/Backends/SystemUpdate/SystemDSystemUpdate.vala src/Backends/SystemUpdate/SystemUpdate.vala src/Backends/SystemUpdate/SystemUpdateProvider.vala src/Utils/PkUtils.vala +src/Utils/SysupdateJob.vala +src/Utils/SysupdateTarget.vala