Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a script for GuiVM session #154

Merged
merged 3 commits into from
Aug 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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