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
This commit is contained in:
Marek Marczykowski-Górecki 2020-04-09 05:24:26 +02:00
commit 4971faa462
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
15 changed files with 365 additions and 266 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 qvm-start-daemon $(DESTDIR)/usr/bin/qvm-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

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

View File

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

View File

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

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

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

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

View File

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

View File

@ -18,15 +18,15 @@
# 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 sys
import xcffib import xcffib
import xcffib.xproto # pylint: disable=unused-import import xcffib.xproto # pylint: disable=unused-import
@ -46,6 +46,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 +114,42 @@ def get_monitor_layout():
return outputs return outputs
class GUILauncher(object): def set_keyboard_layout(vm):
"""Launch GUI daemon for VMs""" """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): 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 +210,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.{}'.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,19 +404,26 @@ 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 and \
return vm.features.check_with_template('gui', True) and \
if not vm.features.check_with_template('gui', True): kwargs.get('start_guid', 'True') == 'True':
return
if kwargs.get('start_guid', 'True') == 'True':
asyncio.ensure_future(self.start_gui_for_vm(vm)) asyncio.ensure_future(self.start_gui_for_vm(vm))
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 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): 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 +431,18 @@ 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:
continue
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))
asyncio.ensure_future(self.start_audio(vm))
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))
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 +464,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='*')
@ -412,16 +482,35 @@ parser.add_argument('--pidfile', action='store', default=pidfile_path,
parser.add_argument('--notify-monitor-layout', action='store_true', parser.add_argument('--notify-monitor-layout', action='store_true',
help='Notify running instance in --watch mode' help='Notify running instance in --watch mode'
' about changed monitor layout') ' 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): 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) 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) 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 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 +557,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

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

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*