2 # -*- coding: utf-8 -*-
3 ##############################################################################
5 # OpenERP, Open Source Management Solution
6 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
7 # The refactoring about the OpenSSL support come from Tryton
8 # Copyright (C) 2007-2009 Cédric Krier.
9 # Copyright (C) 2007-2009 Bertrand Chenal.
10 # Copyright (C) 2008 B2CK SPRL.
12 # This program is free software: you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation, either version 3 of the License, or
15 # (at your option) any later version.
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
22 # You should have received a copy of the GNU General Public License
23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 ##############################################################################
29 import logging.handlers
36 from pprint import pformat
39 class Service(object):
40 """ Base class for *Local* services
42 Functionality here is trusted, no authentication.
45 def __init__(self, name, audience=''):
46 Service._services[name] = self
50 def joinGroup(self, name):
51 raise Exception("No group for local services")
52 #GROUPS.setdefault(name, {})[self.__name] = self
55 def exists(cls, name):
56 return name in cls._services
59 def remove(cls, name):
61 cls._services.pop(name)
63 def exportMethod(self, method):
65 self._methods[method.__name__] = method
67 def abortResponse(self, error, description, origin, details):
68 if not tools.config['debug_mode']:
69 raise Exception("%s -- %s\n\n%s"%(origin, description, details))
73 class LocalService(object):
74 """ Proxy for local services.
76 Any instance of this class will behave like the single instance
79 __logger = logging.getLogger('service')
80 def __init__(self, name):
83 self._service = Service._services[name]
84 for method_name, method_definition in self._service._methods.items():
85 setattr(self, method_name, method_definition)
86 except KeyError, keyError:
87 self.__logger.error('This service does not exist: %s' % (str(keyError),) )
90 def __call__(self, method, *params):
91 return getattr(self, method)(*params)
93 class ExportService(object):
94 """ Proxy for exported services.
96 All methods here should take an AuthProxy as their first parameter. It
97 will be appended by the calling framework.
99 Note that this class has no direct proxy, capable of calling
100 eservice.method(). Rather, the proxy should call
101 dispatch(method,auth,params)
107 def __init__(self, name, audience=''):
108 ExportService._services[name] = self
111 def joinGroup(self, name):
112 ExportService._groups.setdefault(name, {})[self.__name] = self
115 def getService(cls,name):
116 return cls._services[name]
118 def dispatch(self, method, auth, params):
119 raise Exception("stub dispatch at %s" % self.__name)
121 def new_dispatch(self,method,auth,params):
122 raise Exception("stub dispatch at %s" % self.__name)
124 def abortResponse(self, error, description, origin, details):
125 if not tools.config['debug_mode']:
126 raise Exception("%s -- %s\n\n%s"%(origin, description, details))
130 LOG_NOTSET = 'notset'
131 LOG_DEBUG_SQL = 'debug_sql'
132 LOG_DEBUG_RPC = 'debug_rpc'
138 LOG_CRITICAL = 'critical'
140 logging.DEBUG_RPC = logging.DEBUG - 2
141 logging.addLevelName(logging.DEBUG_RPC, 'DEBUG_RPC')
142 logging.DEBUG_SQL = logging.DEBUG_RPC - 2
143 logging.addLevelName(logging.DEBUG_SQL, 'DEBUG_SQL')
145 logging.TEST = logging.INFO - 5
146 logging.addLevelName(logging.TEST, 'TEST')
148 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10)
149 #The background is set with 40 plus the number of the color, and the foreground with 30
150 #These are the sequences need to get colored ouput
151 RESET_SEQ = "\033[0m"
152 COLOR_SEQ = "\033[1;%dm"
154 COLOR_PATTERN = "%s%s%%s%s" % (COLOR_SEQ, COLOR_SEQ, RESET_SEQ)
155 LEVEL_COLOR_MAPPING = {
156 logging.DEBUG_SQL: (WHITE, MAGENTA),
157 logging.DEBUG_RPC: (BLUE, WHITE),
158 logging.DEBUG: (BLUE, DEFAULT),
159 logging.INFO: (GREEN, DEFAULT),
160 logging.TEST: (WHITE, BLUE),
161 logging.WARNING: (YELLOW, DEFAULT),
162 logging.ERROR: (RED, DEFAULT),
163 logging.CRITICAL: (WHITE, RED),
166 class ColoredFormatter(logging.Formatter):
167 def format(self, record):
168 fg_color, bg_color = LEVEL_COLOR_MAPPING[record.levelno]
169 record.levelname = COLOR_PATTERN % (30 + fg_color, 40 + bg_color, record.levelname)
170 return logging.Formatter.format(self, record)
175 from tools.translate import resetlocale
178 logger = logging.getLogger()
179 # create a format for log messages and dates
180 format = '[%(asctime)s] %(levelname)s:%(name)s:%(message)s'
182 if tools.config['syslog']:
185 handler = logging.handlers.NTEventLogHandler("%s %s" % (release.description, release.version))
187 handler = logging.handlers.SysLogHandler('/dev/log')
188 format = '%s %s' % (release.description, release.version) \
189 + ':%(levelname)s:%(name)s:%(message)s'
191 elif tools.config['logfile']:
193 logf = tools.config['logfile']
195 dirname = os.path.dirname(logf)
196 if dirname and not os.path.isdir(dirname):
198 if tools.config['logrotate'] is not False:
199 handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
200 elif os.name == 'posix':
201 handler = logging.handlers.WatchedFileHandler(logf)
203 handler = logging.handlers.FileHandler(logf)
204 except Exception, ex:
205 sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
206 handler = logging.StreamHandler(sys.stdout)
208 # Normal Handler on standard output
209 handler = logging.StreamHandler(sys.stdout)
211 if isinstance(handler, logging.StreamHandler) and os.isatty(handler.stream.fileno()):
212 formatter = ColoredFormatter(format)
214 formatter = logging.Formatter(format)
215 handler.setFormatter(formatter)
217 # add the handler to the root logger
218 logger.addHandler(handler)
219 logger.setLevel(int(tools.config['log_level'] or '0'))
222 class Logger(object):
224 warnings.warn("The netsvc.Logger API shouldn't be used anymore, please "
225 "use the standard `logging.getLogger` API instead",
226 PendingDeprecationWarning, stacklevel=2)
227 super(Logger, self).__init__()
229 def notifyChannel(self, name, level, msg):
230 warnings.warn("notifyChannel API shouldn't be used anymore, please use "
231 "the standard `logging` module instead",
232 PendingDeprecationWarning, stacklevel=2)
233 from service.web_services import common
235 log = logging.getLogger(tools.ustr(name))
237 if level in [LOG_DEBUG_RPC, LOG_TEST] and not hasattr(log, level):
238 fct = lambda msg, *args, **kwargs: log.log(getattr(logging, level.upper()), msg, *args, **kwargs)
239 setattr(log, level, fct)
242 level_method = getattr(log, level)
244 if isinstance(msg, Exception):
245 msg = tools.exception_to_unicode(msg)
248 msg = tools.ustr(msg).strip()
249 if level in (LOG_ERROR, LOG_CRITICAL) and tools.config.get_misc('debug','env_info',False):
250 msg = common().exp_get_server_environment() + "\n" + msg
252 result = msg.split('\n')
253 except UnicodeDecodeError:
254 result = msg.strip().split('\n')
257 for idx, s in enumerate(result):
258 level_method('[%02d]: %s' % (idx+1, s,))
260 level_method(result[0])
262 # TODO: perhaps reset the logger streams?
263 #if logrotate closes our files, we end up here..
266 # better ignore the exception and carry on..
269 def set_loglevel(self, level, logger=None):
270 if logger is not None:
271 log = logging.getLogger(str(logger))
273 log = logging.getLogger()
274 log.setLevel(logging.INFO) # make sure next msg is printed
275 log.info("Log level changed to %s" % logging.getLevelName(level))
288 __logger = logging.getLogger('timer')
290 def setAlarm(self, fn, dt, db_name, *args, **kwargs):
291 wait = dt - time.time()
293 self.__logger.debug("Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
294 timer = threading.Timer(wait, fn, args, kwargs)
296 self._timers.setdefault(db_name, []).append(timer)
298 for db in self._timers:
299 for timer in self._timers[db]:
300 if not timer.isAlive():
301 self._timers[db].remove(timer)
304 def cancel(cls, db_name):
305 """Cancel all timers for a given database. If None passed, all timers are cancelled"""
306 for db in cls._timers:
307 if db_name is None or db == db_name:
308 for timer in cls._timers[db]:
318 """ Generic interface for all servers with an event loop etc.
319 Override this to impement http, net-rpc etc. servers.
321 Servers here must have threaded behaviour. start() must not block,
327 # we don't want blocking server calls (think select()) to
328 # wait forever and possibly prevent exiting the process,
329 # but instead we want a form of polling/busy_wait pattern, where
330 # _server_timeout should be used as the default timeout for
331 # all I/O blocking operations
332 _busywait_timeout = 0.5
335 __logger = logging.getLogger('server')
338 if Server.__is_started:
339 raise Exception('All instances of servers must be inited before the startAll()')
340 Server.__servers.append(self)
343 self.__logger.debug("called stub Server.start")
346 self.__logger.debug("called stub Server.stop")
349 """ This function should return statistics about the server """
350 return "%s: No statistics" % str(self.__class__)
356 cls.__logger.info("Starting %d services" % len(cls.__servers))
357 for srv in cls.__servers:
359 cls.__is_started = True
363 if not cls.__is_started:
365 cls.__logger.info("Stopping %d services" % len(cls.__servers))
366 for srv in cls.__servers:
368 cls.__is_started = False
372 res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
373 res.extend(srv.stats() for srv in cls.__servers)
374 return '\n'.join(res)
376 def _close_socket(self):
379 self.socket.shutdown(getattr(socket, 'SHUT_RDWR', 2))
380 except socket.error, e:
381 if e.errno != errno.ENOTCONN: raise
382 # OSX, socket shutdowns both sides if any side closes it
383 # causing an error 57 'Socket is not connected' on shutdown
384 # of the other side (or something), see
385 # http://bugs.python.org/issue4397
387 '"%s" when shutting down server socket, '
388 'this is normal under OS X', e)
391 class OpenERPDispatcherException(Exception):
392 def __init__(self, exception, traceback):
393 self.exception = exception
394 self.traceback = traceback
396 class OpenERPDispatcher:
397 def log(self, title, msg):
398 logger = logging.getLogger(title)
399 if logger.isEnabledFor(logging.DEBUG_RPC):
400 for line in pformat(msg).split('\n'):
401 logger.log(logging.DEBUG_RPC, line)
403 def dispatch(self, service_name, method, params):
405 self.log('service', service_name)
406 self.log('method', method)
407 self.log('params', params)
408 auth = getattr(self, 'auth_provider', None)
409 result = ExportService.getService(service_name).dispatch(method, auth, params)
410 self.log('result', result)
411 # We shouldn't marshall None,
416 self.log('exception', tools.exception_to_unicode(e))
417 tb = getattr(e, 'traceback', sys.exc_info())
418 tb_s = "".join(traceback.format_exception(*tb))
419 if tools.config['debug_mode']:
421 pdb.post_mortem(tb[2])
422 raise OpenERPDispatcherException(e, tb_s)
424 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: