diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 985b0dbc..612a872c 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -15,10 +15,8 @@ jobs:
- name: Install Dependencies
run: |
apt update
- apt install -y libaccountsservice-dev libdbus-1-dev libgranite-dev libgeoclue-2-dev meson valac
+ apt install -y libaccountsservice-dev libdbus-1-dev libgranite-dev libgeoclue-2-dev libpackagekit-glib2-dev libpolkit-gobject-1-dev meson valac
- name: Build
- env:
- DESTDIR: out
run: |
meson build
ninja -C build
diff --git a/data/io.elementary.settings-daemon.policy.in b/data/io.elementary.settings-daemon.policy.in
new file mode 100644
index 00000000..5f78c8a8
--- /dev/null
+++ b/data/io.elementary.settings-daemon.policy.in
@@ -0,0 +1,20 @@
+
+
+
+ elementary
+ https://elementary.io/
+
+
+ Authentication is required to upgrade elementary OS
+ system-os-installer
+
+ no
+ no
+ auth_admin_keep
+
+ @PKGDATADIR@/io.elementary.settings-daemon.system-upgrade.helper
+
+
+
diff --git a/data/io.elementary.settings-daemon.system-upgrade.helper b/data/io.elementary.settings-daemon.system-upgrade.helper
new file mode 100644
index 00000000..4116b066
--- /dev/null
+++ b/data/io.elementary.settings-daemon.system-upgrade.helper
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+import argparse
+import os
+import re
+import shutil
+
+
+def update_third_party_repository(current, next, repository_file):
+ r = re.compile("[deb|deb-src].* ({}).*$".format(current))
+
+ if os.path.isfile(repository_file):
+ should_delete = False
+ should_update = False
+ with open(repository_file, "r") as f:
+ content = []
+ for line in f.read().split("\n"):
+ if r.match(line):
+ should_update = True
+ line = line.replace(current, next)
+ content.append(line)
+
+ if "ppa.launchpad.net/elementary-os" in line:
+ should_delete = True
+ elif "packages.elementary.io" in line:
+ should_delete = True
+
+ if should_delete:
+ os.remove(repository_file)
+
+ if should_update:
+ with open(repository_file, "w") as f:
+ f.write("\n".join(content))
+
+
+def install_files(next):
+ update_files = os.path.join(
+ "/", "usr", "share", "io.elementary.settings-daemon", "system-upgrades", next)
+ shutil.copytree(update_files, "/", dirs_exist_ok=True)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Helper to upgrade elementary OS")
+
+ parser.add_argument("--update-third-party-repository", action="store_true")
+ parser.add_argument("--install-files", action="store_true")
+
+ parser.add_argument("--current", nargs="?", default=None)
+ parser.add_argument("--next", nargs="?", default=None)
+ parser.add_argument("--repository-file", nargs="?", default=None)
+
+ args = parser.parse_args()
+
+ if args.update_third_party_repository and args.current and args.next and args.repository_file:
+ return update_third_party_repository(args.current, args.next, args.repository_file)
+ elif args.install_files and args.next:
+ return install_files(args.next)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/data/meson.build b/data/meson.build
index 4db0d403..3a1cc626 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -34,3 +34,32 @@ i18n.merge_file(
install: true,
install_dir: join_paths(get_option('datadir'), 'metainfo'),
)
+
+conf = configuration_data()
+conf.set('PKGDATADIR', pkgdatadir)
+conf.set('GETTEXT_PACKAGE', meson.project_name())
+
+gettext_declaration = configure_file(
+ configuration: conf,
+ input: meson.project_name() + '.policy.in',
+ output: meson.project_name() + '.policy.in'
+)
+
+i18n.merge_file(
+ input: gettext_declaration,
+ output: meson.project_name() + '.policy',
+ po_dir: join_paths(meson.source_root(), 'po'),
+ install: true,
+ install_dir: polkit_actiondir
+)
+
+install_subdir(
+ 'system-upgrades',
+ install_dir: pkgdatadir
+)
+
+install_data(
+ meson.project_name() + '.system-upgrade.helper',
+ install_mode: 'r-xr--r--',
+ install_dir: pkgdatadir
+)
diff --git a/data/system-upgrades/jammy/etc/apt/sources.list.d/elementary.list b/data/system-upgrades/jammy/etc/apt/sources.list.d/elementary.list
new file mode 100644
index 00000000..555895ab
--- /dev/null
+++ b/data/system-upgrades/jammy/etc/apt/sources.list.d/elementary.list
@@ -0,0 +1,2 @@
+deb http://ppa.launchpad.net/elementary-os/stable/ubuntu jammy main
+deb-src http://ppa.launchpad.net/elementary-os/stable/ubuntu jammy main
diff --git a/data/system-upgrades/jammy/etc/apt/sources.list.d/patches.list b/data/system-upgrades/jammy/etc/apt/sources.list.d/patches.list
new file mode 100644
index 00000000..c52be081
--- /dev/null
+++ b/data/system-upgrades/jammy/etc/apt/sources.list.d/patches.list
@@ -0,0 +1,2 @@
+deb http://ppa.launchpad.net/elementary-os/os-patches/ubuntu jammy main
+deb-src http://ppa.launchpad.net/elementary-os/os-patches/ubuntu jammy main
diff --git a/meson.build b/meson.build
index fbf051ad..274a631c 100644
--- a/meson.build
+++ b/meson.build
@@ -4,6 +4,15 @@ project('io.elementary.settings-daemon',
license: 'GPL3',
)
+add_project_arguments(
+ '-DI_KNOW_THE_PACKAGEKIT_GLIB2_API_IS_SUBJECT_TO_CHANGE',
+ language:'c'
+)
+
+prefix = get_option('prefix')
+datadir = join_paths(prefix, get_option('datadir'))
+pkgdatadir = join_paths(datadir, meson.project_name())
+
gio_dep = dependency ('gio-2.0')
glib_dep = dependency('glib-2.0')
granite_dep = dependency('granite', version: '>= 5.3.0')
@@ -12,6 +21,11 @@ i18n = import('i18n')
cc = meson.get_compiler('c')
m_dep = cc.find_library('m', required : false)
libgeoclue_dep = dependency ('libgeoclue-2.0')
+packagekit_dep = dependency('packagekit-glib2')
+polkit_dep = dependency('polkit-gobject-1')
+posix_dep = meson.get_compiler('vala').find_library('posix')
+
+polkit_actiondir = polkit_dep.get_pkgconfig_variable('actiondir', define_variable: ['prefix', prefix])
conf_data = configuration_data()
conf_data.set('PROJECT_NAME', meson.project_name())
diff --git a/po/POTFILES b/po/POTFILES
index 31a08460..1b2b0b33 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -1 +1,2 @@
+data/io.elementary.settings-daemon.policy.in
data/settings-daemon.appdata.xml.in
diff --git a/src/Application.vala b/src/Application.vala
index 39c8a5e1..eee06dcf 100644
--- a/src/Application.vala
+++ b/src/Application.vala
@@ -128,6 +128,24 @@ public class SettingsDaemon.Application : GLib.Application {
}
}
+ public override bool dbus_register (DBusConnection connection, string object_path) throws Error {
+ // We must chain up to the parent class:
+ base.dbus_register (connection, object_path);
+
+ // Now we can do our own stuff here. For example, we could export some D-Bus objects
+ connection.register_object (object_path, new Backends.SystemUpgrade ());
+
+ return true;
+ }
+
+ public override void dbus_unregister (DBusConnection connection, string object_path) {
+ // Do our own stuff here, e.g. unexport any D-Bus objects we exported in the dbus_register
+ // hook above. Be sure to check that we actually did export them, since the hook
+ // above might have returned early due to the parent class' hook returning false!
+
+ base.dbus_unregister (connection, object_path);
+ }
+
public static int main (string[] args) {
var application = new Application ();
return application.run (args);
diff --git a/src/Backends/SystemUpgrade.vala b/src/Backends/SystemUpgrade.vala
new file mode 100644
index 00000000..6f6235ce
--- /dev/null
+++ b/src/Backends/SystemUpgrade.vala
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2022 elementary, Inc. (https://elementary.io)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA
+ * Authored by: Marius Meisenzahl
+ */
+
+[DBus (name = "io.elementary.SystemUpgrade")]
+public class SettingsDaemon.Backends.SystemUpgrade : GLib.Object {
+ public bool system_upgrade_available {
+ get {
+ return true;
+ }
+ }
+
+ public signal void system_upgrade_progress (int percentage);
+
+ public signal void system_upgrade_finished ();
+
+ public signal void system_upgrade_failed (string text);
+
+ public signal void system_upgrade_cancelled ();
+
+ public void start_upgrade () throws Error {
+ upgrade_async.begin ();
+ }
+
+ public void cancel () throws Error {
+ cancellable.cancel ();
+
+ if (is_system_upgrade_running) {
+ is_system_upgrade_running = false;
+ }
+ }
+
+ private async void upgrade_async () {
+ if (is_system_upgrade_running) {
+ return;
+ }
+
+ if (cancellable.is_cancelled ()) {
+ cancellable.reset ();
+ }
+
+ is_system_upgrade_running = true;
+
+ Inhibitor.get_instance ().inhibit ();
+
+ Pk.Results? results = null;
+
+ try {
+ debug ("Refresh cache");
+ results = yield task.refresh_cache_async (true, cancellable, (t, p) => { });
+
+ if (cancellable.is_cancelled ()) {
+ Inhibitor.get_instance ().uninhibit ();
+ return;
+ }
+
+ debug ("Get repositories");
+ results = yield task.get_repo_list_async (Pk.Bitfield.from_enums (Pk.Filter.NONE), cancellable, (p, t) => { });
+
+ if (cancellable.is_cancelled ()) {
+ Inhibitor.get_instance ().uninhibit ();
+ return;
+ }
+
+ var repo_files = new Array ();
+ var repos = results.get_repo_detail_array ();
+ for (int i = 0; i < repos.length; i++) {
+ var repo = repos[i];
+
+ // TODO: check for ppas
+
+ var parts = repo.repo_id.split (":", 2);
+ var f = parts[0];
+
+ if (!FileUtils.test (f, FileTest.EXISTS)) {
+ continue;
+ }
+
+ bool found = false;
+ for (int j = 0; j < repo_files.length; j++) {
+ if (repo_files.index (j) == f) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ repo_files.append_val (f);
+ }
+ }
+
+ var helper = new Utils.SystemUpgradeHelper ();
+
+ debug ("Update repositories");
+ for (int i = 0; i < repo_files.length; i++) {
+ var repository_file = repo_files.index (i);
+ debug (" %s", repository_file);
+
+ if (!helper.update_third_party_repository ("focal", "jammy", repository_file)) {
+ throw new Error (0, 0, "Could not update repository: %s\n", repository_file);
+ }
+ }
+
+ if (!helper.install_files ("jammy")) {
+ throw new Error (0, 0, "Could not install files\n");
+ }
+
+ debug ("Refresh cache");
+ results = yield task.refresh_cache_async (true, cancellable, (p, t) => { });
+
+ if (cancellable.is_cancelled ()) {
+ Inhibitor.get_instance ().uninhibit ();
+ return;
+ }
+
+ if (results == null) {
+ throw new Error (0, 0, "Could not refresh cache");
+ }
+
+ debug ("Get updates");
+ results = yield task.get_updates_async (Pk.Bitfield.from_enums (Pk.Filter.NEWEST), cancellable, (t, p) => { });
+
+ if (cancellable.is_cancelled ()) {
+ Inhibitor.get_instance ().uninhibit ();
+ return;
+ }
+
+ if (results == null) {
+ throw new Error (0, 0, "Could not get updates");
+ }
+
+ var sack = results.get_package_sack ();
+ sack.remove_by_filter (update_system_filter_helper);
+ var package_ids = sack.get_ids ();
+
+ task.only_download = true;
+
+ debug ("Download packages");
+ var status = Pk.Status.UNKNOWN;
+ int percentage = -1;
+ results = yield task.update_packages_async (package_ids, cancellable, ((p, t) => {
+ if (t == Pk.ProgressType.STATUS) {
+ status = p.get_status ();
+ }
+
+ int new_percentage = percentage;
+ if (t == Pk.ProgressType.PERCENTAGE && status == Pk.Status.DOWNLOAD) {
+ new_percentage = p.percentage;
+ }
+
+ if (status == Pk.Status.FINISHED) {
+ new_percentage = 100;
+ }
+
+ if (new_percentage != percentage) {
+ percentage = new_percentage;
+ system_upgrade_progress (percentage);
+ }
+ }));
+
+ if (cancellable.is_cancelled ()) {
+ Inhibitor.get_instance ().uninhibit ();
+ return;
+ }
+
+ if (results == null) {
+ throw new Error (0, 0, "Could not download packages");
+ }
+
+ debug ("Set PackageKit reboot action");
+ Pk.offline_trigger (Pk.OfflineAction.REBOOT, cancellable);
+
+ if (cancellable.is_cancelled ()) {
+ Inhibitor.get_instance ().uninhibit ();
+ return;
+ }
+
+ debug ("Ready to reboot");
+
+ system_upgrade_finished ();
+ } catch (Error e) {
+ warning ("Upgrade failed: %s", e.message);
+
+ system_upgrade_failed (e.message);
+ }
+
+ is_system_upgrade_running = false;
+
+ Inhibitor.get_instance ().uninhibit ();
+ }
+
+ construct {
+ task = new Pk.Task ();
+ cancellable = new Cancellable ();
+
+ cancellable.cancelled.connect (() => { system_upgrade_cancelled (); });
+ }
+
+ private static Pk.Task task;
+ private static Cancellable cancellable;
+
+ private bool is_system_upgrade_running = false;
+
+ private bool update_system_filter_helper (Pk.Package package) {
+ var info = package.get_info ();
+ return (info != Pk.Info.OBSOLETING && info != Pk.Info.REMOVING);
+ }
+}
diff --git a/src/Utils/Inhibitor.vala b/src/Utils/Inhibitor.vala
new file mode 100644
index 00000000..8221f0ff
--- /dev/null
+++ b/src/Utils/Inhibitor.vala
@@ -0,0 +1,102 @@
+/*-
+ * Copyright (c) 2016 elementary LLC. (https://elementary.io)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this program. If not, see .
+ */
+
+[DBus (name = "org.freedesktop.ScreenSaver")]
+public interface ScreenSaverIface : Object {
+ public abstract uint32 inhibit (string app_name, string reason) throws Error;
+ public abstract void un_inhibit (uint32 cookie) throws Error;
+ public abstract void simulate_user_activity () throws Error;
+}
+
+public class Inhibitor : Object {
+ private const string IFACE = "org.freedesktop.ScreenSaver";
+ private const string IFACE_PATH = "/ScreenSaver";
+
+ private static Inhibitor? instance = null;
+
+ private uint32? inhibit_cookie = null;
+
+ private ScreenSaverIface? screensaver_iface = null;
+
+ private bool inhibited = false;
+ private bool simulator_started = false;
+
+ private Inhibitor () {
+ try {
+ screensaver_iface = Bus.get_proxy_sync (BusType.SESSION, IFACE, IFACE_PATH, DBusProxyFlags.NONE);
+ } catch (Error e) {
+ warning ("Could not start screensaver interface: %s", e.message);
+ }
+ }
+
+ public static Inhibitor get_instance () {
+ if (instance == null) {
+ instance = new Inhibitor ();
+ }
+
+ return instance;
+ }
+
+ public void inhibit () {
+ if (screensaver_iface != null && !inhibited) {
+ try {
+ inhibited = true;
+ inhibit_cookie = screensaver_iface.inhibit ("Installer", "Installing");
+ simulate_activity ();
+ debug ("Inhibiting screen");
+ } catch (Error e) {
+ warning ("Could not inhibit screen: %s", e.message);
+ }
+ }
+ }
+
+ public void uninhibit () {
+ if (screensaver_iface != null && inhibited) {//&& inhibit_cookie != null) {
+ try {
+ inhibited = false;
+ screensaver_iface.un_inhibit (inhibit_cookie);
+ debug ("Uninhibiting screen");
+ } catch (Error e) {
+ warning ("Could not uninhibit screen: %s", e.message);
+ }
+ }
+ }
+
+ /*
+ * Inhibit currently does not block a suspend from ocurring,
+ * so we simulate user activity every 2 mins to prevent it
+ */
+ private void simulate_activity () {
+ if (simulator_started) return;
+
+ simulator_started = true;
+ Timeout.add_full (Priority.DEFAULT, 120000, ()=> {
+ if (inhibited) {
+ try {
+ debug ("Simulating activity");
+ screensaver_iface.simulate_user_activity ();
+ } catch (Error e) {
+ warning ("Could not simulate user activity: %s", e.message);
+ }
+ } else {
+ simulator_started = false;
+ }
+
+ return inhibited;
+ });
+ }
+}
diff --git a/src/Utils/SystemUpgradeHelper.vala b/src/Utils/SystemUpgradeHelper.vala
new file mode 100644
index 00000000..314d8ea0
--- /dev/null
+++ b/src/Utils/SystemUpgradeHelper.vala
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2022 elementary, Inc. (https://elementary.io)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA
+ * Authored by: Marius Meisenzahl
+ */
+
+public class SettingsDaemon.Utils.SystemUpgradeHelper : GLib.Object {
+ public bool update_third_party_repository (string current, string next, string repository_file) {
+ if (!authenticate ()) {
+ var message = "Unable to authenticate";
+ warning (message);
+ on_standard_error (message);
+ return false;
+ }
+
+ var cmd = "update-third-party-repository";
+
+ var command = "pkexec %s/io.elementary.settings-daemon.system-upgrade.helper --%s --current %s --next %s --repository-file %s".printf (
+ "/usr/share/io.elementary.settings-daemon", cmd, current, next, repository_file);
+
+ if (!run (command)) {
+ on_error ();
+ return false;
+ }
+
+ return true;
+ }
+
+ public bool install_files (string next) {
+ if (!authenticate ()) {
+ var message = "Unable to authenticate";
+ warning (message);
+ on_standard_error (message);
+ return false;
+ }
+
+ var cmd = "install-files";
+
+ var command = "pkexec %s/io.elementary.settings-daemon.system-upgrade.helper --%s --next %s".printf (
+ "/usr/share/io.elementary.settings-daemon", cmd, next);
+
+ if (!run (command)) {
+ on_error ();
+ return false;
+ }
+
+ return true;
+ }
+
+ public signal void on_standard_output (string line);
+ public signal void on_standard_error (string line);
+ public signal void on_error ();
+
+ private static Polkit.Permission? permission = null;
+
+ private bool authenticate () {
+ if (permission == null) {
+ try {
+ permission = new Polkit.Permission.sync (
+ "io.elementary.settings-daemon.system-upgrade",
+ new Polkit.UnixProcess (Posix.getpid ())
+ );
+ } catch (Error e) {
+ warning ("Can't get permission to upgrade without prompting for admin: %s", e.message);
+ return false;
+ }
+ }
+
+ try {
+ if (!permission.allowed) {
+ permission.acquire (null);
+ }
+ } catch (Error e) {
+ warning ("Can't get permission to upgrade without prompting for admin: %s", e.message);
+ return false;
+ }
+
+ return permission.allowed;
+ }
+
+ private bool process_line (IOChannel channel, IOCondition condition, string stream_name) {
+ if (condition == IOCondition.HUP) {
+ // debug ("%s: The fd has been closed.", stream_name);
+ return false;
+ }
+
+ try {
+ string line;
+ channel.read_line (out line, null, null);
+
+ switch (stream_name) {
+ case "stdout":
+ // debug ("%s", line);
+ on_standard_output (line);
+ break;
+ case "stderr":
+ // warning ("\033[0;31m%s\033[0m", line);
+ on_standard_error (line);
+ break;
+ }
+ } catch (IOChannelError e) {
+ // warning ("%s: IOChannelError: %s", stream_name, e.message);
+ return false;
+ } catch (ConvertError e) {
+ // warning ("%s: ConvertError: %s", stream_name, e.message);
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool run (string cmd) {
+ MainLoop loop = new MainLoop ();
+ try {
+ string[] spawn_args = cmd.split (" ");
+ string[] spawn_env = Environ.get ();
+ Pid child_pid;
+
+ int standard_input;
+ int standard_output;
+ int standard_error;
+
+ bool exit_status = false;
+
+ Process.spawn_async_with_pipes ("/",
+ spawn_args,
+ spawn_env,
+ SpawnFlags.SEARCH_PATH | SpawnFlags.DO_NOT_REAP_CHILD,
+ null,
+ out child_pid,
+ out standard_input,
+ out standard_output,
+ out standard_error);
+
+ // stdout:
+ IOChannel output = new IOChannel.unix_new (standard_output);
+ output.add_watch (IOCondition.IN | IOCondition.HUP, (channel, condition) => {
+ return process_line (channel, condition, "stdout");
+ });
+
+ // stderr:
+ IOChannel error = new IOChannel.unix_new (standard_error);
+ error.add_watch (IOCondition.IN | IOCondition.HUP, (channel, condition) => {
+ return process_line (channel, condition, "stderr");
+ });
+
+ ChildWatch.add (child_pid, (pid, status) => {
+ // Triggered when the child indicated by child_pid exits
+ Process.close_pid (pid);
+ exit_status = (status == 0);
+ loop.quit ();
+ });
+
+ loop.run ();
+
+ return exit_status;
+ } catch (Error e) {
+ // warning ("Error: %s", e.message);
+ return false;
+ }
+ }
+}
diff --git a/src/meson.build b/src/meson.build
index 7c726d97..baf43b2b 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -6,7 +6,10 @@ sources = files(
'Backends/KeyboardSettings.vala',
'Backends/MouseSettings.vala',
'Backends/PrefersColorSchemeSettings.vala',
+ 'Backends/SystemUpgrade.vala',
+ 'Utils/Inhibitor.vala',
'Utils/SunriseSunsetCalculator.vala',
+ 'Utils/SystemUpgradeHelper.vala',
)
executable(
@@ -19,6 +22,9 @@ executable(
granite_dep,
libgeoclue_dep,
m_dep,
+ packagekit_dep,
+ polkit_dep,
+ posix_dep,
],
install: true,
)