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