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" % (release.description, release.version))
156 handler = logging.handlers.SysLogHandler('/dev/log')
157 formatter = logging.Formatter("%s %s" % (release.description, release.version) + ':%(levelname)s:%(name)s:%(message)s')
159 elif tools.config['logfile']:
161 logf = tools.config['logfile']
163 dirname = os.path.dirname(logf)
164 if dirname and not os.path.isdir(dirname):
166 if tools.config['logrotate'] is not False:
167 handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
168 elif os.name == 'posix':
169 handler = logging.handlers.WatchedFileHandler(logf)
171 handler = logging.handlers.FileHandler(logf)
172 except Exception, ex:
173 sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
174 handler = logging.StreamHandler(sys.stdout)
176 # Normal Handler on standard output
177 handler = logging.StreamHandler(sys.stdout)
180 # tell the handler to use this format
181 handler.setFormatter(formatter)
183 # add the handler to the root logger
184 logger.addHandler(handler)
185 logger.setLevel(int(tools.config['log_level'] or '0'))
187 if (not isinstance(handler, logging.FileHandler)) and os.name != 'nt':
188 # change color of level names
189 # uses of ANSI color codes
190 # see http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
191 # maybe use http://code.activestate.com/recipes/574451/
192 colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', None, 'default']
193 foreground = lambda f: 30 + colors.index(f)
194 background = lambda f: 40 + colors.index(f)
197 'DEBUG_RPC': ('blue', 'white'),
198 'DEBUG': ('blue', 'default'),
199 'INFO': ('green', 'default'),
200 'TEST': ('white', 'blue'),
201 'WARNING': ('yellow', 'default'),
202 'ERROR': ('red', 'default'),
203 'CRITICAL': ('white', 'red'),
206 for level, (fg, bg) in mapping.items():
207 msg = "\x1b[%dm\x1b[%dm%s\x1b[0m" % (foreground(fg), background(bg), level)
208 # jth: we should not override default level name
209 # jth: use this instead http://stackoverflow.com/questions/384076/how-can-i-make-the-python-logging-output-to-be-colored
210 # jth: also, do not output ANSI if terminal doesn't support it (test it in Eclipse console and on windows)
211 #logging.addLevelName(getattr(logging, level), msg)
214 class Logger(object):
216 warnings.warn("The netsvc.Logger API shouldn't be used anymore, please "
217 "use the standard `logging.getLogger` API instead",
218 PendingDeprecationWarning, stacklevel=2)
219 super(Logger, self).__init__()
221 def notifyChannel(self, name, level, msg):
222 warnings.warn("notifyChannel API shouldn't be used anymore, please use "
223 "the standard `logging` module instead",
224 PendingDeprecationWarning, stacklevel=2)
225 from service.web_services import common
227 log = logging.getLogger(tools.ustr(name))
229 if level in [LOG_DEBUG_RPC, LOG_TEST] and not hasattr(log, level):
230 fct = lambda msg, *args, **kwargs: log.log(getattr(logging, level.upper()), msg, *args, **kwargs)
231 setattr(log, level, fct)
234 level_method = getattr(log, level)
236 if isinstance(msg, Exception):
237 msg = tools.exception_to_unicode(msg)
240 msg = tools.ustr(msg).strip()
241 if level in (LOG_ERROR, LOG_CRITICAL) and tools.config.get_misc('debug','env_info',False):
242 msg = common().exp_get_server_environment() + "\n" + msg
244 result = msg.split('\n')
245 except UnicodeDecodeError:
246 result = msg.strip().split('\n')
249 for idx, s in enumerate(result):
250 level_method('[%02d]: %s' % (idx+1, s,))
252 level_method(result[0])
254 # TODO: perhaps reset the logger streams?
255 #if logrotate closes our files, we end up here..
258 # better ignore the exception and carry on..
261 def set_loglevel(self, level):
262 log = logging.getLogger()
263 log.setLevel(logging.INFO) # make sure next msg is printed
264 log.info("Log level changed to %s" % logging.getLevelName(level))
277 __logger = logging.getLogger('timer')
279 def setAlarm(self, fn, dt, db_name, *args, **kwargs):
280 wait = dt - time.time()
282 self.__logger.debug("Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
283 timer = threading.Timer(wait, fn, args, kwargs)
285 self._timers.setdefault(db_name, []).append(timer)
287 for db in self._timers:
288 for timer in self._timers[db]:
289 if not timer.isAlive():
290 self._timers[db].remove(timer)
293 def cancel(cls, db_name):
294 """Cancel all timers for a given database. If None passed, all timers are cancelled"""
295 for db in cls._timers:
296 if db_name is None or db == db_name:
297 for timer in cls._timers[db]:
307 """ Generic interface for all servers with an event loop etc.
308 Override this to impement http, net-rpc etc. servers.
310 Servers here must have threaded behaviour. start() must not block,
317 __logger = logging.getLogger('server')
320 if Server.__is_started:
321 raise Exception('All instances of servers must be inited before the startAll()')
322 Server.__servers.append(self)
325 self.__logger.debug("called stub Server.start")
328 self.__logger.debug("called stub Server.stop")
331 """ This function should return statistics about the server """
332 return "%s: No statistics" % str(self.__class__)
338 cls.__logger.info("Starting %d services" % len(cls.__servers))
339 for srv in cls.__servers:
341 cls.__is_started = True
345 if not cls.__is_started:
347 cls.__logger.info("Stopping %d services" % len(cls.__servers))
348 for srv in cls.__servers:
350 cls.__is_started = False
354 res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
355 res.extend(srv.stats() for srv in cls.__servers)
356 return '\n'.join(res)
358 class OpenERPDispatcherException(Exception):
359 def __init__(self, exception, traceback):
360 self.exception = exception
361 self.traceback = traceback
363 class OpenERPDispatcher:
364 def log(self, title, msg):
365 Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
367 def dispatch(self, service_name, method, params):
369 self.log('service', service_name)
370 self.log('method', method)
371 self.log('params', params)
372 auth = getattr(self, 'auth_provider', None)
373 result = ExportService.getService(service_name).dispatch(method, auth, params)
374 self.log('result', result)
375 # We shouldn't marshall None,
380 self.log('exception', tools.exception_to_unicode(e))
381 tb = getattr(e, 'traceback', sys.exc_info())
382 tb_s = "".join(traceback.format_exception(*tb))
383 if tools.config['debug_mode']:
385 pdb.post_mortem(tb[2])
386 raise OpenERPDispatcherException(e, tb_s)
388 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: