simplier autoreload working

bzr revid: al@openerp.com-20131005212240-5lp8tgwukeg5wwdq
This commit is contained in:
Antony Lesuisse 2013-10-05 23:22:40 +02:00
parent 082b1dc9fc
commit 0a75a6b5ff
3 changed files with 103 additions and 87 deletions

View File

@ -726,7 +726,7 @@ class module(osv.osv):
if already_installed: if already_installed:
# in this case, force server restart to reload python code... # in this case, force server restart to reload python code...
cr.commit() cr.commit()
openerp.service.restart_server() openerp.service.restart()
return { return {
'type': 'ir.actions.client', 'type': 'ir.actions.client',
'tag': 'home', 'tag': 'home',

View File

@ -6,21 +6,19 @@ import errno
import fcntl import fcntl
import logging import logging
import os import os
import os.path
import platform
import psutil import psutil
import random import random
import resource import resource
import select import select
import signal import signal
import socket import socket
import subprocess
import sys import sys
import threading import threading
import time import time
import traceback import traceback
import subprocess
import os.path
import platform
import wsgi_server
import werkzeug.serving import werkzeug.serving
try: try:
@ -33,17 +31,60 @@ import openerp.tools.config as config
from openerp.release import nt_service_name from openerp.release import nt_service_name
from openerp.tools.misc import stripped_sys_argv from openerp.tools.misc import stripped_sys_argv
import wsgi_server
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
SLEEP_INTERVAL = 60 # 1 min SLEEP_INTERVAL = 60 # 1 min
#---------------------------------------------------------- #----------------------------------------------------------
# Common # Werkzeug WSGI servers patched
#----------------------------------------------------------
class BaseWSGIServerNoBind(werkzeug.serving.BaseWSGIServer):
""" werkzeug Base WSGI Server patched to skip socket binding. PreforkServer
use this class, sets the socket and calls the process_request() manually
"""
def __init__(self, app):
werkzeug.serving.BaseWSGIServer.__init__(self, "1", "1", app)
def server_bind(self):
# we dont bind beause we use the listen socket of PreforkServer#socket
# instead we close the socket
if self.socket:
self.socket.close()
def server_activate(self):
# dont listen as we use PreforkServer#socket
pass
# MAybe NOT useful BECAUSE of SOCKET_REUSE, need to test
class ThreadedWSGIServerReloadable(werkzeug.serving.ThreadedWSGIServer):
""" werkzeug Threaded WSGI Server patched to allow reusing a listen socket
given by the environement, this is used by autoreload to keep the listen
socket open when a reload happens.
"""
def server_bind(self):
envfd = os.environ.get('OPENERP_AUTO_RELOAD_FD')
if envfd:
self.reload_socket = socket.fromfd
# close os.close()fd if fd has been diplucated ?!
else:
self.reload_socket = False
super(ThreadedWSGIServerReloadable, self).server_bind()
def server_activate(self):
if not self.reload_socket:
super(ThreadedWSGIServerReloadable, self).server_activate()
#----------------------------------------------------------
# AutoReload watcher
#---------------------------------------------------------- #----------------------------------------------------------
class AutoReload(object): class AutoReload(object):
def __init__(self): def __init__(self, server):
self.server = server
self.files = {} self.files = {}
self.modules = {}
import pyinotify import pyinotify
class EventHandler(pyinotify.ProcessEvent): class EventHandler(pyinotify.ProcessEvent):
def __init__(self, autoreload): def __init__(self, autoreload):
@ -65,40 +106,22 @@ class AutoReload(object):
_logger.info('Watching addons folder %s', path) _logger.info('Watching addons folder %s', path)
self.wm.add_watch(path, mask, rec=True) self.wm.add_watch(path, mask, rec=True)
def process_data(self, touched_files): def process_data(self, files):
# pyinotify notifier + fs modiciation tracker xml_files = [i for i in files if i.endswith('.xml')]
from openerp.modules.module import load_information_from_description_file as load_manifest
addons_path = openerp.tools.config.options["addons_path"].split(',') addons_path = openerp.tools.config.options["addons_path"].split(',')
registries = openerp.modules.registry.RegistryManager.registries for i in xml_files:
keys = ['data', 'demo', 'test', 'init_xml', 'update_xml', 'demo_xml'] for path in addons_path:
# This will only work for loaded registies, so we have to use -d <database> if i.startswith(path):
# al: proposed to move this code in the registry manager so it can be lazy # find out wich addons path the file belongs to
for db_name, registry in registries.items(): # and extract it's module name
cr = registry.db.cursor() right = i[len(path) + 1:].split('/')
try: if len(right) < 2:
for tfile in touched_files: continue
for path in addons_path: module = right[0]
if tfile.startswith(path): self.modules[module]=1
# find out wich addons path the file belongs to if self.modules:
# and extract it's module name _logger.info('autoreload: xml change detected, autoreload activated')
right = tfile[len(path) + 1:].split('/') restart()
if len(right) < 2:
continue
module = right[0]
relname = "/".join(right[1:])
domain = [('name', '=', module), ('state', 'in', ['installed', 'to upgrade'])]
if registry.get('ir.module.module').search(cr, openerp.SUPERUSER_ID, domain):
manifest = load_manifest(module)
kind = [key for key in keys if relname in manifest[key]]
if kind:
_logger.info('Updating changed xml file: %s', tfile)
idref = {}
openerp.tools.convert_file(cr, module, relname, idref, mode='update', kind=kind[0])
cr.commit()
except Exception,e:
_logger.exception(e)
finally:
cr.close()
def process_python(self, files): def process_python(self, files):
# process python changes # process python changes
@ -118,19 +141,30 @@ class AutoReload(object):
_logger.info('autoreload: SyntaxError %s',i) _logger.info('autoreload: SyntaxError %s',i)
else: else:
_logger.info('autoreload: python code updated, autoreload activated') _logger.info('autoreload: python code updated, autoreload activated')
restart_server() restart()
def check(self): def check_thread(self):
# Check if some files have been touched in the addons path. # Check if some files have been touched in the addons path.
# If true, check if the touched file belongs to an installed module # If true, check if the touched file belongs to an installed module
# in any of the database used in the registry manager. # in any of the database used in the registry manager.
while self.notifier.check_events(0): while 1:
self.notifier.read_events() while self.notifier.check_events(1000):
self.notifier.process_events() self.notifier.read_events()
l = self.files.keys() self.notifier.process_events()
self.files.clear() l = self.files.keys()
self.process_data(l) self.files.clear()
self.process_python(l) self.process_data(l)
self.process_python(l)
def run(self):
t = threading.Thread(target=self.check_thread)
t.setDaemon(True)
t.start()
_logger.info('AutoReload watcher running')
#----------------------------------------------------------
# Servers: Threaded, Gevented and Prefork
#----------------------------------------------------------
class CommonServer(object): class CommonServer(object):
def __init__(self, app): def __init__(self, app):
@ -179,10 +213,6 @@ class CommonServer(object):
raise raise
sock.close() sock.close()
#----------------------------------------------------------
# Threaded
#----------------------------------------------------------
class ThreadedServer(CommonServer): class ThreadedServer(CommonServer):
def __init__(self, app): def __init__(self, app):
super(ThreadedServer, self).__init__(app) super(ThreadedServer, self).__init__(app)
@ -193,7 +223,6 @@ class ThreadedServer(CommonServer):
#self.socket = None #self.socket = None
self.httpd = None self.httpd = None
self.autoreload = None
def signal_handler(self, sig, frame): def signal_handler(self, sig, frame):
if sig in [signal.SIGINT,signal.SIGTERM]: if sig in [signal.SIGINT,signal.SIGTERM]:
@ -244,10 +273,8 @@ class ThreadedServer(CommonServer):
def http_thread(self): def http_thread(self):
def app(e,s): def app(e,s):
if self.autoreload:
self.autoreload.check()
return self.app(e,s) return self.app(e,s)
self.httpd = werkzeug.serving.make_server(self.interface, self.port, app, threaded=True) self.httpd = ThreadedWSGIServerReloadable(self.interface, self.port, app)
self.httpd.serve_forever() self.httpd.serve_forever()
def http_spawn(self): def http_spawn(self):
@ -267,7 +294,6 @@ class ThreadedServer(CommonServer):
win32api.SetConsoleCtrlHandler(lambda sig: signal_handler(sig, None), 1) win32api.SetConsoleCtrlHandler(lambda sig: signal_handler(sig, None), 1)
self.cron_spawn() self.cron_spawn()
self.http_spawn() self.http_spawn()
self.autoreload = AutoReload()
def stop(self): def stop(self):
""" Shutdown the WSGI server. Wait for non deamon threads. """ Shutdown the WSGI server. Wait for non deamon threads.
@ -315,9 +341,8 @@ class ThreadedServer(CommonServer):
self.stop() self.stop()
#---------------------------------------------------------- def reload(self):
# Gevent os.kill(self.pid, signal.SIGHUP)
#----------------------------------------------------------
class GeventServer(CommonServer): class GeventServer(CommonServer):
def __init__(self, app): def __init__(self, app):
@ -354,10 +379,6 @@ class GeventServer(CommonServer):
self.start() self.start()
self.stop() self.stop()
#----------------------------------------------------------
# Prefork
#----------------------------------------------------------
class PreforkServer(CommonServer): class PreforkServer(CommonServer):
""" Multiprocessing inspired by (g)unicorn. """ Multiprocessing inspired by (g)unicorn.
PreforkServer (aka Multicorn) currently uses accept(2) as dispatching PreforkServer (aka Multicorn) currently uses accept(2) as dispatching
@ -684,7 +705,7 @@ class WorkerHTTP(Worker):
# Prevent fd inherientence close_on_exec # Prevent fd inherientence close_on_exec
flags = fcntl.fcntl(client, fcntl.F_GETFD) | fcntl.FD_CLOEXEC flags = fcntl.fcntl(client, fcntl.F_GETFD) | fcntl.FD_CLOEXEC
fcntl.fcntl(client, fcntl.F_SETFD, flags) fcntl.fcntl(client, fcntl.F_SETFD, flags)
# do request using WorkerBaseWSGIServer monkey patched with socket # do request using BaseWSGIServerNoBind monkey patched with socket
self.server.socket = client self.server.socket = client
# tolerate broken pipe when the http client closes the socket before # tolerate broken pipe when the http client closes the socket before
# receiving the full reply # receiving the full reply
@ -705,21 +726,7 @@ class WorkerHTTP(Worker):
def start(self): def start(self):
Worker.start(self) Worker.start(self)
self.server = WorkerBaseWSGIServer(self.multi.app) self.server = BaseWSGIServerNoBind(self.multi.app)
class WorkerBaseWSGIServer(werkzeug.serving.BaseWSGIServer):
""" werkzeug WSGI Server patched to allow using an external listen socket
"""
def __init__(self, app):
werkzeug.serving.BaseWSGIServer.__init__(self, "1", "1", app)
def server_bind(self):
# we dont bind beause we use the listen socket of PreforkServer#socket
# instead we close the socket
if self.socket:
self.socket.close()
def server_activate(self):
# dont listen as we use PreforkServer#socket
pass
class WorkerCron(Worker): class WorkerCron(Worker):
""" Cron workers """ """ Cron workers """
@ -806,12 +813,13 @@ The `web` module is provided by the addons found in the `openerp-web` project.
Maybe you forgot to add those addons in your addons_path configuration.""" Maybe you forgot to add those addons in your addons_path configuration."""
_logger.exception('Failed to load server-wide module `%s`.%s', m, msg) _logger.exception('Failed to load server-wide module `%s`.%s', m, msg)
def _reexec(): def _reexec(updated_modules=None):
"""reexecute openerp-server process with (nearly) the same arguments""" """reexecute openerp-server process with (nearly) the same arguments"""
if openerp.tools.osutil.is_running_as_nt_service(): if openerp.tools.osutil.is_running_as_nt_service():
subprocess.call('net stop {0} && net start {0}'.format(nt_service_name), shell=True) subprocess.call('net stop {0} && net start {0}'.format(nt_service_name), shell=True)
exe = os.path.basename(sys.executable) exe = os.path.basename(sys.executable)
args = stripped_sys_argv() args = stripped_sys_argv()
args += ["-u", ','.join(updated_modules)]
if not args or args[0] != exe: if not args or args[0] != exe:
args.insert(0, exe) args.insert(0, exe)
os.execv(sys.executable, args) os.execv(sys.executable, args)
@ -828,14 +836,22 @@ def start():
server = GeventServer(openerp.service.wsgi_server.application) server = GeventServer(openerp.service.wsgi_server.application)
else: else:
server = ThreadedServer(openerp.service.wsgi_server.application) server = ThreadedServer(openerp.service.wsgi_server.application)
if config['auto_reload']:
autoreload = AutoReload(server)
autoreload.run()
server.run() server.run()
# like the legend of the phoenix, all ends with beginnings # like the legend of the phoenix, all ends with beginnings
if getattr(openerp, 'phoenix', False): if getattr(openerp, 'phoenix', False):
_reexec() modules = []
if config['auto_reload']:
modules = autoreload.modules.keys()
_reexec(modules)
sys.exit(0) sys.exit(0)
def restart_server(): def restart():
""" Restart the server """ Restart the server
""" """
if os.name == 'nt': if os.name == 'nt':
@ -844,5 +860,4 @@ def restart_server():
else: else:
os.kill(server.pid, signal.SIGHUP) os.kill(server.pid, signal.SIGHUP)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -246,6 +246,7 @@ class configmanager(object):
# Advanced options # Advanced options
group = optparse.OptionGroup(parser, "Advanced options") group = optparse.OptionGroup(parser, "Advanced options")
group.add_option('--auto-reload', dest='auto_reload', action='store_true', my_default=False, help='enable auto reload')
group.add_option('--debug', dest='debug_mode', action='store_true', my_default=False, help='enable debug mode') group.add_option('--debug', dest='debug_mode', action='store_true', my_default=False, help='enable debug mode')
group.add_option("--stop-after-init", action="store_true", dest="stop_after_init", my_default=False, group.add_option("--stop-after-init", action="store_true", dest="stop_after_init", my_default=False,
help="stop the server after its initialization") help="stop the server after its initialization")
@ -399,7 +400,7 @@ class configmanager(object):
'list_db', 'xmlrpcs', 'proxy_mode', 'list_db', 'xmlrpcs', 'proxy_mode',
'test_file', 'test_enable', 'test_commit', 'test_report_directory', 'test_file', 'test_enable', 'test_commit', 'test_report_directory',
'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent', 'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent',
'workers', 'limit_memory_hard', 'limit_memory_soft', 'limit_time_cpu', 'limit_time_real', 'limit_request', 'dev' 'workers', 'limit_memory_hard', 'limit_memory_soft', 'limit_time_cpu', 'limit_time_real', 'limit_request', 'dev', 'auto_reload'
] ]
for arg in keys: for arg in keys: