Files
xf86-video-qxl/scripts/Xspice
Christophe Fergeau 9a7eb40fa2 Xspice: Fix Python3 str() vs bytes() confusion
With python3, without universal_newlines=True, Popen().stdout.read()
will return a byte array, while find(str) expects to operate on a
string.
I've checked that this still works with python2 as well.
2017-02-08 17:08:27 +01:00

365 lines
14 KiB
Python
Executable File

#!/usr/bin/python
"""
Xspice
Xspice is a standard X server that is also a Spice server.
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
Xspice is kept in the same repository. It can also be used to debug the qxl
driver.
Xspice (this executable) will set a bunch of environment variables that are
used by spiceqxl_drv.so, and then spawn Xorg, giving it the default config file,
which can be overridden as well.
"""
import argparse
import os
import sys
import tempfile
import atexit
import time
import signal
from subprocess import Popen, PIPE
def which(x):
if not x:
return x
if os.path.exists(x):
return x
for p in os.environ['PATH'].split(':'):
candidate = os.path.join(p, x)
if os.path.exists(candidate):
return candidate
print('Warning: failed to find executable %s' % x)
return None
if 'XSPICE_ENABLE_GDB' in os.environ:
cgdb = which('cgdb')
if not cgdb:
cgdb = which('gdb')
else:
cgdb = None
def add_boolean(flag, *args, **kw):
parser.add_argument(flag, action='store_const', const='1',
*args, **kw)
wan_compression_options = ['auto', 'never', 'always']
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]",
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")
# Don't use any options that are already used by Xorg (unless we must)
# specifically, don't use -p and -s.
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
parser.add_argument('--jpeg-wan-compression',
choices=wan_compression_options,
help="set jpeg wan compression")
parser.add_argument('--zlib-glz-wan-compression',
choices=wan_compression_options,
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')
# TODO - sound support
parser.add_argument('--streaming-video', choices=['off', 'all', 'filter'],
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
parser.add_argument('--vdagent', action='store_true', dest='vdagent_enabled', default=False, help='launch vdagent & vdagentd. They provide clipboard & resolution automation')
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')
parser.add_argument('--vdagent-udcs-path', help='Unix domain socket path used by vdagent and vdagentd')
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')
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")
#TODO
#Option "SpiceAddr" ""
#add_boolean('--agent-mouse')
#Option "EnableImageCache" "True"
#Option "EnableFallbackCache" "True"
#Option "EnableSurfaces" "True"
#parser.add_argument('--playback-compression', choices=['0', '1'], help='enabled by default')
#Option "SpiceDisableCopyPaste" "False"
if cgdb:
parser.add_argument('--cgdb', action='store_true', default=False)
args, xorg_args = parser.parse_known_args(sys.argv[1:])
def agents_new_enough(args):
for f in [args.vdagent_exec, args.vdagentd_exec]:
if not f:
print('please specify path to vdagent/vdagentd executables')
return False
if not os.path.exists(f):
print('error: file not found ', f)
return False
for f in [args.vdagent_exec, args.vdagentd_exec]:
if Popen(args=[f, '-h'], stdout=PIPE, universal_newlines=True).stdout.read().find('-S') == -1:
return False
return True
if args.vdagent_enabled:
if not args.vdagent_exec:
args.vdagent_exec = 'spice-vdagent'
if not args.vdagentd_exec:
args.vdagentd_exec = 'spice-vdagentd'
args.vdagent_exec = which(args.vdagent_exec)
args.vdagentd_exec = which(args.vdagentd_exec)
if not agents_new_enough(args):
if args.vdagent_enabled:
print("error: vdagent is not new enough to support Xspice")
raise SystemExit
args.vdagent_enabled = False
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):
print("missing %s - %s does not exist" % (key, filename))
sys.exit(1)
def error(msg, exit_code=1):
print("Xspice: %s" % msg)
sys.exit(exit_code)
if not args.xorg:
error("Xorg missing")
cleanup_files = []
cleanup_dirs = []
cleanup_processes = []
def cleanup(*args):
for f in cleanup_files:
if os.path.exists(f):
os.remove(f)
for d in cleanup_dirs:
if os.path.exists(d):
os.rmdir(d)
for p in cleanup_processes:
try:
p.kill()
except OSError:
pass
for p in cleanup_processes:
try:
p.wait()
except OSError:
pass
del cleanup_processes[:]
def launch(*args, **kw):
p = Popen(*args, **kw)
cleanup_processes.append(p)
return p
signal.signal(signal.SIGTERM, cleanup)
atexit.register(cleanup)
if args.auto:
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
if args.audio_fifo_dir:
options = 'Option "SpicePlaybackFIFODir" "%s"' % args.audio_fifo_dir
else:
options = ''
cf.write("""
Section "Device"
Identifier "XSPICE"
Driver "spiceqxl"
%(options)s
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
""" % locals())
cf.flush()
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])
var_args = ['port', 'tls_port', 'disable_ticketing',
'x509_dir', 'sasl', 'cacert_file', 'x509_cert_file',
'x509_key_file', 'x509_key_password',
'tls_ciphers', 'dh_file', 'password', 'image_compression',
'jpeg_wan_compression', 'zlib_glz_wan_compression',
'streaming_video', 'video_codecs', 'deferred_fps', 'exit_on_disconnect',
'vdagent_enabled', 'vdagent_virtio_path', 'vdagent_uinput_path',
'vdagent_uid', 'vdagent_gid']
for arg in var_args:
if getattr(args, arg) != None:
# 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
# A few arguments don't follow the XSPICE_ convention - handle them manually
if args.numheads:
os.environ['QXL_NUM_HEADS'] = str(args.numheads)
display=""
for arg in xorg_args:
if arg.startswith(":"):
display = arg
if not display:
print("Error: missing display on line (i.e. :3)")
raise SystemExit
os.environ ['DISPLAY'] = display
exec_args = [args.xorg, '-config', args.config]
if cgdb and args.cgdb:
exec_args = [cgdb, '--args'] + exec_args
args.xorg = cgdb
# This is currently mandatory; the driver cannot survive a reset
xorg_args = [ '-noreset' ] + xorg_args
if args.vdagent_enabled:
for f in [args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path]:
if os.path.exists(f):
os.unlink(f)
cleanup_files.extend([args.vdagent_udcs_path, args.vdagent_virtio_path, args.vdagent_uinput_path])
xorg = launch(executable=args.xorg, args=exec_args + xorg_args)
time.sleep(2)
retpid,rc = os.waitpid(xorg.pid, os.WNOHANG)
if retpid != 0:
print("Error: X server is not running")
else:
if args.vdagent_enabled and args.vdagent_launch:
# XXX use systemd --user for this?
vdagentd = launch(args=[args.vdagentd_exec, '-f', '-x', '-S', args.vdagent_udcs_path,
'-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',
args.vdagent_udcs_path])
if args.xsession:
environ = os.environ
os.spawnlpe(os.P_NOWAIT, args.xsession, environ)
try:
xorg.wait()
except KeyboardInterrupt:
# Catch Ctrl-C as that is the common way of ending this script
print("Keyboard Interrupt")