qvm_start_daemon.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU Lesser General Public License as published by
  10. # the Free Software Foundation; either version 2.1 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. """ GUI/AUDIO daemon launcher tool"""
  21. import os
  22. import signal
  23. import subprocess
  24. import asyncio
  25. import re
  26. import functools
  27. import sys
  28. import xcffib
  29. import xcffib.xproto # pylint: disable=unused-import
  30. import daemon.pidfile
  31. import qubesadmin
  32. import qubesadmin.exc
  33. import qubesadmin.tools
  34. import qubesadmin.vm
  35. from . import xcffibhelpers
  36. have_events = False
  37. try:
  38. # pylint: disable=wrong-import-position
  39. import qubesadmin.events
  40. have_events = True
  41. except ImportError:
  42. pass
  43. GUI_DAEMON_PATH = '/usr/bin/qubes-guid'
  44. PACAT_DAEMON_PATH = '/usr/bin/pacat-simple-vchan'
  45. QUBES_ICON_DIR = '/usr/share/icons/hicolor/128x128/devices'
  46. # "LVDS connected 1024x768+0+0 (normal left inverted right) 304mm x 228mm"
  47. REGEX_OUTPUT = re.compile(r"""
  48. (?x) # ignore whitespace
  49. ^ # start of string
  50. (?P<output>[A-Za-z0-9\-]*)[ ] # LVDS VGA etc
  51. (?P<connect>(dis)?connected) # dis/connected
  52. ([ ]
  53. (?P<primary>(primary)?)[ ]?
  54. (( # a group
  55. (?P<width>\d+)x # either 1024x768+0+0
  56. (?P<height>\d+)[+]
  57. (?P<x>\d+)[+]
  58. (?P<y>\d+)
  59. )|[\D]) # or not a digit
  60. ([ ]\(.*\))?[ ]? # ignore options
  61. ( # 304mm x 228mm
  62. (?P<width_mm>\d+)mm[ ]x[ ]
  63. (?P<height_mm>\d+)mm
  64. )?
  65. .* # ignore rest of line
  66. )? # everything after (dis)connect is optional
  67. """)
  68. class KeyboardLayout:
  69. """Class to store and parse X Keyboard layout data"""
  70. # pylint: disable=too-few-public-methods
  71. def __init__(self, binary_string):
  72. split_string = binary_string.split(b'\0')
  73. self.languages = split_string[2].decode().split(',')
  74. self.variants = split_string[3].decode().split(',')
  75. self.options = split_string[4].decode()
  76. def get_property(self, layout_num):
  77. """Return the selected keyboard layout as formatted for keyboard_layout
  78. property."""
  79. return '+'.join([self.languages[layout_num],
  80. self.variants[layout_num],
  81. self.options])
  82. class XWatcher:
  83. """Watch and react for X events related to the keyboard layout changes."""
  84. def __init__(self, conn, app):
  85. self.app = app
  86. self.current_vm = self.app.domains[self.app.local_name]
  87. self.conn = conn
  88. self.ext = self.initialize_extension()
  89. # get root window
  90. self.setup = self.conn.get_setup()
  91. self.root = self.setup.roots[0].root
  92. # atoms (strings) of events we need to watch
  93. # keyboard layout was switched
  94. self.atom_xklavier = self.conn.core.InternAtom(
  95. False, len("XKLAVIER_ALLOW_SECONDARY"),
  96. "XKLAVIER_ALLOW_SECONDARY").reply().atom
  97. # keyboard layout was changed
  98. self.atom_xkb_rules = self.conn.core.InternAtom(
  99. False, len("_XKB_RULES_NAMES"),
  100. "_XKB_RULES_NAMES").reply().atom
  101. self.conn.core.ChangeWindowAttributesChecked(
  102. self.root, xcffib.xproto.CW.EventMask,
  103. [xcffib.xproto.EventMask.PropertyChange])
  104. self.conn.flush()
  105. # initialize state
  106. self.keyboard_layout = KeyboardLayout(self.get_keyboard_layout())
  107. self.selected_layout = self.get_selected_layout()
  108. def initialize_extension(self):
  109. """Initialize XKB extension (not supported by xcffib by default"""
  110. ext = self.conn(xcffibhelpers.key)
  111. ext.UseExtension()
  112. return ext
  113. def get_keyboard_layout(self):
  114. """Check what is current keyboard layout definition"""
  115. property_cookie = self.conn.core.GetProperty(
  116. False, # delete
  117. self.root, # window
  118. self.atom_xkb_rules,
  119. xcffib.xproto.Atom.STRING,
  120. 0, 1000
  121. )
  122. prop_reply = property_cookie.reply()
  123. return prop_reply.value.buf()
  124. def get_selected_layout(self):
  125. """Check which keyboard layout is currently selected"""
  126. state_reply = self.ext.GetState().reply()
  127. return state_reply.lockedGroup[0]
  128. def update_keyboard_layout(self):
  129. """Update current vm's keyboard_layout property"""
  130. new_property = self.keyboard_layout.get_property(
  131. self.selected_layout)
  132. current_property = self.current_vm.keyboard_layout
  133. if new_property != current_property:
  134. self.current_vm.keyboard_layout = new_property
  135. def event_reader(self, callback):
  136. """Poll for X events related to keyboard layout"""
  137. try:
  138. for event in iter(self.conn.poll_for_event, None):
  139. if isinstance(event, xcffib.xproto.PropertyNotifyEvent):
  140. if event.atom == self.atom_xklavier:
  141. self.selected_layout = self.get_selected_layout()
  142. elif event.atom == self.atom_xkb_rules:
  143. self.keyboard_layout = KeyboardLayout(
  144. self.get_keyboard_layout())
  145. else:
  146. continue
  147. self.update_keyboard_layout()
  148. except xcffib.ConnectionException:
  149. callback()
  150. def get_monitor_layout():
  151. """Get list of monitors and their size/position"""
  152. outputs = []
  153. for line in subprocess.Popen(
  154. ['xrandr', '-q'], stdout=subprocess.PIPE).stdout:
  155. line = line.decode()
  156. if not line.startswith("Screen") and not line.startswith(" "):
  157. output_params = REGEX_OUTPUT.match(line).groupdict()
  158. if output_params['width']:
  159. phys_size = ""
  160. if output_params['width_mm'] and int(output_params['width_mm']):
  161. # don't provide real values for privacy reasons - see
  162. # #1951 for details
  163. dpi = (int(output_params['width']) * 254 //
  164. int(output_params['width_mm']) // 10)
  165. if dpi > 300:
  166. dpi = 300
  167. elif dpi > 200:
  168. dpi = 200
  169. elif dpi > 150:
  170. dpi = 150
  171. else:
  172. # if lower, don't provide this info to the VM at all
  173. dpi = 0
  174. if dpi:
  175. # now calculate dimensions based on approximate DPI
  176. phys_size = " {} {}".format(
  177. int(output_params['width']) * 254 // dpi // 10,
  178. int(output_params['height']) * 254 // dpi // 10,
  179. )
  180. outputs.append("%s %s %s %s%s\n" % (
  181. output_params['width'],
  182. output_params['height'],
  183. output_params['x'],
  184. output_params['y'],
  185. phys_size,
  186. ))
  187. return outputs
  188. class DAEMONLauncher:
  189. """Launch GUI/AUDIO daemon for VMs"""
  190. def __init__(self, app: qubesadmin.app.QubesBase):
  191. """ Initialize DAEMONLauncher.
  192. :param app: :py:class:`qubesadmin.Qubes` instance
  193. """
  194. self.app = app
  195. self.started_processes = {}
  196. @asyncio.coroutine
  197. def send_monitor_layout(self, vm, layout=None, startup=False):
  198. """Send monitor layout to a given VM
  199. This function is a coroutine.
  200. :param vm: VM to which send monitor layout
  201. :param layout: monitor layout to send; if None, fetch it from
  202. local X server.
  203. :param startup:
  204. :return: None
  205. """
  206. # pylint: disable=no-self-use
  207. if vm.features.check_with_template('no-monitor-layout', False) \
  208. or not vm.is_running():
  209. return
  210. if layout is None:
  211. layout = get_monitor_layout()
  212. if not layout:
  213. return
  214. vm.log.info('Sending monitor layout')
  215. if not startup:
  216. with open(self.guid_pidfile(vm.xid)) as pidfile:
  217. pid = int(pidfile.read())
  218. os.kill(pid, signal.SIGHUP)
  219. try:
  220. with open(self.guid_pidfile(vm.stubdom_xid)) as pidfile:
  221. pid = int(pidfile.read())
  222. os.kill(pid, signal.SIGHUP)
  223. except FileNotFoundError:
  224. pass
  225. try:
  226. yield from asyncio.get_event_loop(). \
  227. run_in_executor(None,
  228. functools.partial(
  229. vm.run_service_for_stdio,
  230. 'qubes.SetMonitorLayout',
  231. input=''.join(layout).encode(),
  232. autostart=False))
  233. except subprocess.CalledProcessError as e:
  234. vm.log.warning('Failed to send monitor layout: %s', e.stderr)
  235. def send_monitor_layout_all(self):
  236. """Send monitor layout to all (running) VMs"""
  237. monitor_layout = get_monitor_layout()
  238. for vm in self.app.domains:
  239. if getattr(vm, 'guivm', None) != vm.app.local_name:
  240. continue
  241. if vm.klass == 'AdminVM':
  242. continue
  243. if vm.is_running():
  244. if not vm.features.check_with_template('gui', True):
  245. continue
  246. asyncio.ensure_future(self.send_monitor_layout(vm,
  247. monitor_layout))
  248. @staticmethod
  249. def kde_guid_args(vm):
  250. """Return KDE-specific arguments for gui-daemon, if applicable"""
  251. guid_cmd = []
  252. # Avoid using environment variables for checking the current session,
  253. # because this script may be called with cleared env (like with sudo).
  254. if subprocess.check_output(
  255. ['xprop', '-root', '-notype', 'KWIN_RUNNING']) == \
  256. b'KWIN_RUNNING = 0x1\n':
  257. # native decoration plugins is used, so adjust window properties
  258. # accordingly
  259. guid_cmd += ['-T'] # prefix window titles with VM name
  260. # get owner of X11 session
  261. session_owner = None
  262. for line in subprocess.check_output(['xhost']).splitlines():
  263. if line == b'SI:localuser:root':
  264. pass
  265. elif line.startswith(b'SI:localuser:'):
  266. session_owner = line.split(b':')[2].decode()
  267. if session_owner is not None:
  268. data_dir = os.path.expanduser(
  269. '~{}/.local/share'.format(session_owner))
  270. else:
  271. # fallback to current user
  272. data_dir = os.path.expanduser('~/.local/share')
  273. guid_cmd += ['-p',
  274. '_KDE_NET_WM_COLOR_SCHEME=s:{}'.format(
  275. os.path.join(data_dir,
  276. 'qubes-kde',
  277. vm.label.name + '.colors'))]
  278. return guid_cmd
  279. def common_guid_args(self, vm):
  280. """Common qubes-guid arguments for PV(H), HVM and Stubdomain"""
  281. guid_cmd = [GUI_DAEMON_PATH,
  282. '-N', vm.name,
  283. '-c', vm.label.color,
  284. '-i', os.path.join(QUBES_ICON_DIR, vm.label.icon) + '.png',
  285. '-l', str(vm.label.index)]
  286. if vm.debug:
  287. guid_cmd += ['-v', '-v']
  288. # elif not verbose:
  289. else:
  290. guid_cmd += ['-q']
  291. if vm.features.check_with_template('rpc-clipboard', False):
  292. guid_cmd.extend(['-Q'])
  293. guid_cmd += self.kde_guid_args(vm)
  294. return guid_cmd
  295. @staticmethod
  296. def guid_pidfile(xid):
  297. """Helper function to construct a GUI pidfile path"""
  298. return '/var/run/qubes/guid-running.{}'.format(xid)
  299. @staticmethod
  300. def pacat_pidfile(xid):
  301. """Helper function to construct an AUDIO pidfile path"""
  302. return '/var/run/qubes/pacat.{}'.format(xid)
  303. @staticmethod
  304. def pacat_domid(vm):
  305. """Determine target domid for an AUDIO daemon"""
  306. xid = vm.stubdom_xid \
  307. if vm.features.check_with_template('audio-model', False) \
  308. and vm.virt_mode == 'hvm' \
  309. else vm.xid
  310. return xid
  311. @asyncio.coroutine
  312. def start_gui_for_vm(self, vm, monitor_layout=None):
  313. """Start GUI daemon (qubes-guid) connected directly to a VM
  314. This function is a coroutine.
  315. :param vm: VM for which start GUI daemon
  316. :param monitor_layout: monitor layout to send; if None, fetch it from
  317. local X server.
  318. """
  319. guid_cmd = self.common_guid_args(vm)
  320. guid_cmd.extend(['-d', str(vm.xid)])
  321. if vm.virt_mode == 'hvm':
  322. guid_cmd.extend(['-n'])
  323. stubdom_guid_pidfile = self.guid_pidfile(vm.stubdom_xid)
  324. if not vm.debug and os.path.exists(stubdom_guid_pidfile):
  325. # Terminate stubdom guid once "real" gui agent connects
  326. with open(stubdom_guid_pidfile, 'r') as pidfile:
  327. stubdom_guid_pid = pidfile.read().strip()
  328. guid_cmd += ['-K', stubdom_guid_pid]
  329. vm.log.info('Starting GUI')
  330. yield from asyncio.create_subprocess_exec(*guid_cmd)
  331. yield from self.send_monitor_layout(vm, layout=monitor_layout,
  332. startup=True)
  333. @asyncio.coroutine
  334. def start_gui_for_stubdomain(self, vm, force=False):
  335. """Start GUI daemon (qubes-guid) connected to a stubdomain
  336. This function is a coroutine.
  337. """
  338. want_stubdom = force
  339. if not want_stubdom and \
  340. vm.features.check_with_template('gui-emulated', False):
  341. want_stubdom = True
  342. # if no 'gui' or 'gui-emulated' feature set at all, use emulated GUI
  343. if not want_stubdom and \
  344. vm.features.check_with_template('gui', None) is None and \
  345. vm.features.check_with_template('gui-emulated', None) is None:
  346. want_stubdom = True
  347. if not want_stubdom and vm.debug:
  348. want_stubdom = True
  349. if not want_stubdom:
  350. return
  351. if os.path.exists(self.guid_pidfile(vm.stubdom_xid)):
  352. return
  353. vm.log.info('Starting GUI (stubdomain)')
  354. guid_cmd = self.common_guid_args(vm)
  355. guid_cmd.extend(['-d', str(vm.stubdom_xid), '-t', str(vm.xid)])
  356. yield from asyncio.create_subprocess_exec(*guid_cmd)
  357. @asyncio.coroutine
  358. def start_audio_for_vm(self, vm):
  359. """Start AUDIO daemon (pacat-simple-vchan) connected directly to a VM
  360. This function is a coroutine.
  361. :param vm: VM for which start AUDIO daemon
  362. """
  363. # pylint: disable=no-self-use
  364. pacat_cmd = [PACAT_DAEMON_PATH, '-l', self.pacat_domid(vm), vm.name]
  365. vm.log.info('Starting AUDIO')
  366. yield from asyncio.create_subprocess_exec(*pacat_cmd)
  367. @asyncio.coroutine
  368. def start_gui(self, vm, force_stubdom=False, monitor_layout=None):
  369. """Start GUI daemon regardless of start event.
  370. This function is a coroutine.
  371. :param vm: VM for which GUI daemon should be started
  372. :param force_stubdom: Force GUI daemon for stubdomain, even if the
  373. one for target AppVM is running.
  374. :param monitor_layout: monitor layout configuration
  375. """
  376. guivm = getattr(vm, 'guivm', None)
  377. if guivm != vm.app.local_name:
  378. vm.log.info('GUI connected to {}. Skipping.'.format(guivm))
  379. return
  380. if vm.virt_mode == 'hvm':
  381. yield from self.start_gui_for_stubdomain(vm, force=force_stubdom)
  382. if not vm.features.check_with_template('gui', True):
  383. return
  384. if not os.path.exists(self.guid_pidfile(vm.xid)):
  385. yield from self.start_gui_for_vm(vm, monitor_layout=monitor_layout)
  386. @asyncio.coroutine
  387. def start_audio(self, vm):
  388. """Start AUDIO daemon regardless of start event.
  389. This function is a coroutine.
  390. :param vm: VM for which AUDIO daemon should be started
  391. """
  392. audiovm = getattr(vm, 'audiovm', None)
  393. if audiovm != vm.app.local_name:
  394. vm.log.info('AUDIO connected to {}. Skipping.'.format(audiovm))
  395. return
  396. if not vm.features.check_with_template('audio', True):
  397. return
  398. xid = self.pacat_domid(vm)
  399. if not os.path.exists(self.pacat_pidfile(xid)):
  400. yield from self.start_audio_for_vm(vm)
  401. def on_domain_spawn(self, vm, _event, **kwargs):
  402. """Handler of 'domain-spawn' event, starts GUI daemon for stubdomain"""
  403. try:
  404. if getattr(vm, 'guivm', None) != vm.app.local_name:
  405. return
  406. if not vm.features.check_with_template('gui', True):
  407. return
  408. if vm.virt_mode == 'hvm' and \
  409. kwargs.get('start_guid', 'True') == 'True':
  410. asyncio.ensure_future(self.start_gui_for_stubdomain(vm))
  411. except qubesadmin.exc.QubesException as e:
  412. vm.log.warning('Failed to start GUI for %s: %s', vm.name, str(e))
  413. def on_domain_start(self, vm, _event, **kwargs):
  414. """Handler of 'domain-start' event, starts GUI/AUDIO daemon for
  415. actual VM """
  416. try:
  417. if getattr(vm, 'guivm', None) == vm.app.local_name and \
  418. vm.features.check_with_template('gui', True) and \
  419. kwargs.get('start_guid', 'True') == 'True':
  420. asyncio.ensure_future(self.start_gui_for_vm(vm))
  421. except qubesadmin.exc.QubesException as e:
  422. vm.log.warning('Failed to start GUI for %s: %s', vm.name, str(e))
  423. try:
  424. if getattr(vm, 'audiovm', None) == vm.app.local_name and \
  425. vm.features.check_with_template('audio', True) and \
  426. kwargs.get('start_audio', 'True') == 'True':
  427. asyncio.ensure_future(self.start_audio_for_vm(vm))
  428. except qubesadmin.exc.QubesException as e:
  429. vm.log.warning('Failed to start AUDIO for %s: %s', vm.name, str(e))
  430. def on_connection_established(self, _subject, _event, **_kwargs):
  431. """Handler of 'connection-established' event, used to launch GUI/AUDIO
  432. daemon for domains started before this tool. """
  433. monitor_layout = get_monitor_layout()
  434. self.app.domains.clear_cache()
  435. for vm in self.app.domains:
  436. if vm.klass == 'AdminVM':
  437. continue
  438. power_state = vm.get_power_state()
  439. if power_state == 'Running':
  440. asyncio.ensure_future(
  441. self.start_gui(vm, monitor_layout=monitor_layout))
  442. asyncio.ensure_future(self.start_audio(vm))
  443. elif power_state == 'Transient':
  444. # it is still starting, we'll get 'domain-start'
  445. # event when fully started
  446. if vm.virt_mode == 'hvm':
  447. asyncio.ensure_future(
  448. self.start_gui_for_stubdomain(vm))
  449. def register_events(self, events):
  450. """Register domain startup events in app.events dispatcher"""
  451. events.add_handler('domain-spawn', self.on_domain_spawn)
  452. events.add_handler('domain-start', self.on_domain_start)
  453. events.add_handler('connection-established',
  454. self.on_connection_established)
  455. if 'XDG_RUNTIME_DIR' in os.environ:
  456. pidfile_path = os.path.join(os.environ['XDG_RUNTIME_DIR'],
  457. 'qvm-start-daemon.pid')
  458. else:
  459. pidfile_path = os.path.join(os.environ.get('HOME', '/'),
  460. '.qvm-start-daemon.pid')
  461. parser = qubesadmin.tools.QubesArgumentParser(
  462. description='start GUI for qube(s)', vmname_nargs='*')
  463. parser.add_argument('--watch', action='store_true',
  464. help='Keep watching for further domains'
  465. ' startups, must be used with --all')
  466. parser.add_argument('--force-stubdomain', action='store_true',
  467. help='Start GUI to stubdomain-emulated VGA,'
  468. ' even if gui-agent is running in the VM')
  469. parser.add_argument('--pidfile', action='store', default=pidfile_path,
  470. help='Pidfile path to create in --watch mode')
  471. parser.add_argument('--notify-monitor-layout', action='store_true',
  472. help='Notify running instance in --watch mode'
  473. ' about changed monitor layout')
  474. # Add it for the help only
  475. parser.add_argument('--force', action='store_true', default=False,
  476. help='Force running daemon without enabled services'
  477. ' \'guivm-gui-agent\' or \'audiovm-audio-agent\'')
  478. def main(args=None):
  479. """ Main function of qvm-start-daemon tool"""
  480. only_if_service_enabled = ['guivm-gui-agent', 'audiovm-audio-agent']
  481. enabled_services = [service for service in only_if_service_enabled if
  482. os.path.exists('/var/run/qubes-service/%s' % service)]
  483. if not enabled_services and '--force' not in sys.argv and \
  484. not os.path.exists('/etc/qubes-release'):
  485. print(parser.format_help())
  486. return
  487. args = parser.parse_args(args)
  488. if args.watch and not args.all_domains:
  489. parser.error('--watch option must be used with --all')
  490. if args.watch and args.notify_monitor_layout:
  491. parser.error('--watch cannot be used with --notify-monitor-layout')
  492. launcher = DAEMONLauncher(args.app)
  493. if args.watch:
  494. if not have_events:
  495. parser.error('--watch option require Python >= 3.5')
  496. with daemon.pidfile.TimeoutPIDLockFile(args.pidfile):
  497. loop = asyncio.get_event_loop()
  498. # pylint: disable=no-member
  499. events = qubesadmin.events.EventsDispatcher(args.app)
  500. # pylint: enable=no-member
  501. launcher.register_events(events)
  502. events_listener = asyncio.ensure_future(events.listen_for_events())
  503. for signame in ('SIGINT', 'SIGTERM'):
  504. loop.add_signal_handler(getattr(signal, signame),
  505. events_listener.cancel) # pylint: disable=no-member
  506. loop.add_signal_handler(signal.SIGHUP,
  507. launcher.send_monitor_layout_all)
  508. conn = xcffib.connect()
  509. x_watcher = XWatcher(conn, args.app)
  510. x_fd = conn.get_file_descriptor()
  511. loop.add_reader(x_fd, x_watcher.event_reader,
  512. events_listener.cancel)
  513. try:
  514. loop.run_until_complete(events_listener)
  515. except asyncio.CancelledError:
  516. pass
  517. loop.remove_reader(x_fd)
  518. loop.stop()
  519. loop.run_forever()
  520. loop.close()
  521. elif args.notify_monitor_layout:
  522. try:
  523. with open(pidfile_path, 'r') as pidfile:
  524. pid = int(pidfile.read().strip())
  525. os.kill(pid, signal.SIGHUP)
  526. except (FileNotFoundError, ValueError) as e:
  527. parser.error('Cannot open pidfile {}: {}'.format(pidfile_path,
  528. str(e)))
  529. else:
  530. loop = asyncio.get_event_loop()
  531. tasks = []
  532. for vm in args.domains:
  533. if vm.is_running():
  534. tasks.append(asyncio.ensure_future(launcher.start_gui(
  535. vm, force_stubdom=args.force_stubdomain)))
  536. tasks.append(asyncio.ensure_future(launcher.start_audio(
  537. vm)))
  538. if tasks:
  539. loop.run_until_complete(asyncio.wait(tasks))
  540. loop.stop()
  541. loop.run_forever()
  542. loop.close()
  543. if __name__ == '__main__':
  544. main()