Home | History | Annotate | Line # | Download | only in scripts
      1 #!/usr/bin/python
      2 
      3 """
      4 Xspice
      5 
      6 Xspice is a standard X server that is also a Spice server.
      7 
      8 It is implemented as a module with video, mouse and keyboard drivers.
      9 
     10 The video driver is mostly the same code as the qxl guest driver, hence
     11 Xspice is kept in the same repository. It can also be used to debug the qxl
     12 driver.
     13 
     14 Xspice (this executable) will set a bunch of environment variables that are
     15 used by spiceqxl_drv.so, and then spawn Xorg, giving it the default config file,
     16 which can be overridden as well.
     17 """
     18 
     19 import argparse
     20 import os
     21 import sys
     22 import tempfile
     23 import atexit
     24 import time
     25 import signal
     26 from subprocess import Popen, PIPE
     27 
     28 def 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 
     40 if 'XSPICE_ENABLE_GDB' in os.environ:
     41     cgdb = which('cgdb')
     42     if not cgdb:
     43         cgdb = which('gdb')
     44 else:
     45     cgdb = None
     46 
     47 def add_boolean(flag, *args, **kw):
     48     parser.add_argument(flag, action='store_const', const='1',
     49                         *args, **kw)
     50 
     51 wan_compression_options = ['auto', 'never', 'always']
     52 parser = 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
     58 parser.add_argument('--xorg', default=which('Xorg'), help='specify the path to the Xorg binary')
     59 parser.add_argument('--config', default='spiceqxl.xorg.conf', help='specify the path to the Xspice configuration')
     60 parser.add_argument('--auto', action='store_true', help='automatically create a temporary xorg.conf and start the X server')
     61 parser.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
     64 add_boolean('--disable-ticketing', help="do not require a client password")
     65 parser.add_argument('--password', help="set the password required to connect to the server")
     66 add_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.
     69 parser.add_argument('--port', type=int, help="use the specified port as Spice's regular unencrypted port")
     70 parser.add_argument('--tls-port', type=int, help='use the specified port as a TLS (encrypted) port', default=0)
     71 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 parser.add_argument('--cacert-file', help="set the CA certificate file location for TLS")
     73 parser.add_argument('--x509-key-file', help="set the server key file location for TLS")
     74 parser.add_argument('--x509-key-password', help="set the server key's password for TLS")
     75 parser.add_argument('--x509-cert-file', help="set the server certificate file location for TLS")
     76 parser.add_argument('--dh-file', help="set the server DH file location for TLS")
     77 parser.add_argument('--tls-ciphers', help="set the TLS ciphers preference order")
     78 add_boolean('--ipv4-only', help="only accept IP v4 connections")
     79 add_boolean('--ipv6-only', help="only accept IP v6 connections")
     80 parser.add_argument('--exit-on-disconnect', action='store_true', help='exit the X server when any client disconnects')
     81 
     82 # Monitor configuration options
     83 parser.add_argument('--numheads', type=int, help='number of virtual heads to create')
     84 
     85 # Compression options
     86 parser.add_argument('--jpeg-wan-compression',
     87                     choices=wan_compression_options,
     88                     help="set jpeg wan compression")
     89 parser.add_argument('--zlib-glz-wan-compression',
     90                     choices=wan_compression_options,
     91                     help="set zlib glz wan compressions")
     92 parser.add_argument('--image-compression',
     93                     choices = ['off', 'auto_glz', 'auto_lz', 'quic',
     94                                'glz', 'lz'],
     95                     help="set image compression")
     96 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 # TODO - sound support
     98 parser.add_argument('--streaming-video', choices=['off', 'all', 'filter'],
     99                     help='set the streaming video method')
    100 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 
    102 # VDAgent options
    103 parser.add_argument('--vdagent', action='store_true', dest='vdagent_enabled', default=False, help='launch vdagent & vdagentd. They provide clipboard & resolution automation')
    104 parser.add_argument('--vdagent-virtio-path', help='virtio socket path used by vdagentd')
    105 parser.add_argument('--vdagent-uinput-path', help='uinput socket path used by vdagent')
    106 parser.add_argument('--vdagent-udcs-path', help='Unix domain socket path used by vdagent and vdagentd')
    107 parser.add_argument('--vdagentd-exec', help='path to spice-vdagentd (used with --vdagent)')
    108 parser.add_argument('--vdagent-exec', help='path to spice-vdagent (used with --vdagent)')
    109 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 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 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 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 
    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 
    123 if cgdb:
    124     parser.add_argument('--cgdb', action='store_true', default=False)
    125 
    126 args, xorg_args = parser.parse_known_args(sys.argv[1:])
    127 
    128 def 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 
    142 if 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 
    155 def 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.
    170 for 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 
    175 def error(msg, exit_code=1):
    176     print("Xspice: %s" % msg)
    177     sys.exit(exit_code)
    178 
    179 if not args.xorg:
    180     error("Xorg missing")
    181 
    182 cleanup_files = []
    183 cleanup_dirs = []
    184 cleanup_processes = []
    185 
    186 def 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 
    205 def launch(*args, **kw):
    206     p = Popen(*args, **kw)
    207     cleanup_processes.append(p)
    208     return p
    209 
    210 signal.signal(signal.SIGTERM, cleanup)
    211 atexit.register(cleanup)
    212 
    213 if 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("""
    230 Section "Device"
    231     Identifier "XSPICE"
    232     Driver "spiceqxl"
    233     %(options)s
    234 EndSection
    235 
    236 Section "InputDevice"
    237     Identifier "XSPICE POINTER"
    238     Driver     "xspice pointer"
    239 EndSection
    240 
    241 Section "InputDevice"
    242     Identifier "XSPICE KEYBOARD"
    243     Driver     "xspice keyboard"
    244 EndSection
    245 
    246 Section "Monitor"
    247     Identifier    "Configured Monitor"
    248 EndSection
    249 
    250 Section "Screen"
    251     Identifier     "XSPICE Screen"
    252     Monitor        "Configured Monitor"
    253     Device         "XSPICE"
    254 EndSection
    255 
    256 Section "ServerLayout"
    257     Identifier "XSPICE Example"
    258     Screen "XSPICE Screen"
    259     InputDevice "XSPICE KEYBOARD"
    260     InputDevice "XSPICE POINTER"
    261 EndSection
    262 
    263 # Prevent udev from loading vmmouse in a vm and crashing.
    264 Section "ServerFlags"
    265     Option "AutoAddDevices" "False"
    266 EndSection
    267 
    268 
    269     """ % locals())
    270     cf.flush()
    271 
    272 if 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 
    291 var_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 
    300 for 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
    313 if args.numheads:
    314     os.environ['QXL_NUM_HEADS'] = str(args.numheads)
    315 
    316 
    317 display=""
    318 for arg in xorg_args:
    319     if arg.startswith(":"):
    320         display = arg
    321 if not display:
    322     print("Error: missing display on line (i.e. :3)")
    323     raise SystemExit
    324 os.environ ['DISPLAY'] = display
    325 
    326 exec_args = [args.xorg, '-config', args.config]
    327 if 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
    332 xorg_args = [ '-noreset' ] + xorg_args
    333 
    334 
    335 if 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 
    341 xorg = launch(executable=args.xorg, args=exec_args + xorg_args)
    342 time.sleep(2)
    343 
    344 retpid,rc = os.waitpid(xorg.pid, os.WNOHANG)
    345 if retpid != 0:
    346     print("Error: X server is not running")
    347 else:
    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