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 ##############################################################################
27 import SimpleXMLRPCServer
30 import logging.handlers
40 class Service(object):
41 """ Base class for *Local* services
43 Functionality here is trusted, no authentication.
46 def __init__(self, name, audience=''):
47 Service._services[name] = self
51 def joinGroup(self, name):
52 raise Exception("No group for local services")
53 #GROUPS.setdefault(name, {})[self.__name] = self
55 def service_exist(self,name):
56 return Service._services.has_key(name)
58 def exportMethod(self, method):
60 self._methods[method.__name__] = method
62 def abortResponse(self, error, description, origin, details):
63 if not tools.config['debug_mode']:
64 raise Exception("%s -- %s\n\n%s"%(origin, description, details))
68 class LocalService(object):
69 """ Proxy for local services.
71 Any instance of this class will behave like the single instance
74 def __init__(self, name):
77 self._service = Service._services[name]
78 for method_name, method_definition in self._service._methods.items():
79 setattr(self, method_name, method_definition)
80 except KeyError, keyError:
81 Logger().notifyChannel('module', LOG_ERROR, 'This service does not exist: %s' % (str(keyError),) )
83 def __call__(self, method, *params):
84 return getattr(self, method)(*params)
86 class ExportService(object):
87 """ Proxy for exported services.
89 All methods here should take an AuthProxy as their first parameter. It
90 will be appended by the calling framework.
92 Note that this class has no direct proxy, capable of calling
93 eservice.method(). Rather, the proxy should call
94 dispatch(method,auth,params)
100 def __init__(self, name, audience=''):
101 ExportService._services[name] = self
104 def joinGroup(self, name):
105 ExportService._groups.setdefault(name, {})[self.__name] = self
108 def getService(cls,name):
109 return cls._services[name]
111 def dispatch(self, method, auth, params):
112 raise Exception("stub dispatch at %s" % self.__name)
114 def new_dispatch(self,method,auth,params):
115 raise Exception("stub dispatch at %s" % self.__name)
117 def abortResponse(self, error, description, origin, details):
118 if not tools.config['debug_mode']:
119 raise Exception("%s -- %s\n\n%s"%(origin, description, details))
123 LOG_NOTSET = 'notset'
124 LOG_DEBUG_RPC = 'debug_rpc'
126 LOG_DEBUG2 = 'debug2'
130 LOG_CRITICAL = 'critical'
132 # add new log level below DEBUG
133 logging.DEBUG2 = logging.DEBUG - 1
134 logging.DEBUG_RPC = logging.DEBUG2 - 1
138 from tools.translate import resetlocale
141 logger = logging.getLogger()
142 # create a format for log messages and dates
143 formatter = logging.Formatter('[%(asctime)s] %(levelname)s:%(name)s:%(message)s')
145 if tools.config['syslog']:
148 handler = logging.handlers.NTEventLogHandler("%s %s" %
149 (release.description,
152 handler = logging.handlers.SysLogHandler('/dev/log')
153 formatter = logging.Formatter("%s %s" % (release.description, release.version) + ':%(levelname)s:%(name)s:%(message)s')
155 elif tools.config['logfile']:
157 logf = tools.config['logfile']
159 dirname = os.path.dirname(logf)
160 if dirname and not os.path.isdir(dirname):
162 if tools.config['logrotate'] is not False:
163 handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
164 elif os.name == 'posix':
165 handler = logging.handlers.WatchedFileHandler(logf)
167 handler = logging.handlers.FileHandler(logf)
168 except Exception, ex:
169 sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
170 handler = logging.StreamHandler(sys.stdout)
172 # Normal Handler on standard output
173 handler = logging.StreamHandler(sys.stdout)
176 # tell the handler to use this format
177 handler.setFormatter(formatter)
179 # add the handler to the root logger
180 logger.addHandler(handler)
181 logger.setLevel(int(tools.config['log_level'] or '0'))
183 if (not isinstance(handler, logging.FileHandler)) and os.name != 'nt':
184 # change color of level names
185 # uses of ANSI color codes
186 # see http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
187 # maybe use http://code.activestate.com/recipes/574451/
188 colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', None, 'default']
189 foreground = lambda f: 30 + colors.index(f)
190 background = lambda f: 40 + colors.index(f)
193 'DEBUG_RPC': ('blue', 'white'),
194 'DEBUG2': ('green', 'white'),
195 'DEBUG': ('blue', 'default'),
196 'INFO': ('green', 'default'),
197 'WARNING': ('yellow', 'default'),
198 'ERROR': ('red', 'default'),
199 'CRITICAL': ('white', 'red'),
202 for level, (fg, bg) in mapping.items():
203 msg = "\x1b[%dm\x1b[%dm%s\x1b[0m" % (foreground(fg), background(bg), level)
204 logging.addLevelName(getattr(logging, level), msg)
207 class Logger(object):
209 def notifyChannel(self, name, level, msg):
210 from service.web_services import common
212 log = logging.getLogger(tools.ustr(name))
214 if level == LOG_DEBUG2 and not hasattr(log, level):
215 fct = lambda msg, *args, **kwargs: log.log(logging.DEBUG2, msg, *args, **kwargs)
216 setattr(log, LOG_DEBUG2, fct)
218 if level == LOG_DEBUG_RPC and not hasattr(log, level):
219 fct = lambda msg, *args, **kwargs: log.log(logging.DEBUG_RPC, msg, *args, **kwargs)
220 setattr(log, LOG_DEBUG_RPC, fct)
222 level_method = getattr(log, level)
224 if isinstance(msg, Exception):
225 msg = tools.exception_to_unicode(msg)
228 msg = tools.ustr(msg).strip()
229 if level in (LOG_ERROR,LOG_CRITICAL) and tools.config.get_misc('debug','env_info',False):
230 msg = common().exp_get_server_environment() + "\n" + msg
232 result = msg.split('\n')
233 except UnicodeDecodeError:
234 result = msg.strip().split('\n')
237 for idx, s in enumerate(result):
238 level_method('[%02d]: %s' % (idx+1, s,))
240 level_method(result[0])
242 # TODO: perhaps reset the logger streams?
243 #if logrotate closes our files, we end up here..
246 # better ignore the exception and carry on..
249 def set_loglevel(self, level):
250 log = logging.getLogger()
251 log.setLevel(logging.INFO) # make sure next msg is printed
252 log.info("Log level changed to %s" % logging.getLevelName(level))
265 def setAlarm(self, fn, dt, db_name, *args, **kwargs):
266 wait = dt - time.time()
268 self._logger.notifyChannel('timers', LOG_DEBUG, "Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
269 timer = threading.Timer(wait, fn, args, kwargs)
271 self._timers.setdefault(db_name, []).append(timer)
273 for db in self._timers:
274 for timer in self._timers[db]:
275 if not timer.isAlive():
276 self._timers[db].remove(timer)
279 def cancel(cls, db_name):
280 """Cancel all timers for a given database. If None passed, all timers are cancelled"""
281 for db in cls._timers:
282 if db_name is None or db == db_name:
283 for timer in cls._timers[db]:
293 """ Generic interface for all servers with an event loop etc.
294 Override this to impement http, net-rpc etc. servers.
296 Servers here must have threaded behaviour. start() must not block,
303 if Server.__is_started:
304 raise Exception('All instances of servers must be inited before the startAll()')
305 Server.__servers.append(self)
308 print "called stub Server.start"
312 print "called stub Server.stop"
316 """ This function should return statistics about the server """
317 return "%s: No statistics" % str(self.__class__)
323 Logger().notifyChannel("services", LOG_INFO,
324 "Starting %d services" % len(cls.__servers))
325 for srv in cls.__servers:
327 cls.__is_started = True
331 if not cls.__is_started:
333 Logger().notifyChannel("services", LOG_INFO,
334 "Stopping %d services" % len(cls.__servers))
335 for srv in cls.__servers:
337 cls.__is_started = False
343 res += "Servers started\n"
345 res += "Servers stopped\n"
346 for srv in cls.__servers:
348 res += srv.stats() + "\n"
353 class OpenERPDispatcherException(Exception):
354 def __init__(self, exception, traceback):
355 self.exception = exception
356 self.traceback = traceback
358 class OpenERPDispatcher:
359 def log(self, title, msg):
360 from pprint import pformat
361 Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
363 def dispatch(self, service_name, method, params):
365 self.log('service', service_name)
366 self.log('method', method)
367 self.log('params', params)
368 if hasattr(self,'auth_provider'):
369 auth = self.auth_provider
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 if hasattr(e, 'traceback'):
384 tb_s = "".join(traceback.format_exception(*tb))
385 if tools.config['debug_mode']:
387 pdb.post_mortem(tb[2])
388 raise OpenERPDispatcherException(e, tb_s)
390 class GenericXMLRPCRequestHandler(OpenERPDispatcher):
391 def _dispatch(self, method, params):
393 service_name = self.path.split("/")[-1]
394 return self.dispatch(service_name, method, params)
395 except OpenERPDispatcherException, e:
396 raise xmlrpclib.Fault(tools.exception_to_unicode(e.exception), e.traceback)
398 class SSLSocket(object):
399 def __init__(self, socket):
400 if not hasattr(socket, 'sock_shutdown'):
401 from OpenSSL import SSL
402 ctx = SSL.Context(SSL.SSLv23_METHOD)
403 ctx.use_privatekey_file(tools.config['secure_pkey_file'])
404 ctx.use_certificate_file(tools.config['secure_cert_file'])
405 self.socket = SSL.Connection(ctx, socket)
409 def shutdown(self, how):
410 return self.socket.sock_shutdown(how)
412 def __getattr__(self, name):
413 return getattr(self.socket, name)
415 class SimpleXMLRPCRequestHandler(GenericXMLRPCRequestHandler, SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
416 rpc_paths = map(lambda s: '/xmlrpc/%s' % s, GROUPS.get('web-services', {}).keys())
418 class SecureXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
420 self.connection = SSLSocket(self.request)
421 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
422 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
424 class SimpleThreadedXMLRPCServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer.SimpleXMLRPCServer):
425 def server_bind(self):
426 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
427 SimpleXMLRPCServer.SimpleXMLRPCServer.server_bind(self)
429 class SecureThreadedXMLRPCServer(SimpleThreadedXMLRPCServer):
430 def __init__(self, server_address, HandlerClass, logRequests=1):
431 SimpleThreadedXMLRPCServer.__init__(self, server_address, HandlerClass, logRequests)
432 self.socket = SSLSocket(socket.socket(self.address_family, self.socket_type))
434 self.server_activate()
436 class HttpDaemon(threading.Thread):
437 def __init__(self, interface, port, secure=False):
438 threading.Thread.__init__(self)
440 self.__interface = interface
441 self.secure = bool(secure)
442 handler_class = (SimpleXMLRPCRequestHandler, SecureXMLRPCRequestHandler)[self.secure]
443 server_class = (SimpleThreadedXMLRPCServer, SecureThreadedXMLRPCServer)[self.secure]
446 from OpenSSL.SSL import Error as SSLError
448 class SSLError(Exception): pass
450 self.server = server_class((interface, port), handler_class, 0)
452 Logger().notifyChannel('xml-rpc-ssl', LOG_CRITICAL, "Can not load the certificate and/or the private key files")
455 Logger().notifyChannel('xml-rpc', LOG_CRITICAL, "Error occur when starting the server daemon: %s" % (e,))
459 def attach(self, path, gw):
466 self.server.socket.shutdown(
467 hasattr(socket, 'SHUT_RDWR') and socket.SHUT_RDWR or 2)
468 except socket.error, e:
469 if e.errno != 57: raise
470 # OSX, socket shutdowns both sides if any side closes it
471 # causing an error 57 'Socket is not connected' on shutdown
472 # of the other side (or something), see
473 # http://bugs.python.org/issue4397
474 Logger().notifyChannel(
476 '"%s" when shutting down server socket, '
477 'this is normal under OS X'%e)
478 self.server.socket.close()
481 self.server.register_introspection_functions()
485 self.server.handle_request()
488 # If the server need to be run recursively
490 #signal.signal(signal.SIGALRM, self.my_handler)
493 # self.server.handle_request()
494 #signal.alarm(0) # Disable the alarm
497 class TinySocketClientThread(threading.Thread, OpenERPDispatcher):
498 def __init__(self, sock, threads):
499 threading.Thread.__init__(self)
501 self.threads = threads
507 ts = tiny_socket.mysocket(self.sock)
510 self.threads.remove(self)
517 self.threads.remove(self)
520 result = self.dispatch(msg[0], msg[1], msg[2:])
522 except OpenERPDispatcherException, e:
523 new_e = Exception(tools.exception_to_unicode(e.exception)) # avoid problems of pickeling
524 ts.mysend(new_e, exception=True, traceback=e.traceback)
527 self.threads.remove(self)
534 class TinySocketServerThread(threading.Thread):
535 def __init__(self, interface, port, secure=False):
536 threading.Thread.__init__(self)
538 self.__interface = interface
539 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
540 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
541 self.socket.bind((self.__interface, self.__port))
542 self.socket.listen(5)
550 (clientsocket, address) = self.socket.accept()
551 ct = TinySocketClientThread(clientsocket, self.threads)
552 self.threads.append(ct)
561 for t in self.threads:
564 if hasattr(socket, 'SHUT_RDWR'):
565 self.socket.shutdown(socket.SHUT_RDWR)
567 self.socket.shutdown(2)
572 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: