#!/usr/bin/python """ Xspice Xspice is a standard X server that is also a Spice server. It is implemented as a module with video, mouse and keyboard drivers. The video driver is mostly the same code as the qxl guest driver, hence Xspice is kept in the same repository. It can also be used to debug the qxl driver. Xspice (this executable) will set a bunch of environment variables that are used by spiceqxl_drv.so, and then spawn Xorg, giving it the default config file, which can be overridden as well. """ import argparse import os import sys import tempfile import atexit import time import signal from subprocess import Popen, PIPE def which(x): if not x: return x if os.path.exists(x): return x for p in os.environ['PATH'].split(':'): candidate = os.path.join(p, x) if os.path.exists(candidate): return candidate print('Warning: failed to find executable %s' % x) return None if 'XSPICE_ENABLE_GDB' in os.environ: cgdb = which('cgdb') if not cgdb: cgdb = which('gdb') else: cgdb = None def add_boolean(flag, *args, **kw): parser.add_argument(flag, action='store_const', const='1', *args, **kw) wan_compression_options = ['auto', 'never', 'always'] parser = argparse.ArgumentParser("Xspice", description="X and Spice server. example usage: Xspice --port 5900 --disable-ticketing :1.0", usage="Xspice [Xspice and Xorg options intermixed]", epilog="Any option not parsed by Xspice gets passed to Xorg as is.") # X-related options parser.add_argument('--xorg', default=which('Xorg'), help='specify the path to the Xorg binary') parser.add_argument('--config', default='spiceqxl.xorg.conf', help='specify the path to the Xspice configuration') parser.add_argument('--auto', action='store_true', help='automatically create a temporary xorg.conf and start the X server') parser.add_argument('--xsession', help='if given, will run after Xorg launch. Should be a program like x-session-manager') # Network and security options add_boolean('--disable-ticketing', help="do not require a client password") parser.add_argument('--password', help="set the password required to connect to the server") add_boolean('--sasl', help="use SASL to authenticate to the server") # Don't use any options that are already used by Xorg (unless we must) # specifically, don't use -p and -s. parser.add_argument('--port', type=int, help="use the specified port as Spice's regular unencrypted port") parser.add_argument('--tls-port', type=int, help='use the specified port as a TLS (encrypted) port', default=0) parser.add_argument('--x509-dir', help="set the directory where the CA certificate, server key and server certificate are searched for TLS, using the same predefined names QEMU uses") parser.add_argument('--cacert-file', help="set the CA certificate file location for TLS") parser.add_argument('--x509-key-file', help="set the server key file location for TLS") parser.add_argument('--x509-key-password', help="set the server key's password for TLS") parser.add_argument('--x509-cert-file', help="set the server certificate file location for TLS") parser.add_argument('--dh-file', help="set the server DH file location for TLS") parser.add_argument('--tls-ciphers', help="set the TLS ciphers preference order") add_boolean('--ipv4-only', help="only accept IP v4 connections") add_boolean('--ipv6-only', help="only accept IP v6 connections") parser.add_argument('--exit-on-disconnect', action='store_true', help='exit the X server when any client disconnects') # Monitor configuration options parser.add_argument('--numheads', type=int, help='number of virtual heads to create') # Compression options parser.add_argument('--jpeg-wan-compression', choices=wan_compression_options, help="set jpeg wan compression") parser.add_argument('--zlib-glz-wan-compression', choices=wan_compression_options, help="set zlib glz wan compressions") parser.add_argument('--image-compression', choices = ['off', 'auto_glz', 'auto_lz', 'quic', 'glz', 'lz'], help="set image compression") parser.add_argument('--deferred-fps', type=int, help='if non zero, the driver will render all operations to the frame buffer, and keep track of a changed rectangle list. The changed rectangles will be transmitted at the rate requested (e.g. 10 frames per second). This can dramatically reduce network bandwidth for some use cases') # TODO - sound support parser.add_argument('--streaming-video', choices=['off', 'all', 'filter'], help='set the streaming video method') parser.add_argument('--video-codecs', help='set a semicolon-separated list of preferred video codecs to use. Each takes the form encoder:codec, with spice:mjpeg being the default and other options being provided by gstreamer for the mjpeg, vp8 and h264 codecs') # VDAgent options parser.add_argument('--vdagent', action='store_true', dest='vdagent_enabled', default=False, help='launch vdagent & vdagentd. They provide clipboard & resolution automation') parser.add_argument('--vdagent-virtio-path', help='virtio socket path used by vdagentd') parser.add_argument('--vdagent-uinput-path', help='uinput socket path used by vdagent') parser.add_argument('--vdagent-udcs-path', help='Unix domain socket path used by vdagent and vdagentd') parser.add_argument('--vdagentd-exec', help='path to spice-vdagentd (used with --vdagent)') parser.add_argument('--vdagent-exec', help='path to spice-vdagent (used with --vdagent)') parser.add_argument('--vdagent-no-launch', default=True, action='store_false', dest='vdagent_launch', help='do not launch vdagent & vdagentd, used for debugging or if some external script wants to take care of that') parser.add_argument('--vdagent-uid', default=str(os.getuid()), help='set vdagent user id. changing it makes sense only in conjunction with --vdagent-no-launch') parser.add_argument('--vdagent-gid', default=str(os.getgid()), help='set vdagent group id. changing it makes sense only in conjunction with --vdagent-no-launch') parser.add_argument('--audio-fifo-dir', help="if a directory is given, any file in that directory will be read for audio data to be sent to the client. This is designed to work with PulseAudio's module-pipe-sink") #TODO #Option "SpiceAddr" "" #add_boolean('--agent-mouse') #Option "EnableImageCache" "True" #Option "EnableFallbackCache" "True" #Option "EnableSurfaces" "True" #parser.add_argument('--playback-compression', choices=['0', '1'], help='enabled by default') #Option "SpiceDisableCopyPaste" "False" if cgdb: parser.add_argument('--cgdb', action='store_true', default=False) args, xorg_args = parser.parse_known_args(sys.argv[1:]) def agents_new_enough(args): for f in [args.vdagent_exec, args.vdagentd_exec]: if not f: print('please specify path to vdagent/vdagentd executables') return False if not os.path.exists(f): print('error: file not found ', f) return False for f in [args.vdagent_exec, args.vdagentd_exec]: if Popen(args=[f, '-h'], stdout=PIPE, universal_newlines=True).stdout.read().find('-S') == -1: return False return True if args.vdagent_enabled: if not args.vdagent_exec: args.vdagent_exec = 'spice-vdagent' if not args.vdagentd_exec: args.vdagentd_exec = 'spice-vdagentd' args.vdagent_exec = which(args.vdagent_exec) args.vdagentd_exec = which(args.vdagentd_exec) if not agents_new_enough(args): if args.vdagent_enabled: print("error: vdagent is not new enough to support Xspice") raise SystemExit args.vdagent_enabled = False def tls_files(args): if args.tls_port == 0: return {} files = {} for k, var in [('ca-cert', 'cacert_file'), ('server-key', 'x509_key_file'), ('server-cert', 'x509_cert_file')]: files[k] = os.path.join(args.x509_dir, k + '.pem') if getattr(args, var): files[k] = getattr(args, var) return files # XXX spice-server aborts if it can't find the certificates - avoid by checking # ourselves. This isn't exhaustive - if the server key requires a password # and it isn't supplied spice will still abort, and Xorg with it. for key, filename in tls_files(args).items(): if not os.path.exists(filename): print("missing %s - %s does not exist" % (key, filename)) sys.exit(1) def error(msg, exit_code=1): print("Xspice: %s" % msg) sys.exit(exit_code) if not args.xorg: error("Xorg missing") cleanup_files = [] cleanup_dirs = [] cleanup_processes = [] def cleanup(*args): for f in cleanup_files: if os.path.exists(f): os.remove(f) for d in cleanup_dirs: if os.path.exists(d): os.rmdir(d) for p in cleanup_processes: try: p.kill() except OSError: pass for p in cleanup_processes: try: p.wait() except OSError: pass del cleanup_processes[:] def launch(*args, **kw): p = Popen(*args, **kw) cleanup_processes.append(p) return p signal.signal(signal.SIGTERM, cleanup) atexit.register(cleanup) if args.auto: temp_dir = tempfile.mkdtemp(prefix="Xspice-") cleanup_dirs.append(temp_dir) args.config = temp_dir + "/xorg.conf" cleanup_files.append(args.config) cf = open(args.config, "w+") logfile = temp_dir + "/xorg.log" cleanup_files.append(logfile) xorg_args = [ '-logfile', logfile ] + xorg_args if args.audio_fifo_dir: options = 'Option "SpicePlaybackFIFODir" "%s"' % args.audio_fifo_dir else: options = '' cf.write(""" Section "Device" Identifier "XSPICE" Driver "spiceqxl" %(options)s EndSection Section "InputDevice" Identifier "XSPICE POINTER" Driver "xspice pointer" EndSection Section "InputDevice" Identifier "XSPICE KEYBOARD" Driver "xspice keyboard" EndSection Section "Monitor" Identifier "Configured Monitor" EndSection Section "Screen" Identifier "XSPICE Screen" Monitor "Configured Monitor" Device "XSPICE" EndSection Section "ServerLayout" Identifier "XSPICE Example" Screen "XSPICE Screen" InputDevice "XSPICE KEYBOARD" InputDevice "XSPICE POINTER" EndSection # Prevent udev from loading vmmouse in a vm and crashing. Section "ServerFlags" Option "AutoAddDevices" "False" EndSection """ % locals()) cf.flush() if args.vdagent_enabled: for f in [args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path]: if f and os.path.exists(f): os.unlink(f) if not temp_dir: temp_dir = tempfile.mkdtemp(prefix="Xspice-") cleanup_dirs.append(temp_dir) # Auto generate temporary files for vdagent if not args.vdagent_udcs_path: args.vdagent_udcs_path = temp_dir + "/vdagent.udcs" if not args.vdagent_virtio_path: args.vdagent_virtio_path = temp_dir + "/vdagent.virtio" if not args.vdagent_uinput_path: args.vdagent_uinput_path = temp_dir + "/vdagent.uinput" cleanup_files.extend([args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path]) var_args = ['port', 'tls_port', 'disable_ticketing', 'x509_dir', 'sasl', 'cacert_file', 'x509_cert_file', 'x509_key_file', 'x509_key_password', 'tls_ciphers', 'dh_file', 'password', 'image_compression', 'jpeg_wan_compression', 'zlib_glz_wan_compression', 'streaming_video', 'video_codecs', 'deferred_fps', 'exit_on_disconnect', 'vdagent_enabled', 'vdagent_virtio_path', 'vdagent_uinput_path', 'vdagent_uid', 'vdagent_gid'] for arg in var_args: if getattr(args, arg) != None: # The Qxl code doesn't respect booleans, so pass them as 0/1 a = getattr(args, arg) if a == True: a = "1" elif a == False: a = "0" else: a = str(a) os.environ['XSPICE_' + arg.upper()] = a # A few arguments don't follow the XSPICE_ convention - handle them manually if args.numheads: os.environ['QXL_NUM_HEADS'] = str(args.numheads) display="" for arg in xorg_args: if arg.startswith(":"): display = arg if not display: print("Error: missing display on line (i.e. :3)") raise SystemExit os.environ ['DISPLAY'] = display exec_args = [args.xorg, '-config', args.config] if cgdb and args.cgdb: exec_args = [cgdb, '--args'] + exec_args args.xorg = cgdb # This is currently mandatory; the driver cannot survive a reset xorg_args = [ '-noreset' ] + xorg_args if args.vdagent_enabled: for f in [args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path]: if os.path.exists(f): os.unlink(f) cleanup_files.extend([args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path]) xorg = launch(executable=args.xorg, args=exec_args + xorg_args) time.sleep(2) retpid,rc = os.waitpid(xorg.pid, os.WNOHANG) if retpid != 0: print("Error: X server is not running") else: if args.vdagent_enabled and args.vdagent_launch: # XXX use systemd --user for this? vdagentd = launch(args=[args.vdagentd_exec, '-f', '-x', '-S', args.vdagent_udcs_path, '-s', args.vdagent_virtio_path, '-u', args.vdagent_uinput_path]) time.sleep(1) # TODO wait for uinput pipe open for write vdagent = launch(args=[args.vdagent_exec, '-x', '-s', args.vdagent_virtio_path, '-S', args.vdagent_udcs_path]) if args.xsession: environ = os.environ os.spawnlpe(os.P_NOWAIT, args.xsession, environ) try: xorg.wait() except KeyboardInterrupt: # Catch Ctrl-C as that is the common way of ending this script print("Keyboard Interrupt")