diff --git a/tests/test_kernel_window.py b/tests/test_kernel_window.py index 50bee0615..6d7f32364 100644 --- a/tests/test_kernel_window.py +++ b/tests/test_kernel_window.py @@ -7,7 +7,7 @@ from datetime import datetime from Classes import * from mintUpdate import * -from checkAPT import * +from aptUpdater import * # Test KernelVersion object def test_kernel_version_series_comparison(): diff --git a/usr/lib/linuxmint/mintUpdate/Classes.py b/usr/lib/linuxmint/mintUpdate/Classes.py index 53300c74e..e760f76c1 100644 --- a/usr/lib/linuxmint/mintUpdate/Classes.py +++ b/usr/lib/linuxmint/mintUpdate/Classes.py @@ -5,13 +5,11 @@ import datetime import gettext -import html import json import os import subprocess import sys import time -import re import threading gettext.install("mintupdate", "/usr/share/locale") @@ -71,6 +69,40 @@ def get_release_dates(): pass return release_dates +class AutorefreshTimer(GLib.Source): + # A GSource that can be armed, disarmed, and re-armed without being + # recreated. Attach once at startup; thereafter call arm(seconds) to + # schedule the next dispatch and disarm() to cancel a pending one. + def __init__(self, callback): + super().__init__() + self._callback = callback + + def prepare(self): + ready_time = self.get_ready_time() + if ready_time == -1: + return (False, -1) + now = GLib.get_monotonic_time() + if ready_time <= now: + return (True, 0) + timeout_ms = (ready_time - now + 999) // 1000 + return (False, min(int(timeout_ms), 0x7FFFFFFF)) + + def check(self): + ready_time = self.get_ready_time() + return ready_time != -1 and ready_time <= GLib.get_monotonic_time() + + def dispatch(self, callback, user_data): + self.set_ready_time(-1) + self._callback() + return GLib.SOURCE_CONTINUE + + def arm(self, seconds): + self.set_ready_time(GLib.get_monotonic_time() + seconds * 1_000_000) + + def disarm(self): + self.set_ready_time(-1) + + class KernelVersion(): def __init__(self, version): @@ -210,7 +242,7 @@ class UpdateTracker(): # Loads past updates from JSON file def __init__(self, settings, logger): - os.system("mkdir -p %s" % CONFIG_PATH) + os.makedirs(CONFIG_PATH, exist_ok=True) self.path = os.path.join(CONFIG_PATH, "updates.json") self.test_mode = os.getenv("MINTUPDATE_TEST") == "tracker-max-age" @@ -218,7 +250,7 @@ def __init__(self, settings, logger): self.tracker_version = 1 # version of the data structure self.settings = settings self.tracked_updates = {} - self.refreshed_update_names = [] # updates which are seen in checkAPT + self.refreshed_update_names = [] # updates which are seen in aptUpdater self.today = datetime.date.today().strftime("%Y.%m.%d") self.max_days = 0 # oldest update (in number of days seen) self.oldest_since_date = self.today # oldest update (according to since date) diff --git a/usr/lib/linuxmint/mintUpdate/checkAPT.py b/usr/lib/linuxmint/mintUpdate/aptUpdater.py similarity index 67% rename from usr/lib/linuxmint/mintUpdate/checkAPT.py rename to usr/lib/linuxmint/mintUpdate/aptUpdater.py index 43d77db82..2765b4261 100755 --- a/usr/lib/linuxmint/mintUpdate/checkAPT.py +++ b/usr/lib/linuxmint/mintUpdate/aptUpdater.py @@ -3,18 +3,30 @@ import codecs import fnmatch import gettext +import json import os +import queue import re +import subprocess import sys +import threading import traceback import html import locale import apt -from gi.repository import Gio +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gio, Gtk, GLib -from Classes import (CONFIGURED_KERNEL_TYPE, KERNEL_PKG_NAMES, - PRIORITY_UPDATES, SUPPORTED_KERNEL_TYPES, Alias, KernelVersion, Update) +import aptkit.simpleclient +import aptkit.enums + +from multiprocess import Process, Queue + +from Classes import (CONFIG_PATH, CONFIGURED_KERNEL_TYPE, KERNEL_PKG_NAMES, + PRIORITY_UPDATES, SUPPORTED_KERNEL_TYPES, Alias, KernelVersion, Update, + _idle) gettext.install("mintupdate", "/usr/share/locale") @@ -23,13 +35,193 @@ # packages which description is incorrect in Ubuntu (usually those which were replaced by snap dependencies) NON_TRANSLATED_PKGS = ["firefox", "thunderbird"] -class APTCheck(): +class AptUpdater(): - def __init__(self): + def __init__(self, ui_window=None): self.settings = Gio.Settings(schema_id="com.linuxmint.updates") - self.cache = apt.Cache() + self.ui_window = ui_window + self.cache = None self.priority_updates_available = False + # During build (find_changes/add_update) self.updates is a dict keyed by + # source_name. fetch_updates() replaces it with the final list of Updates. + self.updates = {} + self.error = None + self.install_error = None + self.install_cancelled = False + + def load_cache(self): + self.cache = apt.Cache() + + def refresh(self, interactive=False): + # Refresh the on-disk APT cache. Blocks until done. Returns None on + # success, or a short string describing what went wrong (transaction + # non-success exit_state, daemon error, cancellation, sync exception, + # or non-zero exit from mint-refresh-cache) — caller logs it. + + if interactive: + self._aptkit_done = threading.Event() + self._refresh_error = None + self._refresh_via_aptkit() + if not self._aptkit_done.wait(timeout=600): + self._refresh_error = "Timed out waiting for aptkit cache refresh" + return self._refresh_error + else: + return self._refresh_via_mint_refresh_cache() + + @_idle + def _refresh_via_aptkit(self): + try: + aptkit_client = aptkit.simpleclient.SimpleAPTClient(self.ui_window) + + def on_finished(transaction, exit_state): + if exit_state != aptkit.enums.EXIT_SUCCESS: + self._refresh_error = f"aptkit transaction finished with exit_state={exit_state}" + self._aptkit_done.set() + + def on_error(error_code, error_details): + self._refresh_error = f"aptkit error code={error_code} details={error_details}" + self._aptkit_done.set() + + def on_cancelled(): + self._refresh_error = "aptkit transaction cancelled" + self._aptkit_done.set() + + aptkit_client.set_progress_callback(None) + aptkit_client.set_finished_callback(on_finished) + aptkit_client.set_error_callback(on_error) + aptkit_client.set_cancelled_callback(on_cancelled) + + aptkit_client.update_cache() + except Exception as e: + self._refresh_error = f"aptkit setup raised: {e}" + self._aptkit_done.set() + + @staticmethod + def _refresh_via_mint_refresh_cache(): + try: + result = subprocess.run(["sudo", "/usr/bin/mint-refresh-cache"]) + if result.returncode != 0: + return f"mint-refresh-cache exited with code {result.returncode}" + return None + except Exception as e: + return f"mint-refresh-cache raised: {e}" + + def install_packages(self, packages): + # Install the given list of package names via aptkit. Blocks until the + # transaction finishes, is cancelled, or errors. After return, callers + # should check self.install_cancelled and self.install_error. + self.install_error = None + self.install_cancelled = False + self._aptkit_done = threading.Event() + self._start_install(packages) + # Generous bound (4h) so legitimately long upgrades aren't aborted, but + # we never hang forever if aptkit dies without firing a callback. + if not self._aptkit_done.wait(timeout=4 * 60 * 60): + self.install_error = "Timed out waiting for aptkit install" + + @_idle + def _start_install(self, packages): + try: + client = aptkit.simpleclient.SimpleAPTClient(self.ui_window) + + def on_finished(transaction, exit_state): + if exit_state != aptkit.enums.EXIT_SUCCESS: + self.install_error = f"aptkit transaction finished with exit_state={exit_state}" + self._aptkit_done.set() + + def on_error(error_code, error_details): + self.install_error = f"aptkit error code={error_code} details={error_details}" + self._aptkit_done.set() + + def on_cancelled(): + self.install_cancelled = True + self._aptkit_done.set() + + client.set_finished_callback(on_finished) + client.set_error_callback(on_error) + client.set_cancelled_callback(on_cancelled) + + client.install_packages(packages) + except Exception as e: + self.install_error = f"aptkit setup raised: {e}" + self._aptkit_done.set() + + def fetch_updates(self): + # Compute the list of available updates. Blocks until done, or until + # the child process dies / times out — in which case self.error is set. + self.updates = [] + self.error = None + result_queue = Queue() + process = Process(target=self._fetch_updates_in_process, args=(result_queue,)) + process.start() + try: + self.error, self.updates = result_queue.get(timeout=60) + except queue.Empty: + self.error = "Timed out waiting for fetch_updates result" + if process.is_alive(): + process.terminate() + process.join(timeout=5) + if self.error is None and process.exitcode not in (0, None): + self.error = f"fetch_updates child exited with code {process.exitcode}" + + def _fetch_updates_in_process(self, queue): + try: + self.load_cache() + self.find_changes() + self.apply_l10n_descriptions() + self.load_aliases() + self.apply_aliases() + self.clean_descriptions() + queue.put([None, self.get_updates()]) + except Exception as error: + print(sys.exc_info()[0]) + print("Error in fetch_updates: %s" % error) + traceback.print_exc() + queue.put([str(error).replace("E:", "\n").strip(), []]) + + def fetch_test_updates(self, test_name): + print("SIMULATING TEST MODE:", test_name) self.updates = {} + self.error = None + + if test_name == "error": + self.error = "Testing - this is a simulated error." + elif test_name == "up-to-date": + pass + elif test_name == "self-update": + self.load_cache() + self._add_dummy_update("mintupdate", False) + elif test_name == "updates": + self.load_cache() + self._add_dummy_update("python3", False) + self._add_dummy_update("mint-meta-core", False) + self._add_dummy_update("linux-generic", True) + self._add_dummy_update("xreader", False) + elif test_name == "tracker-max-age": + self.load_cache() + self._add_dummy_update("dnsmasq", False) + self._add_dummy_update("linux-generic", True) + + updates_json = { + "mint-meta-common": { "type": "package", "since": "2020.12.03", "days": 99 }, + "linux-meta": { "type": "security", "since": "2020.12.03", "days": 99 } + } + root_json = { + "updates": updates_json, + "version": 1, + "checked": "2020.12.04", + "notified": "2020.12.03" + } + os.makedirs(CONFIG_PATH, exist_ok=True) + with open(os.path.join(CONFIG_PATH, "updates.json"), "w") as f: + json.dump(root_json, f) + + # Match the post-fetch_updates contract: .updates is a list of Update objects. + self.updates = list(self.updates.values()) + + def _add_dummy_update(self, package_name, kernel_update): + pkg = self.cache[package_name] + self.add_update(pkg, kernel_update, "99.0.0") def load_aliases(self): self.aliases = {} @@ -347,14 +539,13 @@ def capitalize(self, string): if __name__ == "__main__": try: - check = APTCheck() - check.find_changes() - check.apply_l10n_descriptions() - check.load_aliases() - check.apply_aliases() - check.clean_descriptions() - updates = check.get_updates() - for update in updates: + check = AptUpdater() + check.refresh() + check.fetch_updates() + if check.error is not None: + print("Error: %s" % check.error) + sys.exit(1) + for update in check.updates: print(update.display_name, update.new_version, update.short_description) except Exception as error: print(error) diff --git a/usr/lib/linuxmint/mintUpdate/checkWarnings.py b/usr/lib/linuxmint/mintUpdate/checkWarnings.py deleted file mode 100755 index 75b447fa2..000000000 --- a/usr/lib/linuxmint/mintUpdate/checkWarnings.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/python3 - -import sys -import apt_pkg - -try: - selection = sys.argv[1:] - #print ' '.join(str(pkg) for pkg in selection) - packages_to_install = [] - packages_to_remove = [] - - apt_pkg.init() - cache = apt_pkg.Cache(None) - - depcache = apt_pkg.DepCache(cache) - depcache.init() - - with apt_pkg.ActionGroup(depcache): - for package in selection: - pkg = cache[package] - #print "Marking : %s to install" % pkg.Name - depcache.mark_install(pkg) - - depcache.fix_broken() - - #print "Install : %d" % depcache.inst_count - #print "Remove : %d" % depcache.del_count - - # Get changes - for pkg in cache.packages: - if (not depcache.marked_keep(pkg) and - (depcache.marked_install(pkg) or depcache.marked_upgrade(pkg)) and - pkg.name not in selection and - "%s:%s" % (pkg.name, pkg.architecture) not in selection and - pkg.name not in packages_to_install): - packages_to_install.append(pkg.name) - if depcache.marked_delete(pkg) and pkg.name not in packages_to_remove: - packages_to_remove.append(pkg.name) - installations = ' '.join(packages_to_install) - removals = ' '.join(packages_to_remove) - print("%s###%s" % (installations, removals)) -except Exception as e: - print(e) - print(sys.exc_info()[0]) diff --git a/usr/lib/linuxmint/mintUpdate/flatpak-update-worker.py b/usr/lib/linuxmint/mintUpdate/flatpak-update-worker.py index dcace28a9..8f7afbf3d 100755 --- a/usr/lib/linuxmint/mintUpdate/flatpak-update-worker.py +++ b/usr/lib/linuxmint/mintUpdate/flatpak-update-worker.py @@ -13,9 +13,11 @@ import gi gi.require_version('GLib', '2.0') +gi.require_version('GLibUnix', '2.0') gi.require_version('Gtk', '3.0') gi.require_version('Flatpak', '1.0') -from gi.repository import Gtk, GLib, Flatpak, Gio +gi.require_version('GioUnix', '2.0') +from gi.repository import Gtk, GLib, GLibUnix, Flatpak, Gio, GioUnix from mintcommon.installer import installer from mintcommon.installer import _flatpak @@ -68,7 +70,7 @@ def __init__(self): self.cancellable.cancel() self.quit() - self.stdin = Gio.UnixInputStream.new(sys.stdin.fileno(), True) + self.stdin = GioUnix.InputStream.new(sys.stdin.fileno(), True) self.updates = [] @@ -337,7 +339,7 @@ def quit_on_ml(self): args = parser.parse_args() updater = FlatpakUpdateWorker() - GLib.unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGTERM, updater.quit, None) + GLibUnix.signal_add_full(GLib.PRIORITY_HIGH, signal.SIGTERM, updater.quit, None) if args.refresh: try: diff --git a/usr/lib/linuxmint/mintUpdate/mintUpdate.py b/usr/lib/linuxmint/mintUpdate/mintUpdate.py index f256f942b..a5bb7a0e9 100755 --- a/usr/lib/linuxmint/mintUpdate/mintUpdate.py +++ b/usr/lib/linuxmint/mintUpdate/mintUpdate.py @@ -5,11 +5,9 @@ import os import sys import gi -import tempfile import time import gettext import io -import json import locale import tarfile import urllib.request @@ -22,9 +20,7 @@ import setproctitle import platform import re -import aptkit.simpleclient -import checkAPT -from multiprocess import Process, Queue +import aptUpdater gi.require_version('Gtk', '3.0') gi.require_version('Notify', '0.7') @@ -34,7 +30,9 @@ # local imports import logger from kernelwindow import KernelWindow -from Classes import Update, PRIORITY_UPDATES, CONFIG_PATH, UpdateTracker, _idle, _async +from preferences import PreferencesWindow + +from Classes import AutorefreshTimer, Update, PRIORITY_UPDATES, UpdateTracker, _idle, _async settings = Gio.Settings(schema_id="com.linuxmint.updates") @@ -59,10 +57,6 @@ CINNAMON_SUPPORT = cinnamon_support FLATPAK_SUPPORT = flatpak_support -# import AUTOMATIONS dict -with open("/usr/share/linuxmint/mintupdate/automation/index.json") as f: - AUTOMATIONS = json.load(f) - try: os.system("killall -q mintUpdate") except Exception as e: @@ -86,8 +80,9 @@ (UPDATE_CHECKED, UPDATE_DISPLAY_NAME, UPDATE_OLD_VERSION, UPDATE_NEW_VERSION, UPDATE_SOURCE, UPDATE_SIZE, UPDATE_SIZE_STR, UPDATE_TYPE_PIX, UPDATE_TYPE, UPDATE_TOOLTIP, UPDATE_SORT_STR, UPDATE_OBJ) = range(12) -BLACKLIST_PKG_NAME = 0 - +MINUTE = 60 +HOUR = 60 * MINUTE +DAY = 24 * HOUR def size_to_string(size): f_size = float(size) @@ -121,7 +116,7 @@ def __init__(self, application): @_async def start(self): - self.application.refresh(False) + self.application.queue_refresh(False) self.update_cachetime() if os.path.isfile(self.pkgcache) and os.path.isfile(self.dpkgstatus): while True: @@ -134,7 +129,7 @@ def start(self): self.cachetime = cachetime self.statustime = statustime self.application.logger.write("Changes to the package cache detected; triggering refresh") - self.application.refresh(False) + self.application.queue_refresh(False) except: pass time.sleep(90) @@ -175,14 +170,15 @@ class MintUpdate(): def __init__(self): self.information_window_showing = False self.history_window_showing = False - self.preferences_window_showing = False + self.preferences_window = None self.updates_inhibited = False self.reboot_required = False self.refreshing = False - self.refreshing_apt = False - self.refreshing_flatpak = False - self.refreshing_cinnamon = False - self.auto_refresh_is_alive = False + self.refresh_threads = [] + self.auto_refresh_source = AutorefreshTimer(self._auto_refresh_tick) + self.auto_refresh_source.attach(None) + self.initial_refresh_done = False + self.last_refresh_time = 0 self.hidden = True # whether the window is hidden or not self.packages = [] # packages selected for update self.flatpaks = [] # flatpaks selected for update @@ -197,6 +193,8 @@ def __init__(self): self.is_lmde = False self.app_restart_required = False self.show_cinnamon_enabled = False + self.column_upgrade = None + self._bulk_toggle_paths = None self.settings.connect("changed", self._on_settings_changed) self._on_settings_changed(self.settings, None) @@ -241,10 +239,10 @@ def __init__(self): cr.connect("toggled", self.toggled) cr.set_property("activatable", True) - column_upgrade = Gtk.TreeViewColumn(_("Upgrade"), cr) - column_upgrade.add_attribute(cr, "active", UPDATE_CHECKED) - column_upgrade.set_sort_column_id(UPDATE_CHECKED) - column_upgrade.set_resizable(True) + self.column_upgrade = Gtk.TreeViewColumn(_("Upgrade"), cr) + self.column_upgrade.add_attribute(cr, "active", UPDATE_CHECKED) + self.column_upgrade.set_sort_column_id(UPDATE_CHECKED) + self.column_upgrade.set_resizable(True) column_name = Gtk.TreeViewColumn(_("Name"), Gtk.CellRendererText(), markup=UPDATE_DISPLAY_NAME) column_name.set_sort_column_id(UPDATE_DISPLAY_NAME) @@ -274,7 +272,7 @@ def __init__(self): self.treeview.set_search_equal_func(name_search_func) self.treeview.append_column(column_type) - self.treeview.append_column(column_upgrade) + self.treeview.append_column(self.column_upgrade) self.treeview.append_column(column_name) self.treeview.append_column(column_old_version) self.treeview.append_column(column_new_version) @@ -285,10 +283,12 @@ def __init__(self): self.treeview.set_reorderable(False) self.treeview.show() + self.treeview.connect("button-press-event", self.treeview_button_pressed) self.treeview.connect("button-release-event", self.treeview_right_clicked) self.treeview.connect("row-activated", self.treeview_row_activated) selection = self.treeview.get_selection() + selection.set_mode(Gtk.SelectionMode.MULTIPLE) selection.connect("changed", self.display_selected_update) self.ui_notebook_details.connect("switch-page", self.switch_page) self.ui_window.connect("delete_event", self.close_window) @@ -438,11 +438,11 @@ def __init__(self): newVersionColumnMenuItem.connect("toggled", self.setVisibleColumn, column_new_version, "show-new-version-column") visibleColumnsMenu.append(newVersionColumnMenuItem) - sizeColumnMenuItem = Gtk.CheckMenuItem(label=_("Origin")) - sizeColumnMenuItem.set_active(self.settings.get_boolean("show-origin-column")) + originColumnMenuItem = Gtk.CheckMenuItem(label=_("Origin")) + originColumnMenuItem.set_active(self.settings.get_boolean("show-origin-column")) column_origin.set_visible(self.settings.get_boolean("show-origin-column")) - sizeColumnMenuItem.connect("toggled", self.setVisibleColumn, column_origin, "show-origin-column") - visibleColumnsMenu.append(sizeColumnMenuItem) + originColumnMenuItem.connect("toggled", self.setVisibleColumn, column_origin, "show-origin-column") + visibleColumnsMenu.append(originColumnMenuItem) sizeColumnMenuItem = Gtk.CheckMenuItem(label=_("Size")) sizeColumnMenuItem.set_active(self.settings.get_boolean("show-size-column")) @@ -510,6 +510,8 @@ def __init__(self): if showWindow == "show": self.show_window() + self.apt_updater = aptUpdater.AptUpdater(self.ui_window) + if CINNAMON_SUPPORT: self.cinnamon_updater = cinnamon.UpdateManager() else: @@ -532,7 +534,6 @@ def __init__(self): self.ui_notebook_details.set_current_page(0) - self.refresh_schedule_enabled = self.settings.get_boolean("refresh-schedule-enabled") self.start_auto_refresh() Gtk.main() @@ -552,6 +553,11 @@ def _on_settings_changed(self, settings, key, data=None): self.app_restart_required = settings.get_boolean("show-cinnamon-updates") != self.show_cinnamon_enabled or \ settings.get_boolean("show-flatpak-updates") != self.show_flatpak_enabled + if key in ("refresh-minutes", "refresh-hours", "refresh-days", + "autorefresh-minutes", "autorefresh-hours", "autorefresh-days", + "refresh-schedule-enabled"): + self.start_auto_refresh() + ######### EVENT HANDLERS ######### @@ -764,7 +770,6 @@ def set_refresh_mode(self, enabled): # Make sure we're never stuck on the status_refreshing page: if self.ui_stack.get_visible_child_name() == "refresh_page": self.ui_stack.set_visible_child_name("updates_page") - #self.ui_paned.set_position(self.ui_paned.get_position()) self.ui_toolbar.set_sensitive(True) self.ui_menubar.set_sensitive(True) self.set_window_busy(enabled) @@ -871,14 +876,69 @@ def show_welcome_page(self, widget=None): def treeview_row_activated(self, treeview, path, view_column): self.toggled(None, path) + def treeview_button_pressed(self, widget, event): + if event.type != Gdk.EventType.BUTTON_PRESS: + return False + + if event.button == Gdk.BUTTON_SECONDARY: + path_info = widget.get_path_at_pos(int(event.x), int(event.y)) + if path_info is not None: + sel_paths = widget.get_selection().get_selected_rows()[1] + if any(p.compare(path_info[0]) == 0 for p in sel_paths): + return True + return False + + if event.button != Gdk.BUTTON_PRIMARY: + return False + if event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK): + return False + path_info = widget.get_path_at_pos(int(event.x), int(event.y)) + if path_info is None: + return False + path, column, _x, _y = path_info + if column is not self.column_upgrade: + return False + sel_paths = widget.get_selection().get_selected_rows()[1] + if len(sel_paths) > 1 and any(p.compare(path) == 0 for p in sel_paths): + self._bulk_toggle_paths = sel_paths + return False + def toggled(self, renderer, path): model = self.treeview.get_model() + if isinstance(path, str): + path = Gtk.TreePath.new_from_string(path) iter = model.get_iter(path) - if iter is not None: - model.set_value(iter, UPDATE_CHECKED, (not model.get_value(iter, UPDATE_CHECKED))) + if iter is None: + return + + new_value = not model.get_value(iter, UPDATE_CHECKED) + + bulk_paths = self._bulk_toggle_paths + self._bulk_toggle_paths = None + + if bulk_paths is None: + sel_paths = self.treeview.get_selection().get_selected_rows()[1] + if len(sel_paths) > 1 and any(p.compare(path) == 0 for p in sel_paths): + bulk_paths = sel_paths + + if bulk_paths is None: + model.set_value(iter, UPDATE_CHECKED, new_value) + else: + for p in bulk_paths: + it = model.get_iter(p) + if it is not None: + model.set_value(it, UPDATE_CHECKED, new_value) + GLib.idle_add(self._restore_selection, bulk_paths) self.update_installable_state() + def _restore_selection(self, paths): + sel = self.treeview.get_selection() + sel.unselect_all() + for p in paths: + sel.select_path(p) + return GLib.SOURCE_REMOVE + @_idle def set_textview_changes_text(self, text): self.textview_changes.set_text(text) @@ -888,8 +948,12 @@ def display_selected_update(self, selection): self.textview_packages.set_text("") self.textview_description.set_text("") self.textview_changes.set_text("") - (model, iter) = selection.get_selected() - if iter is not None: + model, paths = selection.get_selected_rows() + if len(paths) > 1: + self.display_multi_selection(model, paths) + return + if len(paths) == 1: + iter = model.get_iter(paths[0]) update = model.get_value(iter, UPDATE_OBJ) description = update.description.replace("\\n", "\n") desc_tab = self.ui_notebook_details.get_nth_page(TAB_DESC) @@ -1110,87 +1174,69 @@ def retrieve_changelog(self, update): self.set_textview_changes_text("\n".join(changelog)) - - @_async def start_auto_refresh(self): - self.auto_refresh_is_alive = True - minute = 60 - hour = 60 * minute - day = 24 * hour - initial_refresh = True - settings_prefix = "" - refresh_type = "initial" - - while self.refresh_schedule_enabled: - try: - schedule = { - "minutes": self.settings.get_int("%srefresh-minutes" % settings_prefix), - "hours": self.settings.get_int("%srefresh-hours" % settings_prefix), - "days": self.settings.get_int("%srefresh-days" % settings_prefix) - } - timetosleep = schedule["minutes"] * minute + schedule["hours"] * hour + schedule["days"] * day - - if not timetosleep: - time.sleep(60) # sleep 1 minute, don't mind the config we don't want an infinite loop to go nuts :) - else: - now = int(time.time()) - if not initial_refresh: - refresh_last_run = self.settings.get_int("refresh-last-run") - if not refresh_last_run or refresh_last_run > now: - refresh_last_run = now - self.settings.set_int("refresh-last-run", now) - time_since_last_refresh = now - refresh_last_run - if time_since_last_refresh > 0: - timetosleep = timetosleep - time_since_last_refresh - # always wait at least 1 minute to be on the safe side - if timetosleep < 60: - timetosleep = 60 - - schedule["days"] = int(timetosleep / day) - schedule["hours"] = int((timetosleep - schedule["days"] * day) / hour) - schedule["minutes"] = int((timetosleep - schedule["days"] * day - schedule["hours"] * hour) / minute) - self.logger.write("%s refresh will happen in %d day(s), %d hour(s) and %d minute(s)" % - (refresh_type.capitalize(), schedule["days"], schedule["hours"], schedule["minutes"])) - time.sleep(timetosleep) - if not self.refresh_schedule_enabled: - self.logger.write(f"Auto-refresh disabled in preferences; cancelling {refresh_type} refresh") - self.uninhibit_pm() - return - if self.hidden: - self.logger.write(f"Update Manager is in tray mode; performing {refresh_type} refresh") - self.refresh(True) - # FIXME: self.refresh() is an _idle function, and we're on a thread - we will continue - # and loop before self.refreshing is set. Force a brief dwell to allow the refresh() call - # to get ahead of us and set self.refreshing and update 'refresh-last-run', otherwise we'll - # get a double-refresh 1 minute apart every time. - time.sleep(0.5) - while self.refreshing: - time.sleep(5) - else: - if initial_refresh: - self.logger.write(f"Update Manager window is open; skipping {refresh_type} refresh") - else: - self.logger.write(f"Update Manager window is open; delaying {refresh_type} refresh by 60s") - time.sleep(60) - except Exception as e: - print (e) - self.logger.write_error("Exception occurred during %s refresh: %s" % (refresh_type, str(sys.exc_info()[0]))) + # (Re)schedule the auto-refresh timer based on current settings and + # last_refresh_time. Safe to call any time the schedule needs to be + # reset — settings change, after a refresh kicks off, etc. + self.auto_refresh_source.disarm() + + if not self.settings.get_boolean("refresh-schedule-enabled"): + self.logger.write("Auto-refresh disabled in preferences") + return - if initial_refresh: - initial_refresh = False - settings_prefix = "auto" - refresh_type = "auto" + configured_interval = self._compute_refresh_interval() + if configured_interval == 0: + # No interval configured; poll the setting again in a minute in case + # it changes (settings change also re-triggers us via _on_settings_changed). + interval = 60 + elif self.last_refresh_time: + elapsed = int(time.time()) - self.last_refresh_time + interval = max(60, configured_interval - elapsed) else: - self.logger.write("Auto-refresh disabled in preferences, automatic refresh thread stopped") - self.auto_refresh_is_alive = False + interval = configured_interval + + refresh_type = "Auto" if self.initial_refresh_done else "Initial" + days = int(interval / DAY) + hours = int((interval - days * DAY) / HOUR) + minutes = int((interval - days * DAY - hours * HOUR) / MINUTE) + self.logger.write(f"{refresh_type} refresh will happen in {days} day(s), {hours} hour(s) and {minutes} minute(s)") + self.auto_refresh_source.arm(interval) + + def _auto_refresh_tick(self): + if not self.settings.get_boolean("refresh-schedule-enabled"): + return + + if not self.hidden or self.refreshing: + why = "window is open" if not self.hidden else "another refresh is in progress" + self.logger.write(f"Auto refresh deferred ({why}); will retry shortly") + self.auto_refresh_source.arm(60) + return + + self.logger.write("Update Manager is in tray mode; performing auto refresh") + + try: + if not self.refresh(True): + # refresh declined (e.g. updates_inhibited); try again shortly. + self.auto_refresh_source.arm(60) + return + + # self.refresh() will restart the timer. + except Exception: + self.logger.write_error(f"Exception in auto-refresh tick: {traceback.format_exc()}") + self.auto_refresh_source.arm(60) + + def _compute_refresh_interval(self): + prefix = "auto" if self.initial_refresh_done else "" + return (self.settings.get_int(f"{prefix}refresh-minutes") * MINUTE + + self.settings.get_int(f"{prefix}refresh-hours") * HOUR + + self.settings.get_int(f"{prefix}refresh-days") * DAY) def switch_page(self, notebook, page, page_num): - selection = self.treeview.get_selection() - (model, iter) = selection.get_selected() - if iter and page_num == 2 and not self.changelog_retriever_started: + model, paths = self.treeview.get_selection().get_selected_rows() + if len(paths) == 1 and page_num == 2 and not self.changelog_retriever_started: # Changelog tab - update = model.get_value(iter, UPDATE_OBJ) + update = model.get_value(model.get_iter(paths[0]), UPDATE_OBJ) self.retrieve_changelog(update) self.changelog_retriever_started = True @@ -1214,21 +1260,79 @@ def display_package_list(self, update, is_flatpak=False): size_label, size_to_string(update.size)) self.textview_packages.set_text(packages) + def display_multi_selection(self, model, paths): + self.textview_description.set_text( + _("Select a single update to view its description and changelog.")) + + package_names = set() + total_size = 0 + for p in paths: + update = model.get_value(model.get_iter(p), UPDATE_OBJ) + package_names.update(update.package_names) + total_size += update.size + + prefix = "\n • " + count = len(package_names) + packages = "%s%s%s\n%s %s\n\n" % \ + (gettext.ngettext("The selected updates affect the following installed package:", + "The selected updates affect the following installed packages:", + count), + prefix, + prefix.join(sorted(package_names)), + _("Total size:"), size_to_string(total_size)) + self.textview_packages.set_text(packages) + + self.ui_notebook_details.get_nth_page(TAB_PACKAGES).show() + self.ui_notebook_details.get_nth_page(TAB_CHANGELOG).hide() + self.changelog_retriever_started = False + def treeview_right_clicked(self, widget, event): - if event.button == 3: - (model, iter) = widget.get_selection().get_selected() - if iter is not None: - update = model.get_value(iter, UPDATE_OBJ) - menu = Gtk.Menu() - menuItem = Gtk.MenuItem.new_with_mnemonic(_("Ignore the current update for this package")) - menuItem.connect("activate", self.add_to_ignore_list, update.source_packages, True) - menu.append(menuItem) - menuItem = Gtk.MenuItem.new_with_mnemonic(_("Ignore all future updates for this package")) - menuItem.connect("activate", self.add_to_ignore_list, update.source_packages, False) - menu.append(menuItem) - menu.attach_to_widget (widget, None) - menu.show_all() - menu.popup(None, None, None, None, event.button, event.time) + if event.button != Gdk.BUTTON_SECONDARY: + return + path_info = widget.get_path_at_pos(int(event.x), int(event.y)) + if path_info is None: + return + clicked_path = path_info[0] + model = widget.get_model() + selection = widget.get_selection() + sel_paths = selection.get_selected_rows()[1] + + if any(p.compare(clicked_path) == 0 for p in sel_paths): + target_paths = sel_paths + else: + target_paths = [clicked_path] + selection.unselect_all() + selection.select_path(clicked_path) + + source_packages = [] + seen = set() + for p in target_paths: + update = model.get_value(model.get_iter(p), UPDATE_OBJ) + for pkg in update.source_packages: + if pkg not in seen: + seen.add(pkg) + source_packages.append(pkg) + + count = len(target_paths) + ignore_current = gettext.ngettext( + "Ignore the current update for this package", + "Ignore the current updates for these packages", + count) + ignore_future = gettext.ngettext( + "Ignore all future updates for this package", + "Ignore all future updates for these packages", + count) + + menu = Gtk.Menu() + menuItem = Gtk.MenuItem.new_with_mnemonic(ignore_current) + menuItem.connect("activate", self.add_to_ignore_list, source_packages, True) + menu.append(menuItem) + menuItem = Gtk.MenuItem.new_with_mnemonic(ignore_future) + menuItem.connect("activate", self.add_to_ignore_list, source_packages, False) + menu.append(menuItem) + menu.attach_to_widget(widget, None) + menu.show_all() + menu.popup(None, None, None, None, event.button, event.time) def add_to_ignore_list(self, widget, source_packages, versioned): blacklist = self.settings.get_strv("blacklisted-packages") @@ -1292,12 +1396,17 @@ def update_log(line): # Add mintupdate style class for easier theming self.ui_window.get_style_context().add_class('mintupdate') + def copy_log_path(widget): + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + clipboard.set_text(self.logger.log.name, -1) + textbuffer = builder.get_object("log_textview").get_buffer() window.connect("destroy", destroy_window) builder.get_object("close_button").connect("clicked", destroy_window) builder.get_object("processid_label").set_text(str(os.getpid())) textbuffer.set_text(self.logger.read()) builder.get_object("log_filename").set_text(str(self.logger.log.name)) + builder.get_object("copy_log_path_button").connect("clicked", copy_log_path) self.logger.set_callback(update_log) self.information_window_showing = True @@ -1495,7 +1604,7 @@ def open_repositories(self, widget): def run_mintsources(self): proc = subprocess.Popen(["pkexec", "mintsources"]) proc.wait() - self.refresh(False) + self.queue_refresh(False) def open_timeshift(self, widget): subprocess.Popen(["pkexec", "timeshift-gtk"]) @@ -1517,298 +1626,16 @@ def open_shortcuts(self, widget): ######### PREFERENCES SCREEN ######### def open_preferences(self, widget, show_automation=False): - if self.preferences_window_showing: + if self.preferences_window is not None: + self.preferences_window.window.present() return - self.preferences_window_showing = True self.ui_window.set_sensitive(False) - gladefile = "/usr/share/linuxmint/mintupdate/preferences.ui" - builder = Gtk.Builder() - builder.set_translation_domain("mintupdate") - builder.add_from_file(gladefile) - window = builder.get_object("main_window") - window.set_transient_for(self.ui_window) - window.set_title(_("Preferences")) - window.set_icon_name("mintupdate") + self.preferences_window = PreferencesWindow(self, show_automation=show_automation) + self.preferences_window.window.connect("destroy", self._on_preferences_destroyed) - # Add mintupdate style class for easier theming - self.ui_window.get_style_context().add_class('mintupdate') - - window.connect("destroy", self.close_preferences, window) - - switch_container = builder.get_object("switch_container") - stack = Gtk.Stack() - stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) - stack.set_transition_duration(150) - stack_switcher = Gtk.StackSwitcher() - stack_switcher.set_stack(stack) - switch_container.pack_start(stack_switcher, True, True, 0) - stack_switcher.set_halign(Gtk.Align.CENTER) - - page_holder = builder.get_object("page_container") - page_holder.add(stack) - - stack.add_titled(builder.get_object("page_options"), "page_options", _("Options")) - stack.add_titled(builder.get_object("page_blacklist"), "page_blacklist", _("Packages")) - stack.add_titled(builder.get_object("page_auto"), "page_auto", _("Automation")) - - # Options - box = builder.get_object("page_options") - page = SettingsPage() - box.pack_start(page, True, True, 0) - section = page.add_section(_("Interface")) - section.add_row(GSettingsSwitch(_("Hide the update manager after applying updates"), "com.linuxmint.updates", "hide-window-after-update")) - section.add_row(GSettingsSwitch(_("Only show a tray icon when updates are available or in case of errors"), "com.linuxmint.updates", "hide-systray")) - - section = page.add_section(_("Auto-refresh")) - switch = GSettingsSwitch(_("Refresh the list of updates automatically"), "com.linuxmint.updates", "refresh-schedule-enabled") - switch.content_widget.connect("notify::active", self.auto_refresh_toggled) - section.add_row(switch) - - grid = Gtk.Grid() - grid.set_row_spacing(12) - grid.set_column_spacing(12) - grid.set_margin_top(6) - grid.set_margin_bottom(6) - grid.set_margin_start(32) - grid.set_margin_end(32) - - grid.attach(Gtk.Label(label=_("days")), 1, 0, 1, 1) - grid.attach(Gtk.Label(label=_("hours")), 2, 0, 1, 1) - grid.attach(Gtk.Label(label=_("minutes")), 3, 0, 1, 1) - label = Gtk.Label(label=_("First, refresh the list of updates after:")) - label.set_justify(Gtk.Justification.LEFT) - label.set_alignment(0,0.5) - grid.attach(label, 0, 1, 1, 1) - label = Gtk.Label(label=_("Then, refresh the list of updates every:")) - label.set_justify(Gtk.Justification.LEFT) - label.set_alignment(0,0.5) - grid.attach(label, 0, 2, 1, 1) - - spin_button = GSettingsSpinButton("", "com.linuxmint.updates", "refresh-days", mini=0, maxi=99, step=1, page=2) - spin_button.set_spacing(0) - spin_button.set_margin_start(0) - spin_button.set_margin_end(0) - spin_button.set_border_width(0) - grid.attach(spin_button, 1, 1, 1, 1) - spin_button = GSettingsSpinButton("", "com.linuxmint.updates", "refresh-hours", mini=0, maxi=23, step=1, page=5) - spin_button.set_spacing(0) - spin_button.set_margin_start(0) - spin_button.set_margin_end(0) - spin_button.set_border_width(0) - grid.attach(spin_button, 2, 1, 1, 1) - spin_button = GSettingsSpinButton("", "com.linuxmint.updates", "refresh-minutes", mini=0, maxi=59, step=1, page=10) - spin_button.set_spacing(0) - spin_button.set_margin_start(0) - spin_button.set_margin_end(0) - spin_button.set_border_width(0) - grid.attach(spin_button, 3, 1, 1, 1) - spin_button = GSettingsSpinButton("", "com.linuxmint.updates", "autorefresh-days", mini=0, maxi=99, step=1, page=2) - spin_button.set_spacing(0) - spin_button.set_margin_start(0) - spin_button.set_margin_end(0) - spin_button.set_border_width(0) - grid.attach(spin_button, 1, 2, 1, 1) - spin_button = GSettingsSpinButton("", "com.linuxmint.updates", "autorefresh-hours", mini=0, maxi=23, step=1, page=5) - spin_button.set_spacing(0) - spin_button.set_margin_start(0) - spin_button.set_margin_end(0) - spin_button.set_border_width(0) - grid.attach(spin_button, 2, 2, 1, 1) - spin_button = GSettingsSpinButton("", "com.linuxmint.updates", "autorefresh-minutes", mini=0, maxi=59, step=1, page=10) - spin_button.set_spacing(0) - spin_button.set_margin_start(0) - spin_button.set_margin_end(0) - spin_button.set_border_width(0) - grid.attach(spin_button, 3, 2, 1, 1) - - label = Gtk.Label() - label.set_markup("%s" % _("Note: The list only gets refreshed while the Update Manager window is closed (in system tray mode).")) - grid.attach(label, 0, 3, 4, 1) - section.add_reveal_row(grid, "com.linuxmint.updates", "refresh-schedule-enabled") - - section = SettingsSection(_("Notifications")) - revealer = SettingsRevealer("com.linuxmint.updates", "refresh-schedule-enabled") - revealer.add(section) - section._revealer = revealer - page.pack_start(revealer, False, False, 0) - - switch = GSettingsSwitch(_("Only show notifications for security and kernel updates"), "com.linuxmint.updates", "tracker-security-only") - section.add_reveal_row(switch, "com.linuxmint.updates", "tracker-disable-notifications", [False]) - switch = GSettingsSpinButton(_("Show a notification if an update has been available for (in logged-in days):"), "com.linuxmint.updates", "tracker-max-days", mini=2, maxi=90, step=1, page=5) - section.add_reveal_row(switch, "com.linuxmint.updates", "tracker-disable-notifications", [False]) - switch = GSettingsSpinButton(_("Show a notification if an update is older than (in days):"), "com.linuxmint.updates", "tracker-max-age", mini=2, maxi=90, step=1, page=5) - section.add_reveal_row(switch, "com.linuxmint.updates", "tracker-disable-notifications", [False]) - switch = GSettingsSpinButton(_("Don't show notifications if an update was applied in the last (in days):"), "com.linuxmint.updates", "tracker-grace-period", mini=2, maxi=90, step=1, page=5) - section.add_reveal_row(switch, "com.linuxmint.updates", "tracker-disable-notifications", [False]) - - box = builder.get_object("update_types_box") - page = SettingsPage() - box.pack_start(page, True, True, 0) - - # if False: - if os.path.exists("/usr/bin/cinnamon") or os.path.exists("/usr/bin/flatpak"): - section = page.add_section(_("Update types"), _("In addition to system packages, check for:")) - - if os.path.exists("/usr/bin/cinnamon"): - section.add_row(GSettingsSwitch(_("Cinnamon spice updates"), "com.linuxmint.updates", "show-cinnamon-updates")) - if os.path.exists("/usr/bin/flatpak"): - section.add_row(GSettingsSwitch(_("Flatpak updates"), "com.linuxmint.updates", "show-flatpak-updates")) - box.show_all() - else: - box.set_no_show_all(True) - box.hide() - - # Blacklist - treeview_blacklist = builder.get_object("treeview_blacklist") - column = Gtk.TreeViewColumn(_("Ignored Updates"), Gtk.CellRendererText(), text=BLACKLIST_PKG_NAME) - column.set_sort_column_id(BLACKLIST_PKG_NAME) - column.set_resizable(True) - treeview_blacklist.append_column(column) - treeview_blacklist.set_headers_clickable(True) - treeview_blacklist.set_reorderable(False) - treeview_blacklist.show() - model = Gtk.TreeStore(str) # BLACKLIST_PKG_NAME - model.set_sort_column_id(BLACKLIST_PKG_NAME, Gtk.SortType.ASCENDING ) - treeview_blacklist.set_model(model) - blacklist = self.settings.get_strv("blacklisted-packages") - for ignored_pkg in blacklist: - iter = model.insert_before(None, None) - model.set_value(iter, BLACKLIST_PKG_NAME, ignored_pkg) - builder.get_object("button_add").connect("clicked", self.add_blacklisted_package, treeview_blacklist, window) - builder.get_object("button_remove").connect("clicked", self.remove_blacklisted_package, treeview_blacklist) - builder.get_object("button_add").set_always_show_image(True) - builder.get_object("button_remove").set_always_show_image(True) - - # Automation - box = builder.get_object("page_auto_inner") - page = SettingsPage() - box.pack_start(page, True, True, 0) - section = page.add_section(_("Package Updates"), _("Performed as root on a daily basis")) - autoupgrade_switch = Switch(_("Apply updates automatically")) - autoupgrade_switch.content_widget.set_active(os.path.isfile(AUTOMATIONS["upgrade"][2])) - autoupgrade_switch.content_widget.connect("notify::active", self.set_auto_upgrade) - section.add_row(autoupgrade_switch) - button = Gtk.Button(label=_("Export blacklist to /etc/mintupdate.blacklist")) - button.set_margin_start(20) - button.set_margin_end(20) - button.set_border_width(5) - button.set_tooltip_text(_("Click this button to make automatic updates use your current blacklist.")) - button.connect("clicked", self.export_blacklist) - section.add_row(button) - additional_options = [] - if os.path.exists("/usr/bin/cinnamon"): - switch = GSettingsSwitch(_("Update Cinnamon spices automatically"), "com.linuxmint.updates", "auto-update-cinnamon-spices") - additional_options.append(switch) - if os.path.exists("/usr/bin/flatpak"): - switch = GSettingsSwitch(_("Update Flatpaks automatically"), "com.linuxmint.updates", "auto-update-flatpaks") - additional_options.append(switch) - if len(additional_options) > 0: - section = page.add_section(_("Other Updates"), _("Performed when you log in")) - for switch in additional_options: - section.add_row(switch) - section = page.add_section(_("Automatic Maintenance"), _("Performed as root on a weekly basis")) - autoremove_switch = Switch(_("Remove obsolete kernels and dependencies")) - autoremove_switch.content_widget.set_active(os.path.isfile(AUTOMATIONS["autoremove"][2])) - autoremove_switch.content_widget.connect("notify::active", self.set_auto_remove) - section.add_row(autoremove_switch) - section.add_note(_("This option always leaves at least one older kernel installed and never removes manually installed kernels.")) - - window.show_all() - - if show_automation: - stack.set_visible_child_name("page_auto") - - def export_blacklist(self, widget): - filename = os.path.join(tempfile.gettempdir(), "mintUpdate/blacklist") - blacklist = self.settings.get_strv("blacklisted-packages") - with open(filename, "w") as f: - f.write("\n".join(blacklist) + "\n") - subprocess.run(["pkexec", "/usr/bin/mintupdate-automation", "blacklist", "enable"]) - - def auto_refresh_toggled(self, widget, param): - self.refresh_schedule_enabled = widget.get_active() - if self.refresh_schedule_enabled and not self.auto_refresh_is_alive: - self.start_auto_refresh() - - def set_auto_upgrade(self, widget, param): - exists = os.path.isfile(AUTOMATIONS["upgrade"][2]) - action = None - if widget.get_active() and not exists: - action = "enable" - elif not widget.get_active() and exists: - action = "disable" - if action: - subprocess.run(["pkexec", "/usr/bin/mintupdate-automation", "upgrade", action]) - if widget.get_active() != os.path.isfile(AUTOMATIONS["upgrade"][2]): - widget.set_active(not widget.get_active()) - - def set_auto_remove(self, widget, param): - exists = os.path.isfile(AUTOMATIONS["autoremove"][2]) - action = None - if widget.get_active() and not exists: - action = "enable" - elif not widget.get_active() and exists: - action = "disable" - if action: - subprocess.run(["pkexec", "/usr/bin/mintupdate-automation", "autoremove", action]) - if widget.get_active() != os.path.isfile(AUTOMATIONS["autoremove"][2]): - widget.set_active(not widget.get_active()) - - def save_blacklist(self, treeview_blacklist): - blacklist = [] - model = treeview_blacklist.get_model() - iter = model.get_iter_first() - while iter is not None: - pkg = model.get_value(iter, BLACKLIST_PKG_NAME) - iter = model.iter_next(iter) - blacklist.append(pkg) - self.settings.set_strv("blacklisted-packages", blacklist) - - def add_blacklisted_package(self, widget, treeview_blacklist, window): - dialog = Gtk.MessageDialog(window, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK, None) - dialog.set_markup(_("Please specify the source package name of the update to ignore (wildcards are supported) and optionally the version:")) - dialog.set_title(_("Ignore an Update")) - dialog.set_icon_name("mintupdate") - grid = Gtk.Grid() - grid.set_column_spacing(5) - grid.set_row_spacing(5) - grid.set_halign(Gtk.Align.CENTER) - name_entry = Gtk.Entry() - version_entry = Gtk.Entry() - grid.attach(Gtk.Label(label=_("Name:")), 0, 0, 1, 1) - grid.attach(name_entry, 1, 0, 1, 1) - grid.attach(Gtk.Label(label=_("Version:")), 0, 1, 1, 1) - grid.attach(version_entry, 1, 1, 1, 1) - grid.attach(Gtk.Label(label=_("(optional)")), 2, 1, 1, 1) - dialog.get_content_area().add(grid) - dialog.show_all() - if dialog.run() == Gtk.ResponseType.OK: - name = name_entry.get_text().strip() - version = version_entry.get_text().strip() - if name: - if version: - pkg = "%s=%s" % (name, version) - else: - pkg = name - model = treeview_blacklist.get_model() - iter = model.insert_before(None, None) - model.set_value(iter, BLACKLIST_PKG_NAME, pkg) - dialog.destroy() - self.save_blacklist(treeview_blacklist) - - def remove_blacklisted_package(self, widget, treeview_blacklist): - selection = treeview_blacklist.get_selection() - (model, iter) = selection.get_selected() - if iter is not None: - pkg = model.get_value(iter, BLACKLIST_PKG_NAME) - model.remove(iter) - self.save_blacklist(treeview_blacklist) - - def close_preferences(self, widget, window): + def _on_preferences_destroyed(self, window): self.ui_window.set_sensitive(True) - self.preferences_window_showing = False - window.destroy() - + self.preferences_window = None if self.app_restart_required: self.restart_app() else: @@ -2081,10 +1908,17 @@ def refresh_cleanup(self): self.set_refresh_mode(False) @_idle + def queue_refresh(self, refresh_cache): + self.refresh(refresh_cache) + def refresh(self, refresh_cache): + # Must be called on the main thread. Worker-thread callers should + # use queue_refresh() instead. if self.refreshing: return False + # This really only exists to show the window on first-run when show-welcome-page is True + # TODO: Make this happen more deliberately somewhere. if self.updates_inhibited: self.logger.write("Updates are inhibited, skipping refresh") self.show_window() @@ -2107,32 +1941,27 @@ def refresh(self, refresh_cache): self._on_infobar_reboot, _("Restart")) + self.refresh_threads = [] + if refresh_cache: # Note: All cache refresh happen asynchronously # refresh_updates() waits for them to finish - self.logger.write("Refreshing cache") - # APT - self.settings.set_int("refresh-last-run", int(time.time())) - self.refreshing_apt = True - if self.hidden: - self.refresh_apt_cache_externally() - else: - client = aptkit.simpleclient.SimpleAPTClient(self.ui_window) - client.set_finished_callback(self.on_cache_updated) - client.update_cache() + self.last_refresh_time = int(time.time()) + self.initial_refresh_done = True + + self.refresh_threads.append(self.refresh_apt_cache()) - # Cinnamon if CINNAMON_SUPPORT: - self.refreshing_cinnamon = True - self.refresh_cinnamon_cache() + self.refresh_threads.append(self.refresh_cinnamon_cache()) - # Flatpak if FLATPAK_SUPPORT: - self.refreshing_flatpak = True - self.refresh_flatpak_cache() + self.refresh_threads.append(self.refresh_flatpak_cache()) + + self.start_auto_refresh() self.refresh_updates() + return True def _on_infobar_reboot(self, parent, response_id): session = os.environ.get("XDG_CURRENT_DESKTOP") @@ -2148,14 +1977,11 @@ def _on_infobar_reboot(self, parent, response_id): subprocess.run(['/usr/bin/systemctl', 'reboot']) @_async - def refresh_apt_cache_externally(self): - try: - refresh_command = ["sudo", "/usr/bin/mint-refresh-cache"] - subprocess.run(refresh_command) - except: - print("Exception while calling mint-refresh-cache") - finally: - self.refreshing_apt = False + def refresh_apt_cache(self): + self.logger.write("Refreshing APT cache") + error = self.apt_updater.refresh(interactive=not self.hidden) + if error is not None: + self.logger.write_error(f"APT cache refresh did not complete successfully: {error}") @_async def refresh_cinnamon_cache(self): @@ -2166,98 +1992,21 @@ def refresh_cinnamon_cache(self): except: self.logger.write_error("Something went wrong fetching Cinnamon %ss: %s" % (spice_type, str(sys.exc_info()[0]))) print("-- Exception occurred fetching Cinnamon %ss:\n%s" % (spice_type, traceback.format_exc())) - self.refreshing_cinnamon = False @_async def refresh_flatpak_cache(self): self.logger.write("Refreshing cache for Flatpak updates") self.flatpak_updater.refresh() - self.refreshing_flatpak = False - - def on_cache_updated(self, transaction=None, exit_state=None): - self.refreshing_apt = False - - -# ---------------- Test Mode ------------------------------------------# - def dummy_update(self, check, package_name, kernel=False): - pkg = check.cache[package_name] - check.add_update(pkg, kernel, "99.0.0") - - # Part of check_apt_in_external_process fork - def handle_apt_check_test(self, queue): - print("SIMULATING TEST MODE:", self.test_mode) - if self.test_mode == "error": - # See how an error from checkAPT subprocess is handled - raise Exception("Testing - this is a simulated error.") - elif self.test_mode == "up-to-date": - # Simulate checkAPT finding no updates - queue.put([None, []]) - elif self.test_mode == "self-update": - # Simulate an update of mintupdate itself. - check = checkAPT.APTCheck() - self.dummy_update(check, "mintupdate", False) - queue.put([None, list(check.updates.values())]) - elif self.test_mode == "updates": - # Simulate some normal updates - check = checkAPT.APTCheck() - self.dummy_update(check, "python3", False) - self.dummy_update(check, "mint-meta-core", False) - self.dummy_update(check, "linux-generic", True) - self.dummy_update(check, "xreader", False) - queue.put([None, list(check.updates.values())]) - elif self.test_mode == "tracker-max-age": - # Simulate the UpdateTracker notifying about updates. - check = checkAPT.APTCheck() - self.dummy_update(check, "dnsmasq", False) - self.dummy_update(check, "linux-generic", True) - - updates_json = { - "mint-meta-common": { "type": "package", "since": "2020.12.03", "days": 99 }, - "linux-meta": { "type": "security", "since": "2020.12.03", "days": 99 } - } - root_json = { - "updates": updates_json, - "version": 1, - "checked": "2020.12.04", - "notified": "2020.12.03" - } - - os.makedirs(CONFIG_PATH, exist_ok=True) - with open(os.path.join(CONFIG_PATH, "updates.json"), "w") as f: - json.dump(root_json, f) - - queue.put([None, list(check.updates.values())]) - - return True -# ---------------- Testing ------------------------------------------# - - # called in a different process - def check_apt_in_external_process(self, queue): - # in the queue we put: error_message (None if successful), list_of_updates (None if error) - try: - if self.test_mode: - self.handle_apt_check_test(queue) - else: - check = checkAPT.APTCheck() - check.find_changes() - check.apply_l10n_descriptions() - check.load_aliases() - check.apply_aliases() - check.clean_descriptions() - updates = check.get_updates() - queue.put([None, updates]) - except Exception as error: - error_msg = str(error).replace("E:", "\n").strip() - queue.put([error_msg, None]) - print(sys.exc_info()[0]) - print("Error in checkAPT: %s" % error) - traceback.print_exc() @_async def refresh_updates(self): # Wait for all the caches to be refreshed - while (self.refreshing_apt or self.refreshing_flatpak or self.refreshing_cinnamon): - time.sleep(1) + for t in self.refresh_threads: + t.join(timeout=300) + if t.is_alive(): + self.logger.write_error( + f"Cache refresh thread {t.name} did not complete within 300s; " + "the update list may be stale") # Check presence of Mint layer if self.test_mode == "layer-error" or (not self.check_policy()): @@ -2273,22 +2022,19 @@ def refresh_updates(self): self._on_infobar_mintsources_response) self.refresh_cleanup() return - self.logger.write("Checking for updates") try: - error = None - updates = None + if self.test_mode: + self.apt_updater.fetch_test_updates(self.test_mode) + else: + self.apt_updater.fetch_updates() - # call checkAPT in a different process - queue = Queue() - process = Process(target=self.check_apt_in_external_process, args=[queue]) - process.start() - error, updates = queue.get() - process.join() + error = self.apt_updater.error + updates = self.apt_updater.updates if error is not None: - self.logger.write_error("Error in checkAPT.py, could not refresh the list of updates") + self.logger.write_error("Error in aptUpdater.py, could not refresh the list of updates") if "apt.cache.FetchFailedException" in error and " changed its " in error: error += "\n\n%s" % _("Run 'apt update' in a terminal window to address this") self.show_error(error) @@ -2297,16 +2043,15 @@ def refresh_updates(self): "mintupdate-error-symbolic", True) self.refresh_cleanup() return - else: - self.show_updates(updates) except: print("-- Exception occurred in the refresh thread:\n%s" % traceback.format_exc()) self.logger.write_error("Exception occurred in the refresh thread: %s" % str(sys.exc_info()[0])) self.set_status(_("Could not refresh the list of updates"), _("Could not refresh the list of updates"), "mintupdate-error-symbolic", True) + self.refresh_cleanup() + return - def show_updates(self, updates): try: model_items = [] @@ -2477,52 +2222,123 @@ def _show_cinnamon_error_cb(self, title, message): return GLib.SOURCE_REMOVE - def on_apt_install_finished(self, transaction=None, exit_state=None): - needs_refresh = False - if exit_state == aptkit.enums.EXIT_SUCCESS: - self.logger.write("Install finished successfully") - # override the monitor since there's a forced refresh later already - self.cache_monitor.update_cachetime() - - if self.settings.get_boolean("hide-window-after-update"): - self.hide_window() - - if [pkg for pkg in PRIORITY_UPDATES if pkg in self.packages]: - # Restart - self.uninhibit_pm() - self.logger.write("Mintupdate was updated, restarting it...") - self.logger.close() - self.restart_app() - return + def install(self, widget): + if self.dpkg_locked(): + self.show_dpkg_lock_msg(self.ui_window) + return - # Refresh - needs_refresh = True - else: - self.logger.write_error("APT install failed") - self.set_status(_("Could not install the security updates"), _("Could not install the security updates"), "mintupdate-error-symbolic", True) + self.set_window_busy(True) + + # Find list of packages to install + install_needed = False + self.packages = [] + self.spices = [] + self.flatpaks = [] + model = self.treeview.get_model() + iter = model.get_iter_first() + while iter is not None: + if model.get_value(iter, UPDATE_CHECKED): + install_needed = True + update = model.get_value(iter, UPDATE_OBJ) + if update.type == "cinnamon": + self.spices.append(update) + self.logger.write("Will install spice " + str(update.uuid)) + iter = model.iter_next(iter) + continue + elif update.type == "flatpak": + self.flatpaks.append(update) + self.logger.write("Will install flatpak " + str(update.ref_name)) + iter = model.iter_next(iter) + continue + if update.type == "kernel": + for pkg in update.package_names: + if "-image-" in pkg: + try: + if self.is_lmde: + # In Mint, platform.release() returns the kernel version. In LMDE it returns the kernel + # abi version. So for LMDE, parse platform.version() instead. + version_string = platform.version() + kernel_version = re.search(r"(\d+\.\d+\.\d+)", version_string).group(1) + else: + kernel_version = platform.release().split("-")[0] + + if update.old_version.startswith(kernel_version): + self.reboot_required = True + except Exception as e: + print("Warning: Could not assess the current kernel version: %s" % str(e)) + self.reboot_required = True + break + if update.type == "security" and \ + [True for pkg in update.package_names if "nvidia" in pkg]: + self.reboot_required = True + for package in update.package_names: + self.packages.append(package) + self.logger.write("Will install " + str(package)) + iter = model.iter_next(iter) - self.finish_install(needs_refresh) + self.settings.set_int("install-last-run", int(time.time())) - def on_apt_install_cancelled(self): - self.logger.write("Install cancelled") - self.reboot_required = False - self.set_status("", "", "mintupdate-updates-available-symbolic", True) - self.finish_install(False) + if not install_needed: + self.set_window_busy(False) + return + + self.cache_monitor.pause() + self.inhibit_pm("Installing updates") + self.logger.write("Install requested by user") + self._do_install() @_async - def finish_install(self, refresh_needed): + def _do_install(self): + refresh_needed = False try: - # Install flatpaks - if len(self.flatpaks) > 0: + if self.packages: + self.set_status(_("Installing updates"), _("Installing updates"), + "mintupdate-installing-symbolic", True) + self.logger.write("Ready to launch aptkit") + self.apt_updater.install_packages(self.packages) + + if self.apt_updater.install_cancelled: + self.logger.write("Install cancelled") + self.reboot_required = False + self.set_status("", "", "mintupdate-updates-available-symbolic", True) + self._post_install_cleanup(refresh_needed=False) + return + + if self.apt_updater.install_error: + self.logger.write_error(f"APT install failed: {self.apt_updater.install_error}") + self.set_status(_("Could not install the security updates"), + _("Could not install the security updates"), + "mintupdate-error-symbolic", True) + self._post_install_cleanup(refresh_needed=False) + return + + self.logger.write("Install finished successfully") + # override the monitor since there's a forced refresh later already + self.cache_monitor.update_cachetime() + refresh_needed = True + + if self.settings.get_boolean("hide-window-after-update"): + self.hide_window() + + if [pkg for pkg in PRIORITY_UPDATES if pkg in self.packages]: + # Skip _post_install_cleanup — restart_app spawns a new + # instance that will take over. + self.uninhibit_pm() + self.logger.write("Mintupdate was updated, restarting it...") + self.logger.close() + self.restart_app() + return + + if self.flatpaks: self.flatpak_updater.prepare_start_updates(self.flatpaks) self.flatpak_updater.perform_updates() if self.flatpak_updater.error is not None: self.logger.write_error("Flatpak update failed %s" % self.flatpak_updater.error) refresh_needed = True - # Install spices - if len(self.spices) > 0: - self.set_status(_("Updating Cinnamon Spices"), _("Updating Cinnamon Spices"), "mintupdate-installing-symbolic", True) + if self.spices: + self.set_status(_("Updating Cinnamon Spices"), _("Updating Cinnamon Spices"), + "mintupdate-installing-symbolic", True) need_cinnamon_restart = False try: for update in self.spices: @@ -2534,90 +2350,23 @@ def finish_install(self, refresh_needed): need_cinnamon_restart = True except Exception as e: self.logger.write_error("Cinnamon spice install failed %s" % str(e)) - error_message = str(e) - error_title = _("Could not update Cinnamon Spices") - self.show_cinnamon_error(error_title, error_message) - refresh_needed = True - if need_cinnamon_restart and not self.reboot_required and os.getenv("XDG_CURRENT_DESKTOP") in ["Cinnamon", "X-Cinnamon"]: + self.show_cinnamon_error(_("Could not update Cinnamon Spices"), str(e)) + if need_cinnamon_restart and not self.reboot_required and \ + os.getenv("XDG_CURRENT_DESKTOP") in ["Cinnamon", "X-Cinnamon"]: subprocess.run(["cinnamon-dbus-command", "RestartCinnamon", "0"]) refresh_needed = True except Exception as e: - print (e) + print(e) self.logger.write_error("Exception occurred in the install thread: " + str(sys.exc_info()[0])) + self._post_install_cleanup(refresh_needed) + + def _post_install_cleanup(self, refresh_needed): self.uninhibit_pm() self.cache_monitor.resume(False) - GLib.idle_add(self.set_window_busy, False) if refresh_needed: - self.refresh(False) - - def install(self, widget): - if self.dpkg_locked(): - self.show_dpkg_lock_msg(self.ui_window) - else: - self.set_window_busy(True) - - # Find list of packages to install - install_needed = False - self.packages = [] - self.spices = [] - self.flatpaks = [] - model = self.treeview.get_model() - iter = model.get_iter_first() - while iter is not None: - if model.get_value(iter, UPDATE_CHECKED): - install_needed = True - update = model.get_value(iter, UPDATE_OBJ) - if update.type == "cinnamon": - self.spices.append(update) - self.logger.write("Will install spice " + str(update.uuid)) - iter = model.iter_next(iter) - continue - elif update.type == "flatpak": - self.flatpaks.append(update) - self.logger.write("Will install flatpak " + str(update.ref_name)) - iter = model.iter_next(iter) - continue - if update.type == "kernel": - for pkg in update.package_names: - if "-image-" in pkg: - try: - if self.is_lmde: - # In Mint, platform.release() returns the kernel version. In LMDE it returns the kernel - # abi version. So for LMDE, parse platform.version() instead. - version_string = platform.version() - kernel_version = re.search(r"(\d+\.\d+\.\d+)", version_string).group(1) - else: - kernel_version = platform.release().split("-")[0] - - if update.old_version.startswith(kernel_version): - self.reboot_required = True - except Exception as e: - print("Warning: Could not assess the current kernel version: %s" % str(e)) - self.reboot_required = True - break - if update.type == "security" and \ - [True for pkg in update.package_names if "nvidia" in pkg]: - self.reboot_required = True - for package in update.package_names: - self.packages.append(package) - self.logger.write("Will install " + str(package)) - iter = model.iter_next(iter) - self.settings.set_int("install-last-run", int(time.time())) - if install_needed: - self.cache_monitor.pause() - self.inhibit_pm("Installing updates") - self.logger.write("Install requested by user") - if len(self.packages) > 0: - self.set_status(_("Installing updates"), _("Installing updates"), "mintupdate-installing-symbolic", True) - self.logger.write("Ready to launch aptkit") - client = aptkit.simpleclient.SimpleAPTClient(self.ui_window) - client.set_finished_callback(self.on_apt_install_finished) - client.set_cancelled_callback(self.on_apt_install_cancelled) - client.install_packages(self.packages) - else: - self.finish_install(False) + self.queue_refresh(False) if __name__ == "__main__": MintUpdate() diff --git a/usr/lib/linuxmint/mintUpdate/mintupdate-cli.py b/usr/lib/linuxmint/mintUpdate/mintupdate-cli.py index d99ae5175..38ba0d901 100755 --- a/usr/lib/linuxmint/mintUpdate/mintupdate-cli.py +++ b/usr/lib/linuxmint/mintUpdate/mintupdate-cli.py @@ -7,7 +7,7 @@ import sys import traceback -from checkAPT import APTCheck +from aptUpdater import AptUpdater from Classes import PRIORITY_UPDATES if __name__ == "__main__": @@ -41,7 +41,8 @@ def is_blacklisted(blacklisted_packages, source_name, version): try: if args.refresh_cache: subprocess.run("sudo /usr/bin/mint-refresh-cache", shell=True) - check = APTCheck() + check = AptUpdater() + check.load_cache() check.find_changes() blacklisted = [] diff --git a/usr/lib/linuxmint/mintUpdate/preferences.py b/usr/lib/linuxmint/mintUpdate/preferences.py new file mode 100644 index 000000000..faac243f5 --- /dev/null +++ b/usr/lib/linuxmint/mintUpdate/preferences.py @@ -0,0 +1,296 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +import gettext +import json +import os +import subprocess +import tempfile + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from xapp.GSettingsWidgets import * + +_ = gettext.gettext + +BLACKLIST_PKG_NAME = 0 + +with open("/usr/share/linuxmint/mintupdate/automation/index.json") as f: + AUTOMATIONS = json.load(f) + + +class PreferencesWindow: + def __init__(self, parent, show_automation=False): + self.parent = parent + self.settings = parent.settings + + gladefile = "/usr/share/linuxmint/mintupdate/preferences.ui" + builder = Gtk.Builder() + builder.set_translation_domain("mintupdate") + builder.add_from_file(gladefile) + self.window = builder.get_object("main_window") + self.window.set_transient_for(parent.ui_window) + self.window.set_title(_("Preferences")) + self.window.set_icon_name("mintupdate") + + # Add mintupdate style class for easier theming + parent.ui_window.get_style_context().add_class('mintupdate') + + switch_container = builder.get_object("switch_container") + stack = Gtk.Stack() + stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) + stack.set_transition_duration(150) + stack_switcher = Gtk.StackSwitcher() + stack_switcher.set_stack(stack) + switch_container.pack_start(stack_switcher, True, True, 0) + stack_switcher.set_halign(Gtk.Align.CENTER) + + page_holder = builder.get_object("page_container") + page_holder.add(stack) + + stack.add_titled(builder.get_object("page_options"), "page_options", _("Options")) + stack.add_titled(builder.get_object("page_blacklist"), "page_blacklist", _("Packages")) + stack.add_titled(builder.get_object("page_auto"), "page_auto", _("Automation")) + + self._build_options_page(builder) + self._build_update_types(builder) + self._build_blacklist(builder) + self._build_automation(builder) + + self.window.show_all() + + if show_automation: + stack.set_visible_child_name("page_auto") + + def _build_options_page(self, builder): + box = builder.get_object("page_options") + page = SettingsPage() + box.pack_start(page, True, True, 0) + section = page.add_section(_("Interface")) + section.add_row(GSettingsSwitch(_("Hide the update manager after applying updates"), + "com.linuxmint.updates", "hide-window-after-update")) + section.add_row(GSettingsSwitch(_("Only show a tray icon when updates are available or in case of errors"), + "com.linuxmint.updates", "hide-systray")) + + section = page.add_section(_("Auto-refresh")) + switch = GSettingsSwitch(_("Refresh the list of updates automatically"), + "com.linuxmint.updates", "refresh-schedule-enabled") + section.add_row(switch) + + grid = Gtk.Grid() + grid.set_row_spacing(12) + grid.set_column_spacing(12) + grid.set_margin_top(6) + grid.set_margin_bottom(6) + grid.set_margin_start(32) + grid.set_margin_end(32) + + grid.attach(Gtk.Label(label=_("days")), 1, 0, 1, 1) + grid.attach(Gtk.Label(label=_("hours")), 2, 0, 1, 1) + grid.attach(Gtk.Label(label=_("minutes")), 3, 0, 1, 1) + label = Gtk.Label(label=_("First, refresh the list of updates after:")) + label.set_justify(Gtk.Justification.LEFT) + label.set_alignment(0, 0.5) + grid.attach(label, 0, 1, 1, 1) + label = Gtk.Label(label=_("Then, refresh the list of updates every:")) + label.set_justify(Gtk.Justification.LEFT) + label.set_alignment(0, 0.5) + grid.attach(label, 0, 2, 1, 1) + + for col, key in enumerate(("refresh-days", "refresh-hours", "refresh-minutes"), start=1): + grid.attach(self._make_spin(key), col, 1, 1, 1) + for col, key in enumerate(("autorefresh-days", "autorefresh-hours", "autorefresh-minutes"), start=1): + grid.attach(self._make_spin(key), col, 2, 1, 1) + + label = Gtk.Label() + label.set_markup("%s" % _("Note: The list only gets refreshed while the Update Manager window is closed (in system tray mode).")) + grid.attach(label, 0, 3, 4, 1) + section.add_reveal_row(grid, "com.linuxmint.updates", "refresh-schedule-enabled") + + section = SettingsSection(_("Notifications")) + revealer = SettingsRevealer("com.linuxmint.updates", "refresh-schedule-enabled") + revealer.add(section) + section._revealer = revealer + page.pack_start(revealer, False, False, 0) + + switch = GSettingsSwitch(_("Only show notifications for security and kernel updates"), + "com.linuxmint.updates", "tracker-security-only") + section.add_reveal_row(switch, "com.linuxmint.updates", "tracker-disable-notifications", [False]) + switch = GSettingsSpinButton(_("Show a notification if an update has been available for (in logged-in days):"), + "com.linuxmint.updates", "tracker-max-days", mini=2, maxi=90, step=1, page=5) + section.add_reveal_row(switch, "com.linuxmint.updates", "tracker-disable-notifications", [False]) + switch = GSettingsSpinButton(_("Show a notification if an update is older than (in days):"), + "com.linuxmint.updates", "tracker-max-age", mini=2, maxi=90, step=1, page=5) + section.add_reveal_row(switch, "com.linuxmint.updates", "tracker-disable-notifications", [False]) + switch = GSettingsSpinButton(_("Don't show notifications if an update was applied in the last (in days):"), + "com.linuxmint.updates", "tracker-grace-period", mini=2, maxi=90, step=1, page=5) + section.add_reveal_row(switch, "com.linuxmint.updates", "tracker-disable-notifications", [False]) + + @staticmethod + def _make_spin(key): + ranges = { + "refresh-days": (0, 99, 2), + "refresh-hours": (0, 23, 5), + "refresh-minutes": (0, 59, 10), + "autorefresh-days": (0, 99, 2), + "autorefresh-hours": (0, 23, 5), + "autorefresh-minutes": (0, 59, 10), + } + mini, maxi, page_step = ranges[key] + spin = GSettingsSpinButton("", "com.linuxmint.updates", key, + mini=mini, maxi=maxi, step=1, page=page_step) + spin.set_spacing(0) + spin.set_margin_start(0) + spin.set_margin_end(0) + spin.set_border_width(0) + return spin + + def _build_update_types(self, builder): + box = builder.get_object("update_types_box") + page = SettingsPage() + box.pack_start(page, True, True, 0) + + if os.path.exists("/usr/bin/cinnamon") or os.path.exists("/usr/bin/flatpak"): + section = page.add_section(_("Update types"), _("In addition to system packages, check for:")) + if os.path.exists("/usr/bin/cinnamon"): + section.add_row(GSettingsSwitch(_("Cinnamon spice updates"), + "com.linuxmint.updates", "show-cinnamon-updates")) + if os.path.exists("/usr/bin/flatpak"): + section.add_row(GSettingsSwitch(_("Flatpak updates"), + "com.linuxmint.updates", "show-flatpak-updates")) + box.show_all() + else: + box.set_no_show_all(True) + box.hide() + + def _build_blacklist(self, builder): + treeview = builder.get_object("treeview_blacklist") + column = Gtk.TreeViewColumn(_("Ignored Updates"), Gtk.CellRendererText(), text=BLACKLIST_PKG_NAME) + column.set_sort_column_id(BLACKLIST_PKG_NAME) + column.set_resizable(True) + treeview.append_column(column) + treeview.set_headers_clickable(True) + treeview.set_reorderable(False) + treeview.show() + model = Gtk.TreeStore(str) + model.set_sort_column_id(BLACKLIST_PKG_NAME, Gtk.SortType.ASCENDING) + treeview.set_model(model) + for ignored_pkg in self.settings.get_strv("blacklisted-packages"): + iter = model.insert_before(None, None) + model.set_value(iter, BLACKLIST_PKG_NAME, ignored_pkg) + builder.get_object("button_add").connect("clicked", self._add_blacklisted_package, treeview) + builder.get_object("button_remove").connect("clicked", self._remove_blacklisted_package, treeview) + builder.get_object("button_add").set_always_show_image(True) + builder.get_object("button_remove").set_always_show_image(True) + + def _build_automation(self, builder): + box = builder.get_object("page_auto_inner") + page = SettingsPage() + box.pack_start(page, True, True, 0) + + section = page.add_section(_("Package Updates"), _("Performed as root on a daily basis")) + autoupgrade_switch = Switch(_("Apply updates automatically")) + autoupgrade_switch.content_widget.set_active(os.path.isfile(AUTOMATIONS["upgrade"][2])) + autoupgrade_switch.content_widget.connect("notify::active", self._set_auto_upgrade) + section.add_row(autoupgrade_switch) + button = Gtk.Button(label=_("Export blacklist to /etc/mintupdate.blacklist")) + button.set_margin_start(20) + button.set_margin_end(20) + button.set_border_width(5) + button.set_tooltip_text(_("Click this button to make automatic updates use your current blacklist.")) + button.connect("clicked", self._export_blacklist) + section.add_row(button) + + additional_options = [] + if os.path.exists("/usr/bin/cinnamon"): + additional_options.append(GSettingsSwitch(_("Update Cinnamon spices automatically"), + "com.linuxmint.updates", "auto-update-cinnamon-spices")) + if os.path.exists("/usr/bin/flatpak"): + additional_options.append(GSettingsSwitch(_("Update Flatpaks automatically"), + "com.linuxmint.updates", "auto-update-flatpaks")) + if additional_options: + section = page.add_section(_("Other Updates"), _("Performed when you log in")) + for switch in additional_options: + section.add_row(switch) + + section = page.add_section(_("Automatic Maintenance"), _("Performed as root on a weekly basis")) + autoremove_switch = Switch(_("Remove obsolete kernels and dependencies")) + autoremove_switch.content_widget.set_active(os.path.isfile(AUTOMATIONS["autoremove"][2])) + autoremove_switch.content_widget.connect("notify::active", self._set_auto_remove) + section.add_row(autoremove_switch) + section.add_note(_("This option always leaves at least one older kernel installed and never removes manually installed kernels.")) + + def _export_blacklist(self, widget): + filename = os.path.join(tempfile.gettempdir(), "mintUpdate/blacklist") + blacklist = self.settings.get_strv("blacklisted-packages") + with open(filename, "w") as f: + f.write("\n".join(blacklist) + "\n") + subprocess.run(["pkexec", "/usr/bin/mintupdate-automation", "blacklist", "enable"]) + + def _set_auto_upgrade(self, widget, param): + self._toggle_automation(widget, "upgrade") + + def _set_auto_remove(self, widget, param): + self._toggle_automation(widget, "autoremove") + + @staticmethod + def _toggle_automation(widget, automation_id): + touchfile = AUTOMATIONS[automation_id][2] + exists = os.path.isfile(touchfile) + action = None + if widget.get_active() and not exists: + action = "enable" + elif not widget.get_active() and exists: + action = "disable" + if action: + subprocess.run(["pkexec", "/usr/bin/mintupdate-automation", automation_id, action]) + if widget.get_active() != os.path.isfile(touchfile): + widget.set_active(not widget.get_active()) + + def _save_blacklist(self, treeview): + blacklist = [] + model = treeview.get_model() + iter = model.get_iter_first() + while iter is not None: + blacklist.append(model.get_value(iter, BLACKLIST_PKG_NAME)) + iter = model.iter_next(iter) + self.settings.set_strv("blacklisted-packages", blacklist) + + def _add_blacklisted_package(self, widget, treeview): + dialog = Gtk.MessageDialog(self.window, + Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, + Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK, None) + dialog.set_markup(_("Please specify the source package name of the update to ignore (wildcards are supported) and optionally the version:")) + dialog.set_title(_("Ignore an Update")) + dialog.set_icon_name("mintupdate") + grid = Gtk.Grid() + grid.set_column_spacing(5) + grid.set_row_spacing(5) + grid.set_halign(Gtk.Align.CENTER) + name_entry = Gtk.Entry() + version_entry = Gtk.Entry() + grid.attach(Gtk.Label(label=_("Name:")), 0, 0, 1, 1) + grid.attach(name_entry, 1, 0, 1, 1) + grid.attach(Gtk.Label(label=_("Version:")), 0, 1, 1, 1) + grid.attach(version_entry, 1, 1, 1, 1) + grid.attach(Gtk.Label(label=_("(optional)")), 2, 1, 1, 1) + dialog.get_content_area().add(grid) + dialog.show_all() + if dialog.run() == Gtk.ResponseType.OK: + name = name_entry.get_text().strip() + version = version_entry.get_text().strip() + if name: + pkg = "%s=%s" % (name, version) if version else name + model = treeview.get_model() + iter = model.insert_before(None, None) + model.set_value(iter, BLACKLIST_PKG_NAME, pkg) + dialog.destroy() + self._save_blacklist(treeview) + + def _remove_blacklisted_package(self, widget, treeview): + selection = treeview.get_selection() + (model, iter) = selection.get_selected() + if iter is not None: + model.remove(iter) + self._save_blacklist(treeview) diff --git a/usr/share/glib-2.0/schemas/com.linuxmint.updates.gschema.xml b/usr/share/glib-2.0/schemas/com.linuxmint.updates.gschema.xml index a766cde2e..3cefa2f8b 100644 --- a/usr/share/glib-2.0/schemas/com.linuxmint.updates.gschema.xml +++ b/usr/share/glib-2.0/schemas/com.linuxmint.updates.gschema.xml @@ -31,11 +31,6 @@ - - 0 - - - 0 diff --git a/usr/share/linuxmint/mintupdate/information.ui b/usr/share/linuxmint/mintupdate/information.ui index 2626520ee..790974043 100644 --- a/usr/share/linuxmint/mintupdate/information.ui +++ b/usr/share/linuxmint/mintupdate/information.ui @@ -1,89 +1,118 @@ - + + + True + False + xsi-edit-copy-symbolic + True - False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - center - 640 - 480 - mintupdate - - - + center + 640 + 480 + mintupdate True - False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 12 + 12 vertical 12 True - False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK vertical 12 + True - False - 2 - 6 + False + 2 + 6 True - False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK start Process ID: - 0 - 0 + 0 + 0 True - False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK start - 1 - 0 + 1 + 0 True - False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK start Log file: - 0 - 1 + 0 + 1 True - False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK start - 1 - 1 + 1 + 1 + + + True + False + True + start + image1 + none + + + 2 + 1 + + + + + + + + + + + + + + False @@ -94,19 +123,19 @@ True - True + True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - etched-in + etched-in - 600 - 248 + 600 + 248 True - True + True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 1 + 1 False - 3 + 3 True @@ -127,15 +156,15 @@ True - False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - end + end Close True - True - True + True + True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK