|
@@ -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()
|