diff --git a/qubes/ext/gui.py b/qubes/ext/gui.py index 01027c66b..36f4a1d0d 100644 --- a/qubes/ext/gui.py +++ b/qubes/ext/gui.py @@ -32,21 +32,9 @@ 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 - @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) @@ -74,14 +56,65 @@ 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) + @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) + 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): + 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): for feature in ("gui-videoram-overhead", "gui-videoram-min"): @@ -93,43 +126,36 @@ 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 ) - @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_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): @@ -137,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) ) @@ -149,39 +173,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)), + ) 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 2f8edcd4f..2a1c2879b 100644 --- a/qubes/vm/dispvm.py +++ b/qubes/vm/dispvm.py @@ -506,25 +506,28 @@ 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, ) 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 " "'%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") @@ -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)