Commit d0310869 authored by Bryton Lacquement's avatar Bryton Lacquement 🚪 Committed by Julien Muchembled

Add WSGI support

This first work on WSGI is only to stop using ZServer (Medusa),
which is a required step before moving to Zope 4. This means that
Zope should behave almost exactly the same way as before, notably:

- We don't take advantage yet of what WSGI offers, like IPv6.
- There's extra code to handle errors the same way as before
  (this is something we'll have to change for Zope 4).

The most significant change in behaviour is that the chosen WSGI server
(waitress) does some of the HTTP work in worker threads (Medusa does it
entirely in the IO thread), and the biggest consequence concerns the
deadlock debugger that is now run from the worker thread:
- it does not work if all threads are blocked
- doing better would require to patch waitress in a quite ugly way

About TimerService, we simplify things by removing the egg.
In zope.conf, it's possible to import from the product.

/reviewed-on nexedi/erp5!883
parents 545a0487 5eeb52d5
...@@ -60,12 +60,7 @@ import transaction ...@@ -60,12 +60,7 @@ import transaction
from App.config import getConfiguration from App.config import getConfiguration
from Shared.DC.ZRDB.Results import Results from Shared.DC.ZRDB.Results import Results
import Products.Localizer.patches from zope.globalrequest import getRequest, setRequest
localizer_lock = Products.Localizer.patches._requests_lock
localizer_contexts = Products.Localizer.patches._requests
LocalizerContext = lambda request: request
from Products.MailHost.MailHost import MailHostError from Products.MailHost.MailHost import MailHostError
from zLOG import LOG, INFO, WARNING, ERROR from zLOG import LOG, INFO, WARNING, ERROR
...@@ -139,6 +134,9 @@ def getServerAddress(): ...@@ -139,6 +134,9 @@ def getServerAddress():
global _server_address global _server_address
if _server_address is None: if _server_address is None:
ip = port = '' ip = port = ''
try:
zopewsgi = sys.modules['Products.ERP5.bin.zopewsgi']
except KeyError:
from asyncore import socket_map from asyncore import socket_map
for k, v in socket_map.items(): for k, v in socket_map.items():
if hasattr(v, 'addr'): if hasattr(v, 'addr'):
...@@ -147,6 +145,8 @@ def getServerAddress(): ...@@ -147,6 +145,8 @@ def getServerAddress():
if type == 'ZServer.HTTPServer.zhttp_server': if type == 'ZServer.HTTPServer.zhttp_server':
ip, port = v.addr ip, port = v.addr
break break
else:
ip, port = zopewsgi.server.addr
if ip == '0.0.0.0': if ip == '0.0.0.0':
ip = socket.gethostbyname(socket.gethostname()) ip = socket.gethostbyname(socket.gethostname())
_server_address = '%s:%s' %(ip, port) _server_address = '%s:%s' %(ip, port)
...@@ -1464,7 +1464,7 @@ class ActivityTool (BaseTool): ...@@ -1464,7 +1464,7 @@ class ActivityTool (BaseTool):
def invoke(self, message): def invoke(self, message):
if self.activity_tracking: if self.activity_tracking:
activity_tracking_logger.info('invoking message: object_path=%s, method_id=%s, args=%r, kw=%r, activity_kw=%r, user_name=%s' % ('/'.join(message.object_path), message.method_id, message.args, message.kw, message.activity_kw, message.user_name)) activity_tracking_logger.info('invoking message: object_path=%s, method_id=%s, args=%r, kw=%r, activity_kw=%r, user_name=%s' % ('/'.join(message.object_path), message.method_id, message.args, message.kw, message.activity_kw, message.user_name))
old_localizer_context = False old_request = None
if getattr(self, 'aq_chain', None) is not None: if getattr(self, 'aq_chain', None) is not None:
# Grab existing acquisition chain and extrach base objects. # Grab existing acquisition chain and extrach base objects.
base_chain = [aq_base(x) for x in self.aq_chain] base_chain = [aq_base(x) for x in self.aq_chain]
...@@ -1500,16 +1500,8 @@ class ActivityTool (BaseTool): ...@@ -1500,16 +1500,8 @@ class ActivityTool (BaseTool):
new_request.other['VirtualRootPhysicalPath'] = request_info['VirtualRootPhysicalPath'] new_request.other['VirtualRootPhysicalPath'] = request_info['VirtualRootPhysicalPath']
if 'HTTP_ACCEPT_LANGUAGE' in request_info: if 'HTTP_ACCEPT_LANGUAGE' in request_info:
new_request.environ['HTTP_ACCEPT_LANGUAGE'] = request_info['HTTP_ACCEPT_LANGUAGE'] new_request.environ['HTTP_ACCEPT_LANGUAGE'] = request_info['HTTP_ACCEPT_LANGUAGE']
# Replace Localizer/iHotfix Context, saving existing one old_request = getRequest()
localizer_context = LocalizerContext(new_request) setRequest(new_request)
id = get_ident()
localizer_lock.acquire()
try:
old_localizer_context = localizer_contexts.get(id)
localizer_contexts[id] = localizer_context
finally:
localizer_lock.release()
# Execute Localizer/iHotfix "patch 2"
new_request.processInputs() new_request.processInputs()
new_request_container = request_container.__class__(REQUEST=new_request) new_request_container = request_container.__class__(REQUEST=new_request)
...@@ -1528,17 +1520,7 @@ class ActivityTool (BaseTool): ...@@ -1528,17 +1520,7 @@ class ActivityTool (BaseTool):
# Restore default skin selection # Restore default skin selection
skinnable = self.getPortalObject() skinnable = self.getPortalObject()
skinnable.changeSkin(skinnable.getSkinNameFromRequest(request)) skinnable.changeSkin(skinnable.getSkinNameFromRequest(request))
if old_localizer_context is not False: setRequest(old_request)
# Restore Localizer/iHotfix context
id = get_ident()
localizer_lock.acquire()
try:
if old_localizer_context is None:
del localizer_contexts[id]
else:
localizer_contexts[id] = old_localizer_context
finally:
localizer_lock.release()
if self.activity_tracking: if self.activity_tracking:
activity_tracking_logger.info('invoked message') activity_tracking_logger.info('invoked message')
if my_self is not self: # We rewrapped self if my_self is not self: # We rewrapped self
......
import argparse
from io import BytesIO
import logging
import os
import posixpath
import socket
from tempfile import TemporaryFile
import time
from urllib import quote
from waitress.server import create_server
import ZConfig
import Zope2
from Zope2.Startup.run import make_wsgi_app
from Products.ERP5Type.patches.WSGIPublisher import publish_module
# this class licensed under the MIT license (stolen from pyramid_translogger)
class TransLogger(object):
format = ('%(REMOTE_ADDR)s - %(REMOTE_USER)s [%(time)s] '
'"%(REQUEST_METHOD)s %(REQUEST_URI)s %(HTTP_VERSION)s" '
'%(status)s %(bytes)s "%(HTTP_REFERER)s" "%(HTTP_USER_AGENT)s"')
def __init__(self, application, logger):
self.application = application
self.logger = logger
def __call__(self, environ, start_response):
start = time.localtime()
req_uri = quote(environ.get('SCRIPT_NAME', '')
+ environ.get('PATH_INFO', ''))
if environ.get('QUERY_STRING'):
req_uri += '?'+environ['QUERY_STRING']
method = environ['REQUEST_METHOD']
def replacement_start_response(status, headers, exc_info=None):
# @@: Ideally we would count the bytes going by if no
# content-length header was provided; but that does add
# some overhead, so at least for now we'll be lazy.
bytes = None
for name, value in headers:
if name.lower() == 'content-length':
bytes = value
self.write_log(environ, method, req_uri, start, status, bytes)
return start_response(status, headers)
return self.application(environ, replacement_start_response)
def write_log(self, environ, method, req_uri, start, status, bytes):
if bytes is None:
bytes = '-'
if time.daylight:
offset = time.altzone / 60 / 60 * -100
else:
offset = time.timezone / 60 / 60 * -100
if offset >= 0:
offset = "+%0.4d" % (offset)
elif offset < 0:
offset = "%0.4d" % (offset)
d = {
'REMOTE_ADDR': environ.get('REMOTE_ADDR') or '-',
'REMOTE_USER': environ.get('REMOTE_USER') or '-',
'REQUEST_METHOD': method,
'REQUEST_URI': req_uri,
'HTTP_VERSION': environ.get('SERVER_PROTOCOL'),
'time': time.strftime('%d/%b/%Y:%H:%M:%S ', start) + offset,
'status': status.split(None, 1)[0],
'bytes': bytes,
'HTTP_REFERER': environ.get('HTTP_REFERER', '-'),
'HTTP_USER_AGENT': environ.get('HTTP_USER_AGENT', '-'),
}
message = self.format % d
self.logger.warn(message)
def app_wrapper(large_file_threshold, use_webdav):
try:
from product.DeadlockDebugger.dumper import dump_threads, dump_url
except Exception:
dump_url = '\0'
def app(environ, start_response):
path_info = environ['PATH_INFO']
if dump_url.startswith(path_info):
query_string = environ['QUERY_STRING']
if dump_url == (path_info + '?' + query_string if query_string
else path_info):
start_response('200 OK', (('Content-type', 'text/plain'),))
return [dump_threads()]
original_wsgi_input = environ['wsgi.input']
if not hasattr(original_wsgi_input, 'seek'):
# Convert environ['wsgi.input'] to a file-like object.
cl = environ.get('CONTENT_LENGTH')
cl = int(cl) if cl else 0
if cl > large_file_threshold:
new_wsgi_input = environ['wsgi.input'] = TemporaryFile('w+b')
else:
new_wsgi_input = environ['wsgi.input'] = BytesIO()
rest = cl
chunksize = 1<<20
try:
while chunksize < rest:
new_wsgi_input.write(original_wsgi_input.read(chunksize))
rest -= chunksize
if rest:
new_wsgi_input.write(original_wsgi_input.read(rest))
except (socket.error, IOError):
msg = b'Not enough data in request or socket error'
start_response('400 Bad Request', [
('Content-Type', 'text/plain'),
('Content-Length', str(len(msg))),
]
)
return [msg]
new_wsgi_input.seek(0)
if use_webdav:
# Munge the request to ensure that we call manage_FTPGet.
# Set a flag to indicate this request came through the WebDAV source
# port server.
environ['WEBDAV_SOURCE_PORT'] = 1
if environ['REQUEST_METHOD'] == 'GET':
if os.sep != '/':
path_info = path_info.replace(os.sep, '/')
path_info = posixpath.join(path_info, 'manage_DAVget')
path_info = posixpath.normpath(path_info)
environ['PATH_INFO'] = path_info
return publish_module(environ, start_response)
return app
def runwsgi():
global server
parser = argparse.ArgumentParser()
parser.add_argument('-w', '--webdav', action='store_true')
parser.add_argument('address', help='<ip>:<port>')
parser.add_argument('zope_conf', help='path to zope.conf')
args = parser.parse_args()
startup = os.path.dirname(Zope2.Startup.__file__)
schema = ZConfig.loadSchema(os.path.join(startup, 'zopeschema.xml'))
conf, _ = ZConfig.loadConfig(schema, args.zope_conf)
make_wsgi_app({}, zope_conf=args.zope_conf)
server = create_server(
TransLogger(app_wrapper(conf.large_file_threshold, args.webdav),
logger=logging.getLogger("access")),
listen=args.address,
threads=conf.zserver_threads,
trusted_proxy='*',
trusted_proxy_headers=('x-forwarded-for',),
clear_untrusted_proxy_headers=True,
)
server.run()
...@@ -38,19 +38,7 @@ from App.class_init import default__class_init__, ApplicationDefaultPermissions ...@@ -38,19 +38,7 @@ from App.class_init import default__class_init__, ApplicationDefaultPermissions
# Nicer alias for class initializer. # Nicer alias for class initializer.
InitializeClass = default__class_init__ InitializeClass = default__class_init__
########################################## from zope.globalrequest import getRequest as get_request
# Localizer is not always loaded prior to ERP5 products,
# thus, as Localizer is supposed to patch Global to add get_request to it,
# we prefer to redefine get_request inside ERP5Type/Utils,
# to avoid the case when Global wasn't patched and get_request is not available.
# This is specially important on Zope 2.12 where Globals doesn't even exist.
##########################################
try:
import Products.iHotfix
get_request = Products.iHotfix.get_request
except (ImportError, AttributeError):
import Products.Localizer
get_request = Products.Localizer.get_request
# Persistency stuff also hasn't moved much from Zope 2.8, although the old # Persistency stuff also hasn't moved much from Zope 2.8, although the old
# "Persistence" module remains there for ancient backward compatibility. # "Persistence" module remains there for ancient backward compatibility.
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
############################################################################## ##############################################################################
# Load all monkey patches # Load all monkey patches
from Products.ERP5Type.patches import WSGIPublisher
from Products.ERP5Type.patches import HTTPRequest from Products.ERP5Type.patches import HTTPRequest
from Products.ERP5Type.patches import AccessControl_patch from Products.ERP5Type.patches import AccessControl_patch
from Products.ERP5Type.patches import Restricted from Products.ERP5Type.patches import Restricted
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
ERP5Type is provides a RAD environment for Zope / CMF ERP5Type is provides a RAD environment for Zope / CMF
All ERP5 classes derive from ERP5Type All ERP5 classes derive from ERP5Type
""" """
from patches import python, pylint from patches import python, pylint, globalrequest
from zLOG import LOG, INFO from zLOG import LOG, INFO
DISPLAY_BOOT_PROCESS = False DISPLAY_BOOT_PROCESS = False
......
# Backport (with modified code) from Zope4
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
""" Python Object Publisher -- Publish Python objects on web servers
"""
import sys
from contextlib import closing
from contextlib import contextmanager
from io import BytesIO
from io import IOBase
import logging
from six import binary_type
from six import PY3
from six import reraise
from six import text_type
from six.moves._thread import allocate_lock
import transaction
from AccessControl.SecurityManagement import newSecurityManager
from AccessControl.SecurityManagement import noSecurityManager
from Acquisition import aq_acquire
from Acquisition import aq_inner
from Acquisition import aq_parent
from transaction.interfaces import TransientError
from zExceptions import Redirect
from zExceptions import Unauthorized
from zExceptions import upgradeException
from zope.component import queryMultiAdapter
from zope.event import notify
from zope.globalrequest import clearRequest
from zope.globalrequest import setRequest
from zope.publisher.skinnable import setDefaultSkin
from zope.security.management import endInteraction
from zope.security.management import newInteraction
from Zope2.App.startup import validated_hook
from ZPublisher import pubevents, Retry
from ZPublisher.HTTPRequest import HTTPRequest
from ZPublisher.Iterators import IUnboundStreamIterator
from ZPublisher.mapply import mapply
from ZPublisher.WSGIPublisher import call_object, missing_name, WSGIResponse
if sys.version_info >= (3, ):
_FILE_TYPES = (IOBase, )
else:
_FILE_TYPES = (IOBase, file) # NOQA
_DEFAULT_DEBUG_MODE = False
_DEFAULT_REALM = None
_MODULE_LOCK = allocate_lock()
_MODULES = {}
AC_LOGGER = logging.getLogger('event.AccessControl')
if 1: # upstream moved WSGIResponse to HTTPResponse.py
# According to PEP 333, WSGI applications and middleware are forbidden from
# using HTTP/1.1 "hop-by-hop" features or headers. This patch prevents Zope
# from sending 'Connection' and 'Transfer-Encoding' headers.
def finalize(self):
headers = self.headers
body = self.body
# <patch>
# There's a bug in 'App.ImageFile.index_html': when it returns a 304 status
# code, 'Content-Length' is equal to a nonzero value.
if self.status == 304:
headers.pop('content-length', None)
# Force the removal of "hop-by-hop" headers
headers.pop('Connection', None)
# </patch>
# set 204 (no content) status if 200 and response is empty
# and not streaming
if ('content-type' not in headers and
'content-length' not in headers and
not self._streaming and self.status == 200):
self.setStatus('nocontent')
# add content length if not streaming
content_length = headers.get('content-length')
if content_length is None and not self._streaming:
self.setHeader('content-length', len(body))
# <patch>
# backport from Zope 4.0b1
# (see commit be5b14bd858da787c41a39e2533b0aabcd246fd5)
# </patch>
return '%s %s' % (self.status, self.errmsg), self.listHeaders()
WSGIResponse.finalize = finalize
# From ZPublisher.utils
def recordMetaData(object, request):
if hasattr(object, 'getPhysicalPath'):
path = '/'.join(object.getPhysicalPath())
else:
# Try hard to get the physical path of the object,
# but there are many circumstances where that's not possible.
to_append = ()
if hasattr(object, '__self__') and hasattr(object, '__name__'):
# object is a Python method.
to_append = (object.__name__,)
object = object.__self__
while object is not None and not hasattr(object, 'getPhysicalPath'):
if getattr(object, '__name__', None) is None:
object = None
break
to_append = (object.__name__,) + to_append
object = aq_parent(aq_inner(object))
if object is not None:
path = '/'.join(object.getPhysicalPath() + to_append)
else:
# As Jim would say, "Waaaaaaaa!"
# This may cause problems with virtual hosts
# since the physical path is different from the path
# used to retrieve the object.
path = request.get('PATH_INFO')
T = transaction.get()
T.note(safe_unicode(path))
auth_user = request.get('AUTHENTICATED_USER', None)
if auth_user:
auth_folder = aq_parent(auth_user)
if auth_folder is None:
AC_LOGGER.warning(
'A user object of type %s has no aq_parent.',
type(auth_user))
auth_path = request.get('AUTHENTICATION_PATH')
else:
auth_path = '/'.join(auth_folder.getPhysicalPath()[1:-1])
user_id = auth_user.getId()
user_id = safe_unicode(user_id) if user_id else u'None'
T.setUser(user_id, safe_unicode(auth_path))
def safe_unicode(value):
if isinstance(value, text_type):
return value
elif isinstance(value, binary_type):
try:
value = text_type(value, 'utf-8')
except UnicodeDecodeError:
value = value.decode('utf-8', 'replace')
return value
def dont_publish_class(klass, request):
request.response.forbiddenError("class %s" % klass.__name__)
def get_module_info(module_name='Zope2'):
global _MODULES
info = _MODULES.get(module_name)
if info is not None:
return info
with _MODULE_LOCK:
module = __import__(module_name)
app = getattr(module, 'bobo_application', module)
realm = _DEFAULT_REALM if _DEFAULT_REALM is not None else module_name
error_hook = getattr(module,'zpublisher_exception_hook', None)
_MODULES[module_name] = info = (app, realm, _DEFAULT_DEBUG_MODE, error_hook)
return info
def _exc_view_created_response(exc, request, response):
view = queryMultiAdapter((exc, request), name=u'index.html')
parents = request.get('PARENTS')
if view is None and parents:
# Try a fallback based on the old standard_error_message
# DTML Method in the ZODB
view = queryMultiAdapter((exc, request),
name=u'standard_error_message')
root_parent = parents[0]
try:
aq_acquire(root_parent, 'standard_error_message')
except (AttributeError, KeyError):
view = None
if view is not None:
# Wrap the view in the context in which the exception happened.
if parents:
view.__parent__ = parents[0]
# Set status and headers from the exception on the response,
# which would usually happen while calling the exception
# with the (environ, start_response) WSGI tuple.
response.setStatus(exc.__class__)
if hasattr(exc, 'headers'):
for key, value in exc.headers.items():
response.setHeader(key, value)
# Set the response body to the result of calling the view.
response.setBody(view())
return True
return False
@contextmanager
def transaction_pubevents(request, response, err_hook, tm=transaction.manager):
try:
setDefaultSkin(request)
newInteraction()
tm.begin()
notify(pubevents.PubStart(request))
yield
notify(pubevents.PubBeforeCommit(request))
if tm.isDoomed():
tm.abort()
else:
tm.commit()
notify(pubevents.PubSuccess(request))
except Exception as exc:
# Normalize HTTP exceptions
# (For example turn zope.publisher NotFound into zExceptions NotFound)
exc_type, _ = upgradeException(exc.__class__, None)
if not isinstance(exc, exc_type):
exc = exc_type(str(exc))
# Create new exc_info with the upgraded exception.
exc_info = (exc_type, exc, sys.exc_info()[2])
try:
# Raise exception from app if handle-errors is False
# (set by zope.testbrowser in some cases)
if request.environ.get('x-wsgiorg.throw_errors', False):
reraise(*exc_info)
if err_hook:
parents = request['PARENTS']
if parents:
parents = parents[0]
retry = False
try:
try:
r = err_hook(parents, request, *exc_info)
assert r is response
exc_view_created = True
except Retry:
if request.supports_retry():
retry = True
else:
r = err_hook(parents, request, *sys.exc_info())
assert r is response
exc_view_created = True
except (Redirect, Unauthorized):
response.exception()
exc_view_created = True
except BaseException as e:
if e is not exc:
raise
exc_view_created = False
else:
# Handle exception view
exc_view_created = _exc_view_created_response(
exc, request, response)
if isinstance(exc, Unauthorized):
# _unauthorized modifies the response in-place. If this hook
# is used, an exception view for Unauthorized has to merge
# the state of the response and the exception instance.
exc.setRealm(response.realm)
response._unauthorized()
response.setStatus(exc.getStatus())
retry = isinstance(exc, TransientError) and request.supports_retry()
notify(pubevents.PubBeforeAbort(request, exc_info, retry))
tm.abort()
notify(pubevents.PubFailure(request, exc_info, retry))
if retry:
reraise(*exc_info)
if not (exc_view_created or isinstance(exc, Unauthorized)):
reraise(*exc_info)
finally:
# Avoid traceback / exception reference cycle.
del exc, exc_info
finally:
endInteraction()
def publish(request, module_info):
obj, realm, debug_mode = module_info
request.processInputs()
response = request.response
if debug_mode:
response.debug_mode = debug_mode
if realm and not request.get('REMOTE_USER', None):
response.realm = realm
noSecurityManager()
# Get the path list.
# According to RFC1738 a trailing space in the path is valid.
path = request.get('PATH_INFO')
request['PARENTS'] = [obj]
obj = request.traverse(path, validated_hook=validated_hook)
notify(pubevents.PubAfterTraversal(request))
recordMetaData(obj, request)
result = mapply(obj,
request.args,
request,
call_object,
1,
missing_name,
dont_publish_class,
request,
bind=1)
if result is not response:
response.setBody(result)
return response
@contextmanager
def load_app(module_info):
app_wrapper, realm, debug_mode = module_info
# Loads the 'OFS.Application' from ZODB.
app = app_wrapper()
try:
yield (app, realm, debug_mode)
finally:
if transaction.manager._txn is not None:
# Only abort a transaction, if one exists. Otherwise the
# abort creates a new transaction just to abort it.
transaction.abort()
app._p_jar.close()
def publish_module(environ, start_response,
_publish=publish, # only for testing
_response=None,
_response_factory=WSGIResponse,
_request=None,
_request_factory=HTTPRequest,
_module_name='Zope2'):
module_info = get_module_info(_module_name)
module_info, err_hook = module_info[:3], module_info[3]
result = ()
path_info = environ.get('PATH_INFO')
if path_info and PY3:
# The WSGI server automatically treats the PATH_INFO as latin-1 encoded
# bytestrings. Typically this is a false assumption as the browser
# delivers utf-8 encoded PATH_INFO. We, therefore, need to encode it
# again with latin-1 to get a utf-8 encoded bytestring.
path_info = path_info.encode('latin-1')
# But in Python 3 we need text here, so we decode the bytestring.
path_info = path_info.decode('utf-8')
environ['PATH_INFO'] = path_info
with closing(BytesIO()) as stdout, closing(BytesIO()) as stderr:
new_response = (
_response
if _response is not None
else _response_factory(stdout=stdout, stderr=stderr))
new_response._http_version = environ['SERVER_PROTOCOL'].split('/')[1]
new_response._server_version = environ.get('SERVER_SOFTWARE')
new_request = (
_request
if _request is not None
else _request_factory(environ['wsgi.input'],
environ,
new_response))
for i in range(getattr(new_request, 'retry_max_count', 3) + 1):
request = new_request
response = new_response
setRequest(request)
try:
with load_app(module_info) as new_mod_info:
with transaction_pubevents(request, response, err_hook):
response = _publish(request, new_mod_info)
break
except TransientError:
if request.supports_retry():
new_request = request.retry()
new_response = new_request.response
else:
raise
finally:
request.close()
clearRequest()
# Start the WSGI server response
status, headers = response.finalize()
start_response(status, headers)
if isinstance(response.body, _FILE_TYPES) or \
IUnboundStreamIterator.providedBy(response.body):
result = response.body
else:
# If somebody used response.write, that data will be in the
# response.stdout BytesIO, so we put that before the body.
result = (response.stdout.getvalue(), response.body)
for func in response.after_list:
func()
# Return the result body iterable.
return result
sys.modules['ZPublisher.WSGIPublisher'] = sys.modules[__name__]
try:
import zope.globalrequest
except ImportError:
import sys
sys.modules['zope.globalrequest'] = sys.modules[__name__]
from threading import local
localData = local()
def getRequest():
return getattr(localData, 'request', None)
def setRequest(request):
localData.request = request
def clearRequest():
setRequest(None)
...@@ -31,7 +31,6 @@ import os ...@@ -31,7 +31,6 @@ import os
import sys import sys
import imp import imp
import re import re
import thread
from Testing import ZopeTestCase from Testing import ZopeTestCase
from Testing.ZopeTestCase import PortalTestCase, user_name from Testing.ZopeTestCase import PortalTestCase, user_name
...@@ -97,10 +96,9 @@ class ERP5TypeLiveTestCase(ERP5TypeTestCaseMixin): ...@@ -97,10 +96,9 @@ class ERP5TypeLiveTestCase(ERP5TypeTestCaseMixin):
registry._conns[-1] = portal registry._conns[-1] = portal
# This is for Localizer patch # This is for Localizer patch
from Products.Localizer import patches from zope.globalrequest import setRequest
request = portal.REQUEST request = portal.REQUEST
with patches._requests_lock: setRequest(request)
patches._requests[thread.get_ident()] = request
# Make live tests run under the same server URL than the host instance. # Make live tests run under the same server URL than the host instance.
if _request_server_url: if _request_server_url:
......
...@@ -725,7 +725,7 @@ class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase): ...@@ -725,7 +725,7 @@ class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase):
'''Publishes the object at 'path' returning a response object.''' '''Publishes the object at 'path' returning a response object.'''
from ZPublisher.Response import Response from ZPublisher.Response import Response
from ZPublisher.Test import publish_module from ZPublisher.Publish import publish_module_standard
from AccessControl.SecurityManagement import getSecurityManager from AccessControl.SecurityManagement import getSecurityManager
from AccessControl.SecurityManagement import setSecurityManager from AccessControl.SecurityManagement import setSecurityManager
...@@ -738,8 +738,6 @@ class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase): ...@@ -738,8 +738,6 @@ class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase):
if env is None: if env is None:
env = {} env = {}
if extra is None:
extra = {}
request = self.app.REQUEST request = self.app.REQUEST
...@@ -780,11 +778,20 @@ class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase): ...@@ -780,11 +778,20 @@ class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase):
try: try:
if user: if user:
PAS._extractUserIds = _extractUserIds PAS._extractUserIds = _extractUserIds
publish_module('Zope2',
# The following `HTTPRequest` object would be created anyway inside
# `publish_module_standard` if no `request` argument was given.
request = request.__class__(stdin, env, response)
# However, we need to inject the content of `extra` inside the
# request.
if extra:
for k, v in extra.items(): request[k] = v
publish_module_standard('Zope2',
request=request,
response=response, response=response,
stdin=stdin, stdin=stdin,
environ=env, environ=env,
extra=extra,
debug=not handle_errors, debug=not handle_errors,
) )
finally: finally:
......
...@@ -21,7 +21,6 @@ This is a hotfix, it dynamically applies several patches to Zope. ...@@ -21,7 +21,6 @@ This is a hotfix, it dynamically applies several patches to Zope.
# Import from the Standard Library # Import from the Standard Library
import logging import logging
import os import os
from thread import allocate_lock, get_ident
# Import from itools # Import from itools
from .itools.i18n import AcceptLanguageType from .itools.i18n import AcceptLanguageType
...@@ -30,7 +29,8 @@ from .itools.i18n import AcceptLanguageType ...@@ -30,7 +29,8 @@ from .itools.i18n import AcceptLanguageType
import Globals import Globals
from ZPublisher import Publish from ZPublisher import Publish
from ZPublisher.HTTPRequest import HTTPRequest from ZPublisher.HTTPRequest import HTTPRequest
from zope.globalrequest import clearRequest, setRequest
from zope.globalrequest import getRequest as get_request
# Flag # Flag
patch = False patch = False
...@@ -57,56 +57,21 @@ logger = logging.getLogger('Localizer') ...@@ -57,56 +57,21 @@ logger = logging.getLogger('Localizer')
# Also, we keep the get_request method in the Globals module for backwards # Also, we keep the get_request method in the Globals module for backwards
# compatibility (with TranslationService for example). # compatibility (with TranslationService for example).
_requests = {} def get_new_publish(zope_publish):
_requests_lock = allocate_lock() def publish(request, *args, **kwargs):
def get_request():
"""Get a request object"""
return _requests.get(get_ident(), None)
def new_publish(request, module_name, after_list, debug=0,
zope_publish=Publish.publish):
# Get the process id
ident = get_ident()
# Add the request object to the global dictionnary
_requests_lock.acquire()
try: try:
_requests[ident] = request setRequest(request)
return zope_publish(request, *args, **kwargs)
finally: finally:
_requests_lock.release() clearRequest()
return publish
# Call the old publish
try:
# Publish
x = zope_publish(request, module_name, after_list, debug)
finally:
# Remove the request object.
# When conflicts occur the "publish" method is called again,
# recursively. In this situation the "_requests dictionary would
# be cleaned in the innermost call, hence outer calls find the
# request does not exist anymore. For this reason we check first
# wether the request is there or not.
if ident in _requests:
_requests_lock.acquire()
try:
del _requests[ident]
finally:
_requests_lock.release()
return x
if patch is False: if patch is False:
logger.info('Install "Globals.get_request".') logger.info('Install "Globals.get_request".')
# Apply the patch # Apply the patch
Publish.publish = new_publish Publish.publish = get_new_publish(Publish.publish)
# First import (it's not a refresh operation).
# We need to apply the patches.
patch = True patch = True
# Add to Globals for backwards compatibility # Add to Globals for backwards compatibility
......
...@@ -8,10 +8,10 @@ import traceback ...@@ -8,10 +8,10 @@ import traceback
import thread import thread
import re import re
import sys, os, errno, time, socket import sys, os, errno, time, socket
from functools import partial
from StringIO import StringIO from StringIO import StringIO
from zLOG import LOG, INFO from zLOG import LOG, INFO
from ZServer.PubCore import handle
from ZPublisher.BaseRequest import BaseRequest from ZPublisher.BaseRequest import BaseRequest
from ZPublisher.BaseResponse import BaseResponse from ZPublisher.BaseResponse import BaseResponse
from ZPublisher.HTTPRequest import HTTPRequest from ZPublisher.HTTPRequest import HTTPRequest
...@@ -38,6 +38,9 @@ class TimerServer: ...@@ -38,6 +38,9 @@ class TimerServer:
'\tInterval: %s seconds.\n'%(time.ctime(time.time()), interval)) '\tInterval: %s seconds.\n'%(time.ctime(time.time()), interval))
def run(self): def run(self):
try:
zopewsgi = sys.modules['Products.ERP5.bin.zopewsgi']
except KeyError:
# wait until the zhttp_server exist in socket_map # wait until the zhttp_server exist in socket_map
# because TimerService has to be started after the Zope HTTPServer # because TimerService has to be started after the Zope HTTPServer
from asyncore import socket_map from asyncore import socket_map
...@@ -53,6 +56,28 @@ class TimerServer: ...@@ -53,6 +56,28 @@ class TimerServer:
break break
if port: if port:
break break
from ZServer.PubCore import handle
else:
while 1:
time.sleep(5)
try:
server = zopewsgi.server
break
except AttributeError:
pass
ip, port = server.addr
start_response = lambda *_: None
class handle(object):
def __init__(self, module_name, request, response):
self.service = partial(zopewsgi.publish_module,
request.environ,
start_response,
_module_name=module_name,
_request=request,
_response=response)
server.add_task(self)
if ip == '0.0.0.0': if ip == '0.0.0.0':
ip = socket.gethostbyname(socket.gethostname()) ip = socket.gethostbyname(socket.gethostname())
...@@ -99,6 +124,9 @@ class TimerServer: ...@@ -99,6 +124,9 @@ class TimerServer:
class TimerResponse(BaseResponse): class TimerResponse(BaseResponse):
after_list = ()
def _finish(self): def _finish(self):
pass pass
...@@ -108,6 +136,9 @@ class TimerResponse(BaseResponse): ...@@ -108,6 +136,9 @@ class TimerResponse(BaseResponse):
def _unauthorized(self): def _unauthorized(self):
pass pass
def finalize(self):
return None, None
# This is taken from ZPublisher.HTTPResponse # This is taken from ZPublisher.HTTPResponse
# I don't think it's safe to make TimerResponse a subclass of HTTPResponse, # I don't think it's safe to make TimerResponse a subclass of HTTPResponse,
# so I inline here the method . This is required it you want unicode page # so I inline here the method . This is required it you want unicode page
...@@ -148,6 +179,7 @@ class TimerRequest(HTTPRequest): ...@@ -148,6 +179,7 @@ class TimerRequest(HTTPRequest):
env['SERVER_PORT'] = '' env['SERVER_PORT'] = ''
env['REMOTE_ADDR'] = '' env['REMOTE_ADDR'] = ''
env['GATEWAY_INTERFACE'] = 'CGI/1.1' env['GATEWAY_INTERFACE'] = 'CGI/1.1'
env['SERVER_PROTOCOL'] = 'HTTP/1.0'
env['PATH_INFO']= '/Control_Panel/timer_service/process_timer' env['PATH_INFO']= '/Control_Panel/timer_service/process_timer'
return env return env
......
#!/usr/bin/env python
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
setup(name='timerserver',
version='2.0.4',
license='GPL',
description='Timer Server for Zope',
long_description='',
author='Nikolay Kim',
author_email='fafhrd@legco.biz',
packages=['timerserver'],
zip_safe=False,
package_data={'timerserver': ['component.xml']},
)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment