Skip to content

Commit 41714fd

Browse files
committed
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: QubesOS/qubes-gui-agent-linux#251 Fixes: QubesOS/qubes-issues#9940 For: QubesOS/qubes-issues#1512
1 parent 56fefc3 commit 41714fd

7 files changed

Lines changed: 100 additions & 38 deletions

File tree

qubes/tests/api_admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3996,6 +3996,7 @@ def test_643_vm_create_disposable_preload_autostart(
39963996
)
39973997
self.vm.features["qrexec"] = "1"
39983998
self.vm.features["supported-rpc.qubes.WaitForRunningSystem"] = "1"
3999+
self.vm.features["supported-rpc.qubes.WaitForSession"] = "1"
39994000
self.vm.features["preload-dispvm-max"] = "1"
40004001
for _ in range(10):
40014002
if len(self.vm.get_feat_preload()) == 1:

qubes/tests/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,7 @@ def setUp(self):
704704
self.template.features["supported-rpc.qubes.WaitForRunningSystem"] = (
705705
True
706706
)
707+
self.template.features["supported-rpc.qubes.WaitForSession"] = True
707708
self.appvm = self.app.add_new_vm(
708709
"AppVM",
709710
name="test-dvm",

qubes/tests/integ/backup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ def create_backup_vms(self, pool=None):
188188
self.loop.run_until_complete(testvm5.create_on_disk(pool=pool))
189189
testvm5.features["qrexec"] = True
190190
testvm5.features["supported-rpc.qubes.WaitForRunningSystem"] = True
191+
testvm5.features["supported-rpc.qubes.WaitForSession"] = True
191192
testvm5.features["preload-dispvm-max"] = 0
192193
testvm5.features["preload-dispvm"] = ""
193194
vms.append(testvm5)

qubes/tests/vm/dispvm.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ def test_000_from_appvm_preload_reject_max(self, mock_storage):
155155
self.appvm.template_for_dispvms = True
156156
orig_getitem = self.app.domains.__getitem__
157157
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
158+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
158159
self.appvm.features["preload-dispvm-max"] = "0"
159160
with mock.patch.object(
160161
self.app, "domains", wraps=self.app.domains
@@ -186,6 +187,7 @@ def test_000_from_appvm_preload_use(
186187
self.appvm.template_for_dispvms = True
187188

188189
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
190+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
189191
self.appvm.features["preload-dispvm-max"] = "1"
190192
orig_getitem = self.app.domains.__getitem__
191193
with mock.patch.object(
@@ -250,6 +252,7 @@ def test_000_from_appvm_preload_fill_gap(
250252
mock_start.side_effect = self.mock_coro
251253
self.appvm.template_for_dispvms = True
252254
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
255+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
253256
orig_getitem = self.app.domains.__getitem__
254257
with mock.patch("qubes.events.Emitter.fire_event_async") as mock_events:
255258
self.appvm.features["preload-dispvm-max"] = "1"
@@ -293,6 +296,7 @@ def test_000_from_appvm_preload_fill_gap(
293296
def test_000_get_preload_max(self):
294297
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), None)
295298
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
299+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
296300
self.appvm.features["preload-dispvm-max"] = 1
297301
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), 1)
298302
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.adminvm), None)
@@ -309,9 +313,11 @@ def test_000_get_preload_templates(self):
309313
self.assertEqual(get_preload_templates(self.app), [])
310314

311315
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
316+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
312317
self.appvm_alt.features["supported-rpc.qubes.WaitForRunningSystem"] = (
313318
True
314319
)
320+
self.appvm_alt.features["supported-rpc.qubes.WaitForSession"] = True
315321
self.appvm.features["preload-dispvm-max"] = 1
316322
self.appvm_alt.features["preload-dispvm-max"] = 0
317323
self.assertEqual(get_preload_templates(self.app), [self.appvm])

qubes/tests/vm/mix/dvmtemplate.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def setUp(self):
8484
self.appvm.features["qrexec"] = True
8585
self.appvm.features["gui"] = False
8686
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
87+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
8788
self.app.domains[self.appvm.name] = self.appvm
8889
self.app.domains[self.appvm] = self.appvm
8990
self.app.default_dispvm = self.appvm
@@ -140,9 +141,13 @@ def test_010_dvm_preload_get_max(self):
140141
self.appvm.features["qrexec"] = True
141142
self.appvm.features["gui"] = False
142143
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = False
144+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = False
143145
with self.assertRaises(qubes.exc.QubesValueError):
144146
self.appvm.features["preload-dispvm-max"] = "1"
145147
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
148+
with self.assertRaises(qubes.exc.QubesValueError):
149+
self.appvm.features["preload-dispvm-max"] = "1"
150+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
146151
self.appvm.features["preload-dispvm-max"] = "1"
147152
cases_invalid = ["a", "-1", "1 1"]
148153
for value in cases_invalid:
@@ -435,5 +440,6 @@ def test_040_dvm_preload_set_template_for_dispvms(
435440
def test_100_get_preload_templates(self):
436441
print(qubes.vm.dispvm.get_preload_templates(self.app))
437442
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
443+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
438444
self.appvm.features["preload-dispvm-max"] = 1
439445
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), 1)

qubes/vm/dispvm.py

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -422,27 +422,16 @@ def on_domain_loaded(self, event) -> None:
422422
# pylint: disable=unused-argument
423423
assert self.template
424424

425-
@qubes.events.handler("domain-start")
426-
async def on_domain_started_dispvm(
427-
self,
428-
event,
429-
**kwargs,
430-
):
431-
# pylint: disable=unused-argument
425+
async def wait_operational_preload(
426+
self, rpc: str, service: str, timeout: int | float
427+
) -> None:
432428
"""
433-
When starting a qube, await for basic services to be started on
434-
preloaded disposables and interrupts the domain if the qube has not
435-
been requested yet.
429+
Await for preloaded disposable to become fully operational.
436430
437-
:param str event: Event which was fired.
431+
:param str rpc: Pretty RPC service name.
432+
:param str service: Full command-line.
433+
:param int|float timeout: Fail after timeout is reached.
438434
"""
439-
if not self.is_preload:
440-
return
441-
timeout = self.qrexec_timeout
442-
# https://github.com/QubesOS/qubes-issues/issues/9964
443-
rpc = "qubes.WaitForRunningSystem"
444-
path = "/run/qubes-rpc:/usr/local/etc/qubes-rpc:/etc/qubes-rpc"
445-
service = '$(PATH="' + path + '" command -v ' + rpc + ")"
446435
try:
447436
self.log.info(
448437
"Preload startup waiting '%s' with '%d' seconds timeout",
@@ -457,18 +446,68 @@ async def on_domain_started_dispvm(
457446
),
458447
timeout=timeout,
459448
)
449+
self.log.info("Preload startup completed '%s'", rpc)
460450
except asyncio.TimeoutError:
451+
if rpc == "qubes.WaitForSession":
452+
debug_msg = "systemd-analyze --user blame"
453+
else:
454+
debug_msg = "systemd-analyze blame"
461455
raise qubes.exc.QubesException(
462456
"Timed out call to '%s' after '%d' seconds during preload "
463-
"startup" % (rpc, timeout)
457+
"startup. To debug, run the following on a new disposable of "
458+
"'%s': %s" % (rpc, timeout, self.template, debug_msg)
464459
)
465460
except (subprocess.CalledProcessError, qubes.exc.QubesException):
461+
if rpc == "qubes.WaitForSession":
462+
debug_msg = "systemctl --user --failed"
463+
else:
464+
debug_msg = "systemctl --failed"
466465
raise qubes.exc.QubesException(
467-
"Error on call to '%s' during preload startup. To debug, run "
468-
"the following on a new disposable of '%s': systemctl "
469-
"--failed" % (rpc, self.template)
466+
"Error on call to '%s' during preload startup. To debug, "
467+
"run the following on a new disposable of '%s': %s"
468+
% (rpc, self.template, debug_msg)
470469
)
471470

471+
@qubes.events.handler("domain-start")
472+
async def on_domain_started_dispvm(
473+
self,
474+
event,
475+
**kwargs,
476+
):
477+
# pylint: disable=unused-argument
478+
"""
479+
When starting a qube, await for basic services to be started on
480+
preloaded disposables and interrupts the domain if the qube has not
481+
been requested yet.
482+
483+
:param str event: Event which was fired.
484+
"""
485+
if not self.is_preload:
486+
return
487+
timeout = self.qrexec_timeout
488+
# https://github.com/QubesOS/qubes-issues/issues/9964
489+
path = "/run/qubes-rpc:/usr/local/etc/qubes-rpc:/etc/qubes-rpc"
490+
rpcs = ["qubes.WaitForRunningSystem"]
491+
if self.features.check_with_template(
492+
"supported-feature.late-gui-daemon", False
493+
):
494+
rpcs.append("qubes.WaitForSession")
495+
try:
496+
async with asyncio.TaskGroup() as task_group:
497+
for rpc in rpcs:
498+
service = '$(PATH="' + path + '" command -v ' + rpc + ")"
499+
task_group.create_task(
500+
self.wait_operational_preload(rpc, service, timeout)
501+
)
502+
except ExceptionGroup as e:
503+
# Show detailed exception in desktop notification.
504+
wanted_ex_group, _ = e.split(qubes.exc.QubesException)
505+
if wanted_ex_group:
506+
messages = [
507+
"\n" + str(exc) for exc in wanted_ex_group.exceptions
508+
]
509+
raise qubes.exc.QubesException("\n".join(messages))
510+
raise
472511
if not self.preload_requested:
473512
await self.pause()
474513
self.log.info("Preloading finished")

qubes/vm/mix/dvmtemplate.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
# with this program; if not, see <http://www.gnu.org/licenses/>.
2020

2121
import asyncio
22-
from typing import Optional, Union, Iterator
22+
from typing import Optional, Union, Iterator, Tuple
2323

2424
import qubes.config
2525
import qubes.events
@@ -180,10 +180,11 @@ 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"
184-
if not self.supports_preload():
183+
supported, missing_services = self.supports_preload()
184+
if not supported:
185185
raise qubes.exc.QubesValueError(
186-
"Qube does not support the RPC '%s'" % service
186+
"Qube does not support the RPC(s) '%s'"
187+
% ", ".join(missing_services)
187188
)
188189

189190
value = value or "0"
@@ -445,11 +446,12 @@ 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-
if not self.supports_preload():
449+
450+
supported, missing_services = self.supports_preload()
451+
if not supported:
450452
raise qubes.exc.QubesValueError(
451-
"Qube does not support the RPC '%s' but tried to preload, "
452-
"check if template is outdated" % service
453+
"Qube does not support the RPC(s) '%s' but tried to preload, "
454+
"check if template is outdated" % ", ".join(missing_services)
453455
)
454456
if delay:
455457
await asyncio.sleep(delay)
@@ -672,15 +674,21 @@ def remove_preload_excess(
672674
dispvm = self.app.domains[unwanted_disp]
673675
asyncio.ensure_future(dispvm.cleanup())
674676

675-
def supports_preload(self) -> bool:
677+
def supports_preload(self) -> Tuple[bool, list]:
676678
"""
677-
Check if the necessary RPC is supported.
679+
Check if the necessary RPCs are supported.
678680
679-
:rtype: bool
681+
The first returned value indicates success while the second value is
682+
non empty and contains the missing services if they are not supported.
683+
684+
:rtype: (bool, list)
680685
"""
681686
assert isinstance(self, qubes.vm.BaseVM)
682-
service = "qubes.WaitForRunningSystem"
683-
supported_service = "supported-rpc." + service
684-
if self.features.check_with_template(supported_service, False):
685-
return True
686-
return False
687+
supported = True
688+
missing_services = []
689+
for service in ["qubes.WaitForRunningSystem", "qubes.WaitForSession"]:
690+
feature = "supported-rpc." + service
691+
if not self.features.check_with_template(feature, False):
692+
missing_services.append(service)
693+
supported = False
694+
return (supported, missing_services)

0 commit comments

Comments
 (0)