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