tools: add qvm-start-gui tool
This is mostly moved from qubes.ext.gui module.
This commit is contained in:
		
							parent
							
								
									e66b96edd1
								
							
						
					
					
						commit
						5ed3d8d262
					
				
							
								
								
									
										230
									
								
								qubesmgmt/tools/qvm_start_gui.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								qubesmgmt/tools/qvm_start_gui.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,230 @@ | ||||
| # -*- encoding: utf8 -*- | ||||
| # | ||||
| # The Qubes OS Project, http://www.qubes-os.org | ||||
| # | ||||
| # Copyright (C) 2017 Marek Marczykowski-Górecki | ||||
| #                               <marmarek@invisiblethingslab.com> | ||||
| # | ||||
| # This program is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU Lesser General Public License as published by | ||||
| # the Free Software Foundation; either version 2.1 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU Lesser General Public License for more details. | ||||
| # | ||||
| # 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/>. | ||||
| 
 | ||||
| ''' GUI daemon launcher tool''' | ||||
| 
 | ||||
| import os | ||||
| import signal | ||||
| import subprocess | ||||
| 
 | ||||
| import asyncio | ||||
| 
 | ||||
| import qubesmgmt | ||||
| import qubesmgmt.events | ||||
| import qubesmgmt.tools | ||||
| 
 | ||||
| GUI_DAEMON_PATH = '/usr/bin/qubes-guid' | ||||
| QUBES_ICON_DIR = '/usr/share/icons/hicolor/128x128/devices' | ||||
| 
 | ||||
| 
 | ||||
| class GUILauncher(object): | ||||
|     '''Launch GUI daemon for VMs''' | ||||
|     def __init__(self, app: qubesmgmt.app.QubesBase): | ||||
|         ''' Initialize GUILauncher. | ||||
| 
 | ||||
|         :param app: :py:class:`qubesmgmt.Qubes` instance | ||||
|         ''' | ||||
|         self.app = app | ||||
|         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'] | ||||
| 
 | ||||
|         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) | ||||
| 
 | ||||
|     def start_gui_for_vm(self, vm): | ||||
|         '''Start GUI daemon (qubes-guid) connected directly to a VM | ||||
| 
 | ||||
|         This function is a coroutine. | ||||
|         ''' | ||||
|         guid_cmd = self.common_guid_args(vm) | ||||
|         guid_cmd.extend(['-d', str(vm.xid)]) | ||||
| 
 | ||||
|         if vm.hvm: | ||||
|             guid_cmd.extend(['-n']) | ||||
| 
 | ||||
|             if vm.features.check_with_template('rpc-clipboard', False): | ||||
|                 guid_cmd.extend(['-Q']) | ||||
| 
 | ||||
|             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] | ||||
| 
 | ||||
|         return asyncio.create_subprocess_exec(*guid_cmd) | ||||
| 
 | ||||
|     def start_gui_for_stubdomain(self, vm): | ||||
|         '''Start GUI daemon (qubes-guid) connected to a stubdomain | ||||
| 
 | ||||
|         This function is a coroutine. | ||||
|         ''' | ||||
|         guid_cmd = self.common_guid_args(vm) | ||||
|         guid_cmd.extend(['-d', str(vm.stubdom_xid), '-t', str(vm.xid)]) | ||||
| 
 | ||||
|         return asyncio.create_subprocess_exec(*guid_cmd) | ||||
| 
 | ||||
|     @asyncio.coroutine | ||||
|     def start_gui(self, vm, force_stubdom=False): | ||||
|         '''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. | ||||
|         ''' | ||||
|         if not vm.features.check_with_template('gui', True): | ||||
|             return | ||||
| 
 | ||||
|         vm.log.info('Starting GUI') | ||||
|         if vm.hvm: | ||||
|             if force_stubdom or not os.path.exists(self.guid_pidfile(vm.xid)): | ||||
|                 if not os.path.exists(self.guid_pidfile(vm.stubdom_xid)): | ||||
|                     yield from self.start_gui_for_stubdomain(vm) | ||||
| 
 | ||||
|         if not os.path.exists(self.guid_pidfile(vm.xid)): | ||||
|             yield from self.start_gui_for_vm(vm) | ||||
| 
 | ||||
|     def on_domain_spawn(self, vm, _event, **kwargs): | ||||
|         '''Handler of 'domain-spawn' event, starts GUI daemon for stubdomain''' | ||||
|         if not vm.features.check_with_template('gui', True): | ||||
|             return | ||||
|         if vm.hvm and kwargs.get('start_guid', 'True') == 'True': | ||||
|             asyncio.ensure_future(self.start_gui_for_stubdomain(vm)) | ||||
| 
 | ||||
|     def on_domain_start(self, vm, _event, **kwargs): | ||||
|         '''Handler of 'domain-start' event, starts GUI daemon for actual VM''' | ||||
|         if not vm.features.check_with_template('gui', True): | ||||
|             return | ||||
|         if kwargs.get('start_guid', 'True') == 'True': | ||||
|             asyncio.ensure_future(self.start_gui_for_vm(vm)) | ||||
| 
 | ||||
|     def on_connection_established(self, _subject, _event, **_kwargs): | ||||
|         '''Handler of 'connection-established' event, used to launch GUI | ||||
|         daemon for domains started before this tool. ''' | ||||
|         for vm in self.app.domains: | ||||
|             if isinstance(vm, qubesmgmt.vm.AdminVM): | ||||
|                 continue | ||||
|             if vm.is_running(): | ||||
|                 asyncio.ensure_future(self.start_gui(vm)) | ||||
| 
 | ||||
|     def register_events(self, events): | ||||
|         '''Register domain startup events in app.events dispatcher''' | ||||
|         events.add_handler('domain-spawn', self.on_domain_spawn) | ||||
|         events.add_handler('domain-start', self.on_domain_start) | ||||
|         events.add_handler('connection-established', | ||||
|             self.on_connection_established) | ||||
| 
 | ||||
| 
 | ||||
| parser = qubesmgmt.tools.QubesArgumentParser( | ||||
|     description='forceful shutdown of a domain', vmname_nargs='*') | ||||
| parser.add_argument('--watch', action='store_true', | ||||
|     help='Keep watching for further domains startups, must be used with --all') | ||||
| 
 | ||||
| 
 | ||||
| def main(args=None): | ||||
|     ''' Main function of qvm-start-gui tool''' | ||||
|     args = parser.parse_args(args) | ||||
|     if args.watch and not args.all_domains: | ||||
|         parser.error('--watch option must be used with --all') | ||||
|     launcher = GUILauncher(args.app) | ||||
|     if args.watch: | ||||
|         loop = asyncio.get_event_loop() | ||||
|         events = qubesmgmt.events.EventsDispatcher(args.app) | ||||
|         launcher.register_events(events) | ||||
| 
 | ||||
|         events_listener = asyncio.ensure_future(events.listen_for_events()) | ||||
| 
 | ||||
|         for signame in ('SIGINT', 'SIGTERM'): | ||||
|             loop.add_signal_handler(getattr(signal, signame), | ||||
|                 events_listener.cancel)  # pylint: disable=no-member | ||||
| 
 | ||||
|         loop.run_until_complete(events_listener) | ||||
|         loop.stop() | ||||
|         loop.run_forever() | ||||
|         loop.close() | ||||
|     else: | ||||
|         loop = asyncio.get_event_loop() | ||||
|         tasks = [] | ||||
|         for vm in args.domains: | ||||
|             if vm.is_running(): | ||||
|                 tasks.append(asyncio.ensure_future(launcher.start_gui(vm))) | ||||
|         if tasks: | ||||
|             loop.run_until_complete(asyncio.wait(tasks)) | ||||
|         loop.stop() | ||||
|         loop.run_forever() | ||||
|         loop.close() | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     main() | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Marek Marczykowski-Górecki
						Marek Marczykowski-Górecki