diff --git a/Makefile b/Makefile index 5fe4645f..30951f3d 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,11 @@ install: install -d $(DESTDIR)/etc/xdg/autostart install -m 0644 etc/qvm-start-daemon.desktop $(DESTDIR)/etc/xdg/autostart/ install -m 0644 etc/qvm-start-daemon-kde.desktop $(DESTDIR)/etc/xdg/autostart/ + install -d $(DESTDIR)/usr/share/xsessions/ + install -m 0644 xsessions/sys-gui.desktop $(DESTDIR)/usr/share/xsessions/ install -d $(DESTDIR)/usr/bin ln -sf qvm-start-daemon $(DESTDIR)/usr/bin/qvm-start-gui + install -m 0755 scripts/qubes-gui-session $(DESTDIR)/usr/bin/ clean: rm -rf test-packages/__pycache__ qubesadmin/__pycache__ diff --git a/doc/manpages/qvm-start-daemon.rst b/doc/manpages/qvm-start-daemon.rst index 630bdf94..4fbd8a06 100644 --- a/doc/manpages/qvm-start-daemon.rst +++ b/doc/manpages/qvm-start-daemon.rst @@ -47,7 +47,11 @@ Options .. option:: --watch - Keep watching for further domains startups, must be used with --all + Keep watching for further domains startups + +.. option:: --exit + + Exit after all watched domains have exited .. option:: --force-stubdomain diff --git a/etc/sys-gui.desktop b/etc/sys-gui.desktop new file mode 100644 index 00000000..cb21e213 --- /dev/null +++ b/etc/sys-gui.desktop @@ -0,0 +1,4 @@ +[Desktop Entry] +Name=GUI Domain (sys-gui) +Exec=qubes-gui-session sys-gui +Type=Application diff --git a/qubesadmin/tools/qvm_start_daemon.py b/qubesadmin/tools/qvm_start_daemon.py index 291de09e..85943815 100644 --- a/qubesadmin/tools/qvm_start_daemon.py +++ b/qubesadmin/tools/qvm_start_daemon.py @@ -27,6 +27,8 @@ import re import functools import sys +import logging + import xcffib import xcffib.xproto # pylint: disable=unused-import @@ -46,6 +48,8 @@ except ImportError: pass +logger = logging.getLogger('qvm-start-daemon') + GUI_DAEMON_PATH = '/usr/bin/qubes-guid' PACAT_DAEMON_PATH = '/usr/bin/pacat-simple-vchan' QUBES_ICON_DIR = '/usr/share/icons/hicolor/128x128/devices' @@ -322,14 +326,24 @@ 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, + vms=None, + exit_after_vms=False, + kde=False): """ Initialize DAEMONLauncher. :param app: :py:class:`qubesadmin.Qubes` instance + :param vms: VMs to watch for, or None if watching for all + :param exit_after_vms: exit when all VMs are finished """ self.app = app - self.started_processes = {} - self.kde = False + self.vms = vms + self.exit_after_vms = exit_after_vms + self.kde = kde + + self.started_vms = set() + self.cancel = None @asyncio.coroutine def send_monitor_layout(self, vm, layout=None, startup=False): @@ -596,6 +610,9 @@ def start_audio(self, 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 @@ -610,6 +627,11 @@ 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 + + self.started_vms.add(vm) + try: if getattr(vm, 'guivm', None) == vm.app.local_name and \ vm.features.check_with_template('gui', True) and \ @@ -636,11 +658,15 @@ 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( self.start_gui(vm, monitor_layout=monitor_layout)) asyncio.ensure_future(self.start_audio(vm)) + self.started_vms.add(vm) elif power_state == 'Transient': # it is still starting, we'll get 'domain-start' # event when fully started @@ -648,12 +674,26 @@ def on_connection_established(self, _subject, _event, **_kwargs): asyncio.ensure_future( self.start_gui_for_stubdomain(vm)) + if self.exit_after_vms and not self.started_vms: + logger.info('No VMs started yet, exiting') + self.cancel() + 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) + if vm not in self.started_vms: + logger.warning('VM not in started_vms: %s', vm) + self.started_vms.remove(vm) + if self.exit_after_vms and not self.started_vms: + logger.info('All VMs stopped, exiting') + self.cancel() + def cleanup_guid(self, xid): """ Clean up after qubes-guid. Removes the auto-generated configuration @@ -672,6 +712,20 @@ def register_events(self, events): self.on_connection_established) events.add_handler('domain-stopped', self.on_domain_stopped) + def set_cancel(self, cancel): + """Set a 'cancel' callback in case of early exit""" + self.cancel = cancel + + def is_watched(self, vm): + ''' + Should we watch this VM for changes + ''' + + if self.vms is None: + return True + return vm in self.vms + + if 'XDG_RUNTIME_DIR' in os.environ: pidfile_path = os.path.join(os.environ['XDG_RUNTIME_DIR'], 'qvm-start-daemon.pid') @@ -683,7 +737,9 @@ def register_events(self, events): 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') + ' startups') +parser.add_argument('--exit', action='store_true', + help='Exit when all domains exit') parser.add_argument('--force-stubdomain', action='store_true', help='Start GUI to stubdomain-emulated VGA,' ' even if gui-agent is running in the VM') @@ -710,13 +766,15 @@ 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 + + launcher = DAEMONLauncher( + args.app, + vms=None if args.all_domains else args.domains, + exit_after_vms=args.exit, + kde=args.kde, + ) if args.watch: if not have_events: parser.error('--watch option require Python >= 3.5') @@ -728,6 +786,7 @@ def main(args=None): launcher.register_events(events) events_listener = asyncio.ensure_future(events.listen_for_events()) + launcher.set_cancel(events_listener.cancel) for signame in ('SIGINT', 'SIGTERM'): loop.add_signal_handler(getattr(signal, signame), diff --git a/rpm_spec/qubes-core-admin-client.spec.in b/rpm_spec/qubes-core-admin-client.spec.in index 156c792a..6278248c 100644 --- a/rpm_spec/qubes-core-admin-client.spec.in +++ b/rpm_spec/qubes-core-admin-client.spec.in @@ -60,6 +60,7 @@ make -C doc DESTDIR=$RPM_BUILD_ROOT \ %{_bindir}/qvm-* %{_mandir}/man1/qvm-*.1* %{_mandir}/man1/qubes*.1* +%{_datadir}/xsessions/sys-gui.desktop %files -n python%{python3_pkgversion}-qubesadmin %{python3_sitelib}/qubesadmin-*egg-info diff --git a/scripts/qubes-gui-session b/scripts/qubes-gui-session new file mode 100755 index 00000000..06865f1a --- /dev/null +++ b/scripts/qubes-gui-session @@ -0,0 +1,17 @@ +#!/bin/bash + +print_usage() { +cat >&2 <