Gnumed.pycommon.gmHooks

Package Gnumed :: Package pycommon :: Module gmHooks |
[frames] | no frames]

# Source Code for Module Gnumed.pycommon.gmHooks

      1  """GNUmed hooks framework.
      2
      3  This module provides convenience functions and definitions
      4  for accessing the GNUmed hooks framework.
      5
      6  This framework calls the script
      7
      8          ~/.gnumed/scripts/hook_script.py
      9
     10  at various times during client execution. The script must
     11  contain a function
     12
     13  def run_script(hook=None):
     14          pass
     15
     16  which accepts a single argument <hook>. That argument will
     17  contain the hook that is being activated.
     18  """
     19  # ========================================================================
     20  __author__  = "K. Hilbert <Karsten.Hilbert@gmx.net>"
     21  __license__ = "GPL v2 or later (details at http://www.gnu.org)"
     22
     23  # stdlib
     24  import os
     25  import sys
     26  import stat
     27  import logging
     28  import io
     29
     30
     31  # GNUmed libs
     32  if __name__ == '__main__':
     33          sys.path.insert(0, '../../')
     34  from Gnumed.pycommon import gmDispatcher
     35  from Gnumed.pycommon import gmTools
     36
     37
     38  _log = logging.getLogger('gm.hook')
     39
     40  # ========================================================================
     41  known_hooks = [
     42          'post_patient_activation',
     43          'post_person_creation',
     44
     45          'after_waiting_list_modified',
     46
     47          'shutdown-post-GUI',
     48          'startup-after-GUI-init',
     49          'startup-before-GUI',
     50
     51          'request_user_attention',
     52          'app_activated_startup',
     53          'app_activated',
     54          'app_deactivated',
     55
     56          'after_substance_intake_modified',
     57          'after_test_result_modified',
     58          'after_soap_modified',
     59          'after_code_link_modified',
     60
     61          'after_new_doc_created',
     62          'before_print_doc',
     63          'before_fax_doc',
     64          'before_mail_doc',
     65          'before_print_doc_part',
     66          'before_fax_doc_part',
     67          'before_mail_doc_part',
     68          'before_external_doc_access',
     69
     70          'db_maintenance_warning'
     71  ]
     72
     73
     74  README_pat_dir = """Directory for data files containing the current patient.
     75
     76  Whenever the patient is changed GNUmed will export
     77  formatted demographics into files in this directory
     78  for use by 3rd party software.
     79
     80  Currently exported formats:
     81
     82  GDT, VCF (vcard), MCF (mecard), XML (linuxmednews)"""
     83
     84
     85  HOOK_SCRIPT_EXAMPLE = """#!/usr/bin/python3
     86  # -*- coding: utf-8 -*-
     87  #
     88  #===========================================================================
     89  #
     90  # Example script to be run off GNUmed hooks.
     91  #
     92  # It can print a message to stdout whenever any hook is invoked.
     93  #
     94  # Copy this file to ~/.gnumed/scripts/hook_script.py and modify as needed.
     95  #
     96  # Known hooks:
     97  #
     98  #       %s
     99  #
    100  #===========================================================================
    101  # SPDX-License-Identifier: GPL-2.0-or-later
    102  __license__ = "GPL v2 or later (details at http://www.gnu.org)"
    103  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
    104
    105
    106  import os
    107
    108
    109  from Gnumed.pycommon import gmWorkerThread
    110  from Gnumed.pycommon import gmTools
    111
    112  from Gnumed.business import gmPerson
    113
    114  from Gnumed.wxpython import gmGuiHelpers
    115  from Gnumed.wxpython import gmPatSearchWidgets
    116
    117
    118  PAT_DIR = os.path.expanduser(os.path.join('~', '.gnumed', 'current_patient'))
    119  README_pat_dir = \"\"\"%s
    120  \"\"\"
    121  CURR_PAT = None
    122
    123  #===========================================================================
    124  def on_startup_after_GUI_init():
    125          # examine external patient sources
    126          gmPatSearchWidgets.get_person_from_external_sources(search_immediately = False, activate_immediately = True)
    127
    128
    129  def request_user_attention():
    130          # signal user to look at GNUmed
    131          gmGuiHelpers.gm_show_info(_('Hey, GNUmed wants you to take a look at it !'))
    132
    133
    134  #def on_app_activated_startup():
    135          #pass
    136
    137
    138  def on_app_activated():
    139          # might want to look at external sources again
    140          gmPatSearchWidgets.get_person_from_external_sources(search_immediately = False, activate_immediately = True)
    141
    142
    143  def _export_patient_demographics():
    144          if CURR_PAT is None:
    145                  return
    146
    147          fname = os.path.join(PAT_DIR, 'patient')
    148          CURR_PAT.export_as_gdt(filename = fname + '.gdt', encoding = 'cp850')
    149          CURR_PAT.export_as_xml_linuxmednews(filename = fname + '.xml')
    150          CURR_PAT.export_as_vcard(filename = fname + '.vcf')
    151          CURR_PAT.export_as_mecard(filename = fname + '.mcf')
    152
    153
    154  #def on_app_deactivated():
    155  def on_post_patient_activation():
    156          # might want to export the active patient into an xDT file
    157          global CURR_PAT
    158          curr_pat = gmPerson.gmCurrentPatient()
    159          if not curr_pat.connected:
    160                  CURR_PAT = None
    161                  return
    162
    163          CURR_PAT = curr_pat
    164          gmWorkerThread.execute_in_worker_thread (
    165                  payload_function = _export_patient_demographics,
    166                  worker_name = 'current_patient_demographics_exporter'
    167          )
    168
    169
    170  #===========================================================================
    171  gmTools.mkdir(PAT_DIR)
    172  gmTools.create_directory_description_file(directory = PAT_DIR, readme = README_pat_dir)
    173
    174  # main entry point
    175  def run_script(hook=None):
    176
    177          if hook is None:
    178                  hook = _('no hook specified, please report bug')
    179                  print('hook_script.py::run_script():', hook)
    180
    181          #print('GNUmed invoked the hook:', hook)
    182
    183          # a few examples:
    184
    185          #if hook == 'startup-after-GUI-init':
    186          #       on_startup_after_GUI_init()
    187
    188          #if hook == 'request_user_attention':
    189          #       on_request_user_attention()
    190
    191          #if hook == 'app_activated_startup':
    192          #       on_app_activated_startup()
    193
    194          #if hook == 'app_activated':
    195          #       on_app_activated()
    196
    197          #if hook == 'app_deactivated':
    198          #       on_app_deactivated()
    199
    200          if hook == 'post_patient_activation':
    201                  on_post_patient_activation()
    202
    203          return
    204  """ % (
    205          '\n#\t'.join(known_hooks),
    206          README_pat_dir
    207  )
    208
    209
    210  # hardcoding path and script name allows us to not
    211  # need configuration for it, the environment can
    212  # always be detected at runtime (workplace etc)
    213  HOOK_SCRIPT_NAME = 'hook_script.py'
    214  HOOK_SCRIPT_DIR = os.path.expanduser(os.path.join('~', '.gnumed', 'scripts'))
    215  HOOK_SCRIPT_FULL_NAME = os.path.join(HOOK_SCRIPT_DIR, HOOK_SCRIPT_NAME)
    216
    217
    218  README_hook_dir = """Directory for scripts called from hooks at client runtime.
    219
    220  Known hooks:
    221
    222          %s
    223
    224  You can use <%s.example> as a script template.
    225  """ % (
    226          '\n\t'.join(known_hooks),
    227          HOOK_SCRIPT_NAME
    228  )
    229
    230
    231  # ========================================================================
    232 -def setup_hook_dir():
    233          _log.debug('known hooks:')
    234          for hook in known_hooks:
    235                  _log.debug(hook)
    236          gmTools.mkdir(HOOK_SCRIPT_DIR)
    237          gmTools.create_directory_description_file(directory = HOOK_SCRIPT_DIR, readme = README_hook_dir)
    238          # create hook script example/template
    239          example_name = os.path.join(HOOK_SCRIPT_DIR, HOOK_SCRIPT_NAME + '.example')
    240          example = open(example_name, mode = 'wt', encoding = 'utf8')
    241          example.write(HOOK_SCRIPT_EXAMPLE)
    242          example.close()
    243          os.chmod(example_name, 384)
    244
    245  # ========================================================================
    246  hook_module = None
    247
    248 -def import_hook_module(reimport=False):
    249
    250          global hook_module
    251          if not reimport:
    252                  if hook_module is not None:
    253                          return True
    254
    255          if not os.access(HOOK_SCRIPT_FULL_NAME, os.F_OK):
    256                  _log.warning('creating default hook script')
    257                  f = io.open(HOOK_SCRIPT_FULL_NAME, mode = 'wt', encoding = 'utf8')
    258                  f.write("""
    259  # known hooks:
    260  #  %s
    261
    262  def run_script(hook=None):
    263          pass
    264  """ % '\n#\t'.join(known_hooks))
    265                  f.close()
    266                  os.chmod(HOOK_SCRIPT_FULL_NAME, 384)
    267
    268          if os.path.islink(HOOK_SCRIPT_FULL_NAME):
    269                  gmDispatcher.send (
    270                          signal = 'statustext',
    271                          msg = _('Script must not be a link: [%s].') % HOOK_SCRIPT_FULL_NAME
    272                  )
    273                  return False
    274
    275          if not os.access(HOOK_SCRIPT_FULL_NAME, os.R_OK):
    276                  gmDispatcher.send (
    277                          signal = 'statustext',
    278                          msg = _('Script must be readable by the calling user: [%s].') % HOOK_SCRIPT_FULL_NAME
    279                  )
    280                  return False
    281
    282          script_stat_val = os.stat(HOOK_SCRIPT_FULL_NAME)
    283          _log.debug('hook script stat(): %s', script_stat_val)
    284          script_perms = stat.S_IMODE(script_stat_val.st_mode)
    285          _log.debug('hook script mode: %s (oktal: %s)', script_perms, oct(script_perms))
    286          if script_perms != 384:                         # octal 0600
    287                  if os.name in ['nt']:
    288                          _log.warning('this platform does not support os.stat() file permission checking')
    289                  else:
    290                          gmDispatcher.send (
    291                                  signal = 'statustext',
    292                                  msg = _('Script must be readable by the calling user only (permissions "0600"): [%s].') % HOOK_SCRIPT_FULL_NAME
    293                          )
    294                          return False
    295
    296          try:
    297                  tmp = gmTools.import_module_from_directory(HOOK_SCRIPT_DIR, HOOK_SCRIPT_NAME)
    298          except Exception:
    299                  _log.exception('cannot import hook script')
    300                  return False
    301
    302          hook_module = tmp
    303  #       if reimport:
    304  #               imp.reload(tmp)                 # this has well-known shortcomings !
    305
    306          _log.info('hook script: %s', HOOK_SCRIPT_FULL_NAME)
    307          return True
    308
    309  # ========================================================================
    310  __current_hook_stack = []
    311
    312 -def run_hook_script(hook=None):
    313          # NOTE: this just *might* be a huge security hole
    314
    315          _log.info('told to pull hook [%s]', hook)
    316
    317          if hook not in known_hooks:
    318                  raise ValueError('run_hook_script(): unknown hook [%s]' % hook)
    319
    320          if not import_hook_module(reimport = False):
    321                  _log.debug('cannot import hook module, not pulling hook')
    322                  return False
    323
    324          if hook in __current_hook_stack:
    325                  _log.error('hook-code cycle detected, aborting')
    326                  _log.error('current hook stack: %s', __current_hook_stack)
    327                  return False
    328
    329          __current_hook_stack.append(hook)
    330
    331          try:
    332                  hook_module.run_script(hook = hook)
    333          except Exception:
    334                  _log.exception('error running hook script for [%s]', hook)
    335                  gmDispatcher.send (
    336                          signal = 'statustext',
    337                          msg = _('Error running hook [%s] script.') % hook,
    338                          beep = True
    339                  )
    340                  if __current_hook_stack[-1] != hook:
    341                          _log.error('hook nesting error detected')
    342                          _log.error('latest hook: expected [%s], found [%s]', hook, __current_hook_stack[-1])
    343                          _log.error('current hook stack: %s', __current_hook_stack)
    344                  else:
    345                          __current_hook_stack.pop()
    346                  return False
    347
    348          if __current_hook_stack[-1] != hook:
    349                  _log.error('hook nesting error detected')
    350                  _log.error('latest hook: expected [%s], found [%s]', hook, __current_hook_stack[-1])
    351                  _log.error('current hook stack: %s', __current_hook_stack)
    352          else:
    353                  __current_hook_stack.pop()
    354
    355          return True
    356
    357  # ========================================================================
    358
    359  setup_hook_dir()
    360
    361  if __name__ == '__main__':
    362
    363          if len(sys.argv) < 2:
    364                  sys.exit()
    365
    366          if sys.argv[1] != 'test':
    367                  sys.exit()
    368
    369          run_hook_script(hook = 'shutdown-post-GUI')
    370          run_hook_script(hook = 'post_patient_activation')
    371          run_hook_script(hook = 'invalid hook')
    372
    373  # ========================================================================
    374