Merge remote-tracking branch 'origin/pr/154'
* origin/pr/154: Add qubes-guivm-session utility qvm-start-daemon: allow --watch without --all qvm-start-daemon: convert to async/await syntax
This commit is contained in:
commit
77e1e080d7
1
Makefile
1
Makefile
@ -15,6 +15,7 @@ install:
|
|||||||
install -m 0644 etc/qvm-start-daemon-kde.desktop $(DESTDIR)/etc/xdg/autostart/
|
install -m 0644 etc/qvm-start-daemon-kde.desktop $(DESTDIR)/etc/xdg/autostart/
|
||||||
install -d $(DESTDIR)/usr/bin
|
install -d $(DESTDIR)/usr/bin
|
||||||
ln -sf qvm-start-daemon $(DESTDIR)/usr/bin/qvm-start-gui
|
ln -sf qvm-start-daemon $(DESTDIR)/usr/bin/qvm-start-gui
|
||||||
|
install -m 0755 scripts/qubes-guivm-session $(DESTDIR)/usr/bin/
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf test-packages/__pycache__ qubesadmin/__pycache__
|
rm -rf test-packages/__pycache__ qubesadmin/__pycache__
|
||||||
|
@ -9,3 +9,4 @@ mock
|
|||||||
lxml
|
lxml
|
||||||
PyYAML
|
PyYAML
|
||||||
xcffib
|
xcffib
|
||||||
|
asynctest
|
||||||
|
@ -47,7 +47,7 @@ Options
|
|||||||
|
|
||||||
.. option:: --watch
|
.. option:: --watch
|
||||||
|
|
||||||
Keep watching for further domains startups, must be used with --all
|
Keep watching for further domain startups
|
||||||
|
|
||||||
.. option:: --force-stubdomain
|
.. option:: --force-stubdomain
|
||||||
|
|
||||||
|
@ -23,9 +23,10 @@ import signal
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest.mock
|
import unittest.mock
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
import asynctest
|
||||||
|
|
||||||
import qubesadmin.tests
|
import qubesadmin.tests
|
||||||
import qubesadmin.tools.qvm_start_daemon
|
import qubesadmin.tools.qvm_start_daemon
|
||||||
from qubesadmin.tools.qvm_start_daemon import GUI_DAEMON_OPTIONS
|
from qubesadmin.tools.qvm_start_daemon import GUI_DAEMON_OPTIONS
|
||||||
@ -207,7 +208,7 @@ global: {
|
|||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
|
|
||||||
@unittest.mock.patch('asyncio.create_subprocess_exec')
|
@asynctest.patch('asyncio.create_subprocess_exec')
|
||||||
def test_020_start_gui_for_vm(self, proc_mock):
|
def test_020_start_gui_for_vm(self, proc_mock):
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
@ -238,7 +239,7 @@ global: {
|
|||||||
|
|
||||||
self.assertAllCalled()
|
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):
|
def test_021_start_gui_for_vm_hvm(self, proc_mock):
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
@ -307,7 +308,7 @@ global: {
|
|||||||
pidfile.flush()
|
pidfile.flush()
|
||||||
self.addCleanup(pidfile.close)
|
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,
|
patch_args = unittest.mock.patch.object(self.launcher,
|
||||||
'common_guid_args',
|
'common_guid_args',
|
||||||
lambda vm: [])
|
lambda vm: [])
|
||||||
@ -350,7 +351,7 @@ global: {
|
|||||||
None)] = \
|
None)] = \
|
||||||
b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00'
|
b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00'
|
||||||
proc_mock = unittest.mock.Mock()
|
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,
|
lambda *args: self.mock_coroutine(proc_mock,
|
||||||
*args)):
|
*args)):
|
||||||
with unittest.mock.patch.object(self.launcher,
|
with unittest.mock.patch.object(self.launcher,
|
||||||
@ -384,7 +385,7 @@ global: {
|
|||||||
None)] = \
|
None)] = \
|
||||||
b'0\x001'
|
b'0\x001'
|
||||||
proc_mock = unittest.mock.Mock()
|
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,
|
lambda *args: self.mock_coroutine(proc_mock,
|
||||||
*args)):
|
*args)):
|
||||||
with unittest.mock.patch.object(self.launcher,
|
with unittest.mock.patch.object(self.launcher,
|
||||||
|
@ -32,20 +32,12 @@ import xcffib.xproto # pylint: disable=unused-import
|
|||||||
|
|
||||||
import daemon.pidfile
|
import daemon.pidfile
|
||||||
import qubesadmin
|
import qubesadmin
|
||||||
|
import qubesadmin.events
|
||||||
import qubesadmin.exc
|
import qubesadmin.exc
|
||||||
import qubesadmin.tools
|
import qubesadmin.tools
|
||||||
import qubesadmin.vm
|
import qubesadmin.vm
|
||||||
from . import xcffibhelpers
|
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'
|
GUI_DAEMON_PATH = '/usr/bin/qubes-guid'
|
||||||
PACAT_DAEMON_PATH = '/usr/bin/pacat-simple-vchan'
|
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'
|
||||||
@ -322,17 +314,19 @@ def get_monitor_layout():
|
|||||||
class DAEMONLauncher:
|
class DAEMONLauncher:
|
||||||
"""Launch GUI/AUDIO daemon for VMs"""
|
"""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.
|
""" Initialize DAEMONLauncher.
|
||||||
|
|
||||||
:param app: :py:class:`qubesadmin.Qubes` instance
|
: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.app = app
|
||||||
self.started_processes = {}
|
self.started_processes = {}
|
||||||
self.kde = False
|
self.vm_names = vm_names
|
||||||
|
self.kde = kde
|
||||||
|
|
||||||
@asyncio.coroutine
|
async 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
|
||||||
|
|
||||||
This function is a coroutine.
|
This function is a coroutine.
|
||||||
@ -367,7 +361,7 @@ class DAEMONLauncher:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield from asyncio.get_event_loop(). \
|
await asyncio.get_event_loop(). \
|
||||||
run_in_executor(None,
|
run_in_executor(None,
|
||||||
functools.partial(
|
functools.partial(
|
||||||
vm.run_service_for_stdio,
|
vm.run_service_for_stdio,
|
||||||
@ -476,8 +470,7 @@ class DAEMONLauncher:
|
|||||||
else vm.xid
|
else vm.xid
|
||||||
return xid
|
return xid
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def start_gui_for_vm(self, vm, monitor_layout=None):
|
||||||
def start_gui_for_vm(self, vm, monitor_layout=None):
|
|
||||||
"""Start GUI daemon (qubes-guid) connected directly to a VM
|
"""Start GUI daemon (qubes-guid) connected directly to a VM
|
||||||
|
|
||||||
This function is a coroutine.
|
This function is a coroutine.
|
||||||
@ -503,13 +496,12 @@ class DAEMONLauncher:
|
|||||||
|
|
||||||
vm.log.info('Starting GUI')
|
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)
|
startup=True)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def start_gui_for_stubdomain(self, vm, force=False):
|
||||||
def start_gui_for_stubdomain(self, vm, force=False):
|
|
||||||
"""Start GUI daemon (qubes-guid) connected to a stubdomain
|
"""Start GUI daemon (qubes-guid) connected to a stubdomain
|
||||||
|
|
||||||
This function is a coroutine.
|
This function is a coroutine.
|
||||||
@ -533,10 +525,9 @@ class DAEMONLauncher:
|
|||||||
guid_cmd = self.common_guid_args(vm)
|
guid_cmd = self.common_guid_args(vm)
|
||||||
guid_cmd.extend(['-d', str(vm.stubdom_xid), '-t', str(vm.xid)])
|
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
|
async def start_audio_for_vm(self, vm):
|
||||||
def start_audio_for_vm(self, vm):
|
|
||||||
"""Start AUDIO daemon (pacat-simple-vchan) connected directly to a VM
|
"""Start AUDIO daemon (pacat-simple-vchan) connected directly to a VM
|
||||||
|
|
||||||
This function is a coroutine.
|
This function is a coroutine.
|
||||||
@ -547,10 +538,9 @@ class DAEMONLauncher:
|
|||||||
pacat_cmd = [PACAT_DAEMON_PATH, '-l', self.pacat_domid(vm), vm.name]
|
pacat_cmd = [PACAT_DAEMON_PATH, '-l', self.pacat_domid(vm), vm.name]
|
||||||
vm.log.info('Starting AUDIO')
|
vm.log.info('Starting AUDIO')
|
||||||
|
|
||||||
yield from asyncio.create_subprocess_exec(*pacat_cmd)
|
await asyncio.create_subprocess_exec(*pacat_cmd)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def start_gui(self, vm, force_stubdom=False, monitor_layout=None):
|
||||||
def start_gui(self, vm, force_stubdom=False, monitor_layout=None):
|
|
||||||
"""Start GUI daemon regardless of start event.
|
"""Start GUI daemon regardless of start event.
|
||||||
|
|
||||||
This function is a coroutine.
|
This function is a coroutine.
|
||||||
@ -566,16 +556,15 @@ class DAEMONLauncher:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if vm.virt_mode == 'hvm':
|
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):
|
if not vm.features.check_with_template('gui', True):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not os.path.exists(self.guid_pidfile(vm.xid)):
|
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
|
async def start_audio(self, vm):
|
||||||
def start_audio(self, vm):
|
|
||||||
"""Start AUDIO daemon regardless of start event.
|
"""Start AUDIO daemon regardless of start event.
|
||||||
|
|
||||||
This function is a coroutine.
|
This function is a coroutine.
|
||||||
@ -592,10 +581,14 @@ class DAEMONLauncher:
|
|||||||
|
|
||||||
xid = self.pacat_domid(vm)
|
xid = self.pacat_domid(vm)
|
||||||
if not os.path.exists(self.pacat_pidfile(xid)):
|
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):
|
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"""
|
||||||
|
|
||||||
|
if not self.is_watched(vm):
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if getattr(vm, 'guivm', None) != vm.app.local_name:
|
if getattr(vm, 'guivm', None) != vm.app.local_name:
|
||||||
return
|
return
|
||||||
@ -610,6 +603,10 @@ class DAEMONLauncher:
|
|||||||
def on_domain_start(self, vm, _event, **kwargs):
|
def on_domain_start(self, vm, _event, **kwargs):
|
||||||
"""Handler of 'domain-start' event, starts GUI/AUDIO daemon for
|
"""Handler of 'domain-start' event, starts GUI/AUDIO daemon for
|
||||||
actual VM """
|
actual VM """
|
||||||
|
|
||||||
|
if not self.is_watched(vm):
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if getattr(vm, 'guivm', None) == vm.app.local_name and \
|
if getattr(vm, 'guivm', None) == vm.app.local_name and \
|
||||||
vm.features.check_with_template('gui', True) and \
|
vm.features.check_with_template('gui', True) and \
|
||||||
@ -636,6 +633,9 @@ class DAEMONLauncher:
|
|||||||
if vm.klass == 'AdminVM':
|
if vm.klass == 'AdminVM':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if not self.is_watched(vm):
|
||||||
|
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(
|
||||||
@ -650,6 +650,10 @@ class DAEMONLauncher:
|
|||||||
|
|
||||||
def on_domain_stopped(self, vm, _event, **_kwargs):
|
def on_domain_stopped(self, vm, _event, **_kwargs):
|
||||||
"""Handler of 'domain-stopped' event, cleans up"""
|
"""Handler of 'domain-stopped' event, cleans up"""
|
||||||
|
|
||||||
|
if not self.is_watched(vm):
|
||||||
|
return
|
||||||
|
|
||||||
self.cleanup_guid(vm.xid)
|
self.cleanup_guid(vm.xid)
|
||||||
if vm.virt_mode == 'hvm':
|
if vm.virt_mode == 'hvm':
|
||||||
self.cleanup_guid(vm.stubdom_xid)
|
self.cleanup_guid(vm.stubdom_xid)
|
||||||
@ -672,6 +676,16 @@ class DAEMONLauncher:
|
|||||||
self.on_connection_established)
|
self.on_connection_established)
|
||||||
events.add_handler('domain-stopped', self.on_domain_stopped)
|
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:
|
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-daemon.pid')
|
'qvm-start-daemon.pid')
|
||||||
@ -682,8 +696,7 @@ else:
|
|||||||
parser = qubesadmin.tools.QubesArgumentParser(
|
parser = qubesadmin.tools.QubesArgumentParser(
|
||||||
description='start GUI for qube(s)', vmname_nargs='*')
|
description='start GUI for qube(s)', vmname_nargs='*')
|
||||||
parser.add_argument('--watch', action='store_true',
|
parser.add_argument('--watch', action='store_true',
|
||||||
help='Keep watching for further domains'
|
help='Keep watching for further domain startups')
|
||||||
' startups, must be used with --all')
|
|
||||||
parser.add_argument('--force-stubdomain', action='store_true',
|
parser.add_argument('--force-stubdomain', action='store_true',
|
||||||
help='Start GUI to stubdomain-emulated VGA,'
|
help='Start GUI to stubdomain-emulated VGA,'
|
||||||
' even if gui-agent is running in the VM')
|
' even if gui-agent is running in the VM')
|
||||||
@ -710,16 +723,19 @@ def main(args=None):
|
|||||||
print(parser.format_help())
|
print(parser.format_help())
|
||||||
return
|
return
|
||||||
args = parser.parse_args(args)
|
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:
|
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 = DAEMONLauncher(args.app)
|
|
||||||
if args.kde:
|
if args.all_domains:
|
||||||
launcher.kde = True
|
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 args.watch:
|
||||||
if not have_events:
|
|
||||||
parser.error('--watch option require Python >= 3.5')
|
|
||||||
with daemon.pidfile.TimeoutPIDLockFile(args.pidfile):
|
with daemon.pidfile.TimeoutPIDLockFile(args.pidfile):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
|
21
scripts/qubes-guivm-session
Executable file
21
scripts/qubes-guivm-session
Executable file
@ -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
|
Loading…
Reference in New Issue
Block a user