diff --git a/qui/tray/updates.py b/qui/tray/updates.py
index 65aa268..f9f87cf 100644
--- a/qui/tray/updates.py
+++ b/qui/tray/updates.py
@@ -127,43 +127,44 @@ def check_vms_needing_update(self):
self.obsolete_vms.clear()
for vm in self.qapp.domains:
try:
- updates_available = vm.features.get('updates-available', False)
+ updated: bool = qui.utils.check_update(vm)
+ supported: bool = qui.utils.check_support(vm)
except exc.QubesDaemonCommunicationError:
- updates_available = False
- if updates_available and \
- (getattr(vm, 'updateable', False) or vm.klass == 'AdminVM'):
+ continue
+ if not updated:
self.vms_needing_update.add(vm)
- try:
- supported = qui.utils.check_support(vm)
- except exc.QubesDaemonCommunicationError:
- supported = True
if not supported:
self.obsolete_vms.add(vm.name)
def connect_events(self):
self.dispatcher.add_handler('domain-feature-set:updates-available',
- self.feature_set)
+ self.feature_change)
self.dispatcher.add_handler('domain-feature-delete:updates-available',
- self.feature_unset)
+ self.feature_change)
+ self.dispatcher.add_handler('domain-feature-set:skip-update',
+ self.feature_change)
+ self.dispatcher.add_handler('domain-feature-delete:skip-update',
+ self.feature_change)
self.dispatcher.add_handler('domain-add', self.domain_added)
self.dispatcher.add_handler('domain-delete', self.domain_removed)
self.dispatcher.add_handler('domain-feature-set:os-eol',
- self.feature_set)
+ self.feature_change)
- def domain_added(self, _submitter, _event, vm, *_args, **_kwargs):
+ def domain_added(self, _submitter, _event, vmname, *_args, **_kwargs):
try:
- vm_object = self.qapp.domains[vm]
+ vm = self.qapp.domains[vmname]
+ updated: bool = qui.utils.check_update(vm)
+ supported: bool = qui.utils.check_support(vm)
+ except exc.QubesDaemonCommunicationError:
+ return
except exc.QubesException:
# a disposableVM crashed on start
return
- try:
- updates_available = vm_object.features.get(
- 'updates-available', False)
- except exc.QubesDaemonCommunicationError:
- updates_available = False
- if updates_available and (getattr(vm_object, 'updateable', False) or
- vm_object.klass == 'AdminVM'):
- self.vms_needing_update.add(vm_object.name)
+ if not updated:
+ self.vms_needing_update.add(vm.name)
+ self.update_indicator_state()
+ if not supported:
+ self.obsolete_vms.add(vm)
self.update_indicator_state()
def domain_removed(self, _submitter, _event, vm, *_args, **_kwargs):
@@ -174,34 +175,27 @@ def domain_removed(self, _submitter, _event, vm, *_args, **_kwargs):
self.obsolete_vms.remove(vm)
self.update_indicator_state()
- def feature_unset(self, vm, event, feature, **_kwargs):
+ def feature_change(self, vm, event, feature, **_kwargs):
# pylint: disable=unused-argument
- if vm in self.vms_needing_update:
- self.vms_needing_update.remove(vm)
- self.update_indicator_state()
+ try:
+ updated: bool = qui.utils.check_update(vm)
+ supported: bool = qui.utils.check_support(vm)
+ except exc.QubesDaemonCommunicationError:
+ return
- def feature_set(self, vm, event, feature, value, **_kwargs):
- # pylint: disable=unused-argument
- if feature == 'updates-available':
- if value and vm not in self.vms_needing_update and\
- getattr(vm, 'updateable', False):
- self.vms_needing_update.add(vm)
+ if not updated and vm not in self.vms_needing_update:
+ self.vms_needing_update.add(vm)
+ notification = Gio.Notification.new(
+ _("New updates are available for {}.").format(vm.name))
+ notification.set_priority(Gio.NotificationPriority.NORMAL)
+ self.send_notification(None, notification)
+ elif updated and vm in self.vms_needing_update:
+ self.vms_needing_update.remove(vm)
- notification = Gio.Notification.new(
- _("New updates are available for {}.").format(vm.name))
- notification.set_priority(Gio.NotificationPriority.NORMAL)
- self.send_notification(None, notification)
- elif not value and vm in self.vms_needing_update:
- self.vms_needing_update.remove(vm)
- elif feature == 'os-eol':
- try:
- supported = qui.utils.check_support(vm)
- except exc.QubesDaemonCommunicationError:
- supported = True
- if supported and vm.name in self.obsolete_vms:
- self.obsolete_vms.remove(vm.name)
- elif not supported and vm.name not in self.obsolete_vms:
- self.obsolete_vms.add(vm.name)
+ if not supported and vm not in self.obsolete_vms:
+ self.obsolete_vms.add(vm.name)
+ elif supported and vm in self.obsolete_vms:
+ self.obsolete_vms.remove(vm.name)
self.update_indicator_state()
diff --git a/qui/updater/intro_page.py b/qui/updater/intro_page.py
index 47cd311..3e6dcb0 100644
--- a/qui/updater/intro_page.py
+++ b/qui/updater/intro_page.py
@@ -94,12 +94,30 @@ def populate_vm_list(self, qapp, settings):
for vm in qapp.domains:
if vm.klass == 'AdminVM':
try:
+ if settings.hide_skipped and bool(vm.features.get( \
+ 'skip-update', False)):
+ continue
state = bool(vm.features.get('updates-available', False))
except exc.QubesDaemonCommunicationError:
state = False
self.list_store.append_vm(vm, state)
+ to_update=set()
+ if settings.hide_updated:
+ cmd = ['qubes-vm-update', '--quiet', '--dry-run',
+ '--update-if-stale', str(settings.update_if_stale)]
+ to_update = self._get_stale_qubes(cmd)
+
for vm in qapp.domains:
+ try:
+ if settings.hide_skipped and bool(vm.features.get( \
+ 'skip-update', False)):
+ continue
+ if settings.hide_updated and not vm.name in to_update:
+ # TODO: Make re-filtering possible without App restart
+ continue
+ except exc.QubesDaemonCommunicationError:
+ continue
if getattr(vm, 'updateable', False) and vm.klass != 'AdminVM':
self.list_store.append_vm(vm)
diff --git a/qui/updater/tests/conftest.py b/qui/updater/tests/conftest.py
index 2025881..16d9cc0 100644
--- a/qui/updater/tests/conftest.py
+++ b/qui/updater/tests/conftest.py
@@ -61,6 +61,9 @@ def test_qapp_impl():
add_dom0_feature(qapp, 'gui-default-allow-utf8-titles', '')
add_dom0_feature(qapp, 'gui-default-trayicon-mode', '')
add_dom0_feature(qapp, 'qubes-vm-update-update-if-stale', None)
+ add_dom0_feature(qapp, 'skip-update', None)
+ add_dom0_feature(qapp, 'qubes-vm-update-hide-skipped', None)
+ add_dom0_feature(qapp, 'qubes-vm-update-hide-updated', None)
# setup labels
qapp.expected_calls[('dom0', 'admin.label.List', None, None)] = \
@@ -140,6 +143,7 @@ def test_qapp_impl():
add_feature_to_all(qapp, 'servicevm',
['sys-usb', 'sys-firewall', 'sys-net'])
add_feature_to_all(qapp, 'os-eol', [])
+ add_feature_to_all(qapp, 'skip-update', [])
return qapp
@@ -254,6 +258,8 @@ def __init__(self):
self.restart_service_vms = True
self.restart_other_vms = True
self.max_concurrency = None
+ self.hide_skipped = True
+ self.hide_updated = False
return MockSettings()
diff --git a/qui/updater/updater.py b/qui/updater/updater.py
index 47443f3..3075ff1 100644
--- a/qui/updater/updater.py
+++ b/qui/updater/updater.py
@@ -152,6 +152,7 @@ def perform_setup(self, *_args, **_kwargs):
self.log,
refresh_callback=self.intro_page.refresh_update_list,
overrides=overrides,
+ advanced=self.cliargs.show_advanced_settings
)
headers = [(3, "intro_name"), (3, "progress_name"), (3, "summary_name"),
@@ -418,6 +419,12 @@ def parse_args(args, app):
'Interaction will be required in the event '
'of an update error.')
+ parser.add_argument('--show-advanced-settings', action='store_true',
+ help='Show setting options for filtering-out already '
+ 'updated or skipped VMs from VM selection list. '
+ 'These options require application restart to '
+ 'take effect.')
+
args = parser.parse_args(args)
args.non_default_select = {
diff --git a/qui/updater/updater_settings.py b/qui/updater/updater_settings.py
index f80a65f..e8ebcac 100644
--- a/qui/updater/updater_settings.py
+++ b/qui/updater/updater_settings.py
@@ -53,6 +53,8 @@ class Settings:
MAX_UPDATE_IF_STALE = 99
DEFAULT_RESTART_SERVICEVMS = True
DEFAULT_RESTART_OTHER_VMS = False
+ DEFAULT_HIDE_SKIPPED = True
+ DEFAULT_HIDE_UPDATED = False
def __init__(
self,
@@ -61,6 +63,7 @@ def __init__(
log,
refresh_callback: Callable,
overrides: OverriddenSettings = OverriddenSettings(),
+ advanced: Optional[bool] = False
):
self.qapp = qapp
self.log = log
@@ -78,6 +81,14 @@ def __init__(
self.settings_window: Gtk.Window = self.builder.get_object(
"main_window")
+
+ # Filtering options for advanced users
+ self.builder.get_object("filtering_options").set_visible(advanced)
+ self.builder.get_object("hide_skipped").set_visible(advanced)
+ self.builder.get_object("hide_updated").set_visible(advanced)
+ self.settings_window.set_default_size(
+ self.settings_window.get_size().width, 640 if advanced else 550)
+
self.settings_window.set_transient_for(main_window)
self.settings_window.connect("delete-event", self.close_without_saving)
@@ -108,6 +119,12 @@ def __init__(
self.restart_other_checkbox.connect(
"toggled", self._show_restart_exceptions)
+ self.hide_skipped_checkbox: Gtk.CheckButton = \
+ self.builder.get_object("hide_skipped")
+
+ self.hide_updated_checkbox: Gtk.CheckButton = \
+ self.builder.get_object("hide_updated")
+
self.available_vms = [
vm for vm in self.qapp.domains
if vm.klass == 'DispVM' and not vm.auto_cleanup
@@ -138,6 +155,8 @@ def __init__(
self._init_restart_other_vms: Optional[bool] = None
self._init_limit_concurrency: Optional[bool] = None
self._init_max_concurrency: Optional[int] = None
+ self._init_hide_skipped: Optional[bool] = None
+ self._init_hide_updated: Optional[bool] = None
@property
def update_if_stale(self) -> int:
@@ -176,6 +195,18 @@ def restart_other_vms(self) -> bool:
self.vm, "qubes-vm-update-restart-other",
Settings.DEFAULT_RESTART_OTHER_VMS)
+ @property
+ def hide_skipped(self) -> bool:
+ return get_boolean_feature(
+ self.vm, "qubes-vm-update-hide-skipped",
+ Settings.DEFAULT_HIDE_SKIPPED)
+
+ @property
+ def hide_updated(self) -> bool:
+ return get_boolean_feature(
+ self.vm, "qubes-vm-update-hide-updated",
+ Settings.DEFAULT_HIDE_UPDATED)
+
@property
def max_concurrency(self) -> Optional[int]:
"""Return the current (set by this window or manually) option value."""
@@ -210,6 +241,11 @@ def load_settings(self):
if self._init_limit_concurrency:
self.max_concurrency_button.set_value(self._init_max_concurrency)
+ self._init_hide_skipped = self.hide_skipped
+ self._init_hide_updated = self.hide_updated
+ self.hide_skipped_checkbox.set_active(self._init_hide_skipped)
+ self.hide_updated_checkbox.set_active(self._init_hide_updated)
+
def _show_restart_exceptions(self, _emitter=None):
if self.restart_other_checkbox.get_active():
self.restart_exceptions_page.show_all()
@@ -226,7 +262,7 @@ def _limit_concurrency_toggled(self, _emitter=None):
def show(self):
"""Show a hidden window."""
self.load_settings()
- self.settings_window.show_all()
+ self.settings_window.show()
self._show_restart_exceptions()
self._limit_concurrency_toggled()
@@ -262,6 +298,20 @@ def save_and_close(self, _emitter):
default=Settings.DEFAULT_RESTART_OTHER_VMS
)
+ self._save_option(
+ name="hide-skipped",
+ value=self.hide_skipped_checkbox.get_active(),
+ init=self._init_hide_skipped,
+ default=Settings.DEFAULT_HIDE_SKIPPED
+ )
+
+ self._save_option(
+ name="hide-updated",
+ value=self.hide_updated_checkbox.get_active(),
+ init=self._init_hide_updated,
+ default=Settings.DEFAULT_HIDE_UPDATED
+ )
+
limit_concurrency = self.limit_concurrency_checkbox.get_active()
if self._init_limit_concurrency or limit_concurrency:
if limit_concurrency:
diff --git a/qui/updater_settings.glade b/qui/updater_settings.glade
index 1a18156..0a6a264 100644
--- a/qui/updater_settings.glade
+++ b/qui/updater_settings.glade
@@ -9,7 +9,7 @@
Qubes OS Updater Settings
False
458
- 571
+ 640
+
+
+
+ False
+ True
+ 11
+
+
+
+
+
+ False
+ True
+ 12
+
+
+
+
+
+ False
+ True
+ 13
+
+
diff --git a/qui/utils.py b/qui/utils.py
index 0a60d95..5c56f5f 100644
--- a/qui/utils.py
+++ b/qui/utils.py
@@ -80,11 +80,25 @@ def run_asyncio_and_show_errors(loop, tasks, name, restart=True):
exit_code = 1
return exit_code
+def check_update(vm) -> bool:
+ """Return true if the given template/standalone vm is updated or not
+ updateable or skipped. default returns true"""
+ if not vm.features.get('updates-available', False):
+ return True
+ if not getattr(vm, 'updateable', False):
+ return True
+ if bool(vm.features.get('skip-update', False)):
+ return True
+ return False
-def check_support(vm):
+def check_support(vm) -> bool:
"""Return true if the given template/standalone vm is still supported, by
default returns true"""
- # first, check if qube itself has known eol
+ # first, we skip VMs with `skip-update` feature set to true
+ if bool(vm.features.get('skip-update', False)):
+ return True
+
+ # next, check if qube itself has known eol
eol_string: str = vm.features.get('os-eol', '')
if not eol_string: