diff --git a/doc/manpages/qvm-features.rst b/doc/manpages/qvm-features.rst index efbf56a..143da80 100644 --- a/doc/manpages/qvm-features.rst +++ b/doc/manpages/qvm-features.rst @@ -59,13 +59,28 @@ List of known features gui ^^^ -Qube provide any kind of GUI. Setting this feature to :py:obj:`False` disable -GUI for given qubes - both gui-agent based and emulated VGA based one. Setting -this feature to :py:obj:`True` enable gui-agent based GUI (i.e. with support of -tools installed inside of qube). Not setting this feature at all, enable showing -VGA emulated output. +Qube has gui-agent installed. Setting this feature to :py:obj:`True` enables GUI +based on a gui-agent installed inside the VM. +See also `gui-emulated` feature. -Default: show emulated VGA output only +If neither `gui` nor `gui-emulated` is set, emulated VGA is used (if +applicable for given VM virtualization mode). + +gui-emulated +^^^^^^^^^^^^ + +Qube provides GUI through emulated VGA. Setting this feature to +:py:obj:`True` enables emulated VGA output. Note that when gui-agent connects to +actual VM, emulated VGA output is closed (unless `debug` property is set to +:py:obj:`True`). It's possible to open emulated VGA output for a running qube, +regardless of this feature, using `qvm-start-gui --force-stubdomain QUBE_NAME` +command. + +This feature is applicable only when qube's `virt_mode` is set to `hvm`. +See also `gui` feature. + +If neither `gui` nor `gui-emulated` is set, emulated VGA is used (if +applicable for given VM virtualization mode). qrexec ^^^^^^ diff --git a/doc/manpages/qvm-prefs.rst b/doc/manpages/qvm-prefs.rst index 860c557..afd89d7 100644 --- a/doc/manpages/qvm-prefs.rst +++ b/doc/manpages/qvm-prefs.rst @@ -95,6 +95,8 @@ default_user Default user used by :manpage:`qvm-run(1)`. Note that it make sense only on non-standard template, as the standard one always have "user" account. + TemplateBasedVM use its template's value as a default. + dispvm_allowed Property type: bool @@ -117,7 +119,9 @@ kernel Accepted values: kernel version, empty Kernel version to use. Setting to empty value will use bootloader installed - in root volume (of VM's template) - available only for HVM + in root volume (of VM's template) - available only for HVM. + + TemplateBasedVM use its template's value as a default. kernelopts Accepted values: string @@ -128,6 +132,8 @@ kernelopts Some helpful options (for debugging purposes): ``earlyprintk=xen``, ``init=/bin/bash`` + TemplateBasedVM use its template's value as a default. + label Accepted values: ``red``, ``orange``, ``yellow``, ``green``, ``gray``, ``blue``, ``purple``, ``black`` @@ -151,6 +157,8 @@ maxmem qmemman disabled, this will be overridden by *memory* property (at VM startup). + TemplateBasedVM use its template's value as a default. + memory Accepted values: memory size in MB @@ -158,6 +166,8 @@ memory - before qmemman starts managing memory for this VM. For VM with qmemman disabled, this is static memory size. + TemplateBasedVM use its template's value as a default. + name Accepted values: alphanumerical name @@ -184,6 +194,8 @@ qrexec_timeout Ignored if qrexec not installed at all (`qrexec` feature not set, see :manpage:`qvm-features(1)`). + TemplateBasedVM use its template's value as a default. + stubdom_mem Accepted values: memory in MB @@ -202,12 +214,16 @@ vcpus Number of CPU (cores) available to VM. Some VM types (eg DispVM) will not work properly with more than one CPU. + TemplateBasedVM use its template's value as a default. + virt_mode Accepted values: ``hvm``, ``pv`` Virtualisation mode in VM should be started. ``hvm`` allow to install operating system without Xen-specific integration. + TemplateBasedVM use its template's value as a default. + Authors ------- diff --git a/qubesadmin/tests/tools/qvm_start_gui.py b/qubesadmin/tests/tools/qvm_start_gui.py index d9e2e83..0b6c7b5 100644 --- a/qubesadmin/tests/tools/qvm_start_gui.py +++ b/qubesadmin/tests/tools/qvm_start_gui.py @@ -87,6 +87,10 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase): self.app.expected_calls[ ('dom0', 'admin.label.Index', 'red', None)] = \ b'0\x001' + self.app.expected_calls[ + ('test-vm', 'admin.vm.feature.CheckWithTemplate', + 'rpc-clipboard', None)] = \ + b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00' with unittest.mock.patch.object(self.launcher, 'kde_guid_args') as \ kde_mock: @@ -117,6 +121,10 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase): self.app.expected_calls[ ('dom0', 'admin.label.Index', 'red', None)] = \ b'0\x001' + self.app.expected_calls[ + ('test-vm', 'admin.vm.feature.CheckWithTemplate', + 'rpc-clipboard', None)] = \ + b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00' with unittest.mock.patch.object(self.launcher, 'kde_guid_args') as \ kde_mock: @@ -131,6 +139,40 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase): self.assertAllCalled() + def test_012_common_args_rpc_clipboard(self): + self.app.expected_calls[ + ('dom0', 'admin.vm.List', None, None)] = \ + b'0\x00test-vm class=AppVM state=Running\n' + self.app.expected_calls[ + ('test-vm', 'admin.vm.property.Get', 'label', None)] = \ + b'0\x00default=False type=label red' + self.app.expected_calls[ + ('test-vm', 'admin.vm.property.Get', 'debug', None)] = \ + b'0\x00default=False type=bool False' + self.app.expected_calls[ + ('dom0', 'admin.label.Get', 'red', None)] = \ + b'0\x000xff0000' + self.app.expected_calls[ + ('dom0', 'admin.label.Index', 'red', None)] = \ + b'0\x001' + self.app.expected_calls[ + ('test-vm', 'admin.vm.feature.CheckWithTemplate', + 'rpc-clipboard', None)] = \ + b'0\x001' + + with unittest.mock.patch.object(self.launcher, 'kde_guid_args') as \ + kde_mock: + kde_mock.return_value = [] + + args = self.launcher.common_guid_args(self.app.domains['test-vm']) + self.assertEqual(args, [ + '/usr/bin/qubes-guid', '-N', 'test-vm', + '-c', '0xff0000', + '-i', '/usr/share/icons/hicolor/128x128/devices/appvm-red.png', + '-l', '1', '-q', '-Q']) + + self.assertAllCalled() + @unittest.mock.patch('asyncio.create_subprocess_exec') def test_020_start_gui_for_vm(self, proc_mock): loop = asyncio.new_event_loop() @@ -186,10 +228,6 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase): self.app.expected_calls[ ('test-vm', 'admin.vm.property.Get', 'debug', None)] = \ b'0\x00default=False type=bool False' - self.app.expected_calls[ - ('test-vm', 'admin.vm.feature.CheckWithTemplate', 'rpc-clipboard', - None)] = \ - b'0\x00True' self.app.expected_calls[ ('test-vm', 'admin.vm.feature.CheckWithTemplate', 'no-monitor-layout', None)] = \ @@ -199,7 +237,7 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase): loop.run_until_complete(self.launcher.start_gui_for_vm( self.app.domains['test-vm'])) # common arguments dropped for simplicity - proc_mock.assert_called_once_with('-d', '3000', '-n', '-Q') + proc_mock.assert_called_once_with('-d', '3000', '-n') self.assertAllCalled() @@ -226,10 +264,6 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase): self.app.expected_calls[ ('test-vm', 'admin.vm.property.Get', 'debug', None)] = \ b'0\x00default=False type=bool False' - self.app.expected_calls[ - ('test-vm', 'admin.vm.feature.CheckWithTemplate', 'rpc-clipboard', - None)] = \ - b'0\x00True' self.app.expected_calls[ ('test-vm', 'admin.vm.feature.CheckWithTemplate', 'no-monitor-layout', None)] = \ @@ -252,7 +286,7 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase): self.app.domains['test-vm'])) # common arguments dropped for simplicity mock_proc.assert_called_once_with( - '-d', '3000', '-n', '-Q', '-K', '1234') + '-d', '3000', '-n', '-K', '1234') finally: unittest.mock.patch.stopall() @@ -275,6 +309,43 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase): self.app.expected_calls[ ('test-vm', 'admin.vm.feature.CheckWithTemplate', 'gui', None)] = \ b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00' + self.app.expected_calls[ + ('test-vm', 'admin.vm.feature.CheckWithTemplate', 'gui-emulated', + None)] = \ + b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00' + proc_mock = unittest.mock.Mock() + with unittest.mock.patch('asyncio.create_subprocess_exec', + lambda *args: self.mock_coroutine(proc_mock, *args)): + with unittest.mock.patch.object(self.launcher, + 'common_guid_args', lambda vm: []): + loop.run_until_complete(self.launcher.start_gui_for_stubdomain( + self.app.domains['test-vm'])) + # common arguments dropped for simplicity + proc_mock.assert_called_once_with('-d', '3001', '-t', '3000') + + self.assertAllCalled() + + def test_031_start_gui_for_stubdomain_forced(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-vm class=AppVM state=Running\n' + self.app.expected_calls[ + ('test-vm', 'admin.vm.property.Get', 'xid', None)] = \ + b'0\x00default=False type=int 3000' + self.app.expected_calls[ + ('test-vm', 'admin.vm.property.Get', 'stubdom_xid', None)] = \ + b'0\x00default=False type=int 3001' + # self.app.expected_calls[ + # ('test-vm', 'admin.vm.feature.CheckWithTemplate', 'gui', None)] = \ + # b'0\x00' + self.app.expected_calls[ + ('test-vm', 'admin.vm.feature.CheckWithTemplate', 'gui-emulated', + None)] = \ + b'0\x001' proc_mock = unittest.mock.Mock() with unittest.mock.patch('asyncio.create_subprocess_exec', lambda *args: self.mock_coroutine(proc_mock, *args)): diff --git a/qubesadmin/tools/qvm_start_gui.py b/qubesadmin/tools/qvm_start_gui.py index bbc81c1..d5e8486 100644 --- a/qubesadmin/tools/qvm_start_gui.py +++ b/qubesadmin/tools/qvm_start_gui.py @@ -167,6 +167,9 @@ class GUILauncher(object): else: guid_cmd += ['-q'] + if vm.features.check_with_template('rpc-clipboard', False): + guid_cmd.extend(['-Q']) + guid_cmd += self.kde_guid_args(vm) return guid_cmd @@ -191,9 +194,6 @@ class GUILauncher(object): if vm.virt_mode == 'hvm': guid_cmd.extend(['-n']) - if vm.features.check_with_template('rpc-clipboard', False): - guid_cmd.extend(['-Q']) - stubdom_guid_pidfile = self.guid_pidfile(vm.stubdom_xid) if not vm.debug and os.path.exists(stubdom_guid_pidfile): # Terminate stubdom guid once "real" gui agent connects @@ -215,9 +215,13 @@ class GUILauncher(object): This function is a coroutine. ''' want_stubdom = force - # if no 'gui' feature set at all, assume no gui agent installed if not want_stubdom and \ - vm.features.check_with_template('gui', None) is None: + vm.features.check_with_template('gui-emulated', False): + want_stubdom = True + # if no 'gui' or 'gui-emulated' feature set at all, use emulated GUI + if not want_stubdom and \ + vm.features.check_with_template('gui', None) is None and \ + vm.features.check_with_template('gui-emulated', None) is None: want_stubdom = True if not want_stubdom and vm.debug: want_stubdom = True @@ -241,13 +245,13 @@ class GUILauncher(object): :param force_stubdom: Force GUI daemon for stubdomain, even if the one for target AppVM is running. ''' - if not vm.features.check_with_template('gui', True): - return - if vm.virt_mode == 'hvm': yield from self.start_gui_for_stubdomain(vm, force=force_stubdom) + if not vm.features.check_with_template('gui', True): + return + if not os.path.exists(self.guid_pidfile(vm.xid)): yield from self.start_gui_for_vm(vm, monitor_layout=monitor_layout)