Skip to content

Commit

Permalink
_x11.py: Add experimental support for Xpra
Browse files Browse the repository at this point in the history
  • Loading branch information
hartwork committed Dec 16, 2023
1 parent cf30072 commit 9fe3d6e
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ So what is shared with the application by default then?
- X2Go nxagent (seamless)
- Xephyr
- Xnest
- Xpra (experimental, careful!)
- Xvfb (invisible)
- Support for PulseAudio
- Support for `/etc/resolv.conf` provided by:
Expand Down
13 changes: 11 additions & 2 deletions sandwine/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def parse_command_line(args):
action='store_const',
const=X11Mode.AUTO,
help='enable nested X11 using X2Go nxagent or Xephry or Xnest'
' but not Xvfb'
' but not Xvfb and not Xpra'
' (default: X11 disabled)')
x11_args.add_argument('--nxagent',
dest='x11',
Expand All @@ -109,6 +109,12 @@ def parse_command_line(args):
action='store_const',
const=X11Mode.XNEST,
help='enable nested X11 using Xnest (default: X11 disabled)')
x11_args.add_argument('--xpra',
dest='x11',
action='store_const',
const=X11Mode.XPRA,
help='enable nested X11 using Xpra (EXPERIMENTAL, CAREFUL!)'
' (default: X11 disabled)')
x11_args.add_argument('--xvfb',
dest='x11',
action='store_const',
Expand Down Expand Up @@ -454,7 +460,10 @@ def main():
if X11Mode(config.x11) == X11Mode.HOST:
config.x11_display_number = X11Display.find_used()
else:
config.x11_display_number = X11Display.find_unused()
minimum = 0
if X11Mode(config.x11) == X11Mode.XPRA:
minimum = 10 # Avoids warning from Xpra for displays <=9
config.x11_display_number = X11Display.find_unused(minimum)

_logger.info('Using display ":%s"...', config.x11_display_number)

Expand Down
133 changes: 131 additions & 2 deletions sandwine/_x11.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
import signal
import subprocess
import sys
import tempfile
import time
from abc import ABC, abstractmethod
from contextlib import nullcontext
from enum import Enum
from textwrap import dedent

_logger = logging.getLogger(__name__)

Expand All @@ -41,6 +43,7 @@ class X11Mode(Enum):
NXAGENT = 'nxagent'
XEPHYR = 'xephyr'
XNEST = 'xnest'
XPRA = 'xpra'
XVFB = 'xvfb'
NONE = 'none'

Expand All @@ -62,9 +65,9 @@ def wait_until_available(self):
_wait_until_file_present(x11_unix_socket)

@staticmethod
def find_unused() -> int:
def find_unused(minimum: int = 0) -> int:
used_displays = {int(os.path.basename(p)[1:]) for p in glob.glob('/tmp/.X11-unix/X*')}
candidate_displays = set(list(range(len(used_displays))) + [len(used_displays)])
candidate_displays = set(list(range(len(used_displays))) + [len(used_displays)] + [minimum])
return sorted(candidate_displays - used_displays)[-1]

@staticmethod
Expand Down Expand Up @@ -158,6 +161,130 @@ def _create_argv(self):
return [self._command, '-geometry', self._geometry, f':{self._display_number}']


class XpraContext(_X11Context):
_command = 'xpra'

def __init__(self, **kwargs):
super().__init__(**kwargs)
self._client_process = None
self._server_process = None
self._tempdir = None

@staticmethod
def _write_xvfh_wrapper_script_to(xvfb_wrapper_path):
with open(xvfb_wrapper_path, 'w') as f:
print(dedent("""\
#! /usr/bin/env bash
set -e
args=(
+extension GLX
+extension Composite
# NOTE: Extension MIT-SHM is disabled because it kept crashing Xephyr 21.1.7
# and nxagent/X2Go 4.1.0.3 when moving windows around near screen edges.
-extension MIT-SHM
# NOTE: This is the Xpra default, 1024x768x24+32 was rejected in practice.
-screen 0 8192x4096x24+32
-nolisten tcp
-noreset
# NOTE: We are trying to protect the host from the app,
# *not* the app from the host.
# -auth [..]
-dpi 96
"$@"
)
PS4='# '
set -x
exec Xvfb "${args[@]}"
"""),
file=f)
os.fchmod(f.fileno(), 0o755) # i.e. make executable

def __enter__(self):
_logger.info(self._message_starting)

self._tempdir = tempfile.TemporaryDirectory()
self._tempdir.__enter__()

unix_socket_path = os.path.join(self._tempdir.name, 'xpra-socket')
xvfb_wrapper_path = os.path.join(self._tempdir.name, 'xpra-xvfb.sh')
sessions_path = os.path.join(self._tempdir.name, 'xpra-sessions')

self._write_xvfh_wrapper_script_to(xvfb_wrapper_path)

server_argv = [
self._command,
'start',
# NOTE: This is experimental and some of these options
# may need a closer look and/or re-evaluation.
# Experimental implies risky to depend on for security!
'--attach=no',
'--bandwidth-limit=0',
'--bell=no',
f'--bind={unix_socket_path}',
'--clipboard=no',
'--daemon=no',
'--dbus-launch=',
'--dbus-proxy=no',
'--file-transfer=no',
'--html=off',
'--http-scripts=off',
'--microphone=off',
'--min-quality=100',
'--open-files=no',
'--open-url=no',
'--printing=no',
'--proxy-start-sessions=no',
'--pulseaudio=no',
'--quality=100',
f'--sessions-dir={sessions_path}',
'--speaker=off',
'--start-new-commands=no',
'--systemd-run=no',
'--use-display=no',
'--video-scaling=0',
'--webcam=no',
'--xsettings=no',
f'--xvfb={xvfb_wrapper_path}',
f':{self._display_number}',
]
client_argv = [
self._command,
'attach',
f'--sessions-dir={sessions_path}',
unix_socket_path,
]

client_env = os.environ.copy()
client_env.pop('SSH_AUTH_SOCK', None)

try:
self._server_process = subprocess.Popen(server_argv)
_wait_until_file_present(unix_socket_path)
self._client_process = subprocess.Popen(client_argv, env=client_env)
except FileNotFoundError:
_logger.error(f'Command {self._command!r} is not available, aborting.')
sys.exit(127)

X11Display(self._display_number).wait_until_available()

_logger.info(self._message_started)

def __exit__(self, exc_type, exc_val, exc_tb):
_logger.info(self._message_stopping)

for process in (self._client_process, self._server_process):
if process is None:
continue
# NOTE: Using SIGTERM only because SIGINT showed backtrace output
process.send_signal(signal.SIGTERM)
process.wait()

self._tempdir.__exit__(None, None, None)
self._tempdir = None

_logger.info(self._message_stopped)


class XvfbX11Context(_SimpleX11Context):
_command = 'Xvfb'

Expand Down Expand Up @@ -200,6 +327,8 @@ def create_x11_context(mode: X11Mode, display_number: int, width: int, height: i
return XephyrX11Context(**init_args)
elif mode == X11Mode.XNEST:
return XnestX11Context(**init_args)
elif mode == X11Mode.XPRA:
return XpraContext(**init_args)
elif mode == X11Mode.XVFB:
return XvfbX11Context(**init_args)

Expand Down

0 comments on commit 9fe3d6e

Please sign in to comment.