diff --git a/fs/etc/xpra/conf.d/55_server_x11.conf.in b/fs/etc/xpra/conf.d/55_server_x11.conf.in index 0243264843..71f62a1ab1 100644 --- a/fs/etc/xpra/conf.d/55_server_x11.conf.in +++ b/fs/etc/xpra/conf.d/55_server_x11.conf.in @@ -27,8 +27,16 @@ input-method=auto # sync-xvfb = 0 sync-xvfb = auto -# Virtual display command: -# - Xvfb option (limited DPI support) +# Virtual display command +# common aliases are resolved to full commands at runtime: +# xvfb = Xvfb +# xvfb = Xorg +# xvfb = Xephyr +# xvfb = weston+Xwayland +# xvfb = auto +# +# full commands: +# Xvfb option (limited DPI support) # xvfb = Xvfb -nolisten tcp -noreset \ # +extension GLX +extension Composite \ # +extension RANDR +extension RENDER \ @@ -42,7 +50,7 @@ sync-xvfb = auto # +extension GLX +extension Composite \ # -auth $XAUTHORITY \ # -screen 8192x4096x24+32 -# - Xdummy (better with version 0.4.0 or later): +# - Xdummy: #xvfb = %(xdummy_command)s # # Selecting virtual X server: diff --git a/xpra/scripts/config.py b/xpra/scripts/config.py index 7a6e87fd40..5e2e3d28d1 100755 --- a/xpra/scripts/config.py +++ b/xpra/scripts/config.py @@ -155,31 +155,37 @@ def get_Xdummy_confdir() -> str: def get_Xdummy_command(xorg_cmd="Xorg", log_dir="${XPRA_SESSION_DIR}", - xorg_conf="${XORG_CONFIG_PREFIX}/etc/xpra/xorg.conf") -> list[str]: - return [ + xorg_conf="${XORG_CONFIG_PREFIX}/etc/xpra/xorg.conf", + dpi=0) -> list[str]: + cmd = [ # ie: "Xorg" or "xpra_Xdummy" or "./install/bin/xpra_Xdummy" xorg_cmd, - "-noreset", "-novtswitch", - "-nolisten", "tcp", "+extension", "GLX", "+extension", "RANDR", "+extension", "RENDER", + "-nolisten", "tcp", + "-noreset", + "-novtswitch", "-auth", "$XAUTHORITY", "-logfile", f"{log_dir}/Xorg.log", # must be specified with some Xorg versions (ie: arch linux) # this directory can store xorg config files, it does not need to be created: - "-configdir", f'"{get_Xdummy_confdir()}"', - "-config", f'"{xorg_conf}"', + "-configdir", f"{get_Xdummy_confdir()}", + "-config", f"{xorg_conf}", ] + if dpi > 0: + cmd += ["-dpi", f"{dpi}x{dpi}"] + return cmd -def get_Xvfb_command(width=8192, height=4096, dpi=96) -> list[str]: +def get_Xvfb_command(width=8192, height=4096, depth=24, dpi=96) -> list[str]: cmd = [ "Xvfb", "+extension", "GLX", "+extension", "Composite", - "+extension", "RANDR", "+extension", "RENDER", - "-screen", "0", f"{width}x{height}x24+32", + "+extension", "RANDR", + "+extension", "RENDER", + "-screen", "0", f"{width}x{height}x{depth}+32", # better than leaving to vfb after a resize? "-nolisten", "tcp", "-noreset", @@ -190,9 +196,61 @@ def get_Xvfb_command(width=8192, height=4096, dpi=96) -> list[str]: return cmd +def get_Xephyr_command(width=1920, height=1080, depth=24, dpi=96) -> list[str]: + cmd = [ + "Xephyr", + "+extension", "GLX", + "+extension", "Composite", + "-screen", f"{width}x{height}x{depth}+32", + "-nolisten", "tcp", + "-noreset", + "-auth", "$XAUTHORITY", + ] + if dpi > 0: + cmd += ["-dpi", f"{dpi}x{dpi}"] + return cmd + + +def get_weston_Xwayland_command(dpi=96) -> list[str]: + cmd = [ + "/usr/libexec/xpra/xpra_weston_xvfb", + "+extension", "GLX", + "+extension", "Composite", + "-nolisten", "tcp", + "-noreset", + "-auth", "$XAUTHORITY", + ] + if dpi > 0: + cmd += ["-dpi", f"{dpi}x{dpi}"] + return cmd + + +def xvfb_command(cmd: str, depth=32, dpi=0) -> list[str]: + parts = shlex.split(cmd) + if len(parts) > 1: + return parts + exe = parts[0] + if os.path.isabs(exe): + return parts + if exe == "Xvfb": + return get_Xvfb_command(depth=depth, dpi=dpi) + if exe == "Xephyr": + return get_Xephyr_command(depth=depth, dpi=dpi) + if exe in ("weston", "weston+Xwayland"): + return get_weston_Xwayland_command(dpi=dpi) + if exe in ("Xorg", "Xdummy"): + xorg_bin = get_xorg_bin() + return get_Xdummy_command(xorg_bin, dpi=dpi) + if exe == "auto": + return detect_xvfb_command(dpi=dpi) + return parts + + def detect_xvfb_command(conf_dir="/etc/xpra/", bin_dir="", Xdummy_ENABLED: bool | None = None, Xdummy_wrapper_ENABLED: bool | None = None, - warn_fn: Callable = warn) -> list[str]: + warn_fn: Callable = warn, + dpi=0, + ) -> list[str]: """ This function returns the xvfb command to use. It can either be an `Xvfb` command or one that uses `Xdummy`, @@ -200,18 +258,22 @@ def detect_xvfb_command(conf_dir="/etc/xpra/", bin_dir="", """ if WIN32: # pragma: no cover return [] + + def vfb_default() -> list[str]: + return get_Xvfb_command(dpi=dpi) + if OSX: # pragma: no cover - return get_Xvfb_command() + return vfb_default() if sys.platform.find("bsd") >= 0 and Xdummy_ENABLED is None: # pragma: no cover warn_fn(f"Warning: sorry, no support for Xdummy on {sys.platform}") - return get_Xvfb_command() + return vfb_default() if is_DEB(): # These distros do weird things and this can cause the real X11 server to crash # see ticket #2834 - return get_Xvfb_command() + return vfb_default() if Xdummy_ENABLED is False: - return get_Xvfb_command() + return vfb_default() if Xdummy_ENABLED is None: debug("Xdummy support unspecified, will try to detect") @@ -222,15 +284,16 @@ def detect_xvfb_command(conf_dir="/etc/xpra/", bin_dir="", debug(f" found redhat-release: {relinfo!r}") if relinfo.find("release 10") >= 0: debug(" using `xpra_weston_xvfb` on RHEL 10") - return ["/usr/libexec/xpra/xpra_weston_xvfb"] - return detect_xdummy_command(conf_dir, bin_dir, Xdummy_wrapper_ENABLED, warn_fn) + return get_weston_Xwayland_command(dpi) + return detect_xdummy_command(conf_dir, bin_dir, Xdummy_wrapper_ENABLED, warn_fn, dpi=dpi) def detect_xdummy_command(conf_dir="/etc/xpra/", bin_dir="", Xdummy_wrapper_ENABLED: bool | None = None, - warn_fn: Callable = warn) -> list[str]: + warn_fn: Callable = warn, + dpi=0) -> list[str]: if not POSIX or OSX: - return get_Xvfb_command() + return get_Xvfb_command(dpi=dpi) xorg_bin = get_xorg_bin() if Xdummy_wrapper_ENABLED is not None: # honour what was specified: @@ -245,7 +308,7 @@ def detect_xdummy_command(conf_dir="/etc/xpra/", bin_dir="", if (xorg_stat.st_mode & stat.S_ISUID) != 0: if (xorg_stat.st_mode & stat.S_IROTH) == 0: warn_fn(f"{xorg_bin} is suid and not readable, Xdummy support unavailable") - return get_Xvfb_command() + return get_Xvfb_command(dpi=dpi) debug(f"{xorg_bin} is suid and readable, using the xpra_Xdummy wrapper") use_wrapper = True else: @@ -259,7 +322,7 @@ def detect_xdummy_command(conf_dir="/etc/xpra/", bin_dir="", if bin_dir and os.path.exists(os.path.join(bin_dir, xorg_cmd)): if bin_dir not in os.environ.get("PATH", "/bin:/usr/bin:/usr/local/bin").split(os.pathsep): xorg_cmd = os.path.join(bin_dir, xorg_cmd) - return get_Xdummy_command(xorg_cmd, xorg_conf=xorg_conf) + return get_Xdummy_command(xorg_cmd, xorg_conf=xorg_conf, dpi=dpi) def wrap_cmd_str(cmd) -> str: diff --git a/xpra/scripts/server.py b/xpra/scripts/server.py index 1809d2e0f9..b75b6f7cba 100644 --- a/xpra/scripts/server.py +++ b/xpra/scripts/server.py @@ -35,6 +35,7 @@ FALSE_OPTIONS, ALL_BOOLEAN_OPTIONS, OPTION_TYPES, CLIENT_ONLY_OPTIONS, CLIENT_OPTIONS, parse_bool_or, fixup_options, make_defaults_struct, read_config, dict_to_validated_config, + xvfb_command, ) from xpra.common import ( CLOBBER_USE_DISPLAY, CLOBBER_UPGRADE, SSH_AGENT_DISPATCH, @@ -1066,6 +1067,8 @@ def write_session_file(filename: str, contents) -> str: # with the value supplied by the user: protected_env["XDG_RUNTIME_DIR"] = xrd + xvfb_cmd = xvfb_command(opts.xvfb, opts.pixel_depth, opts.dpi) + sanitize_env() if not shadowing: os.environ.pop("WAYLAND_DISPLAY", None) @@ -1081,7 +1084,7 @@ def write_session_file(filename: str, contents) -> str: os.environ["DISPLAY"] = display_name if POSIX: os.environ["CKCON_X11_DISPLAY"] = display_name - elif not start_vfb or opts.xvfb.find("Xephyr") < 0: + elif not start_vfb or xvfb_cmd[0].find("Xephyr") < 0: os.environ.pop("DISPLAY", None) os.environ.update(protected_env) @@ -1152,7 +1155,7 @@ def write_session_file(filename: str, contents) -> str: if (starting or starting_desktop) and desktop_display and opts.notifications and not opts.dbus_launch: print_DE_warnings() - if start_vfb and opts.sync_xvfb is None and any(opts.xvfb.find(x) >= 0 for x in ("Xephyr", "Xnest")): + if start_vfb and opts.sync_xvfb is None and any(xvfb_cmd[0].find(x) >= 0 for x in ("Xephyr", "Xnest")): # automatically enable sync-xvfb for Xephyr and Xnest: opts.sync_xvfb = 50 @@ -1320,7 +1323,7 @@ def write_session_file(filename: str, contents) -> str: sizes = opts.resize_display.split(":", 1)[-1] vfb_geom = parse_resolutions(sizes, opts.refresh_rate)[0] - xvfb, display_name = start_Xvfb(opts.xvfb, vfb_geom, pixel_depth, display_name, cwd, + xvfb, display_name = start_Xvfb(xvfb_cmd, vfb_geom, pixel_depth, display_name, cwd, uid, gid, username, uinput_uuid) assert xauthority xauth_add(xauthority, display_name, xauth_data, uid, gid) @@ -1333,7 +1336,7 @@ def xvfb_terminated() -> None: if xvfb_pidfile: os.unlink(xvfb_pidfile) - getChildReaper().add_process(xvfb, "xvfb", opts.xvfb, ignore=True, callback=xvfb_terminated) + getChildReaper().add_process(xvfb, "xvfb", xvfb_cmd, ignore=True, callback=xvfb_terminated) # always update as we may now have the "real" display name: os.environ["DISPLAY"] = display_name os.environ["CKCON_X11_DISPLAY"] = display_name @@ -1372,8 +1375,6 @@ def xvfb_terminated() -> None: if uinput_uuid: devices = create_input_devices(uinput_uuid, uid) - xvfb_cmd = opts.xvfb - def check_xvfb(timeout=0) -> bool: if xvfb is None: return True diff --git a/xpra/util/child_reaper.py b/xpra/util/child_reaper.py index dd7e8eaa92..30d244c2fc 100644 --- a/xpra/util/child_reaper.py +++ b/xpra/util/child_reaper.py @@ -12,7 +12,7 @@ import os import signal from typing import Any -from collections.abc import Callable +from collections.abc import Callable, Sequence from xpra.util.env import envint, envbool from xpra.os_util import POSIX, gi_import @@ -103,7 +103,7 @@ def cleanup(self) -> None: self._proc_info = [] self._quit = None - def add_process(self, process, name: str, command, ignore=False, forget=False, callback=None) -> ProcInfo: + def add_process(self, process, name: str, command: str | Sequence[str], ignore=False, forget=False, callback=None) -> ProcInfo: pid = process.pid if pid <= 0: raise RuntimeError(f"process {process} has no pid!") diff --git a/xpra/x11/vfb_util.py b/xpra/x11/vfb_util.py index cdcf874a5f..4c0d92f6b5 100644 --- a/xpra/x11/vfb_util.py +++ b/xpra/x11/vfb_util.py @@ -19,7 +19,7 @@ from xpra.common import RESOLUTION_ALIASES, DEFAULT_REFRESH_RATE, get_refresh_rate_for_value from xpra.scripts.config import InitException, get_Xdummy_confdir, FALSE_OPTIONS from xpra.util.str_fn import csv -from xpra.util.env import envint, envbool, shellsub, osexpand, get_exec_env +from xpra.util.env import envint, envbool, shellsub, osexpand, get_exec_env, get_saved_env_var from xpra.os_util import getuid, getgid, POSIX, OSX from xpra.server.util import setuidgid from xpra.util.io import is_writable, pollwait @@ -190,22 +190,22 @@ def get_xauthority_path(display_name: str) -> str: return os.path.join(d, filename) -def start_Xvfb(xvfb_str: str, vfb_geom, pixel_depth: int, display_name: str, cwd, +def start_Xvfb(xvfb_cmd: list[str], vfb_geom, pixel_depth: int, display_name: str, cwd, uid: int, gid: int, username: str, uinput_uuid="") -> tuple[Popen, str]: if not POSIX: raise InitException(f"starting an Xvfb is not supported on {os.name}") if OSX: raise InitException("starting an Xvfb is not supported on MacOS") - if not xvfb_str: + if not xvfb_cmd: raise InitException("the 'xvfb' command is not defined") log = get_vfb_logger() log("start_Xvfb%s XVFB_EXTRA_ARGS=%s", - (xvfb_str, vfb_geom, pixel_depth, display_name, cwd, uid, gid, username, uinput_uuid), + (xvfb_cmd, vfb_geom, pixel_depth, display_name, cwd, uid, gid, username, uinput_uuid), XVFB_EXTRA_ARGS) use_display_fd = display_name[0] == 'S' if XVFB_EXTRA_ARGS: - xvfb_str += " "+XVFB_EXTRA_ARGS + xvfb_cmd += shlex.split(XVFB_EXTRA_ARGS) subs: dict[str, str] = {} @@ -222,9 +222,6 @@ def pathexpand(s: str) -> str: # identify logfile argument if it exists, # as we may have to rename it, or create the directory for it: - xvfb_cmd = shlex.split(xvfb_str) - if not xvfb_cmd: - raise InitException("cannot start Xvfb, the command definition is missing!") # make sure all path values are expanded: xvfb_cmd = [pathexpand(s) for s in xvfb_cmd] @@ -295,9 +292,14 @@ def pathexpand(s: str) -> str: if (xvfb_executable.endswith("Xorg") or xvfb_executable.endswith("Xdummy")) and pixel_depth > 0: xvfb_cmd.append("-depth") xvfb_cmd.append(str(pixel_depth)) - env = get_exec_env(keep=("SHELL", "HOSTNAME", "XMODIFIERS", - "PWD", "HOME", "USERNAME", "LANG", "TERM", "USER", - "XDG_RUNTIME_DIR", "XDG_DATA_DIR", "PATH")) + keep = [ + "SHELL", "HOSTNAME", "XMODIFIERS", + "PWD", "HOME", "USERNAME", "LANG", "TERM", "USER", + "XDG_RUNTIME_DIR", "XDG_DATA_DIR", "PATH", + ] + env = get_exec_env(keep=keep) + if xvfb_executable.endswith("Xephyr"): + env["DISPLAY"] = get_saved_env_var("DISPLAY") log(f"xvfb env={env}") xvfb = None try: @@ -492,7 +494,7 @@ def preexec(): log.estr(e) -def check_xvfb_process(xvfb=None, cmd: str = "Xvfb", timeout: int = 0, command=None) -> bool: +def check_xvfb_process(xvfb=None, cmd: str = "Xvfb", timeout: int = 0, command=()) -> bool: if xvfb is None: # we don't have a process to check return True @@ -503,9 +505,9 @@ def check_xvfb_process(xvfb=None, cmd: str = "Xvfb", timeout: int = 0, command=N log.error("") log.error("%s command has terminated! xpra cannot continue", cmd) log.error(" if the display is already running, try a different one,") - log.error(" or use the --use-display flag") + log.error(" if the `xvfb` command is invalid, try a different one") if command: - log.error(" full command: %s", command) + log.error(" full command: %r", shlex.join(command)) log.error("") return False