Skip to content

Commit 65c0538

Browse files
autoreload, first working version for both python and data, still wip
bzr revid: [email protected]
1 parent 3bc9a49 commit 65c0538

File tree

1 file changed

+107
-10
lines changed

1 file changed

+107
-10
lines changed

openerp/service/workers.py

+107-10
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,97 @@
4141
# Common
4242
#----------------------------------------------------------
4343

44+
class AutoReload(object):
45+
def __init__(self):
46+
self.files = {}
47+
import pyinotify
48+
class EventHandler(pyinotify.ProcessEvent):
49+
def __init__(self, autoreload):
50+
self.autoreload = autoreload
51+
52+
def process_IN_CREATE(self, event):
53+
_logger.debug('File created: %s', event.pathname)
54+
self.autoreload.files[event.pathname] = 1
55+
56+
def process_IN_MODIFY(self, event):
57+
_logger.debug('File modified: %s', event.pathname)
58+
self.autoreload.files[event.pathname] = 1
59+
60+
self.wm = pyinotify.WatchManager()
61+
self.handler = EventHandler(self)
62+
self.notifier = pyinotify.Notifier(self.wm, self.handler, timeout=0)
63+
mask = pyinotify.IN_MODIFY | pyinotify.IN_CREATE # IN_MOVED_FROM, IN_MOVED_TO ?
64+
for path in openerp.tools.config.options["addons_path"].split(','):
65+
_logger.info('Watching addons folder %s', path)
66+
self.wm.add_watch(path, mask, rec=True)
67+
68+
def process_data(self, touched_files):
69+
# pyinotify notifier + fs modiciation tracker
70+
from openerp.modules.module import load_information_from_description_file as load_manifest
71+
addons_path = openerp.tools.config.options["addons_path"].split(',')
72+
registries = openerp.modules.registry.RegistryManager.registries
73+
keys = ['data', 'demo', 'test', 'init_xml', 'update_xml', 'demo_xml']
74+
# This will only work for loaded registies, so we have to use -d <database>
75+
# al: proposed to move this code in the registry manager so it can be lazy
76+
for db_name, registry in registries.items():
77+
cr = registry.db.cursor()
78+
try:
79+
for tfile in touched_files:
80+
for path in addons_path:
81+
if tfile.startswith(path):
82+
# find out wich addons path the file belongs to
83+
# and extract it's module name
84+
right = tfile[len(path) + 1:].split('/')
85+
if len(right) < 2:
86+
continue
87+
module = right[0]
88+
relname = "/".join(right[1:])
89+
domain = [('name', '=', module), ('state', 'in', ['installed', 'to upgrade'])]
90+
if registry.get('ir.module.module').search(cr, openerp.SUPERUSER_ID, domain):
91+
manifest = load_manifest(module)
92+
kind = [key for key in keys if relname in manifest[key]]
93+
if kind:
94+
_logger.info('Updating changed xml file: %s', tfile)
95+
idref = {}
96+
openerp.tools.convert_file(cr, module, relname, idref, mode='update', kind=kind[0])
97+
cr.commit()
98+
except Exception,e:
99+
_logger.exception(e)
100+
finally:
101+
cr.close()
102+
103+
def process_python(self, files):
104+
# process python changes
105+
py_files = [i for i in files if i.endswith('.py')]
106+
py_errors = []
107+
# TODO keep python errors until they are ok
108+
if py_files:
109+
for i in py_files:
110+
try:
111+
source = open(i, 'rb').read() + '\n'
112+
compile(source, i, 'exec')
113+
except SyntaxError:
114+
py_errors.append(i)
115+
if py_errors:
116+
_logger.info('autoreload: python code change detected, errors found')
117+
for i in py_errors:
118+
_logger.info('autoreload: SyntaxError %s',i)
119+
else:
120+
_logger.info('autoreload: python code updated, autoreload activated')
121+
restart_server()
122+
123+
def check(self):
124+
# Check if some files have been touched in the addons path.
125+
# If true, check if the touched file belongs to an installed module
126+
# in any of the database used in the registry manager.
127+
while self.notifier.check_events(0):
128+
self.notifier.read_events()
129+
self.notifier.process_events()
130+
l = self.files.keys()
131+
self.files.clear()
132+
self.process_data(l)
133+
self.process_python(l)
134+
44135
class CommonServer(object):
45136
def __init__(self, app):
46137
# TODO Change the xmlrpc_* options to http_*
@@ -101,8 +192,8 @@ def __init__(self, app):
101192
self.quit_signals_received = 0
102193

103194
#self.socket = None
104-
#self.queue = []
105195
self.httpd = None
196+
self.autoreload = None
106197

107198
def signal_handler(self, sig, frame):
108199
if sig in [signal.SIGINT,signal.SIGTERM]:
@@ -152,7 +243,11 @@ def target():
152243
_logger.debug("cron%d started!" % i)
153244

154245
def http_thread(self):
155-
self.httpd = werkzeug.serving.make_server(self.interface, self.port, self.app, threaded=True)
246+
def app(e,s):
247+
if self.autoreload:
248+
self.autoreload.check()
249+
return self.app(e,s)
250+
self.httpd = werkzeug.serving.make_server(self.interface, self.port, app, threaded=True)
156251
self.httpd.serve_forever()
157252

158253
def http_spawn(self):
@@ -172,6 +267,7 @@ def start(self):
172267
win32api.SetConsoleCtrlHandler(lambda sig: signal_handler(sig, None), 1)
173268
self.cron_spawn()
174269
self.http_spawn()
270+
self.autoreload = AutoReload()
175271

176272
def stop(self):
177273
""" Shutdown the WSGI server. Wait for non deamon threads.
@@ -189,7 +285,7 @@ def stop(self):
189285
_logger.debug('current thread: %r', me)
190286
for thread in threading.enumerate():
191287
_logger.debug('process %r (%r)', thread, thread.isDaemon())
192-
if thread != me and not thread.isDaemon() and thread.ident != main_thread_id:
288+
if thread != me and not thread.isDaemon() and thread.ident != self.main_thread_id:
193289
while thread.isAlive():
194290
_logger.debug('join and sleep')
195291
# Need a busyloop here as thread.join() masks signals
@@ -262,11 +358,11 @@ def run(self):
262358
# Prefork
263359
#----------------------------------------------------------
264360

265-
class Multicorn(CommonServer):
361+
class PreforkServer(CommonServer):
266362
""" Multiprocessing inspired by (g)unicorn.
267-
Multicorn currently uses accept(2) as dispatching method between workers
268-
but we plan to replace it by a more intelligent dispatcher to will parse
269-
the first HTTP request line.
363+
PreforkServer (aka Multicorn) currently uses accept(2) as dispatching
364+
method between workers but we plan to replace it by a more intelligent
365+
dispatcher to will parse the first HTTP request line.
270366
"""
271367
def __init__(self, app):
272368
# config
@@ -617,12 +713,12 @@ class WorkerBaseWSGIServer(werkzeug.serving.BaseWSGIServer):
617713
def __init__(self, app):
618714
werkzeug.serving.BaseWSGIServer.__init__(self, "1", "1", app)
619715
def server_bind(self):
620-
# we dont bind beause we use the listen socket of Multicorn#socket
716+
# we dont bind beause we use the listen socket of PreforkServer#socket
621717
# instead we close the socket
622718
if self.socket:
623719
self.socket.close()
624720
def server_activate(self):
625-
# dont listen as we use Multicorn#socket
721+
# dont listen as we use PreforkServer#socket
626722
pass
627723

628724
class WorkerCron(Worker):
@@ -723,10 +819,11 @@ def _reexec():
723819
def start():
724820
""" Start the openerp http server and cron processor.
725821
"""
822+
global server
726823
load_server_wide_modules()
727824
if config['workers']:
728825
openerp.multi_process = True
729-
server = Multicorn(openerp.service.wsgi_server.application)
826+
server = PreforkServer(openerp.service.wsgi_server.application)
730827
elif openerp.evented:
731828
server = GeventServer(openerp.service.wsgi_server.application)
732829
else:

0 commit comments

Comments
 (0)