[IMP] stock, mrp: put same icons for scrap button in production order form
[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" %
155                                                          (release.description,
156                                                           release.version))
157         else:
158             handler = logging.handlers.SysLogHandler('/dev/log')
159         formatter = logging.Formatter("%s %s" % (release.description, release.version) + ':%(levelname)s:%(name)s:%(message)s')
160
161     elif tools.config['logfile']:
162         # LogFile Handler
163         logf = tools.config['logfile']
164         try:
165             dirname = os.path.dirname(logf)
166             if dirname and not os.path.isdir(dirname):
167                 os.makedirs(dirname)
168             if tools.config['logrotate'] is not False:
169                 handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
170             elif os.name == 'posix':
171                 handler = logging.handlers.WatchedFileHandler(logf)
172             else:
173                 handler = logging.handlers.FileHandler(logf)
174         except Exception, ex:
175             sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
176             handler = logging.StreamHandler(sys.stdout)
177     else:
178         # Normal Handler on standard output
179         handler = logging.StreamHandler(sys.stdout)
180
181
182     # tell the handler to use this format
183     handler.setFormatter(formatter)
184
185     # add the handler to the root logger
186     logger.addHandler(handler)
187     logger.setLevel(int(tools.config['log_level'] or '0'))
188
189     if (not isinstance(handler, logging.FileHandler)) and os.name != 'nt':
190         # change color of level names
191         # uses of ANSI color codes
192         # see http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
193         # maybe use http://code.activestate.com/recipes/574451/
194         colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', None, 'default']
195         foreground = lambda f: 30 + colors.index(f)
196         background = lambda f: 40 + colors.index(f)
197
198         mapping = {
199             'DEBUG_RPC': ('blue', 'white'),
200             'DEBUG': ('blue', 'default'),
201             'INFO': ('green', 'default'),
202             'TEST': ('white', 'blue'),
203             'WARNING': ('yellow', 'default'),
204             'ERROR': ('red', 'default'),
205             'CRITICAL': ('white', 'red'),
206         }
207
208         for level, (fg, bg) in mapping.items():
209             msg = "\x1b[%dm\x1b[%dm%s\x1b[0m" % (foreground(fg), background(bg), level)
210             logging.addLevelName(getattr(logging, level), msg)
211
212
213 class Logger(object):
214     def __init__(self):
215         warnings.warn("The netsvc.Logger API shouldn't be used anymore, please "
216                       "use the standard `logging.getLogger` API instead",
217                       PendingDeprecationWarning, stacklevel=2)
218         super(Logger, self).__init__()
219
220     def notifyChannel(self, name, level, msg):
221         warnings.warn("notifyChannel API shouldn't be used anymore, please use "
222                       "the standard `logging` module instead",
223                       PendingDeprecationWarning, stacklevel=2)
224         from service.web_services import common
225
226         log = logging.getLogger(tools.ustr(name))
227
228         if level in [LOG_DEBUG_RPC, LOG_TEST] and not hasattr(log, level):
229             fct = lambda msg, *args, **kwargs: log.log(getattr(logging, level.upper()), msg, *args, **kwargs)
230             setattr(log, level, fct)
231
232
233         level_method = getattr(log, level)
234
235         if isinstance(msg, Exception):
236             msg = tools.exception_to_unicode(msg)
237
238         try:
239             msg = tools.ustr(msg).strip()
240             if level in (LOG_ERROR, LOG_CRITICAL) and tools.config.get_misc('debug','env_info',False):
241                 msg = common().exp_get_server_environment() + "\n" + msg
242
243             result = msg.split('\n')
244         except UnicodeDecodeError:
245             result = msg.strip().split('\n')
246         try:
247             if len(result)>1:
248                 for idx, s in enumerate(result):
249                     level_method('[%02d]: %s' % (idx+1, s,))
250             elif result:
251                 level_method(result[0])
252         except IOError,e:
253             # TODO: perhaps reset the logger streams?
254             #if logrotate closes our files, we end up here..
255             pass
256         except:
257             # better ignore the exception and carry on..
258             pass
259
260     def set_loglevel(self, level):
261         log = logging.getLogger()
262         log.setLevel(logging.INFO) # make sure next msg is printed
263         log.info("Log level changed to %s" % logging.getLevelName(level))
264         log.setLevel(level)
265
266     def shutdown(self):
267         logging.shutdown()
268
269 import tools
270 init_logger()
271
272 class Agent(object):
273     _timers = {}
274     _logger = Logger()
275
276     __logger = logging.getLogger('timer')
277
278     def setAlarm(self, fn, dt, db_name, *args, **kwargs):
279         wait = dt - time.time()
280         if wait > 0:
281             self.__logger.debug("Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
282             timer = threading.Timer(wait, fn, args, kwargs)
283             timer.start()
284             self._timers.setdefault(db_name, []).append(timer)
285
286         for db in self._timers:
287             for timer in self._timers[db]:
288                 if not timer.isAlive():
289                     self._timers[db].remove(timer)
290
291     @classmethod
292     def cancel(cls, db_name):
293         """Cancel all timers for a given database. If None passed, all timers are cancelled"""
294         for db in cls._timers:
295             if db_name is None or db == db_name:
296                 for timer in cls._timers[db]:
297                     timer.cancel()
298
299     @classmethod
300     def quit(cls):
301         cls.cancel(None)
302
303 import traceback
304
305 class Server:
306     """ Generic interface for all servers with an event loop etc.
307         Override this to impement http, net-rpc etc. servers.
308
309         Servers here must have threaded behaviour. start() must not block,
310         there is no run().
311     """
312     __is_started = False
313     __servers = []
314
315
316     __logger = logging.getLogger('server')
317
318     def __init__(self):
319         if Server.__is_started:
320             raise Exception('All instances of servers must be inited before the startAll()')
321         Server.__servers.append(self)
322
323     def start(self):
324         self.__logger.debug("called stub Server.start")
325
326     def stop(self):
327         self.__logger.debug("called stub Server.stop")
328
329     def stats(self):
330         """ This function should return statistics about the server """
331         return "%s: No statistics" % str(self.__class__)
332
333     @classmethod
334     def startAll(cls):
335         if cls.__is_started:
336             return
337         cls.__logger.info("Starting %d services" % len(cls.__servers))
338         for srv in cls.__servers:
339             srv.start()
340         cls.__is_started = True
341
342     @classmethod
343     def quitAll(cls):
344         if not cls.__is_started:
345             return
346         cls.__logger.info("Stopping %d services" % len(cls.__servers))
347         for srv in cls.__servers:
348             srv.stop()
349         cls.__is_started = False
350
351     @classmethod
352     def allStats(cls):
353         res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
354         res.extend(srv.stats() for srv in cls.__servers)
355         return '\n'.join(res)
356
357 class OpenERPDispatcherException(Exception):
358     def __init__(self, exception, traceback):
359         self.exception = exception
360         self.traceback = traceback
361
362 class OpenERPDispatcher:
363     def log(self, title, msg):
364         Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
365
366     def dispatch(self, service_name, method, params):
367         try:
368             self.log('service', service_name)
369             self.log('method', method)
370             self.log('params', params)
371             auth = getattr(self, 'auth_provider', None)
372             result = ExportService.getService(service_name).dispatch(method, auth, params)
373             self.log('result', result)
374             # We shouldn't marshall None,
375             if result == None:
376                 result = False
377             return result
378         except Exception, e:
379             self.log('exception', tools.exception_to_unicode(e))
380             tb = getattr(e, 'traceback', sys.exc_info())
381             tb_s = "".join(traceback.format_exception(*tb))
382             if tools.config['debug_mode']:
383                 import pdb
384                 pdb.post_mortem(tb[2])
385             raise OpenERPDispatcherException(e, tb_s)
386
387 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: