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