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