Skip to content

Commit 10f0658

Browse files
committed
Early GUI connection to preloaded disposables
Requires: QubesOS/qubes-core-admin#752 For: QubesOS/qubes-issues#10230 For: QubesOS/qubes-issues#1512
1 parent a7fc1b4 commit 10f0658

5 files changed

Lines changed: 129 additions & 23 deletions

File tree

doc/manpages/qvm-features.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,18 @@ disposables. Ensures preloaded disposables do not consume all available memory.
460460
| **Type**: `int`
461461
| **Default**: `0`
462462
463+
preload-dispvm-early-gui
464+
^^^^^^^^^^^^^^^^^^^^^^^
465+
466+
If `True`, disposables that are preloaded after this feature is set on the
467+
disposable template will wait for the disposable to have a GUI session to
468+
complete the preload stage.
469+
470+
|
471+
| **Valid on**: disposable template and preloaded disposable
472+
| **Type**: `bool`
473+
| **Default**: `False`
474+
463475
preload-dispvm
464476
^^^^^^^^^^^^^^
465477

qubesadmin/app.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -501,10 +501,10 @@ def clone_vm(
501501
raise
502502

503503
for feature, value in src_vm.features.items():
504-
if (
505-
feature.startswith("preload-dispvm")
506-
and feature != "preload-dispvm-max"
507-
):
504+
if feature.startswith("preload-dispvm") and feature not in [
505+
"preload-dispvm-max",
506+
"preload-dispvm-early-gui",
507+
]:
508508
continue
509509
try:
510510
dst_vm.features[feature] = value
@@ -520,8 +520,7 @@ def clone_vm(
520520
if vm_notes:
521521
dst_vm.set_notes(vm_notes)
522522
except qubesadmin.exc.QubesException as e:
523-
dst_vm.log.error(
524-
'Failed to clone qube notes: {!s}'.format(e))
523+
dst_vm.log.error("Failed to clone qube notes: {!s}".format(e))
525524
if not ignore_errors:
526525
raise
527526

qubesadmin/tests/mock_app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,9 @@ def default_string(self):
234234
"last-updates-check",
235235
"last-update",
236236
"prohibit-start",
237-
"preload-dispvm-max",
238237
"preload-dispvm-threshold",
238+
"preload-dispvm-max",
239+
"preload-dispvm-early-gui",
239240
"preload-dispvm",
240241
"preload-dispvm-in-progress",
241242
"preload-dispvm-completed",

qubesadmin/tests/tools/qvm_start_daemon.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ def test_031_start_gui_for_stubdomain_forced(self):
431431
async def mock_coroutine(self, mock, *args, **kwargs):
432432
mock(*args, **kwargs)
433433

434-
def test_038_start_gui_skip_preload(self):
434+
def test_035_start_gui_skip_preload(self):
435435
loop = asyncio.new_event_loop()
436436
asyncio.set_event_loop(loop)
437437
self.addCleanup(loop.close)
@@ -450,6 +450,9 @@ def test_038_start_gui_skip_preload(self):
450450
('test-disp', 'admin.vm.feature.CheckWithTemplate',
451451
'no-monitor-layout', None)] = \
452452
b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00'
453+
self.app.expected_calls[
454+
('test-disp', 'admin.vm.feature.Get', 'preload-dispvm-early-gui', None)] = \
455+
b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00'
453456
self.app.expected_calls[
454457
('test-disp', 'admin.vm.property.Get', 'virt_mode', None)] = \
455458
b'0\x00default=False type=str hvm'
@@ -486,7 +489,7 @@ def test_038_start_gui_skip_preload(self):
486489
finally:
487490
unittest.mock.patch.stopall()
488491

489-
def test_039_start_gui_for_preload(self):
492+
def test_036_start_gui_for_preload(self):
490493
loop = asyncio.new_event_loop()
491494
asyncio.set_event_loop(loop)
492495
self.addCleanup(loop.close)
@@ -548,6 +551,71 @@ def test_039_start_gui_for_preload(self):
548551
mock_start_audio.assert_called_once_with(vm)
549552
mock_start.assert_called_once_with(vm, monitor_layout=None)
550553

554+
def test_037_start_gui_for_early_preload(self):
555+
loop = asyncio.new_event_loop()
556+
asyncio.set_event_loop(loop)
557+
self.addCleanup(loop.close)
558+
559+
self.app.expected_calls[
560+
('dom0', 'admin.vm.List', None, None)] = \
561+
b'0\x00test-disp class=DispVM state=Running\n' \
562+
b'gui-vm class=AppVM state=Running'
563+
self.app.expected_calls[
564+
('test-disp', 'admin.vm.CurrentState', None, None)] = \
565+
b'0\x00power_state=Running'
566+
self.app.expected_calls[
567+
('test-disp', 'admin.vm.feature.CheckWithTemplate', 'gui', None)] = \
568+
b'0\x00True'
569+
self.app.expected_calls[
570+
('test-disp', 'admin.vm.feature.CheckWithTemplate',
571+
'no-monitor-layout', None)] = \
572+
b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00'
573+
self.app.expected_calls[
574+
('test-disp', 'admin.vm.property.Get', 'virt_mode', None)] = \
575+
b'0\x00default=False type=str hvm'
576+
self.app.expected_calls[
577+
('test-disp', 'admin.vm.property.Get', 'xid', None)] = \
578+
b'0\x00default=False type=int 3000'
579+
self.app.expected_calls[
580+
('test-disp', 'admin.vm.property.Get', 'stubdom_xid', None)] = \
581+
b'0\x00default=False type=int 3001'
582+
self.app.expected_calls[
583+
('test-disp', 'admin.vm.property.Get', 'guivm', None)] = \
584+
b'0\x00default=False type=vm gui-vm'
585+
self.app.expected_calls[
586+
('test-disp', 'admin.vm.property.Get', 'is_preload', None)] = \
587+
b'0\x00default=False type=bool True'
588+
self.app.expected_calls[
589+
('test-disp', 'admin.vm.feature.Get', 'preload-dispvm-early-gui', None)] = \
590+
b'0\x00True'
591+
592+
# pylint: disable=protected-access
593+
self.app._local_name = 'gui-vm'
594+
vm = self.app.domains['test-disp']
595+
596+
with \
597+
unittest.mock.patch('asyncio.ensure_future'), \
598+
unittest.mock.patch.object(
599+
self.launcher, 'common_guid_args', lambda vm: []
600+
), \
601+
unittest.mock.patch.object(
602+
qubesadmin.tools.qvm_start_daemon,
603+
'get_monitor_layout',
604+
unittest.mock.Mock(return_value=None)
605+
), \
606+
unittest.mock.patch.object(
607+
self.launcher, 'start_gui', unittest.mock.Mock()
608+
) as mock_start, \
609+
unittest.mock.patch.object(
610+
self.launcher, 'start_audio', unittest.mock.Mock()
611+
) as mock_start_audio:
612+
# Execute and validate
613+
self.launcher.on_property_preload_set(
614+
vm, 'property-reset:is_preload'
615+
)
616+
mock_start_audio.assert_called_once_with(vm)
617+
mock_start.assert_called_once_with(vm, monitor_layout=None)
618+
551619
def test_040_start_gui(self):
552620
loop = asyncio.new_event_loop()
553621
asyncio.set_event_loop(loop)

qubesadmin/tools/qvm_start_daemon.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,10 @@ async def send_monitor_layout(self, vm, layout=None, startup=False):
565565
if (
566566
vm.features.check_with_template("no-monitor-layout", False)
567567
or not vm.is_running()
568-
or getattr(vm, "is_preload", False)
568+
or (
569+
getattr(vm, "is_preload", False)
570+
and not vm.features.get("preload-dispvm-early-gui", None)
571+
)
569572
):
570573
return
571574

@@ -611,12 +614,13 @@ def send_monitor_layout_all(self):
611614
or vm.klass == "AdminVM"
612615
or not vm.is_running()
613616
or not vm.features.check_with_template("gui", True)
614-
or getattr(vm, "is_preload", False)
617+
or (
618+
getattr(vm, "is_preload", False)
619+
and not vm.features.get("preload-dispvm-early-gui", None)
620+
)
615621
):
616622
continue
617-
asyncio.ensure_future(
618-
self.send_monitor_layout(vm, monitor_layout)
619-
)
623+
asyncio.ensure_future(self.send_monitor_layout(vm, monitor_layout))
620624

621625
@staticmethod
622626
def kde_guid_args(vm):
@@ -724,7 +728,9 @@ async def start_gui_for_vm(self, vm, monitor_layout=None):
724728
:param monitor_layout: monitor layout to send; if None, fetch it from
725729
local X server.
726730
"""
727-
if getattr(vm, "is_preload", False):
731+
if getattr(vm, "is_preload", False) and not vm.features.get(
732+
"preload-dispvm-early-gui", None
733+
):
728734
return
729735
guid_cmd = self.common_guid_args(vm)
730736
guid_cmd.extend(["-d", str(vm.xid)])
@@ -752,7 +758,9 @@ async def start_gui_for_stubdomain(self, vm, force=False):
752758
753759
This function is a coroutine.
754760
"""
755-
if getattr(vm, "is_preload", False):
761+
if getattr(vm, "is_preload", False) and not vm.features.get(
762+
"preload-dispvm-early-gui", None
763+
):
756764
return
757765
want_stubdom = force
758766
if not want_stubdom and vm.features.check_with_template(
@@ -785,7 +793,9 @@ async def start_audio_for_vm(self, vm):
785793
786794
:param vm: VM for which start AUDIO daemon
787795
"""
788-
if getattr(vm, "is_preload", False):
796+
if getattr(vm, "is_preload", False) and not vm.features.get(
797+
"preload-dispvm-early-gui", None
798+
):
789799
return
790800
pacat_cmd = [
791801
PACAT_DAEMON_PATH,
@@ -807,7 +817,9 @@ async def start_gui(self, vm, force_stubdom=False, monitor_layout=None):
807817
one for target AppVM is running.
808818
:param monitor_layout: monitor layout configuration
809819
"""
810-
if getattr(vm, "is_preload", False):
820+
if getattr(vm, "is_preload", False) and not vm.features.get(
821+
"preload-dispvm-early-gui", None
822+
):
811823
return
812824
guivm = getattr(vm, "guivm", None)
813825
if guivm != vm.app.local_name:
@@ -830,7 +842,9 @@ async def start_audio(self, vm):
830842
831843
:param vm: VM for which AUDIO daemon should be started
832844
"""
833-
if getattr(vm, "is_preload", False):
845+
if getattr(vm, "is_preload", False) and not vm.features.get(
846+
"preload-dispvm-early-gui", None
847+
):
834848
return
835849
audiovm = getattr(vm, "audiovm", None)
836850
if audiovm != vm.app.local_name:
@@ -849,7 +863,10 @@ async def start_audio(self, vm):
849863
def on_domain_spawn(self, vm, _event, **kwargs):
850864
"""Handler of 'domain-spawn' event, starts GUI daemon for stubdomain"""
851865

852-
if not self.is_watched(vm) or getattr(vm, "is_preload", False):
866+
if not self.is_watched(vm) or (
867+
getattr(vm, "is_preload", False)
868+
and not vm.features.get("preload-dispvm-early-gui", None)
869+
):
853870
return
854871

855872
try:
@@ -874,7 +891,10 @@ def on_domain_start(self, vm, _event, **kwargs):
874891
"""Handler of 'domain-start' event, starts GUI/AUDIO daemon for
875892
actual VM"""
876893

877-
if not self.is_watched(vm) or getattr(vm, "is_preload", False):
894+
if not self.is_watched(vm) or (
895+
getattr(vm, "is_preload", False)
896+
and not vm.features.get("preload-dispvm-early-gui", None)
897+
):
878898
return
879899

880900
self.xid_cache[vm.name] = vm.xid, vm.stubdom_xid
@@ -911,7 +931,10 @@ def on_connection_established(self, _subject, _event, **_kwargs):
911931
if (
912932
vm.klass == "AdminVM"
913933
or not self.is_watched(vm)
914-
or getattr(vm, "is_preload", False)
934+
or (
935+
getattr(vm, "is_preload", False)
936+
and not vm.features.get("preload-dispvm-early-gui", None)
937+
)
915938
):
916939
continue
917940

@@ -960,7 +983,10 @@ def on_property_preload_set(self, vm, _event, **_kwargs):
960983
if (
961984
not vm.klass == "DispVM"
962985
or not self.is_watched(vm)
963-
or getattr(vm, "is_preload", False)
986+
or (
987+
getattr(vm, "is_preload", False)
988+
and not vm.features.get("preload-dispvm-early-gui", None)
989+
)
964990
or not vm.is_running()
965991
):
966992
return

0 commit comments

Comments
 (0)