Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement system updates #88

Merged
merged 21 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from 20 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
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- name: Install Dependencies
run: |
apt update
apt install -y libaccountsservice-dev libdbus-1-dev libgranite-dev libgeoclue-2-dev libfwupd-dev meson valac
apt install -y libaccountsservice-dev libdbus-1-dev libgranite-dev libgeoclue-2-dev libfwupd-dev libpackagekit-glib2-dev meson valac
leolost2605 marked this conversation as resolved.
Show resolved Hide resolved
- name: Build
env:
DESTDIR: out
Expand Down
13 changes: 13 additions & 0 deletions data/io.elementary.settings-daemon.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,17 @@
<description>The day for calendars to use as the first day of the week</description>
</key>
</schema>

<schema path="/io/elementary/settings-daemon/system-update/" id="io.elementary.settings-daemon.system-update">
<key type="x" name="last-refresh-time">
<default>0</default>
<summary>When updates were last refreshed</summary>
<description>When the cache was last refreshed and checked for updates in unix utc.</description>
</key>
<key type="x" name="refresh-interval">
<default>86400</default>
<summary>How often to check for updates</summary>
<description>How often to check for updates in seconds.</description>
</key>
</schema>
</schemalist>
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ fwupd_dep = dependency('fwupd')
gio_dep = dependency ('gio-2.0')
glib_dep = dependency('glib-2.0')
granite_dep = dependency('granite', version: '>= 5.3.0')
pk_dep = dependency('packagekit-glib2')
i18n = import('i18n')
gettext_name = meson.project_name()

Expand Down
17 changes: 17 additions & 0 deletions src/Application.vala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
*/

public sealed class SettingsDaemon.Application : Gtk.Application {
public const string ACTION_PREFIX = "app.";
public const string SHOW_UPDATES_ACTION = "show-updates";

private AccountsService accounts_service;
private Pantheon.AccountsService pantheon_service;
private DisplayManager.AccountsService display_manager_service;
Expand Down Expand Up @@ -60,10 +63,24 @@ public sealed class SettingsDaemon.Application : Gtk.Application {
show_firmware_updates_action.activate.connect (show_firmware_updates);
add_action (show_firmware_updates_action);

var show_updates_action = new GLib.SimpleAction (SHOW_UPDATES_ACTION, null);
show_updates_action.activate.connect (() => {
GLib.AppInfo.launch_default_for_uri_async.begin ("settings://about/os", null);
});
add_action (show_updates_action);

setup_accounts_services.begin ();
hold ();
}

protected override bool dbus_register (DBusConnection connection, string object_path) throws Error {
base.dbus_register (connection, object_path);

connection.register_object (object_path, new Backends.SystemUpdate ());

return true;
}

private async void setup_accounts_services () {
unowned GLib.DBusConnection connection;
string path;
Expand Down
273 changes: 273 additions & 0 deletions src/Backends/SystemUpdate.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
/*
* Copyright 2023 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <[email protected]>
*/

[DBus (name="io.elementary.settings_daemon.SystemUpdate")]
public class SettingsDaemon.Backends.SystemUpdate : Object {
public enum State {
UP_TO_DATE,
CHECKING,
AVAILABLE,
DOWNLOADING,
RESTART_REQUIRED,
ERROR
}

public struct CurrentState {
State state;
string status;
}

public struct UpdateDetails {
string[] packages;
int size;
}

private const string NOTIFICATION_ID = "system-update";

public signal void state_changed ();

private static Settings settings = new GLib.Settings ("io.elementary.settings-daemon.system-update");

private CurrentState current_state;
private UpdateDetails update_details;

private Pk.Task task;
private Pk.PackageSack? available_updates = null;
private GLib.Cancellable cancellable;

construct {
current_state = {
UP_TO_DATE,
""
};

update_details = {
{},
0
};

task = new Pk.Task () {
only_download = true
};

cancellable = new GLib.Cancellable ();

try {
var last_offline_results = Pk.offline_get_results ();

if (last_offline_results.get_exit_code () != SUCCESS && last_offline_results.get_error_code () != null) {
send_error (last_offline_results.get_error_code ().details);
}
} catch (Error e) {
warning ("Couldn't determine last offline results: %s", e.message);
}

check_for_updates.begin (false, true);

Timeout.add_seconds ((uint) settings.get_int64 ("refresh-interval"), () => {
check_for_updates.begin (false, true);
return Source.CONTINUE;
});
danirabbit marked this conversation as resolved.
Show resolved Hide resolved
}

public async void check_for_updates (bool force, bool notify) throws DBusError, IOError {
if (current_state.state != UP_TO_DATE && current_state.state != AVAILABLE && !force) {
return;
}

update_state (CHECKING);

try {
yield task.refresh_cache_async (force, null, progress_callback);
} catch (Error e) {
warning ("Failed to refresh cache: %s", e.message);
}

try {
available_updates = (yield task.get_updates_async (Pk.Filter.NONE, null, progress_callback)).get_package_sack ();

settings.set_int64 ("last-refresh-time", new DateTime.now_utc ().to_unix ());

if (available_updates == null || available_updates.get_size () == 0) {
update_state (UP_TO_DATE);
return;
}

string[] package_names = {};

foreach (var package in available_updates.get_array ()) {
package_names += package.get_name ();
}

update_details = {
package_names,
0 //FIXME: Is there a way to get update size from PackageKit
};

if (notify) {
var notification = new Notification (_("Update available"));
notification.set_body (_("A system update is available"));
notification.set_icon (new ThemedIcon ("software-update-available"));
notification.set_default_action (Application.ACTION_PREFIX + Application.SHOW_UPDATES_ACTION);

GLib.Application.get_default ().send_notification (NOTIFICATION_ID, notification);
}

update_state (AVAILABLE);
} catch (Error e) {
warning ("Failed to get available updates: %s", e.message);
update_state (UP_TO_DATE);
}
}

public async void update () throws DBusError, IOError {
if (current_state.state != AVAILABLE) {
return;
}

cancellable.reset ();

update_state (DOWNLOADING);

try {
var results = yield task.update_packages_async (available_updates.get_ids (), cancellable, progress_callback);

if (results.get_exit_code () == CANCELLED) {
debug ("Updates were cancelled");
check_for_updates.begin (true, false);
return;
}

Pk.offline_trigger (REBOOT);

var notification = new Notification (_("Restart required"));
notification.set_body (_("Please restart your system to finalize updates"));
notification.set_icon (new ThemedIcon ("system-reboot"));
notification.set_default_action (Application.ACTION_PREFIX + Application.SHOW_UPDATES_ACTION);

GLib.Application.get_default ().send_notification (NOTIFICATION_ID, notification);

update_state (RESTART_REQUIRED);
} catch (Error e) {
critical ("Failed to download available updates: %s", e.message);
send_error (e.message);
}
}

public void cancel () throws DBusError, IOError {
cancellable.cancel ();
}

private void progress_callback (Pk.Progress progress, Pk.ProgressType progress_type) {
update_state (current_state.state, status_to_title (progress.status));
}

private void send_error (string message) {
var notification = new Notification (_("System updates couldn't be installed"));
notification.set_body (_("An error occured 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 (State state, string message = "") {
current_state = {
state,
message
};

state_changed ();
}

private unowned string status_to_title (Pk.Status status) {
// From https://github.com/elementary/appcenter/blob/master/src/Core/ChangeInformation.vala#L51
switch (status) {
case Pk.Status.SETUP:
return _("Starting");
case Pk.Status.WAIT:
return _("Waiting");
case Pk.Status.RUNNING:
return _("Running");
case Pk.Status.QUERY:
return _("Querying");
case Pk.Status.INFO:
return _("Getting information");
case Pk.Status.REMOVE:
return _("Removing packages");
case Pk.Status.DOWNLOAD:
return _("Downloading");
case Pk.Status.REFRESH_CACHE:
return _("Refreshing software list");
case Pk.Status.UPDATE:
return _("Installing updates");
case Pk.Status.CLEANUP:
return _("Cleaning up packages");
case Pk.Status.OBSOLETE:
return _("Obsoleting packages");
case Pk.Status.DEP_RESOLVE:
return _("Resolving dependencies");
case Pk.Status.SIG_CHECK:
return _("Checking signatures");
case Pk.Status.TEST_COMMIT:
return _("Testing changes");
case Pk.Status.COMMIT:
return _("Committing changes");
case Pk.Status.REQUEST:
return _("Requesting data");
case Pk.Status.FINISHED:
return _("Finished");
case Pk.Status.CANCEL:
return _("Cancelling");
case Pk.Status.DOWNLOAD_REPOSITORY:
return _("Downloading repository information");
case Pk.Status.DOWNLOAD_PACKAGELIST:
return _("Downloading list of packages");
case Pk.Status.DOWNLOAD_FILELIST:
return _("Downloading file lists");
case Pk.Status.DOWNLOAD_CHANGELOG:
return _("Downloading lists of changes");
case Pk.Status.DOWNLOAD_GROUP:
return _("Downloading groups");
case Pk.Status.DOWNLOAD_UPDATEINFO:
return _("Downloading update information");
case Pk.Status.REPACKAGING:
return _("Repackaging files");
case Pk.Status.LOADING_CACHE:
return _("Loading cache");
case Pk.Status.SCAN_APPLICATIONS:
return _("Scanning applications");
case Pk.Status.GENERATE_PACKAGE_LIST:
return _("Generating package lists");
case Pk.Status.WAITING_FOR_LOCK:
return _("Waiting for package manager lock");
case Pk.Status.WAITING_FOR_AUTH:
return _("Waiting for authentication");
case Pk.Status.SCAN_PROCESS_LIST:
return _("Updating running applications");
case Pk.Status.CHECK_EXECUTABLE_FILES:
return _("Checking applications in use");
case Pk.Status.CHECK_LIBRARIES:
return _("Checking libraries in use");
case Pk.Status.COPY_FILES:
return _("Copying files");
case Pk.Status.INSTALL:
default:
return _("Installing");
}
}

public async CurrentState get_current_state () throws DBusError, IOError {
return current_state;
}

public async UpdateDetails get_update_details () throws DBusError, IOError {
return update_details;
}
}
2 changes: 2 additions & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ sources = files(
'Backends/MouseSettings.vala',
'Backends/NightLightSettings.vala',
'Backends/PrefersColorSchemeSettings.vala',
'Backends/SystemUpdate.vala',
'Utils/SunriseSunsetCalculator.vala',
)

Expand All @@ -21,6 +22,7 @@ executable(
granite_dep,
libgeoclue_dep,
m_dep,
pk_dep
],
install: true,
)