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