Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 9029 #224

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 40 additions & 46 deletions qui/tray/updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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()

Expand Down
18 changes: 18 additions & 0 deletions qui/updater/intro_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,30 @@
for vm in qapp.domains:
if vm.klass == 'AdminVM':
try:
if settings.hide_skipped and bool(vm.features.get( \
'skip-update', False)):
continue

Check warning on line 99 in qui/updater/intro_page.py

View check run for this annotation

Codecov / codecov/patch

qui/updater/intro_page.py#L99

Added line #L99 was not covered by tests
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',

Check warning on line 107 in qui/updater/intro_page.py

View check run for this annotation

Codecov / codecov/patch

qui/updater/intro_page.py#L107

Added line #L107 was not covered by tests
'--update-if-stale', str(settings.update_if_stale)]
to_update = self._get_stale_qubes(cmd)

Check warning on line 109 in qui/updater/intro_page.py

View check run for this annotation

Codecov / codecov/patch

qui/updater/intro_page.py#L109

Added line #L109 was not covered by tests

for vm in qapp.domains:
try:
if settings.hide_skipped and bool(vm.features.get( \
'skip-update', False)):
continue

Check warning on line 115 in qui/updater/intro_page.py

View check run for this annotation

Codecov / codecov/patch

qui/updater/intro_page.py#L115

Added line #L115 was not covered by tests
if settings.hide_updated and not vm.name in to_update:
# TODO: Make re-filtering possible without App restart
continue
except exc.QubesDaemonCommunicationError:
continue

Check warning on line 120 in qui/updater/intro_page.py

View check run for this annotation

Codecov / codecov/patch

qui/updater/intro_page.py#L118-L120

Added lines #L118 - L120 were not covered by tests
if getattr(vm, 'updateable', False) and vm.klass != 'AdminVM':
self.list_store.append_vm(vm)

Expand Down
6 changes: 6 additions & 0 deletions qui/updater/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)] = \
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()

Expand Down
7 changes: 7 additions & 0 deletions qui/updater/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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 = {
Expand Down
52 changes: 51 additions & 1 deletion qui/updater/updater_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -61,6 +63,7 @@ def __init__(
log,
refresh_callback: Callable,
overrides: OverriddenSettings = OverriddenSettings(),
advanced: Optional[bool] = False
):
self.qapp = qapp
self.log = log
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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()
Expand 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()

Expand Down Expand Up @@ -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:
Expand Down
68 changes: 67 additions & 1 deletion qui/updater_settings.glade
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<property name="title" translatable="yes">Qubes OS Updater Settings</property>
<property name="resizable">False</property>
<property name="default-width">458</property>
<property name="default-height">571</property>
<property name="default-height">640</property>
<child>
<!-- n-columns=3 n-rows=2 -->
<object class="GtkGrid">
Expand Down Expand Up @@ -495,6 +495,72 @@
<property name="position">10</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="filtering_options">
<property name="label" translatable="yes">Filtering Options</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="margin-top">18</property>
<property name="hexpand">True</property>
<property name="use-markup">True</property>
<style>
<class name="section_title"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">11</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="hide_skipped">
<property name="label" translatable="yes">Hide qubes with 'skip-update' feature from selection page.</property>
<property name="tooltip-text" translatable="yes">Requires application restart to take effect.</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="margin-top">5</property>
<property name="use-underline">True</property>
<property name="active">True</property>
<property name="draw-indicator">True</property>
<style>
<class name="explanation_text"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">12</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="hide_updated">
<property name="label" translatable="yes">Hide already updated qubes from selection page.</property>
<property name="tooltip-text" translatable="yes">Requires application restart to take effect.</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="margin-top">5</property>
<property name="use-underline">True</property>
<property name="active">False</property>
<property name="draw-indicator">True</property>
<style>
<class name="explanation_text"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">13</property>
</packing>
</child>
</object>
</child>
</object>
Expand Down
Loading