##################################################
# SPYCE - Python-based HTML Scripting
# Copyright (c) 2002 Rimon Barr.
#
# Refer to spyce.py
# CVS: $Id: spyceWWW.py 1159 2006-08-16 23:33:19Z ellisj $
##################################################
import sys, os, string, socket, BaseHTTPServer, SocketServer, cgi, stat, time, threading
import spyce, spyceException, spyceCmd, spyceUtil, spycePreload
import pcqueue
__doc__ = '''Standalone Spyce web server.'''
LOG = 1
def formatBytes(bytes):
bytes = float(bytes)
if bytes<=9999: return "%6.0f" % bytes
bytes = bytes / float(1024)
if bytes<=999: return "%5.1fK" % bytes
bytes = bytes / float(1024)
return "%5.1fM" % bytes
##################################################
# Request / response handlers
#
class spyceHTTPRequest(spyce.spyceRequest):
'HTTP Spyce request object. (see spyce.spyceRequest)'
def __init__(self, httpdHandler, documentRoot):
spyce.spyceRequest.__init__(self)
self._in = httpdHandler.rfile
self._headers = httpdHandler.headers
self._httpdHandler = httpdHandler
self._documentRoot = documentRoot
self._env = None
def env(self, name=None):
if not self._env:
self._env = {
'REMOTE_ADDR': self._httpdHandler.client_address[0],
'REMOTE_PORT': self._httpdHandler.client_address[1],
'GATEWAY_INTERFACE': "CGI/1.1",
'REQUEST_METHOD': self._httpdHandler.command,
'REQUEST_URI': self._httpdHandler.path,
'PATH_INFO': self._httpdHandler.pathinfo,
'SERVER_SOFTWARE': 'spyce/%s' % spyce.__version__,
'SERVER_PROTOCOL': self._httpdHandler.request_version,
'DOCUMENT_ROOT': self._documentRoot,
'QUERY_STRING':
string.join(string.split(self._httpdHandler.path, '?')[1:]) or '',
'CONTENT_LENGTH': self.getHeader('Content-Length'),
'CONTENT_TYPE': self.getHeader('Content-type'),
'HTTP_USER_AGENT': self.getHeader('User-Agent'),
'HTTP_ACCEPT': self.getHeader('Accept'),
'HTTP_ACCEPT_ENCODING': self.getHeader('Accept-Encoding'),
'HTTP_ACCEPT_LANGUAGE': self.getHeader('Accept-Language'),
'HTTP_ACCEPT_CHARSET': self.getHeader('Accept-Charset'),
'HTTP_COOKIE': self.getHeader('Cookie'),
'HTTP_REFERER': self.getHeader('Referer'),
'HTTP_HOST': self.getHeader('Host'),
'HTTP_CONNECTION': self.getHeader('Connection'),
'HTTP_KEEP_ALIVE': self.getHeader('Keep-alive'),
# From ASP
# AUTH_TYPE,
# APPL_PHYSICAL_PATH,
# REMOTE_HOST,
# SERVER_PROTOCOL,
# SERVER_SOFWARE
}
return spyceUtil.extractValue(self._env, name)
def getHeader(self, type=None):
if type: type=string.lower(type)
return spyceUtil.extractValue(self._headers.dict, type)
def getServerID(self):
return os.getpid()
class spyceHTTPResponse(spyceCmd.spyceCmdlineResponse):
'HTTP Spyce response object. (see spyce.spyceResponse)'
def __init__(self, httpdHandler):
self._httpheader = httpdHandler.request_version!='HTTP/0.9'
spyceCmd.spyceCmdlineResponse.__init__(self, spyceUtil.NoCloseOut(httpdHandler.wfile), sys.stdout, self._httpheader)
self._httpdHandler = httpdHandler
# incidentally, this a rather expensive operation!
if LOG:
httpdHandler.log_request()
def sendHeaders(self):
if self._httpheader and not self.headersSent:
resultText = self.RETURN_CODE.get(self.returncode)
self.origout.write('%s %s %s\n' % (self._httpdHandler.request_version, self.returncode, resultText))
spyceCmd.spyceCmdlineResponse.sendHeaders(self)
def close(self):
spyceCmd.spyceCmdlineResponse.close(self)
self._httpdHandler.request.close()
##################################################
# Spyce web server
#
class myHTTPhandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
if spyce.getServer().config.check_modules_and_restart:
L = spyceUtil.scan_modules()
if L:
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write("Detected changed modules %s. Restarting...\n(If you don't want Spyce to do this, turn off check_modules_and_restart in your spyceconf file.)" % L)
self.request.close()
os._exit(3)
try:
start = time.time()
try:
# parse pathinfo
pathinfo = os.path.normpath(string.split(self.path, '?')[0])
while pathinfo and (pathinfo[0]==os.sep or pathinfo[0:2]==os.pardir):
if pathinfo[0:len(os.sep)]==os.sep: pathinfo=pathinfo[len(os.sep):]
if pathinfo[0:len(os.pardir)]==os.pardir: pathinfo=pathinfo[len(os.pardir):]
self.pathinfo = "/"+pathinfo
# convert to path
path = os.path.join(self.server.documentRoot, pathinfo)
if os.path.exists(path):
if os.path.isdir(path):
# directory listing
pathparts = self.path.split('?', 1)
if not pathparts[0].endswith('/'):
self.send_response(spyce.spyceResponse.RETURN_MOVED_PERMANENTLY)
self.send_header('Location', '?'.join([pathparts[0] + '/'] + pathparts[1:]))
self.end_headers()
return
# check for index.spy, index.html, etc.
indexFile = None
for findex in spyce.getServer().config.indexFiles:
p2 = os.path.join(path, findex)
if os.path.exists(p2): indexFile = p2; break
if indexFile is None: handler_type = '/'
else:
path = indexFile
handler_type = os.path.splitext(path)[-1][1:]
else:
# regular files
handler_type = os.path.splitext(path)[-1][1:]
try: handler = self.server.handler[handler_type]
except KeyError: handler = self.server.handler[None]
else: # force 404s to spyce handler so error page can be customized
handler = self.__class__.handler_spyce
if spyce.DEBUG_ERROR: sys.stderr.write('handler is %s\n' % str(handler))
# process request
return handler(self, path)
except IOError:
self.send_error(404, "Unexpected IOError")
return None
finally: spyce.DEBUG('total for %s was %s' % (self.path, time.time() - start))
do_POST=do_GET
def handler_spyce(self, path):
# process spyce
request = spyceHTTPRequest(self, self.server.documentRoot)
response = spyceHTTPResponse(self)
result = spyce.spyceFileHandler(request, response, path)
response.close()
def handler_dump(self, path):
# process content to dump (with correct mime type)
f = None
try:
f = open(path, 'rb')
try:
_, ext = os.path.splitext(path)
if ext: ext=ext[1:]
mimetype = self.server.mimeTable[ext]
except:
mimetype = "application/octet-stream"
self.send_response(200)
self.send_header("Content-type", mimetype)
self.end_headers()
self.wfile.write(f.read())
self.request.close()
finally:
try:
if f: f.close()
except: pass
def handler_directory(self, path):
# process directory
if(self.path[-1:]!='/'):
self.send_response(301)
self.send_header('Location', self.path+'/')
self.end_headers()
return
L = os.listdir(path)
L.sort(lambda a, b: cmp(a.lower(), b.lower()))
def info(name, path=path):
fullname = os.path.join(path, name)
displayname = linkname = name = cgi.escape(name)
# Append / for directories or @ for symbolic links
if os.path.isdir(fullname):
displayname = name + "/"
linkname = name + "/"
elif os.path.islink(fullname):
displayname = name + "@"
statinfo = os.stat(fullname)
mtime = statinfo[stat.ST_MTIME]
size = statinfo[stat.ST_SIZE]
return linkname, displayname, mtime, size
L = map(info, L)
NAME_WIDTH = 30
output = '''
<html><head>
<title>Index of %(title)s</title>
</head>
<body>
<h1>Index of %(title)s</h1>
<pre> Name%(filler)s Date%(filler_date)s Size<hr/>''' % {
'title' : self.pathinfo.replace(os.path.sep, '/'),
'filler': ' '*(NAME_WIDTH-len('Name')),
'filler_date': ' '*(len(time.asctime(time.localtime(0)))-len('Date')),
}
if L:
for link, display, mtime, size in L:
output = output + ' <a href="%(link)s">%(display)s</a>%(filler)s %(mtime)s %(size)s\n' % {
'link': link,
'display': display[:NAME_WIDTH],
'link': link,
'filler': ' '*(NAME_WIDTH-len(display)),
'mtime': time.asctime(time.localtime(mtime)),
'size': formatBytes(size),
}
else:
output = output + 'No files\n'
output = output[:-1] + '''<hr/></pre>
<address>Spyce-WWW/%(version)s server</address>
</body></html>
''' % {
'version' : spyce.__version__,
}
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(output)
def buildMimeTable(files):
mimetable = {}
for file in files:
try:
f = None
try:
f = open(file, 'r')
line = f.readline()
while line:
if line[0]=='#':
line = f.readline(); continue
line = string.strip(line)
if not line:
line = f.readline(); continue
line = string.replace(line, '\t', ' ')
items = filter(None, map(string.strip, string.split(line, ' ')))
mimetype, extensions = items[0], items[1:]
for ext in extensions:
mimetable[ext] = mimetype
line = f.readline()
except IOError: pass
finally:
try:
if f: f.close()
except: pass
return mimetable
def buildHandlerTable(handler, server):
for ext in handler.keys():
handler[ext] = eval('server.handler_'+handler[ext])
if spyce.DEBUG_ERROR: sys.stderr.write('handlers are ' + str(handler) + '\n')
return handler
class ThreadPooledTcpServer(SocketServer.TCPServer):
allow_reuse_address = True
def __init__(self, minthreads, maxthreads, qsize, *args, **kwargs):
SocketServer.TCPServer.__init__(self, *args, **kwargs)
self.queue = pcqueue.PCQueue(minthreads, maxthreads, qsize)
self.queue.consume = self.consume
def consume(self, item):
request, client_address = item
try:
self.finish_request(request, client_address)
self.close_request(request)
except:
self.handle_error(request, client_address)
self.close_request(request)
def process_request(self, request, client_address):
self.queue.put((request, client_address))
def spyceHTTPserver(configFile, daemon=None):
stdout = sys.stdout
os.environ[spyce.SPYCE_ENTRY] = 'www'
config = spycePreload.getConfigModule(configFile)
if config.minthreads <= 0:
raise ValueError('minthreads must be at least 1')
if config.maxthreads < config.minthreads:
raise ValueError('maxthreads must be at least equal to minthreads')
# initialize mtime cache
spyceUtil.scan_modules()
# (_and_restart code inspired by WSGIkit)
if config.check_modules_and_restart:
reloader_environ_key = 'SPYCE_RELOADER_SHOULD_RUN'
if os.environ.get(reloader_environ_key):
stdout.write("# Running reloading file monitor (for check_modules_and_restart option)\n")
def exit_if_modules_changed():
L = spyceUtil.scan_modules()
if L:
stdout.write("# (Detected changed modules %s. Restarting...\n\n(If you don't want Spyce to do this, turn off check_modules_and_restart in your spyceconf file.)\n" % L)
os._exit(3)
import scheduler
scheduler.schedule(1, exit_if_modules_changed)
else:
args = spyceUtil.argsForSpawn([sys.executable, "-u"] + sys.argv)
while 1:
stdout.write("# Spawning another server (for check_modules_and_restart option)\n")
new_environ = os.environ.copy()
new_environ[reloader_environ_key] = 'true'
pid = os.spawnve(os.P_NOWAIT, sys.executable, args, new_environ)
# waitpid ignores keyboardinterrupt until the process it's waiting on finishes;
# the threading here is a workaround for that
exit_wrapper = [None]
def run(): exit_wrapper[0] = os.waitpid(pid, 0)
th = threading.Thread(target=run)
th.setDaemon(True)
th.start()
try:
while th.isAlive(): time.sleep(0.1)
except (SystemExit, KeyboardInterrupt), e:
# try to clean up child process too
try:
import signal
os.kill(pid, signal.SIGTERM)
except (ImportError, AttributeError):
try: import win32api
except ImportError:
print "Unable to terminate child process during shutdown!\nPlease download and install the win32all package so this will work."
else: win32api.TerminateProcess(pid, -1)
raise e
exit_code = exit_wrapper[0][1]
spyce.DEBUG('RAW EXIT CODE %s' % (exit_code,))
exit_code = exit_code >> 8
# (3 == "changes detected, restart me")
if exit_code != 3: return exit_code
print '# Starting Spyce web server. v%s' % spyce.__version__
try:
print '# Configuration - %s' % configFile
server = spyce.getServer(config)
except (spyceException.spyceForbidden, spyceException.spyceNotFound), e:
print e
return
try:
# initialize server
try:
httpd = ThreadPooledTcpServer(config.minthreads, config.maxthreads, config.maxqueuesize,
(config.ipaddr, config.port), myHTTPhandler)
print '# Listening on - %s:%d' % (config.ipaddr, config.port)
httpd.documentRoot = os.path.abspath(config.root)
print '# Document root - '+httpd.documentRoot
httpd.mimeTable = buildMimeTable(config.mime)
httpd.handler = buildHandlerTable(config.www_handlers, myHTTPhandler)
if config.adminport:
import spyceConsole
spyceConsole.start_console_thread(config.adminport)
print '# Admin console listening on port %d' % config.adminport
except (SystemExit, KeyboardInterrupt): raise
except:
print 'Unable to start server on port %s' % config.port
print spyceUtil.exceptionString()
return -1
# daemonize
if daemon:
print '# Daemonizing process.'
try: spyceCmd.daemonize(pidfile=daemon)
except SystemExit: return 0 # expected
global LOG
LOG = 0
# process requests
print '# Ready.'
while 1:
try: httpd.handle_request()
except (SystemExit, KeyboardInterrupt): raise
except: stdout.write('Error: %s\n' % spyceUtil.exceptionString())
except KeyboardInterrupt: print 'Break!'
httpd.queue.join()
return 0
|