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