merge
[odoo/odoo.git] / bin / netsvc.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
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.
10 #
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.
15 #
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.
20 #
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/>.
23 #
24 ##############################################################################
25
26 import SimpleXMLRPCServer
27 import SocketServer
28 import logging
29 import logging.handlers
30 import os
31 import signal
32 import socket
33 import sys
34 import threading
35 import time
36 import xmlrpclib
37 import release
38
39 class Service(object):
40     """ Base class for *Local* services 
41    
42         Functionality here is trusted, no authentication.
43     """
44     _services = {}
45     def __init__(self, name, audience=''):
46         Service._services[name] = self
47         self.__name = name
48         self._methods = {}
49
50     def joinGroup(self, name):
51         raise Exception("No group for local services")
52         #GROUPS.setdefault(name, {})[self.__name] = self
53
54     def service_exist(self,name):
55         return Service._services.has_key(name)
56
57     def exportMethod(self, method):
58         if callable(method):
59             self._methods[method.__name__] = method
60
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))
64         else:
65             raise
66
67 class LocalService(object):
68     """ Proxy for local services. 
69     
70         Any instance of this class will behave like the single instance
71         of Service(name)
72     """
73     def __init__(self, name):
74         self.__name = name
75         try:
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),) )
81             raise
82     def __call__(self, method, *params):
83         return getattr(self, method)(*params)
84
85 class ExportService(object):
86     """ Proxy for exported services. 
87
88     All methods here should take an AuthProxy as their first parameter. It
89     will be appended by the calling framework.
90
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)
94     """
95     
96     _services = {}
97     _groups = {}
98     
99     def __init__(self, name, audience=''):
100         ExportService._services[name] = self
101         self.__name = name
102
103     def joinGroup(self, name):
104         ExportService._groups.setdefault(name, {})[self.__name] = self
105
106     @classmethod
107     def getService(cls,name):
108         return cls._services[name]
109
110     def dispatch(self, method, auth, params):
111         raise Exception("stub dispatch at %s" % self.__name)
112         
113     def new_dispatch(self,method,auth,params):
114         raise Exception("stub dispatch at %s" % self.__name)
115
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))
119         else:
120             raise
121
122 LOG_NOTSET = 'notset'
123 LOG_DEBUG_RPC = 'debug_rpc'
124 LOG_DEBUG = 'debug'
125 LOG_DEBUG2 = 'debug2'
126 LOG_INFO = 'info'
127 LOG_WARNING = 'warn'
128 LOG_ERROR = 'error'
129 LOG_CRITICAL = 'critical'
130
131 # add new log level below DEBUG
132 logging.DEBUG2 = logging.DEBUG - 1
133 logging.DEBUG_RPC = logging.DEBUG2 - 1
134
135 def init_logger():
136     import os
137     from tools.translate import resetlocale
138     resetlocale()
139
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')
143
144     if tools.config['syslog']:
145         # SysLog Handler
146         if os.name == 'nt':
147             handler = logging.handlers.NTEventLogHandler("%s %s" %
148                                                          (release.description,
149                                                           release.version))
150         else:
151             handler = logging.handlers.SysLogHandler('/dev/log')
152         formatter = logging.Formatter("%s %s" % (release.description, release.version) + ':%(levelname)s:%(name)s:%(message)s')
153
154     elif tools.config['logfile']:
155         # LogFile Handler
156         logf = tools.config['logfile']
157         try:
158             dirname = os.path.dirname(logf)
159             if dirname and not os.path.isdir(dirname):
160                 os.makedirs(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)
165             else:
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)
170     else:
171         # Normal Handler on standard output
172         handler = logging.StreamHandler(sys.stdout)
173
174
175     # tell the handler to use this format
176     handler.setFormatter(formatter)
177
178     # add the handler to the root logger
179     logger.addHandler(handler)
180     logger.setLevel(int(tools.config['log_level'] or '0'))
181
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)
190
191         mapping = {
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'),
199         }
200
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)
204
205
206 class Logger(object):
207
208     def notifyChannel(self, name, level, msg):
209         from service.web_services import common
210
211         log = logging.getLogger(tools.ustr(name))
212
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)
216
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)
220
221         level_method = getattr(log, level)
222
223         if isinstance(msg, Exception):
224             msg = tools.exception_to_unicode(msg)
225
226         try:
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
230
231             result = msg.split('\n')
232         except UnicodeDecodeError:
233             result = msg.strip().split('\n')
234         try:
235             if len(result)>1:
236                 for idx, s in enumerate(result):
237                     level_method('[%02d]: %s' % (idx+1, s,))
238             elif result:
239                 level_method(result[0])
240         except IOError,e:
241             # TODO: perhaps reset the logger streams?
242             #if logrotate closes our files, we end up here..
243             pass
244         except:
245             # better ignore the exception and carry on..
246             pass
247
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))
252         log.setLevel(level)
253
254     def shutdown(self):
255         logging.shutdown()
256
257 import tools
258 init_logger()
259
260 class Agent(object):
261     _timers = {}
262     _logger = Logger()
263
264     def setAlarm(self, fn, dt, db_name, *args, **kwargs):
265         wait = dt - time.time()
266         if wait > 0:
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)
269             timer.start()
270             self._timers.setdefault(db_name, []).append(timer)
271
272         for db in self._timers:
273             for timer in self._timers[db]:
274                 if not timer.isAlive():
275                     self._timers[db].remove(timer)
276
277     @classmethod
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]:
283                     timer.cancel()
284
285     @classmethod
286     def quit(cls):
287         cls.cancel(None)
288
289 import traceback
290
291 class Server:
292     """ Generic interface for all servers with an event loop etc.
293         Override this to impement http, net-rpc etc. servers.
294         
295         Servers here must have threaded behaviour. start() must not block,
296         there is no run().
297     """
298     __is_started = False
299     __servers = []
300     
301     def __init__(self):
302         if Server.__is_started:
303             raise Exception('All instances of servers must be inited before the startAll()')
304         Server.__servers.append(self)
305
306     def start(self):
307         print "called stub Server.start"
308         pass
309         
310     def stop(self):
311         print "called stub Server.stop"
312         pass
313
314     def stats(self):
315         """ This function should return statistics about the server """
316         return "%s: No statistics" % str(self.__class__)
317
318     @classmethod
319     def startAll(cls):
320         if cls.__is_started:
321             return
322         Logger().notifyChannel("services", LOG_INFO, 
323             "Starting %d services" % len(cls.__servers))
324         for srv in cls.__servers:
325             srv.start()
326         cls.__is_started = True
327     
328     @classmethod
329     def quitAll(cls):
330         if not cls.__is_started:
331             return
332         Logger().notifyChannel("services", LOG_INFO, 
333             "Stopping %d services" % len(cls.__servers))
334         for srv in cls.__servers:
335             srv.stop()
336         cls.__is_started = False
337
338     @classmethod
339     def allStats(cls):
340         res = ''
341         if cls.__is_started:
342             res += "Servers started\n"
343         else:
344             res += "Servers stopped\n"
345         for srv in cls.__servers:
346             try:
347                 res += srv.stats() + "\n"
348             except:
349                 pass
350         return res
351
352 class OpenERPDispatcherException(Exception):
353     def __init__(self, exception, traceback):
354         self.exception = exception
355         self.traceback = traceback
356
357 class OpenERPDispatcher:
358     def log(self, title, msg):
359         from pprint import pformat
360         Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
361
362     def dispatch(self, service_name, method, params):
363         try:
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
369             else:
370                 auth = None
371             result = ExportService.getService(service_name).dispatch(method, auth, params)
372             self.log('result', result)
373             # We shouldn't marshall None,
374             if result == None:
375                 result = False
376             return result
377         except Exception, e:
378             self.log('exception', tools.exception_to_unicode(e))
379             if hasattr(e, 'traceback'):
380                 tb = e.traceback
381             else:
382                 tb = sys.exc_info()
383             tb_s = "".join(traceback.format_exception(*tb))
384             if tools.config['debug_mode']:
385                 import pdb
386                 pdb.post_mortem(tb[2])
387             raise OpenERPDispatcherException(e, tb_s)
388
389 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: