307 řádky
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			307 řádky
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#
 | 
						|
# 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
 | 
						|
        )?                             # everything after (dis)connect is optional
 | 
						|
        ''')
 | 
						|
 | 
						|
 | 
						|
def get_monitor_layout():
 | 
						|
    outputs = []
 | 
						|
 | 
						|
    for line in subprocess.Popen(
 | 
						|
            ['xrandr', '-q'], stdout=subprocess.PIPE).stdout:
 | 
						|
        line = line.decode()
 | 
						|
        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 == b'SI:localuser:root':
 | 
						|
                    pass
 | 
						|
                elif line.startswith(b'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:
 | 
						|
            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
 | 
						|
 | 
						|
        display = os.getenv('DISPLAY')
 | 
						|
        if not display.startswith(':'):
 | 
						|
            vm.log.error('Expected local $DISPLAY, got \'{}\''.format(display))
 | 
						|
            return
 | 
						|
 | 
						|
        display_num = display[1:].partition('.')[0]
 | 
						|
        shmid_path = '/var/run/qubes/shm.id.{}'.format(display_num)
 | 
						|
        if not os.path.exists(shmid_path):
 | 
						|
            vm.log.error(
 | 
						|
                'Not starting gui daemon, no {} file'.format(shmid_path))
 | 
						|
            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, 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 layout is None:
 | 
						|
            layout = get_monitor_layout()
 | 
						|
            if not layout:
 | 
						|
                return
 | 
						|
 | 
						|
        pipe = vm.run('QUBESRPC qubes.SetMonitorLayout dom0',
 | 
						|
            passio_popen=True, wait=True)
 | 
						|
 | 
						|
        pipe.stdin.write(''.join(layout).encode())
 | 
						|
        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
 |