Account Options

  1. Sign in
The old Google Groups will be going away soon, but your browser is incompatible with the new version.
Google Groups Home
« Groups Home
Message from discussion httpd-serve 1.27 (for emacs-wiki)
The group you are posting to is a Usenet group. Messages posted to this group will make your email address visible to anyone on the Internet.
Your reply message has not been sent.
Your post was successful
 
From:
To:
Cc:
Followup To:
Add Cc | Add Followup-to | Edit Subject
Subject:
Validation:
For verification purposes please type the characters you see in the picture below or the numbers you hear by clicking the accessibility icon. Listen and type the numbers you hear
 
John Wiegley  
View profile  
 More options Oct 25 2001, 8:17 pm
Newsgroups: gnu.emacs.sources
From: John Wiegley <jo...@gnu.org>
Date: Thu, 25 Oct 2001 17:15:54 -0700
Local: Thurs, Oct 25 2001 8:15 pm
Subject: httpd-serve 1.27 (for emacs-wiki)

httpd-serve is a Python HTTP server, which uses a dedicated Emacs
process to serve Wiki pages directly.  It supports editing of pages,
dynamic search queries (if glimpse is installed), etc.

This new version supports gzip'd .html files (i.e., a reference to
foo.html will serve either foo.html or foo.html.gz).  It also fixes
several bugs, and adds a nasty LIMITATIONS section at the beginning of
the script file.

This server has been working stably on alice.dynodns.net for several
months, and is capable of serving around 100 pages/sec.  Be sure to
read the directions both in this script, and in emacs-wiki.  Send mail
to me if you have troubles getting it setup.  Maybe we can work out a
HOWTO together... :)

John Wiegley <jo...@gnu.org>

----------------------------------------------------------------------
#!/usr/bin/env python

# $Revision: 1.27 $
#
# A simple HTTP server, written in Python, that can be used to serve
# Emacs Wiki pages directly from a running Emacs process.
#
# Usage is simple:
#
#   httpd-serve --port 8080 --load ~/Emacs/startup.el /var/www
#
# This will start a dedicated Emacs session, and will listen on port
# 8080 for HTTP requests.  If the request is for a plain file, it will
# be served directly from /var/www -- without using Emacs.  If the
# request is not for a plain file, it is passed to Emacs' httpd.el.
#
# startup.el should contain any startup routines you wish to load into
# your Emacs web server.
#
# NOTE: This script has only been test on Debian GNU/Linux, using
# gnuserv 2.1alpha (with Unix domain sockets), and Python's pty
# module.  See my web page for updates or new versions:
#
#   http://www.gci-net.com/users/j/johnw/emacs.html
#
# LIMITATIONS: The only flaw known so far is that httpd-serve must
# have access to an X server.  This is not necessary on all systems,
# and sometimes a plain pty works just fine.  You can try setting
# no_wins=1 at the top of this file.  If that works, and you're able
# to serve pages, you should not need to run httpd-serve under X.
#
# John Wiegley <jo...@gnu.org>

import os
import pty
import pwd
import re
import sys
import signal
import string
import time
import gzip

from socket import *
from signal import *
from resource import *

import cStringIO
import SimpleHTTPServer
import BaseHTTPServer

# Changing these numbers will affect the memory limits of the Emacs
# dedicated process.  These numbers are in megabytes.

MAX_HEAP  = 64
MAX_STACK = 2

port     = 80
log_file = None
pid_file = None
startup  = "_unknown"
debug    = 0
daemon   = 0
user     = os.geteuid()
no_wins  = 0

ignore_urls = [
    # block the Nimbda worm
    "^/(MSADC|msadc|_mem_bin|_vti_bin|scripts)/",
    "(\.(exe|dll)|c\+dir)",
]

class HTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
    "This class serves possibly httpd.el-related HTTP requests."

    extensions_map = {
        '':      'text/plain',   # Default, *must* be present
        '.css':  'text/css',
        '.html': 'text/html',
        '.htm':  'text/html',
        '.gif':  'image/gif',
        '.jpg':  'image/jpeg',
        '.jpeg': 'image/jpeg',
    }

    time_format = "%a, %d %b %Y %H:%M:%S %Z"

    def parse_request(self):
        """Parse a request (internal).  This is identical to
        BaseHTTPServer.parse_request, except that it leaves self.rfile
        alone."""

        self.request_version = version = "HTTP/0.9" # Default
        requestline = self.raw_requestline
        if requestline[-2:] == '\r\n':
            requestline = requestline[:-2]
        elif requestline[-1:] == '\n':
            requestline = requestline[:-1]
        self.requestline = requestline
        words = string.split(requestline)
        if len(words) == 3:
            [command, path, version] = words
            if version[:5] != 'HTTP/':
                self.send_error(400, "Bad request version (%s)" % `version`)
                return 0
        elif len(words) == 2:
            [command, path] = words
            if command != 'GET':
                self.send_error(400,
                                "Bad HTTP/0.9 request type (%s)" % `command`)
                return 0
        elif not requestline:
            line = ""
            try: line = self.rfile.readline()
            except: pass
            if not line:
                return 0
            else:
                self.raw_requestline = line
                return self.parse_request()
        else:
            self.send_error(400, "Bad request syntax (%s)" % `requestline`)
            return 0
        self.command, self.path, self.request_version = command, path, version
        return 1

    def handle(self):
        """Handle an HTTP request.

        If it's a plain file request, handle it in Python.  Otherwise,
        turn it over to httpd.el."""

        try:
            self.raw_requestline = line = self.rfile.readline()
            if not self.parse_request():
                return

            for regexp in ignore_urls:
                if re.search(regexp, self.path):
                    return
        except:
            return

        content_length = 0
        modified_since = 0
        accept_gzip    = 0
        encoding       = 0

        data = []

        while line != "\r\n" and line != "\n":
            if line[-2:] == '\r\n':
                line = line[:-2]
            elif line[-1:] == '\n':
                line = line[:-1]

            match = re.match("^Content-[Ll]ength:\s+([0-9]+)", line)
            if match:
                content_length = int(match.group(1))
            else:
                match = re.match("^Accept-[Ee]ncoding:\s+((x-)?gzip)", line)
                if match:
                    accept_gzip = match.group(1)
                else:
                    match = re.match("^If-Modified-Since:\s+(.+) GMT", line)
                    if match:
                        modified_since = time.strptime(match.group(1),
                                                       self.time_format)
                        modified_since = time.mktime(modified_since)

            data.append(line)
            line = self.rfile.readline()

        data.append(line)
        data = re.sub("\"", "\\\"", string.join(data, '\n'))

        content = ""
        if content_length > 0:
            content = self.rfile.read(content_length)
        content = re.sub("\"", "\\\"", content)

        # After translating the path, see whether httpd-serve or Emacs
        # will be the one to handle the request

        self.translated_path = self.translate_path(self.path)

        if self.command in ("GET", "HEAD") and \
           (os.path.isfile(self.translated_path) or \
            os.path.isfile(self.translated_path + ".gz")):

            if os.path.isfile(self.translated_path):
                real_file = self.translated_path
            else:
                real_file = self.translated_path + ".gz"

            info  = os.stat(real_file)
            mtime = info[8]

            if modified_since and mtime <= modified_since:
                self.send_response(304, "Not modified")
                self.log_request(304)
                return

            revision = "$Revision: 1.27 $"
            match = re.match(": ([0-9.]+) \$", revision)
            if match:
                revision = match.group(1)
            else:
                revision = "0.1"

            code = 200
            type = self.guess_type(self.translated_path)

            if re.search("\.gz$", real_file):
                encoding = "x-gzip"

            headers = [
                "HTTP/1.0 200 OK",
                "Server: httpd-serve/%s" % revision,
                "MIME-Version: 1.0",
                "Date: %s" % self.date_time_string(),
                "Last-Modified: %s" %
                time.strftime(self.time_format, time.localtime(mtime)),
                "Content-Type: %s" % type
            ]

            if encoding:
                headers.append("Content-Encoding: %s" % encoding)

            headers.append("Content-Length: %d" % info[6])

            fd = open(real_file, "rb")
            data = fd.read()
            fd.close()

            # jww (2001-05-21): Because many browsers are still
            # braindead, compression is only safe with html files.

            if type != "text/html":
                accept_gzip = 0
        else:
            info = None

            # If Emacs died while we were waiting for a web request,
            # restart it now.  In theory this should never occur, but
            # Emacs has been known to core silently.

            try:
                status = os.waitpid(emacs_pid, os.WNOHANG)
                if status != (0, 0):
                    self.log_message("Emacs died waiting with status %s",
                                     `status`)
                    restart_emacs(self)
            except:
                self.log_message("Failed to communicate with Emacs process")
                restart_emacs(self)

            file = gnuserv_socket()
            if not file:
                self.send_error(501, "Gnuserv not found; please try again")
                restart_emacs(self)
                return

            try:
                alarm(60)
                s = socket(AF_UNIX, SOCK_STREAM)
                s.connect(file)
                s.send("""(server-eval
                            '(with-output-to-string
                               (httpd-serve "%s" "%s")))\004""" %
                       (data, content))
                s.shutdown(1)
                alarm(0)
            except AlarmTimeout:
                self.send_error(501, "Timeout waiting for Emacs eval")
                restart_emacs(self)
                return

            data = s.recv(300)
            if data == "nil":
                self.send_error(501, "Error handling request")
                return
            elif not data:
                self.send_error(501, "Something bad happened to Emacs")
                restart_emacs(self)
                return

            code = 501
            if data:
                l = string.split(data)
                if len(l) > 1:
                    code = l[1]

            f = cStringIO.StringIO()
            while data:
                f.write(data)
                data = s.recv(300)
            s.close()
            data = f.getvalue()
            f.close()

            pos = string.find(data, '\n\n')
            if pos < 0: return

            headers = string.split(data[: pos], '\n')
            data    = data[pos :]

            if modified_since:
                mtime = 0
                for header in headers:
                    match = re.match("^Last-Modified:\s+(.+)", header)
                    if match:
                        mtime = time.strptime(match.group(1), self.time_format)
                        break

                if mtime <= modified_since:
                    self.send_response(304, "Not modified")
                    self.log_request(304)
                    return

        if accept_gzip and not encoding:
            f = cStringIO.StringIO()
            g = gzip.GzipFile(filename = "", mode = "wb", fileobj = f)
            g.write(data)
            g.close()
            data = f.getvalue()

            i = len(headers) - 1
            while i >= 0:
                if re.match("^Content-Length:", headers[i]):
                    headers[i] = "Content-Length: %d" % len(data)
                    break
                i = i - 1

            headers = headers[:-1] + \
                      ["Content-Encoding: %s" % accept_gzip] + \
                      headers[-1:]
        elif not info:
            # Add in the two extra bytes for the conversion from \n to
            # \r\n applied by this server
            i = len(headers) - 1
            while i >= 0:
                match = re.match("^Content-Length:\s+([0-9]+)", headers[i])
                if match:
                    headers[i] = "Content-Length: %d" % \
                                 (int(match.group(1)) + 2)
                    break
                i = i - 1

        try:
            self.wfile.write(string.join(headers, '\r\n'))
            self.wfile.write('\r\n')
            self.wfile.write('\r\n')

            if self.command != "HEAD":
                self.wfile.write(data)
        except:
            self.log_message("Error sending HTTP data to client")
            return

        self.log_request(code, len(data))

    def send_head(self):
        """Common code for GET and HEAD commands.

        This sends the response code and MIME headers.

        Return value is either a file object (which has to be copied
        to the outputfile by the caller unless the command was HEAD,
        and must be closed by the caller under all circumstances), or
        None, in which case the caller has nothing further to do."""

        path = self.translated_path
        if os.path.isdir(path):
            self.send_error(403, "Directory listing not supported")
            return None

        try:
            f = open(path, 'rb')
        except IOError:
            self.send_error(404, "File %s not found" % path)
            return None

        self.send_response(200)
        self.send_header("Content-type", self.guess_type(path))
        self.end_headers()

        return f

    def log_message(self, format, *args):
        append_log(self.address_string(), format % args)

monthname = [None,
             'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
             'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

def log_date_time_string():
    """Return the current time formatted for logging."""
    now = time.time()
    year, month, day, hh, mm, ss, x, y, z = time.localtime(now)
    s = "%02d/%3s/%04d %02d:%02d:%02d" % (
            day, monthname[month], year, hh, mm, ss)
    return s

def append_log(stage, str):
    if not log_file: return
    msg = "%s - - [%s] %s\n" % (stage, log_date_time_string(), str)
    if log_file == "stdout":
        print msg,
    else:
        log = open(log_file, 'a')
        log.write(msg)
        log.close()

def limit_process():
    def dopple(num):
        return (num, num)
    setrlimit(RLIMIT_DATA, dopple(MAX_HEAP * 1024 * 1024))
    setrlimit(RLIMIT_RSS, dopple(MAX_HEAP * 1024 * 1024))
    setrlimit(RLIMIT_STACK, dopple(MAX_STACK * 1024 * 1024))
    setrlimit(RLIMIT_NPROC, dopple(100))
    setrlimit(RLIMIT_NOFILE, dopple(32))

emacs_pid = None

def start_emacs():
    global emacs_pid

    if debug:
        emacs_pid = os.fork()
    else:
        emacs_pid = pty.fork()[0]

    if emacs_pid == 0:
        try:
            try:
                if user and user != os.geteuid():
                    os.setuid(user)
            except:
                pass

            limit_process()

            pargs = ["emacs", "--no-init-file", "--no-site-file"]
            if no_wins and not debug:
                pargs.append("--no-windows")
            pargs.extend(["--unibyte", "--eval", """
    (progn
      (setq initial-frame-alist '((visibility . nil))
            message-log-max nil)
      (require 'gnuserv)
      (gnuserv-start)
      (load "httpd")
      (load "cgi" t)
      (load "%s" t)
      (setq httpd-terminate-emacs nil
            httpd-line-terminator "\n"
            httpd-document-root "%s"))""" % (startup, html_dir)])

            os.execvp(pargs[0], pargs)
        finally:
            os._exit(1)
        os._exit(1)             # shouldn't reach here

def kill_emacs(signo = SIGTERM):
    global emacs_pid
    os.kill(emacs_pid, signo)
    os.waitpid(emacs_pid, 0)
    emacs_pid = None

def restart_emacs(handler):
    try:
        kill_emacs(SIGKILL)
        handler.log_message("Killed Emacs process pid %d", emacs_pid)
    except:
        pass
    handler.log_message("Starting new Emacs process")
    start_emacs()
    handler.log_message("New Emacs process is pid %d", emacs_pid)
    time.sleep(2)

def gnuserv_socket():
    file = "/tmp/gsrvdir%d/gsrv" % user
    if os.path.exists(file):
        return file
    file = "/tmp/gsrv%d" % user
    if os.path.exists(file):
        return file
    return None

# daemonize() is a function which makes the process calling it into a
# "daemon". The process is detached from the current shell and will
# not receive hangup when the shell exits.
#
# Steinar Knutsen, 1997

def daemonize():
    import time

    pid = os.fork()
    if not pid:
        otherpid = os.fork()
        if not otherpid:
            ppid = os.getppid()
            while ppid != 1:
                time.sleep(0.5)
                ppid = os.getppid()
            return
        else:
                os._exit(0)
    else:
        os.wait()
        sys.exit(0)

# provide an HTTP server, so that web users can manipulate the cluster
# as well

from getopt import getopt

(opts, args) = getopt(sys.argv[1:], [],
                      longopts = [ "help", "port=", "log-file=", "load=",
                                   "daemon", "pidfile=", "user=", "debug" ])

def usage():
    print """
Welcome to the server program.  Command syntax is:

  httpd-serve [options] [server-directory]

The SERVER-DIRECTORY is where your static HTML files (if any) are
kept.  This includes stylesheet, images, etc.  It will be the "root"
of the httpd server.  The default value is /var/www.

Possible options are:

  --help               Show this page
  --debug              Enable debugging mode
  --daemon             Make the HTTP server run as a daemon
  --load FILE          Eval Emacs Lisp code in FILE during startup
  --log-file FILE      Output logging information into FILE
  --pidfile FILE       Write out the process id of httpd-serve to FILE
  --port NUMBER        Use PORT as the server port; default is 80
  --user USER/UID      Run Emacs process as USER (default is 'nobody')
"""
    sys.exit(0)

for opt in opts:
    if opt[0] == "--port":
        port = int(opt[1])
    elif opt[0] == "--debug":
        debug = 1
    elif opt[0] == "--daemon":
        daemon = 1
    elif opt[0] == "--load":
        startup = opt[1]
    elif opt[0] == "--log-file":
        log_file = opt[1]
    elif opt[0] == "--pidfile":
        pid_file = opt[1]
    elif opt[0] == "--user":
        user = opt[1]
        if not re.match('^[0-9]+$', user):
            user = pwd.getpwnam(user)
            if user:
                user = user[2]
    elif opt[0] == "--help":
        usage()

if len(args) == 0:
    html_dir = "/var/www"
else:
    html_dir = args[0]

os.chdir(html_dir)

if not daemon and not log_file:
    log_file = "stdout"

# Start the Python HTTP server

if daemon: daemonize()
limit_process()

if pid_file:
    fd = open(pid_file, "w")
    fd.write("%d\n" % os.getpid())
    fd.close()

# Start our dedicated Emacs server

start_emacs()

if no_wins == 1:
    time.sleep(5)
    if not gnuserv_socket():
        no_wins = 0
        # Give it a try under X, if available
        if not os.environ.has_key('DISPLAY'):
            os.environ['DISPLAY'] = ":0"
        restart_emacs()

time.sleep(5)
if not gnuserv_socket():
    append_log("localhost", "Gnuserv failed to start, aborting")
    kill_emacs(SIGKILL)
    sys.exit(1)

# Setup signal handlers for the HTTP process

def interrupt(signo, frame):
    append_log("localhost", "httpd-serve died with signal %d" % signo)
    try: kill_emacs(SIGKILL)            # Sorry Emacs, time to go
    except: pass
    sys.exit(1)

signal(SIGTERM, interrupt)
signal(SIGKILL, interrupt)
signal(SIGQUIT, interrupt)

def ignore(signo, frame): pass

signal(SIGINT, ignore)
signal(SIGHUP, ignore)

class AlarmTimeout(Exception): pass

def timeout(signum, frame):
    raise AlarmTimeout()

signal(SIGALRM, timeout)

# Start serving HTTP requests on `port'!

BaseHTTPServer.HTTPServer(('', port), HTTPRequestHandler).serve_forever()


 
You must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.