Xspice revision 1.1 1 #!/usr/bin/python
2
3 """
4 Xspice
5
6 Xspice is a standard X server that is also a Spice server.
7
8 It is implemented as a module with video, mouse and keyboard drivers.
9
10 The video driver is mostly the same code as the qxl guest driver, hence
11 Xspice is kept in the same repository. It can also be used to debug the qxl
12 driver.
13
14 Xspice (this executable) will set a bunch of environment variables that are
15 used by spiceqxl_drv.so, and then spawn Xorg, giving it the default config file,
16 which can be overridden as well.
17 """
18
19 import argparse
20 import os
21 import sys
22 import tempfile
23 import atexit
24 import time
25 import signal
26 from subprocess import Popen, PIPE
27
28 def 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
40 if 'XSPICE_ENABLE_GDB' in os.environ:
41 cgdb = which('cgdb')
42 if not cgdb:
43 cgdb = which('gdb')
44 else:
45 cgdb = None
46
47 def add_boolean(flag, *args, **kw):
48 parser.add_argument(flag, action='store_const', const='1',
49 *args, **kw)
50
51 wan_compression_options = ['auto', 'never', 'always']
52 parser = 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
58 parser.add_argument('--xorg', default=which('Xorg'), help='specify the path to the Xorg binary')
59 parser.add_argument('--config', default='spiceqxl.xorg.conf', help='specify the path to the Xspice configuration')
60 parser.add_argument('--auto', action='store_true', help='automatically create a temporary xorg.conf and start the X server')
61 parser.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
64 add_boolean('--disable-ticketing', help="do not require a client password")
65 parser.add_argument('--password', help="set the password required to connect to the server")
66 add_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.
69 parser.add_argument('--port', type=int, help="use the specified port as Spice's regular unencrypted port")
70 parser.add_argument('--tls-port', type=int, help='use the specified port as a TLS (encrypted) port', default=0)
71 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 parser.add_argument('--cacert-file', help="set the CA certificate file location for TLS")
73 parser.add_argument('--x509-key-file', help="set the server key file location for TLS")
74 parser.add_argument('--x509-key-password', help="set the server key's password for TLS")
75 parser.add_argument('--x509-cert-file', help="set the server certificate file location for TLS")
76 parser.add_argument('--dh-file', help="set the server DH file location for TLS")
77 parser.add_argument('--tls-ciphers', help="set the TLS ciphers preference order")
78 add_boolean('--ipv4-only', help="only accept IP v4 connections")
79 add_boolean('--ipv6-only', help="only accept IP v6 connections")
80 parser.add_argument('--exit-on-disconnect', action='store_true', help='exit the X server when any client disconnects')
81
82 # Monitor configuration options
83 parser.add_argument('--numheads', type=int, help='number of virtual heads to create')
84
85 # Compression options
86 parser.add_argument('--jpeg-wan-compression',
87 choices=wan_compression_options,
88 help="set jpeg wan compression")
89 parser.add_argument('--zlib-glz-wan-compression',
90 choices=wan_compression_options,
91 help="set zlib glz wan compressions")
92 parser.add_argument('--image-compression',
93 choices = ['off', 'auto_glz', 'auto_lz', 'quic',
94 'glz', 'lz'],
95 help="set image compression")
96 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 # TODO - sound support
98 parser.add_argument('--streaming-video', choices=['off', 'all', 'filter'],
99 help='set the streaming video method')
100 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
102 # 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 parser.add_argument('--vdagent-virtio-path', help='virtio socket path used by vdagentd')
105 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 parser.add_argument('--vdagentd-exec', help='path to spice-vdagentd (used with --vdagent)')
108 parser.add_argument('--vdagent-exec', help='path to spice-vdagent (used with --vdagent)')
109 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 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 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 #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
123 if cgdb:
124 parser.add_argument('--cgdb', action='store_true', default=False)
125
126 args, xorg_args = parser.parse_known_args(sys.argv[1:])
127
128 def 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
142 if 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
155 def 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.
170 for 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
175 def error(msg, exit_code=1):
176 print("Xspice: %s" % msg)
177 sys.exit(exit_code)
178
179 if not args.xorg:
180 error("Xorg missing")
181
182 cleanup_files = []
183 cleanup_dirs = []
184 cleanup_processes = []
185
186 def 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
205 def launch(*args, **kw):
206 p = Popen(*args, **kw)
207 cleanup_processes.append(p)
208 return p
209
210 signal.signal(signal.SIGTERM, cleanup)
211 atexit.register(cleanup)
212
213 if 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("""
230 Section "Device"
231 Identifier "XSPICE"
232 Driver "spiceqxl"
233 %(options)s
234 EndSection
235
236 Section "InputDevice"
237 Identifier "XSPICE POINTER"
238 Driver "xspice pointer"
239 EndSection
240
241 Section "InputDevice"
242 Identifier "XSPICE KEYBOARD"
243 Driver "xspice keyboard"
244 EndSection
245
246 Section "Monitor"
247 Identifier "Configured Monitor"
248 EndSection
249
250 Section "Screen"
251 Identifier "XSPICE Screen"
252 Monitor "Configured Monitor"
253 Device "XSPICE"
254 EndSection
255
256 Section "ServerLayout"
257 Identifier "XSPICE Example"
258 Screen "XSPICE Screen"
259 InputDevice "XSPICE KEYBOARD"
260 InputDevice "XSPICE POINTER"
261 EndSection
262
263 # Prevent udev from loading vmmouse in a vm and crashing.
264 Section "ServerFlags"
265 Option "AutoAddDevices" "False"
266 EndSection
267
268
269 """ % locals())
270 cf.flush()
271
272 if 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
291 var_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
300 for 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
313 if args.numheads:
314 os.environ['QXL_NUM_HEADS'] = str(args.numheads)
315
316
317 display=""
318 for arg in xorg_args:
319 if arg.startswith(":"):
320 display = arg
321 if not display:
322 print("Error: missing display on line (i.e. :3)")
323 raise SystemExit
324 os.environ ['DISPLAY'] = display
325
326 exec_args = [args.xorg, '-config', args.config]
327 if 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
332 xorg_args = [ '-noreset' ] + xorg_args
333
334
335 if 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
341 xorg = launch(executable=args.xorg, args=exec_args + xorg_args)
342 time.sleep(2)
343
344 retpid,rc = os.waitpid(xorg.pid, os.WNOHANG)
345 if retpid != 0:
346 print("Error: X server is not running")
347 else:
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