Package Gnumed :: Package wxpython :: Module gmExceptionHandlingWidgets
[frames] | no frames]

Source Code for Module Gnumed.wxpython.gmExceptionHandlingWidgets

  1  """GNUmed exception handling widgets.""" 
  2  # ======================================================================== 
  3  __author__  = "K. Hilbert <Karsten.Hilbert@gmx.net>" 
  4  __license__ = "GPL v2 or later (details at http://www.gnu.org)" 
  5   
  6  import logging 
  7  import traceback 
  8  import sys 
  9  import os 
 10  import shutil 
 11  import datetime as pyDT 
 12  import re as regex 
 13   
 14   
 15  import wx 
 16   
 17   
 18  from Gnumed.pycommon import gmDispatcher 
 19  from Gnumed.pycommon import gmCfg2 
 20  from Gnumed.pycommon import gmI18N 
 21  from Gnumed.pycommon import gmLog2 
 22  from Gnumed.pycommon import gmPG2 
 23  from Gnumed.pycommon import gmExceptions 
 24  from Gnumed.pycommon import gmNetworkTools 
 25  from Gnumed.pycommon.gmTools import u_box_horiz_single 
 26   
 27  from Gnumed.business import gmPraxis 
 28   
 29  from Gnumed.wxpython import gmGuiHelpers 
 30   
 31   
 32  _log = logging.getLogger('gm.gui') 
 33   
 34  _prev_excepthook = None 
 35  application_is_closing = False 
 36   
 37  #========================================================================= 
38 -def set_client_version(version):
39 global _client_version 40 _client_version = version
41 42 #-------------------------------------------------------------------------
43 -def set_sender_email(email):
44 global _sender_email 45 _sender_email = email
46 47 #-------------------------------------------------------------------------
48 -def set_helpdesk(helpdesk):
49 global _helpdesk 50 _helpdesk = helpdesk
51 52 #-------------------------------------------------------------------------
53 -def set_staff_name(staff_name):
54 global _staff_name 55 _staff_name = staff_name
56 57 #-------------------------------------------------------------------------
58 -def set_is_public_database(value):
59 global _is_public_database 60 _is_public_database = value
61 62 #------------------------------------------------------------------------- 63 # exception handlers 64 #-------------------------------------------------------------------------
65 -def __ignore_dead_objects_from_async(t, v, tb):
66 67 if t != RuntimeError: 68 return False 69 70 wx.EndBusyCursor() 71 72 # try to ignore those, they come about from doing 73 # async work in wx as Robin tells us 74 _log.error('RuntimeError = dead object: %s', v) 75 _log.warning('continuing and hoping for the best') 76 return True
77 78 #-------------------------------------------------------------------------
79 -def __handle_exceptions_on_shutdown(t, v, tb):
80 81 if not application_is_closing: 82 return False 83 84 # dead object error ? 85 if t == RuntimeError: 86 return True 87 88 gmLog2.log_stack_trace('exception on shutdown', t, v, tb) 89 return True
90 91 #-------------------------------------------------------------------------
92 -def __handle_import_error(t, v, tb):
93 94 if t == OSError: 95 if not hasattr(t, 'winerror'): 96 return False 97 if getattr(t, 'winerror') != 126: 98 return False 99 else: 100 if t != ImportError: 101 return False 102 103 wx.EndBusyCursor() 104 105 _log.error('module [%s] not installed', v) 106 gmGuiHelpers.gm_show_error ( 107 aTitle = _('Missing GNUmed module'), 108 aMessage = _( 109 'GNUmed detected that parts of it are not\n' 110 'properly installed. The following message\n' 111 'names the missing part:\n' 112 '\n' 113 ' "%s"\n' 114 '\n' 115 'Please make sure to get the missing\n' 116 'parts installed. Otherwise some of the\n' 117 'functionality will not be accessible.' 118 ) % v 119 ) 120 return True
121 122 #-------------------------------------------------------------------------
123 -def __handle_ctrl_c(t, v, tb):
124 125 if t != KeyboardInterrupt: 126 return False 127 128 print("<Ctrl-C>: Shutting down ...") 129 top_win = wx.GetApp().GetTopWindow() 130 wx.CallAfter(top_win.Close) 131 return True
132 133 #-------------------------------------------------------------------------
134 -def __handle_access_violation(t, v, tb):
135 136 if t != gmExceptions.AccessDenied: 137 return False 138 139 _log.error('access permissions violation detected') 140 wx.EndBusyCursor() 141 gmLog2.flush() 142 txt = ' ' + v.errmsg 143 if v.source is not None: 144 txt += _('\n Source: %s') % v.source 145 if v.code is not None: 146 txt += _('\n Code: %s') % v.code 147 if v.details is not None: 148 txt += _('\n Details (first 250 characters):\n%s\n%s\n%s') % ( 149 u_box_horiz_single * 50, 150 v.details[:250], 151 u_box_horiz_single * 50 152 ) 153 gmGuiHelpers.gm_show_error ( 154 aTitle = _('Access violation'), 155 aMessage = _( 156 'You do not have access to this part of GNUmed.\n' 157 '\n' 158 '%s' 159 ) % txt 160 ) 161 return True
162 163 #-------------------------------------------------------------------------
164 -def __handle_lost_db_connection(t, v, tb):
165 166 if not gmPG2.exception_is_connection_loss(v): 167 return False 168 169 gmPG2.log_pg_exception_details(v) 170 gmLog2.log_stack_trace('lost connection', t, v, tb) 171 wx.EndBusyCursor() 172 gmLog2.flush() 173 gmGuiHelpers.gm_show_error ( 174 aTitle = _('Lost connection'), 175 aMessage = _( 176 'Since you were last working in GNUmed,\n' 177 'your database connection timed out.\n' 178 '\n' 179 'This GNUmed session is now expired.\n' 180 '\n' 181 'You will have to close this client and\n' 182 'restart a new GNUmed session.' 183 ) 184 ) 185 return True
186 187 #-------------------------------------------------------------------------
188 -def __handle_wxgtk_assertion(t, v, tb):
189 if t != wx.wxAssertionError: 190 return False 191 _log.exception('a wxGTK assertion failed:') 192 _log.warning('continuing and hoping for the best') 193 return True
194 195 #-------------------------------------------------------------------------
196 -def handle_uncaught_exception_wx(t, v, tb):
197 198 _log.debug('unhandled exception caught:', exc_info = (t, v, tb)) 199 200 if __handle_access_violation(t, v, tb): 201 return 202 203 if __handle_ctrl_c(t, v, tb): 204 return 205 206 if __handle_exceptions_on_shutdown(t, v, tb): 207 return 208 209 if __ignore_dead_objects_from_async(t, v, tb): 210 return 211 212 if __handle_import_error(t, v, tb): 213 return 214 215 # if __handle_wxgtk_assertion(t, v, tb) 216 # return 217 218 # other exceptions 219 _cfg = gmCfg2.gmCfgData() 220 if _cfg.get(option = 'debug') is False: 221 _log.error('enabling debug mode') 222 _cfg.set_option(option = 'debug', value = True) 223 root_logger = logging.getLogger() 224 root_logger.setLevel(logging.DEBUG) 225 _log.debug('unhandled exception caught:', exc_info = (t, v, tb)) 226 227 if __handle_lost_db_connection(t, v, tb): 228 return 229 230 gmLog2.log_stack_trace(None, t, v, tb) 231 232 # only do this here or else we can invalidate the stack trace 233 # by Windows throwing an exception ... |-( 234 # careful: MSW does reference counting on Begin/End* :-( 235 wx.EndBusyCursor() 236 237 name = os.path.basename(_logfile_name) 238 name, ext = os.path.splitext(name) 239 new_name = os.path.expanduser(os.path.join ( 240 '~', 241 '.gnumed', 242 'error_logs', 243 '%s_%s%s' % (name, pyDT.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'), ext) 244 )) 245 246 dlg = cUnhandledExceptionDlg(None, -1, exception = (t, v, tb), logfile = new_name) 247 dlg.ShowModal() 248 comment = dlg._TCTRL_comment.GetValue() 249 dlg.DestroyLater() 250 if (comment is not None) and (comment.strip() != ''): 251 _log.error('user comment: %s', comment.strip()) 252 253 _log.warning('syncing log file for backup to [%s]', new_name) 254 gmLog2.flush() 255 # keep a copy around 256 shutil.copy2(_logfile_name, new_name)
257 258 #------------------------------------------------------------------------
259 -def install_wx_exception_handler():
260 261 global _logfile_name 262 _logfile_name = gmLog2._logfile_name 263 264 global _local_account 265 _local_account = os.path.basename(os.path.expanduser('~')) 266 267 set_helpdesk(gmPraxis.gmCurrentPraxisBranch().helpdesk) 268 set_staff_name(_local_account) 269 set_is_public_database(False) 270 set_sender_email(None) 271 set_client_version('gmExceptionHandlingWidgets.py <default>') 272 273 gmDispatcher.connect(signal = 'application_closing', receiver = _on_application_closing) 274 275 global APP_PID 276 APP_PID = os.getpid() 277 _log.debug('registered PID [%s] for aborting if necessary', APP_PID) 278 279 global _prev_excepthook 280 _prev_excepthook = sys.excepthook 281 sys.excepthook = handle_uncaught_exception_wx 282 283 return True
284 285 #------------------------------------------------------------------------
286 -def uninstall_wx_exception_handler():
287 if _prev_excepthook is None: 288 sys.excepthook = sys.__excepthook__ 289 return True 290 sys.excepthook = _prev_excepthook 291 return True
292 293 #------------------------------------------------------------------------
294 -def _on_application_closing():
295 global application_is_closing 296 # used to ignore a few exceptions, such as when the 297 # C++ object has been destroyed before the Python one 298 application_is_closing = True
299 300 # ========================================================================
301 -def mail_log(parent=None, comment=None, helpdesk=None, sender=None):
302 303 if (comment is None) or (comment.strip() == ''): 304 comment = wx.GetTextFromUser ( 305 message = _( 306 'Please enter a short note on what you\n' 307 'were about to do in GNUmed:' 308 ), 309 caption = _('Sending bug report'), 310 parent = parent 311 ) 312 if comment.strip() == '': 313 comment = '<user did not comment on bug report>' 314 315 receivers = [] 316 if helpdesk is not None: 317 receivers = regex.findall ( 318 '[\S]+@[\S]+', 319 helpdesk.strip(), 320 flags = regex.UNICODE 321 ) 322 if len(receivers) == 0: 323 if _is_public_database: 324 receivers = ['gnumed-bugs@gnu.org'] 325 326 receiver_string = wx.GetTextFromUser ( 327 message = _( 328 'Edit the list of email addresses to send the\n' 329 'bug report to (separate addresses by spaces).\n' 330 '\n' 331 'Note that <gnumed-bugs@gnu.org> refers to\n' 332 'the public (!) GNUmed bugs mailing list.' 333 ), 334 caption = _('Sending bug report'), 335 default_value = ','.join(receivers), 336 parent = parent 337 ) 338 if receiver_string.strip() == '': 339 return 340 341 receivers = regex.findall ( 342 '[\S]+@[\S]+', 343 receiver_string, 344 flags = regex.UNICODE 345 ) 346 347 dlg = gmGuiHelpers.c2ButtonQuestionDlg ( 348 parent, 349 -1, 350 caption = _('Sending bug report'), 351 question = _( 352 'Your bug report will be sent to:\n' 353 '\n' 354 '%s\n' 355 '\n' 356 'Make sure you have reviewed the log file for potentially\n' 357 'sensitive information before sending out the bug report.\n' 358 '\n' 359 'Note that emailing the report may take a while depending\n' 360 'on the speed of your internet connection.\n' 361 ) % '\n'.join(receivers), 362 button_defs = [ 363 {'label': _('Send report'), 'tooltip': _('Yes, send the bug report.')}, 364 {'label': _('Cancel'), 'tooltip': _('No, do not send the bug report.')} 365 ], 366 show_checkbox = True, 367 checkbox_msg = _('include log file in bug report') 368 ) 369 dlg._CHBOX_dont_ask_again.SetValue(_is_public_database) 370 go_ahead = dlg.ShowModal() 371 if go_ahead == wx.ID_NO: 372 dlg.DestroyLater() 373 return 374 375 include_log = dlg._CHBOX_dont_ask_again.GetValue() 376 if not _is_public_database: 377 if include_log: 378 result = gmGuiHelpers.gm_show_question ( 379 _( 380 'The database you are connected to is marked as\n' 381 '"in-production with controlled access".\n' 382 '\n' 383 'You indicated that you want to include the log\n' 384 'file in your bug report. While this is often\n' 385 'useful for debugging the log file might contain\n' 386 'bits of patient data which must not be sent out\n' 387 'without de-identification.\n' 388 '\n' 389 'Please confirm that you want to include the log !' 390 ), 391 _('Sending bug report') 392 ) 393 include_log = (result is True) 394 395 if sender is None: 396 sender = _('<not supplied>') 397 else: 398 if sender.strip() == '': 399 sender = _('<not supplied>') 400 401 msg = """\ 402 Report sent via GNUmed's handler for unexpected exceptions. 403 404 user comment : %s 405 406 client version: %s 407 408 system account: %s 409 staff member : %s 410 sender email : %s 411 412 # enable Launchpad bug tracking 413 affects gnumed 414 tag automatic-report 415 importance medium 416 417 """ % (comment, _client_version, _local_account, _staff_name, sender) 418 if include_log: 419 _log.error(comment) 420 _log.warning('syncing log file for emailing') 421 gmLog2.flush() 422 attachments = [ [_logfile_name, 'text/plain', 'quoted-printable'] ] 423 else: 424 attachments = None 425 426 dlg.DestroyLater() 427 428 wx.BeginBusyCursor() 429 _cfg = gmCfg2.gmCfgData() 430 try: 431 gmNetworkTools.compose_and_send_email ( 432 sender = '%s <%s>' % (_staff_name, gmNetworkTools.default_mail_sender), 433 receiver = receivers, 434 subject = '<bug>: %s' % comment, 435 message = msg, 436 server = gmNetworkTools.default_mail_server, 437 auth = {'user': gmNetworkTools.default_mail_sender, 'password': 'gnumed-at-gmx-net'}, 438 debug = _cfg.get(option = 'debug'), 439 attachments = attachments 440 ) 441 gmDispatcher.send(signal='statustext', msg = _('Bug report has been emailed.')) 442 except: 443 _log.exception('cannot send bug report') 444 gmDispatcher.send(signal='statustext', msg = _('Bug report COULD NOT be emailed.')) 445 wx.EndBusyCursor()
446 447 # ======================================================================== 448 from Gnumed.wxGladeWidgets import wxgUnhandledExceptionDlg 449
450 -class cUnhandledExceptionDlg(wxgUnhandledExceptionDlg.wxgUnhandledExceptionDlg):
451
452 - def __init__(self, *args, **kwargs):
453 454 exception = kwargs['exception'] 455 del kwargs['exception'] 456 self.logfile = kwargs['logfile'] 457 del kwargs['logfile'] 458 459 wxgUnhandledExceptionDlg.wxgUnhandledExceptionDlg.__init__(self, *args, **kwargs) 460 461 self.Title = '%s [#%s]' % (self.Title, APP_PID) 462 463 if _sender_email is not None: 464 self._TCTRL_sender.SetValue(_sender_email) 465 self._TCTRL_helpdesk.SetValue(_helpdesk) 466 self._TCTRL_logfile.SetValue(self.logfile) 467 t, v, tb = exception 468 self._TCTRL_traceback.SetValue ('%s: %s\n%s: %s\n%s' % ( 469 'type', t, 470 'value', v, 471 ''.join(traceback.format_tb(tb)) 472 )) 473 self.Fit()
474 475 #------------------------------------------
476 - def _on_close_gnumed_button_pressed(self, evt):
477 evt.Skip() 478 comment = self._TCTRL_comment.GetValue() 479 if (comment is not None) and (comment.strip() != ''): 480 _log.error('user comment: %s', comment.strip()) 481 _log.warning('syncing log file for backup to [%s]', self.logfile) 482 gmLog2.flush() 483 try: 484 shutil.copy2(_logfile_name, self.logfile) 485 except IOError: 486 _log.error('cannot backup log file') 487 top_win = wx.GetApp().GetTopWindow() 488 wx.CallAfter(top_win.Close)
489 490 #------------------------------------------
491 - def _on_abort_gnumed_button_pressed(self, evt):
492 print('running os._exit(-999)') 493 os._exit(-999) 494 print('running os.kill(9) on current process') 495 os.kill(APP_PID, 9) 496 print('running sys.exit()') 497 sys.exit(-999)
498 # ideas: 499 # - start timer thread with abort code from [close] button 500 # - start a detached shell script at the OS level with "sleep 20 ; kill -15 PID ; kill -9 PID" 501 502 #------------------------------------------
503 - def _on_mail_button_pressed(self, evt):
504 evt.Skip() 505 mail_log ( 506 parent = self, 507 comment = self._TCTRL_comment.GetValue().strip(), 508 helpdesk = self._TCTRL_helpdesk.GetValue().strip(), 509 sender = self._TCTRL_sender.GetValue().strip() 510 )
511 512 #------------------------------------------
513 - def _on_view_log_button_pressed(self, evt):
514 evt.Skip() 515 from Gnumed.pycommon import gmMimeLib 516 gmLog2.flush() 517 gmMimeLib.call_viewer_on_file(_logfile_name, block = False)
518 519 # ======================================================================== 520