[MERGE] merged the apa branch
[odoo/odoo.git] / bin / netsvc.py
1 #!/usr/bin/env python
2 # -*- coding: 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 import logging
28 import logging.handlers
29 import os
30 import sys
31 import threading
32 import time
33 import release
34 from pprint import pformat
35 import warnings
36
37 class Service(object):
38     """ Base class for *Local* services
39
40         Functionality here is trusted, no authentication.
41     """
42     _services = {}
43     def __init__(self, name, audience=''):
44         Service._services[name] = self
45         self.__name = name
46         self._methods = {}
47
48     def joinGroup(self, name):
49         raise Exception("No group for local services")
50         #GROUPS.setdefault(name, {})[self.__name] = self
51
52     @classmethod
53     def exists(cls, name):
54         return name in cls._services
55
56     @classmethod
57     def remove(cls, name):
58         if cls.exists(name):
59             cls._services.pop(name)
60
61     def exportMethod(self, method):
62         if callable(method):
63             self._methods[method.__name__] = method
64
65     def abortResponse(self, error, description, origin, details):
66         if not tools.config['debug_mode']:
67             raise Exception("%s -- %s\n\n%s"%(origin, description, details))
68         else:
69             raise
70
71 class LocalService(object):
72     """ Proxy for local services. 
73     
74         Any instance of this class will behave like the single instance
75         of Service(name)
76     """
77     __logger = logging.getLogger('service')
78     def __init__(self, name):
79         self.__name = name
80         try:
81             self._service = Service._services[name]
82             for method_name, method_definition in self._service._methods.items():
83                 setattr(self, method_name, method_definition)
84         except KeyError, keyError:
85             self.__logger.error('This service does not exist: %s' % (str(keyError),) )
86             raise
87
88     def __call__(self, method, *params):
89         return getattr(self, method)(*params)
90
91 class ExportService(object):
92     """ Proxy for exported services. 
93
94     All methods here should take an AuthProxy as their first parameter. It
95     will be appended by the calling framework.
96
97     Note that this class has no direct proxy, capable of calling 
98     eservice.method(). Rather, the proxy should call 
99     dispatch(method,auth,params)
100     """
101     
102     _services = {}
103     _groups = {}
104     
105     def __init__(self, name, audience=''):
106         ExportService._services[name] = self
107         self.__name = name
108
109     def joinGroup(self, name):
110         ExportService._groups.setdefault(name, {})[self.__name] = self
111
112     @classmethod
113     def getService(cls,name):
114         return cls._services[name]
115
116     def dispatch(self, method, auth, params):
117         raise Exception("stub dispatch at %s" % self.__name)
118         
119     def new_dispatch(self,method,auth,params):
120         raise Exception("stub dispatch at %s" % self.__name)
121
122     def abortResponse(self, error, description, origin, details):
123         if not tools.config['debug_mode']:
124             raise Exception("%s -- %s\n\n%s"%(origin, description, details))
125         else:
126             raise
127
128 LOG_NOTSET = 'notset'
129 LOG_DEBUG_RPC = 'debug_rpc'
130 LOG_DEBUG = 'debug'
131 LOG_TEST = 'test'
132 LOG_INFO = 'info'
133 LOG_WARNING = 'warn'
134 LOG_ERROR = 'error'
135 LOG_CRITICAL = 'critical'
136
137 logging.DEBUG_RPC = logging.DEBUG - 2
138 logging.addLevelName(logging.DEBUG_RPC, 'DEBUG_RPC')
139
140 logging.TEST = logging.INFO - 5
141 logging.addLevelName(logging.TEST, 'TEST')
142
143 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10)
144 #The background is set with 40 plus the number of the color, and the foreground with 30
145 #These are the sequences need to get colored ouput
146 RESET_SEQ = "\033[0m"
147 COLOR_SEQ = "\033[1;%dm"
148 BOLD_SEQ = "\033[1m"
149 COLOR_PATTERN = "%s%s%%s%s" % (COLOR_SEQ, COLOR_SEQ, RESET_SEQ)
150 LEVEL_COLOR_MAPPING = {
151     logging.DEBUG_RPC: (BLUE, WHITE),
152     logging.DEBUG: (BLUE, DEFAULT),
153     logging.INFO: (GREEN, DEFAULT),
154     logging.TEST: (WHITE, BLUE),
155     logging.WARNING: (YELLOW, DEFAULT),
156     logging.ERROR: (RED, DEFAULT),
157     logging.CRITICAL: (WHITE, RED),
158 }
159
160 class ColoredFormatter(logging.Formatter):
161     def format(self, record):
162         fg_color, bg_color = LEVEL_COLOR_MAPPING[record.levelno]
163         record.levelname = COLOR_PATTERN % (30 + fg_color, 40 + bg_color, record.levelname)
164         return logging.Formatter.format(self, record)
165
166
167 def init_logger():
168     import os
169     from tools.translate import resetlocale
170     resetlocale()
171
172     logger = logging.getLogger()
173     # create a format for log messages and dates
174     format = '[%(asctime)s] %(levelname)s:%(name)s:%(message)s'
175
176     if tools.config['syslog']:
177         # SysLog Handler
178         if os.name == 'nt':
179             handler = logging.handlers.NTEventLogHandler("%s %s" % (release.description, release.version))
180         else:
181             handler = logging.handlers.SysLogHandler('/dev/log')
182         format = '%s %s' % (release.description, release.version) \
183                + ':%(levelname)s:%(name)s:%(message)s'
184
185     elif tools.config['logfile']:
186         # LogFile Handler
187         logf = tools.config['logfile']
188         try:
189             dirname = os.path.dirname(logf)
190             if dirname and not os.path.isdir(dirname):
191                 os.makedirs(dirname)
192             if tools.config['logrotate'] is not False:
193                 handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
194             elif os.name == 'posix':
195                 handler = logging.handlers.WatchedFileHandler(logf)
196             else:
197                 handler = logging.handlers.FileHandler(logf)
198         except Exception, ex:
199             sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
200             handler = logging.StreamHandler(sys.stdout)
201     else:
202         # Normal Handler on standard output
203         handler = logging.StreamHandler(sys.stdout)
204
205     if isinstance(handler, logging.StreamHandler) and os.isatty(handler.stream.fileno()):
206         formatter = ColoredFormatter(format)
207     else:
208         formatter = logging.Formatter(format)
209     handler.setFormatter(formatter)
210
211     # add the handler to the root logger
212     logger.addHandler(handler)
213     logger.setLevel(int(tools.config['log_level'] or '0'))
214
215
216
217 class Logger(object):
218     def __init__(self):
219         warnings.warn("The netsvc.Logger API shouldn't be used anymore, please "
220                       "use the standard `logging.getLogger` API instead",
221                       PendingDeprecationWarning, stacklevel=2)
222         super(Logger, self).__init__()
223
224     def notifyChannel(self, name, level, msg):
225         warnings.warn("notifyChannel API shouldn't be used anymore, please use "
226                       "the standard `logging` module instead",
227                       PendingDeprecationWarning, stacklevel=2)
228         from service.web_services import common
229
230         log = logging.getLogger(tools.ustr(name))
231
232         if level in [LOG_DEBUG_RPC, LOG_TEST] and not hasattr(log, level):
233             fct = lambda msg, *args, **kwargs: log.log(getattr(logging, level.upper()), msg, *args, **kwargs)
234             setattr(log, level, fct)
235
236
237         level_method = getattr(log, level)
238
239         if isinstance(msg, Exception):
240             msg = tools.exception_to_unicode(msg)
241
242         try:
243             msg = tools.ustr(msg).strip()
244             if level in (LOG_ERROR, LOG_CRITICAL) and tools.config.get_misc('debug','env_info',False):
245                 msg = common().exp_get_server_environment() + "\n" + msg
246
247             result = msg.split('\n')
248         except UnicodeDecodeError:
249             result = msg.strip().split('\n')
250         try:
251             if len(result)>1:
252                 for idx, s in enumerate(result):
253                     level_method('[%02d]: %s' % (idx+1, s,))
254             elif result:
255                 level_method(result[0])
256         except IOError,e:
257             # TODO: perhaps reset the logger streams?
258             #if logrotate closes our files, we end up here..
259             pass
260         except:
261             # better ignore the exception and carry on..
262             pass
263
264     def set_loglevel(self, level):
265         log = logging.getLogger()
266         log.setLevel(logging.INFO) # make sure next msg is printed
267         log.info("Log level changed to %s" % logging.getLevelName(level))
268         log.setLevel(level)
269
270     def shutdown(self):
271         logging.shutdown()
272
273 import tools
274 init_logger()
275
276 class Agent(object):
277     _timers = {}
278     _logger = Logger()
279
280     __logger = logging.getLogger('timer')
281
282     def setAlarm(self, fn, dt, db_name, *args, **kwargs):
283         wait = dt - time.time()
284         if wait > 0:
285             self.__logger.debug("Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
286             timer = threading.Timer(wait, fn, args, kwargs)
287             timer.start()
288             self._timers.setdefault(db_name, []).append(timer)
289
290         for db in self._timers:
291             for timer in self._timers[db]:
292                 if not timer.isAlive():
293                     self._timers[db].remove(timer)
294
295     @classmethod
296     def cancel(cls, db_name):
297         """Cancel all timers for a given database. If None passed, all timers are cancelled"""
298         for db in cls._timers:
299             if db_name is None or db == db_name:
300                 for timer in cls._timers[db]:
301                     timer.cancel()
302
303     @classmethod
304     def quit(cls):
305         cls.cancel(None)
306
307 import traceback
308
309 class Server:
310     """ Generic interface for all servers with an event loop etc.
311         Override this to impement http, net-rpc etc. servers.
312
313         Servers here must have threaded behaviour. start() must not block,
314         there is no run().
315     """
316     __is_started = False
317     __servers = []
318
319
320     __logger = logging.getLogger('server')
321
322     def __init__(self):
323         if Server.__is_started:
324             raise Exception('All instances of servers must be inited before the startAll()')
325         Server.__servers.append(self)
326
327     def start(self):
328         self.__logger.debug("called stub Server.start")
329
330     def stop(self):
331         self.__logger.debug("called stub Server.stop")
332
333     def stats(self):
334         """ This function should return statistics about the server """
335         return "%s: No statistics" % str(self.__class__)
336
337     @classmethod
338     def startAll(cls):
339         if cls.__is_started:
340             return
341         cls.__logger.info("Starting %d services" % len(cls.__servers))
342         for srv in cls.__servers:
343             srv.start()
344         cls.__is_started = True
345
346     @classmethod
347     def quitAll(cls):
348         if not cls.__is_started:
349             return
350         cls.__logger.info("Stopping %d services" % len(cls.__servers))
351         for srv in cls.__servers:
352             srv.stop()
353         cls.__is_started = False
354
355     @classmethod
356     def allStats(cls):
357         res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
358         res.extend(srv.stats() for srv in cls.__servers)
359         return '\n'.join(res)
360
361 class OpenERPDispatcherException(Exception):
362     def __init__(self, exception, traceback):
363         self.exception = exception
364         self.traceback = traceback
365
366 class OpenERPDispatcher:
367     def log(self, title, msg):
368         Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
369
370     def dispatch(self, service_name, method, params):
371         try:
372             self.log('service', service_name)
373             self.log('method', method)
374             self.log('params', params)
375             auth = getattr(self, 'auth_provider', None)
376             result = ExportService.getService(service_name).dispatch(method, auth, params)
377             self.log('result', result)
378             # We shouldn't marshall None,
379             if result == None:
380                 result = False
381             return result
382         except Exception, e:
383             self.log('exception', tools.exception_to_unicode(e))
384             tb = getattr(e, 'traceback', sys.exc_info())
385             tb_s = "".join(traceback.format_exception(*tb))
386             if tools.config['debug_mode']:
387                 import pdb
388                 pdb.post_mortem(tb[2])
389             raise OpenERPDispatcherException(e, tb_s)
390
391 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: