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