From 32a2dc07c9138547629a28641983da253ea633f0 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Wed, 12 Nov 2025 11:10:13 +0100 Subject: [PATCH] Early GUI connection to preloaded disposables When the GUI session is established before being paused, the preloaded disposables can use the same GUI session after requesting the qube, thus the time to connect to the session is offset from the request stage to the preload stage. Reverting initial iteration: 212b148ee42193024d78e44e12e9542b1dda0bba It isn't the default because it comes with various disadvantages: - Depends on apps not autostarting, else they may appear before the qube is paused and can cause confusion; - Cannot adapt to monitor changes, screen resize, plugging external monitor; - Cannot survive display manager logout+login; - Only works after using has logged in to GUI session; For: https://github.com/qubesos/qubes-issues/issues/10230 For: https://github.com/QubesOS/qubes-issues/issues/1512 --- Makefile | 1 + linux/autostart/Makefile | 6 ++++ .../qubes-preload-dispvm-gui.desktop | 9 +++++ linux/aux-tools/preload-dispvm | 17 +++++++-- linux/systemd/Makefile | 1 + .../systemd/qubes-preload-dispvm-gui.service | 14 ++++++++ linux/systemd/qubes-preload-dispvm.service | 2 +- qubes/api/admin.py | 21 ++++++++--- qubes/vm/dispvm.py | 23 ++++++++++-- qubes/vm/mix/dvmtemplate.py | 36 +++++++++++++++---- rpm_spec/core-dom0.spec.in | 3 ++ 11 files changed, 117 insertions(+), 16 deletions(-) create mode 100644 linux/autostart/Makefile create mode 100644 linux/autostart/qubes-preload-dispvm-gui.desktop create mode 100644 linux/systemd/qubes-preload-dispvm-gui.service diff --git a/Makefile b/Makefile index ceefb386e..ac12f0d05 100644 --- a/Makefile +++ b/Makefile @@ -179,6 +179,7 @@ all: install: ifeq ($(OS),Linux) + $(MAKE) install -C linux/autostart $(MAKE) install -C linux/systemd $(MAKE) install -C linux/aux-tools $(MAKE) install -C linux/system-config diff --git a/linux/autostart/Makefile b/linux/autostart/Makefile new file mode 100644 index 000000000..23c55935b --- /dev/null +++ b/linux/autostart/Makefile @@ -0,0 +1,6 @@ +all: + true + +install: + mkdir -p $(DESTDIR)/etc/xdg/autostart + cp qubes-preload-dispvm-gui.desktop $(DESTDIR)/etc/xdg/autostart diff --git a/linux/autostart/qubes-preload-dispvm-gui.desktop b/linux/autostart/qubes-preload-dispvm-gui.desktop new file mode 100644 index 000000000..acde1f69f --- /dev/null +++ b/linux/autostart/qubes-preload-dispvm-gui.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Icon=qubes +Name=Qubes Preload Disposables +Comment=Workaround for session monitoring with qubes.WaitForSession +Categories=System;Monitor; +Exec=systemctl start qubes-preload-dispvm-gui.service +Terminal=false +NoDisplay=true +Type=Application diff --git a/linux/aux-tools/preload-dispvm b/linux/aux-tools/preload-dispvm index f1c02740b..c8fea96dc 100755 --- a/linux/aux-tools/preload-dispvm +++ b/linux/aux-tools/preload-dispvm @@ -3,10 +3,11 @@ """ This script is outside of qubesd because it relies on systemd to: -- Order this action after the autostart or standard qubes; +- Order this action after the autostart of normal qubes; - Skip preloading if kernel command line prevents autostart. """ +import argparse import asyncio import concurrent.futures import qubesadmin @@ -23,6 +24,18 @@ def get_preload_max(qube) -> int | None: async def main(): + parser = argparse.ArgumentParser( + description="Autostart preloaded disposable cycle" + ) + parser.add_argument( + "--gui", action="store_true", help="start qubes with GUI session" + ) + args = parser.parse_args() + + call_arg = "preload-autostart" + if args.gui: + call_arg += "+gui" + app = qubesadmin.Qubes() domains = app.domains default_dispvm = getattr(app, "default_dispvm", None) @@ -54,7 +67,7 @@ async def main(): maximum = get_preload_max(qube) msg = f"{qube}:{maximum}" print(repr(msg)) - exec_args = qube.qubesd_call, qube.name, method, "preload-autostart" + exec_args = qube.qubesd_call, qube.name, method, call_arg future = loop.run_in_executor(executor, *exec_args) tasks.append(future) await asyncio.gather(*tasks) diff --git a/linux/systemd/Makefile b/linux/systemd/Makefile index 761e980b2..ff3681328 100644 --- a/linux/systemd/Makefile +++ b/linux/systemd/Makefile @@ -10,6 +10,7 @@ install: cp qubes-qmemman.service $(DESTDIR)$(UNITDIR) cp qubesd.service $(DESTDIR)$(UNITDIR) cp qubes-preload-dispvm.service $(DESTDIR)$(UNITDIR) + cp qubes-preload-dispvm-gui.service $(DESTDIR)$(UNITDIR) install -d $(DESTDIR)$(UNITDIR)/lvm2-pvscan@.service.d install -m 0644 lvm2-pvscan@.service.d_30_qubes.conf \ $(DESTDIR)$(UNITDIR)/lvm2-pvscan@.service.d/30_qubes.conf diff --git a/linux/systemd/qubes-preload-dispvm-gui.service b/linux/systemd/qubes-preload-dispvm-gui.service new file mode 100644 index 000000000..1a9731b1c --- /dev/null +++ b/linux/systemd/qubes-preload-dispvm-gui.service @@ -0,0 +1,14 @@ +[Unit] +Description=Preload Qubes' Disposables after DISPLAY is available +ConditionKernelCommandLine=!qubes.skip_autostart +# After qmemman so the daemon can create the file containing available memory. +After=qubesd.service qubes-meminfo-writer-dom0.service + +[Service] +Type=oneshot +ExecStart=/usr/lib/qubes/preload-dispvm --gui +Group=qubes +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target diff --git a/linux/systemd/qubes-preload-dispvm.service b/linux/systemd/qubes-preload-dispvm.service index 3b7e1b336..5edec6376 100644 --- a/linux/systemd/qubes-preload-dispvm.service +++ b/linux/systemd/qubes-preload-dispvm.service @@ -1,5 +1,5 @@ [Unit] -Description=Preload Qubes DispVMs +Description=Preload Qubes' Disposables ConditionKernelCommandLine=!qubes.skip_autostart # After qmemman so the daemon can create the file containing available memory. After=qubesd.service qubes-meminfo-writer-dom0.service diff --git a/qubes/api/admin.py b/qubes/api/admin.py index ff5a1eb8f..1e991787f 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -1308,13 +1308,20 @@ async def _vm_create( @qubes.api.method("admin.vm.CreateDisposable", scope="global", write=True) async def create_disposable(self, untrusted_payload): """ - Create a disposable. If the RPC argument is ``preload-autostart``, - cleanse the preload list and start preloading fresh disposables. + Create a disposable. If the RPC argument starts with + ``preload-autostart``, cleanse the preload list and start preloading + fresh disposables. """ - self.enforce(self.arg in [None, "", "preload-autostart"]) + self.enforce( + self.arg in [None, "", "preload-autostart", "preload-autostart+gui"] + ) preload_autostart = False + preload_autostart_gui = False if self.arg == "preload-autostart": preload_autostart = True + elif self.arg == "preload-autostart+gui": + preload_autostart = True + preload_autostart_gui = True if untrusted_payload not in (b"", b"uuid"): raise qubes.exc.QubesValueError( "Invalid payload for admin.vm.CreateDisposable: " @@ -1327,9 +1334,15 @@ async def create_disposable(self, untrusted_payload): appvm = self.dest self.fire_event_for_permission(dispvm_template=appvm) + + preload_autostart_event = None if preload_autostart: - await appvm.fire_event_async("domain-preload-dispvm-autostart") + preload_autostart_event = "domain-preload-dispvm-autostart" + if preload_autostart_gui: + preload_autostart_event += "-gui" + await appvm.fire_event_async(preload_autostart_event) return + dispvm = await qubes.vm.dispvm.DispVM.from_appvm(appvm) # TODO: move this to extension (in race-free fashion, better than here) dispvm.tags.add("created-by-" + str(self.src)) diff --git a/qubes/vm/dispvm.py b/qubes/vm/dispvm.py index dca744a87..cffa2a4a1 100644 --- a/qubes/vm/dispvm.py +++ b/qubes/vm/dispvm.py @@ -287,7 +287,7 @@ class DispVM(qubes.vm.qubesvm.QubesVM): } def __init__(self, app, xml, *args, **kwargs) -> None: - assert isinstance(self, qubes.vm.BaseVM) + assert isinstance(self, qubes.vm.qubesvm.QubesVM) self.volume_config = copy.deepcopy(self.default_volume_config) template = kwargs.get("template", None) self.preload_complete = asyncio.Event() @@ -370,6 +370,7 @@ def __init__(self, app, xml, *args, **kwargs) -> None: (key, value) for key, value in template.features.items() if not key.startswith("preload-dispvm") + and key != "preload-dispvm-early-gui" ] ) self.tags.update(template.tags) @@ -440,7 +441,7 @@ async def on_domain_started_dispvm( return timeout = self.qrexec_timeout # https://github.com/QubesOS/qubes-issues/issues/9964 - rpc = "qubes.WaitForRunningSystem" + rpc = self.get_preload_service(self) path = "/run/qubes-rpc:/usr/local/etc/qubes-rpc:/etc/qubes-rpc" service = '$(PATH="' + path + '" command -v ' + rpc + ")" try: @@ -844,3 +845,21 @@ async def start(self, **kwargs): def create_qdb_entries(self) -> None: super().create_qdb_entries() self.untrusted_qdb.write("/qubes-vm-persistence", "none") + + @staticmethod + def get_preload_service(obj) -> str: + """ + Check which service is requires to check if system is ready. + + :rtype: str + """ + # pylint: disable=unused-argument + if ( + obj.features.get("preload-dispvm-early-gui", None) + and obj.guivm + and obj.features.get("gui", True) + ): + service = "qubes.WaitForSession" + else: + service = "qubes.WaitForRunningSystem" + return service diff --git a/qubes/vm/mix/dvmtemplate.py b/qubes/vm/mix/dvmtemplate.py index 5ea39f2e7..07900434c 100644 --- a/qubes/vm/mix/dvmtemplate.py +++ b/qubes/vm/mix/dvmtemplate.py @@ -63,7 +63,7 @@ def on_domain_loaded(self, event) -> None: # pylint: disable=unused-argument assert isinstance(self, qubes.vm.BaseVM) changes = False - # Preloading began and host rebooted and autostart event didn't run yet. + # Began preloading, host rebooted and autostart event didn't run since. old_preload = self.get_feat_preload() clean_preload = old_preload.copy() for unwanted_disp in old_preload: @@ -180,7 +180,7 @@ 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" + service = self.get_preload_service() if not self.supports_preload(): raise qubes.exc.QubesValueError( "Qube does not support the RPC '%s'" % service @@ -412,6 +412,7 @@ def __on_property_set_template( @qubes.events.handler( "domain-preload-dispvm-used", "domain-preload-dispvm-autostart", + "domain-preload-dispvm-autostart-gui", "domain-preload-dispvm-start", ) async def on_domain_preload_dispvm_used( @@ -445,16 +446,30 @@ 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(): raise qubes.exc.QubesValueError( "Qube does not support the RPC '%s' but tried to preload, " - "check if template is outdated" % service + "check if template is outdated" % self.get_preload_service() ) + if event.startswith("autostart"): + early_gui = self.features.get("preload-dispvm-early-gui", None) + if event == "autostart" and early_gui: + self.log.info( + "Skipping preload autostart as early GUI was requested" + ) + return + if event == "autostart-gui" and not early_gui: + self.log.info( + "Skipping early GUI preload autostart as it was not " + "configured" + ) + return + if delay: await asyncio.sleep(delay) - if event == "autostart": + if event.startswith("autostart"): self.remove_preload_excess(0, reason="event autostart was called") elif not self.can_preload(): self.remove_preload_excess(reason="there may be absent qubes") @@ -672,6 +687,14 @@ def remove_preload_excess( dispvm = self.app.domains[unwanted_disp] asyncio.ensure_future(dispvm.cleanup()) + def get_preload_service(self) -> str: + """ + Check which service is requires to check if system is ready. + + :rtype: str + """ + return qubes.vm.dispvm.DispVM.get_preload_service(self) + def supports_preload(self) -> bool: """ Check if the necessary RPC is supported. @@ -679,8 +702,7 @@ def supports_preload(self) -> bool: :rtype: bool """ assert isinstance(self, qubes.vm.BaseVM) - service = "qubes.WaitForRunningSystem" - supported_service = "supported-rpc." + service + supported_service = "supported-rpc." + self.get_preload_service() if self.features.check_with_template(supported_service, False): return True return False diff --git a/rpm_spec/core-dom0.spec.in b/rpm_spec/core-dom0.spec.in index 824391807..10d9ca794 100644 --- a/rpm_spec/core-dom0.spec.in +++ b/rpm_spec/core-dom0.spec.in @@ -378,6 +378,8 @@ done %{_mandir}/man1/qubes*.1* +%{_sysconfdir}/xdg/autostart/qubes-preload-dispvm-gui.desktop + %dir %{python3_sitelib}/qubes-*.egg-info %{python3_sitelib}/qubes-*.egg-info/* @@ -578,6 +580,7 @@ done %{_unitdir}/qubes-vm@.service %{_unitdir}/qubesd.service %{_unitdir}/qubes-preload-dispvm.service +%{_unitdir}/qubes-preload-dispvm-gui.service %attr(2770,root,qubes) %dir /var/lib/qubes %attr(2770,root,qubes) %dir /var/lib/qubes/vm-templates %attr(2770,root,qubes) %dir /var/lib/qubes/appvms