class SessionsGUI(gtk.Window): def __init__(self, options, title="Xpra Session Browser"): gtk.Window.__init__(self) self.exit_code = 0 self.set_title(title) self.set_border_width(20) self.set_resizable(True) self.set_decorated(True) self.set_position(WIN_POS_CENTER) icon = self.get_pixbuf("xpra") if icon: self.set_icon(icon) add_close_accel(self, self.quit) self.connect("delete_event", self.quit) self.clients = {} self.clients_disconnecting = set() self.child_reaper = getChildReaper() self.vbox = gtk.VBox(False, 20) self.add(self.vbox) title_label = gtk.Label(title) title_label.modify_font(pango.FontDescription("sans 14")) title_label.show() self.vbox.add(title_label) self.warning = gtk.Label(" ") red = color_parse("red") self.warning.modify_fg(STATE_NORMAL, red) self.warning.show() self.vbox.add(self.warning) hbox = gtk.HBox(False, 10) self.password_label = gtk.Label("Password:"******"" #log.info("options=%s (%s)", options, type(options)) self.local_info_cache = {} self.dotxpra = DotXpra(options.socket_dir, options.socket_dirs, username) self.poll_local_sessions() self.populate() glib.timeout_add(5*1000, self.update) self.vbox.show() self.show() def quit(self, *args): log("quit%s", args) self.do_quit() def do_quit(self): log("do_quit()") gtk.main_quit() def app_signal(self, signum, frame): self.exit_code = 128 + signum log("app_signal(%s, %s) exit_code=%i", signum, frame, self.exit_code) self.do_quit() def update(self): if self.poll_local_sessions(): self.populate() return True def populate(self): if self.local_info_cache: self.password_entry.show() self.password_label.show() else: self.password_entry.hide() self.password_label.hide() self.populate_table() def poll_local_sessions(self): #TODO: run in a thread so we don't block the UI thread! d = self.dotxpra.socket_details(matching_state=DotXpra.LIVE) log("poll_local_sessions() socket_details=%s", d) info_cache = {} for d, details in d.items(): log("poll_local_sessions() %s : %s", d, details) for state, display, sockpath in details: assert state==DotXpra.LIVE key = (display, sockpath) info = self.local_info_cache.get(key) if not info: #try to query it try: info = self.get_session_info(sockpath) except Exception as e: log("get_session_info(%s)", sockpath, exc_info=True) log.error("Error querying session info for %s", sockpath) log.error(" %s", e) del e if not info: continue #log("info(%s)=%s", sockpath, repr_ellipsized(str(info))) info_cache[key] = info def make_text(info): text = {"mode" : "socket"} for k, name in { "platform" : "platform", "uuid" : "uuid", "display" : "display", "session-type" : "type", "session-name" : "name", }.items(): v = info.get(k) if v is not None: text[name] = v return text #first remove any records that are no longer found: for key in self.local_info_cache.keys(): if key not in info_cache: display, sockpath = key self.records = [(interface, protocol, name, stype, domain, host, address, port, text) for (interface, protocol, name, stype, domain, host, address, port, text) in self.records if (protocol!="socket" or domain!="local" or address!=sockpath)] #add the new ones: for key, info in info_cache.items(): if key not in self.local_info_cache: display, sockpath = key self.records.append(("", "socket", "", "", "local", socket.gethostname(), sockpath, 0, make_text(info))) log("poll_local_sessions() info_cache=%s", info_cache) changed = self.local_info_cache!=info_cache self.local_info_cache = info_cache return changed def get_session_info(self, sockpath): #the lazy way using a subprocess cmd = get_nodock_command()+["id", "socket:%s" % sockpath] p = subprocess.Popen(cmd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, _ = p.communicate() log("get_sessions_info(%s) returncode(%s)=%s", sockpath, cmd, p.returncode) if p.returncode!=0: return None out = bytestostr(stdout) info = {} for line in out.splitlines(): parts = line.split("=", 1) if len(parts)==2: info[parts[0]] = parts[1] log("get_sessions_info(%s)=%s", sockpath, info) return info def populate_table(self): log("populate_table: %i records", len(self.records)) if self.table: self.vbox.remove(self.table) self.table = None if not self.records: self.table = gtk.Label("No sessions found") self.vbox.add(self.table) self.table.show() return tb = TableBuilder(1, 6, False) tb.add_row(gtk.Label("Host"), gtk.Label("Display"), gtk.Label("Name"), gtk.Label("Platform"), gtk.Label("Type"), gtk.Label("URI"), gtk.Label("Connect"), gtk.Label("Open in Browser")) self.table = tb.get_table() self.vbox.add(self.table) self.table.resize(1+len(self.records), 5) #group them by uuid d = OrderedDict() for i, record in enumerate(self.records): interface, protocol, name, stype, domain, host, address, port, text = record td = typedict(text) log("populate_table: record[%i]=%s", i, record) uuid = td.strget("uuid", "") display = td.strget("display", "") platform = td.strget("platform", "") dtype = td.strget("type", "") #older servers expose the "session-name" as "session": session_name = td.strget("name", "") or td.strget("session", "") if domain=="local" and host.endswith(".local"): host = host[:-len(".local")] key = (uuid, uuid or i, host, display, session_name, platform, dtype) log("populate_table: key[%i]=%s", i, key) d.setdefault(key, []).append((interface, protocol, name, stype, domain, host, address, port, text)) for key, recs in d.items(): if type(key)==tuple: uuid, _, host, display, name, platform, dtype = key else: display = key uuid, host, name, platform, dtype = None, None, "", sys.platform, None title = uuid if display: title = display label = gtk.Label(title) if uuid!=title: label.set_tooltip_text(uuid) #try to use an icon for the platform: platform_icon_name = self.get_platform_icon_name(platform) pwidget = None if platform_icon_name: pwidget = scaled_image(self.get_pixbuf("%s.png" % platform_icon_name), 28) if pwidget: pwidget.set_tooltip_text(platform_icon_name) if not pwidget: pwidget = gtk.Label(platform) w, c, b = self.make_connect_widgets(key, recs, address, port, display) tb.add_row(gtk.Label(host), label, gtk.Label(name), pwidget, gtk.Label(dtype), w, c, b) self.table.show_all() def get_uri(self, password, interface, protocol, name, stype, domain, host, address, port, text): dstr = "" tt = typedict(text) display = tt.strget("display", "") username = tt.strget("username", "") mode = tt.strget("mode", "") if display.startswith(":"): dstr = display[1:] #append interface to IPv6 host URI for link local addresses ("fe80:"): if interface and if_indextoname and address.lower().startswith("fe80:"): #ie: "fe80::c1:ac45:7351:ea69%eth1" address += "%%%s" % if_indextoname(interface) if username: if password: uri = "%s://%s:%s@%s" % (mode, username, password, address) else: uri = "%s://%s@%s" % (mode, username, address) else: uri = "%s://%s" % (mode, address) if port>0: uri += ":%s" % port if protocol not in ("socket", "namedpipe"): uri += "/" if dstr: uri += "%s" % dstr return uri def attach(self, key, uri): self.warning.set_text("") cmd = get_xpra_command() + ["attach", uri] proc = subprocess.Popen(cmd) log("attach() Popen(%s)=%s", cmd, proc) def proc_exit(*args): log("proc_exit%s", args) c = proc.poll() if key in self.clients_disconnecting: self.clients_disconnecting.remove(key) elif c not in (0, None): self.warning.set_text(EXIT_STR.get(c, "exit code %s" % c).replace("_", " ")) try: del self.clients[key] except: pass else: def update(): self.update() self.populate() glib.idle_add(update) self.child_reaper.add_process(proc, "client-%s" % uri, cmd, True, True, proc_exit) self.clients[key] = proc self.populate() def browser_open(self, rec): import webbrowser password = self.password_entry.get_text() url = self.get_uri(password, *rec) if url.startswith("wss"): url = "https"+url[3:] else: assert url.startswith("ws") url = "http"+url[2:] #trim end of URL: #http://192.168.1.7:10000/10 -> http://192.168.1.7:10000/ url = url[:url.rfind("/")] webbrowser.open_new_tab(url) def make_connect_widgets(self, key, recs, address, port, display): d = {} proc = self.clients.get(key) if proc and proc.poll() is None: icon = self.get_pixbuf("disconnected.png") def disconnect_client(btn): log("disconnect_client(%s) proc=%s", btn, proc) self.clients_disconnecting.add(key) proc.terminate() self.populate() btn = imagebutton("Disconnect", icon, clicked_callback=disconnect_client) return gtk.Label("Already connected with pid=%i" % proc.pid), btn, gtk.Label("") icon = self.get_pixbuf("browser.png") bopen = imagebutton("Open", icon) icon = self.get_pixbuf("connect.png") if len(recs)==1: #single record, single uri: rec = recs[0] uri = self.get_uri(None, *rec) bopen.set_sensitive(uri.startswith("ws")) def browser_open(*_args): self.browser_open(rec) bopen.connect("clicked", browser_open) d[uri] = rec def clicked(*_args): password = self.password_entry.get_text() uri = self.get_uri(password, *rec) self.attach(key, uri) btn = imagebutton("Connect", icon, clicked_callback=clicked) return gtk.Label(uri), btn, bopen #multiple modes / uris uri_menu = gtk.combo_box_new_text() uri_menu.set_size_request(340, 48) #sort by protocol so TCP comes first order = {"socket" : 0, "ssl" :2, "wss" : 3, "tcp" : 4, "ssh" : 6, "ws" : 8} if WIN32: #on MS Windows, prefer ssh which has a GUI for accepting keys #and entering the password: order["ssh"] = 0 def cmp_key(v): text = v[-1] #the text record mode = (text or {}).get("mode", "") host = v[6] host_len = len(host) #log("cmp_key(%s) text=%s, mode=%s, host=%s, host_len=%s", v, text, mode, host, host_len) #prefer order (from mode), then shorter host string: return "%s-%s" % (order.get(mode, mode), host_len) srecs = sorted(recs, key=cmp_key) for rec in srecs: uri = self.get_uri(None, *rec) uri_menu.append_text(uri) d[uri] = rec def connect(*_args): uri = uri_menu.get_active_text() rec = d[uri] password = self.password_entry.get_text() uri = self.get_uri(password, *rec) self.attach(key, uri) uri_menu.set_active(0) btn = imagebutton("Connect", icon, clicked_callback=connect) def uri_changed(*_args): uri = uri_menu.get_active_text() bopen.set_sensitive(uri.startswith("ws")) uri_menu.connect("changed", uri_changed) uri_changed() def browser_open_option(*_args): uri = uri_menu.get_active_text() rec = d[uri] self.browser_open(rec) bopen.connect("clicked", browser_open_option) return uri_menu, btn, bopen def get_platform_icon_name(self, platform): for p,i in { "win32" : "win32", "darwin" : "osx", "linux" : "linux", "freebsd" : "freebsd", }.items(): if platform.startswith(p): return i return None def get_pixbuf(self, icon_name): icon_filename = os.path.join(get_icon_dir(), icon_name) if os.path.exists(icon_filename): return pixbuf_new_from_file(icon_filename) return None
class SessionsGUI(Gtk.Window): def __init__(self, options, title="Xpra Session Browser"): super().__init__() self.exit_code = 0 self.set_title(title) self.set_border_width(20) self.set_resizable(True) self.set_default_size(800, 220) self.set_decorated(True) self.set_size_request(800, 220) self.set_position(Gtk.WindowPosition.CENTER) self.set_wmclass("xpra-sessions-gui", "Xpra-Sessions-GUI") add_close_accel(self, self.quit) self.connect("delete_event", self.quit) icon = get_icon_pixbuf("browse.png") if icon: self.set_icon(icon) hb = Gtk.HeaderBar() hb.set_show_close_button(True) hb.props.title = "Xpra" button = Gtk.Button() icon = Gio.ThemedIcon(name="help-about") image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON) button.add(image) button.set_tooltip_text("About") button.connect("clicked", self.show_about) hb.add(button) hb.show_all() self.set_titlebar(hb) self.clients = {} self.clients_disconnecting = set() self.child_reaper = getChildReaper() self.vbox = Gtk.VBox(False, 20) self.add(self.vbox) title_label = Gtk.Label(title) title_label.modify_font(Pango.FontDescription("sans 14")) title_label.show() self.vbox.add(title_label) self.warning = Gtk.Label(" ") red = color_parse("red") self.warning.modify_fg(Gtk.StateType.NORMAL, red) self.warning.show() self.vbox.add(self.warning) self.password_box = Gtk.HBox(False, 10) self.password_label = Gtk.Label("Password:"******"" #log.info("options=%s (%s)", options, type(options)) self.local_info_cache = {} self.dotxpra = DotXpra(options.socket_dir, options.socket_dirs, username) self.poll_local_sessions() self.populate() GLib.timeout_add(5*1000, self.update) self.vbox.show() self.show() def show(self): #pylint: disable=arguments-differ super().show() def show(): force_focus() self.present() GLib.idle_add(show) def quit(self, *args): log("quit%s", args) self.do_quit() def do_quit(self): log("do_quit()") self.cleanup() Gtk.main_quit() def app_signal(self, signum): self.exit_code = 128 + signum log("app_signal(%s) exit_code=%i", signum, self.exit_code) self.do_quit() def cleanup(self): self.destroy() def show_about(self, *_args): from xpra.gtk_common.about import about about() def update(self): if self.poll_local_sessions(): self.populate() return True def populate(self): self.populate_table() def poll_local_sessions(self): #TODO: run in a thread so we don't block the UI thread! d = self.dotxpra.socket_details(matching_state=DotXpra.LIVE) log("poll_local_sessions() socket_details=%s", d) info_cache = {} for d, details in d.items(): log("poll_local_sessions() %s : %s", d, details) for state, display, sockpath in details: assert state==DotXpra.LIVE key = (display, sockpath) info = self.local_info_cache.get(key) if not info: #try to query it try: info = self.get_session_info(sockpath) if not info: log(" no data for '%s'", sockpath) continue if info.get("session-type")=="client": log(" skipped client socket '%s': %s", sockpath, info) continue except Exception as e: log("get_session_info(%s)", sockpath, exc_info=True) log.error("Error querying session info for %s", sockpath) log.error(" %s", e) del e if not info: continue #log("info(%s)=%s", sockpath, repr_ellipsized(str(info))) info_cache[key] = info if WIN32: socktype = "namedpipe" else: socktype = "socket" def make_text(info): text = {"mode" : socktype} for k, name in { "platform" : "platform", "uuid" : "uuid", "display" : "display", "session-type" : "type", "session-name" : "name", }.items(): v = info.get(k) if v is not None: text[name] = v return text #first remove any records that are no longer found: for key in self.local_info_cache: if key not in info_cache: display, sockpath = key self.records = [(interface, protocol, name, stype, domain, host, address, port, text) for (interface, protocol, name, stype, domain, host, address, port, text) in self.records if (protocol!="socket" or domain!="local" or address!=sockpath)] #add the new ones: for key, info in info_cache.items(): if key not in self.local_info_cache: display, sockpath = key self.records.append(("", "socket", "", "", "local", socket.gethostname(), sockpath, 0, make_text(info))) log("poll_local_sessions() info_cache=%s", info_cache) changed = self.local_info_cache!=info_cache self.local_info_cache = info_cache return changed def get_session_info(self, sockpath): #the lazy way using a subprocess if WIN32: socktype = "namedpipe" else: socktype = "socket" cmd = get_nodock_command()+["id", "%s:%s" % (socktype, sockpath)] p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = p.communicate()[0] log("get_sessions_info(%s) returncode(%s)=%s", sockpath, cmd, p.returncode) if p.returncode!=0: return None out = bytestostr(stdout) info = {} for line in out.splitlines(): parts = line.split("=", 1) if len(parts)==2: info[parts[0]] = parts[1] log("get_sessions_info(%s)=%s", sockpath, info) return info def populate_table(self): log("populate_table: %i records", len(self.records)) if self.table: self.vbox.remove(self.table) self.table = None if not self.records: self.table = Gtk.Label("No sessions found") self.vbox.add(self.table) self.table.show() self.set_size_request(440, 200) self.password_box.hide() return self.password_box.show() self.set_size_request(-1, -1) tb = TableBuilder(1, 6, False) labels = [Gtk.Label(x) for x in ( "Host", "Display", "Name", "Platform", "Type", "URI", "Connect", "Open in Browser", )] tb.add_row(*labels) self.table = tb.get_table() self.vbox.add(self.table) self.table.resize(1+len(self.records), 5) #group them by uuid d = {} session_names = {} for i, record in enumerate(self.records): interface, protocol, name, stype, domain, host, address, port, text = record td = typedict(text) log("populate_table: record[%i]=%s", i, record) uuid = td.strget("uuid", "") display = td.strget("display", "") if domain=="local" and host.endswith(".local"): host = host[:-len(".local")] if uuid: key = uuid else: key = (host.rstrip("."), display) log("populate_table: key[%i]=%s", i, key) d.setdefault(key, []).append((interface, protocol, name, stype, domain, host, address, port, text)) #older servers expose the "session-name" as "session": td = typedict(text) session_name = td.strget("name", "") or td.strget("session", "") if session_name: session_names[key] = session_name for key, recs in d.items(): if isinstance(key, tuple): host, display = key else: uuid = key host, platform, dtype = None, sys.platform, None #try to find a valid host name: hosts = [rec[5] for rec in recs if not rec[5].startswith("local")] if not hosts: hosts = [rec[5] for rec in recs] host = hosts[0] platform, dtype = None, None for rec in recs: td = typedict(rec[-1]) if not platform: platform = td.strget("platform", "") if not dtype: dtype = td.strget("type", "") title = uuid if display: title = display label = Gtk.Label(title) if uuid!=title: label.set_tooltip_text(uuid) #try to use an icon for the platform: platform_icon_name = self.get_platform_icon_name(platform) pwidget = None if platform_icon_name: pwidget = scaled_image(get_icon_pixbuf("%s.png" % platform_icon_name), 28) if pwidget: pwidget.set_tooltip_text(platform_icon_name) if not pwidget: pwidget = Gtk.Label(platform) w, c, b = self.make_connect_widgets(key, recs, address, port, display) session_name = session_names.get(key, "") tb.add_row(Gtk.Label(host), label, Gtk.Label(session_name), pwidget, Gtk.Label(dtype), w, c, b) self.table.show_all() def get_uri(self, password, interface, protocol, name, stype, domain, host, address, port, text): dstr = "" tt = typedict(text) display = tt.strget("display", "") username = tt.strget("username", "") mode = tt.strget("mode", "") if not mode: #guess the mode from the service name, #ie: "localhost.localdomain :2 (wss)" -> "wss" #ie: "localhost.localdomain :2 (ssh-2)" -> "ssh" pos = name.rfind("(") if name.endswith(")") and pos>0: mode = name[pos+1:-1].split("-")[0] if mode not in ("tcp", "ws", "wss", "ssl", "ssh"): return "" else: mode = "tcp" if display and display.startswith(":"): dstr = display[1:] #append interface to IPv6 host URI for link local addresses ("fe80:"): if interface and if_indextoname and address.lower().startswith("fe80:"): #ie: "fe80::c1:ac45:7351:ea69%eth1" address += "%%%s" % if_indextoname(interface) if username: if password: uri = "%s://%s:%s@%s" % (mode, username, password, address) else: uri = "%s://%s@%s" % (mode, username, address) else: uri = "%s://%s" % (mode, address) if port>0 and DEFAULT_PORTS.get(mode, 0)!=port: uri += ":%s" % port if protocol not in ("socket", "namedpipe"): uri += "/" if dstr: uri += "%s" % dstr return uri def attach(self, key, uri): self.warning.set_text("") cmd = get_xpra_command() + ["attach", uri] env = os.environ.copy() env["XPRA_NOTTY"] = "1" proc = subprocess.Popen(cmd, env=env) log("attach() Popen(%s)=%s", cmd, proc) def proc_exit(*args): log("proc_exit%s", args) c = proc.poll() if key in self.clients_disconnecting: self.clients_disconnecting.remove(key) elif c not in (0, None): self.warning.set_text(EXIT_STR.get(c, "exit code %s" % c).replace("_", " ")) client_proc = self.clients.pop(key, None) if client_proc: def update(): self.update() self.populate() GLib.idle_add(update) self.child_reaper.add_process(proc, "client-%s" % uri, cmd, True, True, proc_exit) self.clients[key] = proc self.populate() def browser_open(self, rec): import webbrowser password = self.password_entry.get_text() url = self.get_uri(password, *rec) if url.startswith("wss"): url = "https"+url[3:] else: assert url.startswith("ws") url = "http"+url[2:] #trim end of URL: #http://192.168.1.7:10000/10 -> http://192.168.1.7:10000/ url = url[:url.rfind("/")] webbrowser.open_new_tab(url) def make_connect_widgets(self, key, recs, address, port, display): d = {} proc = self.clients.get(key) if proc and proc.poll() is None: icon = get_icon_pixbuf("disconnected.png") def disconnect_client(btn): log("disconnect_client(%s) proc=%s", btn, proc) self.clients_disconnecting.add(key) proc.terminate() self.populate() btn = imagebutton("Disconnect", icon, clicked_callback=disconnect_client) return Gtk.Label("Already connected with pid=%i" % proc.pid), btn, Gtk.Label("") icon = get_icon_pixbuf("browser.png") bopen = imagebutton("Open", icon) icon = get_icon_pixbuf("connect.png") if len(recs)==1: #single record, single uri: rec = recs[0] uri = self.get_uri(None, *rec) bopen.set_sensitive(uri.startswith("ws")) def browser_open(*_args): self.browser_open(rec) bopen.connect("clicked", browser_open) d[uri] = rec def clicked(*_args): password = self.password_entry.get_text() uri = self.get_uri(password, *rec) self.attach(key, uri) btn = imagebutton("Connect", icon, clicked_callback=clicked) return Gtk.Label(uri), btn, bopen #multiple modes / uris uri_menu = Gtk.ComboBoxText() uri_menu.set_size_request(340, 48) #sort by protocol so TCP comes first order = {"socket" : 0, "ssh" : 1, "tcp" :2, "ssl" : 3, "ws" : 4, "wss" : 8} if WIN32: #on MS Windows, prefer ssh which has a GUI for accepting keys #and entering the password: order["ssh"] = 0 def cmp_key(v): text = v[-1] #the text record mode = (text or {}).get("mode", "") host = v[6] host_len = len(host) #log("cmp_key(%s) text=%s, mode=%s, host=%s, host_len=%s", v, text, mode, host, host_len) #prefer order (from mode), then shorter host string: return "%s-%s" % (order.get(mode, mode), host_len) srecs = sorted(recs, key=cmp_key) has_ws = False for rec in srecs: uri = self.get_uri(None, *rec) uri_menu.append_text(uri) d[uri] = rec if uri.startswith("ws"): has_ws = True def connect(*_args): uri = uri_menu.get_active_text() rec = d[uri] password = self.password_entry.get_text() uri = self.get_uri(password, *rec) self.attach(key, uri) uri_menu.set_active(0) btn = imagebutton("Connect", icon, clicked_callback=connect) def uri_changed(*_args): uri = uri_menu.get_active_text() ws = uri.startswith("ws") bopen.set_sensitive(ws) if ws: bopen.set_tooltip_text("") elif not has_ws: bopen.set_tooltip_text("no 'ws' or 'wss' URIs found") else: bopen.set_tooltip_text("select a 'ws' or 'wss' URI") uri_menu.connect("changed", uri_changed) uri_changed() def browser_open_option(*_args): uri = uri_menu.get_active_text() rec = d[uri] self.browser_open(rec) bopen.connect("clicked", browser_open_option) return uri_menu, btn, bopen def get_platform_icon_name(self, platform): for p,i in { "win32" : "win32", "darwin" : "osx", "linux" : "linux", "freebsd" : "freebsd", }.items(): if platform.startswith(p): return i return None
class TopClient: def __init__(self, opts): self.stdscr = None self.socket_dirs = opts.socket_dirs self.socket_dir = opts.socket_dir self.position = 0 self.selected_session = None self.message = None self.exit_code = None self.dotxpra = DotXpra(self.socket_dir, self.socket_dirs) self.last_getch = 0 self.psprocess = {} def run(self): self.setup() for signum in (signal.SIGINT, signal.SIGTERM): signal.signal(signum, self.signal_handler) self.update_loop() self.cleanup() return self.exit_code def signal_handler(self, signum, *_args): self.exit_code = 128 + signum def setup(self): self.stdscr = curses_init() curses.cbreak() def cleanup(self): scr = self.stdscr if scr: curses.nocbreak() scr.erase() curses_clean(scr) self.stdscr = None def update_loop(self): while self.exit_code is None: self.update_screen() elapsed = int(1000 * monotonic_time() - self.last_getch) delay = max(100, min(1000, 1000 - elapsed)) // 100 curses.halfdelay(delay) try: v = self.stdscr.getch() except Exception: v = -1 self.last_getch = int(1000 * monotonic_time()) if v in EXIT_KEYS: self.exit_code = 0 if v in SIGNAL_KEYS: self.exit_code = 128 + SIGNAL_KEYS[v] if v == 258: #down arrow self.position += 1 elif v == 259: #up arrow self.position = max(self.position - 1, 0) elif v == 10 and self.selected_session: self.show_selected_session() elif v in (ord("s"), ord("S")): self.run_subcommand("stop") elif v in (ord("a"), ord("A")): self.run_subcommand("attach") elif v in (ord("d"), ord("D")): self.run_subcommand("detach") def show_selected_session(self): #show this session: try: self.cleanup() env = os.environ.copy() #we only deal with local sessions, should be fast: env["XPRA_CONNECT_TIMEOUT"] = "3" proc = self.do_run_subcommand("top", env=env) if not proc: self.message = monotonic_time( ), "failed to execute subprocess", curses.color_pair(RED) return exit_code = proc.wait() txt = "top subprocess terminated" attr = 0 if exit_code != 0: attr = curses.color_pair(RED) txt += " with error code %i" % exit_code if exit_code in EXIT_STR: txt += " (%s)" % EXIT_STR.get(exit_code, "").replace( "_", " ") elif (exit_code - 128) in SIGNAMES: #pylint: disable=superfluous-parens txt += " (%s)" % SIGNAMES[exit_code - 128] self.message = monotonic_time(), txt, attr finally: self.setup() def run_subcommand(self, subcommand): return self.do_run_subcommand(subcommand, stdout=DEVNULL, stderr=DEVNULL) def do_run_subcommand(self, subcommand, **kwargs): cmd = get_nodock_command() + [subcommand, self.selected_session] try: return Popen(cmd, **kwargs) except Exception: return None def update_screen(self): self.stdscr.erase() try: self.do_update_screen() finally: self.stdscr.refresh() return True def do_update_screen(self): #c = self.stdscr.getch() #if c==curses.KEY_RESIZE: height, width = self.stdscr.getmaxyx() #log.info("update_screen() %ix%i", height, width) title = get_title() x = max(0, width // 2 - len(title) // 2) try: hpos = 0 self.stdscr.addstr(hpos, x, title, curses.A_BOLD) hpos += 1 if height <= hpos: return sd = self.dotxpra.socket_details() #group them by display instead of socket dir: displays = {} for sessions in sd.values(): for state, display, path in sessions: displays.setdefault(display, []).append((state, path)) self.stdscr.addstr( hpos, 0, "found %i display%s" % (len(displays), engs(displays))) self.position = min(len(displays), self.position) self.selected_session = None hpos += 1 if height <= hpos: return if self.message: ts, txt, attr = self.message if monotonic_time() - ts < 10: self.stdscr.addstr(hpos, 0, txt, attr) hpos += 1 if height <= hpos: return else: self.message = None n = len(displays) for i, (display, state_paths) in enumerate(displays.items()): if height <= hpos: return info = self.get_display_info(display, state_paths) l = len(info) if height <= hpos + l + 2: break self.box(1, hpos, width - 2, l + 2, open_top=i > 0, open_bottom=i < n - 1) hpos += 1 if i == self.position: self.selected_session = display attr = curses.A_REVERSE else: attr = 0 for s in info: if len(s) >= width - 4: s = s[:width - 6] + ".." s = s.ljust(width - 4) self.stdscr.addstr(hpos, 2, s, attr) hpos += 1 except Exception as e: curses_err(self.stdscr, e) def get_display_info(self, display, state_paths): info = [display] valid_path = None for state, path in state_paths: sinfo = "%40s : %s" % (path, state) if POSIX: from pwd import getpwuid from grp import getgrgid try: stat = os.stat(path) #if stat.st_uid!=os.getuid(): sinfo += " uid=%s" % getpwuid(stat.st_uid).pw_name #if stat.st_gid!=os.getgid(): sinfo += " gid=%s" % getgrgid(stat.st_gid).gr_name except Exception as e: sinfo += "(stat error: %s)" % e info.append(sinfo) if state == DotXpra.LIVE: valid_path = path if valid_path: d = self.get_display_id_info(valid_path) name = d.get("session-name") uuid = d.get("uuid") stype = d.get("session-type") error = d.get("error") if error: info[0] = "%s %s" % (display, error) else: info[0] = "%s %s" % (display, name) info.insert(1, "uuid=%s, type=%s" % (uuid, stype)) machine_id = d.get("machine-id") if machine_id is None or machine_id == get_machine_id(): try: pid = int(d.get("pid")) except (ValueError, TypeError): pass else: try: process = self.psprocess.get(pid) if not process: import psutil process = psutil.Process(pid) self.psprocess[pid] = process else: cpu = process.cpu_percent() info[0] += ", %i%% CPU" % (cpu) except Exception: pass return info def get_display_id_info(self, path): d = {} try: cmd = get_nodock_command() + ["id", "socket://%s" % path] proc = Popen(cmd, stdout=PIPE, stderr=PIPE) out, err = proc.communicate() for line in bytestostr(out or err).splitlines(): try: k, v = line.split("=", 1) d[k] = v except ValueError: continue return d except Exception as e: d["error"] = str(e) return d def box(self, x, y, w, h, open_top=False, open_bottom=False): if open_top: ul = curses.ACS_LTEE #@UndefinedVariable ur = curses.ACS_RTEE #@UndefinedVariable else: ul = curses.ACS_ULCORNER #@UndefinedVariable ur = curses.ACS_URCORNER #@UndefinedVariable if open_bottom: ll = curses.ACS_LTEE #@UndefinedVariable lr = curses.ACS_RTEE #@UndefinedVariable else: ll = curses.ACS_LLCORNER #@UndefinedVariable lr = curses.ACS_LRCORNER #@UndefinedVariable box(self.stdscr, x, y, w, h, ul, ur, ll, lr)
class TopClient: def __init__(self, opts): self.stdscr = None self.socket_dirs = opts.socket_dirs self.socket_dir = opts.socket_dir self.position = 0 self.selected_session = None self.exit_code = None self.subprocess_exit_code = None self.dotxpra = DotXpra(self.socket_dir, self.socket_dirs) def run(self): self.stdscr = curses_init() for signum in (signal.SIGINT, signal.SIGTERM): signal.signal(signum, self.signal_handler) self.update_loop() self.cleanup() return self.exit_code def signal_handler(self, *_args): self.exit_code = 128 + signal.SIGINT def cleanup(self): curses_clean(self.stdscr) def update_loop(self): while self.exit_code is None: self.update_screen() curses.halfdelay(50) v = self.stdscr.getch() #print("v=%s" % (v,)) if v in EXIT_KEYS: self.exit_code = 0 elif v == 258: #down arrow self.position += 1 elif v == 259: #up arrow self.position = max(self.position - 1, 0) elif v == 10 and self.selected_session: #show this session: cmd = get_nodock_command() + ["top", self.selected_session] try: self.cleanup() proc = Popen(cmd) exit_code = proc.wait() #TODO: show exit code, especially if non-zero finally: self.stdscr = curses_init() elif v in (ord("s"), ord("S")): self.run_subcommand("stop") elif v in (ord("a"), ord("A")): self.run_subcommand("attach") elif v in (ord("d"), ord("D")): self.run_subcommand("detach") def run_subcommand(self, subcommand): cmd = get_nodock_command() + [subcommand, self.selected_session] try: Popen(cmd, stdout=DEVNULL, stderr=DEVNULL) except: pass def update_screen(self): self.stdscr.erase() try: self.do_update_screen() finally: self.stdscr.refresh() return True def do_update_screen(self): #c = self.stdscr.getch() #if c==curses.KEY_RESIZE: height, width = self.stdscr.getmaxyx() #log.info("update_screen() %ix%i", height, width) title = get_title() x = max(0, width // 2 - len(title) // 2) try: hpos = 0 self.stdscr.addstr(hpos, x, title, curses.A_BOLD) hpos += 1 if height <= hpos: return sd = self.dotxpra.socket_details() #group them by display instead of socket dir: displays = {} for sessions in sd.values(): for state, display, path in sessions: displays.setdefault(display, []).append((state, path)) self.stdscr.addstr( hpos, 0, "found %i display%s" % (len(displays), engs(displays))) self.position = min(len(displays), self.position) self.selected_session = None hpos += 1 if height <= hpos: return n = len(displays) for i, (display, state_paths) in enumerate(displays.items()): if height <= hpos: return info = self.get_display_info(display, state_paths) l = len(info) if height <= hpos + l + 2: break self.box(1, hpos, width - 2, l + 2, open_top=i > 0, open_bottom=i < n - 1) hpos += 1 if i == self.position: self.selected_session = display attr = curses.A_REVERSE else: attr = 0 for s in info: s = s.ljust(width - 4) self.stdscr.addstr(hpos, 2, s, attr) hpos += 1 except Exception as e: curses_err(self.stdscr, e) def get_display_info(self, display, state_paths): info = [display] valid_path = None for state, path in state_paths: sinfo = "%40s : %s" % (path, state) if POSIX: from pwd import getpwuid from grp import getgrgid try: stat = os.stat(path) #if stat.st_uid!=os.getuid(): sinfo += " uid=%s" % getpwuid(stat.st_uid).pw_name #if stat.st_gid!=os.getgid(): sinfo += " gid=%s" % getgrgid(stat.st_gid).gr_name except Exception as e: sinfo += "(stat error: %s)" % e info.append(sinfo) if state == DotXpra.LIVE: valid_path = path if valid_path: d = self.get_display_id_info(valid_path) name = d.get("session-name") uuid = d.get("uuid") stype = d.get("session-type") error = d.get("error") if error: info[0] = "%s %s" % (display, error) else: info[0] = "%s %s" % (display, name) info.insert(1, "uuid=%s, type=%s" % (uuid, stype)) return info def get_display_id_info(self, path): d = {} try: cmd = get_nodock_command() + ["id", "socket://%s" % path] proc = Popen(cmd, stdout=PIPE, stderr=PIPE) out, err = proc.communicate() for line in bytestostr(out or err).splitlines(): try: k, v = line.split("=", 1) d[k] = v except ValueError: continue return d except Exception as e: d["error"] = str(e) return d def box(self, x, y, w, h, open_top=False, open_bottom=False): if open_top: ul = curses.ACS_LTEE #@UndefinedVariable ur = curses.ACS_RTEE #@UndefinedVariable else: ul = curses.ACS_ULCORNER #@UndefinedVariable ur = curses.ACS_URCORNER #@UndefinedVariable if open_bottom: ll = curses.ACS_LTEE #@UndefinedVariable lr = curses.ACS_RTEE #@UndefinedVariable else: ll = curses.ACS_LLCORNER #@UndefinedVariable lr = curses.ACS_LRCORNER #@UndefinedVariable box(self.stdscr, x, y, w, h, ul, ur, ll, lr)