Xspice 13.7 KB
Newer Older
1 2 3
#!/usr/bin/python

"""
Alon Levy's avatar
Alon Levy committed
4
Xspice
5

Alon Levy's avatar
Alon Levy committed
6
Xspice is a standard X server that is also a Spice server.
7 8 9 10

It is implemented as a module with video, mouse and keyboard drivers.

The video driver is mostly the same code as the qxl guest driver, hence
Alon Levy's avatar
Alon Levy committed
11
Xspice is kept in the same repository. It can also be used to debug the qxl
12 13
driver.

Alon Levy's avatar
Alon Levy committed
14
Xspice (this executable) will set a bunch of environment variables that are
Alon Levy's avatar
Alon Levy committed
15
used by spiceqxl_drv.so, and then spawn Xorg, giving it the default config file,
16 17 18 19 20 21
which can be overridden as well.
"""

import argparse
import os
import sys
22 23
import tempfile
import atexit
24
import time
25
import signal
26
from subprocess import Popen, PIPE
27 28

def which(x):
29 30
    if not x:
        return x
31 32
    if os.path.exists(x):
        return x
33 34 35 36
    for p in os.environ['PATH'].split(':'):
        candidate = os.path.join(p, x)
        if os.path.exists(candidate):
            return candidate
37
    print('Warning: failed to find executable %s' % x)
38 39
    return None

40 41 42 43 44 45
if 'XSPICE_ENABLE_GDB' in os.environ:
    cgdb = which('cgdb')
    if not cgdb:
        cgdb = which('gdb')
else:
    cgdb = None
46 47

def add_boolean(flag, *args, **kw):
48
    parser.add_argument(flag, action='store_const', const='1',
49 50 51
                        *args, **kw)

wan_compression_options = ['auto', 'never', 'always']
Alon Levy's avatar
Alon Levy committed
52 53 54
parser = argparse.ArgumentParser("Xspice",
    description="X and Spice server. example usage: Xspice --port 5900 --disable-ticketing :1.0",
    usage="Xspice [Xspice and Xorg options intermixed]",
55 56 57 58 59 60 61 62 63 64 65 66
    epilog="Any option not parsed by Xspice gets passed to Xorg as is.")

# X-related options
parser.add_argument('--xorg', default=which('Xorg'), help='specify the path to the Xorg binary')
parser.add_argument('--config', default='spiceqxl.xorg.conf', help='specify the path to the Xspice configuration')
parser.add_argument('--auto', action='store_true', help='automatically create a temporary xorg.conf and start the X server')
parser.add_argument('--xsession', help='if given, will run after Xorg launch.  Should be a program like x-session-manager')

# Network and security options
add_boolean('--disable-ticketing', help="do not require a client password")
parser.add_argument('--password', help="set the password required to connect to the server")
add_boolean('--sasl', help="use SASL to authenticate to the server")
67 68
# Don't use any options that are already used by Xorg (unless we must)
# specifically, don't use -p and -s.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
parser.add_argument('--port', type=int, help="use the specified port as Spice's regular unencrypted port")
parser.add_argument('--tls-port', type=int, help='use the specified port as a TLS (encrypted) port', default=0)
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")
parser.add_argument('--cacert-file', help="set the CA certificate file location for TLS")
parser.add_argument('--x509-key-file', help="set the server key file location for TLS")
parser.add_argument('--x509-key-password', help="set the server key's password for TLS")
parser.add_argument('--x509-cert-file', help="set the server certificate file location for TLS")
parser.add_argument('--dh-file', help="set the server DH file location for TLS")
parser.add_argument('--tls-ciphers', help="set the TLS ciphers preference order")
add_boolean('--ipv4-only', help="only accept IP v4 connections")
add_boolean('--ipv6-only', help="only accept IP v6 connections")
parser.add_argument('--exit-on-disconnect', action='store_true', help='exit the X server when any client disconnects')

# Monitor configuration options
parser.add_argument('--numheads', type=int, help='number of virtual heads to create')

# Compression options
86 87
parser.add_argument('--jpeg-wan-compression',
                    choices=wan_compression_options,
88
                    help="set jpeg wan compression")
89 90
parser.add_argument('--zlib-glz-wan-compression',
                    choices=wan_compression_options,
91 92 93 94 95 96
                    help="set zlib glz wan compressions")
parser.add_argument('--image-compression',
                    choices = ['off', 'auto_glz', 'auto_lz', 'quic',
                               'glz', 'lz'],
                    help="set image compression")
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 98
# TODO - sound support
parser.add_argument('--streaming-video', choices=['off', 'all', 'filter'],
99 100 101 102
                    help='set the streaming video method')
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')

# 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 105
parser.add_argument('--vdagent-virtio-path', help='virtio socket path used by vdagentd')
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 108 109 110 111
parser.add_argument('--vdagentd-exec', help='path to spice-vdagentd (used with --vdagent)')
parser.add_argument('--vdagent-exec', help='path to spice-vdagent (used with --vdagent)')
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')
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')
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 115 116 117 118 119
#TODO
#Option "SpiceAddr" ""
#add_boolean('--agent-mouse')
#Option "EnableImageCache" "True"
#Option "EnableFallbackCache" "True"
#Option "EnableSurfaces" "True"
120
#parser.add_argument('--playback-compression', choices=['0', '1'], help='enabled by default')
121 122
#Option "SpiceDisableCopyPaste" "False"

123 124 125 126 127
if cgdb:
    parser.add_argument('--cgdb', action='store_true', default=False)

args, xorg_args = parser.parse_known_args(sys.argv[1:])

128
def agents_new_enough(args):
129 130
    for f in [args.vdagent_exec, args.vdagentd_exec]:
        if not f:
131
            print('please specify path to vdagent/vdagentd executables')
132 133
            return False
        if not os.path.exists(f):
134
            print('error: file not found ', f)
135 136
            return False

137 138 139 140 141 142
    for f in [args.vdagent_exec, args.vdagentd_exec]:
        if Popen(args=[f, '-h'], stdout=PIPE).stdout.read().find('-S') == -1:
            return False
    return True

if args.vdagent_enabled:
143 144 145 146
    if not args.vdagent_exec:
        args.vdagent_exec = 'spice-vdagent'
    if not args.vdagentd_exec:
        args.vdagentd_exec = 'spice-vdagentd'
147 148 149 150
    args.vdagent_exec = which(args.vdagent_exec)
    args.vdagentd_exec = which(args.vdagentd_exec)
    if not agents_new_enough(args):
        if args.vdagent_enabled:
151
            print("error: vdagent is not new enough to support Xspice")
152 153 154
            raise SystemExit
        args.vdagent_enabled = False

155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
def tls_files(args):
    if args.tls_port == 0:
        return {}
    files = {}
    for k, var in [('ca-cert', 'cacert_file'),
                   ('server-key', 'x509_key_file'),
                   ('server-cert', 'x509_cert_file')]:
        files[k] = os.path.join(args.x509_dir, k + '.pem')
        if getattr(args, var):
            files[k] = getattr(args, var)
    return files

# XXX spice-server aborts if it can't find the certificates - avoid by checking
# ourselves. This isn't exhaustive - if the server key requires a password
# and it isn't supplied spice will still abort, and Xorg with it.
for key, filename in tls_files(args).items():
    if not os.path.exists(filename):
172
        print("missing %s - %s does not exist" % (key, filename))
173 174
        sys.exit(1)

175
def error(msg, exit_code=1):
176
    print("Xspice: %s" % msg)
177 178 179 180 181
    sys.exit(exit_code)

if not args.xorg:
    error("Xorg missing")

182
cleanup_files = []
183
cleanup_dirs = []
184 185
cleanup_processes = []

186
def cleanup(*args):
187
    for f in cleanup_files:
188
        if os.path.exists(f):
189
            os.remove(f)
190 191 192
    for d in cleanup_dirs:
        if os.path.exists(d):
            os.rmdir(d)
193
    for p in cleanup_processes:
194 195 196 197
        try:
            p.kill()
        except OSError:
            pass
198
    for p in cleanup_processes:
199 200 201 202
        try:
            p.wait()
        except OSError:
            pass
203
    del cleanup_processes[:]
204 205 206 207 208 209

def launch(*args, **kw):
    p = Popen(*args, **kw)
    cleanup_processes.append(p)
    return p

210
signal.signal(signal.SIGTERM, cleanup)
211
atexit.register(cleanup)
212 213

if args.auto:
214 215 216 217 218 219 220 221 222 223 224
    temp_dir  = tempfile.mkdtemp(prefix="Xspice-")
    cleanup_dirs.append(temp_dir)

    args.config = temp_dir + "/xorg.conf"
    cleanup_files.append(args.config)
    cf = open(args.config, "w+")

    logfile = temp_dir + "/xorg.log"
    cleanup_files.append(logfile)

    xorg_args = [ '-logfile', logfile ] + xorg_args
Alon Levy's avatar
Alon Levy committed
225 226 227 228
    if args.audio_fifo_dir:
        options = 'Option "SpicePlaybackFIFODir"  "%s"' % args.audio_fifo_dir
    else:
        options = ''
229 230 231 232
    cf.write("""
Section "Device"
    Identifier "XSPICE"
    Driver "spiceqxl"
Alon Levy's avatar
Alon Levy committed
233
    %(options)s
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
EndSection

Section "InputDevice"
    Identifier "XSPICE POINTER"
    Driver     "xspice pointer"
EndSection

Section "InputDevice"
    Identifier "XSPICE KEYBOARD"
    Driver     "xspice keyboard"
EndSection

Section "Monitor"
    Identifier    "Configured Monitor"
EndSection

Section "Screen"
    Identifier     "XSPICE Screen"
    Monitor        "Configured Monitor"
    Device         "XSPICE"
EndSection

Section "ServerLayout"
    Identifier "XSPICE Example"
    Screen "XSPICE Screen"
    InputDevice "XSPICE KEYBOARD"
    InputDevice "XSPICE POINTER"
EndSection

# Prevent udev from loading vmmouse in a vm and crashing.
Section "ServerFlags"
    Option "AutoAddDevices" "False"
EndSection


Alon Levy's avatar
Alon Levy committed
269
    """ % locals())
270 271
    cf.flush()

272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
if args.vdagent_enabled:
    for f in [args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path]:
        if f and os.path.exists(f):
            os.unlink(f)

    if not temp_dir:
        temp_dir  = tempfile.mkdtemp(prefix="Xspice-")
        cleanup_dirs.append(temp_dir)

    # Auto generate temporary files for vdagent
    if not args.vdagent_udcs_path:
        args.vdagent_udcs_path = temp_dir + "/vdagent.udcs"
    if not args.vdagent_virtio_path:
        args.vdagent_virtio_path = temp_dir + "/vdagent.virtio"
    if not args.vdagent_uinput_path:
        args.vdagent_uinput_path = temp_dir + "/vdagent.uinput"

    cleanup_files.extend([args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path])

291
var_args = ['port', 'tls_port', 'disable_ticketing',
292 293
    'x509_dir', 'sasl', 'cacert_file', 'x509_cert_file',
    'x509_key_file', 'x509_key_password',
294 295
    'tls_ciphers', 'dh_file', 'password', 'image_compression',
    'jpeg_wan_compression', 'zlib_glz_wan_compression',
296
    'streaming_video', 'video_codecs', 'deferred_fps', 'exit_on_disconnect',
297 298
    'vdagent_enabled', 'vdagent_virtio_path', 'vdagent_uinput_path',
    'vdagent_uid', 'vdagent_gid']
299

300
for arg in var_args:
301
    if getattr(args, arg) != None:
302 303 304 305 306 307 308 309 310
        # The Qxl code doesn't respect booleans, so pass them as 0/1
        a = getattr(args, arg)
        if a == True:
            a = "1"
        elif a == False:
            a = "0"
        else:
            a = str(a)
        os.environ['XSPICE_' + arg.upper()] = a
311

312 313 314 315 316
# A few arguments don't follow the XSPICE_ convention - handle them manually
if args.numheads:
    os.environ['QXL_NUM_HEADS'] = str(args.numheads)


317 318 319 320
display=""
for arg in xorg_args:
    if arg.startswith(":"):
        display = arg
321
if not display:
322
    print("Error: missing display on line (i.e. :3)")
323 324
    raise SystemExit
os.environ ['DISPLAY'] = display
325

326 327 328 329 330
exec_args = [args.xorg, '-config', args.config]
if cgdb and args.cgdb:
    exec_args = [cgdb, '--args'] + exec_args
    args.xorg = cgdb

331 332 333
# This is currently mandatory; the driver cannot survive a reset
xorg_args = [ '-noreset' ] + xorg_args

334 335

if args.vdagent_enabled:
336
    for f in [args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path]:
337 338
        if os.path.exists(f):
            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 343
time.sleep(2)

344
retpid,rc = os.waitpid(xorg.pid, os.WNOHANG)
345
if retpid != 0:
346
    print("Error: X server is not running")
347
else:
348 349
    if args.vdagent_enabled and args.vdagent_launch:
        # XXX use systemd --user for this?
350
        vdagentd = launch(args=[args.vdagentd_exec, '-f', '-x', '-S', args.vdagent_udcs_path,
351 352 353 354
                          '-s', args.vdagent_virtio_path, '-u', args.vdagent_uinput_path])
        time.sleep(1)
        # TODO wait for uinput pipe open for write
        vdagent = launch(args=[args.vdagent_exec, '-x', '-s', args.vdagent_virtio_path, '-S',
355
                         args.vdagent_udcs_path])
356 357 358 359 360
    if args.xsession:
        environ = os.environ
        os.spawnlpe(os.P_NOWAIT, args.xsession, environ)

    try:
361
        xorg.wait()
362 363
    except KeyboardInterrupt:
        # Catch Ctrl-C as that is the common way of ending this script
364
        print("Keyboard Interrupt")