Skip to content

Commit 32a2dc0

Browse files
committed
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: 212b148 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: QubesOS/qubes-issues#10230 For: QubesOS/qubes-issues#1512
1 parent 6b978a3 commit 32a2dc0

11 files changed

Lines changed: 117 additions & 16 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ all:
179179

180180
install:
181181
ifeq ($(OS),Linux)
182+
$(MAKE) install -C linux/autostart
182183
$(MAKE) install -C linux/systemd
183184
$(MAKE) install -C linux/aux-tools
184185
$(MAKE) install -C linux/system-config

linux/autostart/Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
all:
2+
true
3+
4+
install:
5+
mkdir -p $(DESTDIR)/etc/xdg/autostart
6+
cp qubes-preload-dispvm-gui.desktop $(DESTDIR)/etc/xdg/autostart
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[Desktop Entry]
2+
Icon=qubes
3+
Name=Qubes Preload Disposables
4+
Comment=Workaround for session monitoring with qubes.WaitForSession
5+
Categories=System;Monitor;
6+
Exec=systemctl start qubes-preload-dispvm-gui.service
7+
Terminal=false
8+
NoDisplay=true
9+
Type=Application

linux/aux-tools/preload-dispvm

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
"""
44
This script is outside of qubesd because it relies on systemd to:
55
6-
- Order this action after the autostart or standard qubes;
6+
- Order this action after the autostart of normal qubes;
77
- Skip preloading if kernel command line prevents autostart.
88
"""
99

10+
import argparse
1011
import asyncio
1112
import concurrent.futures
1213
import qubesadmin
@@ -23,6 +24,18 @@ def get_preload_max(qube) -> int | None:
2324

2425

2526
async def main():
27+
parser = argparse.ArgumentParser(
28+
description="Autostart preloaded disposable cycle"
29+
)
30+
parser.add_argument(
31+
"--gui", action="store_true", help="start qubes with GUI session"
32+
)
33+
args = parser.parse_args()
34+
35+
call_arg = "preload-autostart"
36+
if args.gui:
37+
call_arg += "+gui"
38+
2639
app = qubesadmin.Qubes()
2740
domains = app.domains
2841
default_dispvm = getattr(app, "default_dispvm", None)
@@ -54,7 +67,7 @@ async def main():
5467
maximum = get_preload_max(qube)
5568
msg = f"{qube}:{maximum}"
5669
print(repr(msg))
57-
exec_args = qube.qubesd_call, qube.name, method, "preload-autostart"
70+
exec_args = qube.qubesd_call, qube.name, method, call_arg
5871
future = loop.run_in_executor(executor, *exec_args)
5972
tasks.append(future)
6073
await asyncio.gather(*tasks)

linux/systemd/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ install:
1010
cp qubes-qmemman.service $(DESTDIR)$(UNITDIR)
1111
cp qubesd.service $(DESTDIR)$(UNITDIR)
1212
cp qubes-preload-dispvm.service $(DESTDIR)$(UNITDIR)
13+
cp qubes-preload-dispvm-gui.service $(DESTDIR)$(UNITDIR)
1314
install -d $(DESTDIR)$(UNITDIR)/lvm2-pvscan@.service.d
1415
install -m 0644 lvm2-pvscan@.service.d_30_qubes.conf \
1516
$(DESTDIR)$(UNITDIR)/lvm2-pvscan@.service.d/30_qubes.conf
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[Unit]
2+
Description=Preload Qubes' Disposables after DISPLAY is available
3+
ConditionKernelCommandLine=!qubes.skip_autostart
4+
# After qmemman so the daemon can create the file containing available memory.
5+
After=qubesd.service qubes-meminfo-writer-dom0.service
6+
7+
[Service]
8+
Type=oneshot
9+
ExecStart=/usr/lib/qubes/preload-dispvm --gui
10+
Group=qubes
11+
RemainAfterExit=yes
12+
13+
[Install]
14+
WantedBy=multi-user.target

linux/systemd/qubes-preload-dispvm.service

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[Unit]
2-
Description=Preload Qubes DispVMs
2+
Description=Preload Qubes' Disposables
33
ConditionKernelCommandLine=!qubes.skip_autostart
44
# After qmemman so the daemon can create the file containing available memory.
55
After=qubesd.service qubes-meminfo-writer-dom0.service

qubes/api/admin.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,13 +1308,20 @@ async def _vm_create(
13081308
@qubes.api.method("admin.vm.CreateDisposable", scope="global", write=True)
13091309
async def create_disposable(self, untrusted_payload):
13101310
"""
1311-
Create a disposable. If the RPC argument is ``preload-autostart``,
1312-
cleanse the preload list and start preloading fresh disposables.
1311+
Create a disposable. If the RPC argument starts with
1312+
``preload-autostart``, cleanse the preload list and start preloading
1313+
fresh disposables.
13131314
"""
1314-
self.enforce(self.arg in [None, "", "preload-autostart"])
1315+
self.enforce(
1316+
self.arg in [None, "", "preload-autostart", "preload-autostart+gui"]
1317+
)
13151318
preload_autostart = False
1319+
preload_autostart_gui = False
13161320
if self.arg == "preload-autostart":
13171321
preload_autostart = True
1322+
elif self.arg == "preload-autostart+gui":
1323+
preload_autostart = True
1324+
preload_autostart_gui = True
13181325
if untrusted_payload not in (b"", b"uuid"):
13191326
raise qubes.exc.QubesValueError(
13201327
"Invalid payload for admin.vm.CreateDisposable: "
@@ -1327,9 +1334,15 @@ async def create_disposable(self, untrusted_payload):
13271334
appvm = self.dest
13281335

13291336
self.fire_event_for_permission(dispvm_template=appvm)
1337+
1338+
preload_autostart_event = None
13301339
if preload_autostart:
1331-
await appvm.fire_event_async("domain-preload-dispvm-autostart")
1340+
preload_autostart_event = "domain-preload-dispvm-autostart"
1341+
if preload_autostart_gui:
1342+
preload_autostart_event += "-gui"
1343+
await appvm.fire_event_async(preload_autostart_event)
13321344
return
1345+
13331346
dispvm = await qubes.vm.dispvm.DispVM.from_appvm(appvm)
13341347
# TODO: move this to extension (in race-free fashion, better than here)
13351348
dispvm.tags.add("created-by-" + str(self.src))

qubes/vm/dispvm.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ class DispVM(qubes.vm.qubesvm.QubesVM):
287287
}
288288

289289
def __init__(self, app, xml, *args, **kwargs) -> None:
290-
assert isinstance(self, qubes.vm.BaseVM)
290+
assert isinstance(self, qubes.vm.qubesvm.QubesVM)
291291
self.volume_config = copy.deepcopy(self.default_volume_config)
292292
template = kwargs.get("template", None)
293293
self.preload_complete = asyncio.Event()
@@ -370,6 +370,7 @@ def __init__(self, app, xml, *args, **kwargs) -> None:
370370
(key, value)
371371
for key, value in template.features.items()
372372
if not key.startswith("preload-dispvm")
373+
and key != "preload-dispvm-early-gui"
373374
]
374375
)
375376
self.tags.update(template.tags)
@@ -440,7 +441,7 @@ async def on_domain_started_dispvm(
440441
return
441442
timeout = self.qrexec_timeout
442443
# https://github.com/QubesOS/qubes-issues/issues/9964
443-
rpc = "qubes.WaitForRunningSystem"
444+
rpc = self.get_preload_service(self)
444445
path = "/run/qubes-rpc:/usr/local/etc/qubes-rpc:/etc/qubes-rpc"
445446
service = '$(PATH="' + path + '" command -v ' + rpc + ")"
446447
try:
@@ -844,3 +845,21 @@ async def start(self, **kwargs):
844845
def create_qdb_entries(self) -> None:
845846
super().create_qdb_entries()
846847
self.untrusted_qdb.write("/qubes-vm-persistence", "none")
848+
849+
@staticmethod
850+
def get_preload_service(obj) -> str:
851+
"""
852+
Check which service is requires to check if system is ready.
853+
854+
:rtype: str
855+
"""
856+
# pylint: disable=unused-argument
857+
if (
858+
obj.features.get("preload-dispvm-early-gui", None)
859+
and obj.guivm
860+
and obj.features.get("gui", True)
861+
):
862+
service = "qubes.WaitForSession"
863+
else:
864+
service = "qubes.WaitForRunningSystem"
865+
return service

qubes/vm/mix/dvmtemplate.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def on_domain_loaded(self, event) -> None:
6363
# pylint: disable=unused-argument
6464
assert isinstance(self, qubes.vm.BaseVM)
6565
changes = False
66-
# Preloading began and host rebooted and autostart event didn't run yet.
66+
# Began preloading, host rebooted and autostart event didn't run since.
6767
old_preload = self.get_feat_preload()
6868
clean_preload = old_preload.copy()
6969
for unwanted_disp in old_preload:
@@ -180,7 +180,7 @@ def on_feature_pre_set_preload_dispvm_max(
180180
if not self.features.check_with_template("qrexec", None):
181181
raise qubes.exc.QubesValueError("Qube does not support qrexec")
182182

183-
service = "qubes.WaitForRunningSystem"
183+
service = self.get_preload_service()
184184
if not self.supports_preload():
185185
raise qubes.exc.QubesValueError(
186186
"Qube does not support the RPC '%s'" % service
@@ -412,6 +412,7 @@ def __on_property_set_template(
412412
@qubes.events.handler(
413413
"domain-preload-dispvm-used",
414414
"domain-preload-dispvm-autostart",
415+
"domain-preload-dispvm-autostart-gui",
415416
"domain-preload-dispvm-start",
416417
)
417418
async def on_domain_preload_dispvm_used(
@@ -445,16 +446,30 @@ async def on_domain_preload_dispvm_used(
445446
if delay:
446447
event_log += " with a delay of %s second(s)" % f"{delay:.1f}"
447448
self.log.info(event_log)
448-
service = "qubes.WaitForRunningSystem"
449+
449450
if not self.supports_preload():
450451
raise qubes.exc.QubesValueError(
451452
"Qube does not support the RPC '%s' but tried to preload, "
452-
"check if template is outdated" % service
453+
"check if template is outdated" % self.get_preload_service()
453454
)
455+
if event.startswith("autostart"):
456+
early_gui = self.features.get("preload-dispvm-early-gui", None)
457+
if event == "autostart" and early_gui:
458+
self.log.info(
459+
"Skipping preload autostart as early GUI was requested"
460+
)
461+
return
462+
if event == "autostart-gui" and not early_gui:
463+
self.log.info(
464+
"Skipping early GUI preload autostart as it was not "
465+
"configured"
466+
)
467+
return
468+
454469
if delay:
455470
await asyncio.sleep(delay)
456471

457-
if event == "autostart":
472+
if event.startswith("autostart"):
458473
self.remove_preload_excess(0, reason="event autostart was called")
459474
elif not self.can_preload():
460475
self.remove_preload_excess(reason="there may be absent qubes")
@@ -672,15 +687,22 @@ def remove_preload_excess(
672687
dispvm = self.app.domains[unwanted_disp]
673688
asyncio.ensure_future(dispvm.cleanup())
674689

690+
def get_preload_service(self) -> str:
691+
"""
692+
Check which service is requires to check if system is ready.
693+
694+
:rtype: str
695+
"""
696+
return qubes.vm.dispvm.DispVM.get_preload_service(self)
697+
675698
def supports_preload(self) -> bool:
676699
"""
677700
Check if the necessary RPC is supported.
678701
679702
:rtype: bool
680703
"""
681704
assert isinstance(self, qubes.vm.BaseVM)
682-
service = "qubes.WaitForRunningSystem"
683-
supported_service = "supported-rpc." + service
705+
supported_service = "supported-rpc." + self.get_preload_service()
684706
if self.features.check_with_template(supported_service, False):
685707
return True
686708
return False

0 commit comments

Comments
 (0)