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