Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions po/POTFILES
Original file line number Diff line number Diff line change
Expand Up @@ -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
154 changes: 154 additions & 0 deletions src/Backends/SystemUpdate/SystemDSystemUpdate.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Copyright 2026 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

public class SettingsDaemon.Backends.SystemDSystemUpdate : Object, SystemUpdateProvider {
private const string NOTIFICATION_ID = "system-update";

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, _("Checking for updates"));

/* 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 (string message, uint percentage) {
update_state (
DOWNLOADING,
message,
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;
}
}
5 changes: 3 additions & 2 deletions src/Backends/SystemUpdate/SystemUpdate.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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 ());
}

Expand Down
56 changes: 56 additions & 0 deletions src/DBus/Sysupdate.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2026 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

/**
* 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 abstract uint progress { get; }

public async void cancel () throws Error {
yield call ("Cancel", null, ALLOW_INTERACTIVE_AUTHORIZATION, -1);
}
}
54 changes: 54 additions & 0 deletions src/Utils/SysupdateJob.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2026 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

public class SettingsDaemon.Utils.SysupdateJob : Object {
public ObjectPath path { get; construct; }

private Cancellable cancellable;
private unowned SysupdateTarget.ProgressCallback progress_callback;

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 () requires (job != null) {
progress_callback (_("Downloading new image"), job.progress);
}

private void cancel () requires (job != null) {
job.cancel.begin ((obj, res) => {
try {
job.cancel.end (res);
} catch (Error e) {
warning ("Failed to cancel job: %s", e.message);
}
});
}

internal void notify_completed () {
job = null;
}
}
68 changes: 68 additions & 0 deletions src/Utils/SysupdateJobManager.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2026 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

public class SettingsDaemon.Utils.SysupdateJobManager : Object {
private Sysupdate.Manager manager;

private HashTable<string, int> 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<string, int> (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 (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 == job.path) {
wait_for_job.callback ();
}
});

yield;

manager.disconnect (signal_id);

check_status (job);
}

private void check_status (SysupdateJob job) throws Error {
job.notify_completed ();

var status = received_object_paths[job.path];

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);
}
}
}
Loading
Loading