mirror of
https://github.com/X11Libre/xf86-video-qxl.git
synced 2026-03-24 01:24:24 +00:00
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.
365 lines
14 KiB
Python
Executable File
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")
|