Skip to content

Commit 8376986

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 Requires: QubesOS/qubes-gui-agent-linux#255 Fixes: QubesOS/qubes-issues#9940 For: QubesOS/qubes-issues#1512
1 parent 65f75c5 commit 8376986

9 files changed

Lines changed: 63 additions & 21 deletions

File tree

qubes/tests/api_admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4022,6 +4022,7 @@ def test_643_vm_create_disposable_preload_autostart(
40224022
)
40234023
self.vm.features["qrexec"] = "1"
40244024
self.vm.features["supported-rpc.qubes.WaitForRunningSystem"] = "1"
4025+
self.vm.features["supported-rpc.qubes.WaitForSession"] = "1"
40254026
self.vm.features["preload-dispvm-max"] = "1"
40264027
for _ in range(10):
40274028
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
@@ -695,6 +695,7 @@ def setUp(self):
695695
self.template.features["supported-rpc.qubes.WaitForRunningSystem"] = (
696696
True
697697
)
698+
self.template.features["supported-rpc.qubes.WaitForSession"] = True
698699
self.appvm = self.app.add_new_vm(
699700
"AppVM",
700701
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/integ/dispvm.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ async def run_preload_proc(self):
451451
finally:
452452
logger.info("end")
453453

454-
async def run_preload(self):
454+
async def run_preload(self, assert_stdout: bool = True):
455455
logger.info("start")
456456
appvm = self.disp_base
457457
dispvm = appvm.get_feat_preload()[0]
@@ -463,7 +463,8 @@ async def run_preload(self):
463463
self._test_event_handler_remove(dispvm, "domain-unpaused")
464464

465465
stdout = await self.run_preload_proc()
466-
self.assertEqual(stdout, dispvm_name)
466+
if assert_stdout:
467+
self.assertEqual(stdout, dispvm_name)
467468
test_cases = [
468469
(False, appvm.name, "domain-preload-dispvm-start", True),
469470
(True, appvm.name, "domain-preload-dispvm-used", True),
@@ -554,7 +555,9 @@ async def _test_013_preload_gui(self):
554555
self.disp_base.features["gui"] = True
555556
self.disp_base.features["preload-dispvm-max"] = str(preload_max)
556557
await self.wait_preload(preload_max)
557-
await self.run_preload()
558+
self.preload_cmd.insert(1, "--service")
559+
self.preload_cmd[-1] = "qubes.WaitForSession"
560+
await self.run_preload(assert_stdout=False)
558561
logger.info("end")
559562

560563
def test_014_preload_nogui(self):
@@ -569,7 +572,9 @@ async def _test_014_preload_nogui(self):
569572
self.disp_base.features["preload-dispvm-max"] = str(preload_max)
570573
await self.wait_preload(preload_max, wait_completion=False)
571574
self.preload_cmd.insert(1, "--no-gui")
572-
await self.run_preload()
575+
self.preload_cmd.insert(1, "--service")
576+
self.preload_cmd[-1] = "qubes.WaitForRunningSystem"
577+
await self.run_preload(assert_stdout=False)
573578
logger.info("end")
574579

575580
@unittest.skipUnless(which("xdotool"), "xdotool not installed")

qubes/tests/vm/dispvm.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ def test_001_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
@@ -187,6 +188,7 @@ def test_002_from_appvm_preload_use(
187188
self.appvm.template_for_dispvms = True
188189

189190
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
191+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
190192
self.appvm.features["preload-dispvm-max"] = "1"
191193
orig_getitem = self.app.domains.__getitem__
192194
with mock.patch.object(
@@ -253,6 +255,7 @@ def test_003_from_appvm_preload_fill_gap(
253255
mock_start.side_effect = self.mock_coro
254256
self.appvm.template_for_dispvms = True
255257
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
258+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
256259
orig_getitem = self.app.domains.__getitem__
257260
with mock.patch("qubes.events.Emitter.fire_event_async") as mock_events:
258261
self.appvm.features["preload-dispvm-max"] = "1"
@@ -296,6 +299,7 @@ def test_003_from_appvm_preload_fill_gap(
296299
def test_004_get_preload_max(self):
297300
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), None)
298301
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
302+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
299303
self.appvm.features["preload-dispvm-max"] = 1
300304
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), 1)
301305
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.adminvm), None)
@@ -312,9 +316,11 @@ def test_005_get_preload_templates(self):
312316
self.assertEqual(get_preload_templates(self.app), [])
313317

314318
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
319+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
315320
self.appvm_alt.features["supported-rpc.qubes.WaitForRunningSystem"] = (
316321
True
317322
)
323+
self.appvm_alt.features["supported-rpc.qubes.WaitForSession"] = True
318324
self.appvm.features["preload-dispvm-max"] = 1
319325
self.appvm_alt.features["preload-dispvm-max"] = 0
320326
self.assertEqual(get_preload_templates(self.app), [self.appvm])
@@ -670,6 +676,7 @@ def test_024_is_preload_outdated(self, _mock_makedirs, _mock_symlink):
670676
self.appvm.create_on_disk(pool="alternative")
671677
)
672678
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
679+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
673680
self.appvm.features["preload-dispvm-max"] = "1"
674681
orig_getitem = self.app.domains.__getitem__
675682
with mock.patch.object(

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
@@ -163,9 +164,13 @@ def test_010_dvm_preload_get_max(self):
163164
self.appvm.features["qrexec"] = True
164165
self.appvm.features["gui"] = False
165166
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = False
167+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = False
166168
with self.assertRaises(qubes.exc.QubesValueError):
167169
self.appvm.features["preload-dispvm-max"] = "1"
168170
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
171+
with self.assertRaises(qubes.exc.QubesValueError):
172+
self.appvm.features["preload-dispvm-max"] = "1"
173+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
169174
self.appvm.features["preload-dispvm-max"] = "1"
170175
cases_invalid = ["a", "-1", "1 1"]
171176
for value in cases_invalid:
@@ -458,5 +463,6 @@ def test_040_dvm_preload_set_template_for_dispvms(
458463
def test_100_get_preload_templates(self):
459464
print(qubes.vm.dispvm.get_preload_templates(self.app))
460465
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
466+
self.appvm.features["supported-rpc.qubes.WaitForSession"] = True
461467
self.appvm.features["preload-dispvm-max"] = 1
462468
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), 1)

qubes/vm/dispvm.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,10 @@ async def wait_operational_preload(
512512
)
513513
self.log.info("Preload startup completed '%s'", service)
514514
except asyncio.TimeoutError:
515-
debug_msg = "systemd-analyze blame"
515+
if service == "qubes.WaitForSession":
516+
debug_msg = "systemd-analyze --user blame"
517+
else:
518+
debug_msg = "systemd-analyze blame"
516519
raise qubes.exc.QubesException(
517520
"Timed out call to '%s' after '%d' seconds during preload "
518521
"startup. To debug, run the following on a new disposable of "
@@ -546,6 +549,14 @@ async def on_domain_started_dispvm(
546549
if not self.preload_requested:
547550
timeout = self.qrexec_timeout
548551
services = ["qubes.WaitForRunningSystem"]
552+
if (
553+
self.guivm
554+
and self.features.check_with_template("gui", False)
555+
and self.features.check_with_template(
556+
"supported-feature.late-gui-daemon", False
557+
)
558+
):
559+
services.append("qubes.WaitForSession")
549560
start_tasks = []
550561
for service in services:
551562
start_tasks.append(

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
@@ -223,10 +223,11 @@ def on_feature_pre_set_preload_dispvm_max(
223223
if not self.features.check_with_template("qrexec", None):
224224
raise qubes.exc.QubesValueError("Qube does not support qrexec")
225225

226-
service = "qubes.WaitForRunningSystem"
227-
if not self.supports_preload():
226+
supported, missing_services = self.supports_preload()
227+
if not supported:
228228
raise qubes.exc.QubesValueError(
229-
"Qube does not support the RPC '%s'" % service
229+
"Qube does not support the RPC(s) '%s'"
230+
% ", ".join(missing_services)
230231
)
231232

232233
value = value or "0"
@@ -486,11 +487,12 @@ async def on_domain_preload_dispvm_used(
486487
if delay:
487488
event_log += " with a delay of %s second(s)" % f"{delay:.1f}"
488489
self.log.info(event_log)
489-
service = "qubes.WaitForRunningSystem"
490-
if not self.supports_preload():
490+
491+
supported, missing_services = self.supports_preload()
492+
if not supported:
491493
raise qubes.exc.QubesValueError(
492-
"Qube does not support the RPC '%s' but tried to preload, "
493-
"check if template is outdated" % service
494+
"Qube does not support the RPC(s) '%s' but tried to preload, "
495+
"check if template is outdated" % ", ".join(missing_services)
494496
)
495497
if delay:
496498
await asyncio.sleep(abs(delay))
@@ -796,15 +798,21 @@ def remove_preload_excess(
796798
dispvm = self.app.domains[unwanted_disp]
797799
asyncio.ensure_future(dispvm.cleanup())
798800

799-
def supports_preload(self) -> bool:
801+
def supports_preload(self) -> Tuple[bool, list]:
800802
"""
801-
Check if the necessary RPC is supported.
803+
Check if the necessary RPCs are supported.
802804
803-
:rtype: bool
805+
The first returned value indicates success while the second value is
806+
non empty and contains the missing services if they are not supported.
807+
808+
:rtype: (bool, list)
804809
"""
805810
assert isinstance(self, qubes.vm.BaseVM)
806-
service = "qubes.WaitForRunningSystem"
807-
supported_service = "supported-rpc." + service
808-
if self.features.check_with_template(supported_service, False):
809-
return True
810-
return False
811+
supported = True
812+
missing_services = []
813+
for service in ["qubes.WaitForRunningSystem", "qubes.WaitForSession"]:
814+
feature = "supported-rpc." + service
815+
if not self.features.check_with_template(feature, False):
816+
missing_services.append(service)
817+
supported = False
818+
return (supported, missing_services)

tests/dispvm_perf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,8 @@ async def run_test(self, test: TestConfig):
928928
self.dvm.features["preload-dispvm-delay"] = str(
929929
test.preload_delay
930930
)
931+
if not test.gui:
932+
self.dvm.guivm = None
931933
if test.preload_max:
932934
preload_max = test.preload_max
933935
logger.info("Setting local max feature: '%s'", preload_max)

0 commit comments

Comments
 (0)