1d514b0f3Smrg#!/usr/bin/python 2d514b0f3Smrg 3d514b0f3Smrg""" 4d514b0f3SmrgXspice 5d514b0f3Smrg 6d514b0f3SmrgXspice is a standard X server that is also a Spice server. 7d514b0f3Smrg 8d514b0f3SmrgIt is implemented as a module with video, mouse and keyboard drivers. 9d514b0f3Smrg 10d514b0f3SmrgThe video driver is mostly the same code as the qxl guest driver, hence 11d514b0f3SmrgXspice is kept in the same repository. It can also be used to debug the qxl 12d514b0f3Smrgdriver. 13d514b0f3Smrg 14d514b0f3SmrgXspice (this executable) will set a bunch of environment variables that are 15d514b0f3Smrgused by spiceqxl_drv.so, and then spawn Xorg, giving it the default config file, 16d514b0f3Smrgwhich can be overridden as well. 17d514b0f3Smrg""" 18d514b0f3Smrg 19d514b0f3Smrgimport argparse 20d514b0f3Smrgimport os 21d514b0f3Smrgimport sys 22d514b0f3Smrgimport tempfile 23d514b0f3Smrgimport atexit 24d514b0f3Smrgimport time 25d514b0f3Smrgimport signal 26d514b0f3Smrgfrom subprocess import Popen, PIPE 27d514b0f3Smrg 28d514b0f3Smrgdef which(x): 29d514b0f3Smrg if not x: 30d514b0f3Smrg return x 31d514b0f3Smrg if os.path.exists(x): 32d514b0f3Smrg return x 33d514b0f3Smrg for p in os.environ['PATH'].split(':'): 34d514b0f3Smrg candidate = os.path.join(p, x) 35d514b0f3Smrg if os.path.exists(candidate): 36d514b0f3Smrg return candidate 37d514b0f3Smrg print('Warning: failed to find executable %s' % x) 38d514b0f3Smrg return None 39d514b0f3Smrg 40d514b0f3Smrgif 'XSPICE_ENABLE_GDB' in os.environ: 41d514b0f3Smrg cgdb = which('cgdb') 42d514b0f3Smrg if not cgdb: 43d514b0f3Smrg cgdb = which('gdb') 44d514b0f3Smrgelse: 45d514b0f3Smrg cgdb = None 46d514b0f3Smrg 47d514b0f3Smrgdef add_boolean(flag, *args, **kw): 48d514b0f3Smrg parser.add_argument(flag, action='store_const', const='1', 49d514b0f3Smrg *args, **kw) 50d514b0f3Smrg 51d514b0f3Smrgwan_compression_options = ['auto', 'never', 'always'] 52d514b0f3Smrgparser = argparse.ArgumentParser("Xspice", 53d514b0f3Smrg description="X and Spice server. example usage: Xspice --port 5900 --disable-ticketing :1.0", 54d514b0f3Smrg usage="Xspice [Xspice and Xorg options intermixed]", 55d514b0f3Smrg epilog="Any option not parsed by Xspice gets passed to Xorg as is.") 56d514b0f3Smrg 57d514b0f3Smrg# X-related options 58d514b0f3Smrgparser.add_argument('--xorg', default=which('Xorg'), help='specify the path to the Xorg binary') 59d514b0f3Smrgparser.add_argument('--config', default='spiceqxl.xorg.conf', help='specify the path to the Xspice configuration') 60d514b0f3Smrgparser.add_argument('--auto', action='store_true', help='automatically create a temporary xorg.conf and start the X server') 61d514b0f3Smrgparser.add_argument('--xsession', help='if given, will run after Xorg launch. Should be a program like x-session-manager') 62d514b0f3Smrg 63d514b0f3Smrg# Network and security options 64d514b0f3Smrgadd_boolean('--disable-ticketing', help="do not require a client password") 65d514b0f3Smrgparser.add_argument('--password', help="set the password required to connect to the server") 66d514b0f3Smrgadd_boolean('--sasl', help="use SASL to authenticate to the server") 67d514b0f3Smrg# Don't use any options that are already used by Xorg (unless we must) 68d514b0f3Smrg# specifically, don't use -p and -s. 69d514b0f3Smrgparser.add_argument('--port', type=int, help="use the specified port as Spice's regular unencrypted port") 70d514b0f3Smrgparser.add_argument('--tls-port', type=int, help='use the specified port as a TLS (encrypted) port', default=0) 71d514b0f3Smrgparser.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") 72d514b0f3Smrgparser.add_argument('--cacert-file', help="set the CA certificate file location for TLS") 73d514b0f3Smrgparser.add_argument('--x509-key-file', help="set the server key file location for TLS") 74d514b0f3Smrgparser.add_argument('--x509-key-password', help="set the server key's password for TLS") 75d514b0f3Smrgparser.add_argument('--x509-cert-file', help="set the server certificate file location for TLS") 76d514b0f3Smrgparser.add_argument('--dh-file', help="set the server DH file location for TLS") 77d514b0f3Smrgparser.add_argument('--tls-ciphers', help="set the TLS ciphers preference order") 78d514b0f3Smrgadd_boolean('--ipv4-only', help="only accept IP v4 connections") 79d514b0f3Smrgadd_boolean('--ipv6-only', help="only accept IP v6 connections") 80d514b0f3Smrgparser.add_argument('--exit-on-disconnect', action='store_true', help='exit the X server when any client disconnects') 81d514b0f3Smrg 82d514b0f3Smrg# Monitor configuration options 83d514b0f3Smrgparser.add_argument('--numheads', type=int, help='number of virtual heads to create') 84d514b0f3Smrg 85d514b0f3Smrg# Compression options 86d514b0f3Smrgparser.add_argument('--jpeg-wan-compression', 87d514b0f3Smrg choices=wan_compression_options, 88d514b0f3Smrg help="set jpeg wan compression") 89d514b0f3Smrgparser.add_argument('--zlib-glz-wan-compression', 90d514b0f3Smrg choices=wan_compression_options, 91d514b0f3Smrg help="set zlib glz wan compressions") 92d514b0f3Smrgparser.add_argument('--image-compression', 93d514b0f3Smrg choices = ['off', 'auto_glz', 'auto_lz', 'quic', 94d514b0f3Smrg 'glz', 'lz'], 95d514b0f3Smrg help="set image compression") 96d514b0f3Smrgparser.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') 97d514b0f3Smrg# TODO - sound support 98d514b0f3Smrgparser.add_argument('--streaming-video', choices=['off', 'all', 'filter'], 99d514b0f3Smrg help='set the streaming video method') 100d514b0f3Smrgparser.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') 101d514b0f3Smrg 102d514b0f3Smrg# VDAgent options 103d514b0f3Smrgparser.add_argument('--vdagent', action='store_true', dest='vdagent_enabled', default=False, help='launch vdagent & vdagentd. They provide clipboard & resolution automation') 104d514b0f3Smrgparser.add_argument('--vdagent-virtio-path', help='virtio socket path used by vdagentd') 105d514b0f3Smrgparser.add_argument('--vdagent-uinput-path', help='uinput socket path used by vdagent') 106d514b0f3Smrgparser.add_argument('--vdagent-udcs-path', help='Unix domain socket path used by vdagent and vdagentd') 107d514b0f3Smrgparser.add_argument('--vdagentd-exec', help='path to spice-vdagentd (used with --vdagent)') 108d514b0f3Smrgparser.add_argument('--vdagent-exec', help='path to spice-vdagent (used with --vdagent)') 109d514b0f3Smrgparser.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') 110d514b0f3Smrgparser.add_argument('--vdagent-uid', default=str(os.getuid()), help='set vdagent user id. changing it makes sense only in conjunction with --vdagent-no-launch') 111d514b0f3Smrgparser.add_argument('--vdagent-gid', default=str(os.getgid()), help='set vdagent group id. changing it makes sense only in conjunction with --vdagent-no-launch') 112d514b0f3Smrgparser.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") 113d514b0f3Smrg 114d514b0f3Smrg#TODO 115d514b0f3Smrg#Option "SpiceAddr" "" 116d514b0f3Smrg#add_boolean('--agent-mouse') 117d514b0f3Smrg#Option "EnableImageCache" "True" 118d514b0f3Smrg#Option "EnableFallbackCache" "True" 119d514b0f3Smrg#Option "EnableSurfaces" "True" 120d514b0f3Smrg#parser.add_argument('--playback-compression', choices=['0', '1'], help='enabled by default') 121d514b0f3Smrg#Option "SpiceDisableCopyPaste" "False" 122d514b0f3Smrg 123d514b0f3Smrgif cgdb: 124d514b0f3Smrg parser.add_argument('--cgdb', action='store_true', default=False) 125d514b0f3Smrg 126d514b0f3Smrgargs, xorg_args = parser.parse_known_args(sys.argv[1:]) 127d514b0f3Smrg 128d514b0f3Smrgdef agents_new_enough(args): 129d514b0f3Smrg for f in [args.vdagent_exec, args.vdagentd_exec]: 130d514b0f3Smrg if not f: 131d514b0f3Smrg print('please specify path to vdagent/vdagentd executables') 132d514b0f3Smrg return False 133d514b0f3Smrg if not os.path.exists(f): 134d514b0f3Smrg print('error: file not found ', f) 135d514b0f3Smrg return False 136d514b0f3Smrg 137d514b0f3Smrg for f in [args.vdagent_exec, args.vdagentd_exec]: 138d514b0f3Smrg if Popen(args=[f, '-h'], stdout=PIPE, universal_newlines=True).stdout.read().find('-S') == -1: 139d514b0f3Smrg return False 140d514b0f3Smrg return True 141d514b0f3Smrg 142d514b0f3Smrgif args.vdagent_enabled: 143d514b0f3Smrg if not args.vdagent_exec: 144d514b0f3Smrg args.vdagent_exec = 'spice-vdagent' 145d514b0f3Smrg if not args.vdagentd_exec: 146d514b0f3Smrg args.vdagentd_exec = 'spice-vdagentd' 147d514b0f3Smrg args.vdagent_exec = which(args.vdagent_exec) 148d514b0f3Smrg args.vdagentd_exec = which(args.vdagentd_exec) 149d514b0f3Smrg if not agents_new_enough(args): 150d514b0f3Smrg if args.vdagent_enabled: 151d514b0f3Smrg print("error: vdagent is not new enough to support Xspice") 152d514b0f3Smrg raise SystemExit 153d514b0f3Smrg args.vdagent_enabled = False 154d514b0f3Smrg 155d514b0f3Smrgdef tls_files(args): 156d514b0f3Smrg if args.tls_port == 0: 157d514b0f3Smrg return {} 158d514b0f3Smrg files = {} 159d514b0f3Smrg for k, var in [('ca-cert', 'cacert_file'), 160d514b0f3Smrg ('server-key', 'x509_key_file'), 161d514b0f3Smrg ('server-cert', 'x509_cert_file')]: 162d514b0f3Smrg files[k] = os.path.join(args.x509_dir, k + '.pem') 163d514b0f3Smrg if getattr(args, var): 164d514b0f3Smrg files[k] = getattr(args, var) 165d514b0f3Smrg return files 166d514b0f3Smrg 167d514b0f3Smrg# XXX spice-server aborts if it can't find the certificates - avoid by checking 168d514b0f3Smrg# ourselves. This isn't exhaustive - if the server key requires a password 169d514b0f3Smrg# and it isn't supplied spice will still abort, and Xorg with it. 170d514b0f3Smrgfor key, filename in tls_files(args).items(): 171d514b0f3Smrg if not os.path.exists(filename): 172d514b0f3Smrg print("missing %s - %s does not exist" % (key, filename)) 173d514b0f3Smrg sys.exit(1) 174d514b0f3Smrg 175d514b0f3Smrgdef error(msg, exit_code=1): 176d514b0f3Smrg print("Xspice: %s" % msg) 177d514b0f3Smrg sys.exit(exit_code) 178d514b0f3Smrg 179d514b0f3Smrgif not args.xorg: 180d514b0f3Smrg error("Xorg missing") 181d514b0f3Smrg 182d514b0f3Smrgcleanup_files = [] 183d514b0f3Smrgcleanup_dirs = [] 184d514b0f3Smrgcleanup_processes = [] 185d514b0f3Smrg 186d514b0f3Smrgdef cleanup(*args): 187d514b0f3Smrg for f in cleanup_files: 188d514b0f3Smrg if os.path.exists(f): 189d514b0f3Smrg os.remove(f) 190d514b0f3Smrg for d in cleanup_dirs: 191d514b0f3Smrg if os.path.exists(d): 192d514b0f3Smrg os.rmdir(d) 193d514b0f3Smrg for p in cleanup_processes: 194d514b0f3Smrg try: 195d514b0f3Smrg p.kill() 196d514b0f3Smrg except OSError: 197d514b0f3Smrg pass 198d514b0f3Smrg for p in cleanup_processes: 199d514b0f3Smrg try: 200d514b0f3Smrg p.wait() 201d514b0f3Smrg except OSError: 202d514b0f3Smrg pass 203d514b0f3Smrg del cleanup_processes[:] 204d514b0f3Smrg 205d514b0f3Smrgdef launch(*args, **kw): 206d514b0f3Smrg p = Popen(*args, **kw) 207d514b0f3Smrg cleanup_processes.append(p) 208d514b0f3Smrg return p 209d514b0f3Smrg 210d514b0f3Smrgsignal.signal(signal.SIGTERM, cleanup) 211d514b0f3Smrgatexit.register(cleanup) 212d514b0f3Smrg 213d514b0f3Smrgif args.auto: 214d514b0f3Smrg temp_dir = tempfile.mkdtemp(prefix="Xspice-") 215d514b0f3Smrg cleanup_dirs.append(temp_dir) 216d514b0f3Smrg 217d514b0f3Smrg args.config = temp_dir + "/xorg.conf" 218d514b0f3Smrg cleanup_files.append(args.config) 219d514b0f3Smrg cf = open(args.config, "w+") 220d514b0f3Smrg 221d514b0f3Smrg logfile = temp_dir + "/xorg.log" 222d514b0f3Smrg cleanup_files.append(logfile) 223d514b0f3Smrg 224d514b0f3Smrg xorg_args = [ '-logfile', logfile ] + xorg_args 225d514b0f3Smrg if args.audio_fifo_dir: 226d514b0f3Smrg options = 'Option "SpicePlaybackFIFODir" "%s"' % args.audio_fifo_dir 227d514b0f3Smrg else: 228d514b0f3Smrg options = '' 229d514b0f3Smrg cf.write(""" 230d514b0f3SmrgSection "Device" 231d514b0f3Smrg Identifier "XSPICE" 232d514b0f3Smrg Driver "spiceqxl" 233d514b0f3Smrg %(options)s 234d514b0f3SmrgEndSection 235d514b0f3Smrg 236d514b0f3SmrgSection "InputDevice" 237d514b0f3Smrg Identifier "XSPICE POINTER" 238d514b0f3Smrg Driver "xspice pointer" 239d514b0f3SmrgEndSection 240d514b0f3Smrg 241d514b0f3SmrgSection "InputDevice" 242d514b0f3Smrg Identifier "XSPICE KEYBOARD" 243d514b0f3Smrg Driver "xspice keyboard" 244d514b0f3SmrgEndSection 245d514b0f3Smrg 246d514b0f3SmrgSection "Monitor" 247d514b0f3Smrg Identifier "Configured Monitor" 248d514b0f3SmrgEndSection 249d514b0f3Smrg 250d514b0f3SmrgSection "Screen" 251d514b0f3Smrg Identifier "XSPICE Screen" 252d514b0f3Smrg Monitor "Configured Monitor" 253d514b0f3Smrg Device "XSPICE" 254d514b0f3SmrgEndSection 255d514b0f3Smrg 256d514b0f3SmrgSection "ServerLayout" 257d514b0f3Smrg Identifier "XSPICE Example" 258d514b0f3Smrg Screen "XSPICE Screen" 259d514b0f3Smrg InputDevice "XSPICE KEYBOARD" 260d514b0f3Smrg InputDevice "XSPICE POINTER" 261d514b0f3SmrgEndSection 262d514b0f3Smrg 263d514b0f3Smrg# Prevent udev from loading vmmouse in a vm and crashing. 264d514b0f3SmrgSection "ServerFlags" 265d514b0f3Smrg Option "AutoAddDevices" "False" 266d514b0f3SmrgEndSection 267d514b0f3Smrg 268d514b0f3Smrg 269d514b0f3Smrg """ % locals()) 270d514b0f3Smrg cf.flush() 271d514b0f3Smrg 272d514b0f3Smrgif args.vdagent_enabled: 273d514b0f3Smrg for f in [args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path]: 274d514b0f3Smrg if f and os.path.exists(f): 275d514b0f3Smrg os.unlink(f) 276d514b0f3Smrg 277d514b0f3Smrg if not temp_dir: 278d514b0f3Smrg temp_dir = tempfile.mkdtemp(prefix="Xspice-") 279d514b0f3Smrg cleanup_dirs.append(temp_dir) 280d514b0f3Smrg 281d514b0f3Smrg # Auto generate temporary files for vdagent 282d514b0f3Smrg if not args.vdagent_udcs_path: 283d514b0f3Smrg args.vdagent_udcs_path = temp_dir + "/vdagent.udcs" 284d514b0f3Smrg if not args.vdagent_virtio_path: 285d514b0f3Smrg args.vdagent_virtio_path = temp_dir + "/vdagent.virtio" 286d514b0f3Smrg if not args.vdagent_uinput_path: 287d514b0f3Smrg args.vdagent_uinput_path = temp_dir + "/vdagent.uinput" 288d514b0f3Smrg 289d514b0f3Smrg cleanup_files.extend([args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path]) 290d514b0f3Smrg 291d514b0f3Smrgvar_args = ['port', 'tls_port', 'disable_ticketing', 292d514b0f3Smrg 'x509_dir', 'sasl', 'cacert_file', 'x509_cert_file', 293d514b0f3Smrg 'x509_key_file', 'x509_key_password', 294d514b0f3Smrg 'tls_ciphers', 'dh_file', 'password', 'image_compression', 295d514b0f3Smrg 'jpeg_wan_compression', 'zlib_glz_wan_compression', 296d514b0f3Smrg 'streaming_video', 'video_codecs', 'deferred_fps', 'exit_on_disconnect', 297d514b0f3Smrg 'vdagent_enabled', 'vdagent_virtio_path', 'vdagent_uinput_path', 298d514b0f3Smrg 'vdagent_uid', 'vdagent_gid'] 299d514b0f3Smrg 300d514b0f3Smrgfor arg in var_args: 301d514b0f3Smrg if getattr(args, arg) != None: 302d514b0f3Smrg # The Qxl code doesn't respect booleans, so pass them as 0/1 303d514b0f3Smrg a = getattr(args, arg) 304d514b0f3Smrg if a == True: 305d514b0f3Smrg a = "1" 306d514b0f3Smrg elif a == False: 307d514b0f3Smrg a = "0" 308d514b0f3Smrg else: 309d514b0f3Smrg a = str(a) 310d514b0f3Smrg os.environ['XSPICE_' + arg.upper()] = a 311d514b0f3Smrg 312d514b0f3Smrg# A few arguments don't follow the XSPICE_ convention - handle them manually 313d514b0f3Smrgif args.numheads: 314d514b0f3Smrg os.environ['QXL_NUM_HEADS'] = str(args.numheads) 315d514b0f3Smrg 316d514b0f3Smrg 317d514b0f3Smrgdisplay="" 318d514b0f3Smrgfor arg in xorg_args: 319d514b0f3Smrg if arg.startswith(":"): 320d514b0f3Smrg display = arg 321d514b0f3Smrgif not display: 322d514b0f3Smrg print("Error: missing display on line (i.e. :3)") 323d514b0f3Smrg raise SystemExit 324d514b0f3Smrgos.environ ['DISPLAY'] = display 325d514b0f3Smrg 326d514b0f3Smrgexec_args = [args.xorg, '-config', args.config] 327d514b0f3Smrgif cgdb and args.cgdb: 328d514b0f3Smrg exec_args = [cgdb, '--args'] + exec_args 329d514b0f3Smrg args.xorg = cgdb 330d514b0f3Smrg 331d514b0f3Smrg# This is currently mandatory; the driver cannot survive a reset 332d514b0f3Smrgxorg_args = [ '-noreset' ] + xorg_args 333d514b0f3Smrg 334d514b0f3Smrg 335d514b0f3Smrgif args.vdagent_enabled: 336d514b0f3Smrg for f in [args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path]: 337d514b0f3Smrg if os.path.exists(f): 338d514b0f3Smrg os.unlink(f) 339d514b0f3Smrg cleanup_files.extend([args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path]) 340d514b0f3Smrg 341d514b0f3Smrgxorg = launch(executable=args.xorg, args=exec_args + xorg_args) 342d514b0f3Smrgtime.sleep(2) 343d514b0f3Smrg 344d514b0f3Smrgretpid,rc = os.waitpid(xorg.pid, os.WNOHANG) 345d514b0f3Smrgif retpid != 0: 346d514b0f3Smrg print("Error: X server is not running") 347d514b0f3Smrgelse: 348d514b0f3Smrg if args.vdagent_enabled and args.vdagent_launch: 349d514b0f3Smrg # XXX use systemd --user for this? 350d514b0f3Smrg vdagentd = launch(args=[args.vdagentd_exec, '-f', '-x', '-S', args.vdagent_udcs_path, 351d514b0f3Smrg '-s', args.vdagent_virtio_path, '-u', args.vdagent_uinput_path]) 352d514b0f3Smrg time.sleep(1) 353d514b0f3Smrg # TODO wait for uinput pipe open for write 354d514b0f3Smrg vdagent = launch(args=[args.vdagent_exec, '-x', '-s', args.vdagent_virtio_path, '-S', 355d514b0f3Smrg args.vdagent_udcs_path]) 356d514b0f3Smrg if args.xsession: 357d514b0f3Smrg environ = os.environ 358d514b0f3Smrg os.spawnlpe(os.P_NOWAIT, args.xsession, environ) 359d514b0f3Smrg 360d514b0f3Smrg try: 361d514b0f3Smrg xorg.wait() 362d514b0f3Smrg except KeyboardInterrupt: 363d514b0f3Smrg # Catch Ctrl-C as that is the common way of ending this script 364d514b0f3Smrg print("Keyboard Interrupt") 365