autoreload, first working version for both python and data, still wip

bzr revid: al@openerp.com-20130909230553-jn26ue5qenv0sd3p
This commit is contained in:
Antony Lesuisse 2013-09-10 01:05:53 +02:00
parent 3bc9a499d4
commit 65c0538a83
1 changed files with 107 additions and 10 deletions

View File

@ -41,6 +41,97 @@ SLEEP_INTERVAL = 60 # 1 min
# Common # Common
#---------------------------------------------------------- #----------------------------------------------------------
class AutoReload(object):
def __init__(self):
self.files = {}
import pyinotify
class EventHandler(pyinotify.ProcessEvent):
def __init__(self, autoreload):
self.autoreload = autoreload
def process_IN_CREATE(self, event):
_logger.debug('File created: %s', event.pathname)
self.autoreload.files[event.pathname] = 1
def process_IN_MODIFY(self, event):
_logger.debug('File modified: %s', event.pathname)
self.autoreload.files[event.pathname] = 1
self.wm = pyinotify.WatchManager()
self.handler = EventHandler(self)
self.notifier = pyinotify.Notifier(self.wm, self.handler, timeout=0)
mask = pyinotify.IN_MODIFY | pyinotify.IN_CREATE # IN_MOVED_FROM, IN_MOVED_TO ?
for path in openerp.tools.config.options["addons_path"].split(','):
_logger.info('Watching addons folder %s', path)
self.wm.add_watch(path, mask, rec=True)
def process_data(self, touched_files):
# pyinotify notifier + fs modiciation tracker
from openerp.modules.module import load_information_from_description_file as load_manifest
addons_path = openerp.tools.config.options["addons_path"].split(',')
registries = openerp.modules.registry.RegistryManager.registries
keys = ['data', 'demo', 'test', 'init_xml', 'update_xml', 'demo_xml']
# This will only work for loaded registies, so we have to use -d <database>
# al: proposed to move this code in the registry manager so it can be lazy
for db_name, registry in registries.items():
cr = registry.db.cursor()
try:
for tfile in touched_files:
for path in addons_path:
if tfile.startswith(path):
# find out wich addons path the file belongs to
# and extract it's module name
right = tfile[len(path) + 1:].split('/')
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):
# process python changes
py_files = [i for i in files if i.endswith('.py')]
py_errors = []
# TODO keep python errors until they are ok
if py_files:
for i in py_files:
try:
source = open(i, 'rb').read() + '\n'
compile(source, i, 'exec')
except SyntaxError:
py_errors.append(i)
if py_errors:
_logger.info('autoreload: python code change detected, errors found')
for i in py_errors:
_logger.info('autoreload: SyntaxError %s',i)
else:
_logger.info('autoreload: python code updated, autoreload activated')
restart_server()
def check(self):
# Check if some files have been touched in the addons path.
# If true, check if the touched file belongs to an installed module
# in any of the database used in the registry manager.
while self.notifier.check_events(0):
self.notifier.read_events()
self.notifier.process_events()
l = self.files.keys()
self.files.clear()
self.process_data(l)
self.process_python(l)
class CommonServer(object): class CommonServer(object):
def __init__(self, app): def __init__(self, app):
# TODO Change the xmlrpc_* options to http_* # TODO Change the xmlrpc_* options to http_*
@ -101,8 +192,8 @@ class ThreadedServer(CommonServer):
self.quit_signals_received = 0 self.quit_signals_received = 0
#self.socket = None #self.socket = None
#self.queue = []
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]:
@ -152,7 +243,11 @@ class ThreadedServer(CommonServer):
_logger.debug("cron%d started!" % i) _logger.debug("cron%d started!" % i)
def http_thread(self): def http_thread(self):
self.httpd = werkzeug.serving.make_server(self.interface, self.port, self.app, threaded=True) def app(e,s):
if self.autoreload:
self.autoreload.check()
return self.app(e,s)
self.httpd = werkzeug.serving.make_server(self.interface, self.port, app, threaded=True)
self.httpd.serve_forever() self.httpd.serve_forever()
def http_spawn(self): def http_spawn(self):
@ -172,6 +267,7 @@ 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.
@ -189,7 +285,7 @@ class ThreadedServer(CommonServer):
_logger.debug('current thread: %r', me) _logger.debug('current thread: %r', me)
for thread in threading.enumerate(): for thread in threading.enumerate():
_logger.debug('process %r (%r)', thread, thread.isDaemon()) _logger.debug('process %r (%r)', thread, thread.isDaemon())
if thread != me and not thread.isDaemon() and thread.ident != main_thread_id: if thread != me and not thread.isDaemon() and thread.ident != self.main_thread_id:
while thread.isAlive(): while thread.isAlive():
_logger.debug('join and sleep') _logger.debug('join and sleep')
# Need a busyloop here as thread.join() masks signals # Need a busyloop here as thread.join() masks signals
@ -262,11 +358,11 @@ class GeventServer(CommonServer):
# Prefork # Prefork
#---------------------------------------------------------- #----------------------------------------------------------
class Multicorn(CommonServer): class PreforkServer(CommonServer):
""" Multiprocessing inspired by (g)unicorn. """ Multiprocessing inspired by (g)unicorn.
Multicorn currently uses accept(2) as dispatching method between workers PreforkServer (aka Multicorn) currently uses accept(2) as dispatching
but we plan to replace it by a more intelligent dispatcher to will parse method between workers but we plan to replace it by a more intelligent
the first HTTP request line. dispatcher to will parse the first HTTP request line.
""" """
def __init__(self, app): def __init__(self, app):
# config # config
@ -617,12 +713,12 @@ class WorkerBaseWSGIServer(werkzeug.serving.BaseWSGIServer):
def __init__(self, app): def __init__(self, app):
werkzeug.serving.BaseWSGIServer.__init__(self, "1", "1", app) werkzeug.serving.BaseWSGIServer.__init__(self, "1", "1", app)
def server_bind(self): def server_bind(self):
# we dont bind beause we use the listen socket of Multicorn#socket # we dont bind beause we use the listen socket of PreforkServer#socket
# instead we close the socket # instead we close the socket
if self.socket: if self.socket:
self.socket.close() self.socket.close()
def server_activate(self): def server_activate(self):
# dont listen as we use Multicorn#socket # dont listen as we use PreforkServer#socket
pass pass
class WorkerCron(Worker): class WorkerCron(Worker):
@ -723,10 +819,11 @@ def _reexec():
def start(): def start():
""" Start the openerp http server and cron processor. """ Start the openerp http server and cron processor.
""" """
global server
load_server_wide_modules() load_server_wide_modules()
if config['workers']: if config['workers']:
openerp.multi_process = True openerp.multi_process = True
server = Multicorn(openerp.service.wsgi_server.application) server = PreforkServer(openerp.service.wsgi_server.application)
elif openerp.evented: elif openerp.evented:
server = GeventServer(openerp.service.wsgi_server.application) server = GeventServer(openerp.service.wsgi_server.application)
else: else: