Gnumed.gnumed

Package Gnumed :: Module gnumed |
[frames] | no frames]

# Source Code for Module Gnumed.gnumed

       1  #!/usr/bin/env python3
       2
       3  __doc__ = MANPAGE = """.\\" ========================================================
       4  .\\" SPDX-License-Identifier: GPL-2.0-or-later
       5  .\\" ========================================================
       6
       7  .TH GNUmed 1 "%s" "Manual for GNUmed"
       8
       9  .SH NAME
      10  .B GNUmed
      11  - an electronic MEDICAL record software for GP offices
      12
      13  This is not fully featured yet. Use at your own risk.
      14  You have been warned.
      15
      16  .SH SYNOPSIS
      17  .B gnumed
      18  .RB [--quiet|debug]
      19  .RB [--slave]
      20  .RB [--text-domain=TEXTDOMAIN]
      21  .RB [--log-file=FILE]
      22  .RB [--conf-file=FILE]
      23  .RB [--profile=FILE]
      24  .RB [--lang-gettext=LANGUAGE]
      25  .RB [--tool=TOOL]
      26  .RB [--override-schema-check]
      27  .RB [--skip-update-check]
      28  .RB [--local-import]
      29  .RB [--help]
      30  .RB [--version]
      31  .RB [-V]
      32  .RB [-h|?]
      33
      34
      35  .SH DESCRIPTION
      36  .B GNUmed
      37  is a solution for keeping safe and medically sound electronic
      38  records on a patient's health. It primarily focuses on GP
      39  offices. It is released under the GPL.
      40
      41  GNUmed is written in Python with wxPython/wxWindows. Data is
      42  stored in a PostgreSQL database. Multiple clients can work
      43  with the same database at the same time.
      44
      45  .SH OPTIONS
      46  .PP
      47  .TP
      48  .B \--quiet
      49  Be extra quiet and show only _real_ errors in the log.
      50  .TP
      51  .B \--debug
      52  Pre-set the [debug mode] checkbox in the login dialog
      53  which controls increased verbosity in the log file.
      54
      55  Useful for, well, debugging :-)  Slower, too.
      56  .TP
      57  .B \--slave
      58  Pre-set the [enable remote control] checkbox in the login
      59  dialog to enable the XML-RPC remote control feature.
      60  .TP
      61  .B \--hipaa
      62  Enable HIPAA features with user workflow impact.
      63
      64  Those features which do not affect the workflow of the user
      65  are permanently enabled.
      66  .TP
      67  .B \--log-file=FILE
      68  Use this to change the name of the log file. At startup
      69  GNUmed will tell you the name of the log file in use.
      70  .TP
      71  .B \--conf-file=FILE
      72  Use configuration file FILE instead of searching for it in standard locations.
      73  .TP
      74  .B \--profile=FILE
      75  Activate profiling and write profile data to file FILE.
      76  .TP
      77  .B \--lang-gettext=LANGUAGE
      78  Explicitly set the language to use in gettext translation. The very
      79  same effect can be achieved by setting the environment variable $LANG
      80  from a launcher script.
      81  .TP
      82  .B \--text-domain=TEXTDOMAIN
      83  Set this to change the name of the language file to be loaded.
      84  Note, this does not change the directory the file is searched in,
      85  only the name of the file where messages are loaded from. The
      86  standard textdomain is, of course, "gnumed.mo". You need only
      87  specify the base name of the file without the .mo extension.
      88  .TP
      89  .B \--tool=TOOL
      90  Run the named TOOL instead of a GUI.
      91
      92  Currently implemented tools:
      93
      94          check_enc_epi_xref: Cross-check that foreign keys values in any given row of any table carrying both of fk_episode and fk_encounter do point to episodes and encounters, respectively, of the very same patient.
      95
      96          export_pat_emr_structure: Export the EMR structure (issues and episodes) of a patient into a text file.
      97
      98          check_mimetypes_in_archive: Show mimetypes and related information of all document parts in the archive.
      99
     100          read_all_rows_of_table: Check readability of all rows of a given table.
     101  .TP
     102  .B \--override-schema-check
     103  Continue loading the client even if the database schema
     104  version and the client software version cannot be verified
     105  to be compatible.
     106  .TP
     107  .B \--skip-update-check
     108  Skip checking for client updates. This is useful during
     109  development or when the update check URL is unavailable.
     110  .TP
     111  .B \--local-import
     112  At startup adjust the PYTHONPATH such that the GNUmed client is
     113  run from a local copy of the source tree (say an unpacked tarball
     114  or a GIT repo) rather than from a proper system-wide installation.
     115  .TP
     116  .B \--version, -V
     117  Show version information about the GNUmed client and the
     118  database it needs.
     119  .TP
     120  .B \--help, -h, or -?
     121  Show this help.
     122
     123
     124  .SH CONFIGURATION
     125  .PP
     126  .TP
     127  .B Client startup and shutdown (OS level)
     128
     129  A shell script /usr/bin/gnumed is used to startup the client.
     130  It checks whether the systemwide configuration file
     131
     132          /etc/gnumed/gnumed-client.conf
     133
     134  exists. It then executes the following scripts (in that
     135  order) if found:
     136
     137          /etc/gnumed/gnumed-startup-local.sh
     138
     139          ~/.gnumed/scripts/gnumed-startup-local.sh
     140
     141  When the client terminates it will execute the following
     142  scripts in order if they exist:
     143
     144          /etc/gnumed/gnumed-shutdown-local.sh
     145
     146          ~/.gnumed/scripts/gnumed-shutdown-local.sh
     147
     148  .PP
     149  .TP
     150  .B wxPython client startup
     151
     152  The gnumed.py script checks for INI style configuration files
     153  and fails if it does not find any. The files are searched for
     154  in the following order and extend/overwrite each others
     155  options:
     156
     157          in the current working directory (cwd)
     158
     159                  ./gumed.conf
     160
     161          in the systemwide configuration directory
     162
     163                  /etc/gnumed/gnumed-client.conf
     164
     165          in the home directory
     166
     167                  ~/.gnumed/gnumed.conf
     168
     169          in a local git tree or unpacked tarball
     170
     171                  .../gnumed/client/gnumed.conf
     172
     173          explicitly given by CLI option
     174
     175                  --conf-file=<CONF FILE>
     176
     177  .PP
     178  .TP
     179  .B client/system interfacing
     180
     181  .B ~/.gnumed/gnumed-xsanerc.conf
     182
     183  (requires XSane > v0.992)
     184
     185  When GNUmed invokes XSane for scanning it passes along this file (via =--xsane-rc=). This way a custom XSane configuration can be used with GNUmed. If the file doesn't exist it will be created from ~/.sane/xsane/xsanerc on the first call to XSane.
     186
     187  When you configure XSane after calling it from GNUmed your changes will be stored in the GNUmed-specific XSane configuration file and will not affect your usual XSane settings.
     188
     189  .B mime_type2file_extension.conf
     190
     191  (searched for in ~/.gnumed/ and /etc/gnumed/, in that order)
     192
     193  GNUmed will use these files to map mime types to file extensions if need be.
     194
     195  The file must contain a group [extensions] under which there can be one option per mime type specifying the extension to use on files of said type. Set the value to the raw extension only, omitting the ".", like so:
     196
     197  .nf
     198  [extensions]
     199  image/x-bmp = bmp
     200
     201
     202  .SH EXIT STATUS
     203  .TP
     204   > 0: some error occurred while the GUI client was run
     205  .TP
     206     0: normal termination of the client
     207  .TP
     208   < 0: some error occurred while trying to run a console tool
     209  .TP
     210    -1: an unknown console tool was requested
     211  .TP
     212  < -1: an error occurred while a console tool was run
     213  .TP
     214  -999: hard abort of the client
     215
     216
     217  .SH ENVIRONMENT
     218  .TP
     219  .B LANG, LC_MESSAGES, etc.
     220  See gettext(1) for how the various locale related environment variables work.
     221
     222
     223  .SH OTHER FILES AND DIRECTORIES
     224  .PP
     225  .TP
     226  .B ~/.gnumed/gnumed.log
     227  The default log file.
     228  .TP
     229  .B gnumed-client.tmpfiles.d.conf
     230  Integration with systemd-tmpfiles(8).
     231  .TP
     232  .B gnumed-completion.bash
     233  Integration with BASH completions.
     234
     235
     236  .SH SEE ALSO
     237  .PP
     238  .TP
     239  .B https://www.gnumed.[de|org]
     240  Online documenation.
     241  .TP
     242  .B http://savannah.gnu.org/projects/gnumed
     243  Mailing list home
     244  .TP
     245  .B https://github.org/ncqgm/gnumed
     246  Source code repository (Git)
     247  .TP
     248  .B /usr/share/doc/gnumed/
     249  Local documentation
     250  .TP
     251  .B man -k gm-*
     252  List man pages on gm-* commands.
     253  .TP
     254  .B gettext(1)
     255
     256
     257  .SH BUGS
     258
     259  A lot of functionality is still missing. To make up for
     260  that, there's bugs here and there for you to report :-)
     261
     262  Use at your own risk. You have been warned. Take proper backups !
     263  """
     264
     265  #==========================================================
     266  # SPDX-License-Identifier: GPL-2.0-or-later
     267  __author__ = "H. Herb <hherb@gnumed.net>, K. Hilbert <Karsten.Hilbert@gmx.net>, I. Haywood <i.haywood@ugrad.unimelb.edu.au>"
     268  __license__ = "GPL v2 or later (details at http://www.gnu.org)"
     269
     270
     271  # standard library
     272  import sys
     273  import os
     274  import platform
     275  import tempfile
     276  import faulthandler
     277  import random
     278  import logging
     279  import datetime
     280  import signal
     281  import os.path
     282  import shutil
     283  import stat
     284  import re as regex
     285
     286
     287  # do not run as module
     288  if __name__ != "__main__":
     289          print("GNUmed startup: This is not intended to be imported as a module !")
     290          print("-----------------------------------------------------------------")
     291          sys.exit(1)
     292
     293
     294  # do not run as root
     295  if os.name in ['posix'] and os.geteuid() == 0:
     296          print("""
     297  GNUmed startup: GNUmed should not be run as root.
     298  -------------------------------------------------
     299
     300  Running GNUmed as <root> can potentially put all
     301  your medical data at risk. It is strongly advised
     302  against. Please run GNUmed as a non-root user.
     303  """)
     304          sys.exit(1)
     305
     306  #----------------------------------------------------------
     307  #current_client_version = '1.8.0rc3'
     308  #current_client_branch = '1.8'
     309  current_client_version = 'head'
     310  current_client_branch = 'master'
     311
     312
     313  _log = None
     314  _pre_log_buffer = []
     315  _cfg = None
     316  _old_sig_term = None
     317  _known_short_options = 'h?V'
     318  _known_long_options = [
     319          'debug',
     320          'slave',
     321          'skip-update-check',
     322          'profile=',
     323          'text-domain=',
     324          'log-file=',
     325          'conf-file=',
     326          'lang-gettext=',
     327          'override-schema-check',
     328          'local-import',
     329          'help',
     330          'version',
     331          'hipaa',
     332          'wxp=',
     333          'tool=',
     334          'tui'
     335  ]
     336
     337  _known_tools = [
     338          'check_enc_epi_xref',
     339          'export_pat_emr_structure',
     340          'check_mimetypes_in_archive',
     341          'read_all_rows_of_table',
     342          'fingerprint_db',
     343          'generate_man_page'
     344  ]
     345
     346
     347  import_error_sermon = """
     348  GNUmed startup: Cannot load GNUmed Python modules !
     349  ---------------------------------------------------
     350  CRITICAL ERROR: Program halted.
     351
     352  Please make sure you have:
     353
     354   1) the required third-party Python modules installed
     355   2) the GNUmed Python modules linked or installed into site-packages/
     356      (if you do not run from a CVS tree the installer should have taken care of that)
     357   3) your PYTHONPATH environment variable set up correctly
     358
     359  <sys.path> is currently set to:
     360
     361   %s
     362
     363  If you are running from a copy of the CVS tree make sure you
     364  did run gnumed/check-prerequisites.sh with good results.
     365
     366  If you still encounter errors after checking the above
     367  requirements please ask on the mailing list.
     368  """
     369
     370
     371  missing_cli_config_file = """
     372  GNUmed startup: Missing configuration file.
     373  -------------------------------------------
     374
     375  You explicitly specified a configuration file
     376  on the command line:
     377
     378          --conf-file=%s
     379
     380  The file does not exist, however.
     381  """
     382
     383
     384  no_config_files = """
     385  GNUmed startup: Missing configuration files.
     386  --------------------------------------------
     387
     388  None of the below candidate configuration
     389  files could be found:
     390
     391   %s
     392
     393  Cannot run GNUmed without any of them.
     394  """
     395
     396  #==========================================================
     397  # convenience functions
     398  #----------------------------------------------------------
     399 -def _symlink_windows(source, link_name):
     400          import ctypes
     401          csl = ctypes.windll.kernel32.CreateSymbolicLinkW
     402          csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
     403          csl.restype = ctypes.c_ubyte
     404          if os.path.isdir(source):
     405                  flags = 1
     406          else:
     407                  flags = 0
     408          ret_code = csl(link_name, source.replace('/', '\\'), flags)
     409          if ret_code == 0:
     410                  raise ctypes.WinError()
     411          return ret_code
     412
     413  #==========================================================
     414  # startup helpers
     415  #----------------------------------------------------------
     416 -def setup_fault_handler(target=None):
     417          if target is None:
     418                  faulthandler.enable()
     419                  _pre_log_buffer.append('<faulthandler> enabled, target = [console]: %s' % faulthandler)
     420                  return
     421          _pre_log_buffer.append('<faulthandler> enabled, target = [%s]: %s' % (target, faulthandler))
     422          faulthandler.enable(file = target)
     423
     424  #==========================================================
     425 -def setup_console_encoding():
     426          print_lines = []
     427          try:
     428                  sys.stdout.reconfigure(errors = 'surrogateescape')
     429                  sys.stderr.reconfigure(errors = 'surrogateescape')
     430                  _pre_log_buffer.append('stdout/stderr reconfigured to use <surrogateescape> for encoding errors')
     431                  return
     432          except AttributeError:
     433                  line = 'cannot reconfigure sys.stdout/stderr to use <errors="surrogateescape"> (needs Python 3.7+)'
     434                  _pre_log_buffer.append(line)
     435                  print_lines.append(line)
     436          try:
     437                  _pre_log_buffer.append('sys.stdout/stderr default to "${PYTHONIOENCODING}=%s"' % os.environ['PYTHONIOENCODING'])
     438                  return
     439          except KeyError:
     440                  lines = [
     441                          '${PYTHONIOENCODING} is not set up, use <PYTHONIOENCODING=utf-8:surrogateescape> in the shell (for Python < 3.7)',
     442                          'console encoding errors may occur'
     443                  ]
     444                  for line in lines:
     445                          print_lines.append(line)
     446                          _pre_log_buffer.append(line)
     447                  for line in print_lines:
     448                          print('GNUmed startup:', line)
     449
     450  #==========================================================
     451 -def setup_python_path():
     452
     453          if not '--local-import' in sys.argv:
     454                  _pre_log_buffer.append('running against systemwide install')
     455                  return
     456
     457          local_python_import_dir = os.path.dirname (
     458                  os.path.abspath(os.path.join(sys.argv[0], '..'))
     459          )
     460          print("Running from local source tree (%s) ..." % local_python_import_dir)
     461          _pre_log_buffer.append("running from local source tree: %s" % local_python_import_dir)
     462
     463          # does the path exist at all, physically ?
     464          # (*broken* links are reported as False)
     465          link_name = os.path.join(local_python_import_dir, 'Gnumed')
     466          if os.path.exists(link_name):
     467                  _pre_log_buffer.append('local module import dir symlink exists: %s' % link_name)
     468          else:
     469                  real_dir = os.path.join(local_python_import_dir, 'client')
     470                  print('Creating local module import symlink ...')
     471                  print(' real dir:', real_dir)
     472                  print('     link:', link_name)
     473                  try:
     474                          os.symlink(real_dir, link_name)
     475                  except AttributeError:
     476                          _pre_log_buffer.append('Windows does not have os.symlink(), resorting to ctypes')
     477                          result = _symlink_windows(real_dir, link_name)
     478                          _pre_log_buffer.append('ctypes.windll.kernel32.CreateSymbolicLinkW() exit code: %s', result)
     479                  _pre_log_buffer.append('created local module import dir symlink: link [%s] => dir [%s]' % (link_name, real_dir))
     480
     481          sys.path.insert(0, local_python_import_dir)
     482          _pre_log_buffer.append('sys.path with local module import base dir prepended: %s' % sys.path)
     483
     484  #==========================================================
     485 -def setup_local_repo_path():
     486
     487          local_repo_path = os.path.expanduser(os.path.join (
     488                  '~',
     489                  '.gnumed',
     490                  'local_code',
     491                  str(current_client_branch)
     492          ))
     493          local_wxGladeWidgets_path = os.path.join(local_repo_path, 'Gnumed', 'wxGladeWidgets')
     494
     495          if not os.path.exists(local_wxGladeWidgets_path):
     496                  _log.debug('[%s] not found', local_wxGladeWidgets_path)
     497                  _log.info('local wxGlade widgets repository not available')
     498                  return
     499
     500          _log.info('local wxGlade widgets repository found:')
     501          _log.info(local_wxGladeWidgets_path)
     502
     503          if not os.access(local_wxGladeWidgets_path, os.R_OK):
     504                  _log.error('invalid repo: no read access')
     505                  return
     506
     507          all_entries = os.listdir(os.path.join(local_repo_path, 'Gnumed'))
     508          _log.debug('repo base contains: %s', all_entries)
     509          all_entries.remove('wxGladeWidgets')
     510          try:
     511                  all_entries.remove('__init__.py')
     512          except ValueError:
     513                  _log.error('invalid repo: lacking __init__.py')
     514                  return
     515          try:
     516                  all_entries.remove('__init__.pyc')
     517          except ValueError:
     518                  pass
     519
     520          if len(all_entries) > 0:
     521                  _log.error('insecure repo: additional files or directories found')
     522                  return
     523
     524          # repo must be 0700 (rwx------)
     525          stat_val = os.stat(local_wxGladeWidgets_path)
     526          _log.debug('repo stat(): %s', stat_val)
     527          perms = stat.S_IMODE(stat_val.st_mode)
     528          _log.debug('repo permissions: %s (octal: %s)', perms, oct(perms))
     529          if perms != 448:                                # octal 0700
     530                  if os.name in ['nt']:
     531                          _log.warning('this platform does not support os.stat() permission checking')
     532                  else:
     533                          _log.error('insecure repo: permissions not 0600')
     534                          return
     535
     536          print("Activating local wxGlade widgets repository (%s) ..." % local_wxGladeWidgets_path)
     537          sys.path.insert(0, local_repo_path)
     538          _log.debug('sys.path with repo:')
     539          _log.debug(sys.path)
     540
     541  #==========================================================
     542 -def setup_logging():
     543          try:
     544                  from Gnumed.pycommon import gmLog2 as _gmLog2
     545          except ImportError:
     546                  print(import_error_sermon % '\n '.join(sys.path))
     547                  sys.exit(1)
     548
     549          print("Log file:", _gmLog2._logfile.name)
     550          setup_fault_handler(target = _gmLog2._logfile)
     551
     552          global gmLog2
     553          gmLog2 = _gmLog2
     554
     555          global _log
     556          _log = logging.getLogger('gm.launcher')
     557
     558  #==========================================================
     559 -def log_startup_info():
     560          global _pre_log_buffer
     561          if len(_pre_log_buffer) > 0:
     562                  _log.info('early startup log buffer:')
     563          for line in _pre_log_buffer:
     564                  _log.info(' ' + line)
     565          del _pre_log_buffer
     566          _log.info('GNUmed client version [%s] on branch [%s]', current_client_version, current_client_branch)
     567          _log.info('Platform: %s', platform.uname())
     568          _log.info(('Python %s on %s (%s)' % (sys.version, sys.platform, os.name)).replace('\n', '<\\n>'))
     569          try:
     570                  import lsb_release
     571                  _log.info('lsb_release: %s', lsb_release.get_distro_information())
     572          except ImportError:
     573                  pass
     574          _log.info('module <sys> info:')
     575          attrs2skip = ['__doc__', 'copyright', '__name__', '__spec__']
     576          for attr_name in dir(sys):
     577                  if attr_name in attrs2skip:
     578                          continue
     579                  if attr_name.startswith('set'):
     580                          continue
     581                  attr = getattr(sys, attr_name)
     582                  if not attr_name.startswith('get'):
     583                          _log.info('%s: %s', attr_name.rjust(30), attr)
     584                          continue
     585                  if callable(attr):
     586                          try:
     587                                  _log.info('%s: %s', attr_name.rjust(30), attr())
     588                          except Exception:
     589                                  _log.exception('%s: <cannot log>', attr_name.rjust(30))
     590                          continue
     591          _log.info('module <platform> info:')
     592          attrs2skip = ['__doc__', '__copyright__', '__name__', '__spec__', '__cached__', '__builtins__']
     593          for attr_name in dir(platform):
     594                  if attr_name in attrs2skip:
     595                          continue
     596                  if attr_name.startswith('set'):
     597                          continue
     598                  attr = getattr(platform, attr_name)
     599                  if callable(attr):
     600                          if attr_name.startswith('_'):
     601                                  _log.info('%s: %s', attr_name.rjust(30), attr)
     602                                  continue
     603                          try:
     604                                  _log.info('%s: %s', attr_name.rjust(30), attr())
     605                          except Exception:
     606                                  _log.exception('%s: <cannot log>', attr_name.rjust(30))
     607                          continue
     608                  _log.info('%s: %s', attr_name.rjust(30), attr)
     609                  continue
     610          _log.info('module <os> info:')
     611          for n in os.confstr_names:
     612                  _log.info('%s: %s', ('confstr[%s]' % n).rjust(40), os.confstr(n))
     613          for n in os.sysconf_names:
     614                  try:
     615                          _log.info('%s: %s', ('sysconf[%s]' % n).rjust(40), os.sysconf(n))
     616                  except Exception:
     617                          _log.exception('%s: <invalid> ??', ('sysconf[%s]' % n).rjust(30))
     618          os_attrs = ['name', 'ctermid', 'getcwd', 'get_exec_path', 'getegid', 'geteuid', 'getgid', 'getgroups', 'getlogin', 'getpgrp', 'getpid', 'getppid', 'getresuid', 'getresgid', 'getuid', 'supports_bytes_environ', 'uname', 'get_terminal_size', 'pathconf_names', 'times', 'cpu_count', 'curdir', 'pardir', 'sep', 'altsep', 'extsep', 'pathsep', 'defpath', 'linesep', 'devnull']
     619          for attr_name in os_attrs:
     620                  attr = getattr(os, attr_name)
     621                  if callable(attr):
     622                          _log.info('%s: %s', attr_name.rjust(40), attr())
     623                          continue
     624                  _log.info('%s: %s', attr_name.rjust(40), attr)
     625          _log.info('process environment:')
     626          for key, val in os.environ.items():
     627                  _log.info(' %s: %s' % (('${%s}' % key).rjust(40),       val))
     628          import sysconfig
     629          _log.info('module <sysconfig> info:')
     630          _log.info(' platform [%s] -- python version [%s]', sysconfig.get_platform(), sysconfig.get_python_version())
     631          _log.info(' sysconfig.get_paths():')
     632          paths = sysconfig.get_paths()
     633          for path in paths:
     634                  _log.info('%s: %s', path.rjust(40), paths[path])
     635          _log.info(' sysconfig.get_config_vars():')
     636          conf_vars = sysconfig.get_config_vars()
     637          for var in conf_vars:
     638                  _log.info('%s: %s', var.rjust(45), conf_vars[var])
     639
     640  #==========================================================
     641 -def setup_console_exception_handler():
     642          from Gnumed.pycommon.gmTools import handle_uncaught_exception_console
     643
     644          sys.excepthook = handle_uncaught_exception_console
     645
     646  #==========================================================
     647 -def setup_cli():
     648          from Gnumed.pycommon import gmCfg2
     649
     650          global _cfg
     651          _cfg = gmCfg2.gmCfgData()
     652          _cfg.add_cli (
     653                  short_options = _known_short_options,
     654                  long_options = _known_long_options
     655          )
     656
     657          val = _cfg.get(option = '--debug', source_order = [('cli', 'return')])
     658          if val is None:
     659                  val = False
     660          _cfg.set_option (
     661                  option = 'debug',
     662                  value = val
     663          )
     664
     665          val = _cfg.get(option = '--slave', source_order = [('cli', 'return')])
     666          if val is None:
     667                  val = False
     668          _cfg.set_option (
     669                  option = 'slave',
     670                  value = val
     671          )
     672
     673          val = _cfg.get(option = '--skip-update-check', source_order = [('cli', 'return')])
     674          if val is None:
     675                  val = False
     676          _cfg.set_option (
     677                  option = 'skip-update-check',
     678                  value = val
     679          )
     680
     681          val = _cfg.get(option = '--hipaa', source_order = [('cli', 'return')])
     682          if val is None:
     683                  val = False
     684          _cfg.set_option (
     685                  option = 'hipaa',
     686                  value = val
     687          )
     688
     689          val = _cfg.get(option = '--local-import', source_order = [('cli', 'return')])
     690          if val is None:
     691                  val = False
     692          _cfg.set_option (
     693                  option = 'local-import',
     694                  value = val
     695          )
     696
     697          _cfg.set_option (
     698                  option = 'client_version',
     699                  value = current_client_version
     700          )
     701
     702          _cfg.set_option (
     703                  option = 'client_branch',
     704                  value = current_client_branch
     705          )
     706
     707  #==========================================================
     708 -def handle_sig_term(signum, frame):
     709          _log.critical('SIGTERM (SIG%s) received, shutting down ...' % signum)
     710          gmLog2.flush()
     711          print('GNUmed: SIGTERM (SIG%s) received, shutting down ...' % signum)
     712          if frame is not None:
     713                  print('%s::%s@%s' % (frame.f_code.co_filename, frame.f_code.co_name, frame.f_lineno))
     714
     715          # FIXME: need to do something useful here
     716
     717          if _old_sig_term in [None, signal.SIG_IGN]:
     718                  sys.exit(1)
     719          else:
     720                  _old_sig_term(signum, frame)
     721
     722  #----------------------------------------------------------
     723 -def setup_signal_handlers():
     724          global _old_sig_term
     725          old_sig_term = signal.signal(signal.SIGTERM, handle_sig_term)
     726
     727  #==========================================================
     728 -def setup_locale():
     729          gmI18N.activate_locale()
     730
     731          td = _cfg.get(option = '--text-domain', source_order = [('cli', 'return')])
     732          l =  _cfg.get(option = '--lang-gettext', source_order = [('cli', 'return')])
     733          gmI18N.install_domain(domain = td, language = l, prefer_local_catalog = _cfg.get(option = 'local-import'))
     734
     735  #       # make sure we re-get the default encoding
     736  #       # in case it changed
     737  #       gmLog2.set_string_encoding()
     738
     739  #==========================================================
     740 -def handle_help_request():
     741          src = [('cli', 'return')]
     742
     743          help_requested = (
     744                  _cfg.get(option = '--help', source_order = src) or
     745                  _cfg.get(option = '-h', source_order = src) or
     746                  _cfg.get(option = '-?', source_order = src)
     747          )
     748
     749          if help_requested:
     750                  input('\nHit <ENTER> to display commandline help\n')
     751                  if platform.system() == 'Windows':
     752                          for line in MANPAGE.split('\n'):
     753                                  print(regex.sub('^\.\w+\s*', '', line, count = 1))
     754                          sys.exit(0)
     755
     756                  handle, man_page_fname = tempfile.mkstemp(text = True, suffix = '.1')
     757                  man_page_file = open(man_page_fname, mode = 'wt', encoding = 'utf8')
     758                  man_page_file.write(MANPAGE % datetime.date.today().strftime('%x'))
     759                  man_page_file.close()
     760                  os.system('man %s' % man_page_fname)
     761                  sys.exit(0)
     762
     763  #==========================================================
     764 -def handle_version_request():
     765          gmTools._client_version = current_client_version
     766
     767          src = [('cli', 'return')]
     768
     769          version_requested = (
     770                  _cfg.get(option = '--version', source_order = src) or
     771                  _cfg.get(option = '-V', source_order = src)
     772          )
     773
     774          if version_requested:
     775
     776                  from Gnumed.pycommon.gmPG2 import map_client_branch2required_db_version, known_schema_hashes
     777
     778                  print('GNUmed version information')
     779                  print('--------------------------')
     780                  print('client     : %s on branch [%s]' % (current_client_version, current_client_branch))
     781                  print('database   : %s' % map_client_branch2required_db_version[current_client_branch])
     782                  print('schema hash: %s' % known_schema_hashes[map_client_branch2required_db_version[current_client_branch]])
     783                  sys.exit(0)
     784
     785  #==========================================================
     786 -def setup_paths_and_files():
     787          """Create needed paths in user home directory."""
     788
     789          gmd_dir = os.path.expanduser(os.path.join('~', 'gnumed'))
     790          dot_gmd_dir = os.path.expanduser(os.path.join('~', '.gnumed'))
     791          readme = """GNUmed Electronic Medical Record
     792
     793          %s/
     794
     795  This directory should only ever contain files which the
     796  user will come into direct contact with while using the
     797  application (say, by selecting a file from the file system,
     798  as when selecting document parts from files). You can create
     799  subdirectories here as you see fit for the purpose.
     800
     801  This directory will also serve as the default directory when
     802  GNUmed asks the user to select a directory for storing a
     803  file.
     804
     805  Any files which are NOT intended for direct user interaction
     806  but must be configured to live at a known location (say,
     807  inter-application data exchange files) should be put under
     808  the hidden directory:
     809          "%s/"
     810  """ % (gmd_dir, dot_gmd_dir)
     811          gmTools.mkdir(gmd_dir)
     812          gmTools.create_directory_description_file(directory = gmd_dir, readme = readme)
     813          # remove old README
     814          gmTools.remove_file(os.path.join(gmd_dir, '00_README'))
     815
     816          gmTools.mkdir(os.path.expanduser(os.path.join(dot_gmd_dir, 'spellcheck')))
     817          err_dir = os.path.expanduser(os.path.join(dot_gmd_dir, 'error_logs'))
     818          gmTools.mkdir(err_dir)
     819          readme = """This directory should be used for files not intended for user
     820  interaction at the file system level (file selection dialogs,
     821  file browsers) such as inter-application data exchange files
     822  which need to live at a known location."""
     823          gmTools.create_directory_description_file(directory = dot_gmd_dir, readme = readme)
     824          gmTools.create_directory_description_file (
     825                  directory = err_dir,
     826                  readme = 'Whenever an unhandled exception is detected a copy of the log file is placed here.\n\nThis directory is subject to systemd-tmpfiles cleaning.'
     827          )
     828
     829          # wxPython not available yet
     830          paths = gmTools.gmPaths(app_name = 'gnumed')
     831          print("Temp dir:", paths.tmp_dir)
     832          # ensure there's a user-level config file
     833          open(os.path.expanduser(os.path.join('~', '.gnumed', 'gnumed.conf')), mode = 'a+t').close()
     834          # symlink log file into temporary directory for easier debugging (everything in one place)
     835          logfile_link = os.path.join(paths.tmp_dir, 'zzz-gnumed.log')
     836          gmTools.mklink (gmLog2._logfile.name, logfile_link, overwrite = False)
     837
     838  #==========================================================
     839 -def setup_date_time():
     840          gmDateTime.init()
     841
     842  #==========================================================
     843 -def setup_cfg():
     844          """Detect and setup access to GNUmed config file.
     845
     846          Parts of this will have limited value due to
     847          wxPython not yet being available.
     848          """
     849
     850          enc = gmI18N.get_encoding()
     851          paths = gmTools.gmPaths(app_name = 'gnumed')
     852
     853          candidates = [
     854                  # the current working dir
     855                  ['workbase', os.path.join(paths.working_dir, 'gnumed.conf')],
     856                  # /etc/gnumed/
     857                  ['system', os.path.join(paths.system_config_dir, 'gnumed-client.conf')],
     858                  # ~/.gnumed/
     859                  ['user', os.path.join(paths.user_config_dir, 'gnumed.conf')],
     860                  # CVS/tgz tree .../gnumed/client/ (IOW a local installation)
     861                  ['local', os.path.join(paths.local_base_dir, 'gnumed.conf')]
     862          ]
     863          # --conf-file=
     864          explicit_fname = _cfg.get(option = '--conf-file', source_order = [('cli', 'return')])
     865          if explicit_fname is None:
     866                  candidates.append(['explicit', None])
     867          else:
     868                  candidates.append(['explicit', explicit_fname])
     869
     870          for candidate in candidates:
     871                  _cfg.add_file_source (
     872                          source = candidate[0],
     873                          file = candidate[1],
     874                          encoding = enc
     875                  )
     876
     877          # --conf-file given but does not actually exist ?
     878          if explicit_fname is not None:
     879                  if _cfg.source_files['explicit'] is None:
     880                          _log.error('--conf-file argument does not exist')
     881                          print(missing_cli_config_file % explicit_fname)
     882                          sys.exit(1)
     883
     884          # any config file found at all ?
     885          found_any_file = False
     886          for f in _cfg.source_files.values():
     887                  if f is not None:
     888                          found_any_file = True
     889                          break
     890          if not found_any_file:
     891                  _log.error('no config file found at all')
     892                  print(no_config_files % '\n '.join(candidates))
     893                  sys.exit(1)
     894
     895          # mime type handling sources
     896          fname = 'mime_type2file_extension.conf'
     897          _cfg.add_file_source (
     898                  source = 'user-mime',
     899                  file = os.path.join(paths.user_config_dir, fname),
     900                  encoding = enc
     901          )
     902          _cfg.add_file_source (
     903                  source = 'system-mime',
     904                  file = os.path.join(paths.system_config_dir, fname),
     905                  encoding = enc
     906          )
     907
     908  #==========================================================
     909 -def setup_backend_environment():
     910
     911          db_version = gmPG2.map_client_branch2required_db_version[current_client_branch]
     912          _log.info('client expects database version [%s]', db_version)
     913          _cfg.set_option (
     914                  option = 'database_version',
     915                  value = db_version
     916          )
     917
     918          # set up database connection timezone
     919          timezone = _cfg.get (
     920                  group = 'backend',
     921                  option = 'client timezone',
     922                  source_order = [
     923                          ('explicit', 'return'),
     924                          ('workbase', 'return'),
     925                          ('local', 'return'),
     926                          ('user', 'return'),
     927                          ('system', 'return')
     928                  ]
     929          )
     930  #       if timezone is not None:
     931  #               gmPG2.set_default_client_timezone(timezone)
     932
     933  #==========================================================
     934 -def run_ui():
     935          gmHooks.run_hook_script(hook = 'startup-before-GUI')
     936          _use_tui = _cfg.get(option = '--tui', source_order = [('cli', 'return')])
     937          if _use_tui:
     938                  return run_tui()
     939
     940          run_gui()
     941          gmHooks.run_hook_script(hook = 'shutdown-post-GUI')
     942          return 0
     943
     944  #==========================================================
     945 -def run_tui():
     946          try:
     947                  import urwid
     948          except ModuleNotFoundError:
     949                  _log.exception('cannot import <urwid>')
     950                  return 1
     951
     952          from Gnumed.urwid import gmTuiMain
     953          gmTuiMain.main()
     954          return 0
     955
     956  #==========================================================
     957 -def run_gui():
     958
     959          from Gnumed.wxpython import gmGuiMain
     960          profile_file = _cfg.get(option = '--profile', source_order = [('cli', 'return')])
     961          if profile_file is None:
     962                  gmGuiMain.main()
     963          else:
     964                  _log.info('writing profiling data into %s', profile_file)
     965                  import profile
     966                  profile.run('gmGuiMain.main()', profile_file)
     967
     968          return 0
     969
     970  #==========================================================
     971 -def run_tool():
     972          """Run a console tool.
     973
     974          Exit codes as per man page:
     975                     0: normal termination of the client
     976                   < 0: some error occurred while trying to run a console tool
     977                            -1: an unknown console tool was requested
     978                          < -1: an error occurred while a console tool was run
     979                  -999: hard abort of the client
     980
     981          One of these needs to be returned from this function (and,
     982          by extension from the tool having been run, if any).
     983          """
     984          tool = _cfg.get(option = '--tool', source_order = [('cli', 'return')])
     985          if tool is None:
     986                  # not running a tool
     987                  return None
     988
     989          if tool not in _known_tools:
     990                  _log.error('unknown tool requested: %s', tool)
     991                  print('GNUmed startup: Unknown tool [%s] requested.' % tool)
     992                  print('GNUmed startup: Known tools: %s' % _known_tools)
     993                  return -1
     994
     995          print('')
     996          print('==============================================')
     997          print('Running tool: %s' % tool)
     998          print('----------------------------------------------')
     999          print('')
    1000
    1001          if tool == 'generate_man_page':
    1002                  man_page_fname = os.path.abspath(os.path.join('.', 'gnumed.1'))
    1003                  man_page_file = open(man_page_fname, mode = 'wt', encoding = 'utf8')
    1004                  man_page_file.write(MANPAGE % datetime.date.today().strftime('%x'))
    1005                  man_page_file.close()
    1006                  print('MAN page saved as:', man_page_fname)
    1007                  return 0
    1008
    1009          login, creds = gmPG2.request_login_params()
    1010          pool = gmConnectionPool.gmConnectionPool()
    1011          pool.credentials = creds
    1012          print('')
    1013
    1014          if tool == 'read_all_rows_of_table':
    1015                  result = gmPG2.read_all_rows_of_table()
    1016                  if result in [None, True]:
    1017                          print('Success.')
    1018                          return 0
    1019                  print('Failed. Check the log for details.')
    1020                  return -2
    1021
    1022          if tool == 'check_mimetypes_in_archive':
    1023                  from Gnumed.business import gmDocuments
    1024                  return gmDocuments.check_mimetypes_in_archive()
    1025
    1026          if tool == 'check_enc_epi_xref':
    1027                  from Gnumed.business import gmEMRStructItems
    1028                  return gmEMRStructItems.check_fk_encounter_fk_episode_x_ref()
    1029
    1030          if tool == 'fingerprint_db':
    1031                  fname = 'db-fingerprint.txt'
    1032                  result = gmPG2.get_db_fingerprint(fname = fname, with_dump = True)
    1033                  if result == fname:
    1034                          print('Success: %s' % fname)
    1035                          return 0
    1036                  print('Failed. Check the log for details.')
    1037                  return -2
    1038
    1039          if tool == 'export_pat_emr_structure':
    1040                  # setup praxis
    1041                  from Gnumed.business import gmPraxis
    1042                  praxis = gmPraxis.gmCurrentPraxisBranch(branch = gmPraxis.get_praxis_branches()[0])
    1043                  # get patient
    1044                  from Gnumed.business import gmPersonSearch
    1045                  pat = gmPersonSearch.ask_for_patient()
    1046                  # setup exporters
    1047                  from Gnumed.business import gmEMRStructItems
    1048                  from Gnumed.exporters import gmTimelineExporter
    1049                  from Gnumed.exporters import gmPatientExporter
    1050                  while pat is not None:
    1051                          print('patient:', pat['description_gender'])
    1052                          # as EMR structure
    1053                          fname = os.path.expanduser('~/gnumed/gm-emr_structure-%s.txt' % pat.subdir_name)
    1054                          print('EMR structure:', gmEMRStructItems.export_emr_structure(patient = pat, filename = fname))
    1055                          # as timeline
    1056                          fname = os.path.expanduser('~/gnumed/gm-emr-%s.timeline' % pat.subdir_name)
    1057                          try:
    1058                                  print('EMR timeline:', gmTimelineExporter.create_timeline_file (
    1059                                          patient = pat,
    1060                                          filename = fname,
    1061                                          include_documents = True,
    1062                                          include_vaccinations = True,
    1063                                          include_encounters = True
    1064                                  ))
    1065                          finally:
    1066                                  pass
    1067                          # as journal by encounter
    1068                          exporter = gmPatientExporter.cEMRJournalExporter()
    1069                          fname = os.path.expanduser('~/gnumed/gm-emr-journal_by_encounter-%s.txt' % pat.subdir_name)
    1070                          print('EMR journal (by encounter):', exporter.save_to_file_by_encounter(patient = pat, filename = fname))
    1071                          # as journal by mod time
    1072                          fname = os.path.expanduser('~/gnumed/gm-emr-journal_by_mod_time-%s.txt' % pat.subdir_name)
    1073                          print('EMR journal (by mod time):', exporter.save_to_file_by_mod_time(patient = pat, filename = fname))
    1074                          # as statistical summary
    1075                          fname = os.path.expanduser('~/gnumed/gm-emr-statistics-%s.txt' % pat.subdir_name)
    1076                          output_file = open(fname, mode = 'wt', encoding = 'utf8', errors = 'replace')
    1077                          emr = pat.emr
    1078                          output_file.write(emr.format_statistics())
    1079                          output_file.close()
    1080                          print('EMR statistics:', fname)
    1081                          # as text file
    1082                          exporter = gmPatientExporter.cEmrExport(patient = pat)
    1083                          fname = os.path.expanduser('~/gnumed/gm-emr-text_export-%s.txt' % pat.subdir_name)
    1084                          output_file = open(fname, mode = 'wt', encoding = 'utf8', errors = 'replace')
    1085                          exporter.set_output_file(output_file)
    1086                          exporter.dump_constraints()
    1087                          exporter.dump_demographic_record(True)
    1088                          exporter.dump_clinical_record()
    1089                          exporter.dump_med_docs()
    1090                          output_file.close()
    1091                          print('EMR text file:', fname)
    1092                          # another patient ?
    1093                          pat = gmPersonSearch.ask_for_patient()
    1094                  return 0
    1095
    1096          # tool export_patient_as (vcf, gdt, ...)
    1097          #if tool == 'export_pat_demographics':
    1098
    1099          # should not happen (because checked against _known_tools)
    1100          return -1
    1101
    1102  #==========================================================
    1103  # shutdown helpers
    1104  #----------------------------------------------------------
    1105 -def shutdown_backend():
    1106          gmPG2.shutdown()
    1107
    1108  #==========================================================
    1109 -def shutdown_logging():
    1110
    1111  #       if _cfg.get(option = u'debug'):
    1112  #               import types
    1113
    1114  #               def get_refcounts():
    1115  #                       refcount = {}
    1116  #                       # collect all classes
    1117  #                       for module in sys.modules.values():
    1118  #                               for sym in dir(module):
    1119  #                                       obj = getattr(module, sym)
    1120  #                                       if type(obj) is types.ClassType:
    1121  #                                               refcount[obj] = sys.getrefcount(obj)
    1122  #                       # sort by refcount
    1123  #                       pairs = map(lambda x: (x[1],x[0]), refcount.items())
    1124  #                       pairs.sort()
    1125  #                       pairs.reverse()
    1126  #                       return pairs
    1127
    1128  #               rcfile = open('./gm-refcount.lst', 'wt', encoding = 'utf8')
    1129  #               for refcount, class_ in get_refcounts():
    1130  #                       if not class_.__name__.startswith('wx'):
    1131  #                               rcfile.write(u'%10d %s\n' % (refcount, class_.__name__))
    1132  #               rcfile.close()
    1133
    1134          # do not choke on Windows
    1135          logging.raiseExceptions = False
    1136
    1137  #==========================================================
    1138 -def shutdown_tmp_dir():
    1139
    1140          tmp_dir = gmTools.gmPaths().tmp_dir
    1141
    1142          if _cfg.get(option = 'debug'):
    1143                  _log.debug('not removing tmp dir (--debug mode): %s', tmp_dir)
    1144                  return
    1145
    1146          _log.warning('removing tmp dir: %s', tmp_dir)
    1147          shutil.rmtree(tmp_dir, True)
    1148
    1149  #==========================================================
    1150  # main - launch the GNUmed wxPython GUI client
    1151  #----------------------------------------------------------
    1152
    1153  random.seed()
    1154
    1155  # setup
    1156  setup_fault_handler(target = None)
    1157  setup_console_encoding()
    1158  setup_python_path()
    1159  setup_logging()
    1160  log_startup_info()
    1161  setup_console_exception_handler()
    1162  setup_cli()
    1163  setup_signal_handlers()
    1164  setup_local_repo_path()
    1165
    1166  from Gnumed.pycommon import gmI18N
    1167  from Gnumed.pycommon import gmTools
    1168  from Gnumed.pycommon import gmDateTime
    1169
    1170  setup_locale()
    1171  handle_help_request()
    1172  handle_version_request()
    1173  setup_paths_and_files()
    1174  setup_date_time()
    1175  setup_cfg()
    1176
    1177  from Gnumed.pycommon import gmPG2
    1178  from Gnumed.pycommon import gmConnectionPool
    1179  setup_backend_environment()
    1180
    1181  # main
    1182  exit_code = run_tool()
    1183  if exit_code is None:
    1184          from Gnumed.pycommon import gmHooks
    1185          exit_code = run_ui()
    1186
    1187  # shutdown
    1188  shutdown_backend()
    1189  shutdown_tmp_dir()
    1190  _log.info('Normally shutting down as main module.')
    1191  shutdown_logging()
    1192
    1193  sys.exit(exit_code)
    1194
    1195  #==========================================================
    1196