diff --git a/CHANGELOG b/CHANGELOG index cddf7a087e..edd6980426 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add reload of modules if files have changed * Add salt to sha of password * Add strftime to ir.lang to handle locale's format * Add sqlite backend diff --git a/etc/trytond.conf b/etc/trytond.conf index 5ee2171193..42bbcd5f35 100644 --- a/etc/trytond.conf +++ b/etc/trytond.conf @@ -73,3 +73,6 @@ db_type = postgresql # Enable psyco module # Need to have psyco installed http://psyco.sourceforge.net/ #psyco = False + +# Enable auto-reload of modules if changed +#auto_reload = True diff --git a/trytond/config.py b/trytond/config.py index 3ca6583b84..8915f53338 100644 --- a/trytond/config.py +++ b/trytond/config.py @@ -40,6 +40,7 @@ class ConfigManager(object): 'multi_server': False, 'session_timeout': 600, 'psyco': False, + 'auto_reload': True, } parser = optparse.OptionParser(version=VERSION) diff --git a/trytond/modules/__init__.py b/trytond/modules/__init__.py index 9a42fb41d1..726b6ea4e3 100644 --- a/trytond/modules/__init__.py +++ b/trytond/modules/__init__.py @@ -16,13 +16,17 @@ MODULES_PATH = os.path.dirname(__file__) MODULES = [] EGG_MODULES = {} -try: - import pkg_resources - for ep in pkg_resources.iter_entry_points('trytond.modules'): - mod_name = ep.module_name.split('.')[-1] - EGG_MODULES[mod_name] = ep -except ImportError: - pass + +def update_egg_modules(): + global EGG_MODULES + try: + import pkg_resources + for ep in pkg_resources.iter_entry_points('trytond.modules'): + mod_name = ep.module_name.split('.')[-1] + EGG_MODULES[mod_name] = ep + except ImportError: + pass +update_egg_modules() class Graph(dict): @@ -303,6 +307,7 @@ def get_module_list(): module_list.add(file) elif file[-4:] == '.zip': module_list.add(file[:-4]) + update_egg_modules() module_list.update(EGG_MODULES.keys()) module_list.add('ir') module_list.add('workflow') @@ -311,12 +316,27 @@ def get_module_list(): module_list.add('tests') return list(module_list) -def register_classes(): - import trytond.ir - import trytond.workflow - import trytond.res - import trytond.webdav - import trytond.tests +def register_classes(reload_p=False): + ''' + Import modules to register the classes in the Pool + + :param reload_p: reload modules instead of import it + ''' + if not reload_p: + import trytond.ir + import trytond.workflow + import trytond.res + import trytond.webdav + import trytond.tests + else: + for module in ('trytond.model', 'trytond.report', 'trytond.wizard', + 'trytond.ir', 'trytond.workflow', 'trytond.res', + 'trytond.webdav', 'trytond.tests'): + for i in sys.modules.keys(): + if i.startswith(module) \ + and i != module: + del sys.modules[i] + reload(sys.modules[module]) logger = logging.getLogger('modules') @@ -328,6 +348,14 @@ def register_classes(): MODULES.append(module) continue + if reload_p and 'trytond.modules.' + module in sys.modules: + for i in sys.modules.keys(): + if i.startswith('trytond.modules.' + module) \ + and i != 'trytond.modules.' + module: + del sys.modules[i] + reload(sys.modules['trytond.modules.' + module]) + continue + if os.path.isfile(OPJ(MODULES_PATH, module + '.zip')): mod_path = OPJ(MODULES_PATH, module + '.zip') zimp = zipimport.zipimporter(mod_path) diff --git a/trytond/monitor.py b/trytond/monitor.py new file mode 100644 index 0000000000..6ba2f41248 --- /dev/null +++ b/trytond/monitor.py @@ -0,0 +1,68 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. +import sys +import os +import subprocess +from threading import Lock +from trytond.modules import get_module_list + +_lock = Lock() +_times = {} +_modules = None + +def _modified(path): + global _times + global _lock + _lock.acquire() + try: + try: + if not os.path.isfile(path): + return path in _times + + mtime = os.stat(path).st_mtime + if path not in _times: + _times[path] = mtime + + if mtime != _times[path]: + _times[path] = mtime + return True + except: + return True + finally: + _lock.release() + return False + +def monitor(): + ''' + Monitor module files for change + + :return: True if at least one file has changed + ''' + global _modules + modified = False + for module in sys.modules.keys(): + if not module.startswith('trytond.'): + continue + if not hasattr(sys.modules[module], '__file__'): + continue + path = getattr(sys.modules[module], '__file__') + if not path: + continue + if os.path.splitext(path)[1] in ['.pyc', '.pyo', '.pyd']: + path = path[:-1] + if _modified(path): + if subprocess.call((sys.executable, '-c', 'import %s' % module)): + modified = False + break + modified = True + modules = set(get_module_list()) + if _modules is None: + _modules = modules + for module in modules.difference(_modules): + if subprocess.call((sys.executable, '-c', + 'import trytond.modules.%s' % module)): + modified = False + break + modified = True + _modules = modules + return modified diff --git a/trytond/pool.py b/trytond/pool.py index 650ce6be60..c3c0f7b86d 100644 --- a/trytond/pool.py +++ b/trytond/pool.py @@ -1,6 +1,6 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. -from trytond.modules import load_modules +from trytond.modules import load_modules, register_classes from threading import RLock import logging @@ -54,6 +54,28 @@ class Pool(object): break Pool.classes[type].setdefault(module, []).append(cls) + @staticmethod + def start(): + ''' + Start/restart the Pool + ''' + Pool._lock.acquire() + try: + reload_p = False + for type in Pool.classes: + if Pool.classes[type]: + reload_p = True + Pool.classes[type] = {} + try: + register_classes(reload_p=reload_p) + except: + if not reload_p: + raise + for db_name in Pool.database_list(): + Pool(db_name).init() + finally: + Pool._lock.release() + @staticmethod def database_list(): ''' diff --git a/trytond/protocols/dispatcher.py b/trytond/protocols/dispatcher.py index d3ac1fa17f..2c64147300 100644 --- a/trytond/protocols/dispatcher.py +++ b/trytond/protocols/dispatcher.py @@ -7,6 +7,7 @@ from trytond.tools import Cache, find_language_context from trytond.backend import Database, DatabaseIntegrityError from trytond.config import CONFIG from trytond.version import VERSION +from trytond.monitor import monitor import traceback import logging import time @@ -22,6 +23,9 @@ import pydoc def dispatch(host, port, protocol, database_name, user, session, object_type, object_name, method, *args, **kargs): + if CONFIG['auto_reload'] and monitor(): + Pool.start() + if object_type == 'common': if method == 'login': res = security.login(database_name, user, session) diff --git a/trytond/server.py b/trytond/server.py index 72958239c7..00767bd852 100644 --- a/trytond/server.py +++ b/trytond/server.py @@ -12,7 +12,6 @@ import sys, os, signal import time from trytond.backend import Database, DatabaseOperationalError from config import CONFIG -from trytond.modules import register_classes import mx.DateTime if not hasattr(mx.DateTime, 'strptime'): @@ -29,6 +28,7 @@ import threading from pool import Pool import string import random +from trytond.monitor import monitor class TrytonServer(object): @@ -98,7 +98,7 @@ class TrytonServer(object): elif not cursor.test(): raise Exception("'%s' is not a Tryton database!" % db_name) - register_classes() + Pool.start() for db_name in CONFIG["db_name"]: cursor = Database(db_name).connect().cursor() @@ -175,8 +175,7 @@ class TrytonServer(object): def handler(signum, frame): if hasattr(signal, 'SIGUSR1'): if signum == signal.SIGUSR1: - for db_name in Pool.database_list(): - Pool(db_name).init() + Pool.start() return if CONFIG['netrpc']: netrpcd.stop() @@ -220,6 +219,11 @@ class TrytonServer(object): psyco.full() while True: + if CONFIG['auto_reload'] and monitor(): + try: + Pool.start() + except: + pass for dbname in Pool.database_list(): pool = Pool(dbname) if 'ir.cron' not in pool.object_name_list():