xpra icon
Bug tracker and wiki

This bug tracker and wiki are being discontinued
please use https://github.com/Xpra-org/xpra instead.


Ticket #1110: ui_client_base.py

File ui_client_base.py, 125.5 KB (added by Kundan, 5 years ago)

The xpra client file (modified line 1985 1986)

Line 
1# This file is part of Xpra.
2# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
3# Copyright (C) 2010-2015 Antoine Martin <antoine@devloop.org.uk>
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 re
10import sys
11import time
12import datetime
13import traceback
14import logging
15import hashlib
16from collections import deque
17
18from xpra.log import Logger, set_global_logging_handler
19log = Logger("client")
20windowlog = Logger("client", "window")
21geomlog = Logger("client", "geometry")
22paintlog = Logger("client", "paint")
23focuslog = Logger("client", "focus")
24soundlog = Logger("client", "sound")
25filelog = Logger("client", "file")
26traylog = Logger("client", "tray")
27keylog = Logger("client", "keyboard")
28workspacelog = Logger("client", "workspace")
29rpclog = Logger("client", "rpc")
30grablog = Logger("client", "grab")
31iconlog = Logger("client", "icon")
32screenlog = Logger("client", "screen")
33mouselog = Logger("mouse")
34avsynclog = Logger("av-sync")
35clipboardlog = Logger("clipboard")
36scalinglog = Logger("scaling")
37
38
39from xpra import __version__ as XPRA_VERSION
40from xpra.gtk_common.gobject_util import no_arg_signal
41from xpra.client.client_base import XpraClientBase, EXIT_TIMEOUT, EXIT_MMAP_TOKEN_FAILURE
42from xpra.client.client_tray import ClientTray
43from xpra.client.keyboard_helper import KeyboardHelper
44from xpra.platform.features import MMAP_SUPPORTED, SYSTEM_TRAY_SUPPORTED, CLIPBOARD_WANT_TARGETS, CLIPBOARD_GREEDY, CLIPBOARDS, REINIT_WINDOWS
45from xpra.platform.gui import (ready as gui_ready, get_vrefresh, get_antialias_info, get_double_click_time, show_desktop, get_cursor_size,
46                               get_double_click_distance, get_native_notifier_classes, get_native_tray_classes, get_native_system_tray_classes,
47                               get_native_tray_menu_helper_classes, get_xdpi, get_ydpi, get_number_of_desktops, get_desktop_names, ClientExtras)
48from xpra.codecs.loader import load_codecs, codec_versions, has_codec, get_codec, PREFERED_ENCODING_ORDER, PROBLEMATIC_ENCODINGS
49from xpra.codecs.video_helper import getVideoHelper, NO_GFX_CSC_OPTIONS
50from xpra.scripts.main import sound_option
51from xpra.scripts.config import parse_bool_or_int, FALSE_OPTIONS, TRUE_OPTIONS
52from xpra.simple_stats import std_unit
53from xpra.net import compression, packet_encoding
54from xpra.child_reaper import reaper_cleanup
55from xpra.make_thread import make_thread
56from xpra.os_util import Queue, platform_name, get_machine_id, get_user_uuid, bytestostr
57from xpra.util import nonl, std, iround, AtomicInteger, AdHocStruct, log_screen_sizes, typedict, updict, csv, engs, CLIENT_EXIT
58from xpra.version_util import get_version_info_full, get_platform_info
59try:
60    from xpra.clipboard.clipboard_base import ALL_CLIPBOARDS
61except:
62    ALL_CLIPBOARDS = []
63
64FAKE_BROKEN_CONNECTION = int(os.environ.get("XPRA_FAKE_BROKEN_CONNECTION", "0"))
65PING_TIMEOUT = int(os.environ.get("XPRA_PING_TIMEOUT", "60"))
66UNGRAB_KEY = os.environ.get("XPRA_UNGRAB_KEY", "Escape")
67
68MONITOR_CHANGE_REINIT = os.environ.get("XPRA_MONITOR_CHANGE_REINIT")
69
70AV_SYNC_DELTA = int(os.environ.get("XPRA_AV_SYNC_DELTA", "0"))
71MOUSE_ECHO = os.environ.get("XPRA_MOUSE_ECHO", "0")=="1"
72
73PAINT_FAULT_RATE = int(os.environ.get("XPRA_PAINT_FAULT_INJECTION_RATE", "0"))
74PAINT_FAULT_TELL = os.environ.get("XPRA_PAINT_FAULT_INJECTION_TELL", "1")=="1"
75
76
77#LOG_INFO_RESPONSE = ("^window.*position", "^window.*size$")
78LOG_INFO_RESPONSE = os.environ.get("XPRA_LOG_INFO_RESPONSE", "")
79
80
81MIN_SCALING = float(os.environ.get("XPRA_MIN_SCALING", "0.1"))
82MAX_SCALING = float(os.environ.get("XPRA_MAX_SCALING", "8"))
83SCALING_OPTIONS = [float(x) for x in os.environ.get("XPRA_TRAY_SCALING_OPTIONS", "0.25,0.5,0.666,1,1.25,1.5,2.0,3.0,4.0,5.0").split(",") if float(x)>=MIN_SCALING and float(x)<=MAX_SCALING]
84SCALING_EMBARGO_TIME = int(os.environ.get("XPRA_SCALING_EMBARGO_TIME", "1000"))/1000.0
85
86PYTHON3 = sys.version_info[0] == 3
87WIN32 = sys.platform.startswith("win")
88
89RPC_TIMEOUT = int(os.environ.get("XPRA_RPC_TIMEOUT", "5000"))
90
91
92def r4cmp(v, rounding=1000.0):    #ignore small differences in floats for scale values
93    return iround(v*rounding)
94def fequ(v1, v2):
95    return r4cmp(v1)==r4cmp(v2)
96
97
98"""
99Utility superclass for client classes which have a UI.
100See gtk_client_base and its subclasses.
101"""
102class UIXpraClient(XpraClientBase):
103    #NOTE: these signals aren't registered because this class
104    #does not extend GObject.
105    __gsignals__ = {
106        "first-ui-received"         : no_arg_signal,
107
108        "clipboard-toggled"         : no_arg_signal,
109        "scaling-changed"           : no_arg_signal,
110        "keyboard-sync-toggled"     : no_arg_signal,
111        "speaker-changed"           : no_arg_signal,        #bitrate or pipeline state has changed
112        "microphone-changed"        : no_arg_signal,        #bitrate or pipeline state has changed
113        }
114
115    def __init__(self):
116        XpraClientBase.__init__(self)
117        try:
118            from xpra.src_info import REVISION
119            rev_info = "-r%s" % REVISION
120        except:
121            rev_info = ""
122        log.info("Xpra %s client version %s%s", self.client_toolkit(), XPRA_VERSION, rev_info)
123        try:
124            pinfo = get_platform_info()
125            osinfo = "%s" % platform_name(sys.platform, pinfo.get("linux_distribution") or pinfo.get("release", ""))
126            log.info(" running on %s", osinfo)
127        except:
128            log("platform name error:", exc_info=True)
129        self.start_time = time.time()
130        self._window_to_id = {}
131        self._id_to_window = {}
132        self._ui_events = 0
133        self.title = ""
134        self.session_name = ""
135        self.auto_refresh_delay = -1
136        self.max_window_size = 0, 0
137        self.dpi = 0
138        self.xscale = 1
139        self.yscale = 1
140        self.scale_change_embargo = 0
141        self.shadow_fullscreen = False
142
143        #draw thread:
144        self._draw_queue = None
145        self._draw_thread = None
146        self._draw_counter = 0
147
148        #statistics and server info:
149        self.server_start_time = -1
150        self.server_platform = ""
151        self.server_actual_desktop_size = None
152        self.server_max_desktop_size = None
153        self.server_display = None
154        self.server_randr = False
155        self.pixel_counter = deque(maxlen=1000)
156        self.server_ping_latency = deque(maxlen=1000)
157        self.server_load = None
158        self.client_ping_latency = deque(maxlen=1000)
159        self._server_ok = True
160        self.last_ping_echoed_time = 0
161        self.server_info_request = False
162        self.server_last_info = None
163        self.info_request_pending = False
164        self.screen_size_change_pending = False
165        self.allowed_encodings = []
166        self.core_encodings = None
167        self.encoding = None
168
169        #sound:
170        self.sound_source_plugin = None
171        self.speaker_allowed = False
172        self.speaker_enabled = False
173        self.speaker_codecs = []
174        self.microphone_allowed = False
175        self.microphone_enabled = False
176        self.microphone_codecs = []
177        self.av_sync = False
178        #sound state:
179        self.on_sink_ready = None
180        self.sound_sink = None
181        self.server_sound_sequence = False
182        self.min_sound_sequence = 0
183        self.server_sound_eos_sequence = False
184        self.sound_source = None
185        self.sound_in_bytecount = 0
186        self.sound_out_bytecount = 0
187        self.server_pulseaudio_id = None
188        self.server_pulseaudio_server = None
189        self.server_sound_decoders = []
190        self.server_sound_encoders = []
191        self.server_sound_receive = False
192        self.server_sound_send = False
193        self.server_ogg_latency_fix = False
194        self.queue_used_sent = None
195
196        #rpc / dbus:
197        self.rpc_counter = AtomicInteger()
198        self.rpc_pending_requests = {}
199
200        #mmap:
201        self.mmap_enabled = False
202        self.mmap = None
203        self.mmap_token = None
204        self.mmap_filename = None
205        self.mmap_size = 0
206        self.mmap_group = None
207        self.mmap_tempfile = None
208
209        #features:
210        self.opengl_enabled = False
211        self.opengl_props = {}
212        self.toggle_cursors_bell_notify = False
213        self.toggle_keyboard_sync = False
214        self.force_ungrab = False
215        self.window_unmap = False
216        self.window_refresh_config = False
217        self.server_encodings = []
218        self.server_core_encodings = []
219        self.server_encodings_problematic = PROBLEMATIC_ENCODINGS
220        self.server_encodings_with_speed = ()
221        self.server_encodings_with_quality = ()
222        self.server_encodings_with_lossless = ()
223        self.readonly = False
224        self.windows_enabled = True
225        self.pings = False
226        self.xsettings_enabled = False
227        self.server_dbus_proxy = False
228        self.server_rpc_types = []
229        self.start_new_commands = False
230        self.server_window_frame_extents = False
231        self.server_is_shadow = False
232        self.server_supports_sharing = False
233        self.server_supports_window_filters = False
234        self.server_file_transfer = False
235        self.server_file_size_limit = 10
236        self.server_open_files = False
237        #what we told the server about our encoding defaults:
238        self.encoding_defaults = {}
239
240        self.client_supports_opengl = False
241        self.client_supports_notifications = False
242        self.client_supports_system_tray = False
243        self.client_supports_clipboard = False
244        self.client_supports_cursors = False
245        self.client_supports_bell = False
246        self.client_supports_sharing = False
247        self.client_supports_remote_logging = False
248        self.notifications_enabled = False
249        self.clipboard_enabled = False
250        self.cursors_enabled = False
251        self.bell_enabled = False
252        self.border = None
253
254        self.supports_mmap = MMAP_SUPPORTED
255
256        #helpers and associated flags:
257        self.client_extras = None
258        self.keyboard_helper = None
259        self.kh_warning = False
260        self.clipboard_helper = None
261        self.menu_helper = None
262        self.tray = None
263        self.notifier = None
264        self.in_remote_logging = False
265        self.local_logging = None
266
267        #state:
268        self._focused = None
269        self._window_with_grab = None
270        self._last_screen_settings = None
271        self._suspended_at = 0
272        self._button_state = {}
273        self._on_handshake = []
274
275        self.init_aliases()
276
277
278    def init(self, opts):
279        """ initialize variables from configuration """
280        self.allowed_encodings = opts.encodings
281        self.encoding = opts.encoding
282        self.video_scaling = parse_bool_or_int("video-scaling", opts.video_scaling)
283        self.title = opts.title
284        self.session_name = opts.session_name
285        self.auto_refresh_delay = opts.auto_refresh_delay
286        if opts.max_size:
287            try:
288                self.max_window_size = [int(x.strip()) for x in opts.max_size.split("x", 1)]
289                assert len(self.max_window_size)==2
290            except:
291                #the main script does some checking, but we could be called from a config file launch
292                log.warn("Warning: invalid window max-size specified: %s", opts.max_size)
293                self.max_window_size = 0, 0
294        self.desktop_scaling = opts.desktop_scaling
295        self.can_scale = opts.desktop_scaling not in FALSE_OPTIONS
296        if self.can_scale:
297            self.xscale, self.yscale = self.parse_scaling(opts.desktop_scaling)
298
299        self.dpi = int(opts.dpi)
300        self.xsettings_enabled = opts.xsettings
301        self.supports_mmap = MMAP_SUPPORTED and opts.mmap
302        self.mmap_group = opts.mmap_group
303        self.shadow_fullscreen = opts.shadow_fullscreen
304
305        self.sound_properties = typedict()
306        self.speaker_allowed = sound_option(opts.speaker) in ("on", "off")
307        self.microphone_allowed = sound_option(opts.microphone) in ("on", "off")
308        self.sound_source_plugin = opts.sound_source
309        def sound_option_or_all(*args):
310            return []
311        if self.speaker_allowed or self.microphone_allowed:
312            try:
313                from xpra.sound.gstreamer_util import sound_option_or_all
314                from xpra.sound.wrapper import query_sound
315                self.sound_properties = query_sound()
316                assert self.sound_properties, "query did not return any data"
317                def vinfo(k):
318                    val = self.sound_properties.get(k)
319                    assert val, "%s not found in sound properties" % k
320                    return ".".join(val[:2])
321                log.info("GStreamer version %s for Python %s", vinfo(b"gst.version"), vinfo(b"python.version"))
322            except Exception as e:
323                soundlog.error("Error: failed to query sound subsystem:")
324                soundlog.error(" %s", e)
325                self.speaker_allowed = False
326                self.microphone_allowed = False
327        encoders = self.sound_properties.strlistget("encoders", [])
328        decoders = self.sound_properties.strlistget("decoders", [])
329        self.speaker_codecs = sound_option_or_all("speaker-codec", opts.speaker_codec, decoders)
330        self.microphone_codecs = sound_option_or_all("microphone-codec", opts.microphone_codec, encoders)
331        if not self.speaker_codecs:
332            self.speaker_allowed = False
333        if not self.microphone_codecs:
334            self.microphone_allowed = False
335        self.speaker_enabled = self.speaker_allowed and sound_option(opts.speaker)=="on"
336        self.microphone_enabled = self.microphone_allowed and sound_option(opts.microphone)=="on"
337        self.av_sync = opts.av_sync
338        soundlog("speaker: codecs=%s, allowed=%s, enabled=%s", encoders, self.speaker_allowed, csv(self.speaker_codecs))
339        soundlog("microphone: codecs=%s, allowed=%s, enabled=%s", decoders, self.microphone_allowed, csv(self.microphone_codecs))
340        soundlog("av-sync=%s", self.av_sync)
341
342        self.readonly = opts.readonly
343        self.windows_enabled = opts.windows
344        self.pings = opts.pings
345
346        self.client_supports_notifications = opts.notifications
347        self.client_supports_system_tray = opts.system_tray and SYSTEM_TRAY_SUPPORTED
348        self.client_clipboard_type = opts.clipboard
349        self.client_supports_clipboard = not ((opts.clipboard or "").lower() in FALSE_OPTIONS)
350        self.client_supports_cursors = opts.cursors
351        self.client_supports_bell = opts.bell
352        self.client_supports_sharing = opts.sharing
353        self.client_supports_remote_logging = opts.remote_logging
354
355        #until we add the ability to choose decoders, use all of them:
356        #(and default to non grahics card csc modules if not specified)
357        load_codecs(encoders=False)
358        vh = getVideoHelper()
359        vh.set_modules(video_decoders=opts.video_decoders, csc_modules=opts.csc_modules or NO_GFX_CSC_OPTIONS)
360        vh.init()
361
362
363    def init_ui(self, opts, extra_args=[]):
364        """ initialize user interface """
365        self.init_opengl(opts.opengl)
366
367        if not self.readonly:
368            self.keyboard_helper = self.make_keyboard_helper(opts.keyboard_sync, opts.key_shortcut)
369
370        tray_icon_filename = opts.tray_icon
371        if opts.tray:
372            self.menu_helper = self.make_tray_menu_helper()
373            self.tray = self.setup_xpra_tray(opts.tray_icon)
374            if self.tray:
375                tray_icon_filename = self.tray.get_tray_icon_filename(tray_icon_filename)
376                #keep tray widget hidden until:
377                self.tray.hide()
378                if opts.delay_tray:
379                    def show_tray(*args):
380                        traylog("first ui received, showing tray %s", self.tray)
381                        self.tray.show()
382                    self.connect("first-ui-received", show_tray)
383                else:
384                    #show when the main loop is running:
385                    self.idle_add(self.tray.show)
386
387        if self.client_supports_notifications:
388            self.notifier = self.make_notifier()
389            traylog("using notifier=%s", self.notifier)
390            self.client_supports_notifications = self.notifier is not None
391
392        #audio tagging:
393        if tray_icon_filename and os.path.exists(tray_icon_filename):
394            try:
395                from xpra.sound.pulseaudio_util import set_icon_path
396                set_icon_path(tray_icon_filename)
397            except ImportError as e:
398                log.warn("Warning: failed to set pulseaudio tagging icon:")
399                log.warn(" %s", e)
400
401        if ClientExtras is not None:
402            self.client_extras = ClientExtras(self, opts)
403
404        if opts.border:
405            self.parse_border(opts.border, extra_args)
406
407        #draw thread:
408        self._draw_queue = Queue()
409        self._draw_thread = make_thread(self._draw_thread_loop, "draw")
410
411    def setup_connection(self, conn):
412        XpraClientBase.setup_connection(self, conn)
413        if self.supports_mmap:
414            self.init_mmap(self.mmap_group, conn.filename)
415
416
417    def parse_border(self, border_str, extra_args):
418        #not implemented here (see gtk2 client)
419        pass
420
421    def parse_scaling(self, desktop_scaling):
422        scalinglog("parse_scaling(%s)", desktop_scaling)
423        if desktop_scaling in TRUE_OPTIONS:
424            return 1, 1
425        root_w, root_h = self.get_root_size()
426        if desktop_scaling=="auto":
427            #with auto mode, enable scaling if the desktop is very big:
428            if root_w<=1920 or root_h<=1080:
429                return 1,1              #100% no auto scaling up to 1080p
430            if root_w<=2560 or root_h<=1600:
431                return 1.25,1.25        #125% upscaling up to WQXGA
432            if root_w<=3960 or root_h<=2160:
433                return 1.5,1.5          #150% upscaling up to UHD
434            if root_w<=7680 or root_h<=4320:
435                return 5.0/3,5.0/3      #166% upscaling up to FUHD
436            return 2,2                  #200% if higher (who has this anyway?)
437        def parse_item(v):
438            div = 1
439            try:
440                if v.endswith("%"):
441                    div = 100
442                    v = v[:-1]
443            except:
444                pass
445            if div==1:
446                try:
447                    return int(v)/div       #ie: desktop-scaling=2
448                except:
449                    pass
450            try:
451                return float(v)/div     #ie: desktop-scaling=1.5
452            except:
453                pass
454            #ie: desktop-scaling=3/2, or desktop-scaling=3:2
455            pair = v.replace(":", "/").split("/", 1)
456            try:
457                return float(pair[0])/float(pair[1])
458            except:
459                pass
460            scalinglog.warn("Warning: failed to parse scaling value '%s'", v)
461            return None
462        #split if we have two dimensions: "1600x1200" -> ["1600", "1200"], if not: "2" -> ["2"]
463        values = desktop_scaling.replace(",", "x").split("x", 1)
464        x = parse_item(values[0])
465        if x is None:
466            return 1, 1
467        if len(values)==1:
468            #just one value: use the same for X and Y
469            y = x
470        else:
471            y = parse_item(values[1])
472            if y is None:
473                return 1, 1
474        scalinglog("parse_scaling(%s) parsed items=%s", desktop_scaling, (x, y))
475        #normalize absolute values into floats:
476        if x>MAX_SCALING or y>MAX_SCALING:
477            scalinglog(" normalizing dimensions to a ratio of %ix%i", root_w, root_h)
478            x = float(x / root_w)
479            y = float(y / root_h)
480        if x<MIN_SCALING or y<MIN_SCALING or x>MAX_SCALING or y>MAX_SCALING:
481            scalinglog.warn("Warning: scaling values %sx%s are out of range", x, y)
482            return 1, 1
483        scalinglog("parse_scaling(%s)=%s", desktop_scaling, (x, y))
484        return x, y
485
486
487    def run(self):
488        XpraClientBase.run(self)    #start network threads
489        self._draw_thread.start()
490        self.send_hello()
491
492
493    def quit(self, exit_code=0):
494        raise Exception("override me!")
495
496    def cleanup(self):
497        log("UIXpraClient.cleanup()")
498        XpraClientBase.cleanup(self)
499        #tell the draw thread to exit:
500        dq = self._draw_queue
501        if dq:
502            dq.put(None)
503        self.stop_all_sound()
504        for x in (self.keyboard_helper, self.clipboard_helper, self.tray, self.notifier, self.menu_helper, self.client_extras, getVideoHelper()):
505            if x is None:
506                continue
507            log("UIXpraClient.cleanup() calling %s.cleanup()", type(x))
508            try:
509                x.cleanup()
510            except:
511                log.error("error on %s cleanup", type(x), exc_info=True)
512        #the protocol has been closed, it is now safe to close all the windows:
513        #(cleaner and needed when we run embedded in the client launcher)
514        self.destroy_all_windows()
515        self.clean_mmap()
516        reaper_cleanup()
517        log("UIXpraClient.cleanup() done")
518
519    def stop_all_sound(self):
520        if self.sound_source:
521            self.stop_sending_sound()
522        if self.sound_sink:
523            self.stop_receiving_sound()
524
525    def signal_cleanup(self):
526        log("UIXpraClient.signal_cleanup()")
527        XpraClientBase.signal_cleanup(self)
528        self.stop_all_sound()
529        reaper_cleanup()
530        log("UIXpraClient.signal_cleanup() done")
531
532
533    def destroy_all_windows(self):
534        for wid, window in self._id_to_window.items():
535            try:
536                windowlog("destroy_all_windows() destroying %s / %s", wid, window)
537                self.destroy_window(wid, window)
538            except:
539                pass
540        self._id_to_window = {}
541        self._window_to_id = {}
542
543
544    def suspend(self):
545        log.info("system is suspending")
546        self._suspended_at = time.time()
547        #tell the server to slow down refresh for all the windows:
548        self.control_refresh(-1, True, False)
549
550    def resume(self):
551        elapsed = 0
552        if self._suspended_at>0:
553            elapsed = time.time()-self._suspended_at
554            self._suspended_at = 0
555        delta = datetime.timedelta(seconds=int(elapsed))
556        log.info("system resumed, was suspended for %s", delta)
557        #this will reset the refresh rate too:
558        self.send_refresh_all()
559        if self.opengl_enabled:
560            #with opengl, the buffers sometimes contain garbage after resuming,
561            #this should create new backing buffers:
562            self.reinit_windows()
563
564
565    def control_refresh(self, wid, suspend_resume, refresh, quality=100, options={}, client_properties={}):
566        packet = ["buffer-refresh", wid, 0, quality]
567        if self.window_refresh_config:
568            options["refresh-now"] = bool(refresh)
569            if suspend_resume is True:
570                options["batch"] = {"reset"     : True,
571                                    "delay"     : 1000,
572                                    "locked"    : True,
573                                    "always"    : True}
574            elif suspend_resume is False:
575                options["batch"] = {"reset"     : True}
576            else:
577                pass    #batch unchanged
578            log("sending buffer refresh: options=%s, client_properties=%s", options, client_properties)
579            packet.append(options)
580            packet.append(client_properties)
581        elif not refresh:
582            #we don't really want a refresh, we want to use the "window_refresh_config" feature
583            #but since the server doesn't support it, we can't do anything
584            return
585        self.send(*packet)
586
587    def send_refresh(self, wid):
588        packet = ["buffer-refresh", wid, 0, 100]
589        if self.window_refresh_config:
590            #explicit refresh (should be assumed True anyway),
591            #also force a reset of batch configs:
592            packet.append({
593                           "refresh-now"    : True,
594                           "batch"          : {"reset" : True}
595                           })
596            packet.append({})   #no client_properties
597        self.send(*packet)
598
599    def send_refresh_all(self):
600        log("Automatic refresh for all windows ")
601        self.send_refresh(-1)
602
603
604    def show_about(self, *args):
605        log.warn("show_about() is not implemented in %s", self)
606
607    def show_session_info(self, *args):
608        log.warn("show_session_info() is not implemented in %s", self)
609
610    def show_bug_report(self, *args):
611        log.warn("show_bug_report() is not implemented in %s", self)
612
613
614    def get_encodings(self):
615        """
616            Unlike get_core_encodings(), this method returns "rgb" for both "rgb24" and "rgb32".
617            That's because although we may support both, the encoding chosen is plain "rgb",
618            and the actual encoding used ("rgb24" or "rgb32") depends on the window's bit depth.
619            ("rgb32" if there is an alpha channel, and if the client supports it)
620        """
621        cenc = self.get_core_encodings()
622        if ("rgb24" in cenc or "rgb32" in cenc) and "rgb" not in cenc:
623            cenc.append("rgb")
624        return [x for x in PREFERED_ENCODING_ORDER if x in cenc and x not in ("rgb32", "rgb24")]
625
626    def get_core_encodings(self):
627        if self.core_encodings is None:
628            self.core_encodings = self.do_get_core_encodings()
629        return self.core_encodings
630
631    def do_get_core_encodings(self):
632        """
633            This method returns the actual encodings supported.
634            ie: ["rgb24", "vp8", "webp", "png", "png/L", "png/P", "jpeg", "h264", "vpx"]
635            It is often overriden in the actual client class implementations,
636            where extra encodings can be added (generally just 'rgb32' for transparency),
637            or removed if the toolkit implementation class is more limited.
638        """
639        #we always support rgb24:
640        core_encodings = ["rgb24"]
641        for codec in ("dec_pillow", "dec_webp"):
642            if has_codec(codec):
643                c = get_codec(codec)
644                for e in c.get_encodings():
645                    if e not in core_encodings:
646                        core_encodings.append(e)
647        #we enable all the video decoders we know about,
648        #what will actually get used by the server will still depend on the csc modes supported
649        video_decodings = getVideoHelper().get_decodings()
650        log("video_decodings=%s", video_decodings)
651        for encoding in video_decodings:
652            if encoding not in core_encodings:
653                core_encodings.append(encoding)
654        #remove duplicates and use prefered encoding order:
655        core_encodings = [x for x in PREFERED_ENCODING_ORDER if x in set(core_encodings) and x in self.allowed_encodings]
656        log("do_get_core_encodings()=%s", core_encodings)
657        return core_encodings
658
659
660    def make_keyboard_helper(self, keyboard_sync, key_shortcuts):
661        return KeyboardHelper(self.send, keyboard_sync, key_shortcuts)
662
663    def get_clipboard_helper_classes(self):
664        return []
665
666    def make_clipboard_helper(self):
667        raise Exception("override me!")
668
669
670    def make_notifier(self):
671        nc = self.get_notifier_classes()
672        traylog("make_notifier() notifier classes: %s", nc)
673        return self.make_instance(nc)
674
675    def get_notifier_classes(self):
676        #subclasses will generally add their toolkit specific variants
677        #by overriding this method
678        #use the native ones first:
679        return get_native_notifier_classes()
680
681
682    def make_system_tray(self, *args):
683        """ tray used for application systray forwarding """
684        tc = self.get_system_tray_classes()
685        traylog("make_system_tray%s system tray classes=%s", args, tc)
686        return self.make_instance(tc, self, *args)
687
688    def get_system_tray_classes(self):
689        #subclasses may add their toolkit specific variants, if any
690        #by overriding this method
691        #use the native ones first:
692        return get_native_system_tray_classes()
693
694
695    def make_tray(self, *args):
696        """ tray used by our own application """
697        tc = self.get_tray_classes()
698        traylog("make_tray%s tray classes=%s", args, tc)
699        return self.make_instance(tc, self, *args)
700
701    def get_tray_classes(self):
702        #subclasses may add their toolkit specific variants, if any
703        #by overriding this method
704        #use the native ones first:
705        return get_native_tray_classes()
706
707
708    def make_tray_menu_helper(self):
709        """ menu helper class used by our tray (make_tray / setup_xpra_tray) """
710        mhc = self.get_tray_menu_helper_classes()
711        traylog("make_tray_menu_helper() tray menu helper classes: %s", mhc)
712        return self.make_instance(mhc, self)
713
714    def get_tray_menu_helper_classes(self):
715        #subclasses may add their toolkit specific variants, if any
716        #by overriding this method
717        #use the native ones first:
718        return get_native_tray_menu_helper_classes()
719
720
721    def make_instance(self, class_options, *args):
722        log("make_instance%s", [class_options]+list(args))
723        for c in class_options:
724            try:
725                v = c(*args)
726                log("make_instance(..) %s()=%s", c, v)
727                if v:
728                    return v
729            except:
730                log.error("make_instance%s failed to instantiate %s", class_options+list(args), c, exc_info=True)
731        return None
732
733
734    def show_menu(self, *args):
735        if self.menu_helper:
736            self.menu_helper.activate()
737
738    def setup_xpra_tray(self, tray_icon_filename):
739        tray = None
740        #this is our own tray
741        def xpra_tray_click(button, pressed, time=0):
742            traylog("xpra_tray_click(%s, %s)", button, pressed)
743            if button==1 and pressed:
744                self.menu_helper.activate()
745            elif button==3 and not pressed:
746                self.menu_helper.popup(button, time)
747        def xpra_tray_mouseover(*args):
748            traylog("xpra_tray_mouseover(%s)", args)
749        def xpra_tray_exit(*args):
750            traylog("xpra_tray_exit(%s)", args)
751            self.disconnect_and_quit(0, CLIENT_EXIT)
752        def xpra_tray_geometry(*args):
753            if tray:
754                traylog("xpra_tray_geometry%s geometry=%s", args, tray.get_geometry())
755        menu = None
756        if self.menu_helper:
757            menu = self.menu_helper.build()
758        tray = self.make_tray(menu, self.get_tray_title(), tray_icon_filename, xpra_tray_geometry, xpra_tray_click, xpra_tray_mouseover, xpra_tray_exit)
759        traylog("setup_xpra_tray(%s)=%s", tray_icon_filename, tray)
760        return tray
761
762    def get_tray_title(self):
763        t = []
764        if self.session_name:
765            t.append(self.session_name)
766        if self._protocol and self._protocol._conn:
767            t.append(self._protocol._conn.target)
768        if len(t)==0:
769            t.insert(0, "Xpra")
770        v = "\n".join(t)
771        traylog("get_tray_title()=%s", nonl(v))
772        return v
773
774    def setup_system_tray(self, client, wid, w, h, title):
775        tray_widget = None
776        #this is a tray forwarded for a remote application
777        def tray_click(button, pressed, time=0):
778            tray = self._id_to_window.get(wid)
779            traylog("tray_click(%s, %s, %s) tray=%s", button, pressed, time, tray)
780            if tray:
781                x, y = self.get_mouse_position()
782                modifiers = self.get_current_modifiers()
783                button_packet = ["button-action", wid, button, pressed, (x, y), modifiers]
784                traylog("button_packet=%s", button_packet)
785                self.send_positional(button_packet)
786                tray.reconfigure()
787        def tray_mouseover(x, y):
788            tray = self._id_to_window.get(wid)
789            traylog("tray_mouseover(%s, %s) tray=%s", x, y, tray)
790            if tray:
791                modifiers = self.get_current_modifiers()
792                buttons = []
793                pointer_packet = ["pointer-position", wid, self.cp(x, y), modifiers, buttons]
794                traylog("pointer_packet=%s", pointer_packet)
795                self.send_mouse_position(pointer_packet)
796        def do_tray_geometry(*args):
797            #tell the "ClientTray" where it now lives
798            #which should also update the location on the server if it has changed
799            tray = self._id_to_window.get(wid)
800            if tray_widget:
801                geom = tray_widget.get_geometry()
802            else:
803                geom = None
804            traylog("tray_geometry(%s) widget=%s, geometry=%s tray=%s", args, tray_widget, geom, tray)
805            if tray and geom:
806                tray.move_resize(*geom)
807        def tray_geometry(*args):
808            #the tray widget may still be None if we haven't returned from make_system_tray yet,
809            #in which case we will check the geometry a little bit later:
810            if tray_widget:
811                do_tray_geometry(*args)
812            else:
813                self.idle_add(do_tray_geometry, *args)
814        def tray_exit(*args):
815            traylog("tray_exit(%s)", args)
816        tray_widget = self.make_system_tray(None, title, None, tray_geometry, tray_click, tray_mouseover, tray_exit)
817        traylog("setup_system_tray%s tray_widget=%s", (client, wid, w, h, title), tray_widget)
818        assert tray_widget, "could not instantiate a system tray for tray id %s" % wid
819        tray_widget.show()
820        return ClientTray(client, wid, w, h, tray_widget, self.mmap_enabled, self.mmap)
821
822
823    def desktops_changed(self, *args):
824        workspacelog("desktops_changed%s", args)
825        self.screen_size_changed(*args)
826
827    def workspace_changed(self, *args):
828        workspacelog("workspace_changed%s", args)
829        for win in self._id_to_window.values():
830            win.workspace_changed()
831
832    def screen_size_changed(self, *args):
833        screenlog("screen_size_changed(%s) pending=%s", args, self.screen_size_change_pending)
834        if self.screen_size_change_pending:
835            return
836        #update via timer so the data is more likely to be final (up to date) when we query it,
837        #some properties (like _NET_WORKAREA for X11 clients via xposix "ClientExtras") may
838        #trigger multiple calls to screen_size_changed, delayed by some amount
839        #(sometimes up to 1s..)
840        self.screen_size_change_pending = True
841        delay = 1000
842        #if we are suspending, wait longer:
843        #(better chance that the suspend-resume cycle will have completed)
844        if self._suspended_at>0 and self._suspended_at-time.time()<5*1000:
845            delay = 5*1000
846        self.timeout_add(delay, self.do_process_screen_size_change)
847
848    def do_process_screen_size_change(self):
849        self.update_screen_size()
850        screenlog("do_process_screen_size_change() MONITOR_CHANGE_REINIT=%s, REINIT_WINDOWS=%s", MONITOR_CHANGE_REINIT, REINIT_WINDOWS)
851        if MONITOR_CHANGE_REINIT and MONITOR_CHANGE_REINIT=="0":
852            return
853        if MONITOR_CHANGE_REINIT or REINIT_WINDOWS:
854            screenlog.info("screen size change: will reinit the windows")
855            self.reinit_windows()
856
857
858    def update_screen_size(self):
859        self.screen_size_change_pending = False
860        u_root_w, u_root_h = self.get_root_size()
861        root_w, root_h = self.cp(u_root_w, u_root_h)
862        sss = self.get_screen_sizes(self.xscale, self.yscale)
863        ndesktops = get_number_of_desktops()
864        desktop_names = get_desktop_names()
865        screenlog("update_screen_size() sizes=%s, %s desktops: %s", sss, ndesktops, desktop_names)
866        if self.dpi>0:
867            #use command line value supplied, but scale it:
868            xdpi = self.cx(self.cy(2.0*self.dpi))
869            ydpi = xdpi
870        else:
871            #not supplied, use platform detection code:
872            xdpi = self.cx(get_xdpi())
873            ydpi = self.cy(get_ydpi())
874            screenlog("dpi: %s -> %s", (get_xdpi(), get_ydpi()), (xdpi, ydpi))
875        screen_settings = (root_w, root_h, sss, ndesktops, desktop_names, u_root_w, u_root_h, xdpi, ydpi)
876        screenlog("update_screen_size()     new settings=%s", screen_settings)
877        screenlog("update_screen_size() current settings=%s", self._last_screen_settings)
878        if self._last_screen_settings==screen_settings:
879            log("screen size unchanged")
880            return
881        screenlog.info("sending updated screen size to server: %sx%s with %s screens", root_w, root_h, len(sss))
882        log_screen_sizes(root_w, root_h, sss)
883        self.send("desktop_size", *screen_settings)
884        self._last_screen_settings = screen_settings
885        #update the max packet size (may have gone up):
886        self.set_max_packet_size()
887
888
889    def scaleup(self):
890        scaling = max(self.xscale, self.yscale)
891        options = [v for v in SCALING_OPTIONS if r4cmp(v, 10)>r4cmp(scaling, 10)]
892        scalinglog("scaleup() options>%s : %s", r4cmp(scaling, 1000)/1000.0, options)
893        if options:
894            self._scaleto(min(options))
895
896    def scaledown(self):
897        scaling = max(self.xscale, self.yscale)
898        options = [v for v in SCALING_OPTIONS if r4cmp(v, 10)<r4cmp(scaling, 10)]
899        scalinglog("scaledown() options<%s : %s", r4cmp(scaling, 1000)/1000.0, options)
900        if options:
901            self._scaleto(max(options))
902
903    def _scaleto(self, new_scaling):
904        scaling = max(self.xscale, self.yscale)
905        scalinglog("_scaleto(%s) current value=%s", r4cmp(new_scaling, 1000)/1000.0, r4cmp(scaling, 1000)/1000.0)
906        if new_scaling>0:
907            self.scale_change(new_scaling/scaling, new_scaling/scaling)
908
909    def scalereset(self):
910        self.scaleset(1.0, 1.0)
911
912    def scaleset(self, xscale=1, yscale=1):
913        self.scale_change(xscale/self.xscale, yscale/self.yscale)
914
915    def scale_change(self, xchange=1, ychange=1):
916        if self.server_is_shadow and self.shadow_fullscreen:
917            scalinglog("scale_change(%s, %s) ignored, fullscreen shadow mode is active", xchange, ychange)
918            return
919        if not self.can_scale:
920            scalinglog("scale_change(%s, %s) ignored, scaling is disabled", xchange, ychange)
921            return
922        if self.screen_size_change_pending:
923            scalinglog("scale_change(%s, %s) screen size change is already pending", xchange, ychange)
924            return
925        if time.time()<self.scale_change_embargo:
926            scalinglog("scale_change(%s, %s) screen size change not permitted during embargo time - try again", xchange, ychange)
927            return
928        def clamp(v):
929            return max(MIN_SCALING, min(MAX_SCALING, v))
930        xscale = clamp(self.xscale*xchange)
931        yscale = clamp(self.yscale*ychange)
932        scalinglog("scale_change xscale: clamp(%s*%s)=%s", self.xscale, xchange, xscale)
933        scalinglog("scale_change yscale: clamp(%s*%s)=%s", self.yscale, ychange, yscale)
934        if fequ(xscale, self.xscale) and fequ(yscale, self.yscale):
935            scalinglog("scaling unchanged: %sx%s", self.xscale, self.yscale)
936            return
937        #re-calculate change values against clamped scale:
938        xchange = xscale / self.xscale
939        ychange = yscale / self.yscale
940        #check against maximum server supported size:
941        maxw, maxh = self.server_max_desktop_size
942        root_w, root_h = self.get_root_size()
943        sw = int(root_w / xscale)
944        sh = int(root_h / yscale)
945        scalinglog("scale_change root size=%s x %s, scaled to %s x %s", root_w, root_h, sw, sh)
946        scalinglog("scale_change max server desktop size=%s x %s", maxw, maxh)
947        if not self.server_is_shadow and (sw>(maxw+1) or sh>(maxh+1)):
948            #would overflow..
949            scalinglog.warn("Warning: cannot scale by %i%% x %i%% or lower", (100*xscale), (100*yscale))
950            scalinglog.warn(" the scaled client screen %i x %i -> %i x %i", root_w, root_h, sw, sh)
951            scalinglog.warn(" would overflow the server's screen: %i x %i", maxw, maxh)
952            return
953        self.xscale = xscale
954        self.yscale = yscale
955        scalinglog("scale_change new scaling: %sx%s, change: %sx%s", self.xscale, self.yscale, xchange, ychange)
956        self.scale_reinit(xchange, ychange)
957
958    def scale_reinit(self, xchange=1.0, ychange=1.0):
959        #wait at least one second before changing again:
960        self.scale_change_embargo = time.time()+SCALING_EMBARGO_TIME
961        if fequ(self.xscale, self.yscale):
962            scalinglog.info("setting scaling to %i%%:", iround(100*self.xscale))
963        else:
964            scalinglog.info("setting scaling to %i%% x %i%%:", iround(100*self.xscale), iround(100*self.yscale))
965        self.update_screen_size()
966        #re-initialize all the windows with their new size
967        def new_size_fn(w, h):
968            minx, miny = 16384, 16384
969            if self.max_window_size!=(0, 0):
970                minx, miny = self.max_window_size
971            return max(1, min(minx, int(w*xchange))), max(1, min(miny, int(h*ychange)))
972        self.reinit_windows(new_size_fn)
973        self.emit("scaling-changed")
974
975
976    def get_screen_sizes(self, xscale=1, yscale=1):
977        raise Exception("override me!")
978
979    def get_root_size(self):
980        raise Exception("override me!")
981
982    def set_windows_cursor(self, client_windows, new_cursor):
983        raise Exception("override me!")
984
985    def get_mouse_position(self):
986        raise Exception("override me!")
987
988    def get_current_modifiers(self):
989        raise Exception("override me!")
990
991    def window_bell(self, window, device, percent, pitch, duration, bell_class, bell_id, bell_name):
992        raise Exception("override me!")
993
994
995    def init_mmap(self, mmap_group, socket_filename):
996        log("init_mmap(%s, %s)", mmap_group, socket_filename)
997        from xpra.os_util import get_int_uuid
998        from xpra.net.mmap_pipe import init_client_mmap
999        #calculate size:
1000        root_w, root_h = self.cp(*self.get_root_size())
1001        #at least 256MB, or 8 fullscreen RGBX frames:
1002        mmap_size = max(256*1024*1024, root_w*root_h*4*8)
1003        mmap_size = min(1024*1024*1024, mmap_size)
1004        self.mmap_token = get_int_uuid()
1005        self.mmap_enabled, self.mmap, self.mmap_size, self.mmap_tempfile, self.mmap_filename = \
1006            init_client_mmap(self.mmap_token, mmap_group, socket_filename, mmap_size)
1007
1008    def clean_mmap(self):
1009        log("XpraClient.clean_mmap() mmap_filename=%s", self.mmap_filename)
1010        if self.mmap_tempfile:
1011            try:
1012                self.mmap_tempfile.close()
1013            except Exception as e:
1014                log("clean_mmap error closing file %s: %s", self.mmap_tempfile, e)
1015            self.mmap_tempfile = None
1016        #this should be redundant: closing the tempfile should get it deleted
1017        if self.mmap_filename and os.path.exists(self.mmap_filename):
1018            os.unlink(self.mmap_filename)
1019            self.mmap_filename = None
1020
1021
1022    def init_opengl(self, enable_opengl):
1023        self.opengl_enabled = False
1024        self.client_supports_opengl = False
1025        self.opengl_props = {"info" : "not supported"}
1026
1027
1028    def scale_pointer(self, pointer):
1029        return int(pointer[0]/self.xscale), int(pointer[1]/self.yscale)
1030
1031    def send_button(self, wid, button, pressed, pointer, modifiers, buttons):
1032        def send_button(state):
1033            self.send_positional(["button-action", wid,
1034                                              button, state,
1035                                              pointer, modifiers, buttons])
1036        pressed_state = self._button_state.get(button, False)
1037        if PYTHON3 and WIN32 and pressed_state==pressed:
1038            mouselog("button action: unchanged state, ignoring event")
1039            return
1040        self._button_state[button] = pressed
1041        send_button(pressed)
1042
1043
1044    def window_keyboard_layout_changed(self, window):
1045        #win32 can change the keyboard mapping per window...
1046        keylog("window_keyboard_layout_changed(%s)", window)
1047        if self.keyboard_helper:
1048            self.keyboard_helper.keymap_changed()
1049
1050    def get_keymap_properties(self):
1051        props = self.keyboard_helper.get_keymap_properties()
1052        props["modifiers"] = self.get_current_modifiers()
1053        return  props
1054
1055    def handle_key_action(self, window, key_event):
1056        if self.readonly or self.keyboard_helper is None:
1057            return
1058        wid = self._window_to_id[window]
1059        keylog("handle_key_action(%s, %s) wid=%s", window, key_event, wid)
1060        self.keyboard_helper.handle_key_action(window, wid, key_event)
1061
1062    def mask_to_names(self, mask):
1063        if self.keyboard_helper is None:
1064            return []
1065        return self.keyboard_helper.mask_to_names(mask)
1066
1067
1068    def send_start_command(self, name, command, ignore, sharing=True):
1069        log("send_start_command(%s, %s, %s, %s)", name, command, ignore, sharing)
1070        self.send("start-command", name, command, ignore, sharing)
1071
1072    def send_file(self, filename, data, filesize, openit):
1073        if not self.file_transfer:
1074            filelog.warn("Warning: file transfers are not enabled for %s", self)
1075            return False
1076        assert len(data)>=filesize
1077        data = data[:filesize]          #gio may null terminate it
1078        filelog("send_file%s", (filename, "%i bytes" % filesize, openit))
1079        absfile = os.path.abspath(filename)
1080        basefilename = os.path.basename(filename)
1081        cdata = self.compressed_wrapper("file-data", data)
1082        assert len(cdata)<=filesize     #compressed wrapper ensures this is true
1083        if filesize>self.file_size_limit*1024*1024:
1084            filelog.warn("Warning: cannot upload the file '%s'", basefilename)
1085            filelog.warn(" this file is too large: %sB", std_unit(filesize, unit=1024))
1086            filelog.warn(" the file size limit is %iMB", self.file_size_limit)
1087            return False
1088        if filesize>self.server_file_size_limit*1024*1024:
1089            filelog.warn("Warning: cannot upload the file '%s'", basefilename)
1090            filelog.warn(" this file is too large: %sB", std_unit(filesize, unit=1024))
1091            filelog.warn(" the file size limit for %s is %iMB", self._protocol, self.server_file_size_limit)
1092            return False
1093        printit = False
1094        mimetype = ""
1095        u = hashlib.sha1()
1096        u.update(data)
1097        filelog("sha1 digest(%s)=%s", absfile, u.hexdigest())
1098        options = {"sha1"       : u.hexdigest()}
1099        self.send("send-file", basefilename, mimetype, printit, openit, filesize, cdata, options)
1100        return True
1101
1102
1103    def send_focus(self, wid):
1104        focuslog("send_focus(%s)", wid)
1105        self.send("focus", wid, self.get_current_modifiers())
1106
1107    def update_focus(self, wid, gotit):
1108        focuslog("update_focus(%s, %s) focused=%s, grabbed=%s", wid, gotit, self._focused, self._window_with_grab)
1109        if gotit and self._focused is not wid:
1110            if self.keyboard_helper:
1111                self.keyboard_helper.clear_repeat()
1112            self.send_focus(wid)
1113            self._focused = wid
1114        if not gotit:
1115            if self._window_with_grab:
1116                self.window_ungrab()
1117                self.do_force_ungrab(self._window_with_grab)
1118                self._window_with_grab = None
1119            if wid and self._focused and self._focused!=wid:
1120                #if this window lost focus, it must have had it!
1121                #(catch up - makes things like OR windows work:
1122                # their parent receives the focus-out event)
1123                focuslog("window %s lost a focus it did not have!? (simulating focus before losing it)", wid)
1124                self.send_focus(wid)
1125            if self.keyboard_helper:
1126                self.keyboard_helper.clear_repeat()
1127            if self._focused:
1128                #send the lost-focus via a timer and re-check it
1129                #(this allows a new window to gain focus without having to do a reset_focus)
1130                def send_lost_focus():
1131                    #check that a new window has not gained focus since:
1132                    if self._focused is None:
1133                        self.send_focus(0)
1134                self.timeout_add(20, send_lost_focus)
1135                self._focused = None
1136
1137    def do_force_ungrab(self, wid):
1138        grablog("do_force_ungrab(%s) server supports force ungrab: %s", wid, self.force_ungrab)
1139        if self.force_ungrab:
1140            #ungrab via dedicated server packet:
1141            self.send_force_ungrab(wid)
1142            return
1143        #fallback for older servers: try to find a key to press:
1144        kh = self.keyboard_helper
1145        if not kh:
1146            if not self.kh_warning:
1147                self.kh_warning = True
1148                grablog.warn("no keyboard support, cannot simulate keypress to lose grab!")
1149            return
1150        #xkbmap_keycodes is a list of: (keyval, name, keycode, group, level)
1151        ungrab_keys = [x for x in kh.xkbmap_keycodes if x[1]==UNGRAB_KEY]
1152        if len(ungrab_keys)==0:
1153            if not self.kh_warning:
1154                self.kh_warning = True
1155                grablog.warn("ungrab key %s not found, cannot simulate keypress to lose grab!", UNGRAB_KEY)
1156            return
1157        #ungrab_keys.append((65307, "Escape", 27, 0, 0))     #ugly hardcoded default value
1158        ungrab_key = ungrab_keys[0]
1159        grablog("lost focus whilst window %s has grab, simulating keypress: %s", wid, ungrab_key)
1160        key_event = AdHocStruct()
1161        key_event.keyname = ungrab_key[1]
1162        key_event.pressed = True
1163        key_event.modifiers = []
1164        key_event.keyval = ungrab_key[0]
1165        keycode = ungrab_key[2]
1166        try:
1167            key_event.string = chr(keycode)
1168        except:
1169            key_event.string = str(keycode)
1170        key_event.keycode = keycode
1171        key_event.group = 0
1172        #press:
1173        kh.send_key_action(wid, key_event)
1174        #unpress:
1175        key_event.pressed = False
1176        kh.send_key_action(wid, key_event)
1177
1178    def _process_pointer_grab(self, packet):
1179        wid = packet[1]
1180        window = self._id_to_window.get(wid)
1181        grablog("grabbing %s: %s", wid, window)
1182        if window:
1183            self.window_grab(window)
1184            self._window_with_grab = wid
1185
1186    def window_grab(self, window):
1187        #subclasses should implement this method
1188        pass
1189
1190    def _process_pointer_ungrab(self, packet):
1191        wid = packet[1]
1192        window = self._id_to_window.get(wid)
1193        grablog("ungrabbing %s: %s", wid, window)
1194        self.window_ungrab()
1195        self._window_with_grab = None
1196
1197    def window_ungrab(self):
1198        #subclasses should implement this method
1199        pass
1200
1201
1202    def get_version_info(self):
1203        return get_version_info_full()
1204
1205    def make_hello(self):
1206        capabilities = XpraClientBase.make_hello(self)
1207        updict(capabilities, "platform",  get_platform_info())
1208        if self.readonly:
1209            #don't bother sending keyboard info, as it won't be used
1210            capabilities["keyboard"] = False
1211        else:
1212            for k,v in self.get_keymap_properties().items():
1213                capabilities[k] = v
1214            #show the user a summary of what we have detected:
1215            kb_info = {}
1216            xkbq = capabilities.get("xkbmap_query")
1217            xkbqs = capabilities.get("xkbmap_query_struct")
1218            if xkbqs or xkbq:
1219                if not xkbqs:
1220                    #parse query into a dict
1221                    from xpra.keyboard.layouts import parse_xkbmap_query
1222                    xkbqs = parse_xkbmap_query(xkbq)
1223                for x in ["rules", "model", "layout"]:
1224                    v = xkbqs.get(x)
1225                    if v:
1226                        kb_info[x] = v
1227            if self.keyboard_helper.xkbmap_layout:
1228                kb_info["layout"] = self.keyboard_helper.xkbmap_layout
1229            if len(kb_info)==0:
1230                log.info(" using default keyboard settings")
1231            else:
1232                log.info(" detected keyboard: %s", ", ".join(["%s=%s" % (std(k), std(v)) for k,v in kb_info.items()]))
1233
1234        capabilities["modifiers"] = self.get_current_modifiers()
1235        u_root_w, u_root_h = self.get_root_size()
1236        capabilities["desktop_size"] = self.cp(u_root_w, u_root_h)
1237        ndesktops = get_number_of_desktops()
1238        capabilities["desktops"] = ndesktops
1239        desktop_names = get_desktop_names()
1240        capabilities["desktop.names"] = desktop_names
1241        ss = self.get_screen_sizes()
1242        log.info(" desktop size is %sx%s with %s screen%s:", u_root_w, u_root_h, len(ss), engs(ss))
1243        log_screen_sizes(u_root_w, u_root_h, ss)
1244        if self.xscale!=1 or self.yscale!=1:
1245            capabilities["screen_sizes.unscaled"] = ss
1246            capabilities["desktop_size.unscaled"] = u_root_w, u_root_h
1247            root_w, root_h = self.cp(u_root_w, u_root_h)
1248            if fequ(self.xscale, self.yscale):
1249                sinfo = "%i%%" % iround(self.xscale*100)
1250            else:
1251                sinfo = "%i%% x %i%%" % (iround(self.xscale*100), iround(self.yscale*100))
1252            log.info(" %sscaled by %s, virtual screen size: %ix%i", ["down", "up"][int(u_root_w>root_w or u_root_h>root_h)], sinfo, root_w, root_h)
1253            sss = self.get_screen_sizes(self.xscale, self.yscale)
1254            log_screen_sizes(root_w, root_h, sss)
1255        else:
1256            root_w, root_h = u_root_w, u_root_h
1257            sss = ss
1258        capabilities["screen_sizes"] = sss
1259        #command line (or config file) override supplied:
1260        if self.dpi>0:
1261            #scale it:
1262            dpi = self.cx(self.cy(2.0*self.dpi))
1263            xdpi = self.cx(dpi)
1264            ydpi = self.cy(dpi)
1265        else:
1266            #not supplied, use platform detection code:
1267            xdpi = self.cx(get_xdpi())
1268            ydpi = self.cy(get_ydpi())
1269            dpi = iround((xdpi+ydpi)/2.0)
1270            #platforms may also provide per-axis dpi (later win32 versions do)
1271            capabilities.update({
1272                                 "dpi.x"    : xdpi,
1273                                 "dpi.y"    : ydpi,
1274                                 })
1275        capabilities["dpi"] = dpi
1276        screenlog("dpi: %i, xdpi=%i, ydpi=%i", dpi, xdpi, ydpi)
1277        self._last_screen_settings = (root_w, root_h, sss, ndesktops, desktop_names, u_root_w, u_root_h, xdpi, ydpi)
1278
1279        if self.keyboard_helper:
1280            key_repeat = self.keyboard_helper.keyboard.get_keyboard_repeat()
1281            if key_repeat:
1282                delay_ms,interval_ms = key_repeat
1283                capabilities["key_repeat"] = (delay_ms,interval_ms)
1284            else:
1285                #cannot do keyboard_sync without a key repeat value!
1286                #(maybe we could just choose one?)
1287                self.keyboard_helper.keyboard_sync = False
1288            capabilities["keyboard_sync"] = self.keyboard_helper.keyboard_sync
1289            log("keyboard capabilities: %s", [(k,v) for k,v in capabilities.items() if k.startswith("key")])
1290        if self.mmap_enabled:
1291            capabilities["mmap_file"] = self.mmap_filename
1292            capabilities["mmap_token"] = self.mmap_token
1293        #don't try to find the server uuid if this platform cannot run servers..
1294        #(doing so causes lockups on win32 and startup errors on osx)
1295        if MMAP_SUPPORTED:
1296            #we may be running inside another server!
1297            try:
1298                from xpra.server.server_uuid import get_uuid
1299                capabilities["server_uuid"] = get_uuid() or ""
1300            except:
1301                pass
1302        capabilities.update({
1303            #generic server flags:
1304            "notify-startup-complete"   : True,
1305            "wants_events"              : True,
1306            "randr_notify"              : True,
1307            "screen-scaling"            : True,
1308            "screen-scaling.enabled"    : (self.xscale!=1 or self.yscale!=1),
1309            "screen-scaling.values"     : (int(1000*self.xscale), int(1000*self.yscale)),
1310            #mouse and cursors:
1311            "compressible_cursors"      : True,
1312            "mouse.echo"                : MOUSE_ECHO,
1313            "mouse.initial-position"    : self.get_mouse_position(),
1314            "named_cursors"             : False,
1315            "cursors"                   : self.client_supports_cursors,
1316            "double_click.time"         : get_double_click_time(),
1317            "double_click.distance"     : get_double_click_distance(),
1318            #features:
1319            "notifications"             : self.client_supports_notifications,
1320            "bell"                      : self.client_supports_bell,
1321            "vrefresh"                  : get_vrefresh(),
1322            "share"                     : self.client_supports_sharing,
1323            "windows"                   : self.windows_enabled,
1324            "show-desktop"              : True,
1325            "system_tray"               : self.client_supports_system_tray,
1326            #window meta data and handling:
1327            "generic_window_types"      : True,
1328            "server-window-move-resize" : True,
1329            "server-window-resize"      : True,
1330            #encoding related:
1331            "raw_window_icons"          : True,
1332            "generic-rgb-encodings"     : True,
1333            "auto_refresh_delay"        : int(self.auto_refresh_delay*1000),
1334            "encodings"                 : self.get_encodings(),
1335            "encodings.core"            : self.get_core_encodings(),
1336            #sound:
1337            "sound.server_driven"       : True,
1338            "sound.ogg-latency-fix"     : True,
1339            "av-sync"                   : self.av_sync,
1340            "av-sync.delay.default"     : 0,    #start at 0 and rely on sound-control packets to set the correct value
1341            })
1342        updict(capabilities, "window", {
1343            "raise"                     : True,
1344            #only implemented on posix with the gtk client:
1345            "initiate-moveresize"       : False,
1346            "resize-counter"            : True,
1347            })
1348        updict(capabilities, "clipboard", {
1349            ""                          : self.client_supports_clipboard,
1350            "notifications"             : self.client_supports_clipboard,
1351            "selections"                : CLIPBOARDS,
1352            #buggy osx clipboards:
1353            "want_targets"              : CLIPBOARD_WANT_TARGETS,
1354            #buggy osx and win32 clipboards:
1355            "greedy"                    : CLIPBOARD_GREEDY,
1356            "set_enabled"               : True,
1357            })
1358        updict(capabilities, "encoding", {
1359            "flush"                     : True,
1360            "scaling.control"           : self.video_scaling,
1361            "client_options"            : True,
1362            "csc_atoms"                 : True,
1363            #TODO: check for csc support (swscale only?)
1364            "video_subregion"           : True,
1365            "video_reinit"              : True,
1366            "video_scaling"             : True,
1367            #separate plane is only supported by avcodec2:
1368            "video_separateplane"       : get_codec("dec_avcodec2") is not None,
1369            "webp_leaks"                : False,
1370            "transparency"              : self.has_transparency(),
1371            "rgb24zlib"                 : True,
1372            })
1373        capabilities["antialias"] = get_antialias_info()
1374        capabilities["cursor.size"] = int(2*get_cursor_size()/(self.xscale+self.yscale))
1375        #generic rgb compression flags:
1376        for x in compression.ALL_COMPRESSORS:
1377            capabilities["encoding.rgb_%s" % x] = x in compression.get_enabled_compressors()
1378
1379        control_commands = ["show_session_info", "show_bug_report", "debug"]
1380        for x in compression.get_enabled_compressors():
1381            control_commands.append("enable_"+x)
1382        for x in packet_encoding.get_enabled_encoders():
1383            control_commands.append("enable_"+x)
1384        capabilities["control_commands"] = control_commands
1385        log("control_commands=%s", control_commands)
1386
1387        encoding_caps = {}
1388        if self.encoding:
1389            encoding_caps[""] = self.encoding
1390        for k,v in codec_versions.items():
1391            encoding_caps["%s.version" % k] = v
1392        if self.quality>0:
1393            encoding_caps["quality"] = self.quality
1394        if self.min_quality>0:
1395            encoding_caps["min-quality"] = self.min_quality
1396        if self.speed>=0:
1397            encoding_caps["speed"] = self.speed
1398        if self.min_speed>=0:
1399            encoding_caps["min-speed"] = self.min_speed
1400
1401        #these are the defaults - when we instantiate a window,
1402        #we can send different values as part of the map event
1403        #these are the RGB modes we want (the ones we are expected to be able to paint with):
1404        rgb_formats = ["RGB", "RGBX", "RGBA"]
1405        encoding_caps["rgb_formats"] = rgb_formats
1406        #figure out which CSC modes (usually YUV) can give us those RGB modes:
1407        full_csc_modes = getVideoHelper().get_server_full_csc_modes_for_rgb(*rgb_formats)
1408        log("supported full csc_modes=%s", full_csc_modes)
1409        encoding_caps["full_csc_modes"] = full_csc_modes
1410
1411        if "h264" in self.get_core_encodings():
1412            # some profile options: "baseline", "main", "high", "high10", ...
1413            # set the default to "high10" for I420/YUV420P
1414            # as the python client always supports all the profiles
1415            # whereas on the server side, the default is baseline to accomodate less capable clients.
1416            # I422/YUV422P requires high422, and
1417            # I444/YUV444P requires high444,
1418            # so we don't bother specifying anything for those two.
1419            for old_csc_name, csc_name, default_profile in (
1420                        ("I420", "YUV420P", "high10"),
1421                        ("I422", "YUV422P", ""),
1422                        ("I444", "YUV444P", "")):
1423                profile = default_profile
1424                #try with the old prefix (X264) as well as the more correct one (H264):
1425                for H264_NAME in ("X264", "H264"):
1426                    profile = os.environ.get("XPRA_%s_%s_PROFILE" % (H264_NAME, old_csc_name), profile)
1427                    profile = os.environ.get("XPRA_%s_%s_PROFILE" % (H264_NAME, csc_name), profile)
1428                if profile:
1429                    #send as both old and new names:
1430                    for h264_name in ("x264", "h264"):
1431                        encoding_caps["%s.%s.profile" % (h264_name, old_csc_name)] = profile
1432                        encoding_caps["%s.%s.profile" % (h264_name, csc_name)] = profile
1433            log("x264 encoding options: %s", str([(k,v) for k,v in encoding_caps.items() if k.startswith("x264.")]))
1434        iq = max(self.min_quality, self.quality)
1435        if iq<0:
1436            iq = 70
1437        encoding_caps["initial_quality"] = iq
1438        log("encoding capabilities: %s", encoding_caps)
1439        updict(capabilities, "encoding", encoding_caps)
1440        self.encoding_defaults = encoding_caps
1441        #hack: workaround namespace issue ("encodings" vs "encoding"..)
1442        capabilities["encodings.rgb_formats"] = rgb_formats
1443
1444        if self.sound_properties:
1445            sound_caps = self.sound_properties.copy()
1446            sound_caps["decoders"] = self.speaker_codecs
1447            sound_caps["encoders"] = self.microphone_codecs
1448            sound_caps["send"] = self.microphone_allowed
1449            sound_caps["receive"] = self.speaker_allowed
1450            try:
1451                from xpra.sound.pulseaudio_util import get_info as get_pa_info
1452                sound_caps.update(get_pa_info())
1453            except Exception:
1454                pass
1455            updict(capabilities, "sound", sound_caps)
1456            soundlog("sound capabilities: %s", sound_caps)
1457        #batch options:
1458        for bprop in ("always", "min_delay", "max_delay", "delay", "max_events", "max_pixels", "time_unit"):
1459            evalue = os.environ.get("XPRA_BATCH_%s" % bprop.upper())
1460            if evalue:
1461                try:
1462                    capabilities["batch.%s" % bprop] = int(evalue)
1463                except:
1464                    log.error("invalid environment value for %s: %s", bprop, evalue)
1465        log("batch props=%s", [("%s=%s" % (k,v)) for k,v in capabilities.items() if k.startswith("batch.")])
1466        return capabilities
1467
1468    def has_transparency(self):
1469        return False
1470
1471
1472    def server_ok(self):
1473        return self._server_ok
1474
1475    def check_server_echo(self, ping_sent_time):
1476        if self._protocol is None:
1477            #no longer connected!
1478            return False
1479        last = self._server_ok
1480        if FAKE_BROKEN_CONNECTION>0:
1481            self._server_ok = (int(time.time()) % FAKE_BROKEN_CONNECTION) <= (FAKE_BROKEN_CONNECTION//2)
1482        else:
1483            self._server_ok = not FAKE_BROKEN_CONNECTION and self.last_ping_echoed_time>=ping_sent_time
1484        log("check_server_echo(%s) last=%s, server_ok=%s", ping_sent_time, last, self._server_ok)
1485        if last!=self._server_ok and not self._server_ok:
1486            log.info("server is not responding, drawing spinners over the windows")
1487            def timer_redraw():
1488                if self._protocol is None:
1489                    #no longer connected!
1490                    return False
1491                ok = self.server_ok()
1492                self.redraw_spinners()
1493                if ok:
1494                    log.info("server is OK again")
1495                return not ok           #repaint again until ok
1496            self.idle_add(self.redraw_spinners)
1497            self.timeout_add(250, timer_redraw)
1498        return False
1499
1500    def redraw_spinners(self):
1501        #draws spinner on top of the window, or not (plain repaint)
1502        #depending on whether the server is ok or not
1503        ok = self.server_ok()
1504        log("redraw_spinners() ok=%s", ok)
1505        for w in self._id_to_window.values():
1506            if not w.is_tray():
1507                w.spinner(ok)
1508
1509    def check_echo_timeout(self, ping_time):
1510        log("check_echo_timeout(%s) last_ping_echoed_time=%s", ping_time, self.last_ping_echoed_time)
1511        if self.last_ping_echoed_time<ping_time:
1512            #no point trying to use disconnect_and_quit() to tell the server here..
1513            self.warn_and_quit(EXIT_TIMEOUT, "server ping timeout - waited %s seconds without a response" % PING_TIMEOUT)
1514
1515    def send_ping(self):
1516        now_ms = int(1000.0*time.time())
1517        self.send("ping", now_ms)
1518        self.timeout_add(PING_TIMEOUT*1000, self.check_echo_timeout, now_ms)
1519        wait = 2.0
1520        if len(self.server_ping_latency)>0:
1521            l = [x for _,x in list(self.server_ping_latency)]
1522            avg = sum(l) / len(l)
1523            wait = min(5, 1.0+avg*2.0)
1524            log("average server latency=%.1f, using max wait %.2fs", 1000.0*avg, wait)
1525        self.timeout_add(int(1000.0*wait), self.check_server_echo, now_ms)
1526        return True
1527
1528    def _process_ping_echo(self, packet):
1529        echoedtime, l1, l2, l3, cl = packet[1:6]
1530        self.last_ping_echoed_time = echoedtime
1531        self.check_server_echo(0)
1532        server_ping_latency = time.time()-echoedtime/1000.0
1533        self.server_ping_latency.append((time.time(), server_ping_latency))
1534        self.server_load = l1, l2, l3
1535        if cl>=0:
1536            self.client_ping_latency.append((time.time(), cl/1000.0))
1537        log("ping echo server load=%s, measured client latency=%sms", self.server_load, cl)
1538
1539    def _process_ping(self, packet):
1540        echotime = packet[1]
1541        l1,l2,l3 = 0,0,0
1542        if os.name=="posix":
1543            try:
1544                (fl1, fl2, fl3) = os.getloadavg()
1545                l1,l2,l3 = int(fl1*1000), int(fl2*1000), int(fl3*1000)
1546            except (OSError, AttributeError):
1547                pass
1548        sl = -1
1549        if len(self.server_ping_latency)>0:
1550            _, sl = self.server_ping_latency[-1]
1551        self.send("ping_echo", echotime, l1, l2, l3, int(1000.0*sl))
1552
1553
1554    def _process_server_event(self, packet):
1555        log(u": ".join((str(x) for x in packet[1:])))
1556
1557
1558    def _process_info_response(self, packet):
1559        self.info_request_pending = False
1560        self.server_last_info = packet[1]
1561        log("info-response: %s", self.server_last_info)
1562        if LOG_INFO_RESPONSE:
1563            items = LOG_INFO_RESPONSE.split(",")
1564            logres = [re.compile(v) for v in items]
1565            log.info("info-response debug for %s:", csv(["'%s'" % x for x in items]))
1566            for k in sorted(self.server_last_info.keys()):
1567                if any(lr.match(k) for lr in logres):
1568                    log.info(" %s=%s", k, self.server_last_info[k])
1569
1570    def send_info_request(self):
1571        assert self.server_info_request
1572        if not self.info_request_pending:
1573            self.info_request_pending = True
1574            self.send("info-request", [self.uuid], list(self._id_to_window.keys()))
1575
1576
1577    def send_quality(self):
1578        q = self.quality
1579        assert q==-1 or (q>=0 and q<=100), "invalid quality: %s" % q
1580        self.send("quality", q)
1581
1582    def send_min_quality(self):
1583        q = self.min_quality
1584        assert q==-1 or (q>=0 and q<=100), "invalid quality: %s" % q
1585        self.send("min-quality", q)
1586
1587    def send_speed(self):
1588        s = self.speed
1589        assert s==-1 or (s>=0 and s<=100), "invalid speed: %s" % s
1590        self.send("speed", s)
1591
1592    def send_min_speed(self):
1593        s = self.min_speed
1594        assert s==-1 or (s>=0 and s<=100), "invalid speed: %s" % s
1595        self.send("min-speed", s)
1596
1597
1598    def server_connection_established(self):
1599        if XpraClientBase.server_connection_established(self):
1600            #process the rest from the UI thread:
1601            self.idle_add(self.process_ui_capabilities)
1602
1603
1604    def parse_server_capabilities(self):
1605        if not XpraClientBase.parse_server_capabilities(self):
1606            return  False
1607        c = self.server_capabilities
1608        #enable remote logging asap:
1609        if self.client_supports_remote_logging and c.boolget("remote-logging"):
1610            log.info("enabled remote logging, see server log file for output")
1611            self.local_logging = set_global_logging_handler(self.remote_logging_handler)
1612        if not self.session_name:
1613            self.session_name = c.strget("session_name", "")
1614        from xpra.platform import set_name
1615        set_name("Xpra", self.session_name or "Xpra")
1616        self.window_unmap = c.boolget("window_unmap")
1617        self.window_configure_skip_geometry = c.boolget("window.configure.skip-geometry")
1618        self.window_configure_pointer = c.boolget("window.configure.pointer")
1619        self.force_ungrab = c.boolget("force_ungrab")
1620        self.window_refresh_config = c.boolget("window_refresh_config")
1621        self.server_window_frame_extents = c.boolget("window.frame-extents")
1622        self.suspend_resume = c.boolget("suspend-resume")
1623        self.server_supports_notifications = c.boolget("notifications")
1624        self.notifications_enabled = self.server_supports_notifications and self.client_supports_notifications
1625        self.server_supports_cursors = c.boolget("cursors", True)    #added in 0.5, default to True!
1626        self.cursors_enabled = self.server_supports_cursors and self.client_supports_cursors
1627        self.server_supports_bell = c.boolget("bell")          #added in 0.5, default to True!
1628        self.bell_enabled = self.server_supports_bell and self.client_supports_bell
1629        self.server_supports_clipboard = c.boolget("clipboard")
1630        self.server_supports_clipboard_enable_selections = c.boolget("clipboard.enable-selections")
1631        self.server_clipboards = c.strlistget("clipboards", ALL_CLIPBOARDS)
1632        self.server_compressors = c.strlistget("compressors", ["zlib"])
1633        self.clipboard_enabled = self.client_supports_clipboard and self.server_supports_clipboard
1634        self.server_dbus_proxy = c.boolget("dbus_proxy")
1635        #default for pre-0.16 servers:
1636        if self.server_dbus_proxy:
1637            default_rpc_types = ["dbus"]
1638        else:
1639            default_rpc_types = []
1640        self.server_rpc_types = c.strlistget("rpc-types", default_rpc_types)
1641        self.start_new_commands = c.boolget("start-new-commands")
1642        self.server_file_transfer = c.boolget("file-transfer")
1643        self.server_file_size_limit = c.intget("file-size-limit", 10)
1644        self.server_open_files = c.boolget("open-files")
1645        self.mmap_enabled = self.supports_mmap and self.mmap_enabled and c.boolget("mmap_enabled")
1646        if self.mmap_enabled:
1647            mmap_token = c.intget("mmap_token")
1648            from xpra.net.mmap_pipe import read_mmap_token
1649            token = read_mmap_token(self.mmap)
1650            if token!=mmap_token:
1651                log.error("Error: mmap token verification failed!")
1652                log.error(" expected '%s'", mmap_token)
1653                log.error(" found '%s'", token)
1654                self.mmap_enabled = False
1655                self.quit(EXIT_MMAP_TOKEN_FAILURE)
1656                return
1657            log.info("enabled fast mmap transfers using %sB shared memory area", std_unit(self.mmap_size, unit=1024))
1658        #the server will have a handle on the mmap file by now, safe to delete:
1659        self.clean_mmap()
1660        server_auto_refresh_delay = c.intget("auto_refresh_delay", 0)/1000.0
1661        if server_auto_refresh_delay==0 and self.auto_refresh_delay>0:
1662            log.warn("Warning: server does not support auto-refresh!")
1663        self.server_encodings = c.strlistget("encodings")
1664        self.server_core_encodings = c.strlistget("encodings.core", self.server_encodings)
1665        self.server_encodings_problematic = c.strlistget("encodings.problematic", PROBLEMATIC_ENCODINGS)  #server is telling us to try to avoid those
1666        self.server_encodings_with_speed = c.strlistget("encodings.with_speed", ("h264",)) #old servers only supported x264
1667        self.server_encodings_with_quality = c.strlistget("encodings.with_quality", ("jpeg", "webp", "h264"))
1668        self.server_encodings_with_lossless_mode = c.strlistget("encodings.with_lossless_mode", ())
1669        self.server_start_time = c.intget("start_time", -1)
1670        self.server_platform = c.strget("platform")
1671        self.toggle_cursors_bell_notify = c.boolget("toggle_cursors_bell_notify")
1672        self.toggle_keyboard_sync = c.boolget("toggle_keyboard_sync")
1673
1674        self.server_display = c.strget("display")
1675        self.server_max_desktop_size = c.intpair("max_desktop_size")
1676        self.server_actual_desktop_size = c.intpair("actual_desktop_size")
1677        log("server actual desktop size=%s", self.server_actual_desktop_size)
1678        self.server_randr = c.boolget("resize_screen")
1679        log("server has randr: %s", self.server_randr)
1680        self.server_sound_sequence = c.boolget("sound_sequence")
1681        self.server_sound_eos_sequence = c.boolget("sound.eos-sequence")
1682        self.server_av_sync = c.boolget("av-sync.enabled")
1683        avsynclog("av-sync: server=%s, client=%s", self.server_av_sync, self.av_sync)
1684        self.server_info_request = c.boolget("info-request")
1685        e = c.strget("encoding")
1686        if e:
1687            if self.encoding and e!=self.encoding:
1688                if self.encoding not in self.server_core_encodings:
1689                    log.warn("server does not support %s encoding and has switched to %s", self.encoding, e)
1690                else:
1691                    log.info("server is using %s encoding instead of %s", e, self.encoding)
1692            self.encoding = e
1693        i = platform_name(self._remote_platform, c.strlistget("platform.linux_distribution") or c.strget("platform.release", ""))
1694        r = self._remote_version
1695        if self._remote_revision:
1696            r += "-r%s" % self._remote_revision
1697        mode = c.strget("server.mode", "server")
1698        log.info("Xpra %s server version %s", mode, std(r))
1699        if i:
1700            log.info(" running on %s", std(i))
1701        if c.boolget("proxy"):
1702            proxy_hostname = c.strget("proxy.hostname")
1703            proxy_platform = c.strget("proxy.platform")
1704            proxy_release = c.strget("proxy.platform.release")
1705            proxy_version = c.strget("proxy.version")
1706            proxy_version = c.strget("proxy.build.version", proxy_version)
1707            proxy_distro = c.strget("linux_distribution")
1708            msg = "via: %s proxy version %s" % (platform_name(proxy_platform, proxy_distro or proxy_release), std(proxy_version))
1709            if proxy_hostname:
1710                msg += " on '%s'" % std(proxy_hostname)
1711            log.info(msg)
1712        return True
1713
1714    def process_ui_capabilities(self):
1715        #figure out the maximum actual desktop size and use it to
1716        #calculate the maximum size of a packet (a full screen update packet)
1717        if self.clipboard_enabled:
1718            self.clipboard_helper = self.make_clipboard_helper()
1719            self.clipboard_enabled = self.clipboard_helper is not None
1720            if self.clipboard_enabled and self.server_supports_clipboard_enable_selections:
1721                #tell the server about which selections we really want to sync with
1722                #(could have been translated, or limited if the client only has one, etc)
1723                clipboardlog("clipboard enabled clipboard helper=%s", self.clipboard_helper)
1724                self.send_clipboard_selections(self.clipboard_helper.remote_clipboards)
1725        self.set_max_packet_size()
1726        self.send_deflate_level()
1727        c = self.server_capabilities
1728        server_desktop_size = c.intlistget("desktop_size")
1729        log("server desktop size=%s", server_desktop_size)
1730        self.server_supports_sharing = c.boolget("sharing")
1731        self.server_supports_window_filters = c.boolget("window-filters")
1732        self.server_is_shadow = c.boolget("shadow")
1733        skip_vfb_size_check = False           #if we decide not to use scaling, skip warnings
1734        if not fequ(self.xscale, 1.0) or not fequ(self.yscale, 1.0):
1735            #scaling is used, make sure that we need it and that the server can support it
1736            #(without rounding support, size-hints can cause resize loops)
1737            if self.server_is_shadow and not self.shadow_fullscreen:
1738                #don't honour auto mode in this case
1739                if self.desktop_scaling=="auto":
1740                    log.info(" not scaling a shadow server")
1741                    skip_vfb_size_check = self.xscale>1 or self.yscale>1
1742                    self.scalereset()
1743            elif self.mmap_enabled:
1744                if self.desktop_scaling=="auto":
1745                    log.info(" no need for scaling with mmap")
1746                    skip_vfb_size_check = self.xscale>1 or self.yscale>1
1747                    self.scalereset()
1748                    self.can_scale = False
1749            elif not c.boolget("window.constrain.rounding"):
1750                log.info("Server does not support rounding, disabling scaling")
1751                skip_vfb_size_check = self.xscale>1 or self.yscale>1
1752                self.scalereset()
1753                self.can_scale = False
1754        if self.can_scale:
1755            self.may_adjust_scaling()
1756        if not self.server_is_shadow and not skip_vfb_size_check:
1757            avail_w, avail_h = server_desktop_size
1758            root_w, root_h = self.get_root_size()
1759            if self.cx(root_w)>(avail_w+1) or self.cy(root_h)>(avail_h+1):
1760                log.warn("Server's virtual screen is too small")
1761                log.warn(" server: %sx%s vs client: %sx%s", avail_w, avail_h, self.cx(root_w), self.cy(root_h))
1762                log.warn(" you may see strange behavior,")
1763                log.warn(" please see http://xpra.org/trac/wiki/Xdummy#Configuration")
1764        if self.keyboard_helper:
1765            modifier_keycodes = c.dictget("modifier_keycodes")
1766            if modifier_keycodes:
1767                self.keyboard_helper.set_modifier_mappings(modifier_keycodes)
1768
1769        #sound:
1770        self.server_pulseaudio_id = c.strget("sound.pulseaudio.id")
1771        self.server_pulseaudio_server = c.strget("sound.pulseaudio.server")
1772        self.server_sound_decoders = c.strlistget("sound.decoders", [])
1773        self.server_sound_encoders = c.strlistget("sound.encoders", [])
1774        self.server_sound_receive = c.boolget("sound.receive")
1775        self.server_sound_send = c.boolget("sound.send")
1776        self.server_ogg_latency_fix = c.boolget("sound.ogg-latency-fix", False)
1777        soundlog("pulseaudio id=%s, server=%s, sound decoders=%s, sound encoders=%s, receive=%s, send=%s",
1778                 self.server_pulseaudio_id, self.server_pulseaudio_server,
1779                 csv(self.server_sound_decoders), csv(self.server_sound_encoders),
1780                 self.server_sound_receive, self.server_sound_send)
1781        if self.server_sound_send and self.speaker_enabled:
1782            self.start_receiving_sound()
1783        if self.server_sound_receive and self.microphone_enabled:
1784            self.start_sending_sound()
1785
1786        self.key_repeat_delay, self.key_repeat_interval = c.intpair("key_repeat", (-1,-1))
1787        self.handshake_complete()
1788        #ui may want to know this is now set:
1789        self.emit("clipboard-toggled")
1790        if self.server_supports_clipboard:
1791            #from now on, we will send a message to the server whenever the clipboard flag changes:
1792            self.connect("clipboard-toggled", self.send_clipboard_enabled_status)
1793        if self.toggle_keyboard_sync:
1794            self.connect("keyboard-sync-toggled", self.send_keyboard_sync_enabled_status)
1795        self.send_ping()
1796        if self.pings:
1797            self.timeout_add(1000, self.send_ping)
1798        else:
1799            self.timeout_add(10*1000, self.send_ping)
1800        if not c.boolget("notify-startup-complete"):
1801            #we won't get notified, so assume it is now:
1802            self._startup_complete()
1803
1804    def _startup_complete(self, *args):
1805        log("all the existing windows and system trays have been received: %s items", len(self._id_to_window))
1806        gui_ready()
1807        if self.tray:
1808            self.tray.ready()
1809
1810
1811    def handshake_complete(self):
1812        oh = self._on_handshake
1813        self._on_handshake = None
1814        for cb, args in oh:
1815            try:
1816                cb(*args)
1817            except:
1818                log.error("Error processing handshake callback %s", cb, exc_info=True)
1819
1820    def after_handshake(self, cb, *args):
1821        if self._on_handshake is None:
1822            #handshake has already occurred, just call it:
1823            self.idle_add(cb, *args)
1824        else:
1825            self._on_handshake.append((cb, args))
1826
1827
1828    def remote_logging_handler(self, log, level, msg, *args, **kwargs):
1829        #prevent loops (if our send call ends up firing another logging call):
1830        if self.in_remote_logging:
1831            return
1832        self.in_remote_logging = True
1833        try:
1834            self.send("logging", level, str(msg % args))
1835            exc_info = kwargs.get("exc_info")
1836            if exc_info:
1837                for x in traceback.format_tb(exc_info[2]):
1838                    self.send("logging", level, str(x))
1839        except Exception as e:
1840            if self.exit_code is not None:
1841                #errors can happen during exit, don't care
1842                return
1843            self.local_logging(log, logging.WARNING, "failed to send logging packet: %s" % e)
1844            self.local_logging(log, level, msg, *args, **kwargs)
1845        finally:
1846            self.in_remote_logging = False
1847
1848    def rpc_call(self, rpc_type, rpc_args, reply_handler=None, error_handler=None):
1849        assert rpc_type in self.server_rpc_types, "server does not support %s rpc" % rpc_type
1850        rpcid = self.rpc_counter.increase()
1851        self.rpc_filter_pending()
1852        #keep track of this request (for timeout / error and reply callbacks):
1853        req = time.time(), rpc_type, rpc_args, reply_handler, error_handler
1854        self.rpc_pending_requests[rpcid] = req
1855        rpclog("sending %s rpc request %s to server: %s", rpc_type, rpcid, req)
1856        packet = ["rpc", rpc_type, rpcid] + rpc_args
1857        self.send(*packet)
1858        self.timeout_add(RPC_TIMEOUT, self.rpc_filter_pending)
1859
1860    def rpc_filter_pending(self):
1861        """ removes timed out dbus requests """
1862        for k in list(self.rpc_pending_requests.keys()):
1863            v = self.rpc_pending_requests.get(k)
1864            if v is None:
1865                continue
1866            t, rpc_type, _rpc_args, _reply_handler, ecb = v
1867            if 1000*(time.time()-t)>=RPC_TIMEOUT:
1868                rpclog.warn("%s rpc request: %s has timed out", rpc_type, _rpc_args)
1869                try:
1870                    del self.rpc_pending_requests[k]
1871                    if ecb is not None:
1872                        ecb("timeout")
1873                except Exception as e:
1874                    rpclog.error("Error during timeout handler for %s rpc callback:", rpc_type)
1875                    rpclog.error(" %s", e)
1876
1877    def _process_rpc_reply(self, packet):
1878        rpc_type, rpcid, success, args = packet[1:5]
1879        rpclog("rpc_reply: %s", (rpc_type, rpcid, success, args))
1880        v = self.rpc_pending_requests.get(rpcid)
1881        assert v is not None, "pending dbus handler not found for id %s" % rpcid
1882        assert rpc_type==v[1], "rpc reply type does not match: expected %s got %s" % (v[1], rpc_type)
1883        del self.rpc_pending_requests[rpcid]
1884        if success:
1885            ctype = "ok"
1886            rh = v[-2]      #ok callback
1887        else:
1888            ctype = "error"
1889            rh = v[-1]      #error callback
1890        if rh is None:
1891            rpclog("no %s rpc callback defined, return values=%s", ctype, args)
1892            return
1893        rpclog("calling %s callback %s(%s)", ctype, rh, args)
1894        try:
1895            rh(*args)
1896        except Exception as e:
1897            rpclog.error("Error processing rpc reply handler %s(%s) :", rh, args)
1898            rpclog.error(" %s", e)
1899
1900
1901    def _process_control(self, packet):
1902        command = packet[1]
1903        if command=="show_session_info":
1904            args = packet[2:]
1905            log("calling show_session_info%s on server request", args)
1906            self.show_session_info(*args)
1907        elif command=="show_bug_report":
1908            self.show_bug_report()
1909        elif command in ("enable_%s" % x for x in compression.get_enabled_compressors()):
1910            compressor = command.split("_")[1]
1911            log.info("switching to %s on server request", compressor)
1912            self._protocol.enable_compressor(compressor)
1913        elif command in ("enable_%s" % x for x in packet_encoding.get_enabled_encoders()):
1914            pe = command.split("_")[1]
1915            log.info("switching to %s on server request", pe)
1916            self._protocol.enable_encoder(pe)
1917        elif command=="name":
1918            assert len(args)>=3
1919            self.session_name = args[2]
1920            log.info("session name updated from server: %s", self.session_name)
1921            #TODO: reset tray tooltip, session info title, etc..
1922        elif command=="debug":
1923            args = packet[2:]
1924            if len(args)<2:
1925                log.warn("not enough arguments for debug control command")
1926                return
1927            log_cmd = args[0]
1928            if log_cmd not in ("enable", "disable"):
1929                log.warn("invalid debug control mode: '%s' (must be 'enable' or 'disable')", log_cmd)
1930                return
1931            categories = args[1:]
1932            from xpra.log import add_debug_category, add_disabled_category, enable_debug_for, disable_debug_for
1933            if log_cmd=="enable":
1934                add_debug_category(*categories)
1935                loggers = enable_debug_for(*categories)
1936            else:
1937                assert log_cmd=="disable"
1938                add_disabled_category(*categories)
1939                loggers = disable_debug_for(*categories)
1940            log.info("%sd debugging for: %s", log_cmd, loggers)
1941            return
1942        else:
1943            log.warn("received invalid control command from server: %s", command)
1944
1945
1946    def start_sending_sound(self):
1947        """ (re)start a sound source and emit client signal """
1948        soundlog("start_sending_sound()")
1949        assert self.microphone_allowed, "microphone forwarding is disabled"
1950        assert self.server_sound_receive, "client support for receiving sound is disabled"
1951        from xpra.sound.gstreamer_util import ALLOW_SOUND_LOOP, loop_warning
1952        if self._remote_machine_id and self._remote_machine_id==get_machine_id() and not ALLOW_SOUND_LOOP:
1953            #looks like we're on the same machine, verify it's a different user:
1954            if self._remote_uuid==get_user_uuid():
1955                loop_warning("microphone", self._remote_uuid)
1956                return
1957
1958        ss = self.sound_source
1959        if ss:
1960            if ss.get_state()=="active":
1961                soundlog.error("Error: microphone forwarding is already active")
1962                return
1963            ss.start()
1964        elif not self.start_sound_source():
1965            return
1966        self.microphone_enabled = True
1967        self.emit("microphone-changed")
1968        soundlog("start_sending_sound() done")
1969
1970    def start_sound_source(self):
1971        soundlog("start_sound_source()")
1972        assert self.sound_source is None
1973        def sound_source_state_changed(*args):
1974            self.emit("microphone-changed")
1975        #find the matching codecs:
1976        matching_codecs = [x for x in self.microphone_codecs if x in self.server_sound_decoders]
1977        soundlog("start_sound_source() matching codecs: %s", csv(matching_codecs))
1978        if len(matching_codecs)==0:
1979            log.error("Error: no matching codecs between client and server")
1980            log.error(" server supports: %s", csv(self.server_sound_decoders))
1981            log.error(" client supports: %s", csv(self.microphone_codecs))
1982            return False
1983        try:
1984            from xpra.sound.wrapper import start_sending_sound
1985            plugins = self.sound_properties.get("plugins")
1986            ss = start_sending_sound(plugins, self.sound_source_plugin, None, 1.0, matching_codecs, self.server_pulseaudio_server, self.server_pulseaudio_id)
1987            if not ss:
1988                return False
1989            self.sound_source = ss
1990            ss.connect("new-buffer", self.new_sound_buffer)
1991            ss.connect("state-changed", sound_source_state_changed)
1992            ss.connect("new-stream", self.new_stream)
1993            ss.start()
1994            soundlog("start_sound_source() sound source %s started", ss)
1995            return True
1996        except Exception as e:
1997            log.error("error setting up sound: %s", e)
1998            return False
1999
2000    def new_stream(self, sound_source, codec):
2001        soundlog("new_stream(%s)", codec)
2002        if self.sound_source!=sound_source:
2003            soundlog("dropping new-stream signal (current source=%s, signal source=%s)", self.sound_source, sound_source)
2004            return
2005        sound_source.codec = codec
2006        #tell the server this is the start:
2007        self.send("sound-data", sound_source.codec, "",
2008                  {"start-of-stream"    : True,
2009                   "codec"              : sound_source.codec,
2010                   "sequence"           : sound_source.sequence})
2011
2012    def stop_sending_sound(self):
2013        """ stop the sound source and emit client signal """
2014        soundlog("stop_sending_sound() sound source=%s", self.sound_source)
2015        ss = self.sound_source
2016        self.microphone_enabled = False
2017        self.sound_source = None
2018        if ss is None:
2019            log.warn("stop_sending_sound: sound not started!")
2020            return
2021        ss.cleanup()
2022        self.emit("microphone-changed")
2023
2024    def start_receiving_sound(self):
2025        """ ask the server to start sending sound and emit the client signal """
2026        soundlog("start_receiving_sound() sound sink=%s", self.sound_sink)
2027        if self.sound_sink is not None:
2028            soundlog("start_receiving_sound: we already have a sound sink")
2029            return
2030        elif not self.server_sound_send:
2031            log.error("Error receiving sound: support not enabled on the server")
2032            return
2033        #choose a codec:
2034        matching_codecs = [x for x in self.speaker_codecs if x in self.server_sound_encoders]
2035        soundlog("start_receiving_sound() matching codecs: %s", csv(matching_codecs))
2036        if len(matching_codecs)==0:
2037            log.error("Error: no matching codecs between client and server")
2038            log.error(" server supports: %s", csv(self.server_sound_encoders))
2039            log.error(" client supports: %s", csv(self.speaker_codecs))
2040            return
2041        codec = matching_codecs[0]
2042        if not self.server_ogg_latency_fix and codec in ("flac", "opus", "speex"):
2043            log.warn("Warning: this server's sound support is out of date")
2044            log.warn(" the sound latency with the %s codec will be high", codec)
2045        self.speaker_enabled = True
2046        self.emit("speaker-changed")
2047        def sink_ready(*args):
2048            soundlog("sink_ready(%s) codec=%s", args, codec)
2049            self.send("sound-control", "start", codec)
2050            return False
2051        self.on_sink_ready = sink_ready
2052        self.start_sound_sink(codec)
2053
2054    def stop_receiving_sound(self, tell_server=True):
2055        """ ask the server to stop sending sound, toggle flag so we ignore further packets and emit client signal """
2056        soundlog("stop_receiving_sound(%s) sound sink=%s", tell_server, self.sound_sink)
2057        ss = self.sound_sink
2058        self.speaker_enabled = False
2059        if tell_server:
2060            self.send("sound-control", "stop", self.min_sound_sequence)
2061        if self.server_sound_sequence:
2062            self.min_sound_sequence += 1
2063            self.send("sound-control", "new-sequence", self.min_sound_sequence)
2064        if ss is None:
2065            return
2066        self.sound_sink = None
2067        soundlog("stop_receiving_sound(%s) calling %s", tell_server, ss.cleanup)
2068        ss.cleanup()
2069        self.emit("speaker-changed")
2070        soundlog("stop_receiving_sound(%s) done", tell_server)
2071
2072    def sound_sink_state_changed(self, sound_sink, state):
2073        if sound_sink!=self.sound_sink:
2074            soundlog("sound_sink_state_changed(%s, %s) not the current sink, ignoring it", sound_sink, state)
2075            return
2076        soundlog("sound_sink_state_changed(%s, %s) on_sink_ready=%s", sound_sink, state, self.on_sink_ready)
2077        if state==b"ready" and self.on_sink_ready:
2078            if not self.on_sink_ready():
2079                self.on_sink_ready = None
2080        self.emit("speaker-changed")
2081    def sound_sink_bitrate_changed(self, sound_sink, bitrate):
2082        if sound_sink!=self.sound_sink:
2083            soundlog("sound_sink_bitrate_changed(%s, %s) not the current sink, ignoring it", sound_sink, bitrate)
2084            return
2085        soundlog("sound_sink_bitrate_changed(%s, %s)", sound_sink, bitrate)
2086        #not shown in the UI, so don't bother with emitting a signal:
2087        #self.emit("speaker-changed")
2088    def sound_sink_error(self, sound_sink, error):
2089        if sound_sink!=self.sound_sink:
2090            soundlog("sound_sink_error(%s, %s) not the current sink, ignoring it", sound_sink, error)
2091            return
2092        soundlog.warn("stopping speaker because of error: %s", error)
2093        self.stop_receiving_sound()
2094    def sound_process_stopped(self, sound_sink, *args):
2095        if sound_sink!=self.sound_sink:
2096            soundlog("sound_process_stopped(%s, %s) not the current sink, ignoring it", sound_sink, args)
2097            return
2098        soundlog.warn("the sound process has stopped")
2099        self.stop_receiving_sound()
2100
2101    def sound_sink_exit(self, sound_sink, *args):
2102        log("sound_sink_exit(%s, %s) sound_sink=%s", sound_sink, args, self.sound_sink)
2103        ss = self.sound_sink
2104        if sound_sink!=ss:
2105            soundlog("sound_sink_exit() not the current sink, ignoring it")
2106            return
2107        if ss and ss.codec:
2108            #the mandatory "I've been naughty warning":
2109            #we use the "codec" field as guard to ensure we only print this warning once..
2110            soundlog.warn("the %s sound sink has stopped", ss.codec)
2111            ss.codec = ""
2112        self.stop_receiving_sound()
2113
2114    def start_sound_sink(self, codec):
2115        soundlog("start_sound_sink(%s)", codec)
2116        assert self.sound_sink is None, "sound sink already exists!"
2117        try:
2118            soundlog("starting %s sound sink", codec)
2119            from xpra.sound.wrapper import start_receiving_sound
2120            ss = start_receiving_sound(codec)
2121            if not ss:
2122                return False
2123            self.sound_sink = ss
2124            ss.connect("state-changed", self.sound_sink_state_changed)
2125            ss.connect("error", self.sound_sink_error)
2126            ss.connect("exit", self.sound_sink_exit)
2127            from xpra.net.protocol import Protocol
2128            ss.connect(Protocol.CONNECTION_LOST, self.sound_process_stopped)
2129            ss.start()
2130            soundlog("%s sound sink started", codec)
2131            return True
2132        except Exception as e:
2133            soundlog.error("failed to start sound sink", exc_info=True)
2134            self.sound_sink_error(self.sound_sink, e)
2135            return False
2136
2137    def new_sound_buffer(self, sound_source, data, metadata):
2138        soundlog("new_sound_buffer(%s, %s, %s)", sound_source, len(data or []), metadata)
2139        if self.sound_source:
2140            self.sound_out_bytecount += len(data)
2141            self.send("sound-data", self.sound_source.codec, compression.Compressed(self.sound_source.codec, data), metadata)
2142
2143    def _process_sound_data(self, packet):
2144        codec, data, metadata = packet[1:4]
2145        codec = bytestostr(codec)
2146        metadata = typedict(metadata)
2147        if data:
2148            self.sound_in_bytecount += len(data)
2149        #verify sequence number if present:
2150        seq = metadata.intget("sequence", -1)
2151        if self.min_sound_sequence>0 and seq>=0 and seq<self.min_sound_sequence:
2152            soundlog("ignoring sound data with old sequence number %s (now on %s)", seq, self.min_sound_sequence)
2153            return
2154
2155        if not self.speaker_enabled:
2156            if metadata.boolget("start-of-stream"):
2157                #server is asking us to start playing sound
2158                if not self.speaker_allowed:
2159                    #no can do!
2160                    soundlog.warn("Warning: cannot honour the request to start the speaker")
2161                    soundlog.warn(" speaker forwarding is disabled")
2162                    self.stop_receiving_sound(True)
2163                    return
2164                self.speaker_enabled = True
2165                self.emit("speaker-changed")
2166                self.on_sink_ready = None
2167                codec = metadata.strget("codec")
2168                soundlog("starting speaker on server request using codec %s", codec)
2169                self.start_sound_sink(codec)
2170            else:
2171                soundlog("speaker is now disabled - dropping packet")
2172                return
2173        ss = self.sound_sink
2174        if ss is None:
2175            soundlog("no sound sink to process sound data, dropping it")
2176            return
2177        if metadata.boolget("end-of-stream"):
2178            soundlog("server sent end-of-stream for sequence %s, closing sound pipeline", seq)
2179            self.stop_receiving_sound(False)
2180            return
2181        if codec!=ss.codec:
2182            soundlog.error("sound codec change not supported! (from %s to %s)", ss.codec, codec)
2183            self.stop_receiving_sound()
2184            return
2185        elif ss.get_state()=="stopped":
2186            soundlog("sound data received, sound sink is stopped - telling server to stop")
2187            self.stop_receiving_sound()
2188            return
2189        #(some packets (ie: sos, eos) only contain metadata)
2190        if len(data)>0:
2191            ss.add_data(data, metadata)
2192        if self.av_sync and self.server_av_sync:
2193            info = ss.get_info()
2194            queue_used = info.get("queue.cur")
2195            if queue_used is None:
2196                return
2197            delta = (self.queue_used_sent or 0)-queue_used
2198            #avsynclog("server sound sync: queue info=%s, last sent=%s, delta=%s", dict((k,v) for (k,v) in info.items() if k.startswith("queue")), self.queue_used_sent, delta)
2199            if self.queue_used_sent is None or abs(delta)>=80:
2200                avsynclog("server sound sync: sending updated queue.used=%i (was %s)", queue_used, (self.queue_used_sent or "unset"))
2201                self.queue_used_sent = queue_used
2202                v = queue_used + AV_SYNC_DELTA
2203                if AV_SYNC_DELTA:
2204                    avsynclog(" adjusted value=%i with sync delta=%i", v, AV_SYNC_DELTA)
2205                self.send("sound-control", "sync", v)
2206
2207
2208    def send_notify_enabled(self):
2209        assert self.client_supports_notifications, "cannot toggle notifications: the feature is disabled by the client"
2210        assert self.server_supports_notifications, "cannot toggle notifications: the feature is disabled by the server"
2211        assert self.toggle_cursors_bell_notify, "cannot toggle notifications: server lacks the feature"
2212        self.send("set-notify", self.notifications_enabled)
2213
2214    def send_bell_enabled(self):
2215        assert self.client_supports_bell, "cannot toggle bell: the feature is disabled by the client"
2216        assert self.server_supports_bell, "cannot toggle bell: the feature is disabled by the server"
2217        assert self.toggle_cursors_bell_notify, "cannot toggle bell: server lacks the feature"
2218        self.send("set-bell", self.bell_enabled)
2219
2220    def send_cursors_enabled(self):
2221        assert self.client_supports_cursors, "cannot toggle cursors: the feature is disabled by the client"
2222        assert self.server_supports_cursors, "cannot toggle cursors: the feature is disabled by the server"
2223        assert self.toggle_cursors_bell_notify, "cannot toggle cursors: server lacks the feature"
2224        self.send("set-cursors", self.cursors_enabled)
2225
2226    def send_force_ungrab(self, wid):
2227        assert self.force_ungrab
2228        self.send("force-ungrab", wid)
2229
2230    def set_deflate_level(self, level):
2231        self.compression_level = level
2232        self.send_deflate_level()
2233
2234    def send_deflate_level(self):
2235        self._protocol.set_compression_level(self.compression_level)
2236        self.send("set_deflate", self.compression_level)
2237
2238
2239    def _process_clipboard_enabled_status(self, packet):
2240        clipboard_enabled, reason = packet[1:3]
2241        if self.clipboard_enabled!=clipboard_enabled:
2242            clipboardlog.info("clipboard toggled to %s by the server, reason given:", ["off", "on"][int(clipboard_enabled)])
2243            clipboardlog.info(" %s", reason)
2244            self.clipboard_enabled = bool(clipboard_enabled)
2245            self.emit("clipboard-toggled")
2246
2247    def send_clipboard_enabled_status(self, *args):
2248        clipboardlog("send_clipboard_enabled_status%s clipboard_enabled=%s", args, self.clipboard_enabled)
2249        self.send("set-clipboard-enabled", self.clipboard_enabled)
2250
2251    def send_clipboard_selections(self, selections):
2252        clipboardlog("send_clipboard_selections(%s) server_supports_clipboard_enable_selections=%s", selections, self.server_supports_clipboard_enable_selections)
2253        if self.server_supports_clipboard_enable_selections:
2254            self.send("clipboard-enable-selections", selections)
2255
2256    def send_keyboard_sync_enabled_status(self, *args):
2257        self.send("set-keyboard-sync-enabled", self.keyboard_sync)
2258
2259
2260    def set_encoding(self, encoding):
2261        log("set_encoding(%s)", encoding)
2262        assert encoding in self.get_encodings(), "encoding %s is not supported!" % encoding
2263        assert encoding in self.server_encodings, "encoding %s is not supported by the server! (only: %s)" % (encoding, self.server_encodings)
2264        self.encoding = encoding
2265        self.send("encoding", encoding)
2266
2267
2268    def reset_cursor(self):
2269        self.set_windows_cursor(self._id_to_window.values(), [])
2270
2271    def _ui_event(self):
2272        if self._ui_events==0:
2273            self.emit("first-ui-received")
2274        self._ui_events += 1
2275
2276
2277    def sx(self, v):
2278        """ convert X coordinate from server to client """
2279        return iround(v*self.xscale)
2280    def sy(self, v):
2281        """ convert Y coordinate from server to client """
2282        return iround(v*self.yscale)
2283    def srect(self, x, y, w, h):
2284        """ convert rectangle coordinates from server to client """
2285        return self.sx(x), self.sy(y), self.sx(w), self.sy(h)
2286    def sp(self, x, y):
2287        """ convert X,Y coordinates from server to client """
2288        return self.sx(x), self.sy(y)
2289
2290    def cx(self, v):
2291        """ convert X coordinate from client to server """
2292        return iround(v/self.xscale)
2293    def cy(self, v):
2294        """ convert Y coordinate from client to server """
2295        return iround(v/self.yscale)
2296    def crect(self, x, y, w, h):
2297        """ convert rectangle coordinates from client to server """
2298        return self.cx(x), self.cy(y), self.cx(w), self.cy(h)
2299    def cp(self, x, y):
2300        """ convert X,Y coordinates from client to server """
2301        return self.cx(x), self.cy(y)
2302
2303
2304    def _process_new_common(self, packet, override_redirect):
2305        self._ui_event()
2306        wid, x, y, w, h = packet[1:6]
2307        metadata = self.cook_metadata(True, packet[6])
2308        windowlog("process_new_common: %s, metadata=%s, OR=%s", packet[1:7], metadata, override_redirect)
2309        assert wid not in self._id_to_window, "we already have a window %s" % wid
2310        if w<1 or h<1:
2311            windowlog.error("window dimensions are wrong: %sx%s", w, h)
2312            w, h = 1, 1
2313        x = self.sx(x)
2314        y = self.sy(y)
2315        bw, bh = w, h
2316        ww = max(1, self.sx(w))
2317        wh = max(1, self.sy(h))
2318        client_properties = {}
2319        if len(packet)>=8:
2320            client_properties = packet[7]
2321        self.make_new_window(wid, x, y, ww, wh, bw, bh, metadata, override_redirect, client_properties)
2322
2323    def cook_metadata(self, new_window, metadata):
2324        #convert to a typedict and apply client-side overrides:
2325        metadata = typedict(metadata)
2326        if self.server_is_shadow and self.shadow_fullscreen:
2327            #force it fullscreen:
2328            try:
2329                del metadata["size-constraints"]
2330            except:
2331                pass
2332            metadata["fullscreen"] = True
2333            #FIXME: try to figure out the monitors we go fullscreen on for X11:
2334            #if os.name=="posix":
2335            #    metadata["fullscreen-monitors"] = [0, 1, 0, 1]
2336        return metadata
2337
2338    def make_new_window(self, wid, x, y, ww, wh, bw, bh, metadata, override_redirect, client_properties):
2339        client_window_classes = self.get_client_window_classes(ww, wh, metadata, override_redirect)
2340        group_leader_window = self.get_group_leader(wid, metadata, override_redirect)
2341        #workaround for "popup" OR windows without a transient-for (like: google chrome popups):
2342        #prevents them from being pushed under other windows on OSX
2343        #find a "transient-for" value using the pid to find a suitable window
2344        #if possible, choosing the currently focused window (if there is one..)
2345        pid = metadata.intget("pid", 0)
2346        if override_redirect and pid>0 and metadata.intget("transient-for", 0)>0 is None and metadata.get("role")=="popup":
2347            tfor = None
2348            for twid, twin in self._id_to_window.items():
2349                if not twin._override_redirect and twin._metadata.intget("pid", -1)==pid:
2350                    tfor = twin
2351                    if twid==self._focused:
2352                        break
2353            if tfor:
2354                windowlog("forcing transient for=%s for new window %s", twid, wid)
2355                metadata["transient-for"] = twid
2356        window = None
2357        windowlog("make_new_window(..) client_window_classes=%s, group_leader_window=%s", client_window_classes, group_leader_window)
2358        for cwc in client_window_classes:
2359            try:
2360                window = cwc(self, group_leader_window, wid, x, y, ww, wh, bw, bh, metadata, override_redirect, client_properties, self.border, self.max_window_size)
2361                break
2362            except:
2363                windowlog.warn("failed to instantiate %s", cwc, exc_info=True)
2364        if window is None:
2365            windowlog.warn("no more options.. this window will not be shown, sorry")
2366            return None
2367        self._id_to_window[wid] = window
2368        self._window_to_id[window] = wid
2369        window.show()
2370        return window
2371
2372
2373    def freeze(self):
2374        log("freeze()")
2375        for window in self._id_to_window.values():
2376            window.freeze()
2377
2378
2379    def reinit_windows(self, new_size_fn=None):
2380        assert self.window_unmap, "server support for 'window_unmap' is required for reinitializing windows"
2381        def fake_send(*args):
2382            log("fake_send%s", args)
2383        #now replace all the windows with new ones:
2384        for wid, window in self._id_to_window.items():
2385            if not window:
2386                continue
2387            if window.is_tray():
2388                #trays are never GL enabled, so don't bother re-creating them
2389                #might cause problems anyway if we did
2390                #just send a configure event in case they are moved / scaled
2391                window.send_configure()
2392                continue
2393            #ignore packets from old window:
2394            window.send = fake_send
2395            #copy attributes:
2396            x, y = window._pos
2397            ww, wh = window._size
2398            if new_size_fn:
2399                ww, wh = new_size_fn(ww, wh)
2400            try:
2401                bw, bh = window._backing.size
2402            except:
2403                bw, bh = ww, wh
2404            client_properties = window._client_properties
2405            resize_counter = window._resize_counter
2406            metadata = window._metadata
2407            override_redirect = window._override_redirect
2408            backing = window._backing
2409            current_icon = window._current_icon
2410            delta_pixel_data, video_decoder, csc_decoder, decoder_lock = None, None, None, None
2411            try:
2412                if backing:
2413                    delta_pixel_data = backing._delta_pixel_data
2414                    video_decoder = backing._video_decoder
2415                    csc_decoder = backing._csc_decoder
2416                    decoder_lock = backing._decoder_lock
2417                    if decoder_lock:
2418                        decoder_lock.acquire()
2419                        windowlog("reinit_windows() will preserve video=%s and csc=%s for %s", video_decoder, csc_decoder, wid)
2420                        backing._video_decoder = None
2421                        backing._csc_decoder = None
2422                        backing._decoder_lock = None
2423
2424                #now we can unmap it:
2425                self.destroy_window(wid, window)
2426                #explicitly tell the server we have unmapped it:
2427                #(so it will reset the video encoders, etc)
2428                if not window.is_OR():
2429                    self.send("unmap-window", wid)
2430                try:
2431                    del self._id_to_window[wid]
2432                except:
2433                    pass
2434                try:
2435                    del self._window_to_id[window]
2436                except:
2437                    pass
2438                #create the new window,
2439                #which should honour the new state of the opengl_enabled flag if that's what we changed,
2440                #or the new dimensions, etc
2441                window = self.make_new_window(wid, x, y, ww, wh, bw, bh, metadata, override_redirect, client_properties)
2442                window._resize_counter = resize_counter
2443                if video_decoder or csc_decoder:
2444                    backing = window._backing
2445                    backing._delta_pixel_data = delta_pixel_data
2446                    backing._video_decoder = video_decoder
2447                    backing._csc_decoder = csc_decoder
2448                    backing._decoder_lock = decoder_lock
2449                if current_icon:
2450                    window.update_icon(*current_icon)
2451            finally:
2452                if decoder_lock:
2453                    decoder_lock.release()
2454        self.send_refresh_all()
2455
2456
2457    def get_group_leader(self, wid, metadata, override_redirect):
2458        #subclasses that wish to implement the feature may override this method
2459        return None
2460
2461
2462    def get_client_window_classes(self, w, h, metadata, override_redirect):
2463        return [self.ClientWindowClass]
2464
2465    def _process_new_window(self, packet):
2466        self._process_new_common(packet, False)
2467
2468    def _process_new_override_redirect(self, packet):
2469        self._process_new_common(packet, True)
2470
2471    def _process_new_tray(self, packet):
2472        assert SYSTEM_TRAY_SUPPORTED
2473        self._ui_event()
2474        wid, w, h = packet[1:4]
2475        w = max(1, self.sx(w))
2476        h = max(1, self.sy(h))
2477        metadata = typedict()
2478        if len(packet)>=5:
2479            metadata = self.cook_metadata(True, packet[4])
2480        assert wid not in self._id_to_window, "we already have a window %s" % wid
2481        tray = self.setup_system_tray(self, wid, w, h, metadata.get("title", ""))
2482        traylog("process_new_tray(%s) tray=%s", packet, tray)
2483        self._id_to_window[wid] = tray
2484        self._window_to_id[tray] = wid
2485
2486    def _process_window_move_resize(self, packet):
2487        wid, x, y, w, h = packet[1:6]
2488        ax = self.sx(x)
2489        ay = self.sy(y)
2490        aw = max(1, self.sx(w))
2491        ah = max(1, self.sy(h))
2492        resize_counter = -1
2493        if len(packet)>4:
2494            resize_counter = packet[4]
2495        window = self._id_to_window.get(wid)
2496        geomlog("_process_window_move_resize%s moving / resizing window %s (id=%s) to %s", packet[1:], window, wid, (ax, ay, aw, ah))
2497        if window:
2498            window.move_resize(ax, ay, aw, ah, resize_counter)
2499
2500    def _process_window_resized(self, packet):
2501        wid, w, h = packet[1:4]
2502        aw = max(1, self.sx(w))
2503        ah = max(1, self.sy(h))
2504        resize_counter = -1
2505        if len(packet)>4:
2506            resize_counter = packet[4]
2507        window = self._id_to_window.get(wid)
2508        geomlog("_process_window_resized%s resizing window %s (id=%s) to %s", packet[1:], window, wid, (aw,ah))
2509        if window:
2510            window.resize(aw, ah, resize_counter)
2511
2512    def _process_draw(self, packet):
2513        self._draw_queue.put(packet)
2514
2515    def send_damage_sequence(self, wid, packet_sequence, width, height, decode_time, message=""):
2516        self.send_now("damage-sequence", packet_sequence, wid, width, height, decode_time, message)
2517
2518    def _draw_thread_loop(self):
2519        while self.exit_code is None:
2520            packet = self._draw_queue.get()
2521            if packet is None:
2522                break
2523            try:
2524                self._do_draw(packet)
2525                time.sleep(0)
2526            except KeyboardInterrupt:
2527                raise
2528            except:
2529                log.error("error processing draw packet", exc_info=True)
2530        log("draw thread ended")
2531
2532    def _do_draw(self, packet):
2533        """ this runs from the draw thread above """
2534        wid, x, y, width, height, coding, data, packet_sequence, rowstride = packet[1:10]
2535        #rename old encoding aliases early:
2536        window = self._id_to_window.get(wid)
2537        if not window:
2538            #window is gone
2539            def draw_cleanup():
2540                if coding=="mmap":
2541                    assert self.mmap_enabled
2542                    from xpra.net.mmap_pipe import int_from_buffer
2543                    def free_mmap_area():
2544                        #we need to ack the data to free the space!
2545                        data_start = int_from_buffer(self.mmap, 0)
2546                        offset, length = data[-1]
2547                        data_start.value = offset+length
2548                    #clear the mmap area via idle_add so any pending draw requests
2549                    #will get a chance to run first (preserving the order)
2550                self.send_damage_sequence(wid, packet_sequence, width, height, -1)
2551            self.idle_add(draw_cleanup)
2552            return
2553        options = {}
2554        if len(packet)>10:
2555            options = packet[10]
2556        options = typedict(options)
2557        paintlog("process_draw %s bytes for window %s using %s encoding with options=%s", len(data), wid, coding, options)
2558        start = time.time()
2559        def record_decode_time(success, message=""):
2560            if success>0:
2561                end = time.time()
2562                decode_time = int(end*1000*1000-start*1000*1000)
2563                self.pixel_counter.append((start, end, width*height))
2564                dms = "%sms" % (int(decode_time/100)/10.0)
2565                paintlog("record_decode_time(%s, %s) wid=%s, %s: %sx%s, %s", success, message, wid, coding, width, height, dms)
2566            elif success==0:
2567                decode_time = -1
2568                paintlog("record_decode_time(%s, %s) decoding error on wid=%s, %s: %sx%s", success, message, wid, coding, width, height)
2569            else:
2570                assert success<0
2571                decode_time = 0
2572                paintlog("record_decode_time(%s, %s) decoding or painting skipped on wid=%s, %s: %sx%s", success, message, wid, coding, width, height)
2573            self.send_damage_sequence(wid, packet_sequence, width, height, decode_time, message)
2574        self._draw_counter += 1
2575        if PAINT_FAULT_RATE>0 and (self._draw_counter % PAINT_FAULT_RATE)==0:
2576            log.warn("injecting paint fault for %s draw packet %i, sequence number=%i", coding, self._draw_counter, packet_sequence)
2577            if PAINT_FAULT_TELL:
2578                self.idle_add(record_decode_time, False, "fault injection for %s draw packet %i, sequence number=%i" % (coding, self._draw_counter, packet_sequence))
2579            return
2580        #we could expose this to the csc step? (not sure how this could be used)
2581        #if self.xscale!=1 or self.yscale!=1:
2582        #    options["client-scaling"] = self.xscale, self.yscale
2583        try:
2584            window.draw_region(x, y, width, height, coding, data, rowstride, packet_sequence, options, [record_decode_time])
2585        except KeyboardInterrupt:
2586            raise
2587        except Exception as e:
2588            log.error("draw error", exc_info=True)
2589            self.idle_add(record_decode_time, False, str(e))
2590            raise
2591
2592    def _process_cursor(self, packet):
2593        if not self.cursors_enabled:
2594            return
2595        if len(packet)==2:
2596            new_cursor = packet[1]
2597        elif len(packet)>=8:
2598            new_cursor = packet[1:]
2599        else:
2600            raise Exception("invalid cursor packet: %s items" % len(packet))
2601        self.set_windows_cursor(self._id_to_window.values(), new_cursor)
2602
2603    def _process_bell(self, packet):
2604        if not self.bell_enabled:
2605            return
2606        (wid, device, percent, pitch, duration, bell_class, bell_id, bell_name) = packet[1:9]
2607        window = self._id_to_window.get(wid)
2608        self.window_bell(window, device, percent, pitch, duration, bell_class, bell_id, bell_name)
2609
2610
2611    def _process_notify_show(self, packet):
2612        if not self.notifications_enabled:
2613            return
2614        self._ui_event()
2615        dbus_id, nid, app_name, replaces_nid, app_icon, summary, body, expire_timeout = packet[1:9]
2616        log("_process_notify_show(%s)", packet)
2617        assert self.notifier
2618        #TODO: choose more appropriate tray if we have more than one shown?
2619        tray = self.tray
2620        self.notifier.show_notify(dbus_id, tray, nid, app_name, replaces_nid, app_icon, summary, body, expire_timeout)
2621
2622    def _process_notify_close(self, packet):
2623        if not self.notifications_enabled:
2624            return
2625        assert self.notifier
2626        nid = packet[1]
2627        log("_process_notify_close(%s)", nid)
2628        self.notifier.close_notify(nid)
2629
2630
2631    def _process_raise_window(self, packet):
2632        #only implemented in gtk2 for now
2633        pass
2634
2635    def _process_show_desktop(self, packet):
2636        show = packet[1]
2637        log("calling %s(%s)", show_desktop, show)
2638        show_desktop(show)
2639
2640
2641    def _process_pointer_position(self, packet):
2642        x, y = packet[1:3]
2643        mouselog("process_pointer_position: %s - current position is %s", (x, y), self.get_mouse_position())
2644
2645
2646    def _process_initiate_moveresize(self, packet):
2647        wid = packet[1]
2648        window = self._id_to_window.get(wid)
2649        if window:
2650            x_root, y_root, direction, button, source_indication = packet[2:7]
2651            window.initiate_moveresize(self.sx(x_root), self.sy(y_root), direction, button, source_indication)
2652
2653    def _process_window_metadata(self, packet):
2654        wid, metadata = packet[1:3]
2655        window = self._id_to_window.get(wid)
2656        if window:
2657            metadata = self.cook_metadata(False, metadata)
2658            window.update_metadata(metadata)
2659
2660    def _process_window_icon(self, packet):
2661        wid, w, h, pixel_format, data = packet[1:6]
2662        window = self._id_to_window.get(wid)
2663        iconlog("_process_window_icon(%s, %s, %s, %s, %s bytes) window=%s", wid, w, h, pixel_format, len(data), window)
2664        if window:
2665            window.update_icon(w, h, pixel_format, data)
2666            window._current_icon = (w, h, pixel_format, data)
2667
2668    def _process_configure_override_redirect(self, packet):
2669        wid, x, y, w, h = packet[1:6]
2670        window = self._id_to_window[wid]
2671        ax = self.sx(x)
2672        ay = self.sy(y)
2673        aw = max(1, self.sx(w))
2674        ah = max(1, self.sy(h))
2675        geomlog("_process_configure_override_redirect%s move resize window %s (id=%s) to %s", packet[1:], window, wid, (ax,ay,aw,ah))
2676        window.move_resize(ax, ay, aw, ah, -1)
2677
2678    def _process_lost_window(self, packet):
2679        wid = packet[1]
2680        window = self._id_to_window.get(wid)
2681        if window:
2682            del self._id_to_window[wid]
2683            del self._window_to_id[window]
2684            self.destroy_window(wid, window)
2685        if len(self._id_to_window)==0:
2686            windowlog("last window gone, clearing key repeat")
2687            if self.keyboard_helper:
2688                self.keyboard_helper.clear_repeat()
2689
2690    def destroy_window(self, wid, window):
2691        windowlog("destroy_window(%s, %s)", wid, window)
2692        window.destroy()
2693        if self._window_with_grab==wid:
2694            log("destroying window %s which has grab, ungrabbing!", wid)
2695            self.window_ungrab()
2696            self._window_with_grab = None
2697
2698    def _process_desktop_size(self, packet):
2699        root_w, root_h, max_w, max_h = packet[1:5]
2700        screenlog("server has resized the desktop to: %sx%s (max %sx%s)", root_w, root_h, max_w, max_h)
2701        self.server_max_desktop_size = max_w, max_h
2702        self.server_actual_desktop_size = root_w, root_h
2703        if self.can_scale:
2704            self.may_adjust_scaling()
2705
2706
2707    def may_adjust_scaling(self):
2708        if self.server_is_shadow and not self.shadow_fullscreen:
2709            #don't try to make it fit
2710            return
2711        assert self.can_scale
2712        max_w, max_h = self.server_max_desktop_size             #ie: server limited to 8192x4096?
2713        w, h = self.get_root_size()                             #ie: 5760, 2160
2714        sw, sh = self.cp(w, h)                                  #ie: upscaled to: 11520x4320
2715        scalinglog("may_adjust_scaling() server desktop size=%s, client root size=%s", self.server_actual_desktop_size, self.get_root_size())
2716        scalinglog(" scaled client root size using %sx%s: %s", self.xscale, self.yscale, (sw, sh))
2717        if sw<(max_w+1) and sh<(max_h+1):
2718            #no change needed
2719            return
2720        #server size is too small for the client screen size with the current scaling value,
2721        #calculate the minimum scaling to fit it:
2722        def clamp(v):
2723            return max(MIN_SCALING, min(MAX_SCALING, v))
2724        x = clamp(float(w)/max_w)
2725        y = clamp(float(h)/max_h)
2726        def mint(v):
2727            #prefer int over float:
2728            try:
2729                return int(str(v).rstrip("0").rstrip("."))
2730            except:
2731                return v
2732        if self.server_is_shadow:
2733            self.xscale = mint(x)
2734            self.yscale = mint(y)
2735        else:
2736            #use the same scale for both axis:
2737            self.xscale = mint(max(x, y))
2738            self.yscale = self.xscale
2739        scalinglog.warn("Warning: adjusting scaling to accomodate server")
2740        scalinglog.warn(" server desktop size is %ix%i", max_w, max_h)
2741        scalinglog.warn(" using scaling factor %s x %s", self.xscale, self.yscale)
2742        self.emit("scaling-changed")
2743
2744
2745    def set_max_packet_size(self):
2746        root_w, root_h = self.cp(*self.get_root_size())
2747        maxw, maxh = root_w, root_h
2748        try:
2749            server_w, server_h = self.server_actual_desktop_size
2750            maxw = max(root_w, server_w)
2751            maxh = max(root_h, server_h)
2752        except:
2753            pass
2754        assert maxw>0 and maxh>0 and maxw<32768 and maxh<32768, "invalid maximum desktop size: %ix%i" % (maxw, maxh)
2755        if maxw>=16384 or maxh>=16384:
2756            log.warn("Warning: the desktop size is extremely large: %ix%i", maxw, maxh)
2757        #max packet size to accomodate:
2758        # * full screen RGBX (32 bits) uncompressed
2759        # * file-size-limit
2760        # both with enough headroom for some metadata (4k)
2761        p = self._protocol
2762        if p:
2763            p.max_packet_size = max(maxw*maxh*4, self.file_size_limit*1024*1024) + 4*1024
2764            p.abs_max_packet_size = max(maxw*maxh*4 * 4, self.file_size_limit*1024*1024) + 4*1024
2765            log("maximum packet size set to %i", p.max_packet_size)
2766
2767
2768    def init_authenticated_packet_handlers(self):
2769        log("init_authenticated_packet_handlers()")
2770        XpraClientBase.init_authenticated_packet_handlers(self)
2771        self.set_packet_handlers(self._ui_packet_handlers, {
2772            "startup-complete":     self._startup_complete,
2773            "new-window":           self._process_new_window,
2774            "new-override-redirect":self._process_new_override_redirect,
2775            "new-tray":             self._process_new_tray,
2776            "raise-window":         self._process_raise_window,
2777            "initiate-moveresize":  self._process_initiate_moveresize,
2778            "show-desktop":         self._process_show_desktop,
2779            "window-move-resize":   self._process_window_move_resize,
2780            "window-resized":       self._process_window_resized,
2781            "pointer-position":     self._process_pointer_position,
2782            "cursor":               self._process_cursor,
2783            "bell":                 self._process_bell,
2784            "notify_show":          self._process_notify_show,
2785            "notify_close":         self._process_notify_close,
2786            "set-clipboard-enabled":self._process_clipboard_enabled_status,
2787            "window-metadata":      self._process_window_metadata,
2788            "configure-override-redirect":  self._process_configure_override_redirect,
2789            "lost-window":          self._process_lost_window,
2790            "desktop_size":         self._process_desktop_size,
2791            "window-icon":          self._process_window_icon,
2792            "rpc-reply":            self._process_rpc_reply,
2793            "control" :             self._process_control,
2794            "draw":                 self._process_draw,
2795            # "clipboard-*" packets are handled by a special case below.
2796            })
2797        #these handlers can run directly from the network thread:
2798        self.set_packet_handlers(self._packet_handlers, {
2799            "ping":                 self._process_ping,
2800            "ping_echo":            self._process_ping_echo,
2801            "info-response":        self._process_info_response,
2802            "sound-data":           self._process_sound_data,
2803            "server-event":         self._process_server_event,
2804            })
2805
2806
2807    def process_clipboard_packet(self, packet):
2808        clipboardlog("process_clipboard_packet: %s", packet[0])
2809        self.idle_add(self.clipboard_helper.process_clipboard_packet, packet)
2810
2811    def process_packet(self, proto, packet):
2812        packet_type = packet[0]
2813        self.check_server_echo(0)
2814        packet_type_str = bytestostr(packet_type)
2815        if packet_type_str.startswith("clipboard-"):
2816            if self.clipboard_enabled and self.clipboard_helper:
2817                self.process_clipboard_packet(packet)
2818        else:
2819            XpraClientBase.process_packet(self, proto, packet)