[IMP] Remove account sequence code madness
[odoo/odoo.git] / openerp / wsgi.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 """ WSGI stuffs (proof of concept for now)
23
24 This module offers a WSGI interface to OpenERP.
25
26 """
27
28 import httplib
29 import urllib
30 import xmlrpclib
31 import StringIO
32
33 import logging
34 import os
35 import signal
36 import sys
37 import threading
38 import time
39 import traceback
40
41 import openerp
42 import openerp.modules
43 import openerp.tools.config as config
44 import service.websrv_lib as websrv_lib
45
46 # XML-RPC fault codes. Some care must be taken when changing these: the
47 # constants are also defined client-side and must remain in sync.
48 # User code must use the exceptions defined in ``openerp.exceptions`` (not
49 # create directly ``xmlrpclib.Fault`` objects).
50 XML_RPC_FAULT_CODE_APPLICATION_ERROR = 1
51 XML_RPC_FAULT_CODE_DEFERRED_APPLICATION_ERROR = 2
52 XML_RPC_FAULT_CODE_ACCESS_DENIED = 3
53 XML_RPC_FAULT_CODE_ACCESS_ERROR = 4
54 XML_RPC_FAULT_CODE_WARNING = 5
55
56 def xmlrpc_return(start_response, service, method, params):
57     """
58     Helper to call a service's method with some params, using a wsgi-supplied
59     ``start_response`` callback.
60
61     This is the place to look at to see the mapping between core exceptions
62     and XML-RPC fault codes.
63     """
64     # Map OpenERP core exceptions to XML-RPC fault codes. Specific exceptions
65     # defined in ``openerp.exceptions`` are mapped to specific fault codes;
66     # all the other exceptions are mapped to the generic
67     # XML_RPC_FAULT_CODE_APPLICATION_ERROR value.
68     # This also mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for
69     # exception handling.
70     try:
71         result = openerp.netsvc.dispatch_rpc(service, method, params)
72         response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None)
73     except openerp.exceptions.Warning, e:
74         fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_WARNING, str(e))
75         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
76     except openerp.exceptions.AccessError, e:
77         fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_ACCESS_ERROR, str(e))
78         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
79     except openerp.exceptions.AccessDenied, e:
80         fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_ACCESS_DENIED, str(e))
81         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
82     except openerp.exceptions.DeferredException, e:
83         info = e.traceback
84         # Which one is the best ?
85         formatted_info = "".join(traceback.format_exception(*info))
86         #formatted_info = openerp.tools.exception_to_unicode(e) + '\n' + info
87         fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_DEFERRED_APPLICATION_ERROR, formatted_info)
88         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
89     except Exception, e:
90         info = sys.exc_info()
91         # Which one is the best ?
92         formatted_info = "".join(traceback.format_exception(*info))
93         #formatted_info = openerp.tools.exception_to_unicode(e) + '\n' + info
94         fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
95         response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
96     start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
97     return [response]
98
99 def wsgi_xmlrpc(environ, start_response):
100     """ The main OpenERP WSGI handler."""
101     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/openerp/6.1/xmlrpc'):
102         length = int(environ['CONTENT_LENGTH'])
103         data = environ['wsgi.input'].read(length)
104
105         params, method = xmlrpclib.loads(data)
106
107         path = environ['PATH_INFO'][len('/openerp/6.1/xmlrpc'):]
108         if path.startswith('/'): path = path[1:]
109         if path.endswith('/'): p = path[:-1]
110         path = path.split('/')
111
112         # All routes are hard-coded.
113
114         # No need for a db segment.
115         if len(path) == 1:
116             service = path[0]
117
118             if service == 'common':
119                 if method in ('server_version',):
120                     service = 'db'
121             return xmlrpc_return(start_response, service, method, params)
122
123         # A db segment must be given.
124         elif len(path) == 2:
125             service, db_name = path
126             params = (db_name,) + params
127
128             if service == 'model':
129                 service = 'object'
130             return xmlrpc_return(start_response, service, method, params)
131
132         # TODO the body has been read, need to raise an exception (not return None).
133
134 def legacy_wsgi_xmlrpc(environ, start_response):
135     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'):
136         length = int(environ['CONTENT_LENGTH'])
137         data = environ['wsgi.input'].read(length)
138         path = environ['PATH_INFO'][len('/xmlrpc/'):] # expected to be one of db, object, ...
139
140         params, method = xmlrpclib.loads(data)
141         return xmlrpc_return(start_response, path, method, params)
142
143 def wsgi_jsonrpc(environ, start_response):
144     pass
145
146 def wsgi_webdav(environ, start_response):
147     pi = environ['PATH_INFO']
148     if environ['REQUEST_METHOD'] == 'OPTIONS' and pi in ['*','/']:
149         return return_options(environ, start_response)
150     elif pi.startswith('/webdav'):
151         http_dir = websrv_lib.find_http_service(pi)
152         if http_dir:
153             path = pi[len(http_dir.path):]
154             if path.startswith('/'):
155                 environ['PATH_INFO'] = path
156             else:
157                 environ['PATH_INFO'] = '/' + path
158             return http_to_wsgi(http_dir)(environ, start_response)
159
160 def return_options(environ, start_response):
161     # Microsoft specific header, see
162     # http://www.ibm.com/developerworks/rational/library/2089.html
163     if 'Microsoft' in environ.get('User-Agent', ''):
164         option = [('MS-Author-Via', 'DAV')]
165     else:
166         option = []
167     options += [('DAV', '1 2'), ('Allow', 'GET HEAD PROPFIND OPTIONS REPORT')]
168     start_response("200 OK", [('Content-Length', str(0))] + options)
169     return []
170
171 def http_to_wsgi(http_dir):
172     """
173     Turn a BaseHTTPRequestHandler into a WSGI entry point.
174
175     Actually the argument is not a bare BaseHTTPRequestHandler but is wrapped
176     (as a class, so it needs to be instanciated) in a HTTPDir.
177
178     This code is adapted from wbsrv_lib.MultiHTTPHandler._handle_one_foreign().
179     It is a temporary solution: the HTTP sub-handlers (in particular the
180     document_webdav addon) have to be WSGIfied.
181     """
182     def wsgi_handler(environ, start_response):
183
184         # Extract from the WSGI environment the necessary data.
185         scheme = environ['wsgi.url_scheme']
186
187         headers = {}
188         for key, value in environ.items():
189             if key.startswith('HTTP_'):
190                 key = key[5:].replace('_', '-').title()
191                 headers[key] = value
192             if key == 'CONTENT_LENGTH':
193                 key = key.replace('_', '-').title()
194                 headers[key] = value
195         if environ.get('Content-Type'):
196             headers['Content-Type'] = environ['Content-Type']
197
198         path = urllib.quote(environ.get('PATH_INFO', ''))
199         if environ.get('QUERY_STRING'):
200             path += '?' + environ['QUERY_STRING']
201
202         request_version = 'HTTP/1.1' # TODO
203         request_line = "%s %s %s\n" % (environ['REQUEST_METHOD'], path, request_version)
204
205         class Dummy(object):
206             pass
207
208         # Let's pretend we have a server to hand to the handler.
209         server = Dummy()
210         server.server_name = environ['SERVER_NAME']
211         server.server_port = int(environ['SERVER_PORT'])
212
213         # Initialize the underlying handler and associated auth. provider.
214         con = openerp.service.websrv_lib.noconnection(environ['wsgi.input'])
215         handler = http_dir.instanciate_handler(con, environ['REMOTE_ADDR'], server)
216
217         # Populate the handler as if it is called by a regular HTTP server
218         # and the request is already parsed.
219         handler.wfile = StringIO.StringIO()
220         handler.rfile = environ['wsgi.input']
221         handler.headers = headers
222         handler.command = environ['REQUEST_METHOD']
223         handler.path = path
224         handler.request_version = request_version
225         handler.close_connection = 1
226         handler.raw_requestline = request_line
227         handler.requestline = request_line
228
229         # Handle authentication if there is an auth. provider associated to
230         # the handler.
231         if hasattr(handler, 'auth_provider'):
232             try:
233                 handler.auth_provider.checkRequest(handler, path)
234             except websrv_lib.AuthRequiredExc, ae:
235                 # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the
236                 # authorisation features of HTTP/1.1 
237                 if request_version != 'HTTP/1.1' and ('Darwin/9.' not in handler.headers.get('User-Agent', '')):
238                     start_response("403 Forbidden", [])
239                     return []
240                 start_response("401 Authorization required", [
241                     ('WWW-Authenticate', '%s realm="%s"' % (ae.atype,ae.realm)),
242                     # ('Connection', 'keep-alive'),
243                     ('Content-Type', 'text/html'),
244                     ('Content-Length', 4), # len(self.auth_required_msg)
245                     ])
246                 return ['Blah'] # self.auth_required_msg
247             except websrv_lib.AuthRejectedExc,e:
248                 start_response("403 %s" % (e.args[0],), [])
249                 return []
250
251         method_name = 'do_' + handler.command
252
253         # Support the OPTIONS method even when not provided directly by the
254         # handler. TODO I would prefer to remove it and fix the handler if
255         # needed.
256         if not hasattr(handler, method_name):
257             if handler.command == 'OPTIONS':
258                 return return_options(environ, start_response)
259             start_response("501 Unsupported method (%r)" % handler.command, [])
260             return []
261
262         # Finally, call the handler's method.
263         try:
264             method = getattr(handler, method_name)
265             method()
266             # The DAV handler buffers its output and provides a _flush()
267             # method.
268             getattr(handler, '_flush', lambda: None)()
269             response = parse_http_response(handler.wfile.getvalue())
270             response_headers = response.getheaders()
271             body = response.read()
272             start_response(str(response.status) + ' ' + response.reason, response_headers)
273             return [body]
274         except (websrv_lib.AuthRejectedExc, websrv_lib.AuthRequiredExc):
275             raise
276         except Exception, e:
277             start_response("500 Internal error", [])
278             return []
279
280     return wsgi_handler
281
282 def parse_http_response(s):
283     """ Turn a HTTP response string into a httplib.HTTPResponse object."""
284     class DummySocket(StringIO.StringIO):
285         """
286         This is used to provide a StringIO to httplib.HTTPResponse
287         which, instead of taking a file object, expects a socket and
288         uses its makefile() method.
289         """
290         def makefile(self, *args, **kw):
291             return self
292     response = httplib.HTTPResponse(DummySocket(s))
293     response.begin()
294     return response
295
296 # WSGI handlers registered through the register_wsgi_handler() function below.
297 module_handlers = []
298
299 def register_wsgi_handler(handler):
300     """ Register a WSGI handler.
301
302     Handlers are tried in the order they are added. We might provide a way to
303     register a handler for specific routes later.
304     """
305     module_handlers.append(handler)
306
307 def application(environ, start_response):
308     """ WSGI entry point."""
309
310     # Try all handlers until one returns some result (i.e. not None).
311     wsgi_handlers = [
312         wsgi_xmlrpc,
313         wsgi_jsonrpc,
314         legacy_wsgi_xmlrpc,
315         wsgi_webdav
316         ] + module_handlers
317     for handler in wsgi_handlers:
318         result = handler(environ, start_response)
319         if result is None:
320             continue
321         return result
322
323     # We never returned from the loop.
324     response = 'No handler found.\n'
325     start_response('404 Not Found', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))])
326     return [response]
327
328 # The WSGI server, started by start_server(), stopped by stop_server().
329 httpd = None
330
331 def serve():
332     """ Serve HTTP requests via werkzeug development server.
333
334     If werkzeug can not be imported, we fall back to wsgiref's simple_server.
335
336     Calling this function is blocking, you might want to call it in its own
337     thread.
338     """
339
340     global httpd
341
342     # TODO Change the xmlrpc_* options to http_*
343     interface = config['xmlrpc_interface'] or '0.0.0.0'
344     port = config['xmlrpc_port']
345     try:
346         import werkzeug.serving
347         httpd = werkzeug.serving.make_server(interface, port, application, threaded=True)
348         logging.getLogger('wsgi').info('HTTP service (werkzeug) running on %s:%s', interface, port)
349     except ImportError, e:
350         import wsgiref.simple_server
351         logging.getLogger('wsgi').warn('Werkzeug module unavailable, falling back to wsgiref.')
352         httpd = wsgiref.simple_server.make_server(interface, port, application)
353         logging.getLogger('wsgi').info('HTTP service (wsgiref) running on %s:%s', interface, port)
354
355     httpd.serve_forever()
356
357 def start_server():
358     """ Call serve() in its own thread.
359
360     The WSGI server can be shutdown with stop_server() below.
361     """
362     threading.Thread(target=openerp.wsgi.serve).start()
363
364 def stop_server():
365     """ Initiate the shutdown of the WSGI server.
366
367     The server is supposed to have been started by start_server() above.
368     """
369     if httpd:
370         httpd.shutdown()
371
372 # Master process id, can be used for signaling.
373 arbiter_pid = None
374
375 # Application setup before we can spawn any worker process.
376 # This is suitable for e.g. gunicorn's on_starting hook.
377 def on_starting(server):
378     global arbiter_pid
379     arbiter_pid = os.getpid() # TODO check if this is true even after replacing the executable
380     config = openerp.tools.config
381     #openerp.tools.cache = kill_workers_cache
382     openerp.netsvc.init_logger()
383     openerp.osv.osv.start_object_proxy()
384     openerp.service.web_services.start_web_services()
385
386 # Install our own signal handler on the master process.
387 def when_ready(server):
388     # Hijack gunicorn's SIGWINCH handling; we can choose another one.
389     signal.signal(signal.SIGWINCH, make_winch_handler(server))
390
391 # Our signal handler will signal a SGIQUIT to all workers.
392 def make_winch_handler(server):
393     def handle_winch(sig, fram):
394         server.kill_workers(signal.SIGQUIT) # This is gunicorn specific.
395     return handle_winch
396
397 # Kill gracefuly the workers (e.g. because we want to clear their cache).
398 # This is done by signaling a SIGWINCH to the master process, so it can be
399 # called by the workers themselves.
400 def kill_workers():
401     try:
402         os.kill(arbiter_pid, signal.SIGWINCH)
403     except OSError, e:
404         if e.errno == errno.ESRCH: # no such pid
405             return
406         raise            
407
408 class kill_workers_cache(openerp.tools.ormcache):
409     def clear(self, dbname, *args, **kwargs):
410         kill_workers()
411
412 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: