[FIX] reactive the colored logging
[odoo/odoo.git] / bin / netsvc.py
1 #!/usr/bin/python
2 # -*- encoding: utf-8 -*-
3 ##############################################################################
4 #
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.
11 #
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.
16 #
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.
21 #
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/>.
24 #
25 ##############################################################################
26
27
28 import SimpleXMLRPCServer
29 import SocketServer
30 import logging
31 import logging.handlers
32 import os
33 import signal
34 import socket
35 import sys
36 import threading
37 import time
38 import xmlrpclib
39 import release
40
41 SERVICES = {}
42 GROUPS = {}
43
44 class Service(object):
45     def __init__(self, name, audience=''):
46         SERVICES[name] = self
47         self.__name = name
48         self._methods = {}
49
50     def joinGroup(self, name):
51         GROUPS.setdefault(name, {})[self.__name] = self
52
53     def exportMethod(self, method):
54         if callable(method):
55             self._methods[method.__name__] = method
56
57     def abortResponse(self, error, description, origin, details):
58         if not tools.config['debug_mode']:
59             raise Exception("%s -- %s\n\n%s"%(origin, description, details))
60         else:
61             raise
62
63 class LocalService(Service):
64     def __init__(self, name):
65         self.__name = name
66         try:
67             self._service = SERVICES[name]
68             for method_name, method_definition in self._service._methods.items():
69                 setattr(self, method_name, method_definition)
70         except KeyError, keyError:
71             Logger().notifyChannel('module', LOG_ERROR, 'This service does not exists: %s' % (str(keyError),) )
72             raise
73     def __call__(self, method, *params):
74         return getattr(self, method)(*params)
75
76 def service_exist(name):
77     return SERVICES.get(name, False)
78
79 LOG_NOTSET = 'notset'
80 LOG_DEBUG_RPC = 'debug_rpc'
81 LOG_DEBUG = 'debug'
82 LOG_INFO = 'info'
83 LOG_WARNING = 'warn'
84 LOG_ERROR = 'error'
85 LOG_CRITICAL = 'critical'
86
87 # add new log level below DEBUG
88 logging.DEBUG_RPC = logging.DEBUG - 1
89
90 def init_logger():
91     import os
92
93     logger = logging.getLogger()
94     # create a format for log messages and dates
95     formatter = logging.Formatter('[%(asctime)s] %(levelname)s:%(name)s:%(message)s', '%a %b %d %Y %H:%M:%S')
96     
97     logging_to_stdout = False
98     if tools.config['syslog']:
99         # SysLog Handler
100         if os.name == 'nt':
101             handler = logging.handlers.NTEventLogHandler("%s %s" %
102                                                          (release.description,
103                                                           release.version))
104         else:
105             handler = logging.handlers.SysLogHandler('/dev/log')
106         formatter = logging.Formatter("%s %s" % (release.description, release.version) + ':%(levelname)s:%(name)s:%(message)s')
107
108     elif tools.config['logfile']:
109         # LogFile Handler
110         logf = tools.config['logfile']
111         try:
112             dirname = os.path.dirname(logf)
113             if dirname and not os.path.isdir(dirname):
114                 os.makedirs(dirname)
115             handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
116         except Exception, ex:
117             sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
118             handler = logging.StreamHandler(sys.stdout) 
119             logging_to_stdout = True
120     else:
121         # Normal Handler on standard output
122         handler = logging.StreamHandler(sys.stdout)
123         logging_to_stdout = True
124
125
126     # tell the handler to use this format
127     handler.setFormatter(formatter)
128
129     # add the handler to the root logger
130     logger.addHandler(handler)
131     logger.setLevel(tools.config['log_level'] or '0')
132
133     if logging_to_stdout and os.name != 'nt':
134         # change color of level names
135         # uses of ANSI color codes
136         # see http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
137         # maybe use http://code.activestate.com/recipes/574451/
138         colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', None, 'default']
139         foreground = lambda f: 30 + colors.index(f)
140         background = lambda f: 40 + colors.index(f)
141
142         mapping = {
143             'DEBUG_RPC': ('blue', 'white'),
144             'DEBUG': ('blue', 'default'),
145             'INFO': ('green', 'default'),
146             'WARNING': ('yellow', 'default'),
147             'ERROR': ('red', 'default'),
148             'CRITICAL': ('white', 'red'),
149         }
150
151         for level, (fg, bg) in mapping.items():
152             msg = "\x1b[%dm\x1b[%dm%s\x1b[0m" % (foreground(fg), background(bg), level)
153             logging.addLevelName(getattr(logging, level), msg)
154
155
156 class Logger(object):
157     def notifyChannel(self, name, level, msg):
158         log = logging.getLogger(name)
159
160         if level == LOG_DEBUG_RPC and not hasattr(log, level):
161             fct = lambda msg, *args, **kwargs: log.log(logging.DEBUG_RPC, msg, *args, **kwargs)
162             setattr(log, LOG_DEBUG_RPC, fct)
163
164         level_method = getattr(log, level)
165
166         if isinstance(msg, Exception):
167             msg = tools.exception_to_unicode(msg)
168
169         result = tools.ustr(msg).strip().split('\n')
170         if len(result)>1:
171             for idx, s in enumerate(result):
172                 level_method('[%02d]: %s' % (idx+1, s,))
173         elif result:
174             level_method(result[0])
175
176     def shutdown(self):
177         logging.shutdown()
178
179 import tools
180 init_logger()
181
182 class Agent(object):
183     _timers = []
184     _logger = Logger()
185
186     def setAlarm(self, fn, dt, args=None, kwargs=None):
187         if not args:
188             args = []
189         if not kwargs:
190             kwargs = {}
191         wait = dt - time.time()
192         if wait > 0:
193             self._logger.notifyChannel('timers', LOG_DEBUG, "Job scheduled in %s seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
194             timer = threading.Timer(wait, fn, args, kwargs)
195             timer.start()
196             self._timers.append(timer)
197         for timer in self._timers[:]:
198             if not timer.isAlive():
199                 self._timers.remove(timer)
200
201     def quit(cls):
202         for timer in cls._timers:
203             timer.cancel()
204     quit = classmethod(quit)
205
206
207 import traceback
208
209 class xmlrpc(object):
210     class RpcGateway(object):
211         def __init__(self, name):
212             self.name = name
213
214 class OpenERPDispatcherException(Exception):
215     def __init__(self, exception, traceback):
216         self.exception = exception
217         self.traceback = traceback
218
219 class OpenERPDispatcher:
220     def log(self, title, msg):
221         from pprint import pformat
222         Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
223
224     def dispatch(self, service_name, method, params):
225         try:
226             self.log('service', service_name)
227             self.log('method', method)
228             self.log('params', params)
229             result = LocalService(service_name)(method, *params)
230             self.log('result', result)
231             return result
232         except Exception, e:
233             self.log('exception', tools.exception_to_unicode(e))
234             if hasattr(e, 'traceback'):
235                 tb = e.traceback
236             else:
237                 tb = sys.exc_info()
238             tb_s = "".join(traceback.format_exception(*tb))
239             if tools.config['debug_mode']:
240                 import pdb
241                 pdb.post_mortem(tb[2])
242             raise OpenERPDispatcherException(e, tb_s)
243
244 class GenericXMLRPCRequestHandler(OpenERPDispatcher):
245     def _dispatch(self, method, params):
246         try:
247             service_name = self.path.split("/")[-1]
248             return self.dispatch(service_name, method, params)
249         except OpenERPDispatcherException, e:
250             raise xmlrpclib.Fault(tools.exception_to_unicode(e.exception), e.traceback)
251
252 class SSLSocket(object):
253     def __init__(self, socket):
254         if not hasattr(socket, 'sock_shutdown'):
255             from OpenSSL import SSL
256             ctx = SSL.Context(SSL.SSLv23_METHOD)
257             ctx.use_privatekey_file(tools.config['secure_pkey_file'])
258             ctx.use_certificate_file(tools.config['secure_cert_file'])
259             self.socket = SSL.Connection(ctx, socket)
260         else:
261             self.socket = socket
262
263     def shutdown(self, how):
264         return self.socket.sock_shutdown(how)
265
266     def __getattr__(self, name):
267         return getattr(self.socket, name)
268
269 class SimpleXMLRPCRequestHandler(GenericXMLRPCRequestHandler, SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
270     rpc_paths = map(lambda s: '/xmlrpc/%s' % s, SERVICES.keys())
271
272 class SecureXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
273     def setup(self):
274         self.connection = SSLSocket(self.request)
275         self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
276         self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
277
278 class SimpleThreadedXMLRPCServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer.SimpleXMLRPCServer):
279     def server_bind(self):
280         self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
281         SimpleXMLRPCServer.SimpleXMLRPCServer.server_bind(self)
282
283 class SecureThreadedXMLRPCServer(SimpleThreadedXMLRPCServer):
284     def __init__(self, server_address, HandlerClass, logRequests=1):
285         SimpleThreadedXMLRPCServer.__init__(self, server_address, HandlerClass, logRequests)
286         self.socket = SSLSocket(socket.socket(self.address_family, self.socket_type))
287         self.server_bind()
288         self.server_activate()
289
290 class HttpDaemon(threading.Thread):
291     def __init__(self, interface, port, secure=False):
292         threading.Thread.__init__(self)
293         self.__port = port
294         self.__interface = interface
295         self.secure = bool(secure)
296         handler_class = (SimpleXMLRPCRequestHandler, SecureXMLRPCRequestHandler)[self.secure]
297         server_class = (SimpleThreadedXMLRPCServer, SecureThreadedXMLRPCServer)[self.secure]
298
299         if self.secure:
300             from OpenSSL.SSL import Error as SSLError
301         else:
302             class SSLError(Exception): pass
303         try:
304             self.server = server_class((interface, port), handler_class, 0)
305         except SSLError, e:
306             Logger().notifyChannel('xml-rpc-ssl', LOG_CRITICAL, "Can not load the certificate and/or the private key files")
307             sys.exit(1)
308         except Exception, e:
309             Logger().notifyChannel('xml-rpc', LOG_CRITICAL, "Error occur when starting the server daemon: %s" % (e,))
310             sys.exit(1)
311
312
313     def attach(self, path, gw):
314         pass
315
316     def stop(self):
317         self.running = False
318         if os.name != 'nt':
319             self.server.socket.shutdown( hasattr(socket, 'SHUT_RDWR') and socket.SHUT_RDWR or 2 )
320         self.server.socket.close()
321
322     def run(self):
323         self.server.register_introspection_functions()
324
325         self.running = True
326         while self.running:
327             self.server.handle_request()
328         return True
329
330         # If the server need to be run recursively
331         #
332         #signal.signal(signal.SIGALRM, self.my_handler)
333         #signal.alarm(6)
334         #while True:
335         #   self.server.handle_request()
336         #signal.alarm(0)          # Disable the alarm
337
338 import tiny_socket
339 class TinySocketClientThread(threading.Thread, OpenERPDispatcher):
340     def __init__(self, sock, threads):
341         threading.Thread.__init__(self)
342         self.sock = sock
343         self.threads = threads
344
345     def run(self):
346         import select
347         self.running = True
348         try:
349             ts = tiny_socket.mysocket(self.sock)
350         except:
351             self.sock.close()
352             self.threads.remove(self)
353             return False
354         while self.running:
355             try:
356                 msg = ts.myreceive()
357             except:
358                 self.sock.close()
359                 self.threads.remove(self)
360                 return False
361             try:
362                 result = self.dispatch(msg[0], msg[1], msg[2:])
363                 ts.mysend(result)
364             except OpenERPDispatcherException, e:
365                 new_e = Exception(tools.exception_to_unicode(e.exception)) # avoid problems of pickeling
366                 ts.mysend(new_e, exception=True, traceback=e.traceback)
367
368             self.sock.close()
369             self.threads.remove(self)
370             return True
371
372     def stop(self):
373         self.running = False
374
375
376 class TinySocketServerThread(threading.Thread):
377     def __init__(self, interface, port, secure=False):
378         threading.Thread.__init__(self)
379         self.__port = port
380         self.__interface = interface
381         self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
382         self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
383         self.socket.bind((self.__interface, self.__port))
384         self.socket.listen(5)
385         self.threads = []
386
387     def run(self):
388         import select
389         try:
390             self.running = True
391             while self.running:
392                 (clientsocket, address) = self.socket.accept()
393                 ct = TinySocketClientThread(clientsocket, self.threads)
394                 self.threads.append(ct)
395                 ct.start()
396             self.socket.close()
397         except Exception, e:
398             self.socket.close()
399             return False
400
401     def stop(self):
402         self.running = False
403         for t in self.threads:
404             t.stop()
405         try:
406             if hasattr(socket, 'SHUT_RDWR'):
407                 self.socket.shutdown(socket.SHUT_RDWR)
408             else:
409                 self.socket.shutdown(2)
410             self.socket.close()
411         except:
412             return False
413
414 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: