core-admin-client/qubesadmin/tools/qvm_start_daemon.py

809 lines
29 KiB
Python
Raw Normal View History

# -*- 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
2019-10-20 13:24:56 +02:00
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
'''
2020-07-15 14:01:29 +02:00
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"
2019-10-20 13:24:56 +02:00
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
2019-10-20 13:24:56 +02:00
""")
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():
2019-10-20 13:24:56 +02:00
"""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 //
2019-10-20 13:24:56 +02:00
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" % (
2019-10-20 13:24:56 +02:00
output_params['width'],
output_params['height'],
output_params['x'],
output_params['y'],
phys_size,
))
return outputs
class DAEMONLauncher:
"""Launch GUI/AUDIO daemon for VMs"""
2019-10-20 13:24:56 +02:00
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
2019-10-20 13:24:56 +02:00
"""
self.app = app
self.started_processes = {}
self.vm_names = vm_names
self.kde = kde
# cache XID values when the VM was still running -
# for cleanup purpose
self.xid_cache = {}
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):
2019-10-20 13:24:56 +02:00
"""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):
2019-10-20 13:24:56 +02:00
"""Common qubes-guid arguments for PV(H), HVM and Stubdomain"""
guid_cmd = [GUI_DAEMON_PATH,
2019-10-20 13:24:56 +02:00
'-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)
2020-05-31 20:11:33 +02:00
@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):
2019-10-20 13:24:56 +02:00
"""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.
2019-10-20 13:24:56 +02:00
"""
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,
2019-10-20 13:24:56 +02:00
startup=True)
async def start_gui_for_stubdomain(self, vm, force=False):
2019-10-20 13:24:56 +02:00
"""Start GUI daemon (qubes-guid) connected to a stubdomain
This function is a coroutine.
2019-10-20 13:24:56 +02:00
"""
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
2020-05-31 20:11:33 +02:00
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):
2019-10-20 13:24:56 +02:00
"""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.
2019-10-20 13:24:56 +02:00
: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))
2019-10-20 13:31:57 +02:00
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
2019-10-20 13:24:56 +02:00
"""
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
2020-05-31 20:11:33 +02:00
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):
2019-10-20 13:24:56 +02:00
"""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:
2019-10-20 13:31:57 +02:00
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
self.xid_cache[vm.name] = vm.xid, vm.stubdom_xid
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
2019-10-20 13:24:56 +02:00
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))
self.xid_cache[vm.name] = vm.xid, vm.stubdom_xid
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
# read XID from cache, as stopped domain reports it already as -1
try:
xid, stubdom_xid = self.xid_cache[vm.name]
del self.xid_cache[vm.name]
except KeyError:
return
if xid != -1:
self.cleanup_guid(xid)
if stubdom_xid != -1:
self.cleanup_guid(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):
2019-10-20 13:24:56 +02:00
"""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',
2019-10-20 13:24:56 +02:00
self.on_connection_established)
events.add_handler('domain-stopped', self.on_domain_stopped)
2019-10-20 13:24:56 +02:00
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',
2019-10-20 13:24:56 +02:00
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,
2019-10-20 13:24:56 +02:00
help='Pidfile path to create in --watch mode')
parser.add_argument('--notify-monitor-layout', action='store_true',
2019-10-20 13:24:56 +02:00
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\' or \'audiovm\'')
def main(args=None):
""" Main function of qvm-start-daemon tool"""
only_if_service_enabled = ['guivm', 'audiovm']
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),
2019-10-20 13:24:56 +02:00
events_listener.cancel) # pylint: disable=no-member
loop.add_signal_handler(signal.SIGHUP,
2019-10-20 13:24:56 +02:00
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,
2019-10-20 13:24:56 +02:00
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()