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('os.getcwd(): [%s]', os.getcwd())
364 _log.info('process environment:')
365 for key, val in os.environ.items():
366 _log.info(' %s: %s' % (('${%s}' % key).rjust(30), val))
367
368
373
374
376 from Gnumed.pycommon import gmCfg2
377
378 global _cfg
379 _cfg = gmCfg2.gmCfgData()
380 _cfg.add_cli (
381 short_options = _known_short_options,
382 long_options = _known_long_options
383 )
384
385 val = _cfg.get(option = '--debug', source_order = [('cli', 'return')])
386 if val is None:
387 val = False
388 _cfg.set_option (
389 option = 'debug',
390 value = val
391 )
392
393 val = _cfg.get(option = '--slave', source_order = [('cli', 'return')])
394 if val is None:
395 val = False
396 _cfg.set_option (
397 option = 'slave',
398 value = val
399 )
400
401 val = _cfg.get(option = '--skip-update-check', source_order = [('cli', 'return')])
402 if val is None:
403 val = False
404 _cfg.set_option (
405 option = 'skip-update-check',
406 value = val
407 )
408
409 val = _cfg.get(option = '--hipaa', source_order = [('cli', 'return')])
410 if val is None:
411 val = False
412 _cfg.set_option (
413 option = 'hipaa',
414 value = val
415 )
416
417 val = _cfg.get(option = '--local-import', source_order = [('cli', 'return')])
418 if val is None:
419 val = False
420 _cfg.set_option (
421 option = 'local-import',
422 value = val
423 )
424
425 _cfg.set_option (
426 option = 'client_version',
427 value = current_client_version
428 )
429
430 _cfg.set_option (
431 option = 'client_branch',
432 value = current_client_branch
433 )
434
435
437 _log.critical('SIGTERM (SIG%s) received, shutting down ...' % signum)
438 gmLog2.flush()
439 print('GNUmed: SIGTERM (SIG%s) received, shutting down ...' % signum)
440 if frame is not None:
441 print('%s::%s@%s' % (frame.f_code.co_filename, frame.f_code.co_name, frame.f_lineno))
442
443
444
445 if _old_sig_term in [None, signal.SIG_IGN]:
446 sys.exit(1)
447 else:
448 _old_sig_term(signum, frame)
449
450
454
455
462
463
464
465
466
467
469 src = [('cli', 'return')]
470
471 help_requested = (
472 _cfg.get(option = '--help', source_order = src) or
473 _cfg.get(option = '-h', source_order = src) or
474 _cfg.get(option = '-?', source_order = src)
475 )
476
477 if help_requested:
478 print(_(
479 'Help requested\n'
480 '--------------'
481 ))
482 print(__doc__)
483 sys.exit(0)
484
485
504
505
507 """Create needed paths in user home directory."""
508
509 gnumed_DIR_README_TEXT = """GNUmed Electronic Medical Record
510
511 %s/
512
513 This directory should only ever contain files which the
514 user will come into direct contact with while using the
515 application (say, by selecting a file from the file system,
516 as when selecting document parts from files). You can create
517 subdirectories here as you see fit for the purpose.
518
519 This directory will also serve as the default directory when
520 GNUmed asks the user to select a directory for storing a
521 file.
522
523 Any files which are NOT intended for direct user interaction
524 but must be configured to live at a known location (say,
525 inter-application data exchange files) should be put under
526 the hidden directory "%s/".""" % (
527 os.path.expanduser(os.path.join('~', 'gnumed')),
528 os.path.expanduser(os.path.join('~', '.gnumed'))
529 )
530
531 gmTools.mkdir(os.path.expanduser(os.path.join('~', '.gnumed', 'scripts')))
532 gmTools.mkdir(os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck')))
533 gmTools.mkdir(os.path.expanduser(os.path.join('~', '.gnumed', 'error_logs')))
534 gmTools.mkdir(os.path.expanduser(os.path.join('~', 'gnumed')))
535
536 README = io.open(os.path.expanduser(os.path.join('~', 'gnumed', '00_README')), mode = 'wt', encoding = 'utf8')
537 README.write(gnumed_DIR_README_TEXT)
538 README.close()
539
540
541 paths = gmTools.gmPaths(app_name = 'gnumed')
542 print("Temp dir:", paths.tmp_dir)
543
544
545 io.open(os.path.expanduser(os.path.join('~', '.gnumed', 'gnumed.conf')), mode = 'a+t').close()
546
547
548 logfile_link = os.path.join(paths.tmp_dir, 'zzz-gnumed.log')
549 gmTools.mklink (gmLog2._logfile.name, logfile_link, overwrite = False)
550
551
554
555
557 """Detect and setup access to GNUmed config file.
558
559 Parts of this will have limited value due to
560 wxPython not yet being available.
561 """
562
563 enc = gmI18N.get_encoding()
564 paths = gmTools.gmPaths(app_name = 'gnumed')
565
566 candidates = [
567
568 ['workbase', os.path.join(paths.working_dir, 'gnumed.conf')],
569
570 ['system', os.path.join(paths.system_config_dir, 'gnumed-client.conf')],
571
572 ['user', os.path.join(paths.user_config_dir, 'gnumed.conf')],
573
574 ['local', os.path.join(paths.local_base_dir, 'gnumed.conf')]
575 ]
576
577 explicit_fname = _cfg.get(option = '--conf-file', source_order = [('cli', 'return')])
578 if explicit_fname is None:
579 candidates.append(['explicit', None])
580 else:
581 candidates.append(['explicit', explicit_fname])
582
583 for candidate in candidates:
584 _cfg.add_file_source (
585 source = candidate[0],
586 file = candidate[1],
587 encoding = enc
588 )
589
590
591 if explicit_fname is not None:
592 if _cfg.source_files['explicit'] is None:
593 _log.error('--conf-file argument does not exist')
594 print(missing_cli_config_file % explicit_fname)
595 sys.exit(1)
596
597
598 found_any_file = False
599 for f in _cfg.source_files.values():
600 if f is not None:
601 found_any_file = True
602 break
603 if not found_any_file:
604 _log.error('no config file found at all')
605 print(no_config_files % '\n '.join(candidates))
606 sys.exit(1)
607
608
609 fname = 'mime_type2file_extension.conf'
610 _cfg.add_file_source (
611 source = 'user-mime',
612 file = os.path.join(paths.user_config_dir, fname),
613 encoding = enc
614 )
615 _cfg.add_file_source (
616 source = 'system-mime',
617 file = os.path.join(paths.system_config_dir, fname),
618 encoding = enc
619 )
620
621
623 global ui_type
624 ui_type = _cfg.get(option = '--ui', source_order = [('cli', 'return')])
625 if ui_type in [True, False, None]:
626 ui_type = 'wxp'
627 ui_type = ui_type.strip()
628 if ui_type not in _known_ui_types:
629 _log.error('unknown UI type requested: %s', ui_type)
630 _log.debug('known UI types are: %s', str(_known_ui_types))
631 print("GNUmed startup: Unknown UI type (%s). Defaulting to wxPython client." % ui_type)
632 ui_type = 'wxp'
633 _log.debug('UI type: %s', ui_type)
634
635
637
638 db_version = gmPG2.map_client_branch2required_db_version[current_client_branch]
639 _log.info('client expects database version [%s]', db_version)
640 _cfg.set_option (
641 option = 'database_version',
642 value = db_version
643 )
644
645
646 timezone = _cfg.get (
647 group = 'backend',
648 option = 'client timezone',
649 source_order = [
650 ('explicit', 'return'),
651 ('workbase', 'return'),
652 ('local', 'return'),
653 ('user', 'return'),
654 ('system', 'return')
655 ]
656 )
657 if timezone is not None:
658 gmPG2.set_default_client_timezone(timezone)
659
660
683
684
770
771
772
773
776
777
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804 logging.raiseExceptions = False
805
806
817
818
819
820
821
822 random.seed()
823
824
825 setup_fault_handler(target = None)
826 setup_console_encoding()
827 setup_python_path()
828 setup_logging()
829 log_startup_info()
830 setup_console_exception_handler()
831 setup_cli()
832 setup_signal_handlers()
833 setup_local_repo_path()
834
835 from Gnumed.pycommon import gmI18N
836 from Gnumed.pycommon import gmTools
837 from Gnumed.pycommon import gmDateTime
838
839 setup_locale()
840 handle_help_request()
841 handle_version_request()
842 setup_paths_and_files()
843 setup_date_time()
844 setup_cfg()
845 setup_ui_type()
846
847 from Gnumed.pycommon import gmPG2
848 if ui_type in ['web']:
849 gmPG2.auto_request_login_params = False
850 setup_backend_environment()
851
852
853 exit_code = run_tool()
854 if exit_code is None:
855 from Gnumed.pycommon import gmHooks
856 exit_code = run_gui()
857
858
859 shutdown_backend()
860 shutdown_tmp_dir()
861 _log.info('Normally shutting down as main module.')
862 shutdown_logging()
863
864 sys.exit(exit_code)
865
866
867