#!/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 """
"""
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())