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