odoo源码阅读之自动重载的实现
在阅读源码的时候发现原来odoo是有重载代码的实现的, 加上上一篇的模块远程部署更新功能,组合可以实现在终端上重载服务器了.
odoo服务器启动过程
在odoo的server启动过程中, 首先是执行cli的main方法解析命令参数,默认则是启动服务器.
# odoo/cli/command.py:33
def main():
# ....此处省略若干代码
# 可以看到默认如果第一个参数不是-或者--开头的参数则是server命令启动服务器
command = "server"
# ....此处省略若干代码
if command in commands:
o = commands[command]()
o.run(args)
else:
sys.exit('Unknow command %r' % (command,))
接下来看服务器的run方法, odoo/cli/server.py
的run方法直接是执行main函数,因此看main函数.
# odoo/cli/server.py
def main(args):
# 检查运行环境和解析配置
check_root_user()
odoo.tools.config.parse_config(args)
check_postgres_user()
report_configuration()
config = odoo.tools.config
csv.field_size_limit(500 * 1024 * 1024)
# 如果有指定数据库,数据库不存在则创建
preload = []
if config['db_name']:
preload = config['db_name'].split(',')
for db_name in preload:
try:
odoo.service.db._create_empty_database(db_name)
except ProgrammingError as err:
if err.pgcode == errorcodes.INSUFFICIENT_PRIVILEGE:
_logger.info("Could not determine if database %s exists, "
"skipping auto-creation: %s", db_name, err)
else:
raise err
except odoo.service.db.DatabaseExists:
pass
# 导入导出翻译
if config["translate_out"]:
export_translation()
sys.exit(0)
if config["translate_in"]:
import_translation()
sys.exit(0)
# 指定多进程
if config['workers']:
odoo.multi_process = True
stop = config["stop_after_init"]
setup_pid_file()
# 敲重点,服务器开始运行
rc = odoo.service.server.start(preload=preload, stop=stop)
sys.exit(rc)
odoo服务器启动:
# odoo/service/server.py
def start(preload=None, stop=False):
""" Start the odoo http server and cron processor.
"""
global server
load_server_wide_modules()
odoo.service.wsgi_server._patch_xmlrpc_marshaller()
# 选择服务器类型, gevent服务器 or 多进程 or 多线程
if odoo.evented:
server = GeventServer(odoo.service.wsgi_server.application)
elif config['workers']:
if config['test_enable'] or config['test_file']:
_logger.warning("Unit testing in workers mode could fail; use --workers 0.")
server = PreforkServer(odoo.service.wsgi_server.application)
# Workaround for Python issue24291, fixed in 3.6 (see Python issue26721)
if sys.version_info[:2] == (3,5):
# turn on buffering also for wfile, to avoid partial writes (Default buffer = 8k)
werkzeug.serving.WSGIRequestHandler.wbufsize = -1
else:
server = ThreadedServer(odoo.service.wsgi_server.application)
# 敲重点, 监视文件变化从而自动重载
watcher = None
if 'reload' in config['dev_mode']:
if watchdog:
watcher = FSWatcher()
watcher.start()
else:
_logger.warning("'watchdog' module not installed. "
"Code autoreload feature is disabled")
if 'werkzeug' in config['dev_mode']:
server.app = DebuggedApplication(server.app, evalex=True)
# 敲重点, 启动服务器
rc = server.run(preload, stop)
# 如果标志 phoenix 为 true 则在服务器退出后重新载入
# like the legend of the phoenix, all ends with beginnings
if getattr(odoo, 'phoenix', False):
if watcher:
watcher.stop()
_reexec()
return rc if rc else 0
watchdog实现文件内容更改则reload服务器
在上面的服务器启动过程中, 可以看到, 如果我们配置了dev_mode包含了reload选项, 并且watchdog变量不为空的话, 则会启动watcher观察文件系统.那么, watchdog在哪导入的呢,在源码中有:
# odoo/service/server.py
try:
import watchdog
from watchdog.observers import Observer
from watchdog.events import FileCreatedEvent, FileModifiedEvent, FileMovedEvent
except ImportError:
watchdog = None
可以看到, 尝试导入 watchdog
, 导入失败则为None. 为此我专门去odoo的 requirements.txt
看了下, 并没有 watchdog
这个依赖, 当然, pip install watchdog
就好了, 但是官方这么做是为啥呢? 一脸黑线🌚
接下来就是watchdog的事件处理了:
class FSWatcher(object):
def __init__(self):
self.observer = Observer()
for path in odoo.modules.module.ad_paths:
_logger.info('Watching addons folder %s', path)
# 注册为自己为watchdog的事件处理
self.observer.schedule(self, path, recursive=True)
def dispatch(self, event):
if isinstance(event, (FileCreatedEvent, FileModifiedEvent, FileMovedEvent)):
if not event.is_directory:
path = getattr(event, 'dest_path', event.src_path)
# 如果是py代码则尝试导入, try 没有语法错误 则 重启服务器
if path.endswith('.py') and not os.path.basename(path).startswith('.~'):
try:
source = open(path, 'rb').read() + b'\n'
compile(source, path, 'exec')
except FileNotFoundError:
_logger.error('autoreload: python code change detected, FileNotFound for %s', path)
except SyntaxError:
_logger.error('autoreload: python code change detected, SyntaxError in %s', path)
else:
if not getattr(odoo, 'phoenix', False):
_logger.info('autoreload: python code updated, autoreload activated')
restart()
def start(self):
self.observer.start()
_logger.info('AutoReload watcher running')
def stop(self):
self.observer.stop()
self.observer.join()
在 __init__
有个有点意思的是 self.observer.schedule(self, path, recursive=True)
代码, 查看 schedule
函数签名可以看到对于第一个参数 event_handler 的类型是要求:
`watchdog.events.FileSystemEventHandler` or a subclass
而 watchdog.events.FileSystemEventHandler
的核心就是 dispatch 方法.
看来源码这用的是动态语言的鸭子模型了. (这段有点啰嗦了)
信号处理重载服务器
在odoo服务器启动过程源码中, 可以看到odoo的服务器类型有三种: gevent, 多进程, 多线程.
而默认的就是多线程版本。在多线程版本的start代码中,有一段信号注册代码:
def start(self, stop=False):
_logger.debug("Setting signal handlers")
if os.name == 'posix':
signal.signal(signal.SIGINT, self.signal_handler)
signal.signal(signal.SIGTERM, self.signal_handler)
signal.signal(signal.SIGCHLD, self.signal_handler)
signal.signal(signal.SIGHUP, self.signal_handler)
signal.signal(signal.SIGQUIT, dumpstacks)
signal.signal(signal.SIGUSR1, log_ormcache_stats)
elif os.name == 'nt':
import win32api
win32api.SetConsoleCtrlHandler(lambda sig: self.signal_handler(sig, None), 1)
test_mode = config['test_enable'] or config['test_file']
if test_mode or (config['http_enable'] and not stop):
# some tests need the http deamon to be available...
self.http_spawn()
该代码表示, 如果系统内核是posix(Uni*系统),则注册一系列信号处理。
如果是nt内核(Windows系统),则将事件默认交给signal_handler 函数处理。
接下来就看对应的信号处理函数:
def signal_handler(self, sig, frame):
if sig in [signal.SIGINT, signal.SIGTERM]:
# shutdown on kill -INT or -TERM
self.quit_signals_received += 1
if self.quit_signals_received > 1:
# logging.shutdown was already called at this point.
sys.stderr.write("Forced shutdown.\n")
os._exit(0)
# interrupt run() to start shutdown
raise KeyboardInterrupt()
elif sig == signal.SIGHUP:
# restart on kill -HUP
odoo.phoenix = True
self.quit_signals_received += 1
# interrupt run() to start shutdown
raise KeyboardInterrupt()
可以看到,如果是SIGHUP信号,则标记phoenix为True,同时抛出 KeyboardInterrupt 中断服务器,此后,回到我们的odoo服务器启动过程源码的最后一段:
# like the legend of the phoenix, all ends with beginnings
if getattr(odoo, 'phoenix', False):
if watcher:
watcher.stop()
_reexec()
由此可见,当odoo服务器进程收到SIGHUP信号时,将会重载odoo代码。
对此,我们可以这么操作发送SIGHUP信号:
$ kill -1 pid
# 或者
$ kill -s SIGHUP pid
注意
reload只是重载了python代码,像xml这些视图配置的,还是需要去升级模块才能够更新的,但是可以用deploy命令配合。