1#!/usr/bin/env python3
2# This file is part of Xpra.
3# Copyright (C) 2014-2020 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.path
8import sys
9import time
10from gi.repository import Gtk, Gdk
11
12from xpra.gtk_common.gtk_util import (
13    add_close_accel, scaled_image, get_icon_pixbuf,
14    get_display_info, get_default_root_window,
15    choose_file, get_gtk_version_info,
16    )
17from xpra.os_util import hexstr
18from xpra.platform.gui import force_focus
19from xpra.util import nonl, envint, envbool, repr_ellipsized
20from xpra.log import Logger
21
22log = Logger("util")
23
24STEP_DELAY = envint("XPRA_BUG_REPORT_STEP_DELAY", 0)
25OBFUSCATE = envbool("XPRA_OBFUSCATE_BUG_REPORT", True)
26
27
28class BugReport:
29
30    def __init__(self):
31        self.checkboxes = {}
32        self.server_log = None
33
34    def init(self, show_about=True, get_server_info=None, opengl_info=None, includes=None):
35        self.show_about = show_about
36        self.get_server_info = get_server_info
37        self.opengl_info = opengl_info
38        self.includes = includes or {}
39        self.setup_window()
40
41    def setup_window(self):
42        self.window = Gtk.Window()
43        self.window.set_border_width(20)
44        self.window.connect("delete-event", self.close)
45        self.window.set_default_size(400, 300)
46        self.window.set_title("Xpra Bug Report")
47
48        icon_pixbuf = get_icon_pixbuf("bugs.png")
49        if icon_pixbuf:
50            self.window.set_icon(icon_pixbuf)
51        self.window.set_position(Gtk.WindowPosition.CENTER)
52
53        vbox = Gtk.VBox(False, 0)
54        vbox.set_spacing(15)
55
56        # Title
57        hbox = Gtk.HBox(False, 0)
58        icon_pixbuf = get_icon_pixbuf("xpra.png")
59        if icon_pixbuf and self.show_about:
60            from xpra.gtk_common.about import about
61            logo_button = Gtk.Button("")
62            settings = logo_button.get_settings()
63            settings.set_property('gtk-button-images', True)
64            logo_button.connect("clicked", about)
65            logo_button.set_tooltip_text("About")
66            image = Gtk.Image()
67            image.set_from_pixbuf(icon_pixbuf)
68            logo_button.set_image(image)
69            hbox.pack_start(logo_button, expand=False, fill=False)
70
71        #the box containing all the input:
72        ibox = Gtk.VBox(False, 0)
73        ibox.set_spacing(3)
74        vbox.pack_start(ibox)
75
76        # Description
77        al = Gtk.Alignment(xalign=0, yalign=0.5, xscale=0.0, yscale=0)
78        al.add(Gtk.Label("Please describe the problem:"))
79        ibox.pack_start(al)
80        #self.description = Gtk.Entry(max=128)
81        #self.description.set_width_chars(40)
82        self.description = Gtk.TextView()
83        self.description.set_accepts_tab(True)
84        self.description.set_justification(Gtk.Justification.LEFT)
85        self.description.set_border_width(2)
86        self.description.set_size_request(300, 80)
87        #self.description.modify_bg(Gtk.StateType.NORMAL, Gdk.Color(red=32768, green=32768, blue=32768))
88        ibox.pack_start(self.description, expand=False, fill=False)
89
90        # Toggles:
91        al = Gtk.Alignment(xalign=0, yalign=0.5, xscale=0.0, yscale=0)
92        al.add(Gtk.Label("Include:"))
93        ibox.pack_start(al)
94        #generic toggles:
95        from xpra.gtk_common.keymap import get_gtk_keymap
96        from xpra.codecs.loader import codec_versions, load_codecs, show_codecs
97        load_codecs()
98        show_codecs()
99        try:
100            from xpra.sound.wrapper import query_sound
101            def get_sound_info():
102                return query_sound()
103        except ImportError:
104            get_sound_info = None
105        def get_gl_info():
106            if self.opengl_info:
107                return self.opengl_info
108        from xpra.net.net_util import get_info as get_net_info
109        from xpra.platform.paths import get_info as get_path_info
110        from xpra.platform.gui import get_info as get_gui_info
111        from xpra.version_util import get_version_info, get_platform_info, get_host_info
112        def get_sys_info():
113            from xpra.platform.info import get_user_info
114            from xpra.scripts.config import read_xpra_defaults
115            return {
116                    "argv"          : sys.argv,
117                    "path"          : sys.path,
118                    "exec_prefix"   : sys.exec_prefix,
119                    "executable"    : sys.executable,
120                    "version"       : get_version_info(),
121                    "platform"      : get_platform_info(),
122                    "host"          : get_host_info(OBFUSCATE),
123                    "paths"         : get_path_info(),
124                    "gtk"           : get_gtk_version_info(),
125                    "gui"           : get_gui_info(),
126                    "display"       : get_display_info(),
127                    "user"          : get_user_info(),
128                    "env"           : os.environ,
129                    "config"        : read_xpra_defaults(),
130                    }
131        get_screenshot, take_screenshot_fn = None, None
132        #screenshot: may have OS-specific code
133        try:
134            from xpra.platform.gui import take_screenshot
135            take_screenshot_fn = take_screenshot
136        except ImportError:
137            log("failed to load platfrom specific screenshot code", exc_info=True)
138        if not take_screenshot_fn:
139            #try with Pillow:
140            try:
141                from PIL import ImageGrab           #@UnresolvedImport
142                from io import BytesIO
143                def pillow_imagegrab_screenshot():
144                    img = ImageGrab.grab()
145                    out = BytesIO()
146                    img.save(out, format="PNG")
147                    v = out.getvalue()
148                    out.close()
149                    return (img.width, img.height, "png", img.width*3, v)
150                take_screenshot_fn = pillow_imagegrab_screenshot
151            except Exception as e:
152                log("cannot use Pillow's ImageGrab: %s", e)
153        if not take_screenshot_fn:
154            #default: gtk screen capture
155            try:
156                from xpra.server.shadow.gtk_root_window_model import GTKImageCapture
157                rwm = GTKImageCapture(get_default_root_window())
158                take_screenshot_fn = rwm.take_screenshot
159            except Exception:
160                log.warn("Warning: failed to load gtk screenshot code", exc_info=True)
161        log("take_screenshot_fn=%s", take_screenshot_fn)
162        if take_screenshot_fn:
163            def _get_screenshot():
164                #take_screenshot() returns: w, h, "png", rowstride, data
165                return take_screenshot_fn()[4]
166            get_screenshot = _get_screenshot
167        def get_server_log():
168            return self.server_log
169        self.toggles = (
170            ("system",       "txt",  "System",           get_sys_info,   True,
171             "Xpra version, platform and host information - including hostname and account information"),
172            ("server-log",   "txt",  "Server Log",       get_server_log, bool(self.server_log),
173             "Xpra version, platform and host information - including hostname and account information"),
174            ("network",      "txt",  "Network",          get_net_info,   True,
175             "Compression, packet encoding and encryption"),
176            ("encoding",     "txt",  "Encodings",        codec_versions, bool(codec_versions),
177             "Picture encodings supported"),
178            ("opengl",       "txt",  "OpenGL",           get_gl_info,    bool(self.opengl_info),
179             "OpenGL driver and features"),
180            ("sound",        "txt",  "Sound",            get_sound_info, bool(get_sound_info),
181             "Sound codecs and GStreamer version information"),
182            ("keyboard",     "txt",  "Keyboard Mapping", get_gtk_keymap, True,
183             "Keyboard layout and key mapping"),
184            ("xpra-info",    "txt",  "Server Info",      self.get_server_info,   bool(self.get_server_info),
185             "Full server information from 'xpra info'"),
186            ("screenshot",   "png",  "Screenshot",       get_screenshot, bool(get_screenshot),
187             ""),
188            )
189        self.checkboxes = {}
190        for name, _, title, value_cb, sensitive, tooltip in self.toggles:
191            cb = Gtk.CheckButton(title+[" (not available)", ""][bool(value_cb)])
192            cb.set_active(self.includes.get(name, True))
193            cb.set_sensitive(sensitive)
194            cb.set_tooltip_text(tooltip)
195            ibox.pack_start(cb)
196            self.checkboxes[name] = cb
197
198        # Buttons:
199        hbox = Gtk.HBox(False, 20)
200        vbox.pack_start(hbox)
201        def btn(label, tooltip, callback, icon_name=None):
202            btn = Gtk.Button(label)
203            btn.set_tooltip_text(tooltip)
204            btn.connect("clicked", callback)
205            if icon_name:
206                icon = get_icon_pixbuf(icon_name)
207                if icon:
208                    btn.set_image(scaled_image(icon, 24))
209            hbox.pack_start(btn)
210            return btn
211
212        btn("Copy to clipboard", "Copy all data to clipboard", self.copy_clicked, "clipboard.png")
213        btn("Save", "Save Bug Report", self.save_clicked, "download.png")
214        btn("Cancel", "", self.close, "quit.png")
215
216        def accel_close(*_args):
217            self.close()
218        add_close_accel(self.window, accel_close)
219        vbox.show_all()
220        self.window.vbox = vbox
221        self.window.add(vbox)
222
223
224    def set_server_log_data(self, filedata):
225        self.server_log = filedata
226        cb = self.checkboxes.get("server-log")
227        log("set_server_log_data(%i bytes) cb=%s", len(filedata or b""), cb)
228        if cb:
229            cb.set_sensitive(bool(filedata))
230
231
232    def show(self):
233        log("show()")
234        if not self.window:
235            self.setup_window()
236        force_focus()
237        self.window.show_all()
238        self.window.present()
239
240    def hide(self):
241        log("hide()")
242        if self.window:
243            self.window.hide()
244
245    def close(self, *args):
246        log("close%s", args)
247        if self.window:
248            self.hide()
249            self.window = None
250        return True
251
252    def destroy(self, *args):
253        log("destroy%s", args)
254        if self.window:
255            self.window.destroy()
256            self.window = None
257
258
259    def run(self):
260        log("run()")
261        Gtk.main()
262        log("run() Gtk.main done")
263
264    def quit(self, *args):
265        log("quit%s", args)
266        self.destroy()
267        Gtk.main_quit()
268
269
270    def get_data(self):
271        log("get_data() collecting bug report data")
272        data = []
273        tb = self.description.get_buffer()
274        buf = tb.get_text(*tb.get_bounds(), include_hidden_chars=False)
275        if buf:
276            data.append(("Description", "", "txt", buf))
277        for name, dtype, title, value_cb, _, tooltip in self.toggles:
278            if not bool(value_cb):
279                continue
280            cb = self.checkboxes.get(name)
281            assert cb is not None
282            if not cb.get_active():
283                continue
284            log("%s is enabled (%s)", name, tooltip)
285            #OK, the checkbox is selected, get the data
286            value = value_cb
287            if not isinstance(value_cb, dict):
288                try:
289                    value = value_cb()
290                except TypeError:
291                    log.error("Error collecting %s bug report data using %s", name, value_cb, exc_info=True)
292                    value = str(value_cb)
293                    dtype = "txt"
294                except Exception as e:
295                    log.error("Error collecting %s bug report data using %s", name, value_cb, exc_info=True)
296                    value = e
297                    dtype = "txt"
298            if value is None:
299                s = "not available"
300            elif isinstance(value, dict):
301                s = os.linesep.join("%s : %s" % (k.ljust(32), nonl(str(v))) for k,v in sorted(value.items()))
302            elif isinstance(value, (list, tuple)):
303                s = os.linesep.join(str(x) for x in value)
304            else:
305                s = value
306            log("%s (%s) %s: %s", title, tooltip, dtype, repr_ellipsized(s))
307            data.append((title, tooltip, dtype, s))
308            time.sleep(STEP_DELAY)
309        return data
310
311    def copy_clicked(self, *_args):
312        data = self.get_data()
313        def cdata(v):
314            if isinstance(v, bytes):
315                return hexstr(v)
316            return str(v)
317        text = os.linesep.join("%s: %s%s%s%s" % (title, tooltip, os.linesep, cdata(v), os.linesep)
318                               for (title,tooltip,dtype,v) in data if dtype=="txt")
319        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
320        clipboard.set_text(text, len(text))
321        log.info("%s characters copied to clipboard", len(text))
322
323    def save_clicked(self, *_args):
324        file_filter = Gtk.FileFilter()
325        file_filter.set_name("ZIP")
326        file_filter.add_pattern("*.zip")
327        choose_file(self.window, "Save Bug Report Data",  Gtk.FileChooserAction.SAVE, Gtk.STOCK_SAVE, self.do_save)
328
329    def do_save(self, filename):
330        log("do_save(%s)", filename)
331        if not filename.lower().endswith(".zip"):
332            filename = filename+".zip"
333        basenoext = os.path.splitext(os.path.basename(filename))[0]
334        data = self.get_data()
335        import zipfile
336        zf = None
337        try:
338            zf = zipfile.ZipFile(filename, mode='w', compression=zipfile.ZIP_DEFLATED)
339            for title, tooltip, dtype, s in data:
340                cfile = os.path.join(basenoext, title.replace(" ", "_")+"."+dtype)
341                info = zipfile.ZipInfo(cfile, date_time=time.localtime(time.time()))
342                info.compress_type = zipfile.ZIP_DEFLATED
343                #very poorly documented:
344                info.external_attr = 0o644 << 16
345                info.comment = str(tooltip).encode("utf8")
346                if isinstance(s, bytes):
347                    try:
348                        try:
349                            import tempfile
350                            temp = tempfile.NamedTemporaryFile(prefix="xpra.", suffix=".%s" % dtype, delete=False)
351                            with temp:
352                                temp.write(s)
353                                temp.flush()
354                        except OSError as e:
355                            log.error("Error: cannot create mmap file:")
356                            log.error(" %s", e)
357                        else:
358                            zf.write(temp.name, cfile, zipfile.ZIP_STORED if dtype=="png" else zipfile.ZIP_DEFLATED)
359                    finally:
360                        if temp:
361                            os.unlink(temp.name)
362                else:
363                    zf.writestr(info, str(s))
364        except OSError as e:
365            log("do_save(%s) failed to save zip file", filename, exc_info=True)
366            dialog = Gtk.MessageDialog(self.window, 0, Gtk.MessageType.WARNING,
367                                       Gtk.ButtonsType.CLOSE, "Failed to save ZIP file")
368            dialog.format_secondary_text("%s" % e)
369            def close(*_args):
370                dialog.destroy()
371            dialog.connect("response", close)
372            dialog.show_all()
373        finally:
374            if zf:
375                zf.close()
376