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