1# This file is part of Xpra. 2# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>) 3# Copyright (C) 2010-2021 Antoine Martin <antoine@xpra.org> 4# Copyright (C) 2008, 2010 Nathaniel Smith <njs@pobox.com> 5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 6# later version. See the file COPYING for details. 7 8import os 9import sys 10 11from xpra.client.client_base import XpraClientBase 12from xpra.client.keyboard_helper import KeyboardHelper 13from xpra.platform import set_name 14from xpra.platform.gui import ready as gui_ready, get_wm_name, get_session_type, ClientExtras 15from xpra.version_util import full_version_str 16from xpra.net import compression, packet_encoding 17from xpra.net.net_util import get_info as get_net_info 18from xpra.child_reaper import reaper_cleanup 19from xpra.platform.info import get_sys_info 20from xpra.os_util import ( 21 platform_name, bytestostr, 22 BITS, POSIX, WIN32, OSX, is_Wayland, 23 get_frame_info, get_info_env, get_sysconfig_info, 24 ) 25from xpra.util import ( 26 std, envbool, envint, typedict, updict, repr_ellipsized, ellipsizer, log_screen_sizes, engs, csv, 27 merge_dicts, 28 XPRA_AUDIO_NOTIFICATION_ID, XPRA_DISCONNECT_NOTIFICATION_ID, 29 ) 30from xpra.scripts.config import parse_bool 31from xpra.exit_codes import EXIT_CONNECTION_FAILED, EXIT_CONNECTION_LOST 32from xpra.version_util import get_version_info_full, get_platform_info 33from xpra.client import mixin_features 34from xpra.log import Logger, get_info as get_log_info 35 36 37CLIENT_BASES = [XpraClientBase] 38if mixin_features.display: 39 from xpra.client.mixins.display import DisplayClient 40 CLIENT_BASES.append(DisplayClient) 41if mixin_features.windows: 42 from xpra.client.mixins.window_manager import WindowClient 43 CLIENT_BASES.append(WindowClient) 44if mixin_features.webcam: 45 from xpra.client.mixins.webcam import WebcamForwarder 46 CLIENT_BASES.append(WebcamForwarder) 47if mixin_features.audio: 48 from xpra.client.mixins.audio import AudioClient 49 CLIENT_BASES.append(AudioClient) 50if mixin_features.clipboard: 51 from xpra.client.mixins.clipboard import ClipboardClient 52 CLIENT_BASES.append(ClipboardClient) 53if mixin_features.notifications: 54 from xpra.client.mixins.notifications import NotificationClient 55 CLIENT_BASES.append(NotificationClient) 56if mixin_features.dbus: 57 from xpra.client.mixins.rpc import RPCClient 58 CLIENT_BASES.append(RPCClient) 59if mixin_features.mmap: 60 from xpra.client.mixins.mmap import MmapClient 61 CLIENT_BASES.append(MmapClient) 62if mixin_features.logging: 63 from xpra.client.mixins.remote_logging import RemoteLogging 64 CLIENT_BASES.append(RemoteLogging) 65if mixin_features.network_state: 66 from xpra.client.mixins.network_state import NetworkState 67 CLIENT_BASES.append(NetworkState) 68if mixin_features.network_listener: 69 from xpra.client.mixins.network_listener import NetworkListener 70 CLIENT_BASES.append(NetworkListener) 71if mixin_features.encoding: 72 from xpra.client.mixins.encodings import Encodings 73 CLIENT_BASES.append(Encodings) 74if mixin_features.tray: 75 from xpra.client.mixins.tray import TrayClient 76 CLIENT_BASES.append(TrayClient) 77 78CLIENT_BASES = tuple(CLIENT_BASES) 79ClientBaseClass = type('ClientBaseClass', CLIENT_BASES, {}) 80 81log = Logger("client") 82keylog = Logger("client", "keyboard") 83log("UIXpraClient%s: %s", ClientBaseClass, CLIENT_BASES) 84 85NOTIFICATION_EXIT_DELAY = envint("XPRA_NOTIFICATION_EXIT_DELAY", 2) 86MOUSE_DELAY_AUTO = envbool("XPRA_MOUSE_DELAY_AUTO", True) 87SYSCONFIG = envbool("XPRA_SYSCONFIG", False) 88 89 90""" 91Utility superclass for client classes which have a UI. 92See gtk_client_base and its subclasses. 93""" 94class UIXpraClient(ClientBaseClass): 95 #NOTE: these signals aren't registered here because this class 96 #does not extend GObject, 97 #the gtk client subclasses will take care of it. 98 #these are all "no-arg" signals 99 __signals__ = ["first-ui-received",] 100 for c in CLIENT_BASES: 101 if c!=XpraClientBase: 102 __signals__ += c.__signals__ 103 104 def __init__(self): 105 log.info("Xpra %s client version %s %i-bit", self.client_toolkit(), full_version_str(), BITS) 106 #mmap_enabled belongs in the MmapClient mixin, 107 #but it is used outside it, so make sure we define it: 108 self.mmap_enabled = False 109 #same for tray: 110 self.tray = None 111 for c in CLIENT_BASES: 112 log("calling %s.__init__()", c) 113 c.__init__(self) 114 try: 115 pinfo = get_platform_info() 116 osinfo = "%s" % platform_name(sys.platform, pinfo.get("linux_distribution") or pinfo.get("sysrelease", "")) 117 log.info(" running on %s", osinfo) 118 except Exception: 119 log("platform name error:", exc_info=True) 120 wm = get_wm_name() #pylint: disable=assignment-from-none 121 if wm: 122 log.info(" window manager is '%s'", wm) 123 124 self._ui_events = 0 125 self.title = "" 126 self.session_name = "" 127 128 self.server_platform = "" 129 self.server_session_name = None 130 131 #features: 132 self.opengl_enabled = False 133 self.opengl_props = {} 134 self.readonly = False 135 self.xsettings_enabled = False 136 self.server_start_new_commands = False 137 self.server_xdg_menu = None 138 self.start_new_commands = [] 139 self.start_child_new_commands = [] 140 self.headerbar = None 141 142 #in WindowClient - should it be? 143 #self.server_is_desktop = False 144 self.server_sharing = False 145 self.server_sharing_toggle = False 146 self.server_lock = False 147 self.server_lock_toggle = False 148 self.server_keyboard = True 149 self.server_pointer = True 150 151 self.client_supports_opengl = False 152 self.client_supports_sharing = False 153 self.client_lock = False 154 155 #helpers and associated flags: 156 self.client_extras = None 157 self.keyboard_helper_class = KeyboardHelper 158 self.keyboard_helper = None 159 self.keyboard_grabbed = False 160 self.keyboard_sync = False 161 self.pointer_grabbed = False 162 self.kh_warning = False 163 self.menu_helper = None 164 165 #state: 166 self._on_handshake = [] 167 self._on_server_setting_changed = {} 168 169 170 def init(self, opts): 171 """ initialize variables from configuration """ 172 self.init_aliases() 173 for c in CLIENT_BASES: 174 log("init: %s", c) 175 c.init(self, opts) 176 177 self.title = opts.title 178 self.session_name = bytestostr(opts.session_name) 179 self.xsettings_enabled = not (OSX or WIN32) and parse_bool("xsettings", opts.xsettings, True) 180 self.readonly = opts.readonly 181 self.client_supports_sharing = opts.sharing is True 182 self.client_lock = opts.lock is True 183 self.headerbar = opts.headerbar 184 185 186 def init_ui(self, opts): 187 """ initialize user interface """ 188 if not self.readonly: 189 def noauto(v): 190 if not v: 191 return None 192 if str(v).lower()=="auto": 193 return None 194 return v 195 overrides = [noauto(getattr(opts, "keyboard_%s" % x)) for x in ( 196 "layout", "layouts", "variant", "variants", "options", 197 )] 198 def send_keyboard(*parts): 199 self.after_handshake(self.send, *parts) 200 try: 201 self.keyboard_helper = self.keyboard_helper_class(send_keyboard, opts.keyboard_sync, 202 opts.shortcut_modifiers, 203 opts.key_shortcut, 204 opts.keyboard_raw, *overrides) 205 except ImportError as e: 206 keylog("error instantiating %s", self.keyboard_helper_class, exc_info=True) 207 keylog.warn("Warning: no keyboard support, %s", e) 208 209 if mixin_features.windows: 210 self.init_opengl(opts.opengl) 211 212 if ClientExtras is not None: 213 self.client_extras = ClientExtras(self, opts) #pylint: disable=not-callable 214 215 if opts.start or opts.start_child: 216 from xpra.scripts.main import strip_defaults_start_child 217 from xpra.scripts.config import make_defaults_struct 218 defaults = make_defaults_struct() 219 self.start_new_commands = strip_defaults_start_child(opts.start, defaults.start) #pylint: disable=no-member 220 self.start_child_new_commands = strip_defaults_start_child(opts.start_child, defaults.start_child) #pylint: disable=no-member 221 222 if MOUSE_DELAY_AUTO: 223 try: 224 #some platforms don't detect the vrefresh correctly 225 #(ie: macos in virtualbox?), so use a sane default minimum 226 #discount by 5ms to ensure we have time to hit the target 227 v = max(60, self.get_vrefresh()) 228 self._mouse_position_delay = max(5, 1000//v//2 - 5) 229 log("mouse delay: %s", self._mouse_position_delay) 230 except Exception: 231 log("failed to calculate automatic delay", exc_info=True) 232 233 def get_vrefresh(self): 234 #this method is overriden in the GTK client 235 from xpra.platform.gui import get_vrefresh #pylint: disable=import-outside-toplevel 236 return get_vrefresh() 237 238 239 def run(self): 240 if self.client_extras: 241 self.idle_add(self.client_extras.ready) 242 for c in CLIENT_BASES: 243 c.run(self) 244 245 246 def quit(self, exit_code=0): 247 raise NotImplementedError() 248 249 def cleanup(self): 250 log("UIXpraClient.cleanup()") 251 for c in CLIENT_BASES: 252 c.cleanup(self) 253 for x in (self.keyboard_helper, self.tray, self.menu_helper, self.client_extras): 254 if x is None: 255 continue 256 log("UIXpraClient.cleanup() calling %s.cleanup()", type(x)) 257 try: 258 x.cleanup() 259 except Exception: 260 log.error("error on %s cleanup", type(x), exc_info=True) 261 #the protocol has been closed, it is now safe to close all the windows: 262 #(cleaner and needed when we run embedded in the client launcher) 263 reaper_cleanup() 264 log("UIXpraClient.cleanup() done") 265 266 267 def signal_cleanup(self): 268 log("UIXpraClient.signal_cleanup()") 269 XpraClientBase.signal_cleanup(self) 270 reaper_cleanup() 271 log("UIXpraClient.signal_cleanup() done") 272 273 274 def get_info(self): 275 info = { 276 "pid" : os.getpid(), 277 "threads" : get_frame_info(), 278 "env" : get_info_env(), 279 "sys" : get_sys_info(), 280 "network" : get_net_info(), 281 "logging" : get_log_info(), 282 } 283 if SYSCONFIG: 284 info["sysconfig"] = get_sysconfig_info() 285 for c in CLIENT_BASES: 286 try: 287 i = c.get_info(self) 288 info = merge_dicts(info, i) 289 except Exception: 290 log.error("Error collection information from %s", c, exc_info=True) 291 return info 292 293 294 def show_about(self, *_args): 295 log.warn("show_about() is not implemented in %s", self) 296 297 def show_session_info(self, *_args): 298 log.warn("show_session_info() is not implemented in %s", self) 299 300 def show_bug_report(self, *_args): 301 log.warn("show_bug_report() is not implemented in %s", self) 302 303 304 def init_opengl(self, _enable_opengl): 305 self.opengl_enabled = False 306 self.client_supports_opengl = False 307 self.opengl_props = {"info" : "not supported"} 308 309 310 def _ui_event(self): 311 if self._ui_events==0: 312 self.emit("first-ui-received") 313 self._ui_events += 1 314 315 316 def get_mouse_position(self): 317 raise NotImplementedError() 318 319 def get_current_modifiers(self): 320 raise NotImplementedError() 321 322 323 def send_start_new_commands(self): 324 log("send_start_new_commands() start_new_commands=%s, start_child_new_commands=%s", 325 self.start_new_commands, self.start_child_new_commands) 326 import shlex 327 for cmd in self.start_new_commands: 328 cmd_parts = shlex.split(cmd) 329 self.send_start_command(cmd_parts[0], cmd, True) 330 for cmd in self.start_child_new_commands: 331 cmd_parts = shlex.split(cmd) 332 self.send_start_command(cmd_parts[0], cmd, False) 333 334 def send_start_command(self, name, command, ignore, sharing=True): 335 log("send_start_command(%s, %s, %s, %s)", name, command, ignore, sharing) 336 assert name is not None and command is not None and ignore is not None 337 self.send("start-command", name, command, ignore, sharing) 338 339 def get_version_info(self) -> dict: 340 return get_version_info_full() 341 342 343 ###################################################################### 344 # trigger notifications on disconnection, 345 # and wait before actually exiting so the notification has a chance of being seen 346 def server_disconnect_warning(self, reason, *info): 347 if self.exit_code is None: 348 body = "\n".join(info) 349 if self.connection_established: 350 title = "Xpra Session Disconnected: %s" % reason 351 self.exit_code = EXIT_CONNECTION_LOST 352 else: 353 title = "Connection Failed: %s" % reason 354 self.exit_code = EXIT_CONNECTION_FAILED 355 self.may_notify(XPRA_DISCONNECT_NOTIFICATION_ID, 356 title, body, icon_name="disconnected") 357 #show text notification then quit: 358 delay = NOTIFICATION_EXIT_DELAY*mixin_features.notifications 359 self.timeout_add(delay*1000, XpraClientBase.server_disconnect_warning, self, reason, *info) 360 self.cleanup() 361 362 def server_disconnect(self, reason, *info): 363 body = "\n".join(info) 364 self.may_notify(XPRA_DISCONNECT_NOTIFICATION_ID, 365 "Xpra Session Disconnected: %s" % reason, body, icon_name="disconnected") 366 delay = NOTIFICATION_EXIT_DELAY*mixin_features.notifications 367 if self.exit_code is None: 368 self.exit_code = self.server_disconnect_exit_code(reason, *info) 369 self.timeout_add(delay*1000, XpraClientBase.server_disconnect, self, reason, *info) 370 self.cleanup() 371 372 373 ###################################################################### 374 # hello: 375 def make_hello(self): 376 caps = XpraClientBase.make_hello(self) 377 caps["session-type"] = get_session_type() 378 #don't try to find the server uuid if this platform cannot run servers.. 379 #(doing so causes lockups on win32 and startup errors on osx) 380 if POSIX and not is_Wayland(): 381 #we may be running inside another server! 382 try: 383 from xpra.server.server_uuid import get_uuid, get_mode #pylint: disable=import-outside-toplevel 384 if get_mode()!="shadow": 385 caps["server_uuid"] = get_uuid() or "" 386 except ImportError: 387 pass 388 for x in (#generic feature flags: 389 "wants_events", "setting-change", 390 "xdg-menu-update", 391 ): 392 caps[x] = True 393 caps.update({ 394 #generic server flags: 395 "share" : self.client_supports_sharing, 396 "lock" : self.client_lock, 397 }) 398 caps.update({"mouse" : True}) 399 caps.update(self.get_keyboard_caps()) 400 for c in CLIENT_BASES: 401 caps.update(c.get_caps(self)) 402 def u(prefix, c): 403 updict(caps, prefix, c, flatten_dicts=False) 404 u("control_commands", self.get_control_commands_caps()) 405 u("platform", get_platform_info()) 406 u("opengl", self.opengl_props) 407 return caps 408 409 410 411 ###################################################################### 412 # connection setup: 413 def setup_connection(self, conn): 414 protocol = super().setup_connection(conn) 415 for c in CLIENT_BASES: 416 if c!=XpraClientBase: 417 c.setup_connection(self, conn) 418 return protocol 419 420 def server_connection_established(self, caps : typedict): 421 if not XpraClientBase.server_connection_established(self, caps): 422 return False 423 #process the rest from the UI thread: 424 self.idle_add(self.process_ui_capabilities, caps) 425 return True 426 427 428 def parse_server_capabilities(self, c : typedict) -> bool: 429 for cb in CLIENT_BASES: 430 if not cb.parse_server_capabilities(self, c): 431 log.info("failed to parse server capabilities in %s", cb) 432 return False 433 self.server_session_name = c.uget("session_name") 434 set_name("Xpra", self.session_name or self.server_session_name or "Xpra") 435 self.server_platform = c.strget("platform") 436 self.server_sharing = c.boolget("sharing") 437 self.server_sharing_toggle = c.boolget("sharing-toggle") 438 self.server_lock = c.boolget("lock") 439 self.server_lock_toggle = c.boolget("lock-toggle") 440 self.server_keyboard = c.boolget("keyboard", True) 441 self.server_pointer = c.boolget("pointer", True) 442 self.server_start_new_commands = c.boolget("start-new-commands") 443 if self.server_start_new_commands: 444 self.server_xdg_menu = c.dictget("xdg-menu", None) 445 if self.start_new_commands or self.start_child_new_commands: 446 if self.server_start_new_commands: 447 self.after_handshake(self.send_start_new_commands) 448 else: 449 log.warn("Warning: cannot start new commands") 450 log.warn(" the feature is currently disabled on the server") 451 self.server_commands_info = c.boolget("server-commands-info") 452 self.server_commands_signals = c.strtupleget("server-commands-signals") 453 self.server_readonly = c.boolget("readonly") 454 if self.server_readonly and not self.readonly: 455 log.info("server is read only") 456 self.readonly = True 457 if not self.server_keyboard and self.keyboard_helper: 458 #swallow packets: 459 def nosend(*_args): 460 pass 461 self.keyboard_helper.send = nosend 462 463 i = platform_name(self._remote_platform, 464 c.strtupleget("platform.linux_distribution") or c.strget("platform.release", "")) 465 r = self._remote_version 466 if self._remote_revision: 467 r += "-r%s" % self._remote_revision 468 mode = c.strget("server.mode", "server") 469 bits = c.intget("python.bits", 32) 470 log.info("Xpra %s server version %s %i-bit", mode, std(r), bits) 471 if i: 472 log.info(" running on %s", std(i)) 473 if c.boolget("desktop") or c.boolget("shadow"): 474 v = c.intpair("actual_desktop_size") 475 if v: 476 w, h = v 477 ss = c.tupleget("screen_sizes") 478 if ss: 479 log.info(" remote desktop size is %sx%s with %s screen%s:", w, h, len(ss), engs(ss)) 480 log_screen_sizes(w, h, ss) 481 else: 482 log.info(" remote desktop size is %sx%s", w, h) 483 if c.boolget("proxy"): 484 proxy_hostname = c.strget("proxy.hostname") 485 proxy_platform = c.strget("proxy.platform") 486 proxy_release = c.strget("proxy.platform.release") 487 proxy_version = c.strget("proxy.version") 488 proxy_version = c.strget("proxy.build.version", proxy_version) 489 proxy_distro = c.strget("proxy.linux_distribution") 490 msg = "via: %s proxy version %s" % ( 491 platform_name(proxy_platform, proxy_distro or proxy_release), 492 std(proxy_version or "unknown") 493 ) 494 if proxy_hostname: 495 msg += " on '%s'" % std(proxy_hostname) 496 log.info(msg) 497 return True 498 499 def process_ui_capabilities(self, caps : typedict): 500 for c in CLIENT_BASES: 501 if c!=XpraClientBase: 502 c.process_ui_capabilities(self, caps) 503 #keyboard: 504 if self.keyboard_helper: 505 modifier_keycodes = caps.dictget("modifier_keycodes", {}) 506 if modifier_keycodes: 507 self.keyboard_helper.set_modifier_mappings(modifier_keycodes) 508 self.key_repeat_delay, self.key_repeat_interval = caps.intpair("key_repeat", (-1,-1)) 509 self.handshake_complete() 510 511 512 def _process_startup_complete(self, packet): 513 log("all the existing windows and system trays have been received") 514 super()._process_startup_complete(packet) 515 gui_ready() 516 if self.tray: 517 self.tray.ready() 518 self.send_info_request() 519 msg = "running" 520 try: 521 windows = tuple(self._id_to_window.values()) 522 except AttributeError: 523 pass 524 else: 525 trays = sum(1 for w in windows if w.is_tray()) 526 wins = sum(1 for w in windows if not w.is_tray()) 527 if wins: 528 msg += ", %i window%s" % (wins, engs(wins)) 529 if trays: 530 msg += ", %i tray%s" % (trays, engs(trays)) 531 log.info(msg) 532 533 def handshake_complete(self): 534 oh = self._on_handshake 535 self._on_handshake = None 536 for cb, args in oh: 537 try: 538 cb(*args) 539 except Exception: 540 log.error("Error processing handshake callback %s", cb, exc_info=True) 541 542 def after_handshake(self, cb, *args): 543 log("after_handshake(%s, %s) on_handshake=%s", cb, args, ellipsizer(self._on_handshake)) 544 if self._on_handshake is None: 545 #handshake has already occurred, just call it: 546 self.idle_add(cb, *args) 547 else: 548 self._on_handshake.append((cb, args)) 549 550 551 ###################################################################### 552 # server messages: 553 def _process_server_event(self, packet): 554 log(": ".join((str(x) for x in packet[1:]))) 555 556 def on_server_setting_changed(self, setting, cb): 557 self._on_server_setting_changed.setdefault(setting, []).append(cb) 558 559 def _process_setting_change(self, packet): 560 setting, value = packet[1:3] 561 setting = bytestostr(setting) 562 #convert "hello" / "setting" variable names to client variables: 563 if setting in ( 564 "clipboard-limits", 565 ): 566 pass 567 elif setting in ( 568 "bell", "randr", "cursors", "notifications", "dbus-proxy", "clipboard", 569 "clipboard-direction", "session_name", 570 "sharing", "sharing-toggle", "lock", "lock-toggle", 571 "start-new-commands", "client-shutdown", "webcam", 572 "bandwidth-limit", "clipboard-limits", 573 "xdg-menu", 574 ): 575 setattr(self, "server_%s" % setting.replace("-", "_"), value) 576 else: 577 log.info("unknown server setting changed: %s=%s", setting, repr_ellipsized(bytestostr(value))) 578 return 579 log("_process_setting_change: %s=%s", setting, value) 580 #xdg-menu is too big to log, and we have to update our attribute: 581 if setting=="xdg-menu": 582 self.server_xdg_menu = value 583 else: 584 log.info("server setting changed: %s=%s", setting, repr_ellipsized(value)) 585 self.server_setting_changed(setting, value) 586 587 def server_setting_changed(self, setting, value): 588 log("setting_changed(%s, %s)", setting, value) 589 cbs = self._on_server_setting_changed.get(setting) 590 if cbs: 591 for cb in cbs: 592 log("setting_changed(%s, %s) calling %s", setting, value, cb) 593 cb(setting, value) 594 595 596 def get_control_commands_caps(self): 597 caps = ["show_session_info", "show_bug_report", "show_menu", "name", "debug"] 598 for x in compression.get_enabled_compressors(): 599 caps.append("enable_"+x) 600 for x in packet_encoding.get_enabled_encoders(): 601 caps.append("enable_"+x) 602 log("get_control_commands_caps()=%s", caps) 603 return {"" : caps} 604 605 def _process_control(self, packet): 606 command = bytestostr(packet[1]) 607 args = packet[2:] 608 log("_process_control(%s)", packet) 609 if command=="show_session_info": 610 log("calling %s%s on server request", self.show_session_info, args) 611 self.show_session_info(*args) 612 elif command=="show_bug_report": 613 self.show_bug_report() 614 elif command=="show_menu": 615 self.show_menu() 616 elif command in ("enable_%s" % x for x in compression.get_enabled_compressors()): 617 compressor = command.split("_")[1] 618 log.info("switching to %s on server request", compressor) 619 self._protocol.enable_compressor(compressor) 620 elif command in ("enable_%s" % x for x in packet_encoding.get_enabled_encoders()): 621 pe = command.split("_")[1] 622 log.info("switching to %s on server request", pe) 623 self._protocol.enable_encoder(pe) 624 elif command=="name": 625 assert len(args)>=1 626 self.server_session_name = bytestostr(args[0]) 627 log.info("session name updated from server: %s", self.server_session_name) 628 #TODO: reset tray tooltip, session info title, etc.. 629 elif command=="debug": 630 if not args: 631 log.warn("not enough arguments for debug control command") 632 return 633 from xpra.log import ( 634 add_debug_category, add_disabled_category, 635 enable_debug_for, disable_debug_for, 636 get_all_loggers, 637 ) 638 log_cmd = bytestostr(args[0]) 639 if log_cmd=="status": 640 dloggers = [x for x in get_all_loggers() if x.is_debug_enabled()] 641 if dloggers: 642 log.info("logging is enabled for:") 643 for l in dloggers: 644 log.info(" - %s", l) 645 else: 646 log.info("logging is not enabled for any loggers") 647 return 648 log_cmd = bytestostr(args[0]) 649 if log_cmd not in ("enable", "disable"): 650 log.warn("invalid debug control mode: '%s' (must be 'enable' or 'disable')", log_cmd) 651 return 652 if len(args)<2: 653 log.warn("not enough arguments for '%s' debug control command" % log_cmd) 654 return 655 loggers = [] 656 #each argument is a group 657 groups = [bytestostr(x) for x in args[1:]] 658 for group in groups: 659 #and each group is a list of categories 660 #preferably separated by "+", 661 #but we support "," for backwards compatibility: 662 categories = [v.strip() for v in group.replace("+", ",").split(",")] 663 if log_cmd=="enable": 664 add_debug_category(*categories) 665 loggers += enable_debug_for(*categories) 666 else: 667 assert log_cmd=="disable" 668 add_disabled_category(*categories) 669 loggers += disable_debug_for(*categories) 670 if not loggers: 671 log.info("%s debugging, no new loggers matching: %s", log_cmd, csv(groups)) 672 else: 673 log.info("%sd debugging for:", log_cmd) 674 for l in loggers: 675 log.info(" - %s", l) 676 else: 677 log.warn("received invalid control command from server: %s", command) 678 679 680 def may_notify_audio(self, summary, body): 681 self.may_notify(XPRA_AUDIO_NOTIFICATION_ID, summary, body, icon_name="audio") 682 683 684 ###################################################################### 685 # features: 686 def send_sharing_enabled(self): 687 assert self.server_sharing and self.server_sharing_toggle 688 self.send("sharing-toggle", self.client_supports_sharing) 689 690 def send_lock_enabled(self): 691 assert self.server_lock_toggle 692 self.send("lock-toggle", self.client_lock) 693 694 def send_notify_enabled(self): 695 assert self.client_supports_notifications, "cannot toggle notifications: the feature is disabled by the client" 696 self.send("set-notify", self.notifications_enabled) 697 698 def send_bell_enabled(self): 699 assert self.client_supports_bell, "cannot toggle bell: the feature is disabled by the client" 700 assert self.server_bell, "cannot toggle bell: the feature is disabled by the server" 701 self.send("set-bell", self.bell_enabled) 702 703 def send_cursors_enabled(self): 704 assert self.client_supports_cursors, "cannot toggle cursors: the feature is disabled by the client" 705 assert self.server_cursors, "cannot toggle cursors: the feature is disabled by the server" 706 self.send("set-cursors", self.cursors_enabled) 707 708 def send_force_ungrab(self, wid): 709 self.send("force-ungrab", wid) 710 711 def send_keyboard_sync_enabled_status(self, *_args): 712 self.send("set-keyboard-sync-enabled", self.keyboard_sync) 713 714 715 ###################################################################### 716 # keyboard: 717 def get_keyboard_caps(self): 718 caps = {} 719 if self.readonly or not self.keyboard_helper: 720 #don't bother sending keyboard info, as it won't be used 721 caps["keyboard"] = False 722 else: 723 caps.update(self.get_keymap_properties()) 724 #show the user a summary of what we have detected: 725 self.keyboard_helper.log_keyboard_info() 726 727 caps["modifiers"] = self.get_current_modifiers() 728 delay_ms, interval_ms = self.keyboard_helper.key_repeat_delay, self.keyboard_helper.key_repeat_interval 729 if delay_ms>0 and interval_ms>0: 730 caps["key_repeat"] = (delay_ms,interval_ms) 731 else: 732 #cannot do keyboard_sync without a key repeat value! 733 #(maybe we could just choose one?) 734 self.keyboard_helper.keyboard_sync = False 735 caps["keyboard_sync"] = self.keyboard_helper.keyboard_sync 736 log("keyboard capabilities: %s", caps) 737 return caps 738 739 def window_keyboard_layout_changed(self, window): 740 #win32 can change the keyboard mapping per window... 741 keylog("window_keyboard_layout_changed(%s)", window) 742 if self.keyboard_helper: 743 self.keyboard_helper.keymap_changed() 744 745 def get_keymap_properties(self): 746 if not self.keyboard_helper: 747 return {} 748 props = self.keyboard_helper.get_keymap_properties() 749 props["modifiers"] = self.get_current_modifiers() 750 return props 751 752 def handle_key_action(self, window, key_event): 753 if self.readonly or self.keyboard_helper is None: 754 return False 755 wid = self._window_to_id[window] 756 keylog("handle_key_action(%s, %s) wid=%s", window, key_event, wid) 757 return self.keyboard_helper.handle_key_action(window, wid, key_event) 758 759 def mask_to_names(self, mask): 760 if self.keyboard_helper is None: 761 return [] 762 return self.keyboard_helper.mask_to_names(mask) 763 764 765 ###################################################################### 766 # windows overrides 767 def cook_metadata(self, _new_window, metadata): 768 #convert to a typedict and apply client-side overrides: 769 metadata = typedict(metadata) 770 if self.server_is_desktop and self.desktop_fullscreen: 771 #force it fullscreen: 772 metadata.pop("size-constraints", None) 773 metadata["fullscreen"] = True 774 #FIXME: try to figure out the monitors we go fullscreen on for X11: 775 #if POSIX: 776 # metadata["fullscreen-monitors"] = [0, 1, 0, 1] 777 return metadata 778 779 ###################################################################### 780 # network and status: 781 def server_connection_state_change(self): 782 if not self._server_ok: 783 log.info("server is not responding, drawing spinners over the windows") 784 def timer_redraw(): 785 if self._protocol is None: 786 #no longer connected! 787 return False 788 ok = self.server_ok() 789 self.redraw_spinners() 790 if ok: 791 log.info("server is OK again") 792 return not ok #repaint again until ok 793 self.idle_add(self.redraw_spinners) 794 self.timeout_add(250, timer_redraw) 795 796 def redraw_spinners(self): 797 #draws spinner on top of the window, or not (plain repaint) 798 #depending on whether the server is ok or not 799 ok = self.server_ok() 800 log("redraw_spinners() ok=%s", ok) 801 for w in self._id_to_window.values(): 802 if not w.is_tray(): 803 w.spinner(ok) 804 805 806 ###################################################################### 807 # packets: 808 def init_authenticated_packet_handlers(self): 809 log("init_authenticated_packet_handlers()") 810 for c in CLIENT_BASES: 811 c.init_authenticated_packet_handlers(self) 812 #run from the UI thread: 813 self.add_packet_handlers({ 814 "startup-complete": self._process_startup_complete, 815 "setting-change": self._process_setting_change, 816 "control" : self._process_control, 817 }) 818 #run directly from the network thread: 819 self.add_packet_handler("server-event", self._process_server_event, False) 820 821 822 def process_packet(self, proto, packet): 823 self.check_server_echo(0) 824 XpraClientBase.process_packet(self, proto, packet) 825