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