Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions doc/manpages/qvm-features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,18 @@ disposables. Ensures preloaded disposables do not consume all available memory.
| **Type**: `int`
| **Default**: `0`

preload-dispvm-early-gui
^^^^^^^^^^^^^^^^^^^^^^^

If `True`, disposables that are preloaded after this feature is set on the
disposable template will wait for the disposable to have a GUI session to
complete the preload stage.

|
| **Valid on**: disposable template and preloaded disposable
| **Type**: `bool`
| **Default**: `False`

preload-dispvm
^^^^^^^^^^^^^^

Expand Down
11 changes: 5 additions & 6 deletions qubesadmin/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,10 +501,10 @@ def clone_vm(
raise

for feature, value in src_vm.features.items():
if (
feature.startswith("preload-dispvm")
and feature != "preload-dispvm-max"
):
if feature.startswith("preload-dispvm") and feature not in [
"preload-dispvm-max",
"preload-dispvm-early-gui",
]:
continue
try:
dst_vm.features[feature] = value
Expand All @@ -520,8 +520,7 @@ def clone_vm(
if vm_notes:
dst_vm.set_notes(vm_notes)
except qubesadmin.exc.QubesException as e:
dst_vm.log.error(
'Failed to clone qube notes: {!s}'.format(e))
dst_vm.log.error("Failed to clone qube notes: {!s}".format(e))
if not ignore_errors:
raise

Expand Down
3 changes: 2 additions & 1 deletion qubesadmin/tests/mock_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,9 @@ def default_string(self):
"last-updates-check",
"last-update",
"prohibit-start",
"preload-dispvm-max",
"preload-dispvm-threshold",
"preload-dispvm-max",
"preload-dispvm-early-gui",
"preload-dispvm",
"preload-dispvm-in-progress",
"preload-dispvm-completed",
Expand Down
72 changes: 70 additions & 2 deletions qubesadmin/tests/tools/qvm_start_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ def test_031_start_gui_for_stubdomain_forced(self):
async def mock_coroutine(self, mock, *args, **kwargs):
mock(*args, **kwargs)

def test_038_start_gui_skip_preload(self):
def test_035_start_gui_skip_preload(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.addCleanup(loop.close)
Expand All @@ -450,6 +450,9 @@ def test_038_start_gui_skip_preload(self):
('test-disp', 'admin.vm.feature.CheckWithTemplate',
'no-monitor-layout', None)] = \
b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00'
self.app.expected_calls[
('test-disp', 'admin.vm.feature.Get', 'preload-dispvm-early-gui', None)] = \
b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00'
self.app.expected_calls[
('test-disp', 'admin.vm.property.Get', 'virt_mode', None)] = \
b'0\x00default=False type=str hvm'
Expand Down Expand Up @@ -486,7 +489,7 @@ def test_038_start_gui_skip_preload(self):
finally:
unittest.mock.patch.stopall()

def test_039_start_gui_for_preload(self):
def test_036_start_gui_for_preload(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.addCleanup(loop.close)
Expand Down Expand Up @@ -548,6 +551,71 @@ def test_039_start_gui_for_preload(self):
mock_start_audio.assert_called_once_with(vm)
mock_start.assert_called_once_with(vm, monitor_layout=None)

def test_037_start_gui_for_early_preload(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.addCleanup(loop.close)

self.app.expected_calls[
('dom0', 'admin.vm.List', None, None)] = \
b'0\x00test-disp class=DispVM state=Running\n' \
b'gui-vm class=AppVM state=Running'
self.app.expected_calls[
('test-disp', 'admin.vm.CurrentState', None, None)] = \
b'0\x00power_state=Running'
self.app.expected_calls[
('test-disp', 'admin.vm.feature.CheckWithTemplate', 'gui', None)] = \
b'0\x00True'
self.app.expected_calls[
('test-disp', 'admin.vm.feature.CheckWithTemplate',
'no-monitor-layout', None)] = \
b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00'
self.app.expected_calls[
('test-disp', 'admin.vm.property.Get', 'virt_mode', None)] = \
b'0\x00default=False type=str hvm'
self.app.expected_calls[
('test-disp', 'admin.vm.property.Get', 'xid', None)] = \
b'0\x00default=False type=int 3000'
self.app.expected_calls[
('test-disp', 'admin.vm.property.Get', 'stubdom_xid', None)] = \
b'0\x00default=False type=int 3001'
self.app.expected_calls[
('test-disp', 'admin.vm.property.Get', 'guivm', None)] = \
b'0\x00default=False type=vm gui-vm'
self.app.expected_calls[
('test-disp', 'admin.vm.property.Get', 'is_preload', None)] = \
b'0\x00default=False type=bool True'
self.app.expected_calls[
('test-disp', 'admin.vm.feature.Get', 'preload-dispvm-early-gui', None)] = \
b'0\x00True'

# pylint: disable=protected-access
self.app._local_name = 'gui-vm'
vm = self.app.domains['test-disp']

with \
unittest.mock.patch('asyncio.ensure_future'), \
unittest.mock.patch.object(
self.launcher, 'common_guid_args', lambda vm: []
), \
unittest.mock.patch.object(
qubesadmin.tools.qvm_start_daemon,
'get_monitor_layout',
unittest.mock.Mock(return_value=None)
), \
unittest.mock.patch.object(
self.launcher, 'start_gui', unittest.mock.Mock()
) as mock_start, \
unittest.mock.patch.object(
self.launcher, 'start_audio', unittest.mock.Mock()
) as mock_start_audio:
# Execute and validate
self.launcher.on_property_preload_set(
vm, 'property-reset:is_preload'
)
mock_start_audio.assert_called_once_with(vm)
mock_start.assert_called_once_with(vm, monitor_layout=None)

def test_040_start_gui(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
Expand Down
57 changes: 43 additions & 14 deletions qubesadmin/tools/qvm_start_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,10 @@ async def send_monitor_layout(self, vm, layout=None, startup=False):
if (
vm.features.check_with_template("no-monitor-layout", False)
or not vm.is_running()
or getattr(vm, "is_preload", False)
or (
getattr(vm, "is_preload", False)
and not vm.features.get("preload-dispvm-early-gui", None)
)
):
return

Expand Down Expand Up @@ -611,12 +614,14 @@ def send_monitor_layout_all(self):
or vm.klass == "AdminVM"
or not vm.is_running()
or not vm.features.check_with_template("gui", True)
or getattr(vm, "is_preload", False)
):
continue
asyncio.ensure_future(
self.send_monitor_layout(vm, monitor_layout)
)
if (
getattr(vm, "is_preload", False)
and not vm.features.get("preload-dispvm-early-gui", None)
):
continue
asyncio.ensure_future(self.send_monitor_layout(vm, monitor_layout))

@staticmethod
def kde_guid_args(vm):
Expand Down Expand Up @@ -724,7 +729,9 @@ async def start_gui_for_vm(self, vm, monitor_layout=None):
:param monitor_layout: monitor layout to send; if None, fetch it from
local X server.
"""
if getattr(vm, "is_preload", False):
if getattr(vm, "is_preload", False) and not vm.features.get(
"preload-dispvm-early-gui", None
):
return
guid_cmd = self.common_guid_args(vm)
guid_cmd.extend(["-d", str(vm.xid)])
Expand Down Expand Up @@ -752,7 +759,9 @@ async def start_gui_for_stubdomain(self, vm, force=False):

This function is a coroutine.
"""
if getattr(vm, "is_preload", False):
if getattr(vm, "is_preload", False) and not vm.features.get(
"preload-dispvm-early-gui", None
):
return
want_stubdom = force
if not want_stubdom and vm.features.check_with_template(
Expand Down Expand Up @@ -785,7 +794,9 @@ async def start_audio_for_vm(self, vm):

:param vm: VM for which start AUDIO daemon
"""
if getattr(vm, "is_preload", False):
if getattr(vm, "is_preload", False) and not vm.features.get(
"preload-dispvm-early-gui", None
):
return
pacat_cmd = [
PACAT_DAEMON_PATH,
Expand All @@ -807,7 +818,9 @@ async def start_gui(self, vm, force_stubdom=False, monitor_layout=None):
one for target AppVM is running.
:param monitor_layout: monitor layout configuration
"""
if getattr(vm, "is_preload", False):
if getattr(vm, "is_preload", False) and not vm.features.get(
"preload-dispvm-early-gui", None
):
return
guivm = getattr(vm, "guivm", None)
if guivm != vm.app.local_name:
Expand All @@ -830,7 +843,9 @@ async def start_audio(self, vm):

:param vm: VM for which AUDIO daemon should be started
"""
if getattr(vm, "is_preload", False):
if getattr(vm, "is_preload", False) and not vm.features.get(
"preload-dispvm-early-gui", None
):
return
audiovm = getattr(vm, "audiovm", None)
if audiovm != vm.app.local_name:
Expand All @@ -849,7 +864,10 @@ async def start_audio(self, vm):
def on_domain_spawn(self, vm, _event, **kwargs):
"""Handler of 'domain-spawn' event, starts GUI daemon for stubdomain"""

if not self.is_watched(vm) or getattr(vm, "is_preload", False):
if not self.is_watched(vm) or (
getattr(vm, "is_preload", False)
and not vm.features.get("preload-dispvm-early-gui", None)
):
return

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

if not self.is_watched(vm) or getattr(vm, "is_preload", False):
if not self.is_watched(vm) or (
getattr(vm, "is_preload", False)
and not vm.features.get("preload-dispvm-early-gui", None)
):
return

self.xid_cache[vm.name] = vm.xid, vm.stubdom_xid
Expand Down Expand Up @@ -911,7 +932,12 @@ def on_connection_established(self, _subject, _event, **_kwargs):
if (
vm.klass == "AdminVM"
or not self.is_watched(vm)
or getattr(vm, "is_preload", False)
or (
getattr(vm, "is_preload", False)
and not vm.features.get(
"preload-dispvm-early-gui", None
)
)
):
continue

Expand Down Expand Up @@ -960,7 +986,10 @@ def on_property_preload_set(self, vm, _event, **_kwargs):
if (
not vm.klass == "DispVM"
or not self.is_watched(vm)
or getattr(vm, "is_preload", False)
or (
getattr(vm, "is_preload", False)
and not vm.features.get("preload-dispvm-early-gui", None)
)
or not vm.is_running()
):
return
Expand Down