# -*- encoding: utf-8 -*-
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2017 Marek Marczykowski-Górecki
#                               <marmarek@invisiblethingslab.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.

""" GUI/AUDIO daemon launcher tool"""

import os
import signal
import subprocess
import asyncio
import re
import functools
import sys
import xcffib
import xcffib.xproto  # pylint: disable=unused-import

import daemon.pidfile
import qubesadmin
import qubesadmin.events
import qubesadmin.exc
import qubesadmin.tools
import qubesadmin.vm
from . import xcffibhelpers

GUI_DAEMON_PATH = '/usr/bin/qubes-guid'
PACAT_DAEMON_PATH = '/usr/bin/pacat-simple-vchan'
QUBES_ICON_DIR = '/usr/share/icons/hicolor/128x128/devices'

GUI_DAEMON_OPTIONS = [
    ('allow_fullscreen', 'bool'),
    ('override_redirect_protection', 'bool'),
    ('allow_utf8_titles', 'bool'),
    ('secure_copy_sequence', 'str'),
    ('secure_paste_sequence', 'str'),
    ('windows_count_limit', 'int'),
    ('trayicon_mode', 'str'),
    ('startup_timeout', 'int'),
]


def retrieve_gui_daemon_options(vm, guivm):
    '''
    Construct a list of GUI daemon options based on VM features.

    This checks 'gui-*' features on the VM, and if they're absent,
    'gui-default-*' features on the GuiVM.
    '''

    options = {}

    for name, kind in GUI_DAEMON_OPTIONS:
        feature_value = vm.features.get(
            'gui-' + name.replace('_', '-'), None)
        if feature_value is None:
            feature_value = guivm.features.get(
                'gui-default-' + name.replace('_', '-'), None)
        if feature_value is None:
            continue

        if kind == 'bool':
            value = bool(feature_value)
        elif kind == 'int':
            value = int(feature_value)
        elif kind == 'str':
            value = feature_value
        else:
            assert False, kind

        options[name] = value
    return options


def serialize_gui_daemon_options(options):
    '''
    Prepare configuration file content for GUI daemon. Currently uses libconfig
    format.
    '''

    lines = [
        '# Auto-generated file, do not edit!',
        '',
        'global: {',
    ]
    for name, kind in GUI_DAEMON_OPTIONS:
        if name in options:
            value = options[name]
            if kind == 'bool':
                serialized = 'true' if value else 'false'
            elif kind == 'int':
                serialized = str(value)
            elif kind == 'str':
                serialized = escape_config_string(value)
            else:
                assert False, kind

            lines.append('  {} = {};'.format(name, serialized))
    lines.append('}')
    lines.append('')
    return '\n'.join(lines)


NON_ASCII_RE = re.compile(r'[^\x00-\x7F]')
UNPRINTABLE_CHARACTER_RE = re.compile(r'[\x00-\x1F\x7F]')

def escape_config_string(value):
    '''
    Convert a string to libconfig format.

    Format specification:
    http://www.hyperrealm.com/libconfig/libconfig_manual.html#String-Values

    See dump_string() for python-libconf:
    https://github.com/Grk0/python-libconf/blob/master/libconf.py
    '''

    assert not NON_ASCII_RE.match(value),\
        'expected an ASCII string: {!r}'.format(value)

    value = (
        value.replace('\\', '\\\\')
             .replace('"', '\\"')
             .replace('\f', r'\f')
             .replace('\n', r'\n')
             .replace('\r', r'\r')
             .replace('\t', r'\t')
    )
    value = UNPRINTABLE_CHARACTER_RE.sub(
          lambda m: r'\x{:02x}'.format(ord(m.group(0))),
        value)
    return '"' + value + '"'


# "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
        """)


class KeyboardLayout:
    """Class to store and parse X Keyboard layout data"""
    # pylint: disable=too-few-public-methods
    def __init__(self, binary_string):
        split_string = binary_string.split(b'\0')
        self.languages = split_string[2].decode().split(',')
        self.variants = split_string[3].decode().split(',')
        self.options = split_string[4].decode()

    def get_property(self, layout_num):
        """Return the selected keyboard layout as formatted for keyboard_layout
        property."""
        return '+'.join([self.languages[layout_num],
                         self.variants[layout_num],
                         self.options])


class XWatcher:
    """Watch and react for X events related to the keyboard layout changes."""
    def __init__(self, conn, app):
        self.app = app
        self.current_vm = self.app.domains[self.app.local_name]

        self.conn = conn
        self.ext = self.initialize_extension()

        # get root window
        self.setup = self.conn.get_setup()
        self.root = self.setup.roots[0].root

        # atoms (strings) of events we need to watch
        # keyboard layout was switched
        self.atom_xklavier = self.conn.core.InternAtom(
            False, len("XKLAVIER_ALLOW_SECONDARY"),
            "XKLAVIER_ALLOW_SECONDARY").reply().atom
        # keyboard layout was changed
        self.atom_xkb_rules = self.conn.core.InternAtom(
            False, len("_XKB_RULES_NAMES"),
            "_XKB_RULES_NAMES").reply().atom

        self.conn.core.ChangeWindowAttributesChecked(
            self.root, xcffib.xproto.CW.EventMask,
            [xcffib.xproto.EventMask.PropertyChange])
        self.conn.flush()

        # initialize state
        self.keyboard_layout = KeyboardLayout(self.get_keyboard_layout())
        self.selected_layout = self.get_selected_layout()

    def initialize_extension(self):
        """Initialize XKB extension (not supported by xcffib by default"""
        ext = self.conn(xcffibhelpers.key)
        ext.UseExtension()
        return ext

    def get_keyboard_layout(self):
        """Check what is current keyboard layout definition"""
        property_cookie = self.conn.core.GetProperty(
            False,  # delete
            self.root,  # window
            self.atom_xkb_rules,
            xcffib.xproto.Atom.STRING,
            0, 1000
        )
        prop_reply = property_cookie.reply()
        return prop_reply.value.buf()

    def get_selected_layout(self):
        """Check which keyboard layout is currently selected"""
        state_reply = self.ext.GetState().reply()
        return state_reply.lockedGroup[0]

    def update_keyboard_layout(self):
        """Update current vm's keyboard_layout property"""
        new_property = self.keyboard_layout.get_property(
            self.selected_layout)

        current_property = self.current_vm.keyboard_layout

        if new_property != current_property:
            self.current_vm.keyboard_layout = new_property

    def event_reader(self, callback):
        """Poll for X events related to keyboard layout"""
        try:
            for event in iter(self.conn.poll_for_event, None):
                if isinstance(event, xcffib.xproto.PropertyNotifyEvent):
                    if event.atom == self.atom_xklavier:
                        self.selected_layout = self.get_selected_layout()
                    elif event.atom == self.atom_xkb_rules:
                        self.keyboard_layout = KeyboardLayout(
                            self.get_keyboard_layout())
                    else:
                        continue

                    self.update_keyboard_layout()
        except xcffib.ConnectionException:
            callback()


def get_monitor_layout():
    """Get list of monitors and their size/position"""
    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'] and int(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 DAEMONLauncher:
    """Launch GUI/AUDIO daemon for VMs"""

    def __init__(self, app: qubesadmin.app.QubesBase, vm_names=None, kde=False):
        """ Initialize DAEMONLauncher.

        :param app: :py:class:`qubesadmin.Qubes` instance
        :param vm_names: VM names to watch for, or None if watching for all
        :param kde: add KDE-specific arguments for guid
        """
        self.app = app
        self.started_processes = {}
        self.vm_names = vm_names
        self.kde = kde

    async def send_monitor_layout(self, vm, layout=None, startup=False):
        """Send monitor layout to a given VM

        This function is a coroutine.

        :param vm: VM to which send monitor layout
        :param layout: monitor layout to send; if None, fetch it from
            local X server.
        :param startup:
        :return: None
        """
        # pylint: disable=no-self-use
        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

        vm.log.info('Sending monitor layout')

        if not startup:
            with open(self.guid_pidfile(vm.xid)) as pidfile:
                pid = int(pidfile.read())
            os.kill(pid, signal.SIGHUP)
            try:
                with open(self.guid_pidfile(vm.stubdom_xid)) as pidfile:
                    pid = int(pidfile.read())
                os.kill(pid, signal.SIGHUP)
            except FileNotFoundError:
                pass

        try:
            await asyncio.get_event_loop(). \
                run_in_executor(None,
                                functools.partial(
                                    vm.run_service_for_stdio,
                                    'qubes.SetMonitorLayout',
                                    input=''.join(layout).encode(),
                                    autostart=False))
        except subprocess.CalledProcessError as e:
            vm.log.warning('Failed to send monitor layout: %s', e.stderr)

    def send_monitor_layout_all(self):
        """Send monitor layout to all (running) VMs"""
        monitor_layout = get_monitor_layout()
        for vm in self.app.domains:
            if getattr(vm, 'guivm', None) != vm.app.local_name:
                continue
            if vm.klass == 'AdminVM':
                continue
            if vm.is_running():
                if not vm.features.check_with_template('gui', True):
                    continue
                asyncio.ensure_future(self.send_monitor_layout(vm,
                                                               monitor_layout))

    @staticmethod
    def kde_guid_args(vm):
        """Return KDE-specific arguments for gui-daemon, if applicable"""

        guid_cmd = []
        # 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(b':')[2].decode()
        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

    def common_guid_args(self, vm):
        """Common qubes-guid arguments for PV(H), HVM and Stubdomain"""

        guid_cmd = [GUI_DAEMON_PATH,
                    '-N', vm.name,
                    '-c', vm.label.color,
                    '-i', os.path.join(QUBES_ICON_DIR, vm.label.icon) + '.png',
                    '-l', str(vm.label.index)]

        if vm.debug:
            guid_cmd += ['-v', '-v']
            #       elif not verbose:
        else:
            guid_cmd += ['-q']

        if vm.features.check_with_template('rpc-clipboard', False):
            guid_cmd.extend(['-Q'])

        guivm = self.app.domains[vm.guivm]
        options = retrieve_gui_daemon_options(vm, guivm)
        config = serialize_gui_daemon_options(options)
        config_path = self.guid_config_file(vm.xid)
        self.write_guid_config(config_path, config)
        guid_cmd.extend(['-C', config_path])
        return guid_cmd

    @staticmethod
    def write_guid_config(config_path, config):
        """Write guid configuration to a file"""
        with open(config_path, 'w') as config_file:
            config_file.write(config)

    @staticmethod
    def guid_pidfile(xid):
        """Helper function to construct a GUI pidfile path"""
        return '/var/run/qubes/guid-running.{}'.format(xid)

    @staticmethod
    def guid_config_file(xid):
        """Helper function to construct a GUI configuration file path"""
        return '/var/run/qubes/guid-conf.{}'.format(xid)

    @staticmethod
    def pacat_pidfile(xid):
        """Helper function to construct an AUDIO pidfile path"""
        return '/var/run/qubes/pacat.{}'.format(xid)

    @staticmethod
    def pacat_domid(vm):
        """Determine target domid for an AUDIO daemon"""
        xid = vm.stubdom_xid \
                if vm.features.check_with_template('audio-model', False) \
                and vm.virt_mode == 'hvm' \
                else vm.xid
        return xid

    async def start_gui_for_vm(self, vm, monitor_layout=None):
        """Start GUI daemon (qubes-guid) connected directly to a VM

        This function is a coroutine.

        :param vm: VM for which start GUI daemon
        :param monitor_layout: monitor layout to send; if None, fetch it from
            local X server.
        """
        guid_cmd = self.common_guid_args(vm)
        if self.kde:
            guid_cmd.extend(self.kde_guid_args(vm))
        guid_cmd.extend(['-d', str(vm.xid)])

        if vm.virt_mode == 'hvm':
            guid_cmd.extend(['-n'])

            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
                with open(stubdom_guid_pidfile, 'r') as pidfile:
                    stubdom_guid_pid = pidfile.read().strip()
                guid_cmd += ['-K', stubdom_guid_pid]

        vm.log.info('Starting GUI')

        await asyncio.create_subprocess_exec(*guid_cmd)

        await self.send_monitor_layout(vm, layout=monitor_layout,
                                            startup=True)

    async def start_gui_for_stubdomain(self, vm, force=False):
        """Start GUI daemon (qubes-guid) connected to a stubdomain

        This function is a coroutine.
        """
        want_stubdom = force
        if not want_stubdom and \
                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
        if not want_stubdom:
            return
        if os.path.exists(self.guid_pidfile(vm.stubdom_xid)):
            return
        vm.log.info('Starting GUI (stubdomain)')
        guid_cmd = self.common_guid_args(vm)
        guid_cmd.extend(['-d', str(vm.stubdom_xid), '-t', str(vm.xid)])

        await asyncio.create_subprocess_exec(*guid_cmd)

    async def start_audio_for_vm(self, vm):
        """Start AUDIO daemon (pacat-simple-vchan) connected directly to a VM

        This function is a coroutine.

        :param vm: VM for which start AUDIO daemon
        """
        # pylint: disable=no-self-use
        pacat_cmd = [PACAT_DAEMON_PATH, '-l', self.pacat_domid(vm), vm.name]
        vm.log.info('Starting AUDIO')

        await asyncio.create_subprocess_exec(*pacat_cmd)

    async def start_gui(self, vm, force_stubdom=False, monitor_layout=None):
        """Start GUI daemon regardless of start event.

        This function is a coroutine.

        :param vm: VM for which GUI daemon should be started
        :param force_stubdom: Force GUI daemon for stubdomain, even if the
            one for target AppVM is running.
        :param monitor_layout: monitor layout configuration
        """
        guivm = getattr(vm, 'guivm', None)
        if guivm != vm.app.local_name:
            vm.log.info('GUI connected to {}. Skipping.'.format(guivm))
            return

        if vm.virt_mode == 'hvm':
            await 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)):
            await self.start_gui_for_vm(vm, monitor_layout=monitor_layout)

    async def start_audio(self, vm):
        """Start AUDIO daemon regardless of start event.

        This function is a coroutine.

        :param vm: VM for which AUDIO daemon should be started
        """
        audiovm = getattr(vm, 'audiovm', None)
        if audiovm != vm.app.local_name:
            vm.log.info('AUDIO connected to {}. Skipping.'.format(audiovm))
            return

        if not vm.features.check_with_template('audio', True):
            return

        xid = self.pacat_domid(vm)
        if not os.path.exists(self.pacat_pidfile(xid)):
            await self.start_audio_for_vm(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):
            return

        try:
            if getattr(vm, 'guivm', None) != vm.app.local_name:
                return
            if not vm.features.check_with_template('gui', True):
                return
            if vm.virt_mode == 'hvm' and \
                    kwargs.get('start_guid', 'True') == 'True':
                asyncio.ensure_future(self.start_gui_for_stubdomain(vm))
        except qubesadmin.exc.QubesException as e:
            vm.log.warning('Failed to start GUI for %s: %s', vm.name, str(e))

    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):
            return

        try:
            if getattr(vm, 'guivm', None) == vm.app.local_name and \
                    vm.features.check_with_template('gui', True) and \
                    kwargs.get('start_guid', 'True') == 'True':
                asyncio.ensure_future(self.start_gui_for_vm(vm))
        except qubesadmin.exc.QubesException as e:
            vm.log.warning('Failed to start GUI for %s: %s', vm.name, str(e))

        try:
            if getattr(vm, 'audiovm', None) == vm.app.local_name and \
                    vm.features.check_with_template('audio', True) and \
                    kwargs.get('start_audio', 'True') == 'True':
                asyncio.ensure_future(self.start_audio_for_vm(vm))
        except qubesadmin.exc.QubesException as e:
            vm.log.warning('Failed to start AUDIO for %s: %s', vm.name, str(e))

    def on_connection_established(self, _subject, _event, **_kwargs):
        """Handler of 'connection-established' event, used to launch GUI/AUDIO
        daemon for domains started before this tool. """

        monitor_layout = get_monitor_layout()
        self.app.domains.clear_cache()
        for vm in self.app.domains:
            if vm.klass == 'AdminVM':
                continue

            if not self.is_watched(vm):
                continue

            power_state = vm.get_power_state()
            if power_state == 'Running':
                asyncio.ensure_future(
                    self.start_gui(vm, monitor_layout=monitor_layout))
                asyncio.ensure_future(self.start_audio(vm))
            elif power_state == 'Transient':
                # it is still starting, we'll get 'domain-start'
                # event when fully started
                if vm.virt_mode == 'hvm':
                    asyncio.ensure_future(
                        self.start_gui_for_stubdomain(vm))

    def on_domain_stopped(self, vm, _event, **_kwargs):
        """Handler of 'domain-stopped' event, cleans up"""

        if not self.is_watched(vm):
            return

        self.cleanup_guid(vm.xid)
        if vm.virt_mode == 'hvm':
            self.cleanup_guid(vm.stubdom_xid)

    def cleanup_guid(self, xid):
        """
        Clean up after qubes-guid. Removes the auto-generated configuration
        file, if any.
        """

        config_path = self.guid_config_file(xid)
        if os.path.exists(config_path):
            os.unlink(config_path)

    def register_events(self, events):
        """Register domain startup events in app.events dispatcher"""
        events.add_handler('domain-spawn', self.on_domain_spawn)
        events.add_handler('domain-start', self.on_domain_start)
        events.add_handler('connection-established',
                           self.on_connection_established)
        events.add_handler('domain-stopped', self.on_domain_stopped)

    def is_watched(self, vm):
        """
        Should we watch this VM for changes
        """

        if self.vm_names is None:
            return True
        return vm.name in self.vm_names


if 'XDG_RUNTIME_DIR' in os.environ:
    pidfile_path = os.path.join(os.environ['XDG_RUNTIME_DIR'],
                                'qvm-start-daemon.pid')
else:
    pidfile_path = os.path.join(os.environ.get('HOME', '/'),
                                '.qvm-start-daemon.pid')

parser = qubesadmin.tools.QubesArgumentParser(
    description='start GUI for qube(s)', vmname_nargs='*')
parser.add_argument('--watch', action='store_true',
                    help='Keep watching for further domain startups')
parser.add_argument('--force-stubdomain', action='store_true',
                    help='Start GUI to stubdomain-emulated VGA,'
                         ' even if gui-agent is running in the VM')
parser.add_argument('--pidfile', action='store', default=pidfile_path,
                    help='Pidfile path to create in --watch mode')
parser.add_argument('--notify-monitor-layout', action='store_true',
                    help='Notify running instance in --watch mode'
                         ' about changed monitor layout')
parser.add_argument('--kde', action='store_true',
                    help='Set KDE specific arguments to gui-daemon.')
# Add it for the help only
parser.add_argument('--force', action='store_true', default=False,
                    help='Force running daemon without enabled services'
                         ' \'guivm-gui-agent\' or \'audiovm-audio-agent\'')


def main(args=None):
    """ Main function of qvm-start-daemon tool"""
    only_if_service_enabled = ['guivm-gui-agent', 'audiovm-audio-agent']
    enabled_services = [service for service in only_if_service_enabled if
                        os.path.exists('/var/run/qubes-service/%s' % service)]
    if not enabled_services and '--force' not in sys.argv and \
            not os.path.exists('/etc/qubes-release'):
        print(parser.format_help())
        return
    args = parser.parse_args(args)
    if args.watch and args.notify_monitor_layout:
        parser.error('--watch cannot be used with --notify-monitor-layout')

    if args.all_domains:
        vm_names = None
    else:
        vm_names = [vm.name for vm in args.domains]
    launcher = DAEMONLauncher(
        args.app,
        vm_names=vm_names,
        kde=args.kde)

    if args.watch:
        with daemon.pidfile.TimeoutPIDLockFile(args.pidfile):
            loop = asyncio.get_event_loop()
            # pylint: disable=no-member
            events = qubesadmin.events.EventsDispatcher(args.app)
            # pylint: enable=no-member
            launcher.register_events(events)

            events_listener = asyncio.ensure_future(events.listen_for_events())

            for signame in ('SIGINT', 'SIGTERM'):
                loop.add_signal_handler(getattr(signal, signame),
                                        events_listener.cancel)  # pylint: disable=no-member

            loop.add_signal_handler(signal.SIGHUP,
                                    launcher.send_monitor_layout_all)

            conn = xcffib.connect()
            x_watcher = XWatcher(conn, args.app)
            x_fd = conn.get_file_descriptor()
            loop.add_reader(x_fd, x_watcher.event_reader,
                            events_listener.cancel)

            try:
                loop.run_until_complete(events_listener)
            except asyncio.CancelledError:
                pass
            loop.remove_reader(x_fd)
            loop.stop()
            loop.run_forever()
            loop.close()
    elif args.notify_monitor_layout:
        try:
            with open(pidfile_path, 'r') as pidfile:
                pid = int(pidfile.read().strip())
            os.kill(pid, signal.SIGHUP)
        except (FileNotFoundError, ValueError) as e:
            parser.error('Cannot open pidfile {}: {}'.format(pidfile_path,
                                                             str(e)))
    else:
        loop = asyncio.get_event_loop()
        tasks = []
        for vm in args.domains:
            if vm.is_running():
                tasks.append(asyncio.ensure_future(launcher.start_gui(
                    vm, force_stubdom=args.force_stubdomain)))
                tasks.append(asyncio.ensure_future(launcher.start_audio(
                    vm)))
        if tasks:
            loop.run_until_complete(asyncio.wait(tasks))
        loop.stop()
        loop.run_forever()
        loop.close()


if __name__ == '__main__':
    main()