1
2
3 __doc__ = """GNUmed client launcher.
4
5 This is the launcher for the GNUmed GUI client. It takes
6 care of all the pre- and post-GUI runtime environment setup.
7
8 --quiet
9 Be extra quiet and show only _real_ errors in the log.
10 --debug
11 Pre-set the [debug mode] checkbox in the login dialog to
12 increase verbosity in the log file. Useful for, well, debugging :-)
13 --slave
14 Pre-set the [enable remote control] checkbox in the login
15 dialog to enable the XML-RPC remote control feature.
16 --hipaa
17 Enable HIPAA functionality which has user impact.
18 --profile=<file>
19 Activate profiling and write profile data to <file>.
20 --tool=<TOOL>
21 Run TOOL instead of the main GUI.
22 --text-domain=<text domain>
23 Set this to change the name of the language file to be loaded.
24 Note, this does not change the directory the file is searched in,
25 only the name of the file where messages are loaded from. The
26 standard textdomain is, of course, "gnumed.mo".
27 --log-file=<file>
28 Use this to change the name of the log file.
29 See gmLog2.py to find out where the standard log file would
30 end up.
31 --conf-file=<file>
32 Use configuration file <file> instead of searching for it in
33 standard locations.
34 --lang-gettext=<language>
35 Explicitly set the language to use in gettext translation. The very
36 same effect can be achieved by setting the environment variable $LANG
37 from a launcher script.
38 --override-schema-check
39 Continue loading the client even if the database schema version
40 and the client software version cannot be verified to be compatible.
41 --skip-update-check
42 Skip checking for client updates. This is useful during development
43 and when the update check URL is unavailable (down).
44 --local-import
45 Adjust the PYTHONPATH such that GNUmed can be run from a local source tree.
46 --ui=<ui type>
47 Start an alternative UI. Defaults to wxPython if not specified.
48 Currently "wxp" (wxPython) only.
49 --wxp=<version>
50 Explicitely request a wxPython version. Can be set to either "2" or "3".
51 Defaults to "try 3, then 2" if not set.
52 --version, -V
53 Show version information.
54 --help, -h, or -?
55 Show this help.
56 """
57
58 __author__ = "H. Herb <hherb@gnumed.net>, K. Hilbert <Karsten.Hilbert@gmx.net>, I. Haywood <i.haywood@ugrad.unimelb.edu.au>"
59 __license__ = "GPL v2 or later (details at http://www.gnu.org)"
60
61
62
63 import sys
64 import os
65 import platform
66 import faulthandler
67 import random
68 import logging
69 import signal
70 import os.path
71 import shutil
72 import stat
73 import io
74
75
76
77 if __name__ != "__main__":
78 print("GNUmed startup: This is not intended to be imported as a module !")
79 print("-----------------------------------------------------------------")
80 print(__doc__)
81 sys.exit(1)
82
83
84
85 if os.name in ['posix'] and os.geteuid() == 0:
86 print("""
87 GNUmed startup: GNUmed should not be run as root.
88 -------------------------------------------------
89
90 Running GNUmed as <root> can potentially put all
91 your medical data at risk. It is strongly advised
92 against. Please run GNUmed as a non-root user.
93 """)
94 sys.exit(1)
95
96
97 current_client_version = '1.8.rc1'
98 current_client_branch = '1.8'
99
100 _log = None
101 _pre_log_buffer = []
102 _cfg = None
103 _old_sig_term = None
104 _known_short_options = 'h?V'
105 _known_long_options = [
106 'debug',
107 'slave',
108 'skip-update-check',
109 'profile=',
110 'text-domain=',
111 'log-file=',
112 'conf-file=',
113 'lang-gettext=',
114 'ui=',
115 'override-schema-check',
116 'local-import',
117 'help',
118 'version',
119 'hipaa',
120 'wxp=',
121 'tool='
122 ]
123
124 _known_ui_types = [
125 'web',
126 'wxp',
127 'chweb'
128 ]
129
130 _known_tools = [
131 'check_enc_epi_xref',
132 'export_pat_emr_structure'
133 ]
134
135
136 import_error_sermon = """
137 GNUmed startup: Cannot load GNUmed Python modules !
138 ---------------------------------------------------
139 CRITICAL ERROR: Program halted.
140
141 Please make sure you have:
142
143 1) the required third-party Python modules installed
144 2) the GNUmed Python modules linked or installed into site-packages/
145 (if you do not run from a CVS tree the installer should have taken care of that)
146 3) your PYTHONPATH environment variable set up correctly
147
148 <sys.path> is currently set to:
149
150 %s
151
152 If you are running from a copy of the CVS tree make sure you
153 did run gnumed/check-prerequisites.sh with good results.
154
155 If you still encounter errors after checking the above
156 requirements please ask on the mailing list.
157 """
158
159
160 missing_cli_config_file = """
161 GNUmed startup: Missing configuration file.
162 -------------------------------------------
163
164 You explicitly specified a configuration file
165 on the command line:
166
167 --conf-file=%s
168
169 The file does not exist, however.
170 """
171
172
173 no_config_files = """
174 GNUmed startup: Missing configuration files.
175 --------------------------------------------
176
177 None of the below candidate configuration
178 files could be found:
179
180 %s
181
182 Cannot run GNUmed without any of them.
183 """
184
185
186
187
189 import ctypes
190 csl = ctypes.windll.kernel32.CreateSymbolicLinkW
191 csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
192 csl.restype = ctypes.c_ubyte
193 if os.path.isdir(source):
194 flags = 1
195 else:
196 flags = 0
197 ret_code = csl(link_name, source.replace('/', '\\'), flags)
198 if ret_code == 0:
199 raise ctypes.WinError()
200 return ret_code
201
202
203
204
206 if target is None:
207 faulthandler.enable()
208 _pre_log_buffer.append('<faulthandler> enabled, target = [console]: %s' % faulthandler)
209 return
210 _pre_log_buffer.append('<faulthandler> enabled, target = [%s]: %s' % (target, faulthandler))
211 faulthandler.enable(file = target)
212
213
215 print_lines = []
216 try:
217 sys.stdout.reconfigure(errors = 'surrogateescape')
218 sys.stderr.reconfigure(errors = 'surrogateescape')
219 _pre_log_buffer.append('stdout/stderr reconfigured to use <surrogateescape> for encoding errors')
220 return
221 except AttributeError:
222 line = 'cannot reconfigure sys.stdout/stderr to use <errors="surrogateescape"> (needs Python 3.7+)'
223 _pre_log_buffer.append(line)
224 print_lines.append(line)
225 try:
226 _pre_log_buffer.append('sys.stdout/stderr default to "${PYTHONIOENCODING}=%s"' % os.environ['PYTHONIOENCODING'])
227 return
228 except KeyError:
229 lines = [
230 '${PYTHONIOENCODING} is not set up, use <PYTHONIOENCODING=utf-8:surrogateescape> in the shell (for Python < 3.7)',
231 'console encoding errors may occur'
232 ]
233 for line in lines:
234 print_lines.append(line)
235 _pre_log_buffer.append(line)
236 for line in print_lines:
237 print('GNUmed startup:', line)
238
239
241
242 if not '--local-import' in sys.argv:
243 _pre_log_buffer.append('running against systemwide install')
244 return
245
246 local_python_import_dir = os.path.dirname (
247 os.path.abspath(os.path.join(sys.argv[0], '..'))
248 )
249 print("Running from local source tree (%s) ..." % local_python_import_dir)
250 _pre_log_buffer.append("running from local source tree: %s" % local_python_import_dir)
251
252
253
254 link_name = os.path.join(local_python_import_dir, 'Gnumed')
255 if os.path.exists(link_name):
256 _pre_log_buffer.append('local module import dir symlink exists: %s' % link_name)
257 else:
258 real_dir = os.path.join(local_python_import_dir, 'client')
259 print('Creating local module import symlink ...')
260 print(' real dir:', real_dir)
261 print(' link:', link_name)
262 try:
263 os.symlink(real_dir, link_name)
264 except AttributeError:
265 _pre_log_buffer.append('Windows does not have os.symlink(), resorting to ctypes')
266 result = _symlink_windows(real_dir, link_name)
267 _pre_log_buffer.append('ctypes.windll.kernel32.CreateSymbolicLinkW() exit code: %s', result)
268 _pre_log_buffer.append('created local module import dir symlink: link [%s] => dir [%s]' % (link_name, real_dir))
269
270 sys.path.insert(0, local_python_import_dir)
271 _pre_log_buffer.append('sys.path with local module import base dir prepended: %s' % sys.path)
272
273
275
276 local_repo_path = os.path.expanduser(os.path.join (
277 '~',
278 '.gnumed',
279 'local_code',
280 str(current_client_branch)
281 ))
282 local_wxGladeWidgets_path = os.path.join(local_repo_path, 'Gnumed', 'wxGladeWidgets')
283
284 if not os.path.exists(local_wxGladeWidgets_path):
285 _log.debug('[%s] not found', local_wxGladeWidgets_path)
286 _log.info('local wxGlade widgets repository not available')
287 return
288
289 _log.info('local wxGlade widgets repository found:')
290 _log.info(local_wxGladeWidgets_path)
291
292 if not os.access(local_wxGladeWidgets_path, os.R_OK):
293 _log.error('invalid repo: no read access')
294 return
295
296 all_entries = os.listdir(os.path.join(local_repo_path, 'Gnumed'))
297 _log.debug('repo base contains: %s', all_entries)
298 all_entries.remove('wxGladeWidgets')
299 try:
300 all_entries.remove('__init__.py')
301 except ValueError:
302 _log.error('invalid repo: lacking __init__.py')
303 return
304 try:
305 all_entries.remove('__init__.pyc')
306 except ValueError:
307 pass
308
309 if len(all_entries) > 0:
310 _log.error('insecure repo: additional files or directories found')
311 return
312
313
314 stat_val = os.stat(local_wxGladeWidgets_path)
315 _log.debug('repo stat(): %s', stat_val)
316 perms = stat.S_IMODE(stat_val.st_mode)
317 _log.debug('repo permissions: %s (octal: %s)', perms, oct(perms))
318 if perms != 448:
319 if os.name in ['nt']:
320 _log.warning('this platform does not support os.stat() permission checking')
321 else:
322 _log.error('insecure repo: permissions not 0600')
323 return
324
325 print("Activating local wxGlade widgets repository (%s) ..." % local_wxGladeWidgets_path)
326 sys.path.insert(0, local_repo_path)
327 _log.debug('sys.path with repo:')
328 _log.debug(sys.path)
329
330
346
347
349 global _pre_log_buffer
350 if len(_pre_log_buffer) > 0:
351 _log.info('early startup log buffer:')
352 for line in _pre_log_buffer:
353 _log.info(' ' + line)
354 del _pre_log_buffer
355 _log.info('GNUmed client version [%s] on branch [%s]', current_client_version, current_client_branch)
356 _log.info('Platform: %s', platform.uname())
357 _log.info(('Python %s on %s (%s)' % (sys.version, sys.platform, os.name)).replace('\n', '<\\n>'))
358 try:
359 import lsb_release
360 _log.info('lsb_release: %s', lsb_release.get_distro_information())
361 except ImportError:
362 pass
363 _log.info('threading: %s', sys.thread_info)
364 _log.info('os.getcwd(): [%s]', os.getcwd())
365 _log.info('process environment:')
366 for key, val in os.environ.items():
367 _log.info(' %s: %s' % (('${%s}' % key).rjust(30), val))
368 import sysconfig
369 _log.info('sysconfig - platform [%s] python version [%s]:', sysconfig.get_platform(), sysconfig.get_python_version())
370 paths = sysconfig.get_paths()
371 for path in paths:
372 _log.info(' %s: %s', path.rjust(30), paths[path])
373 conf_vars = sysconfig.get_config_vars()
374 for var in conf_vars:
375 _log.info(' %s: %s', var.rjust(40), conf_vars[var])
376
377
382
383
385 from Gnumed.pycommon import gmCfg2
386
387 global _cfg
388 _cfg = gmCfg2.gmCfgData()
389 _cfg.add_cli (
390 short_options = _known_short_options,
391 long_options = _known_long_options
392 )
393
394 val = _cfg.get(option = '--debug', source_order = [('cli', 'return')])
395 if val is None:
396 val = False
397 _cfg.set_option (
398 option = 'debug',
399 value = val
400 )
401
402 val = _cfg.get(option = '--slave', source_order = [('cli', 'return')])
403 if val is None:
404 val = False
405 _cfg.set_option (
406 option = 'slave',
407 value = val
408 )
409
410 val = _cfg.get(option = '--skip-update-check', source_order = [('cli', 'return')])
411 if val is None:
412 val = False
413 _cfg.set_option (
414 option = 'skip-update-check',
415 value = val
416 )
417
418 val = _cfg.get(option = '--hipaa', source_order = [('cli', 'return')])
419 if val is None:
420 val = False
421 _cfg.set_option (
422 option = 'hipaa',
423 value = val
424 )
425
426 val = _cfg.get(option = '--local-import', source_order = [('cli', 'return')])
427 if val is None:
428 val = False
429 _cfg.set_option (
430 option = 'local-import',
431 value = val
432 )
433
434 _cfg.set_option (
435 option = 'client_version',
436 value = current_client_version
437 )
438
439 _cfg.set_option (
440 option = 'client_branch',
441 value = current_client_branch
442 )
443
444
446 _log.critical('SIGTERM (SIG%s) received, shutting down ...' % signum)
447 gmLog2.flush()
448 print('GNUmed: SIGTERM (SIG%s) received, shutting down ...' % signum)
449 if frame is not None:
450 print('%s::%s@%s' % (frame.f_code.co_filename, frame.f_code.co_name, frame.f_lineno))
451
452
453
454 if _old_sig_term in [None, signal.SIG_IGN]:
455 sys.exit(1)
456 else:
457 _old_sig_term(signum, frame)
458
459
463
464
471
472
473
474
475
476
478 src = [('cli', 'return')]
479
480 help_requested = (
481 _cfg.get(option = '--help', source_order = src) or
482 _cfg.get(option = '-h', source_order = src) or
483 _cfg.get(option = '-?', source_order = src)
484 )
485
486 if help_requested:
487 print(_(
488 'Help requested\n'
489 '--------------'
490 ))
491 print(__doc__)
492 sys.exit(0)
493
494
513
514
516 """Create needed paths in user home directory."""
517
518 gnumed_DIR_README_TEXT = """GNUmed Electronic Medical Record
519
520 %s/
521
522 This directory should only ever contain files which the
523 user will come into direct contact with while using the
524 application (say, by selecting a file from the file system,
525 as when selecting document parts from files). You can create
526 subdirectories here as you see fit for the purpose.
527
528 This directory will also serve as the default directory when
529 GNUmed asks the user to select a directory for storing a
530 file.
531
532 Any files which are NOT intended for direct user interaction
533 but must be configured to live at a known location (say,
534 inter-application data exchange files) should be put under
535 the hidden directory "%s/".""" % (
536 os.path.expanduser(os.path.join('~', 'gnumed')),
537 os.path.expanduser(os.path.join('~', '.gnumed'))
538 )
539
540 gmTools.mkdir(os.path.expanduser(os.path.join('~', '.gnumed', 'scripts')))
541 gmTools.mkdir(os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck')))
542 gmTools.mkdir(os.path.expanduser(os.path.join('~', '.gnumed', 'error_logs')))
543 gmTools.mkdir(os.path.expanduser(os.path.join('~', 'gnumed')))
544
545 README = io.open(os.path.expanduser(os.path.join('~', 'gnumed', '00_README')), mode = 'wt', encoding = 'utf8')
546 README.write(gnumed_DIR_README_TEXT)
547 README.close()
548
549
550 paths = gmTools.gmPaths(app_name = 'gnumed')
551 print("Temp dir:", paths.tmp_dir)
552
553
554 io.open(os.path.expanduser(os.path.join('~', '.gnumed', 'gnumed.conf')), mode = 'a+t').close()
555
556
557 logfile_link = os.path.join(paths.tmp_dir, 'zzz-gnumed.log')
558 gmTools.mklink (gmLog2._logfile.name, logfile_link, overwrite = False)
559
560
563
564
566 """Detect and setup access to GNUmed config file.
567
568 Parts of this will have limited value due to
569 wxPython not yet being available.
570 """
571
572 enc = gmI18N.get_encoding()
573 paths = gmTools.gmPaths(app_name = 'gnumed')
574
575 candidates = [
576
577 ['workbase', os.path.join(paths.working_dir, 'gnumed.conf')],
578
579 ['system', os.path.join(paths.system_config_dir, 'gnumed-client.conf')],
580
581 ['user', os.path.join(paths.user_config_dir, 'gnumed.conf')],
582
583 ['local', os.path.join(paths.local_base_dir, 'gnumed.conf')]
584 ]
585
586 explicit_fname = _cfg.get(option = '--conf-file', source_order = [('cli', 'return')])
587 if explicit_fname is None:
588 candidates.append(['explicit', None])
589 else:
590 candidates.append(['explicit', explicit_fname])
591
592 for candidate in candidates:
593 _cfg.add_file_source (
594 source = candidate[0],
595 file = candidate[1],
596 encoding = enc
597 )
598
599
600 if explicit_fname is not None:
601 if _cfg.source_files['explicit'] is None:
602 _log.error('--conf-file argument does not exist')
603 print(missing_cli_config_file % explicit_fname)
604 sys.exit(1)
605
606
607 found_any_file = False
608 for f in _cfg.source_files.values():
609 if f is not None:
610 found_any_file = True
611 break
612 if not found_any_file:
613 _log.error('no config file found at all')
614 print(no_config_files % '\n '.join(candidates))
615 sys.exit(1)
616
617
618 fname = 'mime_type2file_extension.conf'
619 _cfg.add_file_source (
620 source = 'user-mime',
621 file = os.path.join(paths.user_config_dir, fname),
622 encoding = enc
623 )
624 _cfg.add_file_source (
625 source = 'system-mime',
626 file = os.path.join(paths.system_config_dir, fname),
627 encoding = enc
628 )
629
630
632 global ui_type
633 ui_type = _cfg.get(option = '--ui', source_order = [('cli', 'return')])
634 if ui_type in [True, False, None]:
635 ui_type = 'wxp'
636 ui_type = ui_type.strip()
637 if ui_type not in _known_ui_types:
638 _log.error('unknown UI type requested: %s', ui_type)
639 _log.debug('known UI types are: %s', str(_known_ui_types))
640 print("GNUmed startup: Unknown UI type (%s). Defaulting to wxPython client." % ui_type)
641 ui_type = 'wxp'
642 _log.debug('UI type: %s', ui_type)
643
644
646
647 db_version = gmPG2.map_client_branch2required_db_version[current_client_branch]
648 _log.info('client expects database version [%s]', db_version)
649 _cfg.set_option (
650 option = 'database_version',
651 value = db_version
652 )
653
654
655 timezone = _cfg.get (
656 group = 'backend',
657 option = 'client timezone',
658 source_order = [
659 ('explicit', 'return'),
660 ('workbase', 'return'),
661 ('local', 'return'),
662 ('user', 'return'),
663 ('system', 'return')
664 ]
665 )
666 if timezone is not None:
667 gmPG2.set_default_client_timezone(timezone)
668
669
692
693
779
780
781
782
785
786
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813 logging.raiseExceptions = False
814
815
826
827
828
829
830
831 random.seed()
832
833
834 setup_fault_handler(target = None)
835 setup_console_encoding()
836 setup_python_path()
837 setup_logging()
838 log_startup_info()
839 setup_console_exception_handler()
840 setup_cli()
841 setup_signal_handlers()
842 setup_local_repo_path()
843
844 from Gnumed.pycommon import gmI18N
845 from Gnumed.pycommon import gmTools
846 from Gnumed.pycommon import gmDateTime
847
848 setup_locale()
849 handle_help_request()
850 handle_version_request()
851 setup_paths_and_files()
852 setup_date_time()
853 setup_cfg()
854 setup_ui_type()
855
856 from Gnumed.pycommon import gmPG2
857 if ui_type in ['web']:
858 gmPG2.auto_request_login_params = False
859 setup_backend_environment()
860
861
862 exit_code = run_tool()
863 if exit_code is None:
864 from Gnumed.pycommon import gmHooks
865 exit_code = run_gui()
866
867
868 shutdown_backend()
869 shutdown_tmp_dir()
870 _log.info('Normally shutting down as main module.')
871 shutdown_logging()
872
873 sys.exit(exit_code)
874
875
876