Browse Source

Merge remote-tracking branch 'origin/pr/118'

* origin/pr/118:
  qvm-start-daemon: adjust pacat pid file path
  qvm-start-daemon: check if layout is parsed
  qvm-start-daemon: allow multiple options in keyboard layout
  qvm-start-daemon: improve parsing args for setting keyboard layout
  qvm-start-daemon: set keyboard-layout only for the first set layout
  gui: set keyboard layout when starting daemon
  daemon: start it for dom0 unconditionnaly
  qvm-start-daemon: ensure separate task between GUI/AUDIO
  qvm-start-daemon: allow starting only if service enabled
  Fix and improvements from Marek's comments
  Change qvm-start-gui to qvm-start-daemon for handling audio too
  Support for AudioVM
Marek Marczykowski-Górecki 4 years ago
parent
commit
4971faa462

+ 3 - 2
Makefile

@@ -11,8 +11,9 @@ build:
 install:
 	$(PYTHON) setup.py install -O1 $(PYTHON_PREFIX_ARG) --root $(DESTDIR)
 	install -d $(DESTDIR)/etc/xdg/autostart
-	install -m 0644 etc/qvm-start-gui.desktop $(DESTDIR)/etc/xdg/autostart/
-	install -D scripts/qvm-console $(DESTDIR)/usr/bin/qvm-console
+	install -m 0644 etc/qvm-start-daemon.desktop $(DESTDIR)/etc/xdg/autostart/
+	install -d $(DESTDIR)/usr/bin
+	ln -sf qvm-start-daemon $(DESTDIR)/usr/bin/qvm-start-gui
 
 clean:
 	rm -rf test-packages/__pycache__ qubesadmin/__pycache__

+ 2 - 2
doc/conf.py

@@ -358,8 +358,8 @@ man_pages = [
         u'Manage (Qubes-specific) services started in VM', _man_pages_author, 1),
     ('manpages/qvm-shutdown', 'qvm-shutdown',
         u'Gracefully shut down a qube', _man_pages_author, 1),
-    ('manpages/qvm-start-gui', 'qvm-start-gui',
-        u'Start GUI daemon for qubes', _man_pages_author, 1),
+    ('manpages/qvm-start-daemon', 'qvm-start-daemon',
+        u'Start GUI/AUDIO daemon for qubes', _man_pages_author, 1),
     ('manpages/qvm-start', 'qvm-start',
         u'Start a specified qube', _man_pages_author, 1),
     ('manpages/qvm-tags', 'qvm-tags',

+ 1 - 1
doc/manpages/qvm-features.rst

@@ -73,7 +73,7 @@ Qube provides GUI through emulated VGA. Setting this feature to
 :py:obj:`True` enables emulated VGA output. Note that when gui-agent connects to
 actual VM, emulated VGA output is closed (unless `debug` property is set to
 :py:obj:`True`). It's possible to open emulated VGA output for a running qube,
-regardless of this feature, using `qvm-start-gui --force-stubdomain QUBE_NAME`
+regardless of this feature, using `qvm-start-daemon --force-stubdomain QUBE_NAME`
 command.
 
 This feature is applicable only when qube's `virt_mode` is set to `hvm`.

+ 72 - 0
doc/manpages/qvm-start-daemon.rst

@@ -0,0 +1,72 @@
+.. program:: qvm-start-daemon
+
+:program:`qvm-start-daemon` -- start GUI/AUDIO for qube(s)
+=========================================================
+
+.. note::
+
+   `qvm-start-gui` has been renamed to `qvm-start-daemon` as it handles now
+   `gui` and `audio`.
+
+.. warning::
+
+   This page was autogenerated from command-line parser. It shouldn't be 1:1
+   conversion, because it would add little value. Please revise it and add
+   more descriptive help, which normally won't fit in standard ``--help``
+   option.
+
+   After rewrite, please remove this admonition.
+
+Synopsis
+--------
+
+:command:`qvm-start-daemon` [-h] [--verbose] [--quiet] [--all] [--exclude *EXCLUDE*] [--watch] [--force-stubdomain] [--pidfile *PIDFILE*] [--notify-monitory-layout] [*VMNAME* [*VMNAME* ...]]
+
+Options
+-------
+
+.. option:: --help, -h
+
+   show this help message and exit
+
+.. option:: --verbose, -v
+
+   increase verbosity
+
+.. option:: --quiet, -q
+
+   decrease verbosity
+
+.. option:: --all
+
+   perform the action on all qubes
+
+.. option:: --exclude
+
+   exclude the qube from --all
+
+.. option:: --watch
+
+   Keep watching for further domains startups, must be used with --all
+
+.. option:: --force-stubdomain
+
+   Start GUI to stubdomain-emulated VGA, even if gui-agent is running in the VM
+
+.. option:: --pidfile
+
+   Pidfile path to create in --watch mode
+
+.. option:: --notify-monitor-layout
+
+   Notify running instance in --watch mode about changed monitor layout
+
+Authors
+-------
+
+| Joanna Rutkowska <joanna at invisiblethingslab dot com>
+| Rafal Wojtczuk <rafal at invisiblethingslab dot com>
+| Marek Marczykowski <marmarek at invisiblethingslab dot com>
+| Wojtek Porczyk <woju at invisiblethingslab dot com>
+
+.. vim: ts=3 sw=3 et tw=80

+ 0 - 67
doc/manpages/qvm-start-gui.rst

@@ -1,67 +0,0 @@
-.. program:: qvm-start-gui
-
-:program:`qvm-start-gui` -- start GUI for qube(s)
-=========================================================
-
-.. warning::
-
-   This page was autogenerated from command-line parser. It shouldn't be 1:1
-   conversion, because it would add little value. Please revise it and add
-   more descriptive help, which normally won't fit in standard ``--help``
-   option.
-
-   After rewrite, please remove this admonition.
-
-Synopsis
---------
-
-:command:`qvm-start-gui` [-h] [--verbose] [--quiet] [--all] [--exclude *EXCLUDE*] [--watch] [--force-stubdomain] [--pidfile *PIDFILE*] [--notify-monitory-layout] [*VMNAME* [*VMNAME* ...]]
-
-Options
--------
-
-.. option:: --help, -h
-
-   show this help message and exit
-
-.. option:: --verbose, -v
-
-   increase verbosity
-
-.. option:: --quiet, -q
-
-   decrease verbosity
-
-.. option:: --all
-
-   perform the action on all qubes
-
-.. option:: --exclude
-
-   exclude the qube from --all
-
-.. option:: --watch
-
-   Keep watching for further domains startups, must be used with --all
-
-.. option:: --force-stubdomain
-
-   Start GUI to stubdomain-emulated VGA, even if gui-agent is running in the VM
-
-.. option:: --pidfile
-
-   Pidfile path to create in --watch mode
-
-.. option:: --notify-monitor-layout
-
-   Notify running instance in --watch mode about changed monitor layout
-
-Authors
--------
-
-| Joanna Rutkowska <joanna at invisiblethingslab dot com>
-| Rafal Wojtczuk <rafal at invisiblethingslab dot com>
-| Marek Marczykowski <marmarek at invisiblethingslab dot com>
-| Wojtek Porczyk <woju at invisiblethingslab dot com>
-
-.. vim: ts=3 sw=3 et tw=80

+ 1 - 0
doc/manpages/qvm-start-gui.rst

@@ -0,0 +1 @@
+qvm-start-daemon.rst

+ 2 - 2
doc/qubesadmin.tests.tools.rst

@@ -164,10 +164,10 @@ qubesadmin\.tests\.tools\.qvm\_start module
     :undoc-members:
     :show-inheritance:
 
-qubesadmin\.tests\.tools\.qvm\_start\_gui module
+qubesadmin\.tests\.tools\.qvm\_start\_daemon module
 ------------------------------------------------
 
-.. automodule:: qubesadmin.tests.tools.qvm_start_gui
+.. automodule:: qubesadmin.tests.tools.qvm_start_daemon
     :members:
     :undoc-members:
     :show-inheritance:

+ 2 - 2
doc/qubesadmin.tools.rst

@@ -164,10 +164,10 @@ qubesadmin\.tools\.qvm\_start module
     :undoc-members:
     :show-inheritance:
 
-qubesadmin\.tools\.qvm\_start\_gui module
+qubesadmin\.tools\.qvm\_start\_daemon module
 -----------------------------------------
 
-.. automodule:: qubesadmin.tools.qvm_start_gui
+.. automodule:: qubesadmin.tools.qvm_start_daemon
     :members:
     :undoc-members:
     :show-inheritance:

+ 7 - 0
etc/qvm-start-daemon.desktop

@@ -0,0 +1,7 @@
+[Desktop Entry]
+Name=Qubes Guid/Pacat
+Comment=Starts GUI/AUDIO daemon for Qubes VMs
+Icon=qubes
+Exec=qvm-start-daemon --all --watch
+Terminal=false
+Type=Application

+ 0 - 8
etc/qvm-start-gui.desktop

@@ -1,8 +0,0 @@
-[Desktop Entry]
-Name=Qubes Guid
-Comment=Starts Dom0 GUI daemon for Qubes VMs
-Icon=qubes
-Exec=qvm-start-gui --all --watch
-Terminal=false
-Type=Application
-NotShowIn=X-QUBES

+ 9 - 8
qubesadmin/tests/tools/qvm_start_gui.py → qubesadmin/tests/tools/qvm_start_daemon.py

@@ -26,14 +26,15 @@ import unittest.mock
 import asyncio
 
 import qubesadmin.tests
-import qubesadmin.tools.qvm_start_gui
+import qubesadmin.tools.qvm_start_daemon
 import qubesadmin.vm
 
 
 class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase):
     def setUp(self):
         super(TC_00_qvm_start_gui, self).setUp()
-        self.launcher = qubesadmin.tools.qvm_start_gui.GUILauncher(self.app)
+        self.launcher = \
+            qubesadmin.tools.qvm_start_daemon.DAEMONLauncher(self.app)
 
     @unittest.mock.patch('subprocess.check_output')
     def test_000_kde_args(self, proc_mock):
@@ -449,7 +450,7 @@ HDMI2 disconnected (normal left inverted right x axis y axis)
 VGA1 disconnected (normal left inverted right x axis y axis)
 VIRTUAL1 disconnected (normal left inverted right x axis y axis)
 '''.splitlines()
-        self.assertEqual(qubesadmin.tools.qvm_start_gui.get_monitor_layout(),
+        self.assertEqual(qubesadmin.tools.qvm_start_daemon.get_monitor_layout(),
                          ['1920 1200 0 0\n'])
 
     @unittest.mock.patch('subprocess.Popen')
@@ -458,7 +459,7 @@ VIRTUAL1 disconnected (normal left inverted right x axis y axis)
 LVDS1 connected 1600x900+0+0 (normal left inverted right x axis y axis)
 VGA1 connected 1280x1024+1600+0 (normal left inverted right x axis y axis)
 '''.splitlines()
-        self.assertEqual(qubesadmin.tools.qvm_start_gui.get_monitor_layout(),
+        self.assertEqual(qubesadmin.tools.qvm_start_daemon.get_monitor_layout(),
                          ['1600 900 0 0\n', '1280 1024 1600 0\n'])
 
     @unittest.mock.patch('subprocess.Popen')
@@ -468,7 +469,7 @@ HDMI1 connected 2560x1920+0+0 (normal left inverted right x axis y axis) 372mm x
    1920x1200     60.00*+
 '''.splitlines()
         dpi = 150
-        self.assertEqual(qubesadmin.tools.qvm_start_gui.get_monitor_layout(),
+        self.assertEqual(qubesadmin.tools.qvm_start_daemon.get_monitor_layout(),
                          ['2560 1920 0 0 {} {}\n'.format(
                              int(2560 / dpi * 254 / 10),
                              int(1920 / dpi * 254 / 10))])
@@ -480,7 +481,7 @@ HDMI1 connected 2560x1920+0+0 (normal left inverted right x axis y axis) 310mm x
    1920x1200     60.00*+
 '''.splitlines()
         dpi = 200
-        self.assertEqual(qubesadmin.tools.qvm_start_gui.get_monitor_layout(),
+        self.assertEqual(qubesadmin.tools.qvm_start_daemon.get_monitor_layout(),
                          ['2560 1920 0 0 {} {}\n'.format(
                              int(2560 / dpi * 254 / 10),
                              int(1920 / dpi * 254 / 10))])
@@ -492,7 +493,7 @@ HDMI1 connected 2560x1920+0+0 (normal left inverted right x axis y axis) 206mm x
    1920x1200     60.00*+
 '''.splitlines()
         dpi = 300
-        self.assertEqual(qubesadmin.tools.qvm_start_gui.get_monitor_layout(),
+        self.assertEqual(qubesadmin.tools.qvm_start_daemon.get_monitor_layout(),
                          ['2560 1920 0 0 {} {}\n'.format(
                              int(2560 / dpi * 254 / 10),
                              int(1920 / dpi * 254 / 10))])
@@ -695,7 +696,7 @@ HDMI1 connected 2560x1920+0+0 (normal left inverted right x axis y axis) 206mm x
         patch_send_monitor_layout.start()
         monitor_layout = ['1920 1080 0 0\n']
         mock_get_monior_layout = unittest.mock.patch(
-            'qubesadmin.tools.qvm_start_gui.get_monitor_layout').start()
+            'qubesadmin.tools.qvm_start_daemon.get_monitor_layout').start()
         mock_get_monior_layout.return_value = monitor_layout
 
         self.launcher.send_monitor_layout_all()

+ 5 - 4
qubesadmin/tests/utils.py

@@ -36,8 +36,9 @@ class TestVMUsage(qubesadmin.tests.QubesTestCase):
             b'sys-firewall class=AppVM state=Running\n'
 
         self.global_properties = ['default_dispvm', 'default_netvm',
-                                  'default_guivm', 'default_template',
-                                  'clockvm', 'updatevm', 'management_dispvm']
+                                  'default_guivm', 'default_audiovm',
+                                  'default_template', 'clockvm', 'updatevm',
+                                  'management_dispvm']
 
         for prop in self.global_properties:
             self.app.expected_calls[
@@ -47,8 +48,8 @@ class TestVMUsage(qubesadmin.tests.QubesTestCase):
         self.vms = ['vm1', 'vm2', 'sys-net', 'sys-firewall',
                     'template1', 'template2']
 
-        self.vm_properties = ['template', 'netvm', 'guivm', 'default_dispvm',
-                              'management_dispvm']
+        self.vm_properties = ['template', 'netvm', 'guivm', 'audiovm',
+                              'default_dispvm', 'management_dispvm']
 
         for vm in self.vms:
             for prop in self.vm_properties:

+ 1 - 1
qubesadmin/tools/qvm_prefs.py

@@ -118,7 +118,7 @@ def process_actions(parser, args, target):
     if args.value is not None:
         if str(args.value).lower() == "none":
             if args.property in ["default_dispvm", "netvm", "template",
-                                 "guivm"]:
+                                 "guivm", "audiovm"]:
                 args.value = ''
         try:
             setattr(target, args.property, args.value)

+ 166 - 75
qubesadmin/tools/qvm_start_gui.py → qubesadmin/tools/qvm_start_daemon.py

@@ -18,15 +18,15 @@
 # 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 daemon launcher tool"""
+""" 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
 
@@ -46,6 +46,7 @@ except ImportError:
     pass
 
 GUI_DAEMON_PATH = '/usr/bin/qubes-guid'
+PACAT_DAEMON_PATH = '/usr/bin/pacat-simple-vchan'
 QUBES_ICON_DIR = '/usr/share/icons/hicolor/128x128/devices'
 
 # "LVDS connected 1024x768+0+0 (normal left inverted right) 304mm x 228mm"
@@ -113,17 +114,102 @@ def get_monitor_layout():
     return outputs
 
 
-class GUILauncher(object):
-    """Launch GUI daemon for VMs"""
+def set_keyboard_layout(vm):
+    """Set layout configuration into features for Gui admin extension"""
+    try:
+        # Examples of 'xprop -root _XKB_RULES_NAMES' output values:
+        # "evdev", "pc105", "fr", "oss", ""
+        # "evdev", "pc105", "pl,us", ",", "grp:win_switch,compose:caps"
+
+        # We use the first layout provided
+        xkb_re = r'_XKB_RULES_NAMES\(STRING\) = ' \
+                 r'\"(.*)\", \"(.*)\", \"(.*)\", \"(.*)\", \"(.*)\"\n'
+        xkb_rules_names = subprocess.check_output(
+            ['xprop', '-root', '_XKB_RULES_NAMES']).decode()
+        xkb_parsed = re.match(xkb_re, xkb_rules_names)
+        if xkb_parsed:
+            xkb_layout = [x.split(',')[0] for x in xkb_parsed.groups()[2:4]]
+            # We keep all options
+            xkb_layout.append(xkb_parsed.group(5))
+            keyboard_layout = '+'.join(xkb_layout)
+            vm.features['keyboard-layout'] = keyboard_layout
+        else:
+            vm.log.warning('Failed to parse layout for %s', vm)
+    except subprocess.CalledProcessError as e:
+        vm.log.warning('Failed to set layout for %s: %s', vm, str(e))
+
+
+class DAEMONLauncher:
+    """Launch GUI/AUDIO daemon for VMs"""
 
     def __init__(self, app: qubesadmin.app.QubesBase):
-        """ Initialize GUILauncher.
+        """ Initialize DAEMONLauncher.
 
         :param app: :py:class:`qubesadmin.Qubes` instance
         """
         self.app = app
         self.started_processes = {}
 
+    @asyncio.coroutine
+    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:
+            yield from 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"""
@@ -181,9 +267,14 @@ class GUILauncher(object):
 
     @staticmethod
     def guid_pidfile(xid):
-        """Helper function to construct a pidfile path"""
+        """Helper function to construct a GUI pidfile path"""
         return '/var/run/qubes/guid-running.{}'.format(xid)
 
+    @staticmethod
+    def pacat_pidfile(xid):
+        """Helper function to construct an AUDIO pidfile path"""
+        return '/var/run/qubes/pacat.{}'.format(xid)
+
     @asyncio.coroutine
     def start_gui_for_vm(self, vm, monitor_layout=None):
         """Start GUI daemon (qubes-guid) connected directly to a VM
@@ -241,6 +332,20 @@ class GUILauncher(object):
 
         yield from asyncio.create_subprocess_exec(*guid_cmd)
 
+    @asyncio.coroutine
+    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, vm.xid, vm.name]
+        vm.log.info('Starting AUDIO')
+
+        yield from asyncio.create_subprocess_exec(*pacat_cmd)
+
     @asyncio.coroutine
     def start_gui(self, vm, force_stubdom=False, monitor_layout=None):
         """Start GUI daemon regardless of start event.
@@ -267,64 +372,23 @@ class GUILauncher(object):
             yield from self.start_gui_for_vm(vm, monitor_layout=monitor_layout)
 
     @asyncio.coroutine
-    def send_monitor_layout(self, vm, layout=None, startup=False):
-        """Send monitor layout to a given VM
+    def start_audio(self, vm):
+        """Start AUDIO daemon regardless of start event.
 
         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
+        :param vm: VM for which AUDIO daemon should be started
         """
-        # pylint: disable=no-self-use
-        if vm.features.check_with_template('no-monitor-layout', False) \
-                or not vm.is_running():
+        audiovm = getattr(vm, 'audiovm', None)
+        if audiovm != vm.app.local_name:
+            vm.log.info('AUDIO connected to {}. Skipping.'.format(audiovm))
             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:
-            yield from 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)
+        if not vm.features.check_with_template('audio', True):
+            return
 
-    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))
+        if not os.path.exists(self.pacat_pidfile(vm.xid)):
+            yield from self.start_audio_for_vm(vm)
 
     def on_domain_spawn(self, vm, _event, **kwargs):
         """Handler of 'domain-spawn' event, starts GUI daemon for stubdomain"""
@@ -340,19 +404,26 @@ class GUILauncher(object):
             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 daemon for actual VM"""
+        """Handler of 'domain-start' event, starts GUI/AUDIO daemon for
+        actual VM """
         try:
-            if getattr(vm, 'guivm', None) != vm.app.local_name:
-                return
-            if not vm.features.check_with_template('gui', True):
-                return
-            if kwargs.get('start_guid', 'True') == 'True':
+            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
+        """Handler of 'connection-established' event, used to launch GUI/AUDIO
         daemon for domains started before this tool. """
 
         monitor_layout = get_monitor_layout()
@@ -360,19 +431,18 @@ class GUILauncher(object):
         for vm in self.app.domains:
             if vm.klass == 'AdminVM':
                 continue
-            if getattr(vm, 'guivm', None) != vm.app.local_name:
-                continue
-            if not vm.features.check_with_template('gui', True):
-                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
+                # 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))
+                    asyncio.ensure_future(
+                        self.start_gui_for_stubdomain(vm))
 
     def register_events(self, events):
         """Register domain startup events in app.events dispatcher"""
@@ -394,10 +464,10 @@ def x_reader(conn, callback):
 
 if 'XDG_RUNTIME_DIR' in os.environ:
     pidfile_path = os.path.join(os.environ['XDG_RUNTIME_DIR'],
-                                'qvm-start-gui.pid')
+                                'qvm-start-daemon.pid')
 else:
     pidfile_path = os.path.join(os.environ.get('HOME', '/'),
-                                '.qvm-start-gui.pid')
+                                '.qvm-start-daemon.pid')
 
 parser = qubesadmin.tools.QubesArgumentParser(
     description='start GUI for qube(s)', vmname_nargs='*')
@@ -412,16 +482,35 @@ parser.add_argument('--pidfile', action='store', default=pidfile_path,
 parser.add_argument('--notify-monitor-layout', action='store_true',
                     help='Notify running instance in --watch mode'
                          ' about changed monitor layout')
+parser.add_argument('--set-keyboard-layout', action='store_true',
+                    help='Set keyboard layout values into GuiVM features.'
+                         'This option is implied by --watch')
+# 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-gui tool"""
+    """ 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 not args.all_domains:
         parser.error('--watch option must be used with --all')
     if args.watch and args.notify_monitor_layout:
         parser.error('--watch cannot be used with --notify-monitor-layout')
-    launcher = GUILauncher(args.app)
+    if args.watch and 'guivm-gui-agent' in enabled_services:
+        args.set_keyboard_layout = True
+    if args.set_keyboard_layout or os.path.exists('/etc/qubes-release'):
+        guivm = args.app.domains[args.app.local_name]
+        set_keyboard_layout(guivm)
+    launcher = DAEMONLauncher(args.app)
     if args.watch:
         if not have_events:
             parser.error('--watch option require Python >= 3.5')
@@ -468,6 +557,8 @@ def main(args=None):
             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()

+ 3 - 3
qubesadmin/utils.py

@@ -128,14 +128,14 @@ def vm_dependencies(app, reference_vm):
     result = []
 
     global_properties = ['default_dispvm', 'default_netvm', 'default_guivm',
-                         'default_template', 'clockvm', 'updatevm',
-                         'management_dispvm']
+                         'default_audiovm', 'default_template', 'clockvm',
+                         'updatevm', 'management_dispvm']
 
     for prop in global_properties:
         if reference_vm == getattr(app, prop, None):
             result.append((None, prop))
 
-    vm_properties = ['template', 'netvm', 'guivm',
+    vm_properties = ['template', 'netvm', 'guivm', 'audiovm',
                      'default_dispvm', 'management_dispvm']
 
     for vm in app.domains:

+ 1 - 1
rpm_spec/qubes-core-admin-client.spec.in

@@ -51,7 +51,7 @@ make -C doc DESTDIR=$RPM_BUILD_ROOT \
 %files
 %defattr(-,root,root,-)
 %doc LICENSE
-%config /etc/xdg/autostart/qvm-start-gui.desktop
+%config /etc/xdg/autostart/qvm-start-daemon.desktop
 %{_bindir}/qubes-*
 %{_bindir}/qvm-*
 %{_mandir}/man1/qvm-*.1*