Commit a14c654c authored by Lennart Regebro's avatar Lennart Regebro
parents 4630a5bc 59ef1c77
......@@ -48,6 +48,27 @@ Zope Changes
- Using FastCGI is offically deprecated.
Features added
- Experimental WSGI and Twisted support for http.
Zope now has a WSGI interface for integration with other
web-servers than ZServer. Most notably Twisted is supported.
The WSGI application is ZPublisher.WSGIPublisher.publish_module
You can make ZServer use the twisted interface with the
"use-wsgi on" keyword in the http-server section in zope.conf.
You can run Twisted by installing Twisted (2.1 recommended) and
replacing the http-server section with a server section in
zope.conf. It is not possible to run a Twisted server together with
a ZServer at the same time.
<server>
address 8080
type Zope2-HTTP
</server>
WSGI: http://www.python.org/dev/peps/pep-0333/
Twisted: http://twistedmatrix.com/
- The traversal has been refactored to take heed of Zope3s
IPublishTraverse adapter interfaces. The ZCML directives
......
......@@ -31,6 +31,11 @@ def shutdown(exit_code,fast = 0):
import ZServer
ZServer.exit_code = exit_code
_shutdown_phase = 1
try:
from twisted.internet import reactor
reactor.callLater(0.1, reactor.stop)
except ImportError:
pass
if fast:
# Someone wants us to shutdown fast. This is hooked into SIGTERM - so
# possibly the system is going down and we can expect a SIGKILL within
......
......@@ -223,7 +223,8 @@ class TestPythonScriptErrors(PythonScriptTestBase):
def testBadImports(self):
self.assertPSRaises(ImportError, body="from string import *")
self.assertPSRaises(ImportError, body="import mmap")
self.assertPSRaises(ImportError, body="from datetime import datetime")
#self.assertPSRaises(ImportError, body="import mmap")
def testAttributeAssignment(self):
# It's illegal to assign to attributes of anything that
......
......@@ -122,7 +122,6 @@ def publish(request, module_name, after_list, debug=0,
return response
except:
# DM: provide nicer error message for FTP
sm = None
if response is not None:
......
##############################################################################
#
# Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved.
#
# 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
#
##############################################################################
__doc__="""Python Object Publisher -- Publish Python objects on web servers
$Id: Publish.py 67721 2006-04-28 14:57:35Z regebro $"""
import sys, os, re, time
import transaction
from Response import Response
from Request import Request
from maybe_lock import allocate_lock
from mapply import mapply
from zExceptions import Redirect
from cStringIO import StringIO
from ZServer.medusa.http_date import build_http_date
class WSGIResponse(Response):
"""A response object for WSGI
This Response object knows nothing about ZServer, but tries to be
compatible with the ZServerHTTPResponse.
Most significantly, streaming is not (yet) supported."""
_streaming = 0
def __str__(self,
html_search=re.compile('<html>',re.I).search,
):
if self._wrote:
if self._chunking:
return '0\r\n\r\n'
else:
return ''
headers=self.headers
body=self.body
# set 204 (no content) status if 200 and response is empty
# and not streaming
if not headers.has_key('content-type') and \
not headers.has_key('content-length') and \
not self._streaming and \
self.status == 200:
self.setStatus('nocontent')
# add content length if not streaming
if not headers.has_key('content-length') and \
not self._streaming:
self.setHeader('content-length',len(body))
content_length= headers.get('content-length', None)
if content_length>0 :
self.setHeader('content-length', content_length)
headersl=[]
append=headersl.append
status=headers.get('status', '200 OK')
# status header must come first.
append("HTTP/%s %s" % (self._http_version or '1.0' , status))
if headers.has_key('status'):
del headers['status']
# add zserver headers
append('Server: %s' % self._server_version)
append('Date: %s' % build_http_date(time.time()))
if self._http_version=='1.0':
if self._http_connection=='keep-alive' and \
self.headers.has_key('content-length'):
self.setHeader('Connection','Keep-Alive')
else:
self.setHeader('Connection','close')
# Close the connection if we have been asked to.
# Use chunking if streaming output.
if self._http_version=='1.1':
if self._http_connection=='close':
self.setHeader('Connection','close')
elif not self.headers.has_key('content-length'):
if self.http_chunk and self._streaming:
self.setHeader('Transfer-Encoding','chunked')
self._chunking=1
else:
self.setHeader('Connection','close')
for key, val in headers.items():
if key.lower()==key:
# only change non-literal header names
key="%s%s" % (key[:1].upper(), key[1:])
start=0
l=key.find('-',start)
while l >= start:
key="%s-%s%s" % (key[:l],key[l+1:l+2].upper(),key[l+2:])
start=l+1
l=key.find('-',start)
append("%s: %s" % (key, val))
if self.cookies:
headersl=headersl+self._cookie_list()
headersl[len(headersl):]=[self.accumulated_headers, body]
return "\r\n".join(headersl)
class Retry(Exception):
"""Raise this to retry a request
"""
def __init__(self, t=None, v=None, tb=None):
self._args=t, v, tb
def reraise(self):
t, v, tb = self._args
if t is None: t=Retry
if tb is None: raise t, v
try: raise t, v, tb
finally: tb=None
def call_object(object, args, request):
result=apply(object,args) # Type s<cr> to step into published object.
return result
def missing_name(name, request):
if name=='self': return request['PARENTS'][0]
request.response.badRequestError(name)
def dont_publish_class(klass, request):
request.response.forbiddenError("class %s" % klass.__name__)
_default_debug_mode = False
_default_realm = None
def set_default_debug_mode(debug_mode):
global _default_debug_mode
_default_debug_mode = debug_mode
def set_default_authentication_realm(realm):
global _default_realm
_default_realm = realm
def publish(request, module_name, after_list, debug=0,
# Optimize:
call_object=call_object,
missing_name=missing_name,
dont_publish_class=dont_publish_class,
mapply=mapply,
):
(bobo_before, bobo_after, object, realm, debug_mode, err_hook,
validated_hook, transactions_manager)= get_module_info(module_name)
parents=None
response=None
try:
request.processInputs()
request_get=request.get
response=request.response
# First check for "cancel" redirect:
if request_get('SUBMIT','').strip().lower()=='cancel':
cancel=request_get('CANCEL_ACTION','')
if cancel:
raise Redirect, cancel
after_list[0]=bobo_after
if debug_mode:
response.debug_mode=debug_mode
if realm and not request.get('REMOTE_USER',None):
response.realm=realm
if bobo_before is not None:
bobo_before()
# Get the path list.
# According to RFC1738 a trailing space in the path is valid.
path=request_get('PATH_INFO')
request['PARENTS']=parents=[object]
if transactions_manager:
transactions_manager.begin()
object=request.traverse(path, validated_hook=validated_hook)
if transactions_manager:
transactions_manager.recordMetaData(object, request)
result=mapply(object, request.args, request,
call_object,1,
missing_name,
dont_publish_class,
request, bind=1)
if result is not response:
response.setBody(result)
if transactions_manager:
transactions_manager.commit()
return response
except:
# DM: provide nicer error message for FTP
sm = None
if response is not None:
sm = getattr(response, "setMessage", None)
if sm is not None:
from asyncore import compact_traceback
cl,val= sys.exc_info()[:2]
sm('%s: %s %s' % (
getattr(cl,'__name__',cl), val,
debug_mode and compact_traceback()[-1] or ''))
if err_hook is not None:
if parents:
parents=parents[0]
try:
try:
return err_hook(parents, request,
sys.exc_info()[0],
sys.exc_info()[1],
sys.exc_info()[2],
)
except Retry:
if not request.supports_retry():
return err_hook(parents, request,
sys.exc_info()[0],
sys.exc_info()[1],
sys.exc_info()[2],
)
finally:
if transactions_manager:
transactions_manager.abort()
# Only reachable if Retry is raised and request supports retry.
newrequest=request.retry()
request.close() # Free resources held by the request.
try:
return publish(newrequest, module_name, after_list, debug)
finally:
newrequest.close()
else:
if transactions_manager:
transactions_manager.abort()
raise
def publish_module_standard(environ, start_response):
must_die=0
status=200
after_list=[None]
stdout = StringIO()
stderr = StringIO()
response = WSGIResponse(stdout=stdout, stderr=stderr)
response._http_version = environ['SERVER_PROTOCOL'].split('/')[1]
response._http_connection = environ.get('CONNECTION_TYPE', 'close')
response._server_version = environ['SERVER_SOFTWARE']
request = Request(environ['wsgi.input'], environ, response)
# Let's support post-mortem debugging
handle_errors = environ.get('wsgi.handleErrors', True)
try:
response = publish(request, 'Zope2', after_list=[None],
debug=handle_errors)
except SystemExit, v:
must_die=sys.exc_info()
request.response.exception(must_die)
except ImportError, v:
if isinstance(v, tuple) and len(v)==3: must_die=v
elif hasattr(sys, 'exc_info'): must_die=sys.exc_info()
else: must_die = SystemExit, v, sys.exc_info()[2]
request.response.exception(1, v)
except:
request.response.exception()
status=response.getStatus()
if response:
# Start the WSGI server response
status = response.getHeader('status')
# ZServerHTTPResponse calculates all headers and things when you
# call it's __str__, so we need to get it, and then munge out
# the headers from it. It's a bit backwards, and we might optimize
# this by not using ZServerHTTPResponse at all, and making the
# HTTPResponses more WSGI friendly. But this works.
result = str(response)
headers, body = result.split('\r\n\r\n',1)
headers = [tuple(n.split(': ',1)) for n in headers.split('\r\n')[1:]]
start_response(status, headers)
# If somebody used response.write, that data will be in the
# stdout StringIO, so we put that before the body.
# XXX This still needs verification that it really works.
result=(stdout.getvalue(), body)
request.close()
stdout.close()
if after_list[0] is not None: after_list[0]()
if must_die:
# Try to turn exception value into an exit code.
try:
if hasattr(must_die[1], 'code'):
code = must_die[1].code
else: code = int(must_die[1])
except:
code = must_die[1] and 1 or 0
if hasattr(request.response, '_requestShutdown'):
request.response._requestShutdown(code)
try: raise must_die[0], must_die[1], must_die[2]
finally: must_die=None
# Return the result body iterable.
return result
_l=allocate_lock()
def get_module_info(module_name, modules={},
acquire=_l.acquire,
release=_l.release,
):
if modules.has_key(module_name): return modules[module_name]
if module_name[-4:]=='.cgi': module_name=module_name[:-4]
acquire()
tb=None
g = globals()
try:
try:
module=__import__(module_name, g, g, ('__doc__',))
# Let the app specify a realm
if hasattr(module,'__bobo_realm__'):
realm=module.__bobo_realm__
elif _default_realm is not None:
realm=_default_realm
else:
realm=module_name
# Check for debug mode
debug_mode=None
if hasattr(module,'__bobo_debug_mode__'):
debug_mode=not not module.__bobo_debug_mode__
else:
debug_mode = _default_debug_mode
bobo_before = getattr(module, "__bobo_before__", None)
bobo_after = getattr(module, "__bobo_after__", None)
if hasattr(module,'bobo_application'):
object=module.bobo_application
elif hasattr(module,'web_objects'):
object=module.web_objects
else: object=module
error_hook=getattr(module,'zpublisher_exception_hook', None)
validated_hook=getattr(module,'zpublisher_validated_hook', None)
transactions_manager=getattr(
module,'zpublisher_transactions_manager', None)
if not transactions_manager:
# Create a default transactions manager for use
# by software that uses ZPublisher and ZODB but
# not the rest of Zope.
transactions_manager = DefaultTransactionsManager()
info= (bobo_before, bobo_after, object, realm, debug_mode,
error_hook, validated_hook, transactions_manager)
modules[module_name]=modules[module_name+'.cgi']=info
return info
except:
t,v,tb=sys.exc_info()
v=str(v)
raise ImportError, (t, v), tb
finally:
tb=None
release()
class DefaultTransactionsManager:
def begin(self):
transaction.begin()
def commit(self):
transaction.commit()
def abort(self):
transaction.abort()
def recordMetaData(self, object, request):
# Is this code needed?
request_get = request.get
T= transaction.get()
T.note(request_get('PATH_INFO'))
auth_user=request_get('AUTHENTICATED_USER',None)
if auth_user is not None:
T.setUser(auth_user, request_get('AUTHENTICATION_PATH'))
# profiling support
_pfile = None # profiling filename
_plock=allocate_lock() # profiling lock
_pfunc=publish_module_standard
_pstat=None
def install_profiling(filename):
global _pfile
_pfile = filename
def pm(environ, start_response):
try:
r=_pfunc(environ, start_response)
except: r=None
sys._pr_=r
def publish_module_profiled(environ, start_response):
import profile, pstats
global _pstat
_plock.acquire()
try:
if request is not None:
path_info=request.get('PATH_INFO')
else: path_info=environ.get('PATH_INFO')
if path_info[-14:]=='manage_profile':
return _pfunc(environ, start_response)
pobj=profile.Profile()
pobj.runcall(pm, menviron, start_response)
result=sys._pr_
pobj.create_stats()
if _pstat is None:
_pstat=sys._ps_=pstats.Stats(pobj)
else: _pstat.add(pobj)
finally:
_plock.release()
if result is None:
try:
error=sys.exc_info()
file=open(_pfile, 'w')
file.write(
"See the url "
"http://www.python.org/doc/current/lib/module-profile.html"
"\n for information on interpreting profiler statistics.\n\n"
)
sys.stdout=file
_pstat.strip_dirs().sort_stats('cumulative').print_stats(250)
_pstat.strip_dirs().sort_stats('time').print_stats(250)
file.flush()
file.close()
except: pass
raise error[0], error[1], error[2]
return result
def publish_module(environ, start_response):
""" publish a Python module, with or without profiling enabled """
if _pfile: # profiling is enabled
return publish_module_profiled(environ, start_response)
else:
return publish_module_standard(environ, start_response)
......@@ -311,6 +311,16 @@ class ChannelPipe:
self._close=1
self._request.reply_code=response.status
def start_response(self, status, headers, exc_info=None):
# Used for WSGI
self._request.reply_code = int(status.split(' ')[0])
status = 'HTTP/%s %s\r\n' % (self._request.version, status)
self.write(status)
headers = '\r\n'.join([': '.join(x) for x in headers])
self.write(headers)
self.write('\r\n\r\n')
return self.write
is_proxying_match = re.compile(r'[^ ]* [^ \\]*:').match
proxying_connection_re = re.compile ('Proxy-Connection: (.*)', re.IGNORECASE)
......
......@@ -279,6 +279,48 @@ class zhttp_handler:
</ul>""" %(self.module_name, self.hits)
)
from HTTPResponse import ChannelPipe
class zwsgi_handler(zhttp_handler):
def continue_request(self, sin, request):
"continue handling request now that we have the stdin"
s=get_header(CONTENT_LENGTH, request.header)
if s:
s=int(s)
else:
s=0
DebugLogger.log('I', id(request), s)
env=self.get_environment(request)
version = request.version
if version=='1.0' and is_proxying_match(request.request):
# a request that was made as if this zope was an http 1.0 proxy.
# that means we have to use some slightly different http
# headers to manage persistent connections.
connection_re = proxying_connection_re
else:
# a normal http request
connection_re = CONNECTION
env['http_connection'] = get_header(connection_re,
request.header).lower()
env['server_version']=request.channel.server.SERVER_IDENT
env['wsgi.output'] = ChannelPipe(request)
env['wsgi.input'] = sin
env['wsgi.errors'] = sys.stderr
env['wsgi.version'] = (1,0)
env['wsgi.multithread'] = True
env['wsgi.multiprocess'] = True
env['wsgi.run_once'] = True
env['wsgi.url_scheme'] = env['SERVER_PROTOCOL'].split('/')[0]
request.channel.current_request=None
request.channel.queue.append(('Zope2WSGI', env,
env['wsgi.output'].start_response))
request.channel.work()
class zhttp_channel(http_channel):
......
......@@ -14,13 +14,25 @@
class ZServerPublisher:
def __init__(self, accept):
from ZPublisher import publish_module
from ZPublisher.WSGIPublisher import publish_module as publish_wsgi
while 1:
try:
name, request, response=accept()
publish_module(
name,
request=request,
response=response)
finally:
response._finish()
request=response=None
name, a, b=accept()
if name == "Zope2":
try:
publish_module(
name,
request=a,
response=b)
finally:
b._finish()
a=b=None
elif name == "Zope2WSGI":
try:
res = publish_wsgi(a, b)
for r in res:
a['wsgi.output'].write(r)
finally:
# TODO: Support keeping connections open.
a['wsgi.output']._close = 1
a['wsgi.output'].close()
......@@ -19,6 +19,7 @@
receive WebDAV source responses to GET requests.
</description>
</key>
<key name="use-wsgi" datatype="boolean" default="off" />
</sectiontype>
<sectiontype name="webdav-source-server"
......@@ -26,6 +27,7 @@
implements="ZServer.server">
<key name="address" datatype="inet-binding-address"/>
<key name="force-connection-close" datatype="boolean" default="off"/>
<key name="use-wsgi" datatype="boolean" default="off" />
</sectiontype>
<sectiontype name="persistent-cgi"
......
......@@ -71,6 +71,7 @@ class HTTPServerFactory(ServerFactory):
# webdav-source-server sections won't have webdav_source_clients:
webdav_clients = getattr(section, "webdav_source_clients", None)
self.webdav_source_clients = webdav_clients
self.use_wsgi = section.use_wsgi
def create(self):
from ZServer.AccessLogger import access_logger
......@@ -86,7 +87,10 @@ class HTTPServerFactory(ServerFactory):
def createHandler(self):
from ZServer import HTTPServer
return HTTPServer.zhttp_handler(self.module, '', self.cgienv)
if self.use_wsgi:
return HTTPServer.zwsgi_handler(self.module, '', self.cgienv)
else:
return HTTPServer.zhttp_handler(self.module, '', self.cgienv)
class WebDAVSourceServerFactory(HTTPServerFactory):
......
......@@ -20,12 +20,16 @@ import sys
import socket
from re import compile
from socket import gethostbyaddr
try:
import twisted.internet.reactor
_use_twisted = True
except ImportError:
_use_twisted = True
import ZConfig
from ZConfig.components.logger import loghandler
logger = logging.getLogger("Zope")
started = False
......@@ -96,7 +100,10 @@ class ZopeStarter:
self.makePidFile()
self.setupInterpreter()
self.startZope()
self.registerSignals()
from App.config import getConfiguration
config = getConfiguration()
if not config.twisted_servers:
self.registerSignals()
# emit a "ready" message in order to prevent the kinds of emails
# to the Zope maillist in which people claim that Zope has "frozen"
# after it has emitted ZServer messages.
......@@ -106,10 +113,24 @@ class ZopeStarter:
def run(self):
# the mainloop.
try:
from App.config import getConfiguration
config = getConfiguration()
import ZServer
import Lifetime
Lifetime.loop()
sys.exit(ZServer.exit_code)
if config.twisted_servers and config.servers:
raise ZConfig.ConfigurationError(
"You can't run both ZServer servers and twisted servers.")
if config.twisted_servers:
if not _use_twisted:
raise ZConfig.ConfigurationError(
"You do not have twisted installed.")
twisted.internet.reactor.run()
# Storing the exit code in the ZServer even for twisted,
# but hey, it works...
sys.exit(ZServer.exit_code)
else:
import Lifetime
Lifetime.loop()
sys.exit(ZServer.exit_code)
finally:
self.shutdown()
......
......@@ -339,3 +339,11 @@ def zopeClassFactory(jar, module, name,
# Zope class factory." This no longer works with the implementation of
# mounted databases, so we just use the zopeClassFactory as the default
try:
from zope.app.twisted.server import ServerFactory
class TwistedServerFactory(ServerFactory):
pass
except ImportError:
class TwistedServerFactory:
def __init__(self, section):
raise ImportError("You do not have twisted installed.")
import os
import sys
import time
import logging
from re import compile
from socket import gethostbyaddr
try:
import twisted.internet
from twisted.application.service import MultiService
import zope.app.appsetup.interfaces
import zope.app.twisted.main
import twisted.web2.wsgi
import twisted.web2.server
import twisted.web2.log
try:
from twisted.web2.http import HTTPFactory
except ImportError:
from twisted.web2.channel.http import HTTPFactory
from zope.component import provideUtility
from zope.app.twisted.server import ServerType, SSLServerType
from zope.app.twisted.interfaces import IServerType
from ZPublisher.WSGIPublisher import publish_module
_use_twisted = True
except ImportError:
_use_twisted = False
# top-level key handlers
......@@ -133,7 +159,7 @@ def catalog_getObject_raises(value):
"'catalog-getObject-raises' option will be removed in Zope 2.10:\n",
DeprecationWarning)
from Products.ZCatalog import CatalogBrains
from Products.ZCatalog import CatalogBrains
CatalogBrains.GETOBJECT_RAISES = bool(value)
return value
......@@ -143,7 +169,8 @@ def catalog_getObject_raises(value):
def root_handler(config):
""" Mutate the configuration with defaults and perform
fixups of values that require knowledge about configuration
values outside of their context. """
values outside of their context.
"""
# Set environment variables
for k,v in config.environment.items():
......@@ -165,7 +192,7 @@ def root_handler(config):
instanceprod = os.path.join(config.instancehome, 'Products')
if instanceprod not in config.products:
config.products.append(instanceprod)
import Products
L = []
for d in config.products + Products.__path__:
......@@ -190,6 +217,23 @@ def root_handler(config):
config.cgi_environment,
config.port_base)
if not config.twisted_servers:
config.twisted_servers = []
else:
# Set number of threads (reuse zserver_threads variable)
twisted.internet.reactor.suggestThreadPoolSize(config.zserver_threads)
# Create a root service
rootService = MultiService()
for server in config.twisted_servers:
service = server.create(None)
service.setServiceParent(rootService)
rootService.startService()
twisted.internet.reactor.addSystemEventTrigger(
'before', 'shutdown', rootService.stopService)
# set up trusted proxies
if config.trusted_proxies:
import ZPublisher.HTTPRequest
......@@ -217,3 +261,15 @@ def _name2Ips(host, isIp_=compile(r'(\d+\.){3}').match):
if isIp_(host): return [host]
return gethostbyaddr(host)[2]
# Twisted support:
def createHTTPFactory(ignored):
resource = twisted.web2.wsgi.WSGIResource(publish_module)
resource = twisted.web2.log.LogWrapperResource(resource)
return HTTPFactory(twisted.web2.server.Site(resource))
if _use_twisted:
http = ServerType(createHTTPFactory, 8080)
provideUtility(http, IServerType, 'Zope2-HTTP')
......@@ -11,6 +11,12 @@
<import package="tempstorage"/>
<import package="Zope2.Startup" file="warnfilter.xml"/>
<sectiontype name="server" datatype="Zope2.Startup.datatypes.TwistedServerFactory">
<key name="type" required="yes" />
<key name="address" datatype="inet-address" />
<key name="backlog" datatype="integer" default="50" />
</sectiontype>
<sectiontype name="logger" datatype=".LoggerFactory">
<description>
This "logger" type only applies to access and request ("trace")
......@@ -805,7 +811,9 @@
<metadefault>on</metadefault>
</key>
<multisection type="server" name="*" attribute="twisted_servers" />
<multisection type="ZServer.server" name="*" attribute="servers"/>
<key name="port-base" datatype="integer" default="0">
<description>
Base port number that gets added to the specific port numbers
......
......@@ -904,6 +904,8 @@ instancehome $INSTANCE
# valid keys are "address" and "force-connection-close"
address 8080
# force-connection-close on
# You can also use the WSGI interface between ZServer and ZPublisher:
# use-wsgi on
</http-server>
# Examples:
......@@ -947,6 +949,13 @@ instancehome $INSTANCE
# user admin
# password 123
# </clock-server>
#
# <server>
# # This uses Twisted as the web-server. You must install Twisted
# # separately. You can't run Twisted and ZServer at same time.
# address 8080
# type Zope2-HTTP
# </server>
# Database (zodb_db) section
......
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