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 ##############################################################################
28 import logging.handlers
33 from pprint import pformat
36 class Service(object):
37 """ Base class for *Local* services
39 Functionality here is trusted, no authentication.
42 def __init__(self, name, audience=''):
43 Service._services[name] = self
47 def joinGroup(self, name):
48 raise Exception("No group for local services")
49 #GROUPS.setdefault(name, {})[self.__name] = self
52 def exists(cls, name):
53 return name in cls._services
56 def remove(cls, name):
58 cls._services.pop(name)
60 def exportMethod(self, method):
62 self._methods[method.__name__] = method
64 def abortResponse(self, error, description, origin, details):
65 if not tools.config['debug_mode']:
66 raise Exception("%s -- %s\n\n%s"%(origin, description, details))
70 class LocalService(object):
71 """ Proxy for local services.
73 Any instance of this class will behave like the single instance
76 __logger = logging.getLogger('service')
77 def __init__(self, name):
80 self._service = Service._services[name]
81 for method_name, method_definition in self._service._methods.items():
82 setattr(self, method_name, method_definition)
83 except KeyError, keyError:
84 self.__logger.error('This service does not exist: %s' % (str(keyError),) )
87 def __call__(self, method, *params):
88 return getattr(self, method)(*params)
90 class ExportService(object):
91 """ Proxy for exported services.
93 All methods here should take an AuthProxy as their first parameter. It
94 will be appended by the calling framework.
96 Note that this class has no direct proxy, capable of calling
97 eservice.method(). Rather, the proxy should call
98 dispatch(method,auth,params)
104 def __init__(self, name, audience=''):
105 ExportService._services[name] = self
108 def joinGroup(self, name):
109 ExportService._groups.setdefault(name, {})[self.__name] = self
112 def getService(cls,name):
113 return cls._services[name]
115 def dispatch(self, method, auth, params):
116 raise Exception("stub dispatch at %s" % self.__name)
118 def new_dispatch(self,method,auth,params):
119 raise Exception("stub dispatch at %s" % self.__name)
121 def abortResponse(self, error, description, origin, details):
122 if not tools.config['debug_mode']:
123 raise Exception("%s -- %s\n\n%s"%(origin, description, details))
127 LOG_NOTSET = 'notset'
128 LOG_DEBUG_RPC = 'debug_rpc'
134 LOG_CRITICAL = 'critical'
136 logging.DEBUG_RPC = logging.DEBUG - 2
137 logging.addLevelName(logging.DEBUG_RPC, 'DEBUG_RPC')
139 logging.TEST = logging.INFO - 5
140 logging.addLevelName(logging.TEST, 'TEST')
144 from tools.translate import resetlocale
147 logger = logging.getLogger()
148 # create a format for log messages and dates
149 formatter = logging.Formatter('[%(asctime)s] %(levelname)s:%(name)s:%(message)s')
151 if tools.config['syslog']:
154 handler = logging.handlers.NTEventLogHandler("%s %s" %
155 (release.description,
158 handler = logging.handlers.SysLogHandler('/dev/log')
159 formatter = logging.Formatter("%s %s" % (release.description, release.version) + ':%(levelname)s:%(name)s:%(message)s')
161 elif tools.config['logfile']:
163 logf = tools.config['logfile']
165 dirname = os.path.dirname(logf)
166 if dirname and not os.path.isdir(dirname):
168 if tools.config['logrotate'] is not False:
169 handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
170 elif os.name == 'posix':
171 handler = logging.handlers.WatchedFileHandler(logf)
173 handler = logging.handlers.FileHandler(logf)
174 except Exception, ex:
175 sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
176 handler = logging.StreamHandler(sys.stdout)
178 # Normal Handler on standard output
179 handler = logging.StreamHandler(sys.stdout)
182 # tell the handler to use this format
183 handler.setFormatter(formatter)
185 # add the handler to the root logger
186 logger.addHandler(handler)
187 logger.setLevel(int(tools.config['log_level'] or '0'))
189 if (not isinstance(handler, logging.FileHandler)) and os.name != 'nt':
190 # change color of level names
191 # uses of ANSI color codes
192 # see http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
193 # maybe use http://code.activestate.com/recipes/574451/
194 colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', None, 'default']
195 foreground = lambda f: 30 + colors.index(f)
196 background = lambda f: 40 + colors.index(f)
199 'DEBUG_RPC': ('blue', 'white'),
200 'DEBUG': ('blue', 'default'),
201 'INFO': ('green', 'default'),
202 'TEST': ('white', 'blue'),
203 'WARNING': ('yellow', 'default'),
204 'ERROR': ('red', 'default'),
205 'CRITICAL': ('white', 'red'),
208 for level, (fg, bg) in mapping.items():
209 msg = "\x1b[%dm\x1b[%dm%s\x1b[0m" % (foreground(fg), background(bg), level)
210 logging.addLevelName(getattr(logging, level), msg)
213 class Logger(object):
215 warnings.warn("The netsvc.Logger API shouldn't be used anymore, please "
216 "use the standard `logging.getLogger` API instead",
217 PendingDeprecationWarning, stacklevel=2)
218 super(Logger, self).__init__()
220 def notifyChannel(self, name, level, msg):
221 warnings.warn("notifyChannel API shouldn't be used anymore, please use "
222 "the standard `logging` module instead",
223 PendingDeprecationWarning, stacklevel=2)
224 from service.web_services import common
226 log = logging.getLogger(tools.ustr(name))
228 if level in [LOG_DEBUG_RPC, LOG_TEST] and not hasattr(log, level):
229 fct = lambda msg, *args, **kwargs: log.log(getattr(logging, level.upper()), msg, *args, **kwargs)
230 setattr(log, level, fct)
233 level_method = getattr(log, level)
235 if isinstance(msg, Exception):
236 msg = tools.exception_to_unicode(msg)
239 msg = tools.ustr(msg).strip()
240 if level in (LOG_ERROR, LOG_CRITICAL) and tools.config.get_misc('debug','env_info',False):
241 msg = common().exp_get_server_environment() + "\n" + msg
243 result = msg.split('\n')
244 except UnicodeDecodeError:
245 result = msg.strip().split('\n')
248 for idx, s in enumerate(result):
249 level_method('[%02d]: %s' % (idx+1, s,))
251 level_method(result[0])
253 # TODO: perhaps reset the logger streams?
254 #if logrotate closes our files, we end up here..
257 # better ignore the exception and carry on..
260 def set_loglevel(self, level):
261 log = logging.getLogger()
262 log.setLevel(logging.INFO) # make sure next msg is printed
263 log.info("Log level changed to %s" % logging.getLevelName(level))
276 __logger = logging.getLogger('timer')
278 def setAlarm(self, fn, dt, db_name, *args, **kwargs):
279 wait = dt - time.time()
281 self.__logger.debug("Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
282 timer = threading.Timer(wait, fn, args, kwargs)
284 self._timers.setdefault(db_name, []).append(timer)
286 for db in self._timers:
287 for timer in self._timers[db]:
288 if not timer.isAlive():
289 self._timers[db].remove(timer)
292 def cancel(cls, db_name):
293 """Cancel all timers for a given database. If None passed, all timers are cancelled"""
294 for db in cls._timers:
295 if db_name is None or db == db_name:
296 for timer in cls._timers[db]:
306 """ Generic interface for all servers with an event loop etc.
307 Override this to impement http, net-rpc etc. servers.
309 Servers here must have threaded behaviour. start() must not block,
316 __logger = logging.getLogger('server')
319 if Server.__is_started:
320 raise Exception('All instances of servers must be inited before the startAll()')
321 Server.__servers.append(self)
324 self.__logger.debug("called stub Server.start")
327 self.__logger.debug("called stub Server.stop")
330 """ This function should return statistics about the server """
331 return "%s: No statistics" % str(self.__class__)
337 cls.__logger.info("Starting %d services" % len(cls.__servers))
338 for srv in cls.__servers:
340 cls.__is_started = True
344 if not cls.__is_started:
346 cls.__logger.info("Stopping %d services" % len(cls.__servers))
347 for srv in cls.__servers:
349 cls.__is_started = False
353 res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
354 res.extend(srv.stats() for srv in cls.__servers)
355 return '\n'.join(res)
357 class OpenERPDispatcherException(Exception):
358 def __init__(self, exception, traceback):
359 self.exception = exception
360 self.traceback = traceback
362 class OpenERPDispatcher:
363 def log(self, title, msg):
364 Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
366 def dispatch(self, service_name, method, params):
368 self.log('service', service_name)
369 self.log('method', method)
370 self.log('params', params)
371 auth = getattr(self, 'auth_provider', None)
372 result = ExportService.getService(service_name).dispatch(method, auth, params)
373 self.log('result', result)
374 # We shouldn't marshall None,
379 self.log('exception', tools.exception_to_unicode(e))
380 tb = getattr(e, 'traceback', sys.exc_info())
381 tb_s = "".join(traceback.format_exception(*tb))
382 if tools.config['debug_mode']:
384 pdb.post_mortem(tb[2])
385 raise OpenERPDispatcherException(e, tb_s)
387 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: