Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/pr/154'
Browse files Browse the repository at this point in the history
* origin/pr/154:
  Add qubes-guivm-session utility
  qvm-start-daemon: allow --watch without --all
  qvm-start-daemon: convert to async/await syntax
  • Loading branch information
marmarek committed Jul 30, 2020
2 parents a078e1f + 624e4e3 commit 77e1e08
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 47 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ install:
install -m 0644 etc/qvm-start-daemon-kde.desktop $(DESTDIR)/etc/xdg/autostart/
install -d $(DESTDIR)/usr/bin
ln -sf qvm-start-daemon $(DESTDIR)/usr/bin/qvm-start-gui
install -m 0755 scripts/qubes-guivm-session $(DESTDIR)/usr/bin/

clean:
rm -rf test-packages/__pycache__ qubesadmin/__pycache__
Expand Down
1 change: 1 addition & 0 deletions ci/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ mock
lxml
PyYAML
xcffib
asynctest
2 changes: 1 addition & 1 deletion doc/manpages/qvm-start-daemon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Options

.. option:: --watch

Keep watching for further domains startups, must be used with --all
Keep watching for further domain startups

.. option:: --force-stubdomain

Expand Down
13 changes: 7 additions & 6 deletions qubesadmin/tests/tools/qvm_start_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
import tempfile
import unittest.mock
import re

import asyncio

import asynctest

import qubesadmin.tests
import qubesadmin.tools.qvm_start_daemon
from qubesadmin.tools.qvm_start_daemon import GUI_DAEMON_OPTIONS
Expand Down Expand Up @@ -207,7 +208,7 @@ def test_013_common_args_guid_config(self):
}
''')

@unittest.mock.patch('asyncio.create_subprocess_exec')
@asynctest.patch('asyncio.create_subprocess_exec')
def test_020_start_gui_for_vm(self, proc_mock):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
Expand Down Expand Up @@ -238,7 +239,7 @@ def test_020_start_gui_for_vm(self, proc_mock):

self.assertAllCalled()

@unittest.mock.patch('asyncio.create_subprocess_exec')
@asynctest.patch('asyncio.create_subprocess_exec')
def test_021_start_gui_for_vm_hvm(self, proc_mock):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
Expand Down Expand Up @@ -307,7 +308,7 @@ def test_022_start_gui_for_vm_hvm_stubdom(self):
pidfile.flush()
self.addCleanup(pidfile.close)

patch_proc = unittest.mock.patch('asyncio.create_subprocess_exec')
patch_proc = asynctest.patch('asyncio.create_subprocess_exec')
patch_args = unittest.mock.patch.object(self.launcher,
'common_guid_args',
lambda vm: [])
Expand Down Expand Up @@ -350,7 +351,7 @@ def test_030_start_gui_for_stubdomain(self):
None)] = \
b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00'
proc_mock = unittest.mock.Mock()
with unittest.mock.patch('asyncio.create_subprocess_exec',
with asynctest.patch('asyncio.create_subprocess_exec',
lambda *args: self.mock_coroutine(proc_mock,
*args)):
with unittest.mock.patch.object(self.launcher,
Expand Down Expand Up @@ -384,7 +385,7 @@ def test_031_start_gui_for_stubdomain_forced(self):
None)] = \
b'0\x001'
proc_mock = unittest.mock.Mock()
with unittest.mock.patch('asyncio.create_subprocess_exec',
with asynctest.patch('asyncio.create_subprocess_exec',
lambda *args: self.mock_coroutine(proc_mock,
*args)):
with unittest.mock.patch.object(self.launcher,
Expand Down
96 changes: 56 additions & 40 deletions qubesadmin/tools/qvm_start_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,12 @@

import daemon.pidfile
import qubesadmin
import qubesadmin.events
import qubesadmin.exc
import qubesadmin.tools
import qubesadmin.vm
from . import xcffibhelpers

have_events = False
try:
# pylint: disable=wrong-import-position
import qubesadmin.events

have_events = True
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'
Expand Down Expand Up @@ -322,17 +314,19 @@ def get_monitor_layout():
class DAEMONLauncher:
"""Launch GUI/AUDIO daemon for VMs"""

def __init__(self, app: qubesadmin.app.QubesBase):
def __init__(self, app: qubesadmin.app.QubesBase, vm_names=None, kde=False):
""" Initialize DAEMONLauncher.
:param app: :py:class:`qubesadmin.Qubes` instance
:param vm_names: VM names to watch for, or None if watching for all
:param kde: add KDE-specific arguments for guid
"""
self.app = app
self.started_processes = {}
self.kde = False
self.vm_names = vm_names
self.kde = kde

@asyncio.coroutine
def send_monitor_layout(self, vm, layout=None, startup=False):
async def send_monitor_layout(self, vm, layout=None, startup=False):
"""Send monitor layout to a given VM
This function is a coroutine.
Expand Down Expand Up @@ -367,7 +361,7 @@ def send_monitor_layout(self, vm, layout=None, startup=False):
pass

try:
yield from asyncio.get_event_loop(). \
await asyncio.get_event_loop(). \
run_in_executor(None,
functools.partial(
vm.run_service_for_stdio,
Expand Down Expand Up @@ -476,8 +470,7 @@ def pacat_domid(vm):
else vm.xid
return xid

@asyncio.coroutine
def start_gui_for_vm(self, vm, monitor_layout=None):
async 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.
Expand All @@ -503,13 +496,12 @@ def start_gui_for_vm(self, vm, monitor_layout=None):

vm.log.info('Starting GUI')

yield from asyncio.create_subprocess_exec(*guid_cmd)
await asyncio.create_subprocess_exec(*guid_cmd)

yield from self.send_monitor_layout(vm, layout=monitor_layout,
await self.send_monitor_layout(vm, layout=monitor_layout,
startup=True)

@asyncio.coroutine
def start_gui_for_stubdomain(self, vm, force=False):
async def start_gui_for_stubdomain(self, vm, force=False):
"""Start GUI daemon (qubes-guid) connected to a stubdomain
This function is a coroutine.
Expand All @@ -533,10 +525,9 @@ def start_gui_for_stubdomain(self, vm, force=False):
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)
await asyncio.create_subprocess_exec(*guid_cmd)

@asyncio.coroutine
def start_audio_for_vm(self, vm):
async def start_audio_for_vm(self, vm):
"""Start AUDIO daemon (pacat-simple-vchan) connected directly to a VM
This function is a coroutine.
Expand All @@ -547,10 +538,9 @@ def start_audio_for_vm(self, vm):
pacat_cmd = [PACAT_DAEMON_PATH, '-l', self.pacat_domid(vm), vm.name]
vm.log.info('Starting AUDIO')

yield from asyncio.create_subprocess_exec(*pacat_cmd)
await asyncio.create_subprocess_exec(*pacat_cmd)

@asyncio.coroutine
def start_gui(self, vm, force_stubdom=False, monitor_layout=None):
async def start_gui(self, vm, force_stubdom=False, monitor_layout=None):
"""Start GUI daemon regardless of start event.
This function is a coroutine.
Expand All @@ -566,16 +556,15 @@ def start_gui(self, vm, force_stubdom=False, monitor_layout=None):
return

if vm.virt_mode == 'hvm':
yield from self.start_gui_for_stubdomain(vm, force=force_stubdom)
await 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)
await self.start_gui_for_vm(vm, monitor_layout=monitor_layout)

@asyncio.coroutine
def start_audio(self, vm):
async def start_audio(self, vm):
"""Start AUDIO daemon regardless of start event.
This function is a coroutine.
Expand All @@ -592,10 +581,14 @@ def start_audio(self, vm):

xid = self.pacat_domid(vm)
if not os.path.exists(self.pacat_pidfile(xid)):
yield from self.start_audio_for_vm(vm)
await self.start_audio_for_vm(vm)

def on_domain_spawn(self, vm, _event, **kwargs):
"""Handler of 'domain-spawn' event, starts GUI daemon for stubdomain"""

if not self.is_watched(vm):
return

try:
if getattr(vm, 'guivm', None) != vm.app.local_name:
return
Expand All @@ -610,6 +603,10 @@ def on_domain_spawn(self, vm, _event, **kwargs):
def on_domain_start(self, vm, _event, **kwargs):
"""Handler of 'domain-start' event, starts GUI/AUDIO daemon for
actual VM """

if not self.is_watched(vm):
return

try:
if getattr(vm, 'guivm', None) == vm.app.local_name and \
vm.features.check_with_template('gui', True) and \
Expand All @@ -636,6 +633,9 @@ def on_connection_established(self, _subject, _event, **_kwargs):
if vm.klass == 'AdminVM':
continue

if not self.is_watched(vm):
continue

power_state = vm.get_power_state()
if power_state == 'Running':
asyncio.ensure_future(
Expand All @@ -650,6 +650,10 @@ def on_connection_established(self, _subject, _event, **_kwargs):

def on_domain_stopped(self, vm, _event, **_kwargs):
"""Handler of 'domain-stopped' event, cleans up"""

if not self.is_watched(vm):
return

self.cleanup_guid(vm.xid)
if vm.virt_mode == 'hvm':
self.cleanup_guid(vm.stubdom_xid)
Expand All @@ -672,6 +676,16 @@ def register_events(self, events):
self.on_connection_established)
events.add_handler('domain-stopped', self.on_domain_stopped)

def is_watched(self, vm):
"""
Should we watch this VM for changes
"""

if self.vm_names is None:
return True
return vm.name in self.vm_names


if 'XDG_RUNTIME_DIR' in os.environ:
pidfile_path = os.path.join(os.environ['XDG_RUNTIME_DIR'],
'qvm-start-daemon.pid')
Expand All @@ -682,8 +696,7 @@ def register_events(self, events):
parser = qubesadmin.tools.QubesArgumentParser(
description='start GUI for qube(s)', vmname_nargs='*')
parser.add_argument('--watch', action='store_true',
help='Keep watching for further domains'
' startups, must be used with --all')
help='Keep watching for further domain startups')
parser.add_argument('--force-stubdomain', action='store_true',
help='Start GUI to stubdomain-emulated VGA,'
' even if gui-agent is running in the VM')
Expand All @@ -710,16 +723,19 @@ def main(args=None):
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 = DAEMONLauncher(args.app)
if args.kde:
launcher.kde = True

if args.all_domains:
vm_names = None
else:
vm_names = [vm.name for vm in args.domains]
launcher = DAEMONLauncher(
args.app,
vm_names=vm_names,
kde=args.kde)

if args.watch:
if not have_events:
parser.error('--watch option require Python >= 3.5')
with daemon.pidfile.TimeoutPIDLockFile(args.pidfile):
loop = asyncio.get_event_loop()
# pylint: disable=no-member
Expand Down
21 changes: 21 additions & 0 deletions scripts/qubes-guivm-session
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash -e

print_usage() {
cat >&2 <<USAGE
Usage: $0 vmname
Starts given VM and runs its associated GUI daemon. Used as X session for the
GUI domain.
USAGE
}

if [ $# -lt 1 ] ; then
print_usage
exit 1
fi

# Start VM, gui-daemon and audio
qvm-start --skip-if-running "$1"
qvm-start-daemon --watch "$1" &

# Run the inner session (Xephyr) and wait until it exits
exec qvm-run -p --no-gui --service "$1" qubes.GuiVMSession

0 comments on commit 77e1e08

Please sign in to comment.