diff --git a/setup.py b/setup.py index a9fb77b997..4bbedcb702 100755 --- a/setup.py +++ b/setup.py @@ -2529,6 +2529,7 @@ def bundle_tests() -> None: ) if server_ENABLED or proxy_ENABLED: add_modules("xpra.scripts.server") + add_modules("xpra.scripts.session") toggle_packages(not WIN32, "xpra.platform.pycups_printing") toggle_packages(opengl_ENABLED, "xpra.opengl") diff --git a/xpra/scripts/main.py b/xpra/scripts/main.py index 449bdc9b81..112b15d6f4 100755 --- a/xpra/scripts/main.py +++ b/xpra/scripts/main.py @@ -870,6 +870,17 @@ def do_run_mode(script_file: str, cmdline, error_cb, options, args, full_mode: s if mode == "xvfb-command": print(shlex.join(xvfb_command(options.xvfb, options.pixel_depth, options.dpi))) return ExitCode.OK + if mode == "xvfb": + if len(args) > 1: + raise ValueError("too many arguments") + display = "S" + str(os.getpid()) + if args: + display = args[0] + if not display.startswith(":"): + raise ValueError(f"invalid display format {display!r}") + xvfb_cmd = xvfb_command(options.xvfb, options.pixel_depth, options.dpi) + from xpra.x11.vfb_util import start_xvfb_standalone + return start_xvfb_standalone(xvfb_cmd, options.sessions_dir, options.pixel_depth, display) if mode == "sbom": return run_sbom(args) if mode == "showconfig": @@ -3044,7 +3055,7 @@ def start_server_subprocess(script_file, args, mode, opts, else: assert len(args) == 0 # let the server get one from Xorg via displayfd: - display_name = 'S' + str(os.getpid()) + display_name = "S" + str(os.getpid()) else: if mode not in ("expand", "shadow", "shadow-screen"): raise ValueError(f"invalid mode {mode!r}") @@ -3314,7 +3325,7 @@ def run_proxy(error_cb, opts, script_file, cmdline, args, mode, defaults) -> Exi pass else: try: - from xpra.scripts.server import get_session_dir + from xpra.scripts.session import get_session_dir session_dir = get_session_dir("attach", opts.sessions_dir, display_name, getuid()) # ie: "/run/user/$UID/xpra/$DISPLAY/ssh/$UUID setup_proxy_ssh_socket(cmdline, session_dir=session_dir) @@ -3667,7 +3678,7 @@ def run_clean(opts, args: Iterable[str]) -> ExitValue: uid = int(opts.uid) except (ValueError, TypeError): uid = getuid() - from xpra.scripts.server import get_session_dir + from xpra.scripts.session import get_session_dir clean: dict[str, str] = {} if args: for display in args: @@ -4023,7 +4034,9 @@ def get_x11_display_info(display, sessions_dir="") -> dict[str, Any]: xauthority: str = "" if sessions_dir: try: - from xpra.scripts.server import get_session_dir, load_session_file, session_file_path + from xpra.scripts.session import load_session_file + from xpra.scripts.session import session_file_path + from xpra.scripts.session import get_session_dir except ImportError as e: log(f"get_x11_display_info: {e}") else: diff --git a/xpra/scripts/server.py b/xpra/scripts/server.py index 4e97b4d682..25727d0e4d 100644 --- a/xpra/scripts/server.py +++ b/xpra/scripts/server.py @@ -19,6 +19,8 @@ from collections.abc import Sequence from xpra import __version__ +from xpra.scripts.session import get_session_dir, make_session_dir, session_file_path, load_session_file, \ + save_session_file from xpra.util.io import info, warn from xpra.util.parsing import parse_str_dict from xpra.scripts.parsing import fixup_defaults, MODE_ALIAS @@ -52,7 +54,7 @@ from xpra.server.util import setuidgid from xpra.util.system import SIGNAMES from xpra.util.str_fn import nicestr -from xpra.util.io import load_binary_file, is_writable, stderr_print, which +from xpra.util.io import is_writable, stderr_print, which from xpra.util.env import unsetenv, envbool, osexpand, get_saved_env, get_saved_env_var from xpra.common import GROUP from xpra.util.child_reaper import getChildReaper @@ -432,146 +434,6 @@ def write_displayfd(display_name: str, fd: int) -> None: log.estr(e) -def get_session_dir(mode: str, sessions_dir: str, display_name: str, uid: int) -> str: - session_dir = osexpand(os.path.join(sessions_dir, display_name.lstrip(":")), uid=uid) - if not os.path.exists(session_dir): - ROOT = POSIX and getuid() == 0 - ROOT_FALLBACK = ("/run/xpra", "/var/run/xpra", "/tmp") - if ROOT and uid == 0 and not any(session_dir.startswith(x) for x in ROOT_FALLBACK): - # there is usually no $XDG_RUNTIME_DIR when running as root - # and even if there was, that's probably not a good path to use, - # so try to find a more suitable directory we can use: - for d in ROOT_FALLBACK: - if os.path.exists(d): - if mode == "proxy" and (display_name or "").lstrip(":").split(",")[0] == "14500": - # stash the system-wide proxy session files in a 'proxy' subdirectory: - return os.path.join(d, "proxy") - # otherwise just use the display as subdirectory name: - return os.path.join(d, (display_name or "").lstrip(":")) - return session_dir - - -def make_session_dir(mode: str, sessions_dir: str, display_name: str, uid: int = 0, gid: int = 0) -> str: - session_dir = get_session_dir(mode, sessions_dir, display_name, uid) - if not os.path.exists(session_dir): - try: - os.makedirs(session_dir, 0o750, exist_ok=True) - except OSError: - import tempfile - session_dir = osexpand(os.path.join(tempfile.gettempdir(), display_name.lstrip(":"))) - os.makedirs(session_dir, 0o750, exist_ok=True) - ROOT = POSIX and getuid() == 0 - if ROOT and (session_dir.startswith("/run/user/") or session_dir.startswith("/run/xpra/")): - os.lchown(session_dir, uid, gid) - return session_dir - - -def session_file_path(filename: str) -> str: - session_dir = os.environ.get("XPRA_SESSION_DIR") - if session_dir is None: - raise RuntimeError("'XPRA_SESSION_DIR' must be set to use this function") - return os.path.join(session_dir, filename) - - -def load_session_file(filename: str) -> bytes: - return load_binary_file(session_file_path(filename)) - - -def save_session_file(filename: str, contents: str | bytes, uid: int = -1, gid: int = -1) -> str: - if not os.environ.get("XPRA_SESSION_DIR"): - return "" - if not isinstance(contents, bytes): - contents = str(contents).encode("utf8") - assert contents - path = session_file_path(filename) - try: - with open(path, "wb+") as f: - if POSIX: - os.fchmod(f.fileno(), 0o640) - if getuid() == 0 and uid >= 0 and gid >= 0: - os.fchown(f.fileno(), uid, gid) - f.write(contents) - except OSError as e: - from xpra.log import Logger - log = Logger("server") - log("save_session_file", exc_info=True) - log.error(f"Error saving session file {path!r}") - log.estr(e) - return path - - -def rm_session_dir(warn: bool = True) -> None: - session_dir = os.environ.get("XPRA_SESSION_DIR") - if not session_dir or not os.path.exists(session_dir): - return - from xpra.log import Logger - log = Logger("server") - try: - session_files = os.listdir(session_dir) - except OSError as e: - log("os.listdir(%s)", session_dir, exc_info=True) - if warn: - log.error(f"Error: cannot access {session_dir!r}") - log.estr(e) - return - if session_files: - if warn: - log.info(f"session directory {session_dir!r} was not removed") - log.info(" because it still contains some files:") - for f in session_files: - extra = " (directory)" if os.path.isdir(os.path.join(session_dir, f)) else "" - log.info(f" {f!r}{extra}") - return - try: - os.rmdir(session_dir) - except OSError as e: - log = Logger("server") - log(f"rmdir({session_dir})", exc_info=True) - log.error(f"Error: failed to remove session directory {session_dir!r}") - log.estr(e) - - -def clean_session_files(*filenames) -> None: - if not CLEAN_SESSION_FILES: - return - for filename in filenames: - path = session_file_path(filename) - if filename.find("*") >= 0: - for p in glob.glob(path): - clean_session_path(p) - else: - clean_session_path(path) - rm_session_dir(False) - - -def clean_session_path(path) -> None: - from xpra.log import Logger - log = Logger("server") - log(f"clean_session_path({path})") - if os.path.exists(path): - try: - if os.path.isdir(path): - os.rmdir(path) - else: - os.unlink(path) - except OSError as e: - log(f"clean_session_path({path})", exc_info=True) - log.error(f"Error removing session path {path}") - log.estr(e) - if os.path.isdir(path): - files = os.listdir(path) - if files: - log.error(" this directory still contains some files:") - for file in files: - finfo = repr(file) - try: - if os.path.islink(file): - finfo += " -> "+repr(os.readlink(file)) - except OSError: - pass - log.error(f" {finfo}") - - SERVER_SAVE_SKIP_OPTIONS: Sequence[str] = ( "systemd-run", "daemon", @@ -910,8 +772,8 @@ def _do_run_server(script_file: str, cmdline, display_name = guess_xpra_display(opts.socket_dir, opts.socket_dirs) else: # We will try to find one automatically - # Use the temporary magic value 'S' as marker: - display_name = 'S' + str(os.getpid()) + # Use the temporary magic value "S" as marker: + display_name = "S" + str(os.getpid()) if upgrading: assert display_name, "no display found to upgrade" @@ -1082,7 +944,7 @@ def write_session_file(filename: str, contents) -> str: os.environ["XDG_SESSION_TYPE"] = "x11" if not starting_desktop: os.environ["XDG_CURRENT_DESKTOP"] = opts.wm_name - if display_name[0] != 'S': + if display_name[0] != "S": os.environ["DISPLAY"] = display_name if POSIX: os.environ["CKCON_X11_DISPLAY"] = display_name diff --git a/xpra/scripts/session.py b/xpra/scripts/session.py new file mode 100644 index 0000000000..31340b55cf --- /dev/null +++ b/xpra/scripts/session.py @@ -0,0 +1,153 @@ +# This file is part of Xpra. +# Copyright (C) 2025 Antoine Martin +# Xpra is released under the terms of the GNU GPL v2, or, at your option, any +# later version. See the file COPYING for details. + +import os +import glob + +from xpra.os_util import POSIX, getuid +from xpra.scripts.server import CLEAN_SESSION_FILES +from xpra.util.env import osexpand +from xpra.util.io import load_binary_file + + +def get_session_dir(mode: str, sessions_dir: str, display_name: str, uid: int) -> str: + session_dir = osexpand(os.path.join(sessions_dir, display_name.lstrip(":")), uid=uid) + if not os.path.exists(session_dir): + ROOT = POSIX and getuid() == 0 + ROOT_FALLBACK = ("/run/xpra", "/var/run/xpra", "/tmp") + if ROOT and uid == 0 and not any(session_dir.startswith(x) for x in ROOT_FALLBACK): + # there is usually no $XDG_RUNTIME_DIR when running as root + # and even if there was, that's probably not a good path to use, + # so try to find a more suitable directory we can use: + for d in ROOT_FALLBACK: + if os.path.exists(d): + if mode == "proxy" and (display_name or "").lstrip(":").split(",")[0] == "14500": + # stash the system-wide proxy session files in a 'proxy' subdirectory: + return os.path.join(d, "proxy") + # otherwise just use the display as subdirectory name: + return os.path.join(d, (display_name or "").lstrip(":")) + return session_dir + + +def make_session_dir(mode: str, sessions_dir: str, display_name: str, uid: int = 0, gid: int = 0) -> str: + session_dir = get_session_dir(mode, sessions_dir, display_name, uid) + if not os.path.exists(session_dir): + try: + os.makedirs(session_dir, 0o750, exist_ok=True) + except OSError: + import tempfile + session_dir = osexpand(os.path.join(tempfile.gettempdir(), display_name.lstrip(":"))) + os.makedirs(session_dir, 0o750, exist_ok=True) + ROOT = POSIX and getuid() == 0 + mismatch = ROOT and uid != 0 or gid != 0 + if mismatch and (session_dir.startswith("/run/user/") or session_dir.startswith("/run/xpra/")): + os.lchown(session_dir, uid, gid) + return session_dir + + +def session_file_path(filename: str) -> str: + session_dir = os.environ.get("XPRA_SESSION_DIR") + if session_dir is None: + raise RuntimeError("'XPRA_SESSION_DIR' must be set to use this function") + return os.path.join(session_dir, filename) + + +def load_session_file(filename: str) -> bytes: + return load_binary_file(session_file_path(filename)) + + +def save_session_file(filename: str, contents: str | bytes, uid: int = -1, gid: int = -1) -> str: + if not os.environ.get("XPRA_SESSION_DIR"): + return "" + if not isinstance(contents, bytes): + contents = str(contents).encode("utf8") + assert contents + path = session_file_path(filename) + try: + with open(path, "wb+") as f: + if POSIX: + os.fchmod(f.fileno(), 0o640) + if getuid() == 0 and uid >= 0 and gid >= 0: + os.fchown(f.fileno(), uid, gid) + f.write(contents) + except OSError as e: + from xpra.log import Logger + log = Logger("server") + log("save_session_file", exc_info=True) + log.error(f"Error saving session file {path!r}") + log.estr(e) + return path + + +def rm_session_dir(warn: bool = True) -> None: + session_dir = os.environ.get("XPRA_SESSION_DIR") + if not session_dir or not os.path.exists(session_dir): + return + from xpra.log import Logger + log = Logger("server") + try: + session_files = os.listdir(session_dir) + except OSError as e: + log("os.listdir(%s)", session_dir, exc_info=True) + if warn: + log.error(f"Error: cannot access {session_dir!r}") + log.estr(e) + return + if session_files: + if warn: + log.info(f"session directory {session_dir!r} was not removed") + log.info(" because it still contains some files:") + for f in session_files: + extra = " (directory)" if os.path.isdir(os.path.join(session_dir, f)) else "" + log.info(f" {f!r}{extra}") + return + try: + os.rmdir(session_dir) + except OSError as e: + log = Logger("server") + log(f"rmdir({session_dir})", exc_info=True) + log.error(f"Error: failed to remove session directory {session_dir!r}") + log.estr(e) + + +def clean_session_files(*filenames) -> None: + if not CLEAN_SESSION_FILES: + return + for filename in filenames: + path = session_file_path(filename) + if filename.find("*") >= 0: + for p in glob.glob(path): + clean_session_path(p) + else: + clean_session_path(path) + rm_session_dir(False) + + +def clean_session_path(path) -> None: + from xpra.log import Logger + log = Logger("server") + log(f"clean_session_path({path})") + if os.path.exists(path): + try: + if os.path.isdir(path): + os.rmdir(path) + else: + os.unlink(path) + except OSError as e: + log(f"clean_session_path({path})", exc_info=True) + log.error(f"Error removing session path {path}") + log.estr(e) + if os.path.isdir(path): + files = os.listdir(path) + if files: + log.error(" this directory still contains some files:") + for file in files: + finfo = repr(file) + try: + if os.path.islink(file): + finfo += " -> "+repr(os.readlink(file)) + except OSError: + pass + log.error(f" {finfo}") diff --git a/xpra/server/core.py b/xpra/server/core.py index 8379e94973..aa07da1533 100644 --- a/xpra/server/core.py +++ b/xpra/server/core.py @@ -24,7 +24,8 @@ XPRA_VERSION, XPRA_NUMERIC_VERSION, vparts, version_str, full_version_str, version_compat_check, get_version_info, get_platform_info, get_host_info, parse_version, ) -from xpra.scripts.server import deadly_signal, clean_session_files, rm_session_dir +from xpra.scripts.server import deadly_signal +from xpra.scripts.session import rm_session_dir, clean_session_files from xpra.exit_codes import ExitValue, ExitCode from xpra.server import ServerExitMode from xpra.server import features diff --git a/xpra/server/mixins/audio.py b/xpra/server/mixins/audio.py index 5281942c9e..6a462b663b 100644 --- a/xpra/server/mixins/audio.py +++ b/xpra/server/mixins/audio.py @@ -22,7 +22,7 @@ from xpra.platform.info import get_username from xpra.platform.paths import get_icon_filename from xpra.scripts.parsing import audio_option -from xpra.scripts.server import save_session_file +from xpra.scripts.session import save_session_file from xpra.server.mixins.stub_server_mixin import StubServerMixin from xpra.log import Logger diff --git a/xpra/x11/vfb_util.py b/xpra/x11/vfb_util.py index 4c0d92f6b5..462a6da7b7 100644 --- a/xpra/x11/vfb_util.py +++ b/xpra/x11/vfb_util.py @@ -190,6 +190,20 @@ def get_xauthority_path(display_name: str) -> str: return os.path.join(d, filename) +def get_xvfb_env(xvfb_executable: str) -> dict[str, str]: + keep = [ + "SHELL", "HOSTNAME", "XMODIFIERS", + "PWD", "HOME", "USERNAME", "LANG", "TERM", "USER", + "XDG_RUNTIME_DIR", "XDG_DATA_DIR", "PATH", + "XAUTHORITY", + "XPRA_SESSION_DIR", + ] + env = get_exec_env(keep=keep) + if xvfb_executable.endswith("Xephyr"): + env["DISPLAY"] = get_saved_env_var("DISPLAY") + return env + + 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: @@ -198,12 +212,14 @@ def start_Xvfb(xvfb_cmd: list[str], vfb_geom, pixel_depth: int, display_name: st raise InitException("starting an Xvfb is not supported on MacOS") if not xvfb_cmd: raise InitException("the 'xvfb' command is not defined") + if not display_name: + raise ValueError("no display name") log = get_vfb_logger() log("start_Xvfb%s XVFB_EXTRA_ARGS=%s", (xvfb_cmd, vfb_geom, pixel_depth, display_name, cwd, uid, gid, username, uinput_uuid), XVFB_EXTRA_ARGS) - use_display_fd = display_name[0] == 'S' + use_display_fd = display_name[0] == "S" if XVFB_EXTRA_ARGS: xvfb_cmd += shlex.split(XVFB_EXTRA_ARGS) @@ -226,7 +242,7 @@ def pathexpand(s: str) -> str: xvfb_cmd = [pathexpand(s) for s in xvfb_cmd] # try to honour initial geometries if specified: - if vfb_geom and xvfb_cmd[0].endswith("Xvfb"): + if vfb_geom and xvfb_cmd[0].endswith("Xvfb") or xvfb_cmd[0].endswith("Xephyr"): # find the '-screen' arguments: # "-screen 0 8192x4096x24+32" try: @@ -292,14 +308,7 @@ 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)) - 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") + env = get_xvfb_env(xvfb_executable) log(f"xvfb env={env}") xvfb = None try: @@ -383,6 +392,27 @@ def preexec() -> None: return xvfb, display_name +def start_xvfb_standalone(xvfb_cmd: list[str], sessions_dir: str, pixel_depth=0, display_name="") -> int: + # we may have to tweak the environment to emulate having an xpra session + # as the default xvfb commands may refer to $XAUTHORITY and $XPRA_SESSION_DIR + xauthority = os.environ.get("XAUTHORITY", "") + if "$XAUTHORITY" in xvfb_cmd and not valid_xauth(xauthority): + xauthority = osexpand(get_xauthority_path(display_name)) + os.environ["XAUTHORITY"] = xauthority + if "$XPRA_SESSION_DIR" in xvfb_cmd and "XPRA_SESSION_DIR" not in os.environ: + from xpra.scripts.session import make_session_dir + session_dir = make_session_dir("xvfb", sessions_dir, display_name) + os.environ["XPRA_SESSION_DIR"] = session_dir + vfb_geom = () + cwd = os.getcwd() + xvfb, actual_display_name = start_Xvfb(xvfb_cmd, vfb_geom, pixel_depth, display_name, + cwd, uid=getuid(), gid=getgid(), username="", uinput_uuid="") + if actual_display_name != display_name: + print(f"display {actual_display_name!r} started") + print(f"xvfb pid: {xvfb.pid}") + return xvfb.wait() + + def kill_xvfb(xvfb_pid: int) -> None: log = get_vfb_logger() log.info("killing xvfb with pid %s", xvfb_pid) @@ -391,7 +421,7 @@ def kill_xvfb(xvfb_pid: int) -> None: except OSError as e: log.info("failed to kill xvfb process with pid %s:", xvfb_pid) log.info(" %s", e) - xauthority = os.environ.get("XAUTHORITY") + xauthority = os.environ.get("XAUTHORITY", "") if PRIVATE_XAUTH and xauthority and os.path.exists(xauthority): os.unlink(xauthority)