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