From 9fcaf5cd63d541593522011e87429548577f5e98 Mon Sep 17 00:00:00 2001 From: Leonhard <106322251+leolost2605@users.noreply.github.com> Date: Thu, 30 May 2024 17:51:06 +0200 Subject: [PATCH] Drivers v2 (with support for device grouping) (#141) --- meson_options.txt | 1 + src/Application.vala | 4 + src/Backends/SystemUpdate.vala | 99 +---------- src/Backends/UbuntuDrivers.vala | 292 ++++++++++++++++++++++++++++++++ src/Utils/PkUtils.vala | 92 ++++++++++ src/meson.build | 9 + 6 files changed, 402 insertions(+), 95 deletions(-) create mode 100644 src/Backends/UbuntuDrivers.vala create mode 100644 src/Utils/PkUtils.vala diff --git a/meson_options.txt b/meson_options.txt index f7cf46d2..2689e2e3 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,4 @@ option('busctlpath', type: 'string', value: '', description: 'custom path to busctl executable') +option('ubuntu_drivers', type: 'boolean', value: 'true', description: 'whether to enable ubuntu drivers integration') option('systemdsystemunitdir', type: 'string', value: '', description: 'custom directory for systemd system units, or \'no\' to disable') option('systemduserunitdir', type: 'string', value: '', description: 'custom directory for systemd user units, or \'no\' to disable') diff --git a/src/Application.vala b/src/Application.vala index 6e05f8a5..31322ee4 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -80,6 +80,10 @@ public sealed class SettingsDaemon.Application : Gtk.Application { connection.register_object (object_path, new Backends.SystemUpdate ()); +#if UBUNTU_DRIVERS + connection.register_object (object_path, new Backends.UbuntuDrivers ()); +#endif + return true; } diff --git a/src/Backends/SystemUpdate.vala b/src/Backends/SystemUpdate.vala index 8cbc4e8e..674dc14b 100644 --- a/src/Backends/SystemUpdate.vala +++ b/src/Backends/SystemUpdate.vala @@ -7,20 +7,6 @@ [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; @@ -33,7 +19,7 @@ public class SettingsDaemon.Backends.SystemUpdate : Object { private static Settings settings = new GLib.Settings ("io.elementary.settings-daemon.system-update"); - private CurrentState current_state; + private PkUtils.CurrentState current_state; private UpdateDetails update_details; private Pk.Task task; @@ -187,7 +173,7 @@ public class SettingsDaemon.Backends.SystemUpdate : Object { } private void progress_callback (Pk.Progress progress, Pk.ProgressType progress_type) { - update_state (current_state.state, status_to_title (progress.status)); + update_state (current_state.state, PkUtils.status_to_title (progress.status)); } private void send_error (string message) { @@ -201,7 +187,7 @@ public class SettingsDaemon.Backends.SystemUpdate : Object { update_state (ERROR, message); } - private void update_state (State state, string message = "") { + private void update_state (PkUtils.State state, string message = "") { current_state = { state, message @@ -210,84 +196,7 @@ public class SettingsDaemon.Backends.SystemUpdate : Object { 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 { + public async PkUtils.CurrentState get_current_state () throws DBusError, IOError { return current_state; } diff --git a/src/Backends/UbuntuDrivers.vala b/src/Backends/UbuntuDrivers.vala new file mode 100644 index 00000000..a94f70db --- /dev/null +++ b/src/Backends/UbuntuDrivers.vala @@ -0,0 +1,292 @@ +/* + * 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.Drivers")] +public class SettingsDaemon.Backends.UbuntuDrivers : Object { + private const string NOTIFICATION_ID = "drivers"; + + private class Device : Object { + public string name; + public HashTable available_drivers_with_installed = new HashTable (str_hash, str_equal); + } + + public signal void state_changed (); + + private PkUtils.CurrentState current_state; + private HashTable> available_drivers; + private HashTable devices_by_drivers; + private Device[] devices = {}; + + private Pk.Task task; + private GLib.Cancellable cancellable; + + construct { + current_state = { + UP_TO_DATE, + "" + }; + + available_drivers = new HashTable> (str_hash, str_equal); + devices_by_drivers = new HashTable (str_hash, str_equal); + + task = new Pk.Task (); + + cancellable = new GLib.Cancellable (); + + check_for_drivers.begin (true); + } + + private async string? get_drivers_output (string verb) { + string? drivers_exec_path = Environment.find_program_in_path ("ubuntu-drivers"); + if (drivers_exec_path == null) { + return null; + } + + try { + var command = new Subprocess (SubprocessFlags.STDOUT_PIPE, drivers_exec_path, verb); + + string output; + yield command.communicate_utf8_async (null, cancellable, out output, null); + + return output; + } catch (Error e) { + critical ("Failed to launch ubuntu-drivers: %s", e.message); + return null; + } + } + + private async void check_devices () { + var output = yield get_drivers_output ("devices"); + if (output == null) { + critical ("Failed to get ubuntu-drivers output"); + return; + } + + string[] tokens = output.split ("\n"); + + Device? current_device = null; + foreach (var token in tokens) { + if ("==" in token) { + current_device = new Device (); + devices += current_device; + continue; + } + + if (current_device == null) { + continue; + } + + if (token.has_prefix ("model")) { + var normalized_token = token.splice (0, 11); + current_device.name = normalized_token; + continue; + } + + if (token.has_prefix ("driver")) { + var normalized_token = token.splice (0, 11); + var split_token = normalized_token.split (" - "); + var driver = split_token[0]; + devices_by_drivers[driver] = current_device; + continue; + } + } + } + + public async void check_for_drivers (bool notify) throws DBusError, IOError { + update_state (CHECKING); + + available_drivers.remove_all (); + + yield check_devices (); + + var command_output = yield get_drivers_output ("list"); + if (command_output == null) { + update_state (UP_TO_DATE); + critical ("Failed to get ubuntu-drivers output"); + return; + } + + string[] tokens = command_output.split ("\n"); + foreach (unowned string line in tokens) { + if (line.strip () == "") { + continue; + } + + // Filter out the nvidia server drivers + if (line.contains ("nvidia") && line.contains ("-server")) { + continue; + } + + // ubuntu-drivers returns lines like the following for dkms packages: + // backport-iwlwifi-dkms, (kernel modules provided by backport-iwlwifi-dkms) + // nvidia-driver-470, (kernel modules provided by linux-modules-nvidia-470-generic-hwe-20.04) + // we want to install both packages if they're different + + string[] parts = line.split (","); + + var driver = parts[0]; + + if (driver == null || !(driver in devices_by_drivers)) { + continue; + } + + // Get the driver part (before the comma) + string[] package_names = {}; + + package_names += driver; + + if (parts.length > 1) { + if (parts[1].contains ("kernel modules provided by")) { + string[] kernel_module_parts = parts[1].split (" "); + // Get the remainder of the string after the last space + var last_part = kernel_module_parts[kernel_module_parts.length - 1]; + // Strip off the trailing bracket + last_part = last_part.replace (")", ""); + + if (!(last_part in package_names)) { + package_names += last_part; + } + } else { + warning ("Unrecognised line from ubuntu-drivers, needs checking: %s", line); + } + } + + available_drivers[driver] = yield update_installed (driver, package_names); + } + + if (available_drivers.length == 0) { + update_state (UP_TO_DATE); + return; + } + + update_state (AVAILABLE); + + if (notify) { + var notification = new Notification (_("Drivers available")); + notification.set_default_action (Application.ACTION_PREFIX + Application.SHOW_UPDATES_ACTION); + notification.set_body (_("For your system are drivers available")); + notification.set_icon (new ThemedIcon ("software-update-available")); + + GLib.Application.get_default ().send_notification (NOTIFICATION_ID, notification); + } + } + + private async GenericArray update_installed (string driver, string[] package_names) { + var array = new GenericArray (); + try { + var result = yield task.resolve_async (Pk.Filter.NONE, package_names, null, () => {}); + + var packages = result.get_package_array (); + + bool all_installed = true; + foreach (var package in packages) { + array.add (package.package_id); + + if (!(driver in devices_by_drivers)) { + continue; + } + + if (all_installed && (Pk.Info.INSTALLED == package.info)) { + devices_by_drivers[driver].available_drivers_with_installed[driver] = true; + } else { + all_installed = false; + devices_by_drivers[driver].available_drivers_with_installed[driver] = false; + } + } + } catch (Error e) { + critical ("Failed to get package details, treating as not installed: %s", e.message); + } + + return array; + } + + public async void install (string pkg_name) throws DBusError, IOError { + if (!(pkg_name in available_drivers)) { + critical ("Driver not found"); + return; + } + + if (current_state.state != AVAILABLE) { + warning ("No drivers available, or already downloading a driver."); + return; + } + + cancellable.reset (); + + update_state (DOWNLOADING); + + try { + string[] pkgs = available_drivers[pkg_name].data; // It seems arrays are imediately freed with async methods so this prevents a seg fault + var results = yield task.install_packages_async (pkgs, cancellable, progress_callback); + + if (results.get_exit_code () == CANCELLED) { + debug ("Installation was cancelled"); + update_state (AVAILABLE); + return; + } + + foreach (var driver in available_drivers.get_keys ()) { + string[] driver_pkgs = available_drivers[driver].data; + yield update_installed (driver, driver_pkgs); + } + + var notification = new Notification (_("Restart required")); + notification.set_body (_("Please restart your system to finalize driver installation")); + 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 (AVAILABLE); + } catch (Error e) { + critical ("Failed to install driver: %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, PkUtils.status_to_title (progress.status)); + } + + private void send_error (string message) { + var notification = new Notification (_("A driver couldn't be installed")); + notification.set_body (_("An error occurred while trying to install a driver")); + 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 = "") { + current_state = { + state, + message + }; + + state_changed (); + } + + public async PkUtils.CurrentState get_current_state () throws DBusError, IOError { + return current_state; + } + + public async HashTable> get_available_drivers () throws DBusError, IOError { + var map = new HashTable> (str_hash, str_equal); + + foreach (var device in devices) { + map[device.name] = device.available_drivers_with_installed; + } + + return map; + } +} diff --git a/src/Utils/PkUtils.vala b/src/Utils/PkUtils.vala new file mode 100644 index 00000000..c6e23e71 --- /dev/null +++ b/src/Utils/PkUtils.vala @@ -0,0 +1,92 @@ +namespace SettingsDaemon.PkUtils { + public enum State { + UP_TO_DATE, + CHECKING, + AVAILABLE, + DOWNLOADING, + RESTART_REQUIRED, + ERROR + } + + public struct CurrentState { + State state; + string status; + } + + public static 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"); + } + } +} diff --git a/src/meson.build b/src/meson.build index 645db1c1..eda9764c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -10,9 +10,17 @@ sources = files( 'Backends/PowerProfilesSync.vala', 'Backends/PrefersColorSchemeSettings.vala', 'Backends/SystemUpdate.vala', + 'Utils/PkUtils.vala', 'Utils/SunriseSunsetCalculator.vala', ) +args = [] + +if get_option('ubuntu_drivers') + sources += files('Backends/UbuntuDrivers.vala') + args += '--define=UBUNTU_DRIVERS' +endif + executable( meson.project_name(), sources, @@ -28,5 +36,6 @@ executable( m_dep, pk_dep ], + vala_args: args, install: true, )