1# -*- coding: utf-8 -*-
2# This file is part of Xpra.
3# Copyright (C) 2011-2021 Antoine Martin <antoine@xpra.org>
4# Copyright (C) 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 sys
9import datetime
10import platform
11from time import monotonic
12from collections import deque
13from gi.repository import GLib, Gtk, Gdk
14
15from xpra.version_util import XPRA_VERSION
16from xpra.os_util import bytestostr, strtobytes, get_linux_distribution
17from xpra.util import prettify_plug_name, typedict, envint, csv
18from xpra.gtk_common.graph import make_graph_imagesurface
19from xpra.simple_stats import values_to_scaled_values, values_to_diff_scaled_values, to_std_unit, std_unit_dec, std_unit
20from xpra.client import mixin_features
21from xpra.client.gobject_client_base import InfoTimerClient
22from xpra.gtk_common.gtk_util import (
23    add_close_accel, label,
24    TableBuilder, imagebutton, get_gtk_version_info,
25    get_icon_pixbuf,
26    )
27from xpra.net.net_util import get_network_caps
28from xpra.log import Logger
29
30log = Logger("info")
31
32N_SAMPLES = 20      #how many sample points to show on the graphs
33SHOW_PIXEL_STATS = True
34SHOW_SOUND_STATS = True
35SHOW_RECV = True
36
37
38def make_version_str(version):
39    if version and isinstance(version, (tuple, list)):
40        version = ".".join(bytestostr(x) for x in version)
41    return bytestostr(version or "unknown")
42
43def make_datetime(date, time):
44    if not time:
45        return bytestostr(date)
46    return "%s %s" % (bytestostr(date), bytestostr(time))
47
48def title_box(label_str):
49    eb = Gtk.EventBox()
50    l = label(label_str)
51    l.modify_fg(Gtk.StateType.NORMAL, Gdk.Color(red=48*256, green=0, blue=0))
52    al = Gtk.Alignment(xalign=0.0, yalign=0.5, xscale=0.0, yscale=0.0)
53    al.set_padding(0, 0, 10, 10)
54    al.add(l)
55    eb.add(al)
56    eb.modify_bg(Gtk.StateType.NORMAL, Gdk.Color(red=219*256, green=226*256, blue=242*256))
57    return eb
58
59def pixelstr(v):
60    if v<0:
61        return  "n/a"
62    return std_unit_dec(v)
63def fpsstr(v):
64    if v<0:
65        return  "n/a"
66    return "%s" % (int(v*10)/10.0)
67
68def average(seconds, pixel_counter):
69    now = monotonic()
70    total = 0
71    total_n = 0
72    mins = None
73    maxs = 0
74    avgs = 0
75    mint = now-seconds      #ignore records older than N seconds
76    startt = now            #when we actually start counting from
77    for _, t, count in pixel_counter:
78        if t>=mint:
79            total += count
80            total_n += 1
81            startt = min(t, startt)
82            if mins:
83                mins = min(mins,count)
84            else:
85                mins = count
86            maxs = max(maxs, count)
87            avgs += count
88    if total==0 or startt==now:
89        return  None
90    avgs = avgs/total_n
91    elapsed = now-startt
92    return int(total/elapsed), total_n/elapsed, mins, avgs, maxs
93
94def dictlook(d, k, fallback=None):
95    #deal with old-style non-namespaced dicts first:
96    #"batch.delay.avg"
97    if d is None:
98        return fallback
99    v = d.get(k)
100    if v is not None:
101        return v
102    parts = (b"client."+strtobytes(k)).split(b".")
103    v = newdictlook(d, parts, fallback)
104    if v is None:
105        parts = strtobytes(k).split(b".")
106        v = newdictlook(d, parts, fallback)
107    if v is None:
108        return fallback
109    return v
110
111def newdictlook(d, parts, fallback=None):
112    #ie: {}, ["batch", "delay", "avg"], 0
113    v = d
114    for p in parts:
115        try:
116            newv = v.get(p)
117            if newv is None:
118                newv = v.get(strtobytes(p))
119                if newv is None:
120                    return fallback
121            v = newv
122        except Exception:
123            return fallback
124    return v
125
126def slabel(text="", tooltip=None, font=None):
127    l = label(text, tooltip, font)
128    l.set_selectable(True)
129    return l
130
131
132class SessionInfo(Gtk.Window):
133
134    def __init__(self, client, session_name, conn, show_client=True, show_server=True):
135        assert show_client or show_server
136        self.show_client = show_client
137        self.show_server = show_server
138        super().__init__()
139        self.client = client
140        self.session_name = session_name
141        self.connection = conn
142        self.last_populate_time = 0
143        self.last_populate_statistics = 0
144        self.is_closed = False
145        self.set_title(self.get_window_title())
146        self.set_destroy_with_parent(True)
147        self.set_resizable(True)
148        self.set_decorated(True)
149        window_icon_pixbuf = get_icon_pixbuf("statistics.png") or get_icon_pixbuf("xpra.png")
150        if window_icon_pixbuf:
151            self.set_icon(window_icon_pixbuf)
152        self.set_position(Gtk.WindowPosition.CENTER)
153
154        #tables on the left in a vbox with buttons at the top:
155        self.tab_box = Gtk.VBox(False, 0)
156        self.tab_button_box = Gtk.HBox(True, 0)
157        self.tabs = []          #pairs of button, table
158        self.populate_cb = None
159        self.tab_box.pack_start(self.tab_button_box, expand=False, fill=True, padding=0)
160
161        fillexpand = Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND
162
163        #Package Table:
164        tb = self.table_tab("package.png", "Software", self.populate_software)[0]
165        if show_client and show_server:
166            #title row:
167            tb.attach(title_box(""), 0, xoptions=fillexpand, xpadding=0)
168            tb.attach(title_box("Client"), 1, xoptions=fillexpand, xpadding=0)
169            tb.attach(title_box("Server"), 2, xoptions=fillexpand, xpadding=0)
170            tb.inc()
171
172        def clrow(label_str, client_label, server_label, **kwargs):
173            args = [label_str]
174            if show_client:
175                args.append(client_label)
176            if show_server:
177                args.append(server_label)
178            tb.new_row(*args, **kwargs)
179
180        def csrow(label_str, client_str, server_str, **kwargs):
181            clrow(label_str,
182                  slabel(client_str) if show_client else None,
183                  slabel(server_str) if show_server else None,
184                  **kwargs)
185
186        def make_os_str(sys_platform, platform_release, platform_platform, platform_linux_distribution):
187            from xpra.os_util import platform_name
188            s = [platform_name(sys_platform, platform_release)]
189            if platform_linux_distribution and len(platform_linux_distribution)==3 and len(platform_linux_distribution[0])>0:
190                s.append(" ".join([str(x) for x in platform_linux_distribution]))
191            elif platform_platform:
192                s.append(platform_platform)
193            return "\n".join(s)
194        distro = get_linux_distribution()
195        LOCAL_PLATFORM_NAME = make_os_str(sys.platform, platform.release(), platform.platform(), distro)
196        def cattr(name, default_value=""):
197            return getattr(self.client, name, default_value)
198        SERVER_PLATFORM_NAME = make_os_str(cattr("_remote_platform"),
199                                           cattr("_remote_platform_release"),
200                                           cattr("_remote_platform_platform"),
201                                           cattr("_remote_platform_linux_distribution"))
202        csrow("Operating System", LOCAL_PLATFORM_NAME, SERVER_PLATFORM_NAME)
203        csrow("Xpra", XPRA_VERSION, cattr("_remote_version", "unknown"))
204        try:
205            from xpra.build_info import BUILD_DATE as cl_date, BUILD_TIME as cl_time
206        except ImportError:
207            cl_date = cl_time = ""
208        from xpra.version_util import revision_str, make_revision_str
209        csrow("Revision", revision_str(),
210              make_revision_str(
211                  cattr("_remote_revision"),
212                  cattr("_remote_modifications"),
213                  cattr("_remote_branch"),
214                  cattr("_remote_commit"),
215                  )
216              )
217        csrow("Build date", make_datetime(cl_date, cl_time),
218              make_datetime(cattr("_remote_build_date"), cattr("_remote_build_time")))
219        gtk_version_info = get_gtk_version_info()
220        def client_vinfo(prop, fallback="unknown"):
221            return make_version_str(newdictlook(gtk_version_info, (prop, "version"), fallback))
222        def server_vinfo(lib):
223            rlv = getattr(self.client, "_remote_lib_versions", {})
224            return make_version_str(rlv.get(lib, ""))
225        csrow("Glib",      client_vinfo("glib"),       server_vinfo("glib"))
226        csrow("Gobject",   client_vinfo("gobject"),    server_vinfo("gobject"))
227        csrow("GTK",       client_vinfo("gtk"),        server_vinfo("gtk"))
228        csrow("GDK",       client_vinfo("gdk"),        server_vinfo("gdk"))
229        csrow("Cairo",     client_vinfo("cairo"),      server_vinfo("cairo"))
230        csrow("Pango",     client_vinfo("pango"),      server_vinfo("pango"))
231        csrow("Python",    platform.python_version(),  server_vinfo("python"))
232
233        try:
234            from xpra.sound.wrapper import query_sound
235            props = query_sound()
236        except Exception:
237            log("cannot load sound information: %s", exc_info=True)
238            props = typedict()
239        gst_version = props.strtupleget("gst.version")
240        pygst_version = props.strtupleget("pygst.version")
241        csrow("GStreamer", make_version_str(gst_version), server_vinfo("sound.gst"))
242        csrow("pygst", make_version_str(pygst_version), server_vinfo("sound.pygst"))
243        def clientgl(prop="opengl", default_value="n/a"):
244            if not show_client:
245                return ""
246            return make_version_str(self.client.opengl_props.get(prop, default_value))
247        def servergl(prop="opengl", default_value="n/a"):
248            if not show_server:
249                return ""
250            return make_version_str(typedict(self.client.server_opengl or {}).strget(prop, default_value))
251        for prop in ("OpenGL", "Vendor", "PyOpenGL"):
252            key = prop.lower()
253            csrow(prop, clientgl(key), servergl(key))
254
255        # Features Table:
256        vbox = self.vbox_tab("features.png", "Features", self.populate_features)
257        #add top table:
258        tb = TableBuilder(rows=1, columns=2, row_spacings=15)
259        table = tb.get_table()
260        al = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=1.0)
261        al.add(table)
262        vbox.pack_start(al, expand=True, fill=False, padding=10)
263        #top table contents:
264        randr_box = Gtk.HBox(False, 20)
265        self.server_randr_label = slabel()
266        self.server_randr_icon = Gtk.Image()
267        randr_box.add(self.server_randr_icon)
268        randr_box.add(self.server_randr_label)
269        tb.new_row("RandR Support", randr_box)
270        if show_client:
271            self.client_display = slabel()
272            tb.new_row("Client Display", self.client_display)
273            opengl_box = Gtk.HBox(False, 20)
274            self.client_opengl_label = slabel()
275            self.client_opengl_label.set_line_wrap(True)
276            self.client_opengl_icon = Gtk.Image()
277            opengl_box.add(self.client_opengl_icon)
278            opengl_box.add(self.client_opengl_label)
279            tb.new_row("Client OpenGL", opengl_box)
280            self.opengl_buffering = slabel()
281            tb.new_row("OpenGL Mode", self.opengl_buffering)
282            self.window_rendering = slabel()
283            tb.new_row("Window Rendering", self.window_rendering)
284        if mixin_features.mmap:
285            self.server_mmap_icon = Gtk.Image()
286            tb.new_row("Memory Mapped Transfers", self.server_mmap_icon)
287        if mixin_features.clipboard:
288            self.server_clipboard_icon = Gtk.Image()
289            tb.new_row("Clipboard", self.server_clipboard_icon)
290        if mixin_features.notifications:
291            self.server_notifications_icon = Gtk.Image()
292            tb.new_row("Notifications", self.server_notifications_icon)
293        if mixin_features.windows:
294            self.server_bell_icon = Gtk.Image()
295            tb.new_row("Bell", self.server_bell_icon)
296            self.server_cursors_icon = Gtk.Image()
297            tb.new_row("Cursors", self.server_cursors_icon)
298
299        # Codecs Table:
300        vbox = self.vbox_tab("encoding.png", "Codecs", self.populate_codecs)
301        tb = TableBuilder(rows=1, columns=2, col_spacings=0, row_spacings=10)
302        table = tb.get_table()
303        al = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=1.0)
304        al.add(table)
305        vbox.pack_start(al, expand=True, fill=False, padding=10)
306        if show_client and show_server:
307            #table headings:
308            tb.attach(title_box(""), 0, xoptions=fillexpand, xpadding=0)
309            tb.attach(title_box("Client"), 1, xoptions=fillexpand, xpadding=0)
310            tb.attach(title_box("Server"), 2, xoptions=fillexpand, xpadding=0)
311            tb.inc()
312        #table contents:
313        self.client_encodings_label = slabel()
314        self.client_encodings_label.set_line_wrap(True)
315        self.server_encodings_label = slabel()
316        self.server_encodings_label.set_line_wrap(True)
317        clrow("Picture Encodings",
318              self.client_encodings_label, self.server_encodings_label,
319              xoptions=fillexpand, yoptions=fillexpand)
320        self.client_speaker_codecs_label = slabel()
321        self.client_speaker_codecs_label.set_line_wrap(True)
322        self.server_speaker_codecs_label = slabel()
323        self.server_speaker_codecs_label.set_line_wrap(True)
324        clrow("Speaker Codecs",
325              self.client_speaker_codecs_label, self.server_speaker_codecs_label,
326              xoptions=fillexpand, yoptions=fillexpand)
327        self.client_microphone_codecs_label = slabel()
328        self.client_microphone_codecs_label.set_line_wrap(True)
329        self.server_microphone_codecs_label = slabel()
330        self.server_microphone_codecs_label.set_line_wrap(True)
331        clrow("Microphone Codecs",
332              self.client_microphone_codecs_label, self.server_microphone_codecs_label,
333              xoptions=fillexpand, yoptions=fillexpand)
334        self.client_packet_encoders_label = slabel()
335        self.client_packet_encoders_label.set_line_wrap(True)
336        self.server_packet_encoders_label = slabel()
337        self.server_packet_encoders_label.set_line_wrap(True)
338        clrow("Packet Encoders",
339              self.client_packet_encoders_label, self.server_packet_encoders_label,
340              xoptions=fillexpand, yoptions=fillexpand)
341        self.client_packet_compressors_label = slabel()
342        self.server_packet_compressors_label = slabel()
343        clrow("Packet Compressors",
344              self.client_packet_compressors_label, self.server_packet_compressors_label,
345              xoptions=fillexpand, yoptions=fillexpand)
346
347        # Connection Table:
348        tb = self.table_tab("connect.png", "Connection", self.populate_connection)[0]
349        if self.connection:
350            tb.new_row("Server Endpoint", slabel(self.connection.target))
351        if mixin_features.display and self.client.server_display:
352            tb.new_row("Server Display", slabel(prettify_plug_name(self.client.server_display)))
353        tb.new_row("Server Hostname", slabel(cattr("_remote_hostname")))
354        if cattr("server_platform"):
355            tb.new_row("Server Platform", slabel(cattr("server_platform")))
356        self.server_load_label = slabel()
357        tb.new_row("Server Load", self.server_load_label, label_tooltip="Average over 1, 5 and 15 minutes")
358        self.session_started_label = slabel()
359        tb.new_row("Session Started", self.session_started_label)
360        self.session_connected_label = slabel()
361        if show_client:
362            tb.new_row("Session Connected", self.session_connected_label)
363            self.input_packets_label = slabel()
364            tb.new_row("Packets Received", self.input_packets_label)
365            self.input_bytes_label = slabel()
366            tb.new_row("Bytes Received", self.input_bytes_label)
367            self.output_packets_label = slabel()
368            tb.new_row("Packets Sent", self.output_packets_label)
369            self.output_bytes_label = slabel()
370            tb.new_row("Bytes Sent", self.output_bytes_label)
371            self.compression_label = slabel()
372            tb.new_row("Encoding + Compression", self.compression_label)
373            self.connection_type_label = slabel()
374            tb.new_row("Connection Type", self.connection_type_label)
375            self.input_encryption_label = slabel()
376            tb.new_row("Input Encryption", self.input_encryption_label)
377            self.output_encryption_label = slabel()
378            tb.new_row("Output Encryption", self.output_encryption_label)
379
380            self.speaker_label = slabel()
381            self.speaker_details = slabel(font="monospace 10")
382            tb.new_row("Speaker", self.speaker_label, self.speaker_details)
383            self.microphone_label = slabel()
384            tb.new_row("Microphone", self.microphone_label)
385
386            # Details:
387            tb, stats_box = self.table_tab("browse.png", "Statistics", self.populate_statistics)
388            tb.widget_xalign = 1.0
389            tb.attach(title_box(""), 0, xoptions=fillexpand, xpadding=0)
390            tb.attach(title_box("Latest"), 1, xoptions=fillexpand, xpadding=0)
391            tb.attach(title_box("Minimum"), 2, xoptions=fillexpand, xpadding=0)
392            tb.attach(title_box("Average"), 3, xoptions=fillexpand, xpadding=0)
393            tb.attach(title_box("90 percentile"), 4, xoptions=fillexpand, xpadding=0)
394            tb.attach(title_box("Maximum"), 5, xoptions=fillexpand, xpadding=0)
395            tb.inc()
396
397            def maths_labels():
398                return slabel(), slabel(), slabel(), slabel(), slabel()
399            self.server_latency_labels = maths_labels()
400            tb.add_row(slabel("Server Latency (ms)", "The time it takes for the server to respond to pings"),
401                       *self.server_latency_labels)
402            self.client_latency_labels = maths_labels()
403            tb.add_row(slabel("Client Latency (ms)",
404                              "The time it takes for the client to respond to pings, as measured by the server"),
405                       *self.client_latency_labels)
406            if mixin_features.windows and self.client.windows_enabled:
407                self.batch_labels = maths_labels()
408                tb.add_row(slabel("Batch Delay (MPixels / ms)",
409                                  "How long the server waits for new screen updates to accumulate before processing them"),
410                           *self.batch_labels)
411                self.damage_labels = maths_labels()
412                tb.add_row(slabel("Damage Latency (ms)",
413                                  "The time it takes to compress a frame and pass it to the OS network layer"),
414                           *self.damage_labels)
415                self.quality_labels = maths_labels()
416                tb.add_row(slabel("Encoding Quality (pct)",
417                                  "Automatic picture quality, average for all the windows"),
418                                  *self.quality_labels)
419                self.speed_labels = maths_labels()
420                tb.add_row(slabel("Encoding Speed (pct)",
421                                  "Automatic picture encoding speed (bandwidth vs CPU usage), average for all the windows"),
422                                  *self.speed_labels)
423
424                self.decoding_labels = maths_labels()
425                tb.add_row(slabel("Decoding Latency (ms)",
426                                  "How long it takes the client to decode a screen update"),
427                                  *self.decoding_labels)
428                self.regions_per_second_labels = maths_labels()
429                tb.add_row(slabel("Regions/s",
430                                  "The number of screen updates per second"
431                                  +" (includes both partial and full screen updates)"),
432                                  *self.regions_per_second_labels)
433                self.regions_sizes_labels = maths_labels()
434                tb.add_row(slabel("Pixels/region", "The number of pixels per screen update"), *self.regions_sizes_labels)
435                self.pixels_per_second_labels = maths_labels()
436                tb.add_row(slabel("Pixels/s", "The number of pixels processed per second"), *self.pixels_per_second_labels)
437
438                #Window count stats:
439                wtb = TableBuilder()
440                stats_box.add(wtb.get_table())
441                #title row:
442                wtb.attach(title_box(""), 0, xoptions=fillexpand, xpadding=0)
443                wtb.attach(title_box("Regular"), 1, xoptions=fillexpand, xpadding=0)
444                wtb.attach(title_box("Transient"), 2, xoptions=fillexpand, xpadding=0)
445                wtb.attach(title_box("Trays"), 3, xoptions=fillexpand, xpadding=0)
446                if self.client.client_supports_opengl:
447                    wtb.attach(title_box("OpenGL"), 4, xoptions=fillexpand, xpadding=0)
448                wtb.inc()
449
450                wtb.attach(slabel("Windows:"), 0, xoptions=fillexpand, xpadding=0)
451                self.windows_managed_label = slabel()
452                wtb.attach(self.windows_managed_label, 1)
453                self.transient_managed_label = slabel()
454                wtb.attach(self.transient_managed_label, 2)
455                self.trays_managed_label = slabel()
456                wtb.attach(self.trays_managed_label, 3)
457                if self.client.client_supports_opengl:
458                    self.opengl_label = slabel()
459                    wtb.attach(self.opengl_label, 4)
460
461                #add encoder info:
462                etb = TableBuilder()
463                stats_box.add(etb.get_table())
464                self.encoder_info_box = Gtk.HBox(spacing=4)
465                etb.new_row("Window Encoders", self.encoder_info_box)
466
467        if show_client:
468            self.graph_box = Gtk.VBox(False, 10)
469            self.add_tab("statistics.png", "Graphs", self.populate_graphs, self.graph_box)
470            bandwidth_label = "Bandwidth used"
471            if SHOW_PIXEL_STATS:
472                bandwidth_label += ",\nand number of pixels rendered"
473            self.bandwidth_graph = self.add_graph_button(bandwidth_label, self.save_graph)
474            self.latency_graph = self.add_graph_button(None, self.save_graph)
475            if SHOW_SOUND_STATS:
476                self.sound_queue_graph = self.add_graph_button(None, self.save_graph)
477            else:
478                self.sound_queue_graph = None
479            self.connect("realize", self.populate_graphs)
480            self.pixel_in_data = deque(maxlen=N_SAMPLES+4)
481            self.net_in_bitcount = deque(maxlen=N_SAMPLES+4)
482            self.net_out_bitcount = deque(maxlen=N_SAMPLES+4)
483            self.sound_in_bitcount = deque(maxlen=N_SAMPLES+4)
484            self.sound_out_bitcount = deque(maxlen=N_SAMPLES+4)
485            self.sound_out_queue_min = deque(maxlen=N_SAMPLES*10+4)
486            self.sound_out_queue_max = deque(maxlen=N_SAMPLES*10+4)
487            self.sound_out_queue_cur  = deque(maxlen=N_SAMPLES*10+4)
488
489        self.set_border_width(15)
490        self.add(self.tab_box)
491        def window_deleted(*_args):
492            self.is_closed = True
493        self.connect('delete_event', window_deleted)
494        self.show_tab(self.tabs[0][2])
495        self.set_size_request(-1, -1)
496        self.init_counters()
497        self.populate()
498        self.populate_all()
499        GLib.timeout_add(1000, self.populate)
500        GLib.timeout_add(100, self.populate_tab)
501        if mixin_features.audio and SHOW_SOUND_STATS and show_client:
502            GLib.timeout_add(100, self.populate_sound_stats)
503        add_close_accel(self, self.destroy)
504
505
506    def get_window_title(self):
507        t = ["Session Info"]
508        c = self.client
509        if c:
510            if c.session_name or c.server_session_name:
511                t.append(c.session_name or c.server_session_name)
512            p = c._protocol
513            if p:
514                conn = getattr(p, "_conn", None)
515                if conn:
516                    cinfo = conn.get_info()
517                    t.append(cinfo.get("endpoint", bytestostr(conn.target)))
518        v = " - ".join(str(x) for x in t)
519        return v
520
521    def table_tab(self, icon_filename, title, populate_cb):
522        tb = TableBuilder()
523        table = tb.get_table()
524        vbox = self.vbox_tab(icon_filename, title, populate_cb)
525        al = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=1.0)
526        al.add(table)
527        vbox.pack_start(al, expand=True, fill=True, padding=20)
528        return tb, vbox
529
530    def vbox_tab(self, icon_filename, title, populate_cb):
531        vbox = Gtk.VBox(False, 0)
532        self.add_tab(icon_filename, title, populate_cb, contents=vbox)
533        return vbox
534
535
536    def add_tab(self, icon_filename, title, populate_cb, contents):
537        icon = get_icon_pixbuf(icon_filename)
538        def show_tab(*_args):
539            self.show_tab(contents)
540        button = imagebutton(title, icon, clicked_callback=show_tab)
541        button.connect("clicked", show_tab)
542        button.set_relief(Gtk.ReliefStyle.NONE)
543        self.tab_button_box.add(button)
544        self.tabs.append((title, button, contents, populate_cb))
545
546    def show_tab(self, table):
547        button = None
548        for _, b, t, p_cb in self.tabs:
549            if t==table:
550                button = b
551                b.set_relief(Gtk.ReliefStyle.NORMAL)
552                b.grab_focus()
553                self.populate_cb = p_cb
554            else:
555                b.set_relief(Gtk.ReliefStyle.NONE)
556        assert button
557        for x in self.tab_box.get_children():
558            if x!=self.tab_button_box:
559                self.tab_box.remove(x)
560        self.tab_box.pack_start(table, expand=True, fill=True, padding=0)
561        table.show_all()
562        #ensure we re-draw the whole window:
563        window = self.get_window()
564        if window:
565            alloc = self.get_allocation()
566            window.invalidate_rect(alloc, True)
567
568    def set_args(self, *args):
569        #this is a generic way for keyboard shortcuts or remote commands
570        #to pass parameters to us
571        log("set_args%s", args)
572        if not args:
573            return
574        #at the moment, we only handle the tab name as argument:
575        tab_name = args[0]
576        if tab_name.lower()!="help":
577            title = ""
578            for title, _, table, _ in self.tabs:
579                if title.lower()==tab_name.lower():
580                    self.show_tab(table)
581                    return
582            log.warn("could not find session info tab named: %s", title)
583        log.warn("The options for tab names are: %s)", [x[0] for x in self.tabs])
584
585    def populate_all(self):
586        for tab in self.tabs:
587            p_cb = tab[3]
588            if p_cb:
589                p_cb()
590
591    def add_graph_button(self, tooltip, click_cb):
592        button = Gtk.EventBox()
593        def set_cursor(widget):
594            cursor = Gdk.Cursor.new(Gdk.CursorType.BASED_ARROW_DOWN)
595            widget.get_window().set_cursor(cursor)
596        button.connect("realize", set_cursor)
597        graph = Gtk.Image()
598        graph.set_size_request(0, 0)
599        button.connect("button_press_event", click_cb, graph)
600        button.add(graph)
601        if tooltip:
602            graph.set_tooltip_text(tooltip)
603        self.graph_box.add(button)
604        return graph
605
606    def bool_icon(self, image, on_off):
607        if on_off:
608            icon = get_icon_pixbuf("ticked-small.png")
609        else:
610            icon = get_icon_pixbuf("unticked-small.png")
611        image.set_from_pixbuf(icon)
612
613    def populate_sound_stats(self, *_args):
614        #runs every 100ms
615        if self.is_closed:
616            return False
617        ss = self.client.sound_sink
618        if SHOW_SOUND_STATS and ss:
619            info = ss.get_info()
620            if info:
621                info = typedict(info)
622                def qlookup(attr):
623                    return int(newdictlook(info, ("queue", attr), 0))
624                self.sound_out_queue_cur.append(qlookup("cur"))
625                self.sound_out_queue_min.append(qlookup("min"))
626                self.sound_out_queue_max.append(qlookup("max"))
627        return not self.is_closed
628
629    def populate(self, *_args):
630        conn = self.connection
631        if self.is_closed or not conn:
632            return False
633        self.client.send_ping()
634        self.last_populate_time = monotonic()
635
636        if self.show_client:
637            self.show_opengl_state()
638            self.show_window_renderers()
639            #record bytecount every second:
640            self.net_in_bitcount.append(conn.input_bytecount*8)
641            self.net_out_bitcount.append(conn.output_bytecount*8)
642            if mixin_features.audio and SHOW_SOUND_STATS:
643                if self.client.sound_in_bytecount>0:
644                    self.sound_in_bitcount.append(self.client.sound_in_bytecount * 8)
645                if self.client.sound_out_bytecount>0:
646                    self.sound_out_bitcount.append(self.client.sound_out_bytecount * 8)
647
648        if self.show_client and mixin_features.windows:
649            #count pixels in the last second:
650            since = monotonic()-1
651            decoded = [0]+[pixels for _,t,pixels in self.client.pixel_counter if t>since]
652            self.pixel_in_data.append(sum(decoded))
653        #update latency values
654        #there may be more than one record for each second
655        #so we have to average them to prevent the graph from "jumping":
656        def get_ping_latency_records(src, size=25):
657            recs = {}
658            src_list = list(src)
659            now = int(monotonic())
660            while src_list and len(recs)<size:
661                when, value = src_list.pop()
662                if when>=(now-1):           #ignore last second
663                    continue
664                iwhen = int(when)
665                cv = recs.get(iwhen)
666                v = 1000.0*value
667                if cv:
668                    v = (v+cv) / 2.0        #not very fair if more than 2 values... but this shouldn't happen anyway
669                recs[iwhen] = v
670            #ensure we always have a record for the last N seconds, even an empty one
671            for x in range(size):
672                i = now-2-x
673                if i not in recs:
674                    recs[i] = None
675            return tuple(recs.get(x) for x in sorted(recs.keys()))
676        self.server_latency = get_ping_latency_records(self.client.server_ping_latency)
677        self.client_latency = get_ping_latency_records(self.client.client_ping_latency)
678        if self.client.server_last_info:
679            #populate running averages for graphs:
680            def getavg(*names):
681                return (
682                    newdictlook(self.client.server_last_info, list(names)+["avg"]) or
683                    newdictlook(self.client.server_last_info, ["client"]+list(names)+["avg"])
684                    )
685            def addavg(l, *names):
686                v = getavg(*names)
687                if v:
688                    l.append(v)
689            addavg(self.avg_batch_delay, "batch", "delay")
690            addavg(self.avg_damage_out_latency, "damage", "out_latency")
691            spl = tuple(1000.0*x[1] for x in tuple(self.client.server_ping_latency))
692            cpl = tuple(1000.0*x[1] for x in tuple(self.client.client_ping_latency))
693            if spl and cpl:
694                self.avg_ping_latency.append(round(sum(spl+cpl)/len(spl+cpl)))
695            if mixin_features.windows and self.show_client:
696                pc = tuple(self.client.pixel_counter)
697                if pc:
698                    tsize = 0
699                    ttime = 0
700                    for start_time, end_time, size in pc:
701                        ttime += 1000.0 * (end_time-start_time) * size
702                        tsize += size
703                    self.avg_decoding_latency.append(round(ttime/tsize))
704        #totals: ping latency is halved since we only care about sending, not sending+receiving
705        els  = (
706            (tuple(self.avg_batch_delay), 1),
707            (tuple(self.avg_damage_out_latency), 1),
708            (tuple(self.avg_ping_latency), 2),
709            (tuple(self.avg_decoding_latency), 1),
710            )
711        if all(x[0] for x in els):
712            totals = tuple(x[-1]/r for x, r in els)
713            log("frame totals=%s", totals)
714            self.avg_total.append(round(sum(totals)))
715        return not self.is_closed
716
717    def populate_software(self):
718        #called from the "fast" timer
719        return False
720
721    def init_counters(self):
722        self.avg_batch_delay = deque(maxlen=N_SAMPLES+4)
723        self.avg_damage_out_latency = deque(maxlen=N_SAMPLES+4)
724        self.avg_ping_latency = deque(maxlen=N_SAMPLES+4)
725        self.avg_decoding_latency = deque(maxlen=N_SAMPLES+4)
726        self.avg_total = deque(maxlen=N_SAMPLES+4)
727
728    def populate_tab(self, *_args):
729        if self.is_closed:
730            return False
731        #now re-populate the tab we are seeing:
732        if self.populate_cb:
733            if not self.populate_cb():
734                self.populate_cb = None
735        return not self.is_closed
736
737
738    def show_opengl_state(self):
739        if self.client.opengl_enabled:
740            glinfo = "%s / %s" % (
741                self.client.opengl_props.get("vendor", ""),
742                self.client.opengl_props.get("renderer", ""),
743                )
744            display_mode = self.client.opengl_props.get("display_mode", [])
745            bit_depth = self.client.opengl_props.get("depth", 0)
746            info = []
747            if bit_depth:
748                info.append("%i-bit" % bit_depth)
749            if "DOUBLE" in display_mode:
750                info.append("double buffering")
751            elif "SINGLE" in display_mode:
752                info.append("single buffering")
753            else:
754                info.append("unknown buffering")
755            if "ALPHA" in display_mode:
756                info.append("with transparency")
757            else:
758                info.append("without transparency")
759        else:
760            #info could be telling us that the gl bindings are missing:
761            glinfo = self.client.opengl_props.get("info", "disabled")
762            info = ["n/a"]
763        self.client_opengl_label.set_text(glinfo)
764        self.opengl_buffering.set_text(" ".join(info))
765
766    def show_window_renderers(self):
767        if not mixin_features.windows:
768            return
769        wr = []
770        renderers = {}
771        for wid, window in tuple(self.client._id_to_window.items()):
772            renderers.setdefault(window.get_backing_class(), []).append(wid)
773        for bclass, windows in renderers.items():
774            wr.append("%s (%i)" % (bclass.__name__.replace("Backing", ""), len(windows)))
775        self.window_rendering.set_text("GTK3: %s" % csv(wr))
776
777    def populate_features(self):
778        size_info = ""
779        if mixin_features.windows:
780            if self.client.server_actual_desktop_size:
781                w,h = self.client.server_actual_desktop_size
782                size_info = "%sx%s" % (w,h)
783                if self.client.server_randr and self.client.server_max_desktop_size:
784                    size_info += " (max %s)" % ("x".join([str(x) for x in self.client.server_max_desktop_size]))
785                self.bool_icon(self.server_randr_icon, self.client.server_randr)
786        else:
787            size_info = "unknown"
788            unknown = get_icon_pixbuf("unknown.png")
789            if unknown:
790                self.server_randr_icon.set_from_pixbuf(unknown)
791        self.server_randr_label.set_text("%s" % size_info)
792        if self.show_client:
793            root_w, root_h = self.client.get_root_size()
794            if mixin_features.windows and (self.client.xscale!=1 or self.client.yscale!=1):
795                sw, sh = self.client.cp(root_w, root_h)
796                display_info = "%ix%i (scaled from %ix%i)" % (sw, sh, root_w, root_h)
797            else:
798                display_info = "%ix%i" % (root_w, root_h)
799            self.client_display.set_text(display_info)
800            self.bool_icon(self.client_opengl_icon, self.client.client_supports_opengl)
801            self.show_window_renderers()
802
803        if mixin_features.mmap:
804            self.bool_icon(self.server_mmap_icon, self.client.mmap_enabled)
805        if mixin_features.clipboard:
806            self.bool_icon(self.server_clipboard_icon, self.client.server_clipboard)
807        if mixin_features.notifications:
808            self.bool_icon(self.server_notifications_icon, self.client.server_notifications)
809        if mixin_features.windows:
810            self.bool_icon(self.server_bell_icon, self.client.server_bell)
811            self.bool_icon(self.server_cursors_icon, self.client.server_cursors)
812
813    def populate_codecs(self):
814        #clamp the large labels so they will overflow vertically:
815        w = self.tab_box.get_preferred_width()[0]
816        lw = max(200, int(w//2.5))
817        if self.show_client:
818            self.client_encodings_label.set_size_request(lw, -1)
819            self.client_speaker_codecs_label.set_size_request(lw, -1)
820            self.client_microphone_codecs_label.set_size_request(lw, -1)
821        if self.show_server:
822            self.server_encodings_label.set_size_request(lw, -1)
823            self.server_speaker_codecs_label.set_size_request(lw, -1)
824            self.server_microphone_codecs_label.set_size_request(lw, -1)
825        #sound/video codec table:
826        def codec_info(enabled, codecs):
827            if not enabled:
828                return "n/a"
829            return ", ".join(codecs or ())
830        if mixin_features.audio:
831            c = self.client
832            if self.show_server:
833                self.server_speaker_codecs_label.set_text(codec_info(c.server_sound_send, c.server_sound_encoders))
834            if self.show_client:
835                self.client_speaker_codecs_label.set_text(codec_info(c.speaker_allowed, c.speaker_codecs))
836            if self.show_server:
837                self.server_microphone_codecs_label.set_text(codec_info(c.server_sound_receive, c.server_sound_decoders))
838            if self.show_client:
839                self.client_microphone_codecs_label.set_text(codec_info(c.microphone_allowed, c.microphone_codecs))
840        def encliststr(v):
841            v = list(v)
842            try:
843                v.remove("rgb")
844            except ValueError:
845                pass
846            return csv(sorted(v))
847        se = ()
848        if mixin_features.encoding:
849            se = self.client.server_core_encodings
850        self.server_encodings_label.set_text(encliststr(se))
851        if self.show_client and mixin_features.encoding:
852            self.client_encodings_label.set_text(encliststr(self.client.get_core_encodings()))
853        else:
854            self.client_encodings_label.set_text("n/a")
855
856        from xpra.net.packet_encoding import get_enabled_encoders
857        if self.show_client:
858            self.client_packet_encoders_label.set_text(csv(get_enabled_encoders()))
859        if self.show_server:
860            self.server_packet_encoders_label.set_text(csv(self.client.server_packet_encoders))
861
862        from xpra.net.compression import get_enabled_compressors
863        if self.show_client:
864            self.client_packet_compressors_label.set_text(csv(get_enabled_compressors()))
865        if self.show_server:
866            self.server_packet_compressors_label.set_text(csv(self.client.server_compressors))
867        return False
868
869    def populate_connection(self):
870        def settimedeltastr(label, from_time):
871            import time
872            delta = datetime.timedelta(seconds=(int(time.time())-int(from_time)))
873            label.set_text(str(delta))
874        if self.client.server_load:
875            self.server_load_label.set_text("  ".join("%.1f" % (x/1000.0) for x in self.client.server_load))
876        if self.client.server_start_time>0:
877            settimedeltastr(self.session_started_label, self.client.server_start_time)
878        else:
879            self.session_started_label.set_text("unknown")
880        settimedeltastr(self.session_connected_label, self.client.start_time)
881
882        p = self.client._protocol
883        if not self.show_client:
884            return True
885        if p is None:
886            #no longer connected!
887            return False
888        c = p._conn
889        if c:
890            self.input_packets_label.set_text(std_unit_dec(p.input_packetcount))
891            self.input_bytes_label.set_text(std_unit_dec(c.input_bytecount))
892            self.output_packets_label.set_text(std_unit_dec(p.output_packetcount))
893            self.output_bytes_label.set_text(std_unit_dec(c.output_bytecount))
894        else:
895            for l in (
896                self.input_packets_label,
897                self.input_bytes_label,
898                self.output_packets_label,
899                self.output_bytes_label,
900                ):
901                l.set_text("n/a")
902
903        if mixin_features.audio:
904            def get_sound_info(supported, prop):
905                if not supported:
906                    return {"state" : "disabled"}
907                if prop is None:
908                    return {"state" : "inactive"}
909                return prop.get_info()
910            def set_sound_info(label, details, supported, prop):
911                d = typedict(get_sound_info(supported, prop))
912                state = d.strget("state", "")
913                codec_descr = d.strget("codec") or d.strget("codec_description")
914                container_descr = d.strget("container_description", "")
915                if state=="active" and codec_descr:
916                    if codec_descr.find(container_descr)>=0:
917                        descr = codec_descr
918                    else:
919                        descr = csv(x for x in (codec_descr, container_descr) if x)
920                    state = "%s: %s" % (state, descr)
921                label.set_text(state)
922                if details:
923                    s = ""
924                    bitrate = d.intget("bitrate", 0)
925                    if bitrate>0:
926                        s = "%sbit/s" % std_unit(bitrate)
927                    details.set_text(s)
928            set_sound_info(self.speaker_label, self.speaker_details, self.client.speaker_enabled, self.client.sound_sink)
929            set_sound_info(self.microphone_label, None, self.client.microphone_enabled, self.client.sound_source)
930
931        self.connection_type_label.set_text(c.socktype)
932        protocol_info = p.get_info()
933        encoder = protocol_info.get("encoder", "bug")
934        compressor = protocol_info.get("compressor", "none")
935        level = protocol_info.get("compression_level", 0)
936        compression_str = encoder + " + "+compressor
937        if level>0:
938            compression_str += " (level %s)" % level
939        self.compression_label.set_text(compression_str)
940
941        def enclabel(label_widget, cipher):
942            if not cipher:
943                info = "None"
944            else:
945                info = str(cipher)
946            if c.socktype.lower()=="ssh":
947                info += " (%s)" % c.socktype
948            ncaps = get_network_caps()
949            backend = ncaps.get("backend")
950            if backend=="python-cryptography":
951                info += " / python-cryptography"
952            label_widget.set_text(info)
953        enclabel(self.input_encryption_label, p.cipher_in_name)
954        enclabel(self.output_encryption_label, p.cipher_out_name)
955        return True
956
957
958    def getval(self, prefix, suffix, alt=""):
959        if self.client.server_last_info is None:
960            return ""
961        altv = ""
962        if alt:
963            altv = dictlook(self.client.server_last_info, (alt+"."+suffix).encode(), "")
964        return dictlook(self.client.server_last_info, (prefix+"."+suffix).encode(), altv)
965
966    def values_from_info(self, prefix, alt=None):
967        def getv(suffix):
968            return self.getval(prefix, suffix, alt)
969        return getv("cur"), getv("min"), getv("avg"), getv("90p"), getv("max")
970
971    def all_values_from_info(self, *window_props):
972        #newer (2.4 and later) servers can just give us the value directly:
973        for window_prop in window_props:
974            prop_path = "client.%s" % window_prop
975            v = dictlook(self.client.server_last_info, prop_path)
976            if v is not None:
977                try:
978                    v = typedict(v)
979                except TypeError:
980                    #backwards compatibility:
981                    #older servers don't expose the correct value or type here
982                    #so don't use this value
983                    log("expected dictionary for %s", prop_path)
984                    log(" got %s: %s", type(v), v)
985                else:
986                    iget = v.intget
987                    return iget("cur"), iget("min"), iget("avg"), iget("90p"), iget("max")
988
989        #legacy servers: sum up the values for all the windows found
990        def avg(values):
991            if not values:
992                return ""
993            return sum(values) // len(values)
994        def getv(suffix, op):
995            if self.client.server_last_info is None:
996                return ""
997            values = []
998            for wid in self.client._window_to_id.values():
999                for window_prop in window_props:
1000                    #Warning: this is ugly...
1001                    proppath = "window[%s].%s.%s" % (wid, window_prop, suffix)
1002                    v = self.client.server_last_info.get(proppath)
1003                    if v is None:
1004                        wprop = window_prop.split(".")              #ie: "encoding.speed" -> ["encoding", "speed"]
1005                        newpath = ["window", wid]+wprop+[suffix]    #ie: ["window", 1, "encoding", "speed", "cur"]
1006                        v = newdictlook(self.client.server_last_info, newpath)
1007                    if v is not None:
1008                        values.append(v)
1009                        break
1010            if not values:
1011                return ""
1012            try:
1013                return op(values)
1014            except (TypeError, ValueError):
1015                log("%s(%s)", op, values, exc_info=True)
1016                return ""
1017        return getv("cur", avg), getv("min", min), getv("avg", avg), getv("90p", avg), getv("max", max)
1018
1019    def populate_statistics(self):
1020        log("populate_statistics()")
1021        if monotonic()-self.last_populate_statistics<1.0:
1022            #don't repopulate more than every second
1023            return True
1024        self.last_populate_statistics = monotonic()
1025        self.client.send_info_request()
1026        def setall(labels, values):
1027            assert len(labels)==len(values), "%s labels and %s values (%s vs %s)" % (
1028                len(labels), len(values), labels, values)
1029            for i, l in enumerate(labels):
1030                l.set_text(str(values[i]))
1031        def setlabels(labels, values, rounding=int):
1032            if not values:
1033                return
1034            avg = sum(values)/len(values)
1035            svalues = sorted(values)
1036            l = len(svalues)
1037            assert l>0
1038            if l<10:
1039                index = l-1
1040            else:
1041                index = int(l*90/100)
1042            index = max(0, min(l-1, index))
1043            pct = svalues[index]
1044            disp = values[-1], min(values), avg, pct, max(values)
1045            rounded_values = [rounding(v) for v in disp]
1046            setall(labels, rounded_values)
1047
1048        if self.client.server_ping_latency:
1049            spl = tuple(int(1000*x[1]) for x in tuple(self.client.server_ping_latency))
1050            setlabels(self.server_latency_labels, spl)
1051        if self.client.client_ping_latency:
1052            cpl = tuple(int(1000*x[1]) for x in tuple(self.client.client_ping_latency))
1053            setlabels(self.client_latency_labels, cpl)
1054        if mixin_features.windows and self.client.windows_enabled:
1055            setall(self.batch_labels, self.values_from_info("batch_delay", "batch.delay"))
1056            setall(self.damage_labels, self.values_from_info("damage_out_latency", "damage.out_latency"))
1057            setall(self.quality_labels, self.all_values_from_info("quality", "encoding.quality"))
1058            setall(self.speed_labels, self.all_values_from_info("speed", "encoding.speed"))
1059
1060            region_sizes = []
1061            rps = []
1062            pps = []
1063            decoding_latency = []
1064            if self.client.pixel_counter:
1065                min_time = None
1066                max_time = None
1067                regions_per_second = {}
1068                pixels_per_second = {}
1069                for start_time, end_time, size in self.client.pixel_counter:
1070                    decoding_latency.append(int(1000.0*(end_time-start_time)))
1071                    region_sizes.append(size)
1072                    if min_time is None or min_time>end_time:
1073                        min_time = end_time
1074                    if max_time is None or max_time<end_time:
1075                        max_time = end_time
1076                    time_in_seconds = int(end_time)
1077                    regions = regions_per_second.get(time_in_seconds, 0)
1078                    regions_per_second[time_in_seconds] = regions+1
1079                    pixels = pixels_per_second.get(time_in_seconds, 0)
1080                    pixels_per_second[time_in_seconds] = pixels + size
1081                if int(min_time)+1 < int(max_time):
1082                    for t in range(int(min_time)+1, int(max_time)):
1083                        rps.append(regions_per_second.get(t, 0))
1084                        pps.append(pixels_per_second.get(t, 0))
1085            setlabels(self.decoding_labels, decoding_latency)
1086            setlabels(self.regions_per_second_labels, rps)
1087            setlabels(self.regions_sizes_labels, region_sizes, rounding=std_unit_dec)
1088            setlabels(self.pixels_per_second_labels, pps, rounding=std_unit_dec)
1089
1090            windows, gl, transient, trays = 0, 0, 0, 0
1091            for w in self.client._window_to_id.keys():
1092                if w.is_tray():
1093                    trays += 1
1094                elif w.is_OR():
1095                    transient +=1
1096                else:
1097                    windows += 1
1098                if w.is_GL():
1099                    gl += 1
1100            self.windows_managed_label.set_text(str(windows))
1101            self.transient_managed_label.set_text(str(transient))
1102            self.trays_managed_label.set_text(str(trays))
1103            if self.client.client_supports_opengl:
1104                self.opengl_label.set_text(str(gl))
1105
1106            #remove all the current labels:
1107            for x in self.encoder_info_box.get_children():
1108                self.encoder_info_box.remove(x)
1109            if self.client.server_last_info:
1110                window_encoder_stats = self.get_window_encoder_stats()
1111                #log("window_encoder_stats=%s", window_encoder_stats)
1112                for wid, props in window_encoder_stats.items():
1113                    l = slabel("%s (%s)" % (wid, bytestostr(props.get(""))))
1114                    l.show()
1115                    info = ("%s=%s" % (k,v) for k,v in props.items() if k!="")
1116                    l.set_tooltip_text(" ".join(info))
1117                    self.encoder_info_box.add(l)
1118        return True
1119
1120    def get_window_encoder_stats(self):
1121        window_encoder_stats = {}
1122        #new-style server with namespace (easier):
1123        window_dict = self.client.server_last_info.get("window")
1124        if window_dict and isinstance(window_dict, dict):
1125            for k,v in window_dict.items():
1126                try:
1127                    wid = int(k)
1128                    encoder_stats = v.get("encoder")
1129                    if encoder_stats:
1130                        window_encoder_stats[wid] = encoder_stats
1131                except Exception:
1132                    log.error("Error: cannot lookup window dict", exc_info=True)
1133        return window_encoder_stats
1134
1135
1136    def set_graph_surface(self, graph, surface):
1137        w = surface.get_width()
1138        h = surface.get_height()
1139        graph.set_size_request(w, h)
1140        graph.surface = surface
1141        graph.set_from_surface(surface)
1142
1143    def populate_graphs(self, *_args):
1144        #older servers have 'batch' at top level,
1145        #newer servers store it under client
1146        self.client.send_info_request("network", "damage", "state", "batch", "client")
1147        box = self.tab_box
1148        h = box.get_preferred_height()[0]
1149        bh = self.tab_button_box.get_preferred_height()[0]
1150        if h<=0:
1151            return True
1152        start_x_offset = min(1.0, (monotonic()-self.last_populate_time)*0.95)
1153        rect = box.get_allocation()
1154        maxw, maxh = self.client.get_root_size()
1155        ngraphs = 2+int(SHOW_SOUND_STATS)
1156        #the preferred size (which does not cause the window to grow too big):
1157        W = 360
1158        H = 160*3//ngraphs
1159        w = min(maxw, max(W, rect.width-20))
1160        #need some padding to be able to shrink the window again:
1161        pad = 50
1162        h = min(maxh-pad//ngraphs, max(H, (h-bh-pad)//ngraphs, (rect.height-bh-pad)//ngraphs))
1163        #bandwidth graph:
1164        labels, datasets = [], []
1165        if self.net_in_bitcount and self.net_out_bitcount:
1166            def unit(scale):
1167                if scale==1:
1168                    return ""
1169                unit, value = to_std_unit(scale)
1170                if value==1:
1171                    return str(unit)
1172                return "x%s%s" % (int(value), unit)
1173            net_in_scale, net_in_data = values_to_diff_scaled_values(tuple(self.net_in_bitcount)[1:N_SAMPLES+3], scale_unit=1000, min_scaled_value=50)
1174            net_out_scale, net_out_data = values_to_diff_scaled_values(tuple(self.net_out_bitcount)[1:N_SAMPLES+3], scale_unit=1000, min_scaled_value=50)
1175            if SHOW_RECV:
1176                labels += ["recv %sb/s" % unit(net_in_scale), "sent %sb/s" % unit(net_out_scale)]
1177                datasets += [net_in_data, net_out_data]
1178            else:
1179                labels += ["recv %sb/s" % unit(net_in_scale)]
1180                datasets += [net_in_data]
1181        if mixin_features.windows and SHOW_PIXEL_STATS and self.client.windows_enabled:
1182            pixel_scale, in_pixels = values_to_scaled_values(tuple(self.pixel_in_data)[3:N_SAMPLES+4], min_scaled_value=100)
1183            datasets.append(in_pixels)
1184            labels.append("%s pixels/s" % unit(pixel_scale))
1185        if mixin_features.audio and SHOW_SOUND_STATS and self.sound_in_bitcount:
1186            sound_in_scale, sound_in_data = values_to_diff_scaled_values(tuple(self.sound_in_bitcount)[1:N_SAMPLES+3], scale_unit=1000, min_scaled_value=50)
1187            datasets.append(sound_in_data)
1188            labels.append("Speaker %sb/s" % unit(sound_in_scale))
1189        if mixin_features.audio and SHOW_SOUND_STATS and self.sound_out_bitcount:
1190            sound_out_scale, sound_out_data = values_to_diff_scaled_values(tuple(self.sound_out_bitcount)[1:N_SAMPLES+3], scale_unit=1000, min_scaled_value=50)
1191            datasets.append(sound_out_data)
1192            labels.append("Mic %sb/s" % unit(sound_out_scale))
1193
1194        if labels and datasets:
1195            surface = make_graph_imagesurface(datasets, labels=labels,
1196                                              width=w, height=h,
1197                                              title="Bandwidth", min_y_scale=10, rounding=10,
1198                                              start_x_offset=start_x_offset)
1199            self.set_graph_surface(self.bandwidth_graph, surface)
1200
1201        def norm_lists(items, size=N_SAMPLES):
1202            #ensures we always have exactly 20 values,
1203            #(and skip if we don't have any)
1204            values, labels = [], []
1205            for l, name in items:
1206                if not l:
1207                    continue
1208                l = list(l)
1209                if len(l)<size:
1210                    for _ in range(size-len(l)):
1211                        l.insert(0, None)
1212                else:
1213                    l = l[:size]
1214                values.append(l)
1215                labels.append(name)
1216            return values, labels
1217
1218        #latency graph:
1219        latency_values, latency_labels = norm_lists(
1220            (
1221                (self.avg_ping_latency,         "network"),
1222                (self.avg_batch_delay,          "batch delay"),
1223                (self.avg_damage_out_latency,   "encode&send"),
1224                (self.avg_decoding_latency,     "decoding"),
1225                (self.avg_total,                "frame total"),
1226            ))
1227        #debug:
1228        #for i, v in enumerate(latency_values):
1229        #    log.warn("%20s = %s", latency_labels[i], v)
1230        surface = make_graph_imagesurface(latency_values, labels=latency_labels,
1231                                          width=w, height=h,
1232                                          title="Latency (ms)", min_y_scale=10, rounding=25,
1233                                          start_x_offset=start_x_offset)
1234        self.set_graph_surface(self.latency_graph, surface)
1235
1236        if mixin_features.audio and SHOW_SOUND_STATS and self.client.sound_sink:
1237            #sound queue graph:
1238            queue_values, queue_labels = norm_lists(
1239                (
1240                    (self.sound_out_queue_max, "Max"),
1241                    (self.sound_out_queue_cur, "Level"),
1242                    (self.sound_out_queue_min, "Min"),
1243                    ), N_SAMPLES*10)
1244            surface = make_graph_imagesurface(queue_values, labels=queue_labels,
1245                                              width=w, height=h,
1246                                              title="Sound Buffer (ms)", min_y_scale=10, rounding=25,
1247                                              start_x_offset=start_x_offset)
1248            self.set_graph_surface(self.sound_queue_graph, surface)
1249        return True
1250
1251    def save_graph(self, _ebox, btn, graph):
1252        log("save_graph%s", (btn, graph))
1253        chooser = Gtk.FileChooserDialog("Save graph as a PNG image",
1254                                    parent=self, action=Gtk.FileChooserAction.SAVE,
1255                                    buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
1256        chooser.set_select_multiple(False)
1257        chooser.set_default_response(Gtk.ResponseType.OK)
1258        file_filter = Gtk.FileFilter()
1259        file_filter.set_name("PNG")
1260        file_filter.add_pattern("*.png")
1261        chooser.add_filter(file_filter)
1262        response = chooser.run()
1263        filenames = chooser.get_filenames()
1264        chooser.hide()
1265        chooser.destroy()
1266        if response == Gtk.ResponseType.OK:
1267            if len(filenames)==1:
1268                filename = filenames[0]
1269                if not filename.lower().endswith(".png"):
1270                    filename += ".png"
1271                surface = graph.surface
1272                log("saving surface %s to %s", surface, filename)
1273                from io import BytesIO
1274                b = BytesIO()
1275                surface.write_to_png(b)
1276                def save_file():
1277                    with open(filename, "wb") as f:
1278                        f.write(b.getvalue())
1279                from xpra.make_thread import start_thread
1280                start_thread(save_file, "save-graph")
1281        elif response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.CLOSE, Gtk.ResponseType.DELETE_EVENT):
1282            log("closed/cancelled")
1283        else:
1284            log.warn("unknown chooser response: %d" % response)
1285
1286    def destroy(self, *args):
1287        log("SessionInfo.destroy(%s) is_closed=%s", args, self.is_closed)
1288        self.is_closed = True
1289        super().destroy()
1290        log("SessionInfo.destroy(%s) done", args)
1291
1292
1293class SessionInfoClient(InfoTimerClient):
1294
1295    REFRESH_RATE = envint("XPRA_INFO_REFRESH_RATE", 2)
1296
1297    def setup_connection(self, conn):
1298        self.session_name = self.server_session_name = "session-info"
1299        self.mmap_enabled = False
1300        self.windows_enabled = False
1301        def noop(*_args):
1302            pass
1303        self.send_ping = noop
1304        self.server_sound_send = self.server_sound_receive = True
1305        self.server_sound_encoders = self.server_sound_decoders = []
1306        self.server_ping_latency = self.client_ping_latency = []
1307        self.server_start_time = 0
1308        protocol = super().setup_connection(conn)
1309        self.window = None
1310        return protocol
1311
1312    def run_loop(self):
1313        Gtk.main()
1314
1315    def exit_loop(self):
1316        Gtk.main_quit()
1317
1318    def client_type(self):
1319        #overriden in subclasses!
1320        return "session-info"
1321
1322    def update_screen(self):
1323        #this is called every time we get the server info back
1324        #log.info("server_last_info=%s", self.server_last_info)
1325        if not self.server_last_info:
1326            return
1327        td = typedict(self.server_last_info)
1328        def rtdict(*keys):
1329            d = td
1330            for prop in keys:
1331                v = d.dictget(prop)
1332                #log.info("%s.get(%s)=%s", repr_ellipsized(d), prop, repr_ellipsized(v))
1333                d = typedict(v or {})
1334            return d
1335        from xpra.client.mixins.serverinfo_mixin import get_remote_lib_versions
1336        features = rtdict("features")
1337        self.server_clipboard = features.boolget("clipboard")
1338        self.server_notifications = features.boolget("notifications")
1339        display = rtdict("display")
1340        self.server_opengl = rtdict("display", "opengl")
1341        self.server_bell = display.boolget("bell")
1342        self.server_cursors = display.boolget("cursors")
1343        self.server_randr = display.boolget("randr")
1344        server = rtdict("server")
1345        self.server_actual_desktop_size = server.inttupleget("root_window_size")
1346        self.server_max_desktop_size = server.inttupleget("max_desktop_size")
1347        self._remote_lib_versions = get_remote_lib_versions(rtdict("server"))
1348        encodings = rtdict("encodings")
1349        self.server_core_encodings = encodings.strtupleget("core")
1350        network = rtdict("network")
1351        self.server_packet_encoders = network.strtupleget("encoders")
1352        self.server_compressors = network.strtupleget("compressors")
1353        self.server_load = server.inttupleget("load")
1354        self._remote_hostname = server.strget("hostname")
1355        build = rtdict("server", "build")
1356        self._remote_version = build.strget("version")
1357        self._remote_build_date = build.strget("date")
1358        self._remote_build_time = build.strget("time")
1359        self._remote_revision = build.intget("revision")
1360        self._remote_modifications = build.intget("local_modifications")
1361        self._remote_branch = build.strget("branch")
1362        self._remote_commit = build.strget("commit")
1363        platform_info = rtdict("server", "platform")
1364        self._remote_platform = platform_info.strget("")
1365        self._remote_platform_release = platform_info.strget("release")
1366        self._remote_platform_platform = platform_info.strget("platform")
1367        self._remote_platform_linux_distribution = platform_info.strget("linux_distribution")
1368        self.server_display = server.strget("display")
1369        self.idle_add(self.do_update_screen)
1370
1371    def do_update_screen(self):
1372        if not self.window:
1373            self.window = SessionInfo(self, "session-info", self._protocol._conn, show_client=False)
1374            self.window.show_all()
1375            def destroy(*_args):
1376                self.exit_loop()
1377            self.window.connect("destroy", destroy)
1378        self.window.populate()
1379        return False
1380