From 0f9ca47d90a99bccd96f65c4dc85905c1e2b09b7 Mon Sep 17 00:00:00 2001 From: Wojtek Porczyk Date: Sat, 5 Mar 2016 10:57:58 +0100 Subject: [PATCH] qubes/ext/guid: Move gui-related code to extension --- qubes/ext/gui.py | 92 ++++++++++++++++++ qubes/tools/__init__.py | 4 +- qubes/tools/qubes_monitor_layout_notify.py | 107 +++++++++++++++++++++ qubes/vm/qubesvm.py | 74 +++----------- rpm_spec/core-dom0.spec | 2 + setup.py | 1 + 6 files changed, 219 insertions(+), 61 deletions(-) create mode 100644 qubes/ext/gui.py create mode 100644 qubes/tools/qubes_monitor_layout_notify.py diff --git a/qubes/ext/gui.py b/qubes/ext/gui.py new file mode 100644 index 00000000..2d0b044e --- /dev/null +++ b/qubes/ext/gui.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# vim: fileencoding=utf-8 + +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2010-2016 Joanna Rutkowska +# Copyright (C) 2013-2016 Marek Marczykowski-Górecki +# +# Copyright (C) 2014-2016 Wojtek Porczyk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +import os +import subprocess + +import qubes.config +import qubes.ext + +class GUI(qubes.ext.Extension): + @qubes.ext.handler('domain-start', 'domain-cmd-pre-start') + def start_guid(self, vm, start_guid, preparing_dvm=False, + extra_guid_args=None, **kwargs): + '''Launch gui daemon. + + GUI daemon securely displays windows from domain. + ''' # pylint: disable=no-self-use,unused-argument + + if not start_guid or preparing_dvm \ + or not os.path.exists('/var/run/shm.id'): + return + + if not vm.features.check_with_template('gui', not vm.hvm): + vm.log.debug('Not starting gui daemon, disabled by features') + return + + if not os.getenv('DISPLAY'): + vm.log.error('Not starting gui daemon, no DISPLAY set') + return + + vm.log.info('Starting gui daemon') + + guid_cmd = [qubes.config.system_path['qubes_guid_path'], + '-d', str(vm.xid), '-N', vm.name, + '-c', vm.label.color, + '-i', vm.label.icon_path, + '-l', str(vm.label.index)] + if extra_guid_args is not None: + guid_cmd += extra_guid_args + + if vm.debug: + guid_cmd += ['-v', '-v'] + +# elif not verbose: + else: + guid_cmd += ['-q'] + + retcode = subprocess.call(guid_cmd) + if retcode != 0: + raise qubes.exc.QubesVMError(vm, + 'Cannot start qubes-guid for domain {!r}'.format(vm.name)) + + vm.notify_monitor_layout() + vm.wait_for_session() + + + @qubes.ext.handler('monitor-layout-change') + def on_monitor_layout_change(self, vm, monitor_layout): + # pylint: disable=no-self-use + if vm.features.check_with_template('no-monitor-layout', False) \ + or not vm.is_running(): + return + + pipe = vm.run('QUBESRPC qubes.SetMonitorLayout dom0', + passio_popen=True, wait=True) + + pipe.stdin.write(''.join(monitor_layout)) + pipe.stdin.close() + pipe.wait() diff --git a/qubes/tools/__init__.py b/qubes/tools/__init__.py index c177fc77..78757229 100644 --- a/qubes/tools/__init__.py +++ b/qubes/tools/__init__.py @@ -152,6 +152,7 @@ class QubesArgumentParser(argparse.ArgumentParser): want_app_no_instance=False, want_force_root=False, want_vm=False, + want_vm_optional=False, want_vm_all=False, **kwargs): @@ -161,6 +162,7 @@ class QubesArgumentParser(argparse.ArgumentParser): self._want_app_no_instance = want_app_no_instance self._want_force_root = want_force_root self._want_vm = want_vm + self._want_vm_optional = want_vm_optional self._want_vm_all = want_vm_all if self._want_app: @@ -193,7 +195,7 @@ class QubesArgumentParser(argparse.ArgumentParser): nargs = '?' else: vmchoice = self - nargs = None + nargs = '?' if self._want_vm_optional else None vmchoice.add_argument('vm', metavar='VMNAME', action='store', nargs=nargs, diff --git a/qubes/tools/qubes_monitor_layout_notify.py b/qubes/tools/qubes_monitor_layout_notify.py new file mode 100644 index 00000000..8294017e --- /dev/null +++ b/qubes/tools/qubes_monitor_layout_notify.py @@ -0,0 +1,107 @@ +#!/usr/bin/python2 -O +# vim: fileencoding=utf-8 + +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2015-2016 Joanna Rutkowska +# Copyright (C) 2015-2016 Wojtek Porczyk +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +'''qvm-create - Create new Qubes OS store''' + +# TODO allow to set properties and create domains + +import re +import subprocess +import threading + +import qubes.tools + +# "LVDS connected 1024x768+0+0 (normal left inverted right) 304mm x 228mm" +REGEX_OUTPUT = re.compile(r''' + (?x) # ignore whitespace + ^ # start of string + (?P[A-Za-z0-9\-]*)[ ] # LVDS VGA etc + (?P(dis)?connected)[ ]# dis/connected + (?P(primary)?)[ ]? + (( # a group + (?P\d+)x # either 1024x768+0+0 + (?P\d+)[+] + (?P\d+)[+] + (?P\d+) + )|[\D]) # or not a digit + .* # ignore rest of line + ''') + + +def get_monitor_layout(): + outputs = [] + + for line in subprocess.Popen( + ['xrandr', '-q'], stdout=subprocess.PIPE).stdout: + if not line.startswith("Screen") and not line.startswith(" "): + output_params = REGEX_OUTPUT.match(line).groupdict() + if output_params['width']: + outputs.append("%s %s %s %s\n" % ( + output_params['width'], + output_params['height'], + output_params['x'], + output_params['y'])) + return outputs + + +parser = qubes.tools.QubesArgumentParser( + description='Send monitor layout to one qube or to all of them', + want_app=True, + want_vm=True, + want_vm_optional=True) + + +def main(args=None): + '''Main routine of :program:`qubes-create`. + + :param list args: Optional arguments to override those delivered from \ + command line. + ''' + + args = parser.parse_args(args) + monitor_layout = get_monitor_layout() + + # notify only if we've got a non-empty monitor_layout or else we + # break proper qube resolution set by gui-agent + if not monitor_layout: + args.app.log.error('cannot get monitor layout') + return 1 + + if args.vm: + args.vm.fire_event('monitor-layout-change', monitor_layout) + else: + subprocess.check_call(['killall', '-HUP', 'qubes-guid']) + threads = [] + + for vm in args.app.domains: + thread = threading.Thread(name=vm.name, target=vm.fire_event, + args=('monitor-layout-change',), + kwargs={'monitor_layout': monitor_layout}) + threads.append(thread) + thread.run() + + for thread in threads: + thread.join() + + return 0 diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 1f30dbe8..7408ff3d 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -624,6 +624,9 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): self.log.info('Starting {}'.format(self.name)) + self.fire_event_pre('domain-pre-start', preparing_dvm=preparing_dvm, + start_guid=start_guid, mem_required=mem_required) + self.verify_files() if self.netvm is not None: @@ -660,13 +663,16 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): self.libvirt_domain.createWithFlags(libvirt.VIR_DOMAIN_START_PAUSED) try: - if preparing_dvm: - self.features['dvm'] = True + self.fire_event('domain-spawn', + preparing_dvm=preparing_dvm, start_guid=start_guid) self.log.info('Setting Qubes DB info for the VM') self.start_qubesdb() self.create_qdb_entries() + if preparing_dvm: + self.qdb.write('/dvm', '1') + self.log.info('Updating firewall rules') for vm in self.app.domains: @@ -691,10 +697,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): if not preparing_dvm: self.start_qrexec_daemon() - if start_guid and not preparing_dvm \ - and os.path.exists('/var/run/shm.id'): - self.start_guid() - self.fire_event('domain-start', preparing_dvm=preparing_dvm, start_guid=start_guid) @@ -850,9 +852,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): raise qubes.exc.QubesVMError( self, 'Domain {!r}: qrexec not connected'.format(self.name)) - if gui and os.getenv("DISPLAY") is not None \ - and not self.is_guid_running(): - self.start_guid() + self.fire_event_pre('domain-cmd-pre-run', start_guid=gui) args = [qubes.config.system_path['qrexec_client_path'], '-d', str(self.name), @@ -951,46 +951,17 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): return qmemman_client - def start_guid(self, extra_guid_args=None): - '''Launch gui daemon. - - GUI daemon securely displays windows from domain. - - :param list extra_guid_args: Extra argv to pass to :program:`guid`. - ''' - - self.log.info('Starting gui daemon') - - guid_cmd = [qubes.config.system_path['qubes_guid_path'], - '-d', str(self.xid), "-N", self.name, - '-c', self.label.color, - '-i', self.label.icon_path, - '-l', str(self.label.index)] - if extra_guid_args is not None: - guid_cmd += extra_guid_args - - if self.debug: - guid_cmd += ['-v', '-v'] - -# elif not verbose: - else: - guid_cmd += ['-q'] - - retcode = subprocess.call(guid_cmd) - if retcode != 0: - raise qubes.exc.QubesVMError(self, - 'Cannot start qubes-guid for domain {!r}'.format(self.name)) - - self.notify_monitor_layout() - self.wait_for_session() - - def start_qrexec_daemon(self): '''Start qrexec daemon. :raises OSError: when starting fails. ''' + if not self.features.check_with_template('qrexec', not self.hvm): + self.log.debug( + 'Not starting the qrexec daemon, disabled by features') + return + self.log.debug('Starting the qrexec daemon') qrexec_args = [str(self.xid), self.name, self.default_user] if not self.debug: @@ -1036,23 +1007,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): p.communicate(input=self.default_user) - # TODO event, extension - def notify_monitor_layout(self): - try: - import qubes.monitorlayoutnotify - monitor_layout = qubes.monitorlayoutnotify.get_monitor_layout() - - # notify qube only if we've got a non-empty monitor_layout or else we - # break proper qube resolution set by gui-agent - if not monitor_layout: - return - - self.log.info('Sending monitor layout') - qubes.monitorlayoutnotify.notify_vm(self, monitor_layout) - except ImportError: - self.log.warning('Monitor layout notify module not installed') - - # TODO move to storage def create_on_disk(self, source_template=None): '''Create files needed for VM. diff --git a/rpm_spec/core-dom0.spec b/rpm_spec/core-dom0.spec index 73bbe1fb..77eb10e4 100644 --- a/rpm_spec/core-dom0.spec +++ b/rpm_spec/core-dom0.spec @@ -231,6 +231,7 @@ fi %{python_sitelib}/qubes/tools/__init__.py* %{python_sitelib}/qubes/tools/qmemmand.py* %{python_sitelib}/qubes/tools/qubes_create.py* +%{python_sitelib}/qubes/tools/qubes_monitor_layout_notify.py* %{python_sitelib}/qubes/tools/qubes_prefs.py* %{python_sitelib}/qubes/tools/qvm_create.py* %{python_sitelib}/qubes/tools/qvm_kill.py* @@ -243,6 +244,7 @@ fi %dir %{python_sitelib}/qubes/ext %{python_sitelib}/qubes/ext/__init__.py* +%{python_sitelib}/qubes/ext/gui.py* %{python_sitelib}/qubes/ext/qubesmanager.py* %dir %{python_sitelib}/qubes/tests diff --git a/setup.py b/setup.py index 43c38907..8a3a40f3 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ if __name__ == '__main__': ], 'qubes.ext': [ 'qubes.ext.qubesmanager = qubes.ext.qubesmanager:QubesManager', + 'qubes.ext.gui = qubes.ext.gui:GUI', ], } )