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