From 4478671fead8887e228b936b08ee7e747d830918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Tue, 2 Jun 2026 17:01:27 +0200 Subject: [PATCH 1/2] Fix interrupting update When SIGINT is sent during last qube in a given group (admin, template/standalone, derived), given group isn't really interrupted, so none of the returned update status is FinalStatus.CANCELLED. This meant that update cancel request was canceled in practice and the update proceeded to the next group uninterrupted. And also that overall exit code didn't inform about the cancellation request. Fix this by returning EXIT.SIGINT if SIGINT was received, instead of checking if any update was actually cancelled. And then check for the EXIT.SIGINT status between update groups. Fixes QubesOS/qubes-issues#10900 --- vmupdate/update_manager.py | 4 ++++ vmupdate/vmupdate.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/vmupdate/update_manager.py b/vmupdate/update_manager.py index fdabe4e..0c11779 100644 --- a/vmupdate/update_manager.py +++ b/vmupdate/update_manager.py @@ -112,6 +112,10 @@ def run(self, agent_args): progress_bar.close() self.log.info("Update Manager: Finished, collecting success info") + # inform caller about requested cancel, even if all requested targets were updated + if progress_bar.termination.value: + self.ret_code = EXIT.SIGINT + stats = list(progress_bar.statuses.values()) if FinalStatus.CANCELLED in stats: self.ret_code = max(self.ret_code, EXIT.SIGINT) diff --git a/vmupdate/vmupdate.py b/vmupdate/vmupdate.py index d9fa476..1f96802 100644 --- a/vmupdate/vmupdate.py +++ b/vmupdate/vmupdate.py @@ -93,6 +93,8 @@ def main(args=None, app=qubesadmin.Qubes()): no_updates = all( stat == FinalStatus.NO_UPDATES for stat in admin_status.values() ) + if ret_code_admin == EXIT.SIGINT: + return EXIT.SIGINT # independent qubes first (TemplateVMs, StandaloneVMs) ret_code_independent, templ_statuses = run_update( @@ -102,12 +104,16 @@ def main(args=None, app=qubesadmin.Qubes()): all(stat == FinalStatus.NO_UPDATES for stat in templ_statuses.values()) and no_updates ) + if ret_code_independent == EXIT.SIGINT: + return EXIT.SIGINT # then derived qubes (AppVMs...) ret_code_appvm, app_statuses = run_update(derived, args, log) no_updates = ( all(stat == FinalStatus.NO_UPDATES for stat in app_statuses.values()) and no_updates ) + if ret_code_appvm == EXIT.SIGINT: + return EXIT.SIGINT ret_code_restart = apply_updates_to_appvm( args, independent, templ_statuses, app_statuses, log From 954eb3d9e6ceb6ed417879c1c4912097809d3061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Thu, 4 Jun 2026 01:26:00 +0200 Subject: [PATCH 2/2] tests: adjust for fixed cancellation Now that canceling updates actually prevents starting further updates, test that has some cancelled qubes didn't finish all expected updates. Adjust the test to not generate cancelled statuses unless explicitly expected. --- vmupdate/tests/conftest.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/vmupdate/tests/conftest.py b/vmupdate/tests/conftest.py index f6401fb..e881b36 100644 --- a/vmupdate/tests/conftest.py +++ b/vmupdate/tests/conftest.py @@ -145,7 +145,7 @@ def run_agent(self, agent_args, status_notifier, termination): return closure -def generate_vm_variations(app, variations): +def generate_vm_variations(app, variations, include_cancelled=False): """ Generate all possible variations of vms for the given list of features. """ @@ -182,15 +182,27 @@ def generate_vm_variations(app, variations): FinalStatus.SUCCESS: set(), FinalStatus.NO_UPDATES: set(), FinalStatus.ERROR: set(), - FinalStatus.CANCELLED: set(), - }, + } + | ( + { + FinalStatus.CANCELLED: set(), + } + if include_cancelled + else {} + ), "has_template_updated": { FinalStatus.SUCCESS: set(), FinalStatus.NO_UPDATES: set(), FinalStatus.ERROR: set(), - FinalStatus.CANCELLED: set(), FinalStatus.UNKNOWN: set(), - }, + } + | ( + { + FinalStatus.CANCELLED: set(), + } + if include_cancelled + else {} + ), } klasses = list(reversed(sorted(list(domains["klass"].keys()))))