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
207 if not '--local-import' in sys.argv:
208 _pre_log_buffer.append('running against systemwide install')
209 return
210
211 local_python_import_dir = os.path.dirname (
212 os.path.abspath(os.path.join(sys.argv[0], '..'))
213 )
214 print("Running from local source tree (%s) ..." % local_python_import_dir)
215 _pre_log_buffer.append("running from local source tree: %s" % local_python_import_dir)
216
217
218
219 link_name = os.path.join(local_python_import_dir, 'Gnumed')
220 if os.path.exists(link_name):
221 _pre_log_buffer.append('local module import dir symlink exists: %s' % link_name)
222 else:
223 real_dir = os.path.join(local_python_import_dir, 'client')
224 print('Creating local module import symlink ...')
225 print(' real dir:', real_dir)
226 print(' link:', link_name)
227 try:
228 os.symlink(real_dir, link_name)
229 except AttributeError:
230 _pre_log_buffer.append('Windows does not have os.symlink(), resorting to ctypes')
231 result = _symlink_windows(real_dir, link_name)
232 _pre_log_buffer.append('ctypes.windll.kernel32.CreateSymbolicLinkW() exit code: %s', result)
233 _pre_log_buffer.append('created local module import dir symlink: link [%s] => dir [%s]' % (link_name, real_dir))
234
235 print("Adjusting PYTHONPATH ...")
236 sys.path.insert(0, local_python_import_dir)
237 _pre_log_buffer.append('sys.path with local module import base dir prepended: %s' % sys.path)
238
239
241
242 local_repo_path = os.path.expanduser(os.path.join (
243 '~',
244 '.gnumed',
245 'local_code',
246 str(current_client_branch)
247 ))
248 local_wxGladeWidgets_path = os.path.join(local_repo_path, 'Gnumed', 'wxGladeWidgets')
249
250 if not os.path.exists(local_wxGladeWidgets_path):
251 _log.debug('[%s] not found', local_wxGladeWidgets_path)
252 _log.info('local wxGlade widgets repository not available')
253 return
254
255 _log.info('local wxGlade widgets repository found:')
256 _log.info(local_wxGladeWidgets_path)
257
258 if not os.access(local_wxGladeWidgets_path, os.R_OK):
259 _log.error('invalid repo: no read access')
260 return
261
262 all_entries = os.listdir(os.path.join(local_repo_path, 'Gnumed'))
263 _log.debug('repo base contains: %s', all_entries)
264 all_entries.remove('wxGladeWidgets')
265 try:
266 all_entries.remove('__init__.py')
267 except ValueError:
268 _log.error('invalid repo: lacking __init__.py')
269 return
270 try:
271 all_entries.remove('__init__.pyc')
272 except ValueError:
273 pass
274
275 if len(all_entries) > 0:
276 _log.error('insecure repo: additional files or directories found')
277 return
278
279
280 stat_val = os.stat(local_wxGladeWidgets_path)
281 _log.debug('repo stat(): %s', stat_val)
282 perms = stat.S_IMODE(stat_val.st_mode)
283 _log.debug('repo permissions: %s (octal: %s)', perms, oct(perms))
284 if perms != 448:
285 if os.name in ['nt']:
286 _log.warning('this platform does not support os.stat() permission checking')
287 else:
288 _log.error('insecure repo: permissions not 0600')
289 return
290
291 print("Activating local wxGlade widgets repository (%s) ..." % local_wxGladeWidgets_path)
292 sys.path.insert(0, local_repo_path)
293 _log.debug('sys.path with repo:')
294 _log.debug(sys.path)
295
296
298 if target is None:
299 faulthandler.enable()
300 _pre_log_buffer.append('<faulthandler> enabled, target = [console]: %s' % faulthandler)
301 return
302 _pre_log_buffer.append('<faulthandler> enabled, target = [%s]: %s' % (target, faulthandler))
303 faulthandler.enable(file = target)
304
305
321
322
324 global _pre_log_buffer
325 if len(_pre_log_buffer) > 0:
326 _log.info('early startup log buffer:')
327 for line in _pre_log_buffer:
328 _log.info(' ' + line)
329 del _pre_log_buffer
330 _log.info('GNUmed client version [%s] on branch [%s]', current_client_version, current_client_branch)
331 _log.info('Platform: %s', platform.uname())
332 _log.info(('Python %s on %s (%s)' % (sys.version, sys.platform, os.name)).replace('\n', '<\\n>'))
333 try:
334 import lsb_release
335 _log.info('lsb_release: %s', lsb_release.get_distro_information())
336 except ImportError:
337 pass
338 _log.info('os.getcwd(): [%s]', os.getcwd())
339 _log.info('process environment:')
340 for key, val in os.environ.items():
341 _log.info(' %s: %s' % (('${%s}' % key).rjust(30), val))
342
343
348
349
351 from Gnumed.pycommon import gmCfg2
352
353 global _cfg
354 _cfg = gmCfg2.gmCfgData()
355 _cfg.add_cli (
356 short_options = _known_short_options,
357 long_options = _known_long_options
358 )
359
360 val = _cfg.get(option = '--debug', source_order = [('cli', 'return')])
361 if val is None:
362 val = False
363 _cfg.set_option (
364 option = 'debug',
365 value = val
366 )
367
368 val = _cfg.get(option = '--slave', source_order = [('cli', 'return')])
369 if val is None:
370 val = False
371 _cfg.set_option (
372 option = 'slave',
373 value = val
374 )
375
376 val = _cfg.get(option = '--skip-update-check', source_order = [('cli', 'return')])
377 if val is None:
378 val = False
379 _cfg.set_option (
380 option = 'skip-update-check',
381 value = val
382 )
383
384 val = _cfg.get(option = '--hipaa', source_order = [('cli', 'return')])
385 if val is None:
386 val = False
387 _cfg.set_option (
388 option = 'hipaa',
389 value = val
390 )
391
392 val = _cfg.get(option = '--local-import', source_order = [('cli', 'return')])
393 if val is None:
394 val = False
395 _cfg.set_option (
396 option = 'local-import',
397 value = val
398 )
399
400 _cfg.set_option (
401 option = 'client_version',
402 value = current_client_version
403 )
404
405 _cfg.set_option (
406 option = 'client_branch',
407 value = current_client_branch
408 )
409
410
412 _log.critical('SIGTERM (SIG%s) received, shutting down ...' % signum)
413 gmLog2.flush()
414 print('GNUmed: SIGTERM (SIG%s) received, shutting down ...' % signum)
415 if frame is not None:
416 print('%s::%s@%s' % (frame.f_code.co_filename, frame.f_code.co_name, frame.f_lineno))
417
418
419
420 if _old_sig_term in [None, signal.SIG_IGN]:
421 sys.exit(1)
422 else:
423 _old_sig_term(signum, frame)
424
425
429
430
437
438
439
440
441
442
444 src = [('cli', 'return')]
445
446 help_requested = (
447 _cfg.get(option = '--help', source_order = src) or
448 _cfg.get(option = '-h', source_order = src) or
449 _cfg.get(option = '-?', source_order = src)
450 )
451
452 if help_requested:
453 print(_(
454 'Help requested\n'
455 '--------------'
456 ))
457 print(__doc__)
458 sys.exit(0)
459
460
479
480
482 """Create needed paths in user home directory."""
483
484 gnumed_DIR_README_TEXT = """GNUmed Electronic Medical Record
485
486 %s/
487
488 This directory should only ever contain files which the
489 user will come into direct contact with while using the
490 application (say, by selecting a file from the file system,
491 as when selecting document parts from files). You can create
492 subdirectories here as you see fit for the purpose.
493
494 This directory will also serve as the default directory when
495 GNUmed asks the user to select a directory for storing a
496 file.
497
498 Any files which are NOT intended for direct user interaction
499 but must be configured to live at a known location (say,
500 inter-application data exchange files) should be put under
501 the hidden directory "%s/".""" % (
502 os.path.expanduser(os.path.join('~', 'gnumed')),
503 os.path.expanduser(os.path.join('~', '.gnumed'))
504 )
505
506 gmTools.mkdir(os.path.expanduser(os.path.join('~', '.gnumed', 'scripts')))
507 gmTools.mkdir(os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck')))
508 gmTools.mkdir(os.path.expanduser(os.path.join('~', '.gnumed', 'error_logs')))
509 gmTools.mkdir(os.path.expanduser(os.path.join('~', 'gnumed')))
510
511 README = io.open(os.path.expanduser(os.path.join('~', 'gnumed', '00_README')), mode = 'wt', encoding = 'utf8')
512 README.write(gnumed_DIR_README_TEXT)
513 README.close()
514
515
516 paths = gmTools.gmPaths(app_name = 'gnumed')
517 print("Temp dir:", paths.tmp_dir)
518
519
520 io.open(os.path.expanduser(os.path.join('~', '.gnumed', 'gnumed.conf')), mode = 'a+t').close()
521
522
523 logfile_link = os.path.join(paths.tmp_dir, 'zzz-gnumed.log')
524 gmTools.mklink (gmLog2._logfile.name, logfile_link, overwrite = False)
525
526
529
530
532 """Detect and setup access to GNUmed config file.
533
534 Parts of this will have limited value due to
535 wxPython not yet being available.
536 """
537
538 enc = gmI18N.get_encoding()
539 paths = gmTools.gmPaths(app_name = 'gnumed')
540
541 candidates = [
542
543 ['workbase', os.path.join(paths.working_dir, 'gnumed.conf')],
544
545 ['system', os.path.join(paths.system_config_dir, 'gnumed-client.conf')],
546
547 ['user', os.path.join(paths.user_config_dir, 'gnumed.conf')],
548
549 ['local', os.path.join(paths.local_base_dir, 'gnumed.conf')]
550 ]
551
552 explicit_fname = _cfg.get(option = '--conf-file', source_order = [('cli', 'return')])
553 if explicit_fname is None:
554 candidates.append(['explicit', None])
555 else:
556 candidates.append(['explicit', explicit_fname])
557
558 for candidate in candidates:
559 _cfg.add_file_source (
560 source = candidate[0],
561 file = candidate[1],
562 encoding = enc
563 )
564
565
566 if explicit_fname is not None:
567 if _cfg.source_files['explicit'] is None:
568 _log.error('--conf-file argument does not exist')
569 print(missing_cli_config_file % explicit_fname)
570 sys.exit(1)
571
572
573 found_any_file = False
574 for f in _cfg.source_files.values():
575 if f is not None:
576 found_any_file = True
577 break
578 if not found_any_file:
579 _log.error('no config file found at all')
580 print(no_config_files % '\n '.join(candidates))
581 sys.exit(1)
582
583
584 fname = 'mime_type2file_extension.conf'
585 _cfg.add_file_source (
586 source = 'user-mime',
587 file = os.path.join(paths.user_config_dir, fname),
588 encoding = enc
589 )
590 _cfg.add_file_source (
591 source = 'system-mime',
592 file = os.path.join(paths.system_config_dir, fname),
593 encoding = enc
594 )
595
596
598 global ui_type
599 ui_type = _cfg.get(option = '--ui', source_order = [('cli', 'return')])
600 if ui_type in [True, False, None]:
601 ui_type = 'wxp'
602 ui_type = ui_type.strip()
603 if ui_type not in _known_ui_types:
604 _log.error('unknown UI type requested: %s', ui_type)
605 _log.debug('known UI types are: %s', str(_known_ui_types))
606 print("GNUmed startup: Unknown UI type (%s). Defaulting to wxPython client." % ui_type)
607 ui_type = 'wxp'
608 _log.debug('UI type: %s', ui_type)
609
610
612
613 db_version = gmPG2.map_client_branch2required_db_version[current_client_branch]
614 _log.info('client expects database version [%s]', db_version)
615 _cfg.set_option (
616 option = 'database_version',
617 value = db_version
618 )
619
620
621 timezone = _cfg.get (
622 group = 'backend',
623 option = 'client timezone',
624 source_order = [
625 ('explicit', 'return'),
626 ('workbase', 'return'),
627 ('local', 'return'),
628 ('user', 'return'),
629 ('system', 'return')
630 ]
631 )
632 if timezone is not None:
633 gmPG2.set_default_client_timezone(timezone)
634
635
658
659
688
689
690
691
694
695
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722 logging.raiseExceptions = False
723
724
735
736
737
738
739
740 random.seed()
741
742
743 setup_fault_handler(target = None)
744 setup_python_path()
745 setup_logging()
746 log_startup_info()
747 setup_console_exception_handler()
748 setup_cli()
749 setup_signal_handlers()
750 setup_local_repo_path()
751
752 from Gnumed.pycommon import gmI18N
753 from Gnumed.pycommon import gmTools
754 from Gnumed.pycommon import gmDateTime
755
756 setup_locale()
757 handle_help_request()
758 handle_version_request()
759 setup_paths_and_files()
760 setup_date_time()
761 setup_cfg()
762 setup_ui_type()
763
764 from Gnumed.pycommon import gmPG2
765 if ui_type in ['web']:
766 gmPG2.auto_request_login_params = False
767 setup_backend_environment()
768
769
770 exit_code = run_tool()
771 if exit_code is None:
772 from Gnumed.pycommon import gmHooks
773 exit_code = run_gui()
774
775
776 shutdown_backend()
777 shutdown_tmp_dir()
778 _log.info('Normally shutting down as main module.')
779 shutdown_logging()
780
781 sys.exit(exit_code)
782
783
784