Home | History | Annotate | Line # | Download | only in scripts
Xspice revision 1.1
      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