[IMP] wsgi: added exception handling.
[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 from wsgiref.simple_server import make_server
29 from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
30 import xmlrpclib
31
32 import os
33 import signal
34 import sys
35 import time
36
37 import openerp
38 import openerp.tools.config as config
39
40 def xmlrpc_return(start_response, service, method, params):
41     """ Helper to call a service's method with some params, using a
42     wsgi-supplied ``start_response`` callback."""
43     # This mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for exception
44     # handling.
45     try:
46         result = openerp.netsvc.dispatch_rpc(service, method, params, None) # TODO auth
47         response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None)
48     except openerp.netsvc.OpenERPDispatcherException, e:
49         fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e.exception), e.traceback)
50         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
51     except:
52         exc_type, exc_value, exc_tb = sys.exc_info()
53         fault = xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value))
54         response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
55     start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
56     return [response]
57
58 def wsgi_xmlrpc(environ, start_response):
59     """ The main OpenERP WSGI handler."""
60     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/openerp/xmlrpc'):
61         length = int(environ['CONTENT_LENGTH'])
62         data = environ['wsgi.input'].read(length)
63
64         params, method = xmlrpclib.loads(data)
65
66         path = environ['PATH_INFO'][len('/openerp/xmlrpc'):]
67         if path.startswith('/'): path = path[1:]
68         if path.endswith('/'): p = path[:-1]
69         path = path.split('/')
70
71         # All routes are hard-coded. Need a way to register addons-supplied handlers.
72
73         # No need for a db segment.
74         if len(path) == 1:
75             service = path[0]
76
77             if service == 'common':
78                 if method in ('create_database', 'list', 'server_version'):
79                     return xmlrpc_return(start_response, 'db', method, params)
80                 else:
81                     return xmlrpc_return(start_response, 'common', method, params)
82         # A db segment must be given.
83         elif len(path) == 2:
84             service, db_name = path
85             params = (db_name,) + params
86
87             if service == 'model':
88                 return xmlrpc_return(start_response, 'object', method, params)
89             elif service == 'report':
90                 return xmlrpc_return(start_response, 'report', method, params)
91
92 def legacy_wsgi_xmlrpc(environ, start_response):
93     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'):
94         length = int(environ['CONTENT_LENGTH'])
95         data = environ['wsgi.input'].read(length)
96         path = environ['PATH_INFO'][len('/xmlrpc/'):] # expected to be one of db, object, ...
97
98         params, method = xmlrpclib.loads(data)
99         return xmlrpc_return(start_response, path, method, params)
100
101 def wsgi_jsonrpc(environ, start_response):
102     pass
103
104 def wsgi_modules(environ, start_response):
105     """ WSGI handler dispatching to addons-provided entry points."""
106     pass
107
108 # WSGI handlers provided by modules loaded with the --load command-line option.
109 module_handlers = []
110
111 def register_wsgi_handler(handler):
112     """ Register a WSGI handler.
113
114     Handlers are tried in the order they are added. We might provide a way to
115     register a handler for specific routes later.
116     """
117     module_handlers.append(handler)
118
119 def application(environ, start_response):
120     """ WSGI entry point."""
121
122     # Try all handlers until one returns some result (i.e. not None).
123     wsgi_handlers = [
124         wsgi_xmlrpc,
125         wsgi_jsonrpc,
126         legacy_wsgi_xmlrpc,
127         wsgi_modules,
128         ] + module_handlers
129     for handler in wsgi_handlers:
130         result = handler(environ, start_response)
131         if result is None:
132             continue
133         return result
134
135     # We never returned from the loop. Needs something else than 200 OK.
136     response = 'No handler found.\n'
137     start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))])
138     return [response]
139
140 def serve():
141     """ Serve XMLRPC requests via wsgiref's simple_server.
142
143     Blocking, should probably be called in its own process.
144     """
145     httpd = make_server('localhost', config['xmlrpc_port'], application)
146     httpd.serve_forever()
147
148 # Master process id, can be used for signaling.
149 arbiter_pid = None
150
151 # Application setup before we can spawn any worker process.
152 # This is suitable for e.g. gunicorn's on_starting hook.
153 def on_starting(server):
154     global arbiter_pid
155     arbiter_pid = os.getpid() # TODO check if this is true even after replacing the executable
156     config = openerp.tools.config
157     config['addons_path'] = '/home/openerp/repos/addons/trunk-xmlrpc-no-osv-memory' # need a config file
158     #openerp.tools.cache = kill_workers_cache
159     openerp.netsvc.init_logger()
160     openerp.osv.osv.start_object_proxy()
161     openerp.service.web_services.start_web_services()
162
163 # Install our own signal handler on the master process.
164 def when_ready(server):
165     # Hijack gunicorn's SIGWINCH handling; we can choose another one.
166     signal.signal(signal.SIGWINCH, make_winch_handler(server))
167
168 # Our signal handler will signal a SGIQUIT to all workers.
169 def make_winch_handler(server):
170     def handle_winch(sig, fram):
171         server.kill_workers(signal.SIGQUIT) # This is gunicorn specific.
172     return handle_winch
173
174 # Kill gracefuly the workers (e.g. because we want to clear their cache).
175 # This is done by signaling a SIGWINCH to the master process, so it can be
176 # called by the workers themselves.
177 def kill_workers():
178     try:
179         os.kill(arbiter_pid, signal.SIGWINCH)
180     except OSError, e:
181         if e.errno == errno.ESRCH: # no such pid
182             return
183         raise            
184
185 class kill_workers_cache(openerp.tools.ormcache):
186     def clear(self, dbname, *args, **kwargs):
187         kill_workers()
188
189 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: