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  #---------------------------------------------------------- 
 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