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_RPC = 'debug_rpc'
137 LOG_CRITICAL = 'critical'
139 logging.DEBUG_RPC = logging.DEBUG - 2
140 logging.addLevelName(logging.DEBUG_RPC, 'DEBUG_RPC')
142 logging.TEST = logging.INFO - 5
143 logging.addLevelName(logging.TEST, 'TEST')
145 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10)
146 #The background is set with 40 plus the number of the color, and the foreground with 30
147 #These are the sequences need to get colored ouput
148 RESET_SEQ = "\033[0m"
149 COLOR_SEQ = "\033[1;%dm"
151 COLOR_PATTERN = "%s%s%%s%s" % (COLOR_SEQ, COLOR_SEQ, RESET_SEQ)
152 LEVEL_COLOR_MAPPING = {
153 logging.DEBUG_RPC: (BLUE, WHITE),
154 logging.DEBUG: (BLUE, DEFAULT),
155 logging.INFO: (GREEN, DEFAULT),
156 logging.TEST: (WHITE, BLUE),
157 logging.WARNING: (YELLOW, DEFAULT),
158 logging.ERROR: (RED, DEFAULT),
159 logging.CRITICAL: (WHITE, RED),
162 class ColoredFormatter(logging.Formatter):
163 def format(self, record):
164 fg_color, bg_color = LEVEL_COLOR_MAPPING[record.levelno]
165 record.levelname = COLOR_PATTERN % (30 + fg_color, 40 + bg_color, record.levelname)
166 return logging.Formatter.format(self, record)
171 from tools.translate import resetlocale
174 logger = logging.getLogger()
175 # create a format for log messages and dates
176 format = '[%(asctime)s] %(levelname)s:%(name)s:%(message)s'
178 if tools.config['syslog']:
181 handler = logging.handlers.NTEventLogHandler("%s %s" % (release.description, release.version))
183 handler = logging.handlers.SysLogHandler('/dev/log')
184 format = '%s %s' % (release.description, release.version) \
185 + ':%(levelname)s:%(name)s:%(message)s'
187 elif tools.config['logfile']:
189 logf = tools.config['logfile']
191 dirname = os.path.dirname(logf)
192 if dirname and not os.path.isdir(dirname):
194 if tools.config['logrotate'] is not False:
195 handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
196 elif os.name == 'posix':
197 handler = logging.handlers.WatchedFileHandler(logf)
199 handler = logging.handlers.FileHandler(logf)
200 except Exception, ex:
201 sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
202 handler = logging.StreamHandler(sys.stdout)
204 # Normal Handler on standard output
205 handler = logging.StreamHandler(sys.stdout)
207 if isinstance(handler, logging.StreamHandler) and os.isatty(handler.stream.fileno()):
208 formatter = ColoredFormatter(format)
210 formatter = logging.Formatter(format)
211 handler.setFormatter(formatter)
213 # add the handler to the root logger
214 logger.addHandler(handler)
215 logger.setLevel(int(tools.config['log_level'] or '0'))
219 class Logger(object):
221 warnings.warn("The netsvc.Logger API shouldn't be used anymore, please "
222 "use the standard `logging.getLogger` API instead",
223 PendingDeprecationWarning, stacklevel=2)
224 super(Logger, self).__init__()
226 def notifyChannel(self, name, level, msg):
227 warnings.warn("notifyChannel API shouldn't be used anymore, please use "
228 "the standard `logging` module instead",
229 PendingDeprecationWarning, stacklevel=2)
230 from service.web_services import common
232 log = logging.getLogger(tools.ustr(name))
234 if level in [LOG_DEBUG_RPC, LOG_TEST] and not hasattr(log, level):
235 fct = lambda msg, *args, **kwargs: log.log(getattr(logging, level.upper()), msg, *args, **kwargs)
236 setattr(log, level, fct)
239 level_method = getattr(log, level)
241 if isinstance(msg, Exception):
242 msg = tools.exception_to_unicode(msg)
245 msg = tools.ustr(msg).strip()
246 if level in (LOG_ERROR, LOG_CRITICAL) and tools.config.get_misc('debug','env_info',False):
247 msg = common().exp_get_server_environment() + "\n" + msg
249 result = msg.split('\n')
250 except UnicodeDecodeError:
251 result = msg.strip().split('\n')
254 for idx, s in enumerate(result):
255 level_method('[%02d]: %s' % (idx+1, s,))
257 level_method(result[0])
259 # TODO: perhaps reset the logger streams?
260 #if logrotate closes our files, we end up here..
263 # better ignore the exception and carry on..
266 def set_loglevel(self, level):
267 log = logging.getLogger()
268 log.setLevel(logging.INFO) # make sure next msg is printed
269 log.info("Log level changed to %s" % logging.getLevelName(level))
282 __logger = logging.getLogger('timer')
284 def setAlarm(self, fn, dt, db_name, *args, **kwargs):
285 wait = dt - time.time()
287 self.__logger.debug("Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
288 timer = threading.Timer(wait, fn, args, kwargs)
290 self._timers.setdefault(db_name, []).append(timer)
292 for db in self._timers:
293 for timer in self._timers[db]:
294 if not timer.isAlive():
295 self._timers[db].remove(timer)
298 def cancel(cls, db_name):
299 """Cancel all timers for a given database. If None passed, all timers are cancelled"""
300 for db in cls._timers:
301 if db_name is None or db == db_name:
302 for timer in cls._timers[db]:
312 """ Generic interface for all servers with an event loop etc.
313 Override this to impement http, net-rpc etc. servers.
315 Servers here must have threaded behaviour. start() must not block,
322 __logger = logging.getLogger('server')
325 if Server.__is_started:
326 raise Exception('All instances of servers must be inited before the startAll()')
327 Server.__servers.append(self)
330 self.__logger.debug("called stub Server.start")
333 self.__logger.debug("called stub Server.stop")
336 """ This function should return statistics about the server """
337 return "%s: No statistics" % str(self.__class__)
343 cls.__logger.info("Starting %d services" % len(cls.__servers))
344 for srv in cls.__servers:
346 cls.__is_started = True
350 if not cls.__is_started:
352 cls.__logger.info("Stopping %d services" % len(cls.__servers))
353 for srv in cls.__servers:
355 cls.__is_started = False
359 res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
360 res.extend(srv.stats() for srv in cls.__servers)
361 return '\n'.join(res)
363 def _close_socket(self):
366 self.socket.shutdown(getattr(socket, 'SHUT_RDWR', 2))
367 except socket.error, e:
368 if e.errno != errno.ENOTCONN: raise
369 # OSX, socket shutdowns both sides if any side closes it
370 # causing an error 57 'Socket is not connected' on shutdown
371 # of the other side (or something), see
372 # http://bugs.python.org/issue4397
374 '"%s" when shutting down server socket, '
375 'this is normal under OS X', e)
378 class OpenERPDispatcherException(Exception):
379 def __init__(self, exception, traceback):
380 self.exception = exception
381 self.traceback = traceback
383 class OpenERPDispatcher:
384 def log(self, title, msg):
385 if tools.config['log_level'] == logging.DEBUG_RPC:
386 Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
388 def dispatch(self, service_name, method, params):
390 self.log('service', service_name)
391 self.log('method', method)
392 self.log('params', params)
393 auth = getattr(self, 'auth_provider', None)
394 result = ExportService.getService(service_name).dispatch(method, auth, params)
395 self.log('result', result)
396 # We shouldn't marshall None,
401 self.log('exception', tools.exception_to_unicode(e))
402 tb = getattr(e, 'traceback', sys.exc_info())
403 tb_s = "".join(traceback.format_exception(*tb))
404 if tools.config['debug_mode']:
406 pdb.post_mortem(tb[2])
407 raise OpenERPDispatcherException(e, tb_s)
409 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: