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