diff --git a/doc/commands.rst b/doc/commands.rst new file mode 100644 index 00000000000..7a758b1ab2a --- /dev/null +++ b/doc/commands.rst @@ -0,0 +1,40 @@ +.. _commands: + +Available commands +================== + +This page explain some of the available ``oe`` commands. For an overview about +``oe``, see :doc:`openerp-command`. + +Keep in mind that ``oe --help`` and ``oe --help`` already give a lot +of information about the commands and their options and flags. + +``web`` +------- + +The ``web`` command is used to create a single OpenERP server process to handle +regular HTTP requests and XML-RPC requests. It is possible to execute such +process multiple times, possibly on different machines. + +It is possible to chose the ``--threaded`` or ``--gevent`` flags. It is +recommanded to use ``--threaded`` only when running a single process. +``--gevent`` is experimental; it is planned to use it for the embedded chat +feature. + +Example invocation:: + + > oe web --addons ../../addons/trunk:../../web/trunk/addons --threaded + +``cron`` +-------- + +The ``cron`` command is used to create a single OpenERP process to execute +so-called cron jobs, also called scheduled tasks in the OpenERP interface. As +for the ``web`` command, multiple cron processes can be run side by side. + +It is necessary to specify on the command-line which database need to be +watched by the cron process with the ``--database`` option. + +Example invocation:: + + > oe cron --addons ../../addons/trunk:../../web/trunk/addons --database production diff --git a/doc/index.rst b/doc/index.rst index 64fc04b552e..37870af49a8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -25,6 +25,7 @@ OpenERP Command :maxdepth: 1 openerp-command.rst + commands.rst adding-command.rst OpenERP Server API diff --git a/doc/openerp-command.rst b/doc/openerp-command.rst index 2bfd845f8e3..6ca7b02b645 100644 --- a/doc/openerp-command.rst +++ b/doc/openerp-command.rst @@ -1,21 +1,22 @@ .. _openerp-command: -OpenERP Command -=============== +The ``oe`` script +================= The ``oe`` script provides a set of command-line tools around the OpenERP -framework. +framework. It is meant to replace the older ``openerp-server`` script (which +is still available). -Using OpenERP Command ---------------------- +Using ``oe`` +------------ In contrast to the previous ``openerp-server`` script, ``oe`` defines a few -sub-commands, each with its own set of flags and options. You can get some +commands, each with its own set of flags and options. You can get some information for any of them with :: - > oe --help + > oe --help For instance:: @@ -43,6 +44,11 @@ for instance here is a, say, ``test-trunk-view-validation.sh`` file:: # itself does not need it. oe run-tests -d ignored -m openerp.test_view_validation +Available commands +------------------- + +See the :doc:`commands` page. + Adding new commands ------------------- @@ -57,4 +63,4 @@ A preliminary ``oe-bash-completion`` file is provided. After sourcing it, > . oe-bash-completion -completion (using the TAB character) in Bash should be working. +completion (using the TAB character) in Bash should work. diff --git a/openerp/__init__.py b/openerp/__init__.py index c9db7076e71..307170b9033 100644 --- a/openerp/__init__.py +++ b/openerp/__init__.py @@ -51,6 +51,8 @@ wsgi.register_wsgi_handler = wsgi.wsgi_server.register_wsgi_handler # its own copy of the data structure and we don't need to care about # locks between threads. multi_process = False +# Is the server running with gevent. +evented = False # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 4bf99cf9934..60aa685b6fc 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -135,6 +135,10 @@ class ir_cron(osv.osv): _logger.debug('%.3fs (%s, %s)' % (end_time - start_time, model_name, method_name)) except Exception, e: self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e) + else: + msg = "Method `%s.%s` do not exist." % (model._name, method_name) \ + if model else "Model `%s` do not exist." % model._name + _logger.warning(msg) def _process_job(self, job_cr, job, cron_cr): """ Run a given job taking care of the repetition. diff --git a/openerp/cli/server.py b/openerp/cli/server.py index 7fb252f905f..2408703d6b8 100644 --- a/openerp/cli/server.py +++ b/openerp/cli/server.py @@ -181,9 +181,9 @@ def dumpstacks(sig, frame): code.append(" %s" % (line.strip())) _logger.info("\n".join(code)) -def setup_signal_handlers(): - """ Register the signal handler defined above. """ - SIGNALS = map(lambda x: getattr(signal, "SIG%s" % x), "INT TERM".split()) +def setup_signal_handlers(signal_handler): + """ Register the given signal handler. """ + SIGNALS = (signal.SIGINT, signal.SIGTERM) if os.name == 'posix': map(lambda sig: signal.signal(sig, signal_handler), SIGNALS) signal.signal(signal.SIGQUIT, dumpstacks) @@ -238,7 +238,7 @@ def main(args): configure_babel_localedata_path() - setup_signal_handlers() + setup_signal_handlers(signal_handler) if config["test_file"]: run_test_file(config['db_name'], config['test_file']) diff --git a/openerp/service/wsgi_server.py b/openerp/service/wsgi_server.py index 67436a1c0bf..c9294365d71 100644 --- a/openerp/service/wsgi_server.py +++ b/openerp/service/wsgi_server.py @@ -410,22 +410,15 @@ def application(environ, start_response): # The WSGI server, started by start_server(), stopped by stop_server(). httpd = None -def serve(): +def serve(interface, port, threaded): """ Serve HTTP requests via werkzeug development server. - If werkzeug can not be imported, we fall back to wsgiref's simple_server. - Calling this function is blocking, you might want to call it in its own thread. """ global httpd - - # TODO Change the xmlrpc_* options to http_* - interface = config['xmlrpc_interface'] or '0.0.0.0' - port = config['xmlrpc_port'] - httpd = werkzeug.serving.make_server(interface, port, application, threaded=True) - _logger.info('HTTP service (werkzeug) running on %s:%s', interface, port) + httpd = werkzeug.serving.make_server(interface, port, application, threaded=threaded) httpd.serve_forever() def start_service(): @@ -433,7 +426,11 @@ def start_service(): The WSGI server can be shutdown with stop_server() below. """ - threading.Thread(target=serve).start() + # TODO Change the xmlrpc_* options to http_* + interface = config['xmlrpc_interface'] or '0.0.0.0' + port = config['xmlrpc_port'] + _logger.info('HTTP service (werkzeug) running on %s:%s', interface, port) + threading.Thread(target=serve, args=(interface, port, True)).start() def stop_service(): """ Initiate the shutdown of the WSGI server. diff --git a/openerpcommand/__init__.py b/openerpcommand/__init__.py index 53d456c7639..6d41c2842dc 100644 --- a/openerpcommand/__init__.py +++ b/openerpcommand/__init__.py @@ -8,6 +8,7 @@ from .bench_sale_mrp import BenchSaleMrp from . import common from . import conf # Not really server-side (in the `for` below). +from . import cron from . import drop from . import initialize from . import model @@ -17,9 +18,10 @@ from . import run_tests from . import scaffold from . import uninstall from . import update +from . import web -command_list_server = (conf, drop, initialize, model, module, read, run_tests, - scaffold, uninstall, update, ) +command_list_server = (conf, cron, drop, initialize, model, module, read, run_tests, + scaffold, uninstall, update, web, ) command_list_client = (Call, Open, Show, ConsumeNothing, ConsumeMemory, LeakMemory, ConsumeCPU, Bench, BenchRead, diff --git a/openerpcommand/common.py b/openerpcommand/common.py index 89898ff4c3c..45c2742d68f 100644 --- a/openerpcommand/common.py +++ b/openerpcommand/common.py @@ -3,6 +3,10 @@ Define a few common arguments for server-side command-line tools. """ import argparse import os +try: + from setproctitle import setproctitle +except ImportError: + setproctitle = lambda x: None import sys def add_addons_argument(parser): @@ -12,6 +16,21 @@ def add_addons_argument(parser): parser.add_argument('--addons', metavar='ADDONS', **required_or_default('ADDONS', 'colon-separated list of paths to addons')) +def set_addons(args): + """ + Turn args.addons into a list instead of a column-separated strings. + Set openerp.toools.config accordingly. + """ + import openerp.tools.config + config = openerp.tools.config + + assert hasattr(args, 'addons') + if args.addons: + args.addons = args.addons.split(':') + else: + args.addons = [] + + config['addons_path'] = ','.join(args.addons) def get_addons_from_paths(paths, exclude): """ diff --git a/openerpcommand/cron.py b/openerpcommand/cron.py new file mode 100644 index 00000000000..16b983dbc50 --- /dev/null +++ b/openerpcommand/cron.py @@ -0,0 +1,47 @@ +""" +Run an OpenERP cron process. +""" + +import os + +import common + +def run(args): + import openerp + import openerp.cli.server + import openerp.tools.config + import openerp.service.cron + config = openerp.tools.config + + os.environ["TZ"] = "UTC" + common.set_addons(args) + args.database = args.database or [] + + config['log_handler'] = [':WARNING', 'openerp.addons.base.ir.ir_cron:DEBUG'] + + openerp.multi_process = True + common.setproctitle('openerp-cron [%s]' % ', '.join(args.database)) + + openerp.cli.server.check_root_user() + openerp.netsvc.init_logger() + #openerp.cli.server.report_configuration() + openerp.cli.server.configure_babel_localedata_path() + openerp.cli.server.setup_signal_handlers(openerp.cli.server.signal_handler) + import openerp.addons.base + if args.database: + for db in args.database: + openerp.cli.server.preload_registry(db) + openerp.service.cron.start_service() + openerp.cli.server.quit_on_signals() + else: + print "No database given." + + +def add_parser(subparsers): + parser = subparsers.add_parser('cron', + description='Run an OpenERP cron process.') + common.add_addons_argument(parser) + parser.add_argument('--database', action='append', + help='Database for which cron jobs are processed (can be repeated)') + + parser.set_defaults(run=run) diff --git a/openerpcommand/read.py b/openerpcommand/read.py index 83d6a5340f8..d8455e16fff 100644 --- a/openerpcommand/read.py +++ b/openerpcommand/read.py @@ -5,6 +5,8 @@ import os import sys import textwrap +import common + # TODO provide a --rpc flag to use XML-RPC (with a specific username) instead # of server-side library. def run(args): @@ -13,6 +15,7 @@ def run(args): import openerp config = openerp.tools.config config['log_handler'] = [':CRITICAL'] + common.set_addons(args) openerp.netsvc.init_logger() registry = openerp.modules.registry.RegistryManager.get( args.database, update_module=False) @@ -44,8 +47,9 @@ def run(args): def add_parser(subparsers): parser = subparsers.add_parser('read', description='Display a record.') - parser.add_argument('-d', '--database', metavar='DATABASE', required=True, - help='the database to connect to') + parser.add_argument('-d', '--database', metavar='DATABASE', + **common.required_or_default('DATABASE', 'the database to connect to')) + common.add_addons_argument(parser) parser.add_argument('-m', '--model', metavar='MODEL', required=True, help='the model for which a record should be read') parser.add_argument('-i', '--id', metavar='RECORDID', required=True, diff --git a/openerpcommand/web.py b/openerpcommand/web.py new file mode 100644 index 00000000000..728318d1d9f --- /dev/null +++ b/openerpcommand/web.py @@ -0,0 +1,94 @@ +""" +Run a normal OpenERP HTTP process. +""" + +import logging +import os +import signal + +import common + +_logger = logging.getLogger(__name__) + +def mk_signal_handler(server): + def signal_handler(sig, frame): + """ + Specialized signal handler for the evented process. + """ + print "\n\n\nStopping gevent HTTP server...\n\n\n" + server.stop() + return signal_handler + +def setup_signal_handlers(signal_handler): + SIGNALS = (signal.SIGINT, signal.SIGTERM) + map(lambda sig: signal.signal(sig, signal_handler), SIGNALS) + +def run(args): + # Note that gevent monkey patching must be done before importing the + # `threading` module, see http://stackoverflow.com/questions/8774958/. + if args.gevent: + import gevent + import gevent.monkey + import gevent.wsgi + import gevent_psycopg2 + gevent.monkey.patch_all() + gevent_psycopg2.monkey_patch() + import threading + import openerp + import openerp.cli.server + import openerp.service.wsgi_server + import openerp.tools.config + config = openerp.tools.config + + os.environ["TZ"] = "UTC" + common.set_addons(args) + + openerp.multi_process = True + common.setproctitle('openerp-web') + + openerp.cli.server.check_root_user() + openerp.netsvc.init_logger() + #openerp.cli.server.report_configuration() + openerp.cli.server.configure_babel_localedata_path() + + target = openerp.service.wsgi_server.serve + if not args.gevent: + openerp.evented = False + openerp.cli.server.setup_signal_handlers(openerp.cli.server.signal_handler) + # TODO openerp.multi_process with a multi-threaded process probably + # doesn't work very well (e.g. waiting for all threads to complete + # before killing the process is not implemented). + arg = (args.interface, int(args.port), args.threaded) + threading.Thread(target=target, args=arg).start() + openerp.cli.server.quit_on_signals() + else: + openerp.evented = True + + app = openerp.service.wsgi_server.application + server = gevent.wsgi.WSGIServer((args.interface, int(args.port)), app) + setup_signal_handlers(mk_signal_handler(server)) + try: + server.serve_forever() + except KeyboardInterrupt: + try: + server.stop() + gevent.shutdown() + except KeyboardInterrupt: + sys.stderr.write("Forced shutdown.\n") + gevent.shutdown() + +def add_parser(subparsers): + parser = subparsers.add_parser('web', + description='Run a normal OpenERP HTTP process. By default a ' + 'singly-threaded Werkzeug server is used.') + common.add_addons_argument(parser) + parser.add_argument('--interface', default='0.0.0.0', + help='HTTP interface to listen on (default is %(default)s)') + parser.add_argument('--port', metavar='INT', default=8069, + help='HTTP port to listen on (default is %(default)s)') + parser.add_argument('--threaded', action='store_true', + help='Use a multithreaded Werkzeug server (incompatible with --gevent)') + parser.add_argument('--gevent', action='store_true', + help="Use gevent's WSGI server (incompatible with --threaded)") + + parser.set_defaults(run=run)