Skip to content

Commit 4644fb9

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 4644fb9

6 files changed

Lines changed: 65 additions & 43 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/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: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -440,34 +440,34 @@ async def on_domain_started_dispvm(
440440
return
441441
timeout = self.qrexec_timeout
442442
# https://github.com/QubesOS/qubes-issues/issues/9964
443-
rpc = "qubes.WaitForRunningSystem"
444443
path = "/run/qubes-rpc:/usr/local/etc/qubes-rpc:/etc/qubes-rpc"
445-
service = '$(PATH="' + path + '" command -v ' + rpc + ")"
446-
try:
447-
self.log.info(
448-
"Preload startup waiting '%s' with '%d' seconds timeout",
449-
rpc,
450-
timeout,
451-
)
452-
await asyncio.wait_for(
453-
self.run_for_stdio(
454-
service,
455-
stdout=subprocess.DEVNULL,
456-
stderr=subprocess.DEVNULL,
457-
),
458-
timeout=timeout,
459-
)
460-
except asyncio.TimeoutError:
461-
raise qubes.exc.QubesException(
462-
"Timed out call to '%s' after '%d' seconds during preload "
463-
"startup" % (rpc, timeout)
464-
)
465-
except (subprocess.CalledProcessError, qubes.exc.QubesException):
466-
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)
470-
)
444+
for rpc in ["qubes.WaitForRunningSystem", "qubes.WaitForSession"]:
445+
service = '$(PATH="' + path + '" command -v ' + rpc + ")"
446+
try:
447+
self.log.info(
448+
"Preload startup waiting '%s' with '%d' seconds timeout",
449+
rpc,
450+
timeout,
451+
)
452+
await asyncio.wait_for(
453+
self.run_for_stdio(
454+
service,
455+
stdout=subprocess.DEVNULL,
456+
stderr=subprocess.DEVNULL,
457+
),
458+
timeout=timeout,
459+
)
460+
except asyncio.TimeoutError:
461+
raise qubes.exc.QubesException(
462+
"Timed out call to '%s' after '%d' seconds during preload "
463+
"startup" % (rpc, timeout)
464+
)
465+
except (subprocess.CalledProcessError, qubes.exc.QubesException):
466+
raise qubes.exc.QubesException(
467+
"Error on call to '%s' during preload startup. To debug, "
468+
"run the following on a new disposable of '%s': systemctl "
469+
"--failed" % (rpc, self.template)
470+
)
471471

472472
if not self.preload_requested:
473473
await self.pause()

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)