1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 # The refactoring about the OpenSSL support come from Tryton
7 # Copyright (C) 2007-2009 Cédric Krier.
8 # Copyright (C) 2007-2009 Bertrand Chenal.
9 # Copyright (C) 2008 B2CK SPRL.
11 # This program is free software: you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation, either version 3 of the License, or
14 # (at your option) any later version.
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 ##############################################################################
26 import SimpleXMLRPCServer
29 import logging.handlers
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
54 def service_exist(self,name):
55 return Service._services.has_key(name)
57 def exportMethod(self, method):
59 self._methods[method.__name__] = method
61 def abortResponse(self, error, description, origin, details):
62 if not tools.config['debug_mode']:
63 raise Exception("%s -- %s\n\n%s"%(origin, description, details))
67 class LocalService(object):
68 """ Proxy for local services.
70 Any instance of this class will behave like the single instance
73 def __init__(self, name):
76 self._service = Service._services[name]
77 for method_name, method_definition in self._service._methods.items():
78 setattr(self, method_name, method_definition)
79 except KeyError, keyError:
80 Logger().notifyChannel('module', LOG_ERROR, 'This service does not exist: %s' % (str(keyError),) )
82 def __call__(self, method, *params):
83 return getattr(self, method)(*params)
85 class ExportService(object):
86 """ Proxy for exported services.
88 All methods here should take an AuthProxy as their first parameter. It
89 will be appended by the calling framework.
91 Note that this class has no direct proxy, capable of calling
92 eservice.method(). Rather, the proxy should call
93 dispatch(method,auth,params)
99 def __init__(self, name, audience=''):
100 ExportService._services[name] = self
103 def joinGroup(self, name):
104 ExportService._groups.setdefault(name, {})[self.__name] = self
107 def getService(cls,name):
108 return cls._services[name]
110 def dispatch(self, method, auth, params):
111 raise Exception("stub dispatch at %s" % self.__name)
113 def new_dispatch(self,method,auth,params):
114 raise Exception("stub dispatch at %s" % self.__name)
116 def abortResponse(self, error, description, origin, details):
117 if not tools.config['debug_mode']:
118 raise Exception("%s -- %s\n\n%s"%(origin, description, details))
122 LOG_NOTSET = 'notset'
123 LOG_DEBUG_RPC = 'debug_rpc'
125 LOG_DEBUG2 = 'debug2'
129 LOG_CRITICAL = 'critical'
131 # add new log level below DEBUG
132 logging.DEBUG2 = logging.DEBUG - 1
133 logging.DEBUG_RPC = logging.DEBUG2 - 1
137 from tools.translate import resetlocale
140 logger = logging.getLogger()
141 # create a format for log messages and dates
142 formatter = logging.Formatter('[%(asctime)s] %(levelname)s:%(name)s:%(message)s')
144 if tools.config['syslog']:
147 handler = logging.handlers.NTEventLogHandler("%s %s" %
148 (release.description,
151 handler = logging.handlers.SysLogHandler('/dev/log')
152 formatter = logging.Formatter("%s %s" % (release.description, release.version) + ':%(levelname)s:%(name)s:%(message)s')
154 elif tools.config['logfile']:
156 logf = tools.config['logfile']
158 dirname = os.path.dirname(logf)
159 if dirname and not os.path.isdir(dirname):
161 if tools.config['logrotate'] is not False:
162 handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
163 elif os.name == 'posix':
164 handler = logging.handlers.WatchedFileHandler(logf)
166 handler = logging.handlers.FileHandler(logf)
167 except Exception, ex:
168 sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
169 handler = logging.StreamHandler(sys.stdout)
171 # Normal Handler on standard output
172 handler = logging.StreamHandler(sys.stdout)
175 # tell the handler to use this format
176 handler.setFormatter(formatter)
178 # add the handler to the root logger
179 logger.addHandler(handler)
180 logger.setLevel(int(tools.config['log_level'] or '0'))
182 if (not isinstance(handler, logging.FileHandler)) and os.name != 'nt':
183 # change color of level names
184 # uses of ANSI color codes
185 # see http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
186 # maybe use http://code.activestate.com/recipes/574451/
187 colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', None, 'default']
188 foreground = lambda f: 30 + colors.index(f)
189 background = lambda f: 40 + colors.index(f)
192 'DEBUG_RPC': ('blue', 'white'),
193 'DEBUG2': ('green', 'white'),
194 'DEBUG': ('blue', 'default'),
195 'INFO': ('green', 'default'),
196 'WARNING': ('yellow', 'default'),
197 'ERROR': ('red', 'default'),
198 'CRITICAL': ('white', 'red'),
201 for level, (fg, bg) in mapping.items():
202 msg = "\x1b[%dm\x1b[%dm%s\x1b[0m" % (foreground(fg), background(bg), level)
203 logging.addLevelName(getattr(logging, level), msg)
206 class Logger(object):
208 def notifyChannel(self, name, level, msg):
209 from service.web_services import common
211 log = logging.getLogger(tools.ustr(name))
213 if level == LOG_DEBUG2 and not hasattr(log, level):
214 fct = lambda msg, *args, **kwargs: log.log(logging.DEBUG2, msg, *args, **kwargs)
215 setattr(log, LOG_DEBUG2, fct)
217 if level == LOG_DEBUG_RPC and not hasattr(log, level):
218 fct = lambda msg, *args, **kwargs: log.log(logging.DEBUG_RPC, msg, *args, **kwargs)
219 setattr(log, LOG_DEBUG_RPC, fct)
221 level_method = getattr(log, level)
223 if isinstance(msg, Exception):
224 msg = tools.exception_to_unicode(msg)
227 msg = tools.ustr(msg).strip()
228 if level in (LOG_ERROR,LOG_CRITICAL) and tools.config.get_misc('debug','env_info',False):
229 msg = common().exp_get_server_environment() + "\n" + msg
231 result = msg.split('\n')
232 except UnicodeDecodeError:
233 result = msg.strip().split('\n')
236 for idx, s in enumerate(result):
237 level_method('[%02d]: %s' % (idx+1, s,))
239 level_method(result[0])
241 # TODO: perhaps reset the logger streams?
242 #if logrotate closes our files, we end up here..
245 # better ignore the exception and carry on..
248 def set_loglevel(self, level):
249 log = logging.getLogger()
250 log.setLevel(logging.INFO) # make sure next msg is printed
251 log.info("Log level changed to %s" % logging.getLevelName(level))
264 def setAlarm(self, fn, dt, db_name, *args, **kwargs):
265 wait = dt - time.time()
267 self._logger.notifyChannel('timers', LOG_DEBUG, "Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
268 timer = threading.Timer(wait, fn, args, kwargs)
270 self._timers.setdefault(db_name, []).append(timer)
272 for db in self._timers:
273 for timer in self._timers[db]:
274 if not timer.isAlive():
275 self._timers[db].remove(timer)
278 def cancel(cls, db_name):
279 """Cancel all timers for a given database. If None passed, all timers are cancelled"""
280 for db in cls._timers:
281 if db_name is None or db == db_name:
282 for timer in cls._timers[db]:
292 """ Generic interface for all servers with an event loop etc.
293 Override this to impement http, net-rpc etc. servers.
295 Servers here must have threaded behaviour. start() must not block,
302 if Server.__is_started:
303 raise Exception('All instances of servers must be inited before the startAll()')
304 Server.__servers.append(self)
307 print "called stub Server.start"
311 print "called stub Server.stop"
315 """ This function should return statistics about the server """
316 return "%s: No statistics" % str(self.__class__)
322 Logger().notifyChannel("services", LOG_INFO,
323 "Starting %d services" % len(cls.__servers))
324 for srv in cls.__servers:
326 cls.__is_started = True
330 if not cls.__is_started:
332 Logger().notifyChannel("services", LOG_INFO,
333 "Stopping %d services" % len(cls.__servers))
334 for srv in cls.__servers:
336 cls.__is_started = False
342 res += "Servers started\n"
344 res += "Servers stopped\n"
345 for srv in cls.__servers:
347 res += srv.stats() + "\n"
352 class OpenERPDispatcherException(Exception):
353 def __init__(self, exception, traceback):
354 self.exception = exception
355 self.traceback = traceback
357 class OpenERPDispatcher:
358 def log(self, title, msg):
359 from pprint import pformat
360 Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
362 def dispatch(self, service_name, method, params):
364 self.log('service', service_name)
365 self.log('method', method)
366 self.log('params', params)
367 if hasattr(self,'auth_provider'):
368 auth = self.auth_provider
371 result = ExportService.getService(service_name).dispatch(method, auth, params)
372 self.log('result', result)
373 # We shouldn't marshall None,
378 self.log('exception', tools.exception_to_unicode(e))
379 if hasattr(e, 'traceback'):
383 tb_s = "".join(traceback.format_exception(*tb))
384 if tools.config['debug_mode']:
386 pdb.post_mortem(tb[2])
387 raise OpenERPDispatcherException(e, tb_s)
389 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: