Change qvm-start-gui to qvm-start-daemon for handling audio too

This commit is contained in:
Frédéric Pierret (fepitre) 2019-11-17 23:54:05 +01:00
parent 6d9e5bbf07
commit 0e049e682a
No known key found for this signature in database
GPG Key ID: 484010B5CDC576E2
11 changed files with 252 additions and 195 deletions

View File

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

View File

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

View File

@ -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 :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 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, :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. command.
This feature is applicable only when qube's `virt_mode` is set to `hvm`. This feature is applicable only when qube's `virt_mode` is set to `hvm`.

View File

@ -1,6 +1,6 @@
.. program:: qvm-start-gui .. program:: qvm-start-daemon
:program:`qvm-start-gui` -- start GUI for qube(s) :program:`qvm-start-daemon` -- start GUI/AUDIO for qube(s)
========================================================= =========================================================
.. warning:: .. warning::
@ -15,7 +15,7 @@
Synopsis Synopsis
-------- --------
:command:`qvm-start-gui` [-h] [--verbose] [--quiet] [--all] [--exclude *EXCLUDE*] [--watch] [--force-stubdomain] [--pidfile *PIDFILE*] [--notify-monitory-layout] [*VMNAME* [*VMNAME* ...]] :command:`qvm-start-daemon` [-h] [--verbose] [--quiet] [--all] [--exclude *EXCLUDE*] [--watch] [--force-stubdomain] [--pidfile *PIDFILE*] [--notify-monitory-layout] [*VMNAME* [*VMNAME* ...]]
Options Options
------- -------

View File

@ -164,10 +164,10 @@ qubesadmin\.tests\.tools\.qvm\_start module
:undoc-members: :undoc-members:
:show-inheritance: :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: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View File

@ -164,10 +164,10 @@ qubesadmin\.tools\.qvm\_start module
:undoc-members: :undoc-members:
:show-inheritance: :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: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View File

@ -0,0 +1,8 @@
[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
NotShowIn=X-QUBES

View File

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

View File

@ -26,14 +26,15 @@ import unittest.mock
import asyncio import asyncio
import qubesadmin.tests import qubesadmin.tests
import qubesadmin.tools.qvm_start_gui import qubesadmin.tools.qvm_start_daemon
import qubesadmin.vm import qubesadmin.vm
class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase): class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase):
def setUp(self): def setUp(self):
super(TC_00_qvm_start_gui, self).setUp() 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') @unittest.mock.patch('subprocess.check_output')
def test_000_kde_args(self, proc_mock): 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) VGA1 disconnected (normal left inverted right x axis y axis)
VIRTUAL1 disconnected (normal left inverted right x axis y axis) VIRTUAL1 disconnected (normal left inverted right x axis y axis)
'''.splitlines() '''.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']) ['1920 1200 0 0\n'])
@unittest.mock.patch('subprocess.Popen') @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) 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) VGA1 connected 1280x1024+1600+0 (normal left inverted right x axis y axis)
'''.splitlines() '''.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']) ['1600 900 0 0\n', '1280 1024 1600 0\n'])
@unittest.mock.patch('subprocess.Popen') @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*+ 1920x1200 60.00*+
'''.splitlines() '''.splitlines()
dpi = 150 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( ['2560 1920 0 0 {} {}\n'.format(
int(2560 / dpi * 254 / 10), int(2560 / dpi * 254 / 10),
int(1920 / 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*+ 1920x1200 60.00*+
'''.splitlines() '''.splitlines()
dpi = 200 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( ['2560 1920 0 0 {} {}\n'.format(
int(2560 / dpi * 254 / 10), int(2560 / dpi * 254 / 10),
int(1920 / 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*+ 1920x1200 60.00*+
'''.splitlines() '''.splitlines()
dpi = 300 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( ['2560 1920 0 0 {} {}\n'.format(
int(2560 / dpi * 254 / 10), int(2560 / dpi * 254 / 10),
int(1920 / 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() patch_send_monitor_layout.start()
monitor_layout = ['1920 1080 0 0\n'] monitor_layout = ['1920 1080 0 0\n']
mock_get_monior_layout = unittest.mock.patch( 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 mock_get_monior_layout.return_value = monitor_layout
self.launcher.send_monitor_layout_all() self.launcher.send_monitor_layout_all()

View File

@ -18,14 +18,13 @@
# You should have received a copy of the GNU Lesser General Public License along # 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/>. # with this program; if not, see <http://www.gnu.org/licenses/>.
""" GUI daemon launcher tool""" """ GUI/AUDIO daemon launcher tool"""
import os import os
import signal import signal
import subprocess import subprocess
import asyncio import asyncio
import re import re
import functools import functools
import xcffib import xcffib
import xcffib.xproto # pylint: disable=unused-import import xcffib.xproto # pylint: disable=unused-import
@ -46,6 +45,7 @@ except ImportError:
pass pass
GUI_DAEMON_PATH = '/usr/bin/qubes-guid' GUI_DAEMON_PATH = '/usr/bin/qubes-guid'
PACAT_DAEMON_PATH = '/usr/bin/pacat-simple-vchan'
QUBES_ICON_DIR = '/usr/share/icons/hicolor/128x128/devices' QUBES_ICON_DIR = '/usr/share/icons/hicolor/128x128/devices'
# "LVDS connected 1024x768+0+0 (normal left inverted right) 304mm x 228mm" # "LVDS connected 1024x768+0+0 (normal left inverted right) 304mm x 228mm"
@ -113,159 +113,17 @@ def get_monitor_layout():
return outputs return outputs
class GUILauncher(object): class DAEMONLauncher:
"""Launch GUI daemon for VMs""" """Launch GUI/AUDIO daemon for VMs"""
def __init__(self, app: qubesadmin.app.QubesBase): def __init__(self, app: qubesadmin.app.QubesBase):
""" Initialize GUILauncher. """ Initialize DAEMONLauncher.
:param app: :py:class:`qubesadmin.Qubes` instance :param app: :py:class:`qubesadmin.Qubes` instance
""" """
self.app = app self.app = app
self.started_processes = {} self.started_processes = {}
@staticmethod
def kde_guid_args(vm):
"""Return KDE-specific arguments for gui-daemon, if applicable"""
guid_cmd = []
# Avoid using environment variables for checking the current session,
# because this script may be called with cleared env (like with sudo).
if subprocess.check_output(
['xprop', '-root', '-notype', 'KWIN_RUNNING']) == \
b'KWIN_RUNNING = 0x1\n':
# 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'])
guid_cmd += self.kde_guid_args(vm)
return guid_cmd
@staticmethod
def guid_pidfile(xid):
"""Helper function to construct a pidfile path"""
return '/var/run/qubes/guid-running.{}'.format(xid)
@asyncio.coroutine
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)
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')
yield from asyncio.create_subprocess_exec(*guid_cmd)
yield from self.send_monitor_layout(vm, layout=monitor_layout,
startup=True)
@asyncio.coroutine
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)])
yield from asyncio.create_subprocess_exec(*guid_cmd)
@asyncio.coroutine
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':
yield from 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)):
yield from self.start_gui_for_vm(vm, monitor_layout=monitor_layout)
@asyncio.coroutine @asyncio.coroutine
def send_monitor_layout(self, vm, layout=None, startup=False): def send_monitor_layout(self, vm, layout=None, startup=False):
"""Send monitor layout to a given VM """Send monitor layout to a given VM
@ -326,6 +184,186 @@ class GUILauncher(object):
asyncio.ensure_future(self.send_monitor_layout(vm, asyncio.ensure_future(self.send_monitor_layout(vm,
monitor_layout)) monitor_layout))
@staticmethod
def kde_guid_args(vm):
"""Return KDE-specific arguments for gui-daemon, if applicable"""
guid_cmd = []
# Avoid using environment variables for checking the current session,
# because this script may be called with cleared env (like with sudo).
if subprocess.check_output(
['xprop', '-root', '-notype', 'KWIN_RUNNING']) == \
b'KWIN_RUNNING = 0x1\n':
# 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'])
guid_cmd += self.kde_guid_args(vm)
return guid_cmd
@staticmethod
def guid_pidfile(xid):
"""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-running.{}'.format(xid)
@asyncio.coroutine
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)
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')
yield from asyncio.create_subprocess_exec(*guid_cmd)
yield from self.send_monitor_layout(vm, layout=monitor_layout,
startup=True)
@asyncio.coroutine
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)])
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.
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':
yield from 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)):
yield from self.start_gui_for_vm(vm, monitor_layout=monitor_layout)
@asyncio.coroutine
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
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): def on_domain_spawn(self, vm, _event, **kwargs):
"""Handler of 'domain-spawn' event, starts GUI daemon for stubdomain""" """Handler of 'domain-spawn' event, starts GUI daemon for stubdomain"""
try: try:
@ -340,7 +378,8 @@ class GUILauncher(object):
vm.log.warning('Failed to start GUI for %s: %s', vm.name, str(e)) vm.log.warning('Failed to start GUI for %s: %s', vm.name, str(e))
def on_domain_start(self, vm, _event, **kwargs): 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: try:
if getattr(vm, 'guivm', None) != vm.app.local_name: if getattr(vm, 'guivm', None) != vm.app.local_name:
return return
@ -351,8 +390,18 @@ class GUILauncher(object):
except qubesadmin.exc.QubesException as e: except qubesadmin.exc.QubesException as e:
vm.log.warning('Failed to start GUI for %s: %s', vm.name, str(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:
return
if not vm.features.check_with_template('audio', True):
return
if 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): 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. """ daemon for domains started before this tool. """
monitor_layout = get_monitor_layout() monitor_layout = get_monitor_layout()
@ -360,19 +409,23 @@ class GUILauncher(object):
for vm in self.app.domains: for vm in self.app.domains:
if vm.klass == 'AdminVM': if vm.klass == 'AdminVM':
continue continue
if getattr(vm, 'guivm', None) != vm.app.local_name: if getattr(vm, 'guivm', None) == vm.app.local_name:
continue if vm.features.check_with_template('gui', True):
if not vm.features.check_with_template('gui', True):
continue
power_state = vm.get_power_state() power_state = vm.get_power_state()
if power_state == 'Running': if power_state == 'Running':
asyncio.ensure_future( asyncio.ensure_future(
self.start_gui(vm, monitor_layout=monitor_layout)) self.start_gui(vm, monitor_layout=monitor_layout))
elif power_state == 'Transient': elif power_state == 'Transient':
# it is still starting, we'll get 'domain-start' event when # it is still starting, we'll get 'domain-start'
# fully started # event when fully started
if vm.virt_mode == 'hvm': if vm.virt_mode == 'hvm':
asyncio.ensure_future(self.start_gui_for_stubdomain(vm)) asyncio.ensure_future(
self.start_gui_for_stubdomain(vm))
if getattr(vm, 'audiovm', None) == vm.app.local_name:
if vm.features.check_with_template('audio', True):
power_state = vm.get_power_state()
if power_state == 'Running':
asyncio.ensure_future(self.start_audio(vm))
def register_events(self, events): def register_events(self, events):
"""Register domain startup events in app.events dispatcher""" """Register domain startup events in app.events dispatcher"""
@ -394,10 +447,10 @@ def x_reader(conn, callback):
if 'XDG_RUNTIME_DIR' in os.environ: if 'XDG_RUNTIME_DIR' in os.environ:
pidfile_path = os.path.join(os.environ['XDG_RUNTIME_DIR'], pidfile_path = os.path.join(os.environ['XDG_RUNTIME_DIR'],
'qvm-start-gui.pid') 'qvm-start-daemon.pid')
else: else:
pidfile_path = os.path.join(os.environ.get('HOME', '/'), pidfile_path = os.path.join(os.environ.get('HOME', '/'),
'.qvm-start-gui.pid') '.qvm-start-daemon.pid')
parser = qubesadmin.tools.QubesArgumentParser( parser = qubesadmin.tools.QubesArgumentParser(
description='start GUI for qube(s)', vmname_nargs='*') description='start GUI for qube(s)', vmname_nargs='*')
@ -415,13 +468,13 @@ parser.add_argument('--notify-monitor-layout', action='store_true',
def main(args=None): def main(args=None):
""" Main function of qvm-start-gui tool""" """ Main function of qvm-start-daemon tool"""
args = parser.parse_args(args) args = parser.parse_args(args)
if args.watch and not args.all_domains: if args.watch and not args.all_domains:
parser.error('--watch option must be used with --all') parser.error('--watch option must be used with --all')
if args.watch and args.notify_monitor_layout: if args.watch and args.notify_monitor_layout:
parser.error('--watch cannot be used with --notify-monitor-layout') parser.error('--watch cannot be used with --notify-monitor-layout')
launcher = GUILauncher(args.app) launcher = DAEMONLauncher(args.app)
if args.watch: if args.watch:
if not have_events: if not have_events:
parser.error('--watch option require Python >= 3.5') parser.error('--watch option require Python >= 3.5')
@ -468,6 +521,8 @@ def main(args=None):
if vm.is_running(): if vm.is_running():
tasks.append(asyncio.ensure_future(launcher.start_gui( tasks.append(asyncio.ensure_future(launcher.start_gui(
vm, force_stubdom=args.force_stubdomain))) vm, force_stubdom=args.force_stubdomain)))
tasks.append(asyncio.ensure_future(launcher.start_audio(
vm)))
if tasks: if tasks:
loop.run_until_complete(asyncio.wait(tasks)) loop.run_until_complete(asyncio.wait(tasks))
loop.stop() loop.stop()

View File

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