1# This file is part of Xpra.
2# Copyright (C) 2008, 2009 Nathaniel Smith <njs@pobox.com>
3# Copyright (C) 2013-2021 Antoine Martin <antoine@xpra.org>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
6
7import os
8import re
9import sys
10import binascii
11import traceback
12import threading
13from itertools import chain
14
15
16XPRA_APP_ID = 0
17
18XPRA_GUID1 = 0x67b3efa2
19XPRA_GUID2 = 0xe470
20XPRA_GUID3 = 0x4a5f
21XPRA_GUID4 = (0xb6, 0x53, 0x6f, 0x6f, 0x98, 0xfe, 0x60, 0x81)
22XPRA_GUID_STR = "67B3EFA2-E470-4A5F-B653-6F6F98FE6081"
23XPRA_GUID_BYTES = binascii.unhexlify(XPRA_GUID_STR.replace("-",""))
24
25
26XPRA_NOTIFICATIONS_OFFSET = 2**24
27XPRA_BANDWIDTH_NOTIFICATION_ID  = XPRA_NOTIFICATIONS_OFFSET+1
28XPRA_IDLE_NOTIFICATION_ID       = XPRA_NOTIFICATIONS_OFFSET+2
29XPRA_WEBCAM_NOTIFICATION_ID     = XPRA_NOTIFICATIONS_OFFSET+3
30XPRA_AUDIO_NOTIFICATION_ID      = XPRA_NOTIFICATIONS_OFFSET+4
31XPRA_OPENGL_NOTIFICATION_ID     = XPRA_NOTIFICATIONS_OFFSET+5
32XPRA_SCALING_NOTIFICATION_ID    = XPRA_NOTIFICATIONS_OFFSET+6
33XPRA_NEW_USER_NOTIFICATION_ID   = XPRA_NOTIFICATIONS_OFFSET+7
34XPRA_CLIPBOARD_NOTIFICATION_ID  = XPRA_NOTIFICATIONS_OFFSET+8
35XPRA_FAILURE_NOTIFICATION_ID    = XPRA_NOTIFICATIONS_OFFSET+9
36XPRA_DPI_NOTIFICATION_ID        = XPRA_NOTIFICATIONS_OFFSET+10
37XPRA_DISCONNECT_NOTIFICATION_ID = XPRA_NOTIFICATIONS_OFFSET+11
38XPRA_DISPLAY_NOTIFICATION_ID    = XPRA_NOTIFICATIONS_OFFSET+12
39XPRA_STARTUP_NOTIFICATION_ID    = XPRA_NOTIFICATIONS_OFFSET+13
40XPRA_FILETRANSFER_NOTIFICATION_ID = XPRA_NOTIFICATIONS_OFFSET+14
41XPRA_SHADOWWAYLAND_NOTIFICATION_ID = XPRA_NOTIFICATIONS_OFFSET+15
42
43
44#constants shared between client and server:
45#(do not modify the values, see also disconnect_is_an_error)
46#timeouts:
47CLIENT_PING_TIMEOUT     = "client ping timeout"
48LOGIN_TIMEOUT           = "login timeout"
49CLIENT_EXIT_TIMEOUT     = "client exit timeout"
50#errors:
51PROTOCOL_ERROR          = "protocol error"
52VERSION_ERROR           = "version error"
53CONTROL_COMMAND_ERROR   = "control command error"
54AUTHENTICATION_ERROR    = "authentication error"
55PERMISSION_ERROR        = "permission error"
56SERVER_ERROR            = "server error"
57SESSION_NOT_FOUND       = "session not found error"
58#informational (not a problem):
59DONE                    = "done"
60SERVER_EXIT             = "server exit"
61SERVER_UPGRADE          = "server upgrade"
62SERVER_SHUTDOWN         = "server shutdown"
63CLIENT_REQUEST          = "client request"
64DETACH_REQUEST          = "detach request"
65NEW_CLIENT              = "new client"
66IDLE_TIMEOUT            = "idle timeout"
67SESSION_BUSY            = "session busy"
68#client telling the server:
69CLIENT_EXIT             = "client exit"
70
71
72DEFAULT_PORT = 14500
73
74DEFAULT_PORTS = {
75    "ws"    : 80,
76    "wss"   : 443,
77    "ssl"   : DEFAULT_PORT, #could also default to 443?
78    "ssh"   : 22,
79    "tcp"   : DEFAULT_PORT,
80    "vnc"   : 5900,
81    }
82
83
84#magic value for "workspace" window property, means unset
85WORKSPACE_UNSET = 65535
86WORKSPACE_ALL = 0xffffffff
87
88WORKSPACE_NAMES = {
89                   WORKSPACE_UNSET  : "unset",
90                   WORKSPACE_ALL    : "all",
91                   }
92
93#this default value is based on 0.14.19 clients,
94#later clients should provide the 'metadata.supported" capability instead
95DEFAULT_METADATA_SUPPORTED = ("title", "icon-title", "pid", "iconic",
96                              "size-hints", "class-instance", "client-machine",
97                              "transient-for", "window-type",
98                              "fullscreen", "maximized", "decorations", "skip-taskbar", "skip-pager",
99                              "has-alpha", "override-redirect", "tray", "modal",
100                              "role", "opacity", "xid", "group-leader",
101                              "opaque-region",
102                              )
103
104
105#initiate-moveresize X11 constants
106MOVERESIZE_SIZE_TOPLEFT      = 0
107MOVERESIZE_SIZE_TOP          = 1
108MOVERESIZE_SIZE_TOPRIGHT     = 2
109MOVERESIZE_SIZE_RIGHT        = 3
110MOVERESIZE_SIZE_BOTTOMRIGHT  = 4
111MOVERESIZE_SIZE_BOTTOM       = 5
112MOVERESIZE_SIZE_BOTTOMLEFT   = 6
113MOVERESIZE_SIZE_LEFT         = 7
114MOVERESIZE_MOVE              = 8
115MOVERESIZE_SIZE_KEYBOARD     = 9
116MOVERESIZE_MOVE_KEYBOARD     = 10
117MOVERESIZE_CANCEL            = 11
118MOVERESIZE_DIRECTION_STRING = {
119                               MOVERESIZE_SIZE_TOPLEFT      : "SIZE_TOPLEFT",
120                               MOVERESIZE_SIZE_TOP          : "SIZE_TOP",
121                               MOVERESIZE_SIZE_TOPRIGHT     : "SIZE_TOPRIGHT",
122                               MOVERESIZE_SIZE_RIGHT        : "SIZE_RIGHT",
123                               MOVERESIZE_SIZE_BOTTOMRIGHT  : "SIZE_BOTTOMRIGHT",
124                               MOVERESIZE_SIZE_BOTTOM       : "SIZE_BOTTOM",
125                               MOVERESIZE_SIZE_BOTTOMLEFT   : "SIZE_BOTTOMLEFT",
126                               MOVERESIZE_SIZE_LEFT         : "SIZE_LEFT",
127                               MOVERESIZE_MOVE              : "MOVE",
128                               MOVERESIZE_SIZE_KEYBOARD     : "SIZE_KEYBOARD",
129                               MOVERESIZE_MOVE_KEYBOARD     : "MOVE_KEYBOARD",
130                               MOVERESIZE_CANCEL            : "CANCEL",
131                               }
132SOURCE_INDICATION_UNSET     = 0
133SOURCE_INDICATION_NORMAL    = 1
134SOURCE_INDICATION_PAGER     = 2
135SOURCE_INDICATION_STRING    = {
136                               SOURCE_INDICATION_UNSET      : "UNSET",
137                               SOURCE_INDICATION_NORMAL     : "NORMAL",
138                               SOURCE_INDICATION_PAGER      : "PAGER",
139                               }
140
141
142util_logger = None
143def get_util_logger():
144    global util_logger
145    if not util_logger:
146        from xpra.log import Logger
147        util_logger = Logger("util")
148    return util_logger
149
150
151#convenience method based on the strings above:
152def disconnect_is_an_error(reason):
153    return reason.find("error")>=0 or (reason.find("timeout")>=0 and reason!=IDLE_TIMEOUT)
154
155
156def dump_exc():
157    """Call this from a except: clause to print a nice traceback."""
158    print("".join(traceback.format_exception(*sys.exc_info())))
159
160def noerr(fn, *args):
161    try:
162        return fn(*args)
163    except Exception:
164        return None
165
166
167def net_utf8(value):
168    """
169    Given a value received by the network layer,
170    convert it to a string.
171    Gymnastics are involved if the rencode packet encoder is used
172    as it ends up giving us a string which is actually utf8 bytes.
173    """
174    #with 'rencodeplus' or 'bencode', we just get the unicode string directly:
175    if isinstance(value, str):
176        return value
177    #with rencode v1, we have to decode the value:
178    #(after converting it to 'bytes' if necessary)
179    return u(strtobytes(value))
180
181
182def u(v):
183    if isinstance(v, str):
184        return v
185    try:
186        return v.decode("utf8")
187    except (AttributeError, UnicodeDecodeError):
188        return bytestostr(v)
189
190
191# A simple little class whose instances we can stick random bags of attributes
192# on.
193class AdHocStruct:
194    def __repr__(self):
195        return ("<%s object, contents: %r>"
196                % (type(self).__name__, self.__dict__))
197
198
199def remove_dupes(seq):
200    seen = set()
201    seen_add = seen.add
202    return [x for x in seq if not (x in seen or seen_add(x))]
203
204def merge_dicts(a, b, path=None):
205    """ merges b into a """
206    if path is None:
207        path = []
208    for key in b:
209        if key in a:
210            if isinstance(a[key], dict) and isinstance(b[key], dict):
211                merge_dicts(a[key], b[key], path + [str(key)])
212            elif a[key] == b[key]:
213                pass # same leaf value
214            else:
215                raise Exception('Conflict at %s: existing value is %s, new value is %s' % (
216                    '.'.join(path + [str(key)]), a[key], b[key]))
217        else:
218            a[key] = b[key]
219    return a
220
221def make_instance(class_options, *args):
222    log = get_util_logger()
223    log("make_instance%s", tuple([class_options]+list(args)))
224    for c in class_options:
225        if c is None:
226            continue
227        try:
228            v = c(*args)
229            log("make_instance(..) %s()=%s", c, v)
230            if v:
231                return v
232        except Exception:
233            log.error("make_instance(%s, %s)", class_options, args, exc_info=True)
234            log.error("Error: cannot instantiate %s:", c)
235            log.error(" with arguments %s", tuple(args))
236    return None
237
238
239def roundup(n, m):
240    return (n + m - 1) & ~(m - 1)
241
242
243class AtomicInteger:
244    __slots__ = ("counter", "lock")
245    def __init__(self, integer = 0):
246        self.counter = integer
247        self.lock = threading.RLock()
248
249    def increase(self, inc = 1):
250        with self.lock:
251            self.counter = self.counter + inc
252            return self.counter
253
254    def decrease(self, dec = 1):
255        with self.lock:
256            self.counter = self.counter - dec
257            return self.counter
258
259    def get(self):
260        return self.counter
261
262    def __str__(self):
263        return str(self.counter)
264
265    def __repr__(self):
266        return "AtomicInteger(%s)" % self.counter
267
268
269    def __int__(self):
270        return self.counter
271
272    def __eq__(self, other):
273        try:
274            return self.counter==int(other)
275        except ValueError:
276            return -1
277
278    def __cmp__(self, other):
279        try:
280            return self.counter-int(other)
281        except ValueError:
282            return -1
283
284
285class MutableInteger(object):
286    __slots__ = ("counter")
287    def __init__(self, integer = 0):
288        self.counter = integer
289
290    def increase(self, inc = 1):
291        self.counter = self.counter + inc
292        return self.counter
293
294    def decrease(self, dec = 1):
295        self.counter = self.counter - dec
296        return self.counter
297
298    def get(self):
299        return self.counter
300
301    def __str__(self):
302        return str(self.counter)
303
304    def __repr__(self):
305        return "MutableInteger(%s)" % self.counter
306
307
308    def __int__(self):
309        return self.counter
310
311    def __eq__(self, other):
312        return self.counter==int(other)
313    def __ne__(self, other):
314        return self.counter!=int(other)
315    def __lt__(self, other):
316        return self.counter<int(other)
317    def __le__(self, other):
318        return self.counter<=int(other)
319    def __gt__(self, other):
320        return self.counter>int(other)
321    def __ge__(self, other):
322        return self.counter>=int(other)
323    def __cmp__(self, other):
324        return self.counter-int(other)
325
326
327def strtobytes(x) -> bytes:
328    if isinstance(x, bytes):
329        return x
330    return str(x).encode("latin1")
331def bytestostr(x) -> str:
332    if isinstance(x, (bytes, bytearray)):
333        return x.decode("latin1")
334    return str(x)
335
336def decode_str(x, try_encoding="utf8"):
337    """
338    When we want to decode something (usually a byte string) no matter what.
339    Try with utf8 first then fallback to just bytestostr().
340    """
341    try:
342        return x.decode(try_encoding)
343    except (AttributeError, UnicodeDecodeError):
344        return bytestostr(x)
345
346
347_RaiseKeyError = object()
348
349class typedict(dict):
350    __slots__ = ("warn", ) # no __dict__ - that would be redundant
351    @staticmethod # because this doesn't make sense as a global function.
352    def _process_args(mapping=(), **kwargs):
353        if hasattr(mapping, "items"):
354            mapping = getattr(mapping, "items")()
355        return ((bytestostr(k), v) for k, v in chain(mapping, getattr(kwargs, "items")()))
356    def __init__(self, mapping=(), **kwargs):
357        super().__init__(self._process_args(mapping, **kwargs))
358        self.warn = self._warn
359    def __getitem__(self, k):
360        return super().__getitem__(bytestostr(k))
361    def __setitem__(self, k, v):
362        return super().__setitem__(bytestostr(k), v)
363    def __delitem__(self, k):
364        return super().__delitem__(bytestostr(k))
365    def get(self, k, default=None):
366        return super().get(bytestostr(k), default)
367    def setdefault(self, k, default=None):
368        return super().setdefault(bytestostr(k), default)
369    def pop(self, k, v=_RaiseKeyError):
370        if v is _RaiseKeyError:
371            return super().pop(bytestostr(k))
372        return super().pop(bytestostr(k), v)
373    def update(self, mapping=(), **kwargs):
374        super().update(self._process_args(mapping, **kwargs))
375    def __contains__(self, k):
376        return super().__contains__(bytestostr(k))
377    @classmethod
378    def fromkeys(cls, keys, v=None):
379        return super().fromkeys((bytestostr(k) for k in keys), v)
380    def __repr__(self):
381        return '{0}({1})'.format(type(self).__name__, super().__repr__())
382
383    def _warn(self, msg, *args):
384        get_util_logger().warn(msg, *args)
385
386    def conv_get(self, k, default=None, conv=None):
387        if not super().__contains__(bytestostr(k)):
388            return default
389        v = self.get(k, default)
390        try:
391            return conv(v)
392        except (TypeError, ValueError, AssertionError) as e:
393            self._warn("Warning: failed to convert %s using %s: %s", type(v), conv, e)
394            return default
395
396    def uget(self, k, default=None):
397        return self.conv_get(k, default, u)
398
399    def strget(self, k, default=None):
400        return self.conv_get(k, default, bytestostr)
401
402    def bytesget(self, k : str, default=None):
403        return self.conv_get(k, default, strtobytes)
404
405    def intget(self, k : str, default=0):
406        return self.conv_get(k, default, int)
407
408    def boolget(self, k : str, default=False):
409        return self.conv_get(k, default, bool)
410
411    def dictget(self, k : str, default=None):
412        def checkdict(v):
413            assert isinstance(v, dict)
414            return v
415        return self.conv_get(k, default, checkdict)
416
417    def intpair(self, k : str, default_value=None):
418        v = self.inttupleget(k, default_value)
419        if v is None:
420            return default_value
421        if len(v)!=2:
422            #"%s is not a pair of numbers: %s" % (k, len(v))
423            return default_value
424        try:
425            return int(v[0]), int(v[1])
426        except ValueError:
427            return default_value
428
429    def strtupleget(self, k : str, default_value=(), min_items=None, max_items=None):
430        return self.tupleget(k, default_value, str, min_items, max_items)
431
432    def inttupleget(self, k : str, default_value=(), min_items=None, max_items=None):
433        return self.tupleget(k, default_value, int, min_items, max_items)
434
435    def tupleget(self, k : str, default_value=(), item_type=None, min_items=None, max_items=None):
436        v = self._listget(k, default_value, item_type, min_items, max_items)
437        if isinstance(v, list):
438            v = tuple(v)
439        return v
440
441    def _listget(self, k : str, default_value, item_type=None, min_items=None, max_items=None):
442        v = self.get(k)
443        if v is None:
444            return default_value
445        if not isinstance(v, (list, tuple)):
446            self._warn("listget%s", (k, default_value, item_type, max_items))
447            self._warn("expected a list or tuple value for %s but got %s", k, type(v))
448            return default_value
449        if min_items is not None:
450            if len(v)<min_items:
451                self._warn("too few items in %s %s: minimum %s allowed, but got %s", type(v), k, max_items, len(v))
452                return default_value
453        if max_items is not None:
454            if len(v)>max_items:
455                self._warn("too many items in %s %s: maximum %s allowed, but got %s", type(v), k, max_items, len(v))
456                return default_value
457        aslist = list(v)
458        if item_type:
459            for i, x in enumerate(aslist):
460                if isinstance(x, bytes) and item_type==str:
461                    x = bytestostr(x)
462                    aslist[i] = x
463                elif isinstance(x, str) and item_type==str:
464                    x = str(x)
465                    aslist[i] = x
466                if not isinstance(x, item_type):
467                    if callable(item_type):
468                        try:
469                            return item_type(x)
470                        except Exception:
471                            self._warn("invalid item type for %s %s: %s cannot be used with %s",
472                                       type(v), k, item_type, type(x))
473                            return default_value
474                    self._warn("invalid item type for %s %s: expected %s but got %s",
475                               type(v), k, item_type, type(x))
476                    return default_value
477        return aslist
478
479
480def parse_scaling_value(v):
481    if not v:
482        return None
483    if v.endswith("%"):
484        return float(v[:1]).as_integer_ratio()
485    values = v.replace("/", ":").replace(",", ":").split(":", 1)
486    values = [int(x) for x in values]
487    for x in values:
488        assert x>0, "invalid scaling value %s" % x
489    if len(values)==1:
490        ret = 1, values[0]
491    else:
492        assert values[0]<=values[1], "cannot upscale"
493        ret = values[0], values[1]
494    return ret
495
496def from0to100(v):
497    return intrangevalidator(v, 0, 100)
498
499def intrangevalidator(v, min_value=None, max_value=None):
500    v = int(v)
501    if min_value is not None and v<min_value:
502        raise ValueError("value must be greater than %i" % min_value)
503    if max_value is not None and v>max_value:
504        raise ValueError("value must be lower than %i" % max_value)
505    return v
506
507
508def log_screen_sizes(root_w, root_h, sizes):
509    try:
510        do_log_screen_sizes(root_w, root_h, sizes)
511    except Exception as e:
512        get_util_logger().warn("failed to parse screen size information: %s", e, exc_info=True)
513
514def prettify_plug_name(s, default=""):
515    if not s:
516        return default
517    try:
518        s = s.decode("utf8")
519    except (AttributeError, UnicodeDecodeError):
520        pass
521    #prettify strings on win32
522    s = re.sub(r"[0-9\.]*\\", "-", s).lstrip("-")
523    if s.startswith("WinSta-"):
524        s = s[len("WinSta-"):]
525    if s.startswith("(Standard monitor types) "):
526        s = s[len("(Standard monitor types) "):]
527    if s=="0":
528        s = default
529    return s
530
531def do_log_screen_sizes(root_w, root_h, sizes):
532    from xpra.log import Logger
533    log = Logger("screen")
534    #old format, used by some clients (android):
535    if not isinstance(sizes, (tuple, list)):
536        return
537    if any(True for x in sizes if not isinstance(x, (tuple, list))):
538        return
539    def dpi(size_pixels, size_mm):
540        if size_mm==0:
541            return 0
542        return round(size_pixels * 254 / size_mm / 10)
543    def add_workarea(info, wx, wy, ww, wh):
544        info.append("workarea: %4ix%-4i" % (ww, wh))
545        if wx!=0 or wy!=0:
546            #log position if not (0, 0)
547            info.append("at %4ix%-4i" % (wx, wy))
548    for s in sizes:
549        if len(s)<10:
550            log.info(" %s", s)
551            continue
552        #more detailed output:
553        display_name, width, height, width_mm, height_mm, \
554        monitors, work_x, work_y, work_width, work_height = s[:10]
555        #always log plug name:
556        info = ["%s" % prettify_plug_name(display_name)]
557        if width!=root_w or height!=root_h:
558            #log plug dimensions if not the same as display (root):
559            info.append("%ix%i" % (width, height))
560        sdpix = dpi(width, width_mm)
561        sdpiy = dpi(height, height_mm)
562        info.append("(%ix%i mm - DPI: %ix%i)" % (width_mm, height_mm, sdpix, sdpiy))
563
564        if work_width!=width or work_height!=height or work_x!=0 or work_y!=0:
565            add_workarea(info, work_x, work_y, work_width, work_height)
566        log.info("  "+" ".join(info))
567        #sort monitors from left to right, top to bottom:
568        monitors_distances = []
569        for m in monitors:
570            plug_x, plug_y = m[1:3]
571            monitors_distances.append((plug_x+plug_y*width, m))
572        sorted_monitors = [x[1] for x in sorted(monitors_distances)]
573        for i, m in enumerate(sorted_monitors, start=1):
574            if len(m)<7:
575                log.info("    %s", m)
576                continue
577            plug_name, plug_x, plug_y, plug_width, plug_height, plug_width_mm, plug_height_mm = m[:7]
578            default_name = "monitor %i" % i
579            info = ['%-16s' % prettify_plug_name(plug_name, default_name)]
580            if plug_width!=width or plug_height!=height or plug_x!=0 or plug_y!=0:
581                info.append("%4ix%-4i" % (plug_width, plug_height))
582                if plug_x!=0 or plug_y!=0 or len(sorted_monitors)>1:
583                    info.append("at %4ix%-4i" % (plug_x, plug_y))
584            if (plug_width_mm!=width_mm or plug_height_mm!=height_mm) and (plug_width_mm>0 or plug_height_mm>0):
585                dpix = dpi(plug_width, plug_width_mm)
586                dpiy = dpi(plug_height, plug_height_mm)
587                dpistr = ""
588                if sdpix!=dpix or sdpiy!=dpiy or len(sorted_monitors)>1:
589                    dpistr = " - DPI: %ix%i" % (dpix, dpiy)
590                info.append("(%3ix%-3i mm%s)" % (plug_width_mm, plug_height_mm, dpistr))
591            if len(m)>=11:
592                dwork_x, dwork_y, dwork_width, dwork_height = m[7:11]
593                #only show it again if different from the screen workarea
594                if dwork_x!=work_x or dwork_y!=work_y or dwork_width!=work_width or dwork_height!=work_height:
595                    add_workarea(info, dwork_x, dwork_y, dwork_width, dwork_height)
596            istr = (" ".join(info)).rstrip(" ")
597            if len(monitors)==1 and istr.lower() in ("unknown unknown", "0", "1", default_name, "screen", "monitor"):
598                #a single monitor with no real name,
599                #so don't bother showing it:
600                continue
601            log.info("    "+istr)
602
603def get_screen_info(screen_sizes):
604    #same format as above
605    if not screen_sizes:
606        return {}
607    info = {
608            "screens" : len(screen_sizes)
609            }
610    for i, x in enumerate(screen_sizes):
611        if not isinstance(x, (tuple, list)):
612            continue
613        sinfo = info.setdefault("screen", {}).setdefault(i, {})
614        sinfo["display"] = x[0]
615        if len(x)>=3:
616            sinfo["size"] = x[1], x[2]
617        if len(x)>=5:
618            sinfo["size_mm"] = x[3], x[4]
619        if len(x)>=6:
620            monitors = x[5]
621            for j, monitor in enumerate(monitors):
622                if len(monitor)>=7:
623                    minfo = sinfo.setdefault("monitor", {}).setdefault(j, {})
624                    for k,v in {
625                                "name"      : monitor[0],
626                                "geometry"  : monitor[1:5],
627                                "size_mm"   : monitor[5:7],
628                                }.items():
629                        minfo[k] = v
630        if len(x)>=10:
631            sinfo["workarea"] = x[6:10]
632    return info
633
634def dump_all_frames(logger=None):
635    try:
636        frames = sys._current_frames()      #pylint: disable=protected-access
637    except AttributeError:
638        return
639    else:
640        dump_frames(frames.items(), logger)
641
642def dump_gc_frames(logger=None):
643    import gc
644    #import types
645    import inspect
646    gc.collect()
647    #frames = tuple(x for x in gc.get_objects() if isinstance(x, types.FrameType))
648    frames = tuple((None, x) for x in gc.get_objects() if inspect.isframe(x))
649    dump_frames(frames, logger)
650
651def dump_frames(frames, logger=None):
652    if not logger:
653        logger = get_util_logger()
654    logger("found %s frames:", len(frames))
655    for i,(fid,frame) in enumerate(frames):
656        fidstr = ""
657        if fid is not None:
658            try:
659                fidstr = hex(fid)
660            except TypeError:
661                fidstr = str(fid)
662        logger("%i: %s %s:", i, fidstr, frame)
663        for x in traceback.format_stack(frame):
664            for l in x.splitlines():
665                logger("%s", l)
666
667
668def detect_leaks():
669    import tracemalloc
670    tracemalloc.start()
671    last_snapshot = [tracemalloc.take_snapshot()]
672    def print_leaks():
673        s1 = last_snapshot[0]
674        s2 = tracemalloc.take_snapshot()
675        last_snapshot[0] = s2
676        top_stats = s2.compare_to(s1, 'lineno')
677        print("[ Top 20 differences ]")
678        for stat in top_stats[:20]:
679            print(stat)
680        for i, stat in enumerate(top_stats[:20]):
681            print()
682            print("top %i:" % i)
683            print("%s memory blocks: %.1f KiB" % (stat.count, stat.size / 1024))
684            for line in stat.traceback.format():
685                print(line)
686        return True
687    return print_leaks
688
689def start_mem_watcher(ms):
690    from xpra.make_thread import start_thread
691    start_thread(mem_watcher, name="mem-watcher", daemon=True, args=(ms,))
692
693def mem_watcher(ms, pid=os.getpid()):
694    import time
695    import psutil
696    process = psutil.Process(pid)
697    while True:
698        mem = process.memory_full_info()
699        #get_util_logger().info("memory usage: %s", mem.mem//1024//1024)
700        get_util_logger().info("memory usage for %s: %s", pid, mem)
701        time.sleep(ms/1000.0)
702
703def log_mem_info(prefix="memory usage: ", pid=os.getpid()):
704    import psutil
705    process = psutil.Process(pid)
706    mem = process.memory_full_info()
707    print("%i %s%s" % (pid, prefix, mem))
708
709
710class ellipsizer:
711    __slots__ = ("obj", "limit")
712    def __init__(self, obj, limit=100):
713        self.obj = obj
714        self.limit = limit
715    def __str__(self):
716        if self.obj is None:
717            return "None"
718        return repr_ellipsized(self.obj, self.limit)
719    def __repr__(self):
720        if self.obj is None:
721            return "None"
722        return repr_ellipsized(self.obj, self.limit)
723
724def repr_ellipsized(obj, limit=100):
725    if isinstance(obj, str):
726        if len(obj)>limit>6:
727            return nonl(obj[:limit//2-2]+" .. "+obj[2-limit//2:])
728        return nonl(obj)
729    if isinstance(obj, memoryview):
730        obj = obj.tobytes()
731    if isinstance(obj, bytes):
732        try:
733            s = nonl(repr(obj))
734        except Exception:
735            s = binascii.hexlify(obj).decode()
736        if len(s)>limit>6:
737            return nonl(s[:limit//2-2]+" .. "+s[2-limit//2:])
738        return s
739    return repr_ellipsized(repr(obj), limit)
740
741
742def rindex(alist, avalue):
743    return len(alist) - alist[::-1].index(avalue) - 1
744
745
746def notypedict(d):
747    for k in list(d.keys()):
748        v = d[k]
749        if isinstance(v, dict):
750            d[k] = notypedict(v)
751    return dict(d)
752
753def flatten_dict(info, sep="."):
754    to = {}
755    _flatten_dict(to, sep, None, info)
756    return to
757
758def _flatten_dict(to, sep, path, d):
759    for k,v in d.items():
760        if path:
761            if k:
762                npath = path+sep+bytestostr(k)
763            else:
764                npath = path
765        else:
766            npath = bytestostr(k)
767        if isinstance(v, dict):
768            _flatten_dict(to, sep, npath, v)
769        elif v is not None:
770            to[npath] = v
771
772def parse_simple_dict(s="", sep=","):
773    #parse the options string and add the pairs:
774    d = {}
775    for el in s.split(sep):
776        if not el:
777            continue
778        try:
779            k, v = el.split("=", 1)
780            cur = d.get(k)
781            if cur:
782                if not isinstance(cur, list):
783                    cur = [cur]
784                cur.append(v)
785                v = cur
786            d[k] = v
787        except Exception as e:
788            log = get_util_logger()
789            log.warn("Warning: failed to parse dictionary option '%s':", s)
790            log.warn(" %s", e)
791    return d
792
793#used for merging dicts with a prefix and suffix
794#non-None values get added to <todict> with a prefix and optional suffix
795def updict(todict, prefix, d, suffix="", flatten_dicts=False):
796    if not d:
797        return todict
798    for k,v in d.items():
799        if v is not None:
800            if k:
801                k = prefix+"."+str(k)
802            else:
803                k = prefix
804            if suffix:
805                k = k+"."+suffix
806            if flatten_dicts and isinstance(v, dict):
807                updict(todict, k, v)
808            else:
809                todict[k] = v
810    return todict
811
812def pver(v, numsep=".", strsep=", "):
813    #print for lists with version numbers, or CSV strings
814    if isinstance(v, (list, tuple)):
815        types = list(set(type(x) for x in v))
816        if len(types)==1:
817            if types[0]==int:
818                return numsep.join(str(x) for x in v)
819            if types[0]==str:
820                return strsep.join(str(x) for x in v)
821            if types[0]==bytes:
822                def s(x):
823                    try:
824                        return x.decode("utf8")
825                    except UnicodeDecodeError:
826                        return bytestostr(x)
827                return strsep.join(s(x) for x in v)
828    return bytestostr(v)
829
830def sorted_nicely(l):
831    """ Sort the given iterable in the way that humans expect."""
832    def convert(text):
833        if text.isdigit():
834            return int(text)
835        return text
836    alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', bytestostr(key))]
837    return sorted(l, key = alphanum_key)
838
839def print_nested_dict(d, prefix="", lchar="*", pad=32, vformat=None, print_fn=None,
840                      version_keys=("version", "revision"), hex_keys=("data", )):
841    #"smart" value formatting function:
842    def sprint(arg):
843        if print_fn:
844            print_fn(arg)
845        else:
846            print(arg)
847    def vf(k, v):
848        if vformat:
849            fmt = vformat
850            if isinstance(vformat, dict):
851                fmt = vformat.get(k)
852            if fmt is not None:
853                return nonl(fmt(v))
854        try:
855            if any(k.find(x)>=0 for x in version_keys):
856                return nonl(pver(v)).lstrip("v")
857            if any(k.find(x)>=0 for x in hex_keys):
858                return binascii.hexlify(v)
859        except Exception:
860            pass
861        return nonl(pver(v, ", ", ", "))
862    l = pad-len(prefix)-len(lchar)
863    for k in sorted_nicely(d.keys()):
864        v = d[k]
865        if isinstance(v, dict):
866            nokey = v.get("", (v.get(None)))
867            if nokey is not None:
868                sprint("%s%s %s : %s" % (prefix, lchar, bytestostr(k).ljust(l), vf(k, nokey)))
869                for x in ("", None):
870                    v.pop(x, None)
871            else:
872                sprint("%s%s %s" % (prefix, lchar, bytestostr(k)))
873            print_nested_dict(v, prefix+"  ", "-", vformat=vformat, print_fn=print_fn,
874                              version_keys=version_keys, hex_keys=hex_keys)
875        else:
876            sprint("%s%s %s : %s" % (prefix, lchar, bytestostr(k).ljust(l), vf(k, v)))
877
878def reverse_dict(d):
879    reversed_d = {}
880    for k,v in d.items():
881        reversed_d[v] = k
882    return reversed_d
883
884
885def std(s, extras="-,./: "):
886    s = s or ""
887    try:
888        s = s.decode("latin1")
889    except Exception:
890        pass
891    def c(v):
892        try:
893            return chr(v)
894        except Exception:
895            return str(v)
896    def f(v):
897        return str.isalnum(c(v)) or v in extras
898    return "".join(filter(f, s))
899
900def alnum(s):
901    try:
902        s = s.encode("latin1")
903    except Exception:
904        pass
905    def c(v):
906        try:
907            return chr(v)
908        except Exception:
909            return str(v)
910    def f(v):
911        return str.isalnum(c(v))
912    return "".join(c(v) for v in filter(f, s))
913
914def nonl(x):
915    if x is None:
916        return None
917    return str(x).replace("\n", "\\n").replace("\r", "\\r")
918
919def engs(v):
920    if isinstance(v, int):
921        l = v
922    else:
923        try:
924            l = len(v)
925        except TypeError:
926            return ""
927    return "s" if l!=1 else ""
928
929
930def obsc(v):
931    OBSCURE_PASSWORDS = envbool("XPRA_OBSCURE_PASSWORDS", True)
932    if OBSCURE_PASSWORDS:
933        return "".join("*" for _ in (v or ""))
934    return v
935
936
937def csv(v):
938    try:
939        return ", ".join(str(x) for x in v)
940    except Exception:
941        return str(v)
942
943
944def unsetenv(*varnames):
945    for x in varnames:
946        os.environ.pop(x, None)
947
948def envint(name : str, d=0):
949    try:
950        return int(os.environ.get(name, d))
951    except ValueError:
952        return d
953
954def envbool(name : str, d=False):
955    try:
956        v = os.environ.get(name, "").lower()
957        if v is None:
958            return d
959        if v in ("yes", "true", "on"):
960            return True
961        if v in ("no", "false", "off"):
962            return False
963        return bool(int(v))
964    except ValueError:
965        return d
966
967def envfloat(name : str, d=0):
968    try:
969        return float(os.environ.get(name, d))
970    except ValueError:
971        return d
972
973
974#give warning message just once per key then ignore:
975_once_only = set()
976def first_time(key):
977    global _once_only
978    if key not in _once_only:
979        _once_only.add(key)
980        return True
981    return False
982