[MERGE] upstream
[odoo/odoo.git] / openerp / cli / server.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 """
23 OpenERP - Server
24 OpenERP is an ERP+CRM program for small and medium businesses.
25
26 The whole source code is distributed under the terms of the
27 GNU Public Licence.
28
29 (c) 2003-TODAY, Fabien Pinckaers - OpenERP SA
30 """
31
32 import logging
33 import os
34 import signal
35 import sys
36 import threading
37 import traceback
38 import time
39
40 import openerp
41
42 from . import Command
43
44 __author__ = openerp.release.author
45 __version__ = openerp.release.version
46
47 # Also use the `openerp` logger for the main script.
48 _logger = logging.getLogger('openerp')
49
50 def check_root_user():
51     """ Exit if the process's user is 'root' (on POSIX system)."""
52     if os.name == 'posix':
53         import pwd
54         if pwd.getpwuid(os.getuid())[0] == 'root' :
55             sys.stderr.write("Running as user 'root' is a security risk, aborting.\n")
56             sys.exit(1)
57
58 def check_postgres_user():
59     """ Exit if the configured database user is 'postgres'.
60
61     This function assumes the configuration has been initialized.
62     """
63     config = openerp.tools.config
64     if config['db_user'] == 'postgres':
65         sys.stderr.write("Using the database user 'postgres' is a security risk, aborting.")
66         sys.exit(1)
67
68 def report_configuration():
69     """ Log the server version and some configuration values.
70
71     This function assumes the configuration has been initialized.
72     """
73     config = openerp.tools.config
74     _logger.info("OpenERP version %s", __version__)
75     for name, value in [('addons paths', config['addons_path']),
76                         ('database hostname', config['db_host'] or 'localhost'),
77                         ('database port', config['db_port'] or '5432'),
78                         ('database user', config['db_user'])]:
79         _logger.info("%s: %s", name, value)
80
81 def setup_pid_file():
82     """ Create a file with the process id written in it.
83
84     This function assumes the configuration has been initialized.
85     """
86     config = openerp.tools.config
87     if config['pidfile']:
88         fd = open(config['pidfile'], 'w')
89         pidtext = "%d" % (os.getpid())
90         fd.write(pidtext)
91         fd.close()
92
93 def preload_registry(dbname):
94     """ Preload a registry, and start the cron."""
95     try:
96         update_module = True if openerp.tools.config['init'] or openerp.tools.config['update'] else False
97         openerp.modules.registry.RegistryManager.new(dbname, update_module=update_module)
98     except Exception:
99         _logger.exception('Failed to initialize database `%s`.', dbname)
100         return False
101     return True
102
103 def run_test_file(dbname, test_file):
104     """ Preload a registry, possibly run a test file, and start the cron."""
105     try:
106         config = openerp.tools.config
107         registry = openerp.modules.registry.RegistryManager.new(dbname, update_module=config['init'] or config['update'])
108         cr = registry.db.cursor()
109         _logger.info('loading test file %s', test_file)
110         openerp.tools.convert_yaml_import(cr, 'base', file(test_file), 'test', {}, 'test', True)
111         cr.rollback()
112         cr.close()
113     except Exception:
114         _logger.exception('Failed to initialize database `%s` and run test file `%s`.', dbname, test_file)
115
116 def export_translation():
117     config = openerp.tools.config
118     dbname = config['db_name']
119
120     if config["language"]:
121         msg = "language %s" % (config["language"],)
122     else:
123         msg = "new language"
124     _logger.info('writing translation file for %s to %s', msg,
125         config["translate_out"])
126
127     fileformat = os.path.splitext(config["translate_out"])[-1][1:].lower()
128     buf = file(config["translate_out"], "w")
129     registry = openerp.modules.registry.RegistryManager.new(dbname)
130     cr = registry.db.cursor()
131     openerp.tools.trans_export(config["language"],
132         config["translate_modules"] or ["all"], buf, fileformat, cr)
133     cr.close()
134     buf.close()
135
136     _logger.info('translation file written successfully')
137
138 def import_translation():
139     config = openerp.tools.config
140     context = {'overwrite': config["overwrite_existing_translations"]}
141     dbname = config['db_name']
142
143     registry = openerp.modules.registry.RegistryManager.new(dbname)
144     cr = registry.db.cursor()
145     openerp.tools.trans_load( cr, config["translate_in"], config["language"],
146         context=context)
147     cr.commit()
148     cr.close()
149
150 # Variable keeping track of the number of calls to the signal handler defined
151 # below. This variable is monitored by ``quit_on_signals()``.
152 quit_signals_received = 0
153
154 def signal_handler(sig, frame):
155     """ Signal handler: exit ungracefully on the second handled signal.
156
157     :param sig: the signal number
158     :param frame: the interrupted stack frame or None
159     """
160     global quit_signals_received
161     quit_signals_received += 1
162     if quit_signals_received > 1:
163         # logging.shutdown was already called at this point.
164         sys.stderr.write("Forced shutdown.\n")
165         os._exit(0)
166
167 def dumpstacks(sig, frame):
168     """ Signal handler: dump a stack trace for each existing thread."""
169     # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696
170     # modified for python 2.5 compatibility
171     threads_info = dict([(th.ident, {'name': th.name,
172                                     'uid': getattr(th,'uid','n/a')})
173                                 for th in threading.enumerate()])
174     code = []
175     for threadId, stack in sys._current_frames().items():
176         thread_info = threads_info.get(threadId)
177         code.append("\n# Thread: %s (id:%s) (uid:%s)" % \
178                     (thread_info and thread_info['name'] or 'n/a',
179                      threadId,
180                      thread_info and thread_info['uid'] or 'n/a'))
181         for filename, lineno, name, line in traceback.extract_stack(stack):
182             code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
183             if line:
184                 code.append("  %s" % (line.strip()))
185     _logger.info("\n".join(code))
186
187 def setup_signal_handlers(signal_handler):
188     """ Register the given signal handler. """
189     SIGNALS = (signal.SIGINT, signal.SIGTERM)
190     if os.name == 'posix':
191         map(lambda sig: signal.signal(sig, signal_handler), SIGNALS)
192         signal.signal(signal.SIGQUIT, dumpstacks)
193     elif os.name == 'nt':
194         import win32api
195         win32api.SetConsoleCtrlHandler(lambda sig: signal_handler(sig, None), 1)
196
197 def quit_on_signals():
198     """ Wait for one or two signals then shutdown the server.
199
200     The first SIGINT or SIGTERM signal will initiate a graceful shutdown while
201     a second one if any will force an immediate exit.
202
203     """
204     # Wait for a first signal to be handled. (time.sleep will be interrupted
205     # by the signal handler.) The try/except is for the win32 case.
206     try:
207         while quit_signals_received == 0:
208             time.sleep(60)
209     except KeyboardInterrupt:
210         pass
211
212     config = openerp.tools.config
213     openerp.service.stop_services()
214
215     if getattr(openerp, 'phoenix', False):
216         # like the phoenix, reborn from ashes...
217         openerp.service._reexec()
218         return
219
220     if config['pidfile']:
221         os.unlink(config['pidfile'])
222     sys.exit(0)
223
224 def watch_parent(beat=4):
225     import gevent
226     ppid = os.getppid()
227     while True:
228         if ppid != os.getppid():
229             pid = os.getpid()
230             _logger.info("LongPolling (%s) Parent changed", pid)
231             # suicide !!
232             os.kill(pid, signal.SIGTERM)
233             return
234         gevent.sleep(beat)
235
236 def main(args):
237     check_root_user()
238     openerp.tools.config.parse_config(args)
239
240     if openerp.tools.config.options["gevent"]:
241         openerp.evented = True
242         _logger.info('Using gevent mode')
243         import gevent.monkey
244         gevent.monkey.patch_all()
245         import gevent_psycopg2
246         gevent_psycopg2.monkey_patch()
247
248     check_postgres_user()
249     openerp.netsvc.init_logger()
250     report_configuration()
251
252     config = openerp.tools.config
253
254     setup_signal_handlers(signal_handler)
255
256     if config["test_file"]:
257         run_test_file(config['db_name'], config['test_file'])
258         sys.exit(0)
259
260     if config["translate_out"]:
261         export_translation()
262         sys.exit(0)
263
264     if config["translate_in"]:
265         import_translation()
266         sys.exit(0)
267
268     if not config["stop_after_init"]:
269         setup_pid_file()
270         # Some module register themselves when they are loaded so we need the
271         # services to be running before loading any registry.
272         if not openerp.evented:
273             if config['workers']:
274                 openerp.service.start_services_workers()
275             else:
276                 openerp.service.start_services()
277         else:
278             config['xmlrpc_port'] = config['longpolling_port']
279             import gevent
280             gevent.spawn(watch_parent)
281             openerp.service.start_services()
282
283     rc = 0
284     if config['db_name']:
285         for dbname in config['db_name'].split(','):
286             if not preload_registry(dbname):
287                 rc += 1
288
289     if config["stop_after_init"]:
290         sys.exit(rc)
291
292     _logger.info('OpenERP server is running, waiting for connections...')
293     quit_on_signals()
294
295 class Server(Command):
296     def run(self, args):
297         main(args)
298
299 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: