Gnumed.wxpython.gmExceptionHandlingWidgets

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