123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- #!/usr/bin/env python
- # vim: fileencoding=utf-8
- #
- # The Qubes OS Project, https://www.qubes-os.org/
- #
- # Copyright (C) 2010-2016 Joanna Rutkowska <joanna@invisiblethingslab.com>
- # Copyright (C) 2013-2016 Marek Marczykowski-Górecki
- # <marmarek@invisiblethingslab.com>
- # Copyright (C) 2014-2016 Wojtek Porczyk <woju@invisiblethingslab.com>
- #
- # 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 re
- import subprocess
- import qubes.config
- import qubes.ext
- # "LVDS connected 1024x768+0+0 (normal left inverted right) 304mm x 228mm"
- REGEX_OUTPUT = re.compile(r'''
- (?x) # ignore whitespace
- ^ # start of string
- (?P<output>[A-Za-z0-9\-]*)[ ] # LVDS VGA etc
- (?P<connect>(dis)?connected)[ ]# dis/connected
- (?P<primary>(primary)?)[ ]?
- (( # a group
- (?P<width>\d+)x # either 1024x768+0+0
- (?P<height>\d+)[+]
- (?P<x>\d+)[+]
- (?P<y>\d+)
- )|[\D]) # or not a digit
- ([ ]\(.*\))?[ ]? # ignore options
- ( # 304mm x 228mm
- (?P<width_mm>\d+)mm[ ]x[ ]
- (?P<height_mm>\d+)mm
- )?
- .* # 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']:
- phys_size = ""
- if output_params['width_mm']:
- # don't provide real values for privacy reasons - see
- # #1951 for details
- dpi = (int(output_params['width']) * 254 /
- int(output_params['width_mm']) / 10)
- if dpi > 300:
- dpi = 300
- elif dpi > 200:
- dpi = 200
- elif dpi > 150:
- dpi = 150
- else:
- # if lower, don't provide this info to the VM at all
- dpi = 0
- if dpi:
- # now calculate dimensions based on approximate DPI
- phys_size = " {} {}".format(
- int(output_params['width']) * 254 / dpi / 10,
- int(output_params['height']) * 254 / dpi / 10,
- )
- outputs.append("%s %s %s %s%s\n" % (
- output_params['width'],
- output_params['height'],
- output_params['x'],
- output_params['y'],
- phys_size,
- ))
- return outputs
- class GUI(qubes.ext.Extension):
- @staticmethod
- def kde_guid_args(vm):
- '''Return KDE-specific arguments for guid, if applicable'''
- guid_cmd = []
- # Avoid using environment variables for checking the current session,
- # because this script may be called with cleared env (like with sudo).
- if subprocess.check_output(
- ['xprop', '-root', '-notype', 'KDE_SESSION_VERSION']) == \
- 'KDE_SESSION_VERSION = 5\n':
- # native decoration plugins is used, so adjust window properties
- # accordingly
- guid_cmd += ['-T'] # prefix window titles with VM name
- # get owner of X11 session
- session_owner = None
- for line in subprocess.check_output(['xhost']).splitlines():
- if line == 'SI:localuser:root':
- pass
- elif line.startswith('SI:localuser:'):
- session_owner = line.split(":")[2]
- if session_owner is not None:
- data_dir = os.path.expanduser(
- '~{}/.local/share'.format(session_owner))
- else:
- # fallback to current user
- data_dir = os.path.expanduser('~/.local/share')
- guid_cmd += ['-p',
- '_KDE_NET_WM_COLOR_SCHEME=s:{}'.format(
- os.path.join(data_dir,
- 'qubes-kde', vm.label.name + '.colors'))]
- return guid_cmd
- @qubes.ext.handler('domain-start', 'domain-cmd-pre-run')
- def start_guid(self, vm, event, preparing_dvm=False, start_guid=True,
- 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 self.is_guid_running(vm):
- 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']
- if vm.hvm:
- guid_cmd += ['-Q', '-n']
- stubdom_guid_pidfile = '/var/run/qubes/guid-running.{}'.format(
- self.get_stubdom_xid(vm))
- if not vm.debug and os.path.exists(stubdom_guid_pidfile):
- # Terminate stubdom guid once "real" gui agent connects
- stubdom_guid_pid = \
- open(stubdom_guid_pidfile, 'r').read().strip()
- guid_cmd += ['-K', stubdom_guid_pid]
- guid_cmd += self.kde_guid_args(vm)
- try:
- vm.start_daemon(guid_cmd)
- except subprocess.CalledProcessError:
- raise qubes.exc.QubesVMError(vm,
- 'Cannot start qubes-guid for domain {!r}'.format(vm.name))
- vm.fire_event('monitor-layout-change')
- vm.wait_for_session()
- @staticmethod
- def get_stubdom_xid(vm):
- if vm.xid < 0:
- return -1
- if vm.app.vmm.xs is None:
- return -1
- stubdom_xid_str = vm.app.vmm.xs.read('',
- '/local/domain/{}/image/device-model-domid'.format(vm.xid))
- if stubdom_xid_str is None or not stubdom_xid_str.isdigit():
- return -1
- return int(stubdom_xid_str)
- @staticmethod
- def send_gui_mode(vm):
- vm.run_service('qubes.SetGuiMode',
- input=('SEAMLESS'
- if vm.features.get('gui-seamless', False)
- else 'FULLSCREEN'))
- @qubes.ext.handler('domain-spawn')
- def on_domain_spawn(self, vm, event, start_guid=True, **kwargs):
- # pylint: disable=unused-argument
- if not start_guid:
- return
- if not vm.hvm:
- return
- if not os.getenv('DISPLAY'):
- vm.log.error('Not starting gui daemon, no DISPLAY set')
- return
- guid_cmd = [qubes.config.system_path['qubes_guid_path'],
- '-d', str(self.get_stubdom_xid(vm)),
- '-t', str(vm.xid),
- '-N', vm.name,
- '-c', vm.label.color,
- '-i', vm.label.icon_path,
- '-l', str(vm.label.index),
- ]
- if vm.debug:
- guid_cmd += ['-v', '-v']
- else:
- guid_cmd += ['-q']
- guid_cmd += self.kde_guid_args(vm)
- try:
- vm.start_daemon(guid_cmd)
- except subprocess.CalledProcessError:
- raise qubes.exc.QubesVMError(vm, 'Cannot start gui daemon')
- @qubes.ext.handler('monitor-layout-change')
- def on_monitor_layout_change(self, vm, event, monitor_layout=None):
- # pylint: disable=no-self-use,unused-argument
- if vm.features.check_with_template('no-monitor-layout', False) \
- or not vm.is_running():
- return
- if monitor_layout is None:
- monitor_layout = get_monitor_layout()
- if not monitor_layout:
- return
- pipe = vm.run('QUBESRPC qubes.SetMonitorLayout dom0',
- passio_popen=True, wait=True)
- pipe.stdin.write(''.join(monitor_layout))
- pipe.stdin.close()
- pipe.wait()
- @staticmethod
- def is_guid_running(vm):
- '''Check whether gui daemon for this domain is available.
- :returns: :py:obj:`True` if guid is running, \
- :py:obj:`False` otherwise.
- :rtype: bool
- '''
- xid = vm.xid
- if xid < 0:
- return False
- if not os.path.exists('/var/run/qubes/guid-running.{}'.format(xid)):
- return False
- return True
- @qubes.ext.handler('domain-is-fully-usable')
- def on_domain_is_fully_usable(self, vm, event):
- # pylint: disable=unused-argument
- if not self.is_guid_running(vm):
- yield False
|