From 5dd6b106bc63b3234fbf42e3a32d765740bff3da Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Mon, 12 Jan 2026 09:33:37 +0100 Subject: [PATCH 1/4] Order GUI extension methods --- qubes/ext/gui.py | 132 +++++++++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/qubes/ext/gui.py b/qubes/ext/gui.py index 01027c66b..1f9f6619a 100644 --- a/qubes/ext/gui.py +++ b/qubes/ext/gui.py @@ -35,18 +35,6 @@ def attached_vms(vm): if getattr(domain, "guivm", None) and domain.guivm == vm: yield domain - @qubes.ext.handler("domain-pre-shutdown") - def on_domain_pre_shutdown(self, vm, event, **kwargs): - attached_vms = [ - domain for domain in self.attached_vms(vm) if domain.is_running() - ] - if attached_vms and not kwargs.get("force", False): - raise qubes.exc.QubesVMError( - self, - "There are running VMs using this VM as GuiVM: " - "{}".format(", ".join(vm.name for vm in attached_vms)), - ) - @staticmethod def send_gui_mode(vm): vm.run_service( @@ -58,12 +46,6 @@ def send_gui_mode(vm): ), ) - @qubes.ext.handler("domain-init", "domain-load") - def on_domain_init_load(self, vm, event): - if getattr(vm, "guivm", None): - if "guivm-" + vm.guivm.name not in vm.tags: - self.on_property_set(vm, event, name="guivm", newvalue=vm.guivm) - @qubes.ext.handler("property-reset:guivm") def on_property_reset(self, subject, event, name, oldvalue=None): newvalue = getattr(subject, "guivm", None) @@ -82,6 +64,51 @@ def on_property_set(self, subject, event, name, newvalue, oldvalue=None): guivm = "guivm-" + newvalue.name subject.tags.add(guivm) + @qubes.ext.handler("property-set:default_guivm", system=True) + def on_property_set_default_guivm( + self, app, event, name, newvalue, oldvalue=None + ): + for vm in app.domains: + if hasattr(vm, "guivm") and vm.property_is_default("guivm"): + vm.fire_event( + "property-set:guivm", + name="guivm", + newvalue=newvalue, + oldvalue=oldvalue, + ) + + @qubes.ext.handler("property-reset:keyboard_layout") + def on_keyboard_reset(self, vm, event, name, oldvalue=None): + if not vm.is_running(): + return + kbd_layout = vm.keyboard_layout + + vm.untrusted_qdb.write("/keyboard-layout", kbd_layout) + + @qubes.ext.handler("property-set:keyboard_layout") + def on_keyboard_set(self, vm, event, name, newvalue, oldvalue=None): + if newvalue == oldvalue: + return + + if vm.is_running(): + vm.untrusted_qdb.write("/keyboard-layout", newvalue) + + for domain in vm.app.domains: + if getattr( + domain, "guivm", None + ) == vm and domain.property_is_default("keyboard_layout"): + domain.fire_event( + "property-reset:keyboard_layout", + name="keyboard_layout", + oldvalue=oldvalue, + ) + + @qubes.ext.handler("domain-init", "domain-load") + def on_domain_init_load(self, vm, event): + if getattr(vm, "guivm", None): + if "guivm-" + vm.guivm.name not in vm.tags: + self.on_property_set(vm, event, name="guivm", newvalue=vm.guivm) + @qubes.ext.handler("domain-qdb-create") def on_domain_qdb_create(self, vm, event): for feature in ("gui-videoram-overhead", "gui-videoram-min"): @@ -118,18 +145,16 @@ def on_domain_qdb_create(self, vm, event): "/guivm-windows-prefix", guivm_windows_prefix ) - @qubes.ext.handler("property-set:default_guivm", system=True) - def on_property_set_default_guivm( - self, app, event, name, newvalue, oldvalue=None - ): - for vm in app.domains: - if hasattr(vm, "guivm") and vm.property_is_default("guivm"): - vm.fire_event( - "property-set:guivm", - name="guivm", - newvalue=newvalue, - oldvalue=oldvalue, - ) + @qubes.ext.handler("domain-tag-add:created-by-*") + def set_guivm_on_created_by(self, vm, event, tag, **kwargs): + """Set GuiVM based on 'tag-created-vm-with' and 'set-created-guivm' + features + """ + # pylint: disable=unused-argument + created_by = vm.app.domains[tag.partition("created-by-")[2]] + if created_by.features.get("set-created-guivm", None): + guivm = vm.app.domains[created_by.features["set-created-guivm"]] + vm.guivm = guivm @qubes.ext.handler("domain-start") async def on_domain_start(self, vm, event, **kwargs): @@ -149,39 +174,14 @@ async def on_domain_start(self, vm, event, **kwargs): "/usr/bin/qubes-input-trigger", "--all", "--dom0" ) - @qubes.ext.handler("property-reset:keyboard_layout") - def on_keyboard_reset(self, vm, event, name, oldvalue=None): - if not vm.is_running(): - return - kbd_layout = vm.keyboard_layout - - vm.untrusted_qdb.write("/keyboard-layout", kbd_layout) - - @qubes.ext.handler("property-set:keyboard_layout") - def on_keyboard_set(self, vm, event, name, newvalue, oldvalue=None): - if newvalue == oldvalue: - return - - if vm.is_running(): - vm.untrusted_qdb.write("/keyboard-layout", newvalue) - - for domain in vm.app.domains: - if getattr( - domain, "guivm", None - ) == vm and domain.property_is_default("keyboard_layout"): - domain.fire_event( - "property-reset:keyboard_layout", - name="keyboard_layout", - oldvalue=oldvalue, - ) - - @qubes.ext.handler("domain-tag-add:created-by-*") - def set_guivm_on_created_by(self, vm, event, tag, **kwargs): - """Set GuiVM based on 'tag-created-vm-with' and 'set-created-guivm' - features - """ - # pylint: disable=unused-argument - created_by = vm.app.domains[tag.partition("created-by-")[2]] - if created_by.features.get("set-created-guivm", None): - guivm = vm.app.domains[created_by.features["set-created-guivm"]] - vm.guivm = guivm + @qubes.ext.handler("domain-pre-shutdown") + def on_domain_pre_shutdown(self, vm, event, **kwargs): + attached_vms = [ + domain for domain in self.attached_vms(vm) if domain.is_running() + ] + if attached_vms and not kwargs.get("force", False): + raise qubes.exc.QubesVMError( + self, + "There are running VMs using this VM as GuiVM: " + "{}".format(", ".join(vm.name for vm in attached_vms)), + ) From 7f7b49a89f81c49f1d2b2dc136923a05296b8c78 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Mon, 12 Jan 2026 09:54:36 +0100 Subject: [PATCH 2/4] Cleanup GUI extension to return early when viable --- qubes/ext/gui.py | 69 ++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/qubes/ext/gui.py b/qubes/ext/gui.py index 1f9f6619a..36f4a1d0d 100644 --- a/qubes/ext/gui.py +++ b/qubes/ext/gui.py @@ -32,7 +32,7 @@ class GUI(qubes.ext.Extension): @staticmethod def attached_vms(vm): for domain in vm.app.domains: - if getattr(domain, "guivm", None) and domain.guivm == vm: + if hasattr(domain, "guivm") and domain.guivm is vm: yield domain @staticmethod @@ -56,10 +56,15 @@ def on_property_set(self, subject, event, name, newvalue, oldvalue=None): # Clean other 'guivm-XXX' tags. # gui-daemon can connect to only one domain tags_list = list(subject.tags) + found = False for tag in tags_list: if tag.startswith("guivm-"): + if newvalue and tag == "guivm-" + newvalue.name: + found = True + continue subject.tags.remove(tag) - + if found: + return if newvalue: guivm = "guivm-" + newvalue.name subject.tags.add(guivm) @@ -82,32 +87,33 @@ def on_keyboard_reset(self, vm, event, name, oldvalue=None): if not vm.is_running(): return kbd_layout = vm.keyboard_layout - vm.untrusted_qdb.write("/keyboard-layout", kbd_layout) @qubes.ext.handler("property-set:keyboard_layout") def on_keyboard_set(self, vm, event, name, newvalue, oldvalue=None): if newvalue == oldvalue: return - if vm.is_running(): vm.untrusted_qdb.write("/keyboard-layout", newvalue) - - for domain in vm.app.domains: - if getattr( - domain, "guivm", None - ) == vm and domain.property_is_default("keyboard_layout"): - domain.fire_event( - "property-reset:keyboard_layout", - name="keyboard_layout", - oldvalue=oldvalue, - ) + attached_vms = [ + domain + for domain in self.attached_vms(vm) + if domain.property_is_default("keyboard_layout") + ] + for domain in attached_vms: + domain.fire_event( + "property-reset:keyboard_layout", + name="keyboard_layout", + oldvalue=oldvalue, + ) @qubes.ext.handler("domain-init", "domain-load") def on_domain_init_load(self, vm, event): - if getattr(vm, "guivm", None): - if "guivm-" + vm.guivm.name not in vm.tags: - self.on_property_set(vm, event, name="guivm", newvalue=vm.guivm) + guivm = getattr(vm, "guivm", None) + if not guivm: + return + if "guivm-" + guivm.name not in vm.tags: + self.on_property_set(vm, event, name="guivm", newvalue=guivm) @qubes.ext.handler("domain-qdb-create") def on_domain_qdb_create(self, vm, event): @@ -120,27 +126,23 @@ def on_domain_qdb_create(self, vm, event): except KeyError: pass - vm.untrusted_qdb.write( - "/qubes-gui-enabled", - str( - bool( - getattr(vm, "guivm", None) and vm.features.get("gui", True) - ) - ), - ) + guivm = getattr(vm, "guivm", None) + gui = bool(guivm and vm.features.get("gui", True)) + vm.untrusted_qdb.write("/qubes-gui-enabled", str(gui)) # Add GuiVM Xen ID for gui-daemon - if getattr(vm, "guivm", None): + if guivm: if vm != vm.guivm: vm.untrusted_qdb.write("/keyboard-layout", vm.keyboard_layout) - if vm.guivm.is_running(): vm.untrusted_qdb.write( "/qubes-gui-domain-xid", str(vm.guivm.xid) ) # Set GuiVM prefix - guivm_windows_prefix = vm.features.get("guivm-windows-prefix", "GuiVM") if vm.features.get("service.guivm", None): + guivm_windows_prefix = vm.features.get( + "guivm-windows-prefix", "GuiVM" + ) vm.untrusted_qdb.write( "/guivm-windows-prefix", guivm_windows_prefix ) @@ -152,9 +154,8 @@ def set_guivm_on_created_by(self, vm, event, tag, **kwargs): """ # pylint: disable=unused-argument created_by = vm.app.domains[tag.partition("created-by-")[2]] - if created_by.features.get("set-created-guivm", None): - guivm = vm.app.domains[created_by.features["set-created-guivm"]] - vm.guivm = guivm + if created_guivm := created_by.features.get("set-created-guivm", None): + vm.guivm = vm.app.domains[created_guivm] @qubes.ext.handler("domain-start") async def on_domain_start(self, vm, event, **kwargs): @@ -162,10 +163,8 @@ async def on_domain_start(self, vm, event, **kwargs): domain for domain in self.attached_vms(vm) if domain.is_running() ] for attached_vm in attached_vms: - attached_vm.untrusted_qdb.write( - "/qubes-gui-enabled", - str(bool(attached_vm.features.get("gui", True))), - ) + gui = bool(attached_vm.features.get("gui", True)) + attached_vm.untrusted_qdb.write("/qubes-gui-enabled", str(gui)) attached_vm.untrusted_qdb.write( "/qubes-gui-domain-xid", str(vm.xid) ) From 65f75c51d1e59532210d6a4ff458dc6dcfabce24 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 2 Apr 2026 03:43:08 +0200 Subject: [PATCH 3/4] Log preload startup failure reason --- qubes/vm/dispvm.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/qubes/vm/dispvm.py b/qubes/vm/dispvm.py index 2f8edcd4f..31ed0e917 100644 --- a/qubes/vm/dispvm.py +++ b/qubes/vm/dispvm.py @@ -506,8 +506,7 @@ async def wait_operational_preload( await asyncio.wait_for( self.run_service_for_stdio( service, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + stderr=subprocess.STDOUT, ), timeout=timeout, ) @@ -519,12 +518,13 @@ async def wait_operational_preload( "startup. To debug, run the following on a new disposable of " "'%s': %s" % (service, timeout, self.template, debug_msg) ) - except (subprocess.CalledProcessError, qubes.exc.QubesException): - debug_msg = "systemctl --failed" + except subprocess.CalledProcessError as e: raise qubes.exc.QubesException( - "Error on call to '%s' during preload startup. To debug, " - "disable preloading from '%s' and run the following on a new " - "disposable: %s" % (service, self.template, debug_msg) + "Error on call to '%s' during preload startup: %s" + % ( + service, + qubes.utils.sanitize_stderr_for_log(e.stdout), + ) ) @qubes.events.handler("domain-start") From 8376986464cf281c34d3e194d6bee5f3603a8331 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Wed, 26 Nov 2025 11:58:18 +0100 Subject: [PATCH 4/4] Wait for user session for preloaded disposables With the GUI agent patch, it can start before the GUI daemon connects, allowing the user session to complete. Wait both services to guarantee no enabled user or system service tries to start after the preload is used. Requires: https://github.com/QubesOS/qubes-gui-agent-linux/pull/251 Requires: https://github.com/QubesOS/qubes-gui-agent-linux/pull/255 Fixes: https://github.com/QubesOS/qubes-issues/issues/9940 For: https://github.com/QubesOS/qubes-issues/issues/1512 --- qubes/tests/api_admin.py | 1 + qubes/tests/app.py | 1 + qubes/tests/integ/backup.py | 1 + qubes/tests/integ/dispvm.py | 13 ++++++---- qubes/tests/vm/dispvm.py | 7 ++++++ qubes/tests/vm/mix/dvmtemplate.py | 6 +++++ qubes/vm/dispvm.py | 13 +++++++++- qubes/vm/mix/dvmtemplate.py | 40 ++++++++++++++++++------------- tests/dispvm_perf.py | 2 ++ 9 files changed, 63 insertions(+), 21 deletions(-) diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index bc14f32fe..fd9099e97 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -4022,6 +4022,7 @@ def test_643_vm_create_disposable_preload_autostart( ) self.vm.features["qrexec"] = "1" self.vm.features["supported-rpc.qubes.WaitForRunningSystem"] = "1" + self.vm.features["supported-rpc.qubes.WaitForSession"] = "1" self.vm.features["preload-dispvm-max"] = "1" for _ in range(10): if len(self.vm.get_feat_preload()) == 1: diff --git a/qubes/tests/app.py b/qubes/tests/app.py index a3ab4a4b7..09341606e 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -695,6 +695,7 @@ def setUp(self): self.template.features["supported-rpc.qubes.WaitForRunningSystem"] = ( True ) + self.template.features["supported-rpc.qubes.WaitForSession"] = True self.appvm = self.app.add_new_vm( "AppVM", name="test-dvm", diff --git a/qubes/tests/integ/backup.py b/qubes/tests/integ/backup.py index 2fcd707fa..2b4be612f 100644 --- a/qubes/tests/integ/backup.py +++ b/qubes/tests/integ/backup.py @@ -188,6 +188,7 @@ def create_backup_vms(self, pool=None): self.loop.run_until_complete(testvm5.create_on_disk(pool=pool)) testvm5.features["qrexec"] = True testvm5.features["supported-rpc.qubes.WaitForRunningSystem"] = True + testvm5.features["supported-rpc.qubes.WaitForSession"] = True testvm5.features["preload-dispvm-max"] = 0 testvm5.features["preload-dispvm"] = "" vms.append(testvm5) diff --git a/qubes/tests/integ/dispvm.py b/qubes/tests/integ/dispvm.py index 516e19e87..19413d9d5 100644 --- a/qubes/tests/integ/dispvm.py +++ b/qubes/tests/integ/dispvm.py @@ -451,7 +451,7 @@ async def run_preload_proc(self): finally: logger.info("end") - async def run_preload(self): + async def run_preload(self, assert_stdout: bool = True): logger.info("start") appvm = self.disp_base dispvm = appvm.get_feat_preload()[0] @@ -463,7 +463,8 @@ async def run_preload(self): self._test_event_handler_remove(dispvm, "domain-unpaused") stdout = await self.run_preload_proc() - self.assertEqual(stdout, dispvm_name) + if assert_stdout: + self.assertEqual(stdout, dispvm_name) test_cases = [ (False, appvm.name, "domain-preload-dispvm-start", True), (True, appvm.name, "domain-preload-dispvm-used", True), @@ -554,7 +555,9 @@ async def _test_013_preload_gui(self): self.disp_base.features["gui"] = True self.disp_base.features["preload-dispvm-max"] = str(preload_max) await self.wait_preload(preload_max) - await self.run_preload() + self.preload_cmd.insert(1, "--service") + self.preload_cmd[-1] = "qubes.WaitForSession" + await self.run_preload(assert_stdout=False) logger.info("end") def test_014_preload_nogui(self): @@ -569,7 +572,9 @@ async def _test_014_preload_nogui(self): self.disp_base.features["preload-dispvm-max"] = str(preload_max) await self.wait_preload(preload_max, wait_completion=False) self.preload_cmd.insert(1, "--no-gui") - await self.run_preload() + self.preload_cmd.insert(1, "--service") + self.preload_cmd[-1] = "qubes.WaitForRunningSystem" + await self.run_preload(assert_stdout=False) logger.info("end") @unittest.skipUnless(which("xdotool"), "xdotool not installed") diff --git a/qubes/tests/vm/dispvm.py b/qubes/tests/vm/dispvm.py index 393dc16b9..23749ea5a 100644 --- a/qubes/tests/vm/dispvm.py +++ b/qubes/tests/vm/dispvm.py @@ -155,6 +155,7 @@ def test_001_from_appvm_preload_reject_max(self, mock_storage): self.appvm.template_for_dispvms = True orig_getitem = self.app.domains.__getitem__ self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = "0" with mock.patch.object( self.app, "domains", wraps=self.app.domains @@ -187,6 +188,7 @@ def test_002_from_appvm_preload_use( self.appvm.template_for_dispvms = True self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = "1" orig_getitem = self.app.domains.__getitem__ with mock.patch.object( @@ -253,6 +255,7 @@ def test_003_from_appvm_preload_fill_gap( mock_start.side_effect = self.mock_coro self.appvm.template_for_dispvms = True self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True orig_getitem = self.app.domains.__getitem__ with mock.patch("qubes.events.Emitter.fire_event_async") as mock_events: self.appvm.features["preload-dispvm-max"] = "1" @@ -296,6 +299,7 @@ def test_003_from_appvm_preload_fill_gap( def test_004_get_preload_max(self): self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), None) self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = 1 self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), 1) self.assertEqual(qubes.vm.dispvm.get_preload_max(self.adminvm), None) @@ -312,9 +316,11 @@ def test_005_get_preload_templates(self): self.assertEqual(get_preload_templates(self.app), []) self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm_alt.features["supported-rpc.qubes.WaitForRunningSystem"] = ( True ) + self.appvm_alt.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = 1 self.appvm_alt.features["preload-dispvm-max"] = 0 self.assertEqual(get_preload_templates(self.app), [self.appvm]) @@ -670,6 +676,7 @@ def test_024_is_preload_outdated(self, _mock_makedirs, _mock_symlink): self.appvm.create_on_disk(pool="alternative") ) self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = "1" orig_getitem = self.app.domains.__getitem__ with mock.patch.object( diff --git a/qubes/tests/vm/mix/dvmtemplate.py b/qubes/tests/vm/mix/dvmtemplate.py index 9412c5300..cebc5ab72 100755 --- a/qubes/tests/vm/mix/dvmtemplate.py +++ b/qubes/tests/vm/mix/dvmtemplate.py @@ -84,6 +84,7 @@ def setUp(self): self.appvm.features["qrexec"] = True self.appvm.features["gui"] = False self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.app.domains[self.appvm.name] = self.appvm self.app.domains[self.appvm] = self.appvm self.app.default_dispvm = self.appvm @@ -163,9 +164,13 @@ def test_010_dvm_preload_get_max(self): self.appvm.features["qrexec"] = True self.appvm.features["gui"] = False self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = False + self.appvm.features["supported-rpc.qubes.WaitForSession"] = False with self.assertRaises(qubes.exc.QubesValueError): self.appvm.features["preload-dispvm-max"] = "1" self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + with self.assertRaises(qubes.exc.QubesValueError): + self.appvm.features["preload-dispvm-max"] = "1" + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = "1" cases_invalid = ["a", "-1", "1 1"] for value in cases_invalid: @@ -458,5 +463,6 @@ def test_040_dvm_preload_set_template_for_dispvms( def test_100_get_preload_templates(self): print(qubes.vm.dispvm.get_preload_templates(self.app)) self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = 1 self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), 1) diff --git a/qubes/vm/dispvm.py b/qubes/vm/dispvm.py index 31ed0e917..2a1c2879b 100644 --- a/qubes/vm/dispvm.py +++ b/qubes/vm/dispvm.py @@ -512,7 +512,10 @@ async def wait_operational_preload( ) self.log.info("Preload startup completed '%s'", service) except asyncio.TimeoutError: - debug_msg = "systemd-analyze blame" + if service == "qubes.WaitForSession": + debug_msg = "systemd-analyze --user blame" + else: + debug_msg = "systemd-analyze blame" raise qubes.exc.QubesException( "Timed out call to '%s' after '%d' seconds during preload " "startup. To debug, run the following on a new disposable of " @@ -546,6 +549,14 @@ async def on_domain_started_dispvm( if not self.preload_requested: timeout = self.qrexec_timeout services = ["qubes.WaitForRunningSystem"] + if ( + self.guivm + and self.features.check_with_template("gui", False) + and self.features.check_with_template( + "supported-feature.late-gui-daemon", False + ) + ): + services.append("qubes.WaitForSession") start_tasks = [] for service in services: start_tasks.append( diff --git a/qubes/vm/mix/dvmtemplate.py b/qubes/vm/mix/dvmtemplate.py index 19c07e70d..629bfe213 100644 --- a/qubes/vm/mix/dvmtemplate.py +++ b/qubes/vm/mix/dvmtemplate.py @@ -19,7 +19,7 @@ # with this program; if not, see . import asyncio -from typing import Optional, Union, Iterator +from typing import Optional, Union, Iterator, Tuple import qubes.config import qubes.events @@ -223,10 +223,11 @@ def on_feature_pre_set_preload_dispvm_max( if not self.features.check_with_template("qrexec", None): raise qubes.exc.QubesValueError("Qube does not support qrexec") - service = "qubes.WaitForRunningSystem" - if not self.supports_preload(): + supported, missing_services = self.supports_preload() + if not supported: raise qubes.exc.QubesValueError( - "Qube does not support the RPC '%s'" % service + "Qube does not support the RPC(s) '%s'" + % ", ".join(missing_services) ) value = value or "0" @@ -486,11 +487,12 @@ async def on_domain_preload_dispvm_used( if delay: event_log += " with a delay of %s second(s)" % f"{delay:.1f}" self.log.info(event_log) - service = "qubes.WaitForRunningSystem" - if not self.supports_preload(): + + supported, missing_services = self.supports_preload() + if not supported: raise qubes.exc.QubesValueError( - "Qube does not support the RPC '%s' but tried to preload, " - "check if template is outdated" % service + "Qube does not support the RPC(s) '%s' but tried to preload, " + "check if template is outdated" % ", ".join(missing_services) ) if delay: await asyncio.sleep(abs(delay)) @@ -796,15 +798,21 @@ def remove_preload_excess( dispvm = self.app.domains[unwanted_disp] asyncio.ensure_future(dispvm.cleanup()) - def supports_preload(self) -> bool: + def supports_preload(self) -> Tuple[bool, list]: """ - Check if the necessary RPC is supported. + Check if the necessary RPCs are supported. - :rtype: bool + The first returned value indicates success while the second value is + non empty and contains the missing services if they are not supported. + + :rtype: (bool, list) """ assert isinstance(self, qubes.vm.BaseVM) - service = "qubes.WaitForRunningSystem" - supported_service = "supported-rpc." + service - if self.features.check_with_template(supported_service, False): - return True - return False + supported = True + missing_services = [] + for service in ["qubes.WaitForRunningSystem", "qubes.WaitForSession"]: + feature = "supported-rpc." + service + if not self.features.check_with_template(feature, False): + missing_services.append(service) + supported = False + return (supported, missing_services) diff --git a/tests/dispvm_perf.py b/tests/dispvm_perf.py index 1d8284f73..16fdf3347 100755 --- a/tests/dispvm_perf.py +++ b/tests/dispvm_perf.py @@ -928,6 +928,8 @@ async def run_test(self, test: TestConfig): self.dvm.features["preload-dispvm-delay"] = str( test.preload_delay ) + if not test.gui: + self.dvm.guivm = None if test.preload_max: preload_max = test.preload_max logger.info("Setting local max feature: '%s'", preload_max)