diff --git a/src/etc/xpra/xpra.conf.in b/src/etc/xpra/xpra.conf.in index 072b0319cc..ace4ada3cc 100644 --- a/src/etc/xpra/xpra.conf.in +++ b/src/etc/xpra/xpra.conf.in @@ -518,3 +518,15 @@ xvfb = %(xvfb_command)s # Does the xvfb command support the "-displayfd" argument? displayfd = %(has_displayfd)s + +# Nested display command: +# xnest = Xnest \ +# -geometry 1600x1200+100+100 \ +# -dpi 96 -nolisten tcp -noreset \ +# -auth $XAUTHORITY +# xnest = Xephyr \ +# +extension Composite \ +# -screen 1600x1200x24+32 \ +# -dpi 96 -nolisten tcp -noreset \ +# -auth $XAUTHORITY +xnest = %(xnest_command)s diff --git a/src/man/xpra.1 b/src/man/xpra.1 index 528ad06279..cdc7f55ca7 100644 --- a/src/man/xpra.1 +++ b/src/man/xpra.1 @@ -14,7 +14,8 @@ xpra \- viewer for remote, persistent X applications .SH SYNOPSIS .PD 0 .HP \w'xpra\ 'u -\fBxpra\fP \fBstart\fP [\fI:DISPLAY\fP] | \fBxpra\fP \fBstart\fP \fIssh:HOST:DISPLAY\fP +\fBxpra\fP \fBstart\fP [\fI:DISPLAY\fP] | \fBxpra\fP \fBstart\fP \fIssh:HOST:DISPLAY\fP | +\fBxpra\fP \fBstart-desktop\fP [\fI:DISPLAY\fP] | \fBxpra\fP \fBstart-desktop\fP \fIssh:HOST:DISPLAY\fP [\fB\-\-start\fP=\fICOMMAND\fP]\fB .\|.\|.\fP [\fB\-\-start\-child\fP=\fICOMMAND\fP]\fB .\|.\|.\fP [\fB\-\-start\-after\-connect\fP=\fIyes\fP|\fIno\fP] @@ -245,6 +246,9 @@ Start an xpra server using display number \fI:7\fP. \fBxpra start\fP \fIssh:bigbox:7 \-\-start=xterm\fP Start an xpra server on \fIbigbox\fP with an xterm in it, and connect to it. +\fBxpra start-desktop \-\-start=xfce4-session\fP +Start an xfce session in a nested X11 server on an automatically +assigned display number. .TP \fBDISPLAY=\fP\fI:7 firefox\fP Start \fIfirefox\fP running inside the xpra server. Run this on the host @@ -349,6 +353,9 @@ number.) This command starts a new xpra server, including any necessary setup. (When starting a remote server with the \fBssh:HOST:DISPLAY\fP syntax, the new session will also be attached.) +.SS xpra start-desktop +Starts a nested X11 server, all child commands will be started in the +nested X11 server. .SS xpra attach This command attaches to a running xpra server, and forwards any applications using that server to appear on your current screen. diff --git a/src/xpra/client/gtk_base/client_launcher.py b/src/xpra/client/gtk_base/client_launcher.py index 8736fd010b..254e861df4 100755 --- a/src/xpra/client/gtk_base/client_launcher.py +++ b/src/xpra/client/gtk_base/client_launcher.py @@ -698,8 +698,28 @@ def destroy(self, *args): self.window = None gtk.main_quit() + def update_options_from_URL(self, url): + from xpra.scripts.main import parse_URL + address, props = parse_URL(url) + pa = address.split(":") + if pa[0] in ("tcp", "ssh") and len(pa)>=2: + props["mode"] = pa + host = pa[1] + ph = host.split("@", 1) + if len(ph)==2: + username, host = ph + props["username"] = username + props["host"] = host + if len(pa)>=3: + props["port"] = pa[2] + self._apply_props(props) + def update_options_from_file(self, filename): + log("update_options_from_file(%s)", filename) props = read_config(filename) + self._apply_props(props) + + def _apply_props(self, props): #we rely on "ssh_port" being defined on the config object #so try to load it from file, and define it if not present: options = validate_config(props, extras_types=LAUNCHER_OPTION_TYPES, extras_validation=self.get_launcher_validation()) @@ -707,7 +727,7 @@ def update_options_from_file(self, filename): fn = k.replace("-", "_") setattr(self.config, fn, v) self.config_keys = self.config_keys.union(set(props.keys())) - log("update_options_from_file(%s) populated config with keys '%s', ssh=%s", filename, options.keys(), self.config.ssh) + log("_apply_props(%s) populated config with keys '%s', ssh=%s", props, options.keys(), self.config.ssh) def choose_session_file(self, title, action, action_button, callback): file_filter = gtk.FileFilter() @@ -816,9 +836,9 @@ def show_signal(): #maybe we should always run this code from the main loop instead if sys.platform.startswith("darwin"): #wait a little bit for the "openFile" signal - app.__osx_open_file = False + app.__osx_open_signal = False def do_open_file(filename): - app.__osx_open_file = True + app.__osx_open_signal = True app.update_options_from_file(filename) #the compressors and packet encoders cannot be changed from the UI #so apply them now: @@ -829,11 +849,24 @@ def do_open_file(filename): def open_file(_, filename): log("open_file(%s)", filename) glib.idle_add(do_open_file, filename) + def do_open_URL(url): + app.__osx_open_signal = True + app.update_options_from_URL(url) + #the compressors and packet encoders cannot be changed from the UI + #so apply them now: + configure_network(app.config) + app.update_gui_from_config() + if app.config.autoconnect: + glib.idle_add(app.do_connect) + def open_URL(_, url): + log("open_URL(%s)", url) + glib.idle_add(do_open_URL, url) from xpra.platform.darwin.gui import get_OSXApplication + get_OSXApplication().connect("NSApplicationOpenURL", open_URL) get_OSXApplication().connect("NSApplicationOpenFile", open_file) def may_show(): - log("may_show() osx open file=%s", app.__osx_open_file) - if not app.__osx_open_file: + log("may_show() osx open file=%s", app.__osx_open_signal) + if not app.__osx_open_signal: app.show() glib.timeout_add(500, may_show) else: diff --git a/src/xpra/scripts/config.py b/src/xpra/scripts/config.py index 477e0994e1..0826dabdac 100755 --- a/src/xpra/scripts/config.py +++ b/src/xpra/scripts/config.py @@ -82,6 +82,18 @@ def get_Xvfb_command(): ] return cmd +def get_Xephyr_command(): + cmd = ["Xephyr", + "+extension", "Composite", + "-screen", "1600x1200x24+32", + #better than leaving to vfb after a resize? + "-dpi", "96", + "-nolisten", "tcp", + "-noreset", + "-auth", "$XAUTHORITY" + ] + return cmd + def OpenGL_safety_check(): #Ubuntu 12.04 will just crash on you if you try: @@ -303,6 +315,7 @@ def read_xpra_defaults(): "mode" : str, "ssh" : str, "xvfb" : str, + "xnest" : str, "socket-dir" : str, "mmap" : str, "log-dir" : str, @@ -446,8 +459,10 @@ def get_defaults(): username = get_username() except: username = "" + xnest = get_Xephyr_command() if WIN32 or PYTHON3: xvfb = "" + xnest = "" elif XDUMMY: xvfb = get_Xdummy_command(use_wrapper=XDUMMY_WRAPPER, log_dir=get_default_log_dir()) else: @@ -485,6 +500,7 @@ def addtrailingslash(v): "tcp-encryption-keyfile": "", "ssh" : DEFAULT_SSH_COMMAND, "xvfb" : " ".join(xvfb), + "xnest" : " ".join(xnest), "socket-dir" : "", "log-dir" : get_default_log_dir(), "log-file" : "$DISPLAY.log", diff --git a/src/xpra/scripts/main.py b/src/xpra/scripts/main.py index 830e3fa692..4a0a7b24b2 100755 --- a/src/xpra/scripts/main.py +++ b/src/xpra/scripts/main.py @@ -227,6 +227,7 @@ def do_parse_cmdline(cmdline, defaults): server_modes = [] if supports_server: server_modes.append("start") + server_modes.append("start-desktop") server_modes.append("upgrade") #display: default to required dstr = " DISPLAY" @@ -234,6 +235,7 @@ def do_parse_cmdline(cmdline, defaults): #display argument is optional (we can use "-displayfd") dstr = " [DISPLAY]" command_options = ["\t%prog start"+dstr+"\n", + "\t%prog start-desktop"+dstr+"\n", "\t%prog stop [DISPLAY]\n", "\t%prog exit [DISPLAY]\n", "\t%prog list\n", @@ -386,9 +388,15 @@ def ignore(defaults): dest="fake_xinerama", default=defaults.fake_xinerama, help="Setup fake xinerama support for the session. Default: %s." % enabled_str(defaults.fake_xinerama)) + group.add_option("--xnest", action="store", + dest="xnest", + default=defaults.xnest, + metavar="CMD", + help="How to run the nested X server. Default: '%default'.") else: ignore({"use-display" : False, "xvfb" : '', + "xnest" : '', "fake-xinerama" : defaults.fake_xinerama}) group.add_option("--resize-display", action="store", dest="resize_display", default=defaults.resize_display, metavar="yes|no", @@ -927,7 +935,7 @@ def configure_logging(options, mode): to = sys.stderr if mode in ("showconfig", "info", "control", "list", "attach", "stop", "version", "print", "opengl"): to = sys.stdout - if mode in ("start", "upgrade", "attach", "shadow", "proxy", "_sound_record", "_sound_play", "stop", "print", "showconfig"): + if mode in ("start", "start-desktop", "upgrade", "attach", "shadow", "proxy", "_sound_record", "_sound_play", "stop", "print", "showconfig"): if "help" in options.speaker_codec or "help" in options.microphone_codec: info = show_sound_codec_help(mode!="attach", options.speaker_codec, options.microphone_codec) raise InitInfo("\n".join(info)) @@ -999,16 +1007,16 @@ def run_mode(script_file, error_cb, options, args, mode, defaults): #sound commands don't want to set the name #(they do it later to prevent glib import conflicts) #"attach" does it when it received the session name from the server - if mode not in ("attach", "start", "upgrade", "proxy", "shadow"): + if mode not in ("attach", "start", "start-desktop", "upgrade", "proxy", "shadow"): from xpra.platform import set_name set_name("Xpra", "Xpra %s" % mode.strip("_")) try: ssh_display = len(args)>0 and (args[0].startswith("ssh/") or args[0].startswith("ssh:")) - if mode in ("start", "shadow") and ssh_display: + if mode in ("start", "start-desktop", "shadow") and ssh_display: #ie: "xpra start ssh:HOST:DISPLAY --start-child=xterm" return run_remote_server(error_cb, options, args, mode, defaults) - elif (mode in ("start", "upgrade", "proxy") and supports_server) or (mode=="shadow" and supports_shadow): + elif (mode in ("start", "start-desktop", "upgrade", "proxy") and supports_server) or (mode=="shadow" and supports_shadow): current_display = nox() from xpra.scripts.server import run_server return run_server(error_cb, options, mode, script_file, args, current_display) @@ -1019,7 +1027,7 @@ def run_mode(script_file, error_cb, options, args, mode, defaults): return run_stopexit(mode, error_cb, options, args) elif mode == "list" and (supports_server or supports_shadow): return run_list(error_cb, options, args) - elif mode in ("_proxy", "_proxy_start", "_shadow_start") and (supports_server or supports_shadow): + elif mode in ("_proxy", "_proxy_start", "_proxy_start_desktop", "_shadow_start") and (supports_server or supports_shadow): nox() return run_proxy(error_cb, options, script_file, args, mode, defaults) elif mode in ("_sound_record", "_sound_play", "_sound_query"): @@ -1643,9 +1651,10 @@ def run_remote_server(error_cb, opts, args, mode, defaults): #and use _proxy_start subcommand: if mode=="shadow": params["proxy_command"] = ["_shadow_start"] - else: - assert mode=="start" + elif mode=="start": params["proxy_command"] = ["_proxy_start"] + elif mode=="start-desktop": + params["proxy_command"] = ["_proxy_start_desktop"] app = make_client(error_cb, opts) app.init(opts) app.init_ui(opts) @@ -1726,12 +1735,15 @@ def run_glcheck(opts): def run_proxy(error_cb, opts, script_file, args, mode, defaults): from xpra.scripts.fdproxy import XpraProxy no_gtk() - if mode in ("_proxy_start", "_shadow_start"): + if mode in ("_proxy_start", "_proxy_start_desktop", "_shadow_start"): dotxpra = DotXpra(opts.socket_dir, opts.socket_dirs) #we must use a subprocess to avoid messing things up - yuk cmd = [script_file] - if mode=="_proxy_start": - cmd.append("start") + if mode in ("_proxy_start", "_proxy_start_desktop"): + if mode=="start": + cmd.append("start") + else: + cmd.append("start-desktop") if len(args)==1: display_name = args[0] elif len(args)==0: @@ -1741,7 +1753,7 @@ def run_proxy(error_cb, opts, script_file, args, mode, defaults): display_name = 'S' + str(os.getpid()) existing_sockets = set(dotxpra.sockets(matching_state=dotxpra.LIVE)) else: - raise InitException("_proxy_start: expected 0 or 1 arguments but got %s: %s" % (len(args), args)) + raise InitException("%s: expected 0 or 1 arguments but got %s: %s" % (mode, len(args), args)) else: assert mode=="_shadow_start" assert len(args) in (0, 1), "_shadow_start: expected 0 or 1 arguments but got %s: %s" % (len(args), args) diff --git a/src/xpra/scripts/server.py b/src/xpra/scripts/server.py index 40cfeb5bc9..3215ade7f8 100644 --- a/src/xpra/scripts/server.py +++ b/src/xpra/scripts/server.py @@ -674,6 +674,7 @@ def start_Xvfb(xvfb_str, display_name, cwd): except Exception as e: #trying to continue anyway! sys.stderr.write("Error trying to create XAUTHORITY file %s: %s\n" % (xauthority, e)) + use_display_fd = display_name[0]=='S' #identify logfile argument if it exists, #as we may have to rename it, or create the directory for it: @@ -685,7 +686,7 @@ def start_Xvfb(xvfb_str, display_name, cwd): assert logfile_argindex+10: - if display_name[0]=='S': + if use_display_fd: #keep track of it so we can rename it later: tmp_xorg_log_file = xvfb_cmd[logfile_argindex+1] #make sure the Xorg log directory exists: @@ -714,7 +715,9 @@ def setsid(): if not xvfb_cmd: raise InitException("cannot start Xvfb, the command definition is missing!") xvfb_executable = xvfb_cmd[0] - if display_name[0]=='S': + sys.stdout.write("running %s\n" % xvfb_cmd) + sys.stdout.flush() + if use_display_fd: # 'S' means that we allocate the display automatically r_pipe, w_pipe = os.pipe() xvfb_cmd += ["-displayfd", str(w_pipe)] @@ -723,7 +726,7 @@ def preexec(): setsid() close_fds([0, 1, 2, r_pipe, w_pipe]) xvfb = subprocess.Popen(xvfb_cmd, executable=xvfb_executable, close_fds=False, - stdin=subprocess.PIPE, preexec_fn=preexec) + stdin=subprocess.PIPE, preexec_fn=preexec, cwd=cwd) # Read the display number from the pipe we gave to Xvfb # waiting up to 10 seconds for it to show up limit = time.time()+10 @@ -767,7 +770,10 @@ def preexec(): xvfb_cmd.append(display_name) xvfb = subprocess.Popen(xvfb_cmd, executable=xvfb_executable, close_fds=True, stdin=subprocess.PIPE, preexec_fn=setsid) + xauth_add(display_name) + return xvfb, display_name +def xauth_add(display_name): from xpra.os_util import get_hex_uuid xauth_cmd = ["xauth", "add", display_name, "MIT-MAGIC-COOKIE-1", get_hex_uuid()] try: @@ -777,9 +783,8 @@ def preexec(): except OSError as e: #trying to continue anyway! sys.stderr.write("Error running \"%s\": %s\n" % (" ".join(xauth_cmd), e)) - return xvfb, display_name -def check_xvfb_process(xvfb=None): +def check_xvfb_process(xvfb=None, cmd="Xvfb"): if xvfb is None: #we don't have a process to check return True @@ -789,23 +794,35 @@ def check_xvfb_process(xvfb=None): from xpra.log import Logger log = Logger("server") log.error("") - log.error("Xvfb command has terminated! xpra cannot continue") + 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("") return False -def verify_display_ready(xvfb, display_name, shadowing): - from xpra.log import Logger - log = Logger("server") +def verify_display_ready(xvfb, display_name, shadowing_check=True): from xpra.x11.bindings.wait_for_x_server import wait_for_x_server #@UnresolvedImport # Whether we spawned our server or not, it is now running -- or at least # starting. First wait for it to start up: try: wait_for_x_server(display_name, 3) # 3s timeout except Exception as e: + sys.stderr.write("display %s failed:\n" % display_name) sys.stderr.write("%s\n" % e) - return None + return False + if shadowing_check and not check_xvfb_process(xvfb): + #if we're here, there is an X11 server, but it isn't the one we started! + from xpra.log import Logger + log = Logger("server") + log.error("There is an X11 server already running on display %s:" % display_name) + log.error("You may want to use:") + log.error(" 'xpra upgrade %s' if an instance of xpra is still connected to it" % display_name) + log.error(" 'xpra --use-display start %s' to connect xpra to an existing X11 server only" % display_name) + log.error("") + return False + return True + +def verify_gdk_display(display_name): # Now we can safely load gtk and connect: no_gtk() import gtk.gdk #@Reimport @@ -821,14 +838,6 @@ def verify_display_ready(xvfb, display_name, shadowing): if default_display is not None: default_display.close() manager.set_default_display(display) - if not shadowing and not check_xvfb_process(xvfb): - #if we're here, there is an X11 server, but it isn't the one we started! - log.error("There is an X11 server already running on display %s:" % display_name) - log.error("You may want to use:") - log.error(" 'xpra upgrade %s' if an instance of xpra is still connected to it" % display_name) - log.error(" 'xpra --use-display start %s' to connect xpra to an existing X11 server only" % display_name) - log.error("") - return None return display def guess_xpra_display(socket_dir, socket_dirs): @@ -907,8 +916,9 @@ def run_server(error_cb, opts, mode, xpra_file, extra_args, desktop_display=None bind_tcp = parse_bind_tcp(opts.bind_tcp) bind_vsock = parse_bind_vsock(opts.bind_vsock) - assert mode in ("start", "upgrade", "shadow", "proxy") + assert mode in ("start", "start-desktop", "upgrade", "shadow", "proxy") starting = mode == "start" + starting_desktop = mode == "start-desktop" upgrading = mode == "upgrade" shadowing = mode == "shadow" proxying = mode == "proxy" @@ -998,7 +1008,7 @@ def run_server(error_cb, opts, mode, xpra_file, extra_args, desktop_display=None log = Logger("server") #warn early about this: - if starting and desktop_display: + if (starting or starting_desktop) and desktop_display: de = os.environ.get("XDG_SESSION_DESKTOP") or os.environ.get("SESSION_DESKTOP") if de: warn = [] @@ -1043,6 +1053,11 @@ def run_server(error_cb, opts, mode, xpra_file, extra_args, desktop_display=None # Do this after writing out the shell script: if display_name[0] != 'S': os.environ["DISPLAY"] = display_name + else: + try: + del os.environ["DISPLAY"] + except: + pass sanitize_env() os.environ["XDG_CURRENT_DESKTOP"] = opts.wm_name configure_imsettings_env(opts.input_method) @@ -1062,6 +1077,20 @@ def run_server(error_cb, opts, mode, xpra_file, extra_args, desktop_display=None #always update as we may now have the "real" display name: os.environ["DISPLAY"] = display_name + child_display = display_name + nested = None + if starting_desktop: + nested_cmd = opts.xnest + try: + #will allocate a new display automatically: + child_display = 'SN' + str(os.getpid()) + nested, child_display = start_Xvfb(nested_cmd, child_display, cwd) + except OSError as e: + log.error("Error starting '%s':", nested_cmd) + log.error(" %s", e) + log("start_Xvfb error", exc_info=True) + return 1 + if opts.daemon: log_filename1 = select_log_file(log_dir, opts.log_file, display_name) if log_filename0 != log_filename1: @@ -1074,10 +1103,17 @@ def run_server(error_cb, opts, mode, xpra_file, extra_args, desktop_display=None if not check_xvfb_process(xvfb): #xvfb problem: exit now return 1 + if nested and not check_xvfb_process(nested): + #nested display problem: exit now + return 1 display = None - if not sys.platform.startswith("win") and not sys.platform.startswith("darwin") and not proxying: - display = verify_display_ready(xvfb, display_name, shadowing) + if start_vfb: + if not verify_display_ready(xvfb, display_name, shadowing): + return 1 + if nested and not verify_display_ready(nested, child_display, False): + return 1 + display = verify_gdk_display(display_name) if not display: return 1 elif not proxying: @@ -1106,7 +1142,7 @@ def run_server(error_cb, opts, mode, xpra_file, extra_args, desktop_display=None app = ProxyServer() info = "proxy" else: - assert starting or upgrading + assert starting or starting_desktop or upgrading from xpra.x11.gtk2 import gdk_display_source assert gdk_display_source #(now we can access the X11 server) @@ -1117,7 +1153,7 @@ def run_server(error_cb, opts, mode, xpra_file, extra_args, desktop_display=None dbus_pid = get_dbus_pid() dbus_env = get_dbus_env() else: - assert starting + assert starting or starting_desktop if xvfb_pid is not None: #save the new pid (we should have one): save_xvfb_pid(xvfb_pid) @@ -1195,12 +1231,19 @@ def kill_dbus(): #the server should be able to manage the display #from now on, if we exit without upgrading we will also kill the Xvfb def kill_xvfb(): + if nested and nested.poll() is None: + try: + log.info("killing nested X11 server with pid %s" % nested.pid) + os.kill(nested.pid, signal.SIGTERM) + except: + pass # Close our display(s) first, so the server dying won't kill us. log.info("killing xvfb with pid %s" % xvfb_pid) import gtk #@Reimport for display in gtk.gdk.display_manager_get().list_displays(): display.close() os.kill(xvfb_pid, signal.SIGTERM) + if xvfb_pid is not None and not opts.use_display and not shadowing: _cleanups.append(kill_xvfb) @@ -1209,6 +1252,9 @@ def kill_xvfb(): app.exec_cwd = cwd app.init(opts) app.init_components(opts) + if nested: + app.add_process(nested, "nested display", nested_cmd, True) + app.child_display = child_display except InitException as e: log.error("xpra server initialization error:") log.error(" %s", e) diff --git a/src/xpra/server/server_base.py b/src/xpra/server/server_base.py index 388908b30f..0e53f7fff3 100644 --- a/src/xpra/server/server_base.py +++ b/src/xpra/server/server_base.py @@ -132,6 +132,7 @@ def __init__(self): self.lpadmin = "" self.lpinfo = "" #starting child commands: + self.child_display = None self.start_commands = [] self.start_child_commands = [] self.start_after_connect = [] @@ -768,6 +769,8 @@ def get_child_env(self): #subclasses may add more items (ie: fakexinerama) env = os.environ.copy() env.update(self.env) + if self.child_display: + env["DISPLAY"] = self.child_display return env def get_full_child_command(self, cmd, use_wrapper=True): diff --git a/src/xpra/x11/gtk2/gdk_display_source.pyx b/src/xpra/x11/gtk2/gdk_display_source.pyx index d49469973d..38ba7b04ba 100644 --- a/src/xpra/x11/gtk2/gdk_display_source.pyx +++ b/src/xpra/x11/gtk2/gdk_display_source.pyx @@ -74,7 +74,7 @@ def init_gdk_display_source(): cdef Display * x11_display if not gtk.gdk.display_get_default(): from xpra.scripts.config import InitException - raise InitException("cannot access the display") + raise InitException("cannot access the default display") root_window = gtk.gdk.get_default_root_window() assert root_window, "cannot get the root window" display = root_window.get_display()