#!/usr/bin/env python __doc__ = """GNUmed web client launcher. """ #========================================================== # $Source: /home/ncq/Projekte/cvs2git/vcs-mirror/gnumed/gnumed/client/wxpython/gnumed.py,v $ # $Id: gnumed.py,v 1.169 2010-01-31 18:20:41 ncq Exp $ __version__ = "$Revision: 1.169 $" __author__ = "H. Herb , K. Hilbert , I. Haywood " __license__ = "GPL (details at http://www.gnu.org)" import cherrypy # standard library import sys, os, os.path, signal, logging, platform # do not run as module if __name__ != "__main__": print "GNUmed startup: This is not intended to be imported as a module !" print "-----------------------------------------------------------------" print __doc__ sys.exit(1) #---------------------------------------------------------- #current_client_version = u'0.7.rc1' current_client_version = u'GIT HEAD' #current_client_branch = u'0.7' current_client_branch = u'GIT tree' _log = None _cfg = None _old_sig_term = None _known_short_options = u'h?V' _known_long_options = [ u'debug', u'slave', u'skip-update-check', u'profile=', u'text-domain=', u'log-file=', u'conf-file=', u'lang-gettext=', u'override-schema-check', u'local-import', u'help', u'version', u'hipaa' ] # do not run as root if os.name in ['posix'] and os.geteuid() == 0: print """ GNUmed startup: GNUmed should not be run as root. ------------------------------------------------- Running GNUmed as can potentially put all your medical data at risk. It is strongly advised against. Please run GNUmed as a non-root user. """ sys.exit(1) #========================================================== def setup_python_path(): if not u'--local-import' in sys.argv: return print "GNUmed startup: Running from local source tree." print "-----------------------------------------------" local_python_base_dir = os.path.dirname ( os.path.abspath(os.path.join(sys.argv[0], '..', '..')) ) # does the path exist at all, physically ? # (*broken* links are reported as False) link_name = os.path.join(local_python_base_dir, 'Gnumed') if not os.path.exists(link_name): real_dir = os.path.join(local_python_base_dir, 'client') print "Creating module import symlink ..." print ' real dir:', real_dir print ' link:', link_name os.symlink(real_dir, link_name) print "Adjusting PYTHONPATH ..." sys.path.insert(0, local_python_base_dir) #========================================================== def setup_logging(): try: from Gnumed.pycommon import gmLog2 as _gmLog2 except ImportError: sys.exit(import_error_sermon % '\n '.join(sys.path)) global gmLog2 gmLog2 = _gmLog2 global _log _log = logging.getLogger('gm.launcher') #========================================================== def handle_sig_term(signum, frame): _log.critical('SIGTERM (SIG%s) received, shutting down ...' % signum) gmLog2.flush() print 'GNUmed: SIGTERM (SIG%s) received, shutting down ...' % signum if frame is not None: print '%s::address@hidden' % (frame.f_code.co_filename, frame.f_code.co_name, frame.f_lineno) # FIXME: need to do something useful here if _old_sig_term in [None, signal.SIG_IGN]: sys.exit(signal.SIGTERM) else: _old_sig_term(signum, frame) #---------------------------------------------------------- def setup_signal_handlers(): global _old_sig_term old_sig_term = signal.signal(signal.SIGTERM, handle_sig_term) #========================================================== def setup_locale(): gmI18N.activate_locale() td = _cfg.get(option = '--text-domain', source_order = [('cli', 'return')]) l = _cfg.get(option = '--lang-gettext', source_order = [('cli', 'return')]) gmI18N.install_domain(domain = td, language = l, prefer_local_catalog = _cfg.get(option = u'local-import')) # make sure we re-get the default encoding # in case it changed gmLog2.set_string_encoding() #========================================================== def handle_help_request(): src = [(u'cli', u'return')] help_requested = ( _cfg.get(option = u'--help', source_order = src) or _cfg.get(option = u'-h', source_order = src) or _cfg.get(option = u'-?', source_order = src) ) if help_requested: print _( 'Help requested\n' '--------------' ) print __doc__ sys.exit(0) #========================================================== def handle_version_request(): src = [(u'cli', u'return')] version_requested = ( _cfg.get(option = u'--version', source_order = src) or _cfg.get(option = u'-V', source_order = src) ) if version_requested: from Gnumed.pycommon.gmPG2 import map_client_branch2required_db_version, known_schema_hashes print 'GNUmed version information' print '--------------------------' print 'client : %s on branch [%s]' % (current_client_version, current_client_branch) print 'database : %s' % map_client_branch2required_db_version[current_client_branch] print 'schema hash: %s' % known_schema_hashes[map_client_branch2required_db_version[current_client_branch]] sys.exit(0) #========================================================== def setup_paths_and_files(): """Create needed paths in user home directory.""" gmTools.mkdir(os.path.expanduser(os.path.join('~', '.gnumed', 'scripts'))) gmTools.mkdir(os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck'))) gmTools.mkdir(os.path.expanduser(os.path.join('~', '.gnumed', 'tmp'))) gmTools.mkdir(os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'docs'))) gmTools.mkdir(os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'xDT'))) gmTools.mkdir(os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'EMR'))) gmTools.mkdir(os.path.expanduser(os.path.join('~', 'gnumed', 'xDT'))) gmTools.mkdir(os.path.expanduser(os.path.join('~', 'gnumed', 'logs'))) paths = gmTools.gmPaths(app_name = u'gnumed') open(os.path.expanduser(os.path.join('~', '.gnumed', 'gnumed.conf')), 'a+').close() #========================================================== def setup_date_time(): gmDateTime.init() #========================================================== def setup_console_exception_handler(): from Gnumed.pycommon.gmTools import handle_uncaught_exception_console sys.excepthook = handle_uncaught_exception_console #========================================================== def setup_cli(): from Gnumed.pycommon import gmCfg2 global _cfg _cfg = gmCfg2.gmCfgData() _cfg.add_cli ( short_options = _known_short_options, long_options = _known_long_options ) val = _cfg.get(option = '--debug', source_order = [('cli', 'return')]) if val is None: val = False _cfg.set_option ( option = u'debug', value = val ) val = _cfg.get(option = '--slave', source_order = [('cli', 'return')]) if val is None: val = False _cfg.set_option ( option = u'slave', value = val ) val = _cfg.get(option = '--skip-update-check', source_order = [('cli', 'return')]) if val is None: val = False _cfg.set_option ( option = u'skip-update-check', value = val ) val = _cfg.get(option = '--hipaa', source_order = [('cli', 'return')]) if val is None: val = False _cfg.set_option ( option = u'hipaa', value = val ) val = _cfg.get(option = '--local-import', source_order = [('cli', 'return')]) if val is None: val = False _cfg.set_option ( option = u'local-import', value = val ) _cfg.set_option ( option = u'client_version', value = current_client_version ) _cfg.set_option ( option = u'client_branch', value = current_client_branch ) #========================================================== def setup_cfg(): """Detect and setup access to GNUmed config file. Parts of this will have limited value due to wxPython not yet being available. """ enc = gmI18N.get_encoding() paths = gmTools.gmPaths(app_name = u'gnumed') candidates = [ # the current working dir [u'workbase', os.path.join(paths.working_dir, 'gnumed.conf')], # /etc/gnumed/ [u'system', os.path.join(paths.system_config_dir, 'gnumed-client.conf')], # ~/.gnumed/ [u'user', os.path.join(paths.user_config_dir, 'gnumed.conf')], # CVS/tgz tree .../gnumed/client/ (IOW a local installation) [u'local', os.path.join(paths.local_base_dir, 'gnumed.conf')] ] # --conf-file= explicit_fname = _cfg.get(option = u'--conf-file', source_order = [(u'cli', u'return')]) if explicit_fname is None: candidates.append([u'explicit', None]) else: candidates.append([u'explicit', explicit_fname]) for candidate in candidates: _cfg.add_file_source ( source = candidate[0], file = candidate[1], encoding = enc ) # --conf-file given but does not actually exist ? if explicit_fname is not None: if _cfg.source_files['explicit'] is None: _log.error('--conf-file argument does not exist') sys.exit(missing_config_file % fname) # any config file found at all ? found_any_file = False for f in _cfg.source_files.values(): if f is not None: found_any_file = True break if not found_any_file: _log.error('no config file found at all') sys.exit(no_config_files % '\n '.join(candidates)) # mime type handling sources fname = u'mime_type2file_extension.conf' _cfg.add_file_source ( source = u'user-mime', file = os.path.join(paths.user_config_dir, fname), encoding = enc ) _cfg.add_file_source ( source = u'system-mime', file = os.path.join(paths.system_config_dir, fname), encoding = enc ) #========================================================== def setup_backend(): _log.info('client expects database version [%s]', gmPG2.map_client_branch2required_db_version[current_client_branch]) # set up database connection timezone timezone = _cfg.get ( group = u'backend', option = 'client timezone', source_order = [ ('explicit', 'return'), ('workbase', 'return'), ('local', 'return'), ('user', 'return'), ('system', 'return') ] ) if timezone is not None: gmPG2.set_default_client_timezone(timezone) #========================================================== def shutdown_backend(): gmPG2.shutdown() #========================================================== def shutdown_logging(): # do not choke on Windows logging.raiseExceptions = False #================================================================ # convenience functions #---------------------------------------------------------------- def connect_to_database(login_info=None, max_attempts=3, expected_version=None, require_version=True): """Display the login dialog and try to log into the backend. - up to max_attempts times - returns True/False """ # force programmer to set a valid expected_version expected_hash = gmPG2.known_schema_hashes[expected_version] client_version = _cfg.get(option = u'client_version') global current_db_name current_db_name = u'gnumed_%s' % expected_version attempt = 0 while attempt < max_attempts: _log.debug('login attempt %s of %s', (attempt+1), max_attempts) connected = False login = login_info if login is None: _log.info("did not provide a login information") # try getting a connection to verify the DSN works dsn = gmPG2.make_psycopg2_dsn ( database = login.database, host = login.host, port = login.port, user = login.user, password = login.password ) try: conn = gmPG2.get_raw_connection(dsn = dsn, verbose = True, readonly = True) connected = True except gmPG2.cAuthenticationError, e: attempt += 1 _log.error(u"login attempt failed: %s", e) if attempt < max_attempts: if (u'host=127.0.0.1' in (u'%s' % e)) or (u'host=' not in (u'%s' % e)): msg = _( 'Unable to connect to database:\n\n' '%s\n\n' "Are you sure you have got a local database installed ?\n" '\n' "Please retry with proper credentials or cancel.\n" '\n' 'You may also need to check the PostgreSQL client\n' 'authentication configuration in pg_hba.conf. For\n' 'details see:\n' '\n' 'wiki.gnumed.de/bin/view/Gnumed/ConfigurePostgreSQL' ) else: msg = _( "Unable to connect to database:\n\n" "%s\n\n" "Please retry with proper credentials or cancel.\n" "\n" 'You may also need to check the PostgreSQL client\n' 'authentication configuration in pg_hba.conf. For\n' 'details see:\n' '\n' 'wiki.gnumed.de/bin/view/Gnumed/ConfigurePostgreSQL' ) msg = msg % e msg = regex.sub(r'password=[^\s]+', u'password=%s' % gmTools.u_replacement_character, msg) gmGuiHelpers.gm_show_error ( msg, _('Connecting to backend') ) del e continue except gmPG2.dbapi.OperationalError, e: _log.error(u"login attempt failed: %s", e) msg = _( "Unable to connect to database:\n\n" "%s\n\n" "Please retry another backend / user / password combination !\n" ) % gmPG2.extract_msg_from_pg_exception(e) msg = regex.sub(r'password=[^\s]+', u'password=%s' % gmTools.u_replacement_character, msg) gmGuiHelpers.gm_show_error ( msg, _('Connecting to backend') ) del e continue # connect was successful gmPG2.set_default_login(login = login) #gmPG2.set_default_client_encoding(encoding = dlg.panel.backend_profile.encoding) # compatible = gmPG2.database_schema_compatible(version = expected_version) # if compatible or not require_version: #dlg.panel.save_state() # continue # if not compatible: # connected_db_version = gmPG2.get_schema_version() # msg = msg_generic % ( # client_version, # connected_db_version, # expected_version, # gmTools.coalesce(login.host, ''), # login.database, # login.user # ) # if require_version: # gmGuiHelpers.gm_show_error(msg + msg_fail, _('Verifying database version')) # pass #gmGuiHelpers.gm_show_info(msg + msg_override, _('Verifying database version')) # # FIXME: make configurable # max_skew = 1 # minutes # if _cfg.get(option = 'debug'): # max_skew = 10 # if not gmPG2.sanity_check_time_skew(tolerance = (max_skew * 60)): # if _cfg.get(option = 'debug'): # gmGuiHelpers.gm_show_warning(msg_time_skew_warn % max_skew, _('Verifying database settings')) # else: # gmGuiHelpers.gm_show_error(msg_time_skew_fail % max_skew, _('Verifying database settings')) # continue # sanity_level, message = gmPG2.sanity_check_database_settings() # if sanity_level != 0: # gmGuiHelpers.gm_show_error((msg_insanity % message), _('Verifying database settings')) # if sanity_level == 2: # continue # gmExceptionHandlingWidgets.set_is_public_database(login.public_db) # gmExceptionHandlingWidgets.set_helpdesk(login.helpdesk) listener = gmBackendListener.gmBackendListener(conn = conn) break #dlg.Destroy() return connected #================================================================ class cBackendProfile: pass class Root: #---------------------------------------------------------------------------- #internal helper functions #---------------------------------------------------- def __get_backend_profiles(self): """Get server profiles from the configuration files. 1) from system-wide file 2) from user file Profiles in the user file which have the same name as a profile in the system file will override the system file. """ # find active profiles src_order = [ (u'explicit', u'extend'), (u'system', u'extend'), (u'user', u'extend'), (u'workbase', u'extend') ] profile_names = gmTools.coalesce ( _cfg.get(group = u'backend', option = u'profiles', source_order = src_order), [] ) # find data for active profiles src_order = [ (u'explicit', u'return'), (u'workbase', u'return'), (u'user', u'return'), (u'system', u'return') ] profiles = {} for profile_name in profile_names: # FIXME: once the profile has been found always use the corresponding source ! # FIXME: maybe not or else we cannot override parts of the profile profile = cBackendProfile() profile_section = 'profile %s' % profile_name profile.name = profile_name profile.host = gmTools.coalesce(_cfg.get(profile_section, u'host', src_order), u'').strip() port = gmTools.coalesce(_cfg.get(profile_section, u'port', src_order), 5432) try: profile.port = int(port) if profile.port < 1024: raise ValueError('refusing to use priviledged port (< 1024)') except ValueError: _log.warning('invalid port definition: [%s], skipping profile [%s]', port, profile_name) continue profile.database = gmTools.coalesce(_cfg.get(profile_section, u'database', src_order), u'').strip() if profile.database == u'': _log.warning('database name not specified, skipping profile [%s]', profile_name) continue profile.encoding = gmTools.coalesce(_cfg.get(profile_section, u'encoding', src_order), u'UTF8') profile.public_db = bool(_cfg.get(profile_section, u'public/open access', src_order)) profile.helpdesk = _cfg.get(profile_section, u'help desk', src_order) label = u'%s (address@hidden)' % (profile_name, profile.database, profile.host) profiles[label] = profile # sort out profiles with incompatible database versions if not --debug # NOTE: this essentially hardcodes the database name in production ... if not (_cfg.get(option = 'debug') or current_db_name.endswith('_devel')): profiles2remove = [] for label in profiles: if profiles[label].database != current_db_name: profiles2remove.append(label) for label in profiles2remove: del profiles[label] if len(profiles) == 0: host = u'salaam.homeunix.com' label = u'public GNUmed database (address@hidden)' % (current_db_name, host) profiles[label] = cBackendProfile() profiles[label].name = label profiles[label].host = host profiles[label].port = 5432 profiles[label].database = current_db_name profiles[label].encoding = u'UTF8' profiles[label].public_db = True profiles[label].helpdesk = u'http://wiki.gnumed.de' return profiles def GetLoginInfo(self, username=None, password=None, backend=None ): # username is provided through the web interface # password is provided # we need the profile """convenience function for compatibility with gmLoginInfo.LoginInfo""" #if not self.cancelled: # FIXME: do not assume conf file is latin1 ! #profile = self.__backend_profiles[self._CBOX_profile.GetValue().encode('latin1').strip()] #self.__backend_profiles = self.__get_backend_profiles() self.__backend_profiles = self.__get_backend_profiles() profile = self.__backend_profiles[backend.encode('utf8').strip()] _log.debug(u'backend profile "%s" selected', profile.name) _log.debug(u' details: <%s> on address@hidden:%s (%s, %s)', username, profile.database, profile.host, profile.port, profile.encoding, gmTools.bool2subst(profile.public_db, u'public', u'private') ) #_log.debug(u' helpdesk: "%s"', profile.helpdesk) login = gmLoginInfo.LoginInfo ( user = username, password = password, host = profile.host, database = profile.database, port = profile.port ) #login.public_db = profile.public_db #login.helpdesk = profile.helpdesk return login # ------------------------------------------------------------ def doLogin(self, username=None, password=None, backend=None): login_info = self.GetLoginInfo(username, password, backend) override = _cfg.get(option = '--override-schema-check', source_order = [('cli', 'return')]) connected = connect_to_database ( login_info, expected_version = gmPG2.map_client_branch2required_db_version[_cfg.get(option = 'client_branch')], require_version = not override ) return "You have currently selected the following language in the database: " + self.check_db_lang() doLogin.exposed = True #---------------------------------------------- def get_document_types(self): rows, idx = gmPG2.run_ro_queries ( queries = [{'cmd': u"SELECT * FROM blobs.v_doc_type"}], get_col_idx = True ) doc_types = [] for row in rows: row_def = { 'pk_field': 'pk_doc_type', 'idx': idx, 'data': row } doc_types.append(cDocumentType(row = row_def)) return doc_types #---------------------------------------------- def check_db_lang(self): if gmI18N.system_locale is None or gmI18N.system_locale == '': _log.warning("system locale is undefined (probably meaning 'C')") return True # get current database locale rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': u"select i18n.get_curr_lang() as lang"}]) db_lang = rows[0]['lang'] return db_lang #---------------------------------------------- def index(self): # backend is hardcoded for now, make it use drop down list later return """

Backend

Username

Password

""" index.exposed = True #========================================================== # main - launch the GNUmed wxPython GUI client #---------------------------------------------------------- setup_python_path() setup_logging() from Gnumed.pycommon import gmI18N, gmTools, gmDateTime, gmHooks from Gnumed.pycommon import gmLoginInfo, gmPG2, gmBackendListener, gmTools, gmCfg2, gmI18N _log.info('Starting up as main module (%s).', __version__) _log.info('GNUmed client version [%s] on branch [%s]', current_client_version, current_client_branch) _log.info('Platform: %s', platform.uname()) _log.info('Python %s on %s (%s)', sys.version, sys.platform, os.name) try: import lsb_release _log.info('%s' % lsb_release.get_distro_information()) except ImportError: pass setup_console_exception_handler() setup_cli() setup_signal_handlers() setup_locale() handle_help_request() handle_version_request() setup_paths_and_files() setup_date_time() setup_cfg() cherrypy.quickstart(Root())