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 @@
-
+
+