From 50dca46b0fe695a518600f652c2dbdf06cddf055 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 5 Jan 2010 16:23:12 +0100 Subject: [PATCH] [FIX] Made timezone detect more robust and added a config option to override it. Stay backwards-compatible by using time.tzname[0] as we did before if no config option is provided, but only if it is a valid timezone (in the pytz sense). If no config option is set and time.tzname does't work, fallback, in this order, to: - environment variable 'TZ' - /etc/timezone on Unix platforms - Windows registry on win32 systems - if nothing works, default to UTC (because we MUST provide a valid timezone to clients) lp bug: https://launchpad.net/bugs/389544 fixed bzr revid: odo@openerp.com-20100105152312-fgayeaks12ed3no8 --- bin/service/web_services.py | 5 ++- bin/tools/config.py | 17 +++++++- bin/tools/misc.py | 83 +++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/bin/service/web_services.py b/bin/service/web_services.py index ec554ac0325..acfe1bfd41e 100644 --- a/bin/service/web_services.py +++ b/bin/service/web_services.py @@ -443,7 +443,10 @@ GNU Public Licence. return info def exp_timezone_get(self, db, login, password): - return time.tzname[0] + #timezone detection is safe in multithread, so lazy init is ok here + if (not tools.config['timezone']): + tools.config['timezone'] = tools.misc.detect_server_timezone() + return tools.config['timezone'] def exp_get_available_updates(self, contract_id, contract_password): diff --git a/bin/tools/config.py b/bin/tools/config.py index 4cdee8736c2..e0ef74dbb56 100644 --- a/bin/tools/config.py +++ b/bin/tools/config.py @@ -77,10 +77,11 @@ class configmanager(object): 'secure' : False, 'syslog' : False, 'log_level': logging.INFO, - 'assert_exit_level': logging.WARNING, # level above which a failed assert will be raise + 'assert_exit_level': logging.WARNING, # level above which a failed assert will be raised 'cache_timeout': 100000, 'login_message': False, 'list_db' : True, + 'timezone' : False, # to override the default TZ } self.misc = {} @@ -111,6 +112,7 @@ class configmanager(object): help="update a module (use \"all\" for all modules)") parser.add_option("--cache-timeout", dest="cache_timeout", help="set the timeout for the cache system", default=100000, type="int") + parser.add_option("-t", "--timezone", dest="timezone", help="specify reference timezone for the server (e.g. Europe/Brussels") # stops the server from launching after initialization parser.add_option("--stop-after-init", action="store_true", dest="stop_after_init", default=False, @@ -227,7 +229,7 @@ class configmanager(object): 'db_port', 'list_db', 'logfile', 'pidfile', 'smtp_port', 'cache_timeout', 'email_from', 'smtp_server', 'smtp_user', 'smtp_password', 'price_accuracy', 'netinterface', 'netport', 'db_maxconn', 'import_partial', 'addons_path', - 'netrpc', 'xmlrpc', 'syslog', 'without_demo'] + 'netrpc', 'xmlrpc', 'syslog', 'without_demo', 'timezone',] if hasSSL: keys.extend(['smtp_ssl', 'secure_cert_file', 'secure_pkey_file']) @@ -266,6 +268,17 @@ class configmanager(object): self.options['translate_modules'] = opt.translate_modules and map(lambda m: m.strip(), opt.translate_modules.split(',')) or ['all'] self.options['translate_modules'].sort() + if self.options['timezone']: + # If an explicit TZ was provided in the config, make sure it is known + try: + import pytz + tz = pytz.timezone(self.options['timezone']) + except pytz.UnknownTimeZoneError: + die(True, "The specified timezone (%s) is invalid" % self.options['timezone']) + except: + # If pytz is missing, don't check the provided TZ, it will be ignored anyway. + pass + if opt.pg_path: self.options['pg_path'] = opt.pg_path diff --git a/bin/tools/misc.py b/bin/tools/misc.py index 001f62e0968..331649b53c7 100644 --- a/bin/tools/misc.py +++ b/bin/tools/misc.py @@ -1174,6 +1174,89 @@ def detect_ip_addr(): ip_addr = 'localhost' return ip_addr +# RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT: +# The server side never does any timestamp calculation, always +# sends them in a naive (timezone agnostic) format supposed to be +# expressed within the server timezone, and expects the clients to +# provide timestamps in the server timezone as well. +# It stores all timestamps in the database in naive format as well, +# which also expresses the time in the server timezone. +# For this reason the server makes its timezone name available via the +# common/timezone_get() rpc method, which clients need to read +# to know the appropriate time offset to use when reading/writing +# times. +def get_win32_timezone(): + """Attempt to return the "standard name" of the current timezone on a win32 system. + @return: the standard name of the current win32 timezone, or False if it cannot be found. + """ + res = False + if (sys.platform == "win32"): + try: + import _winreg + hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE) + current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS) + res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0]) # [0] is value, [1] is type code + _winreg.CloseKey(current_tz_key) + _winreg.CloseKey(hklm) + except: + pass + return res + +def detect_server_timezone(): + """Attempt to detect the timezone to use on the server side. + Defaults to UTC if no working timezone can be found. + @return: the timezone identifier as expected by pytz.timezone. + """ + import time + import netsvc + try: + import pytz + except: + netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING, + "Python pytz module is not available. Timezone will be set to UTC by default.") + return 'UTC' + + # Option 1: the configuration option (did not exist before, so no backwards compatibility issue) + # Option 2: to be backwards compatible with 5.0 or earlier, the value from time.tzname[0], but only if it is known to pytz + # Option 3: the environment variable TZ + sources = [ (config['timezone'], 'OpenERP configuration'), + (time.tzname[0], 'time.tzname'), + (os.environ.get('TZ',False),'TZ environment variable'), ] + # Option 4: OS-specific: /etc/timezone on Unix + if (os.path.exists("/etc/timezone")): + tz_value = False + try: + f = open("/etc/timezone") + tz_value = f.read(128).strip() + except: + pass + finally: + f.close() + sources.append((tz_value,"/etc/timezone file")) + # Option 5: timezone info from registry on Win32 + if (sys.platform == "win32"): + # Timezone info is stored in windows registry. + # However this is not likely to work very well as the standard name + # of timezones in windows is rarely something that is known to pytz. + # But that's ok, it is always possible to use a config option to set + # it explicitly. + sources.append((get_win32_tz(),"Windows Registry")) + + for (value,source) in sources: + if value: + try: + tz = pytz.timezone(value) + netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_INFO, + "Using timezone %s obtained from %s." % (tz.zone,source)) + return value + except pytz.UnknownTimeZoneError: + netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING, + "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value)) + + netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING, + "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.") + return 'UTC' + if __name__ == '__main__': import doctest