Hi

Intrigued by the revelation that lighttpd (www.lighttpd.net) does scgi out of 
the box and would also do file based socket connections (AF_UNIX).
I quickly produced modified scgi_server.py and quixote_handler.py scripts
which allow a unix socket to be used instead. These are mods on top of 
scgi-1.4.

I attach those below; they are not perfect, but currently it works for me.
I think it could be made a bit cleaner....

The quixote handler script takes an extra argument -s to specify the socket 
file.
The scgi_server script (when run standalone) assumes that if the 2nd arg is an 
int then it's to run on a tcp port; if it is a string it assumes it's a file 
name.

comments welcome

Jon
#!/usr/bin/env python
"""
A SCGI handler that uses Quixote to publish dynamic content.
"""

import sys
import time
import os
import getopt
import signal
from quixote import enable_ptl, publish
import scgi_server

pidfilename = None # set by main()

def debug(msg):
    timestamp = time.strftime("%Y-%m-%d %H:%M:%S",
                              time.localtime(time.time()))
    sys.stderr.write("[%s] %s\n" % (timestamp, msg))


class QuixoteHandler(scgi_server.SCGIHandler):

    # override in subclass
    publisher_class = publish.Publisher
    root_namespace = None
    prefix = ""

    def __init__(self, *args, **kwargs):
        debug("%s created" % self.__class__.__name__)
        scgi_server.SCGIHandler.__init__(self, *args, **kwargs)
        assert self.root_namespace, "You must provide a namespace to publish"
        self.publisher = self.publisher_class(self.root_namespace)

    def handle_connection (self, conn):
        input = conn.makefile("r")
        output = conn.makefile("w")

        env = self.read_env(input)

        # mod_scgi never passes PATH_INFO, fake it
        prefix = self.prefix
        path = env['SCRIPT_NAME']
        assert path[:len(prefix)] == prefix, (
                "path %r doesn't start with prefix %r" % (path, prefix))
        env['SCRIPT_NAME'] = prefix
        env['PATH_INFO'] = path[len(prefix):] + env.get('PATH_INFO', '')

        self.publisher.publish(input, output, sys.stderr, env)

        try:
            input.close()
            output.close()
            conn.close()
        except IOError, err:
            debug("IOError while closing connection ignored: %s" % err)

        if self.publisher.config.run_once:
            sys.exit(0)


class DemoHandler(QuixoteHandler):

    root_namespace = "quixote.demo"
    prefix = "/dynamic" # must match Location directive

    def __init__(self, *args, **kwargs):
        enable_ptl()
        QuixoteHandler.__init__(self, *args, **kwargs)


def change_uid_gid(uid, gid=None):
    "Try to change UID and GID to the provided values"
    # This will only work if this script is run by root.

    # Try to convert uid and gid to integers, in case they're numeric
    import pwd, grp
    try:
        uid = int(uid)
        default_grp = pwd.getpwuid(uid)[3]
    except ValueError:
        uid, default_grp = pwd.getpwnam(uid)[2:4]

    if gid is None:
        gid = default_grp
    else:
        try:
            gid = int(gid)
        except ValueError:
            gid = grp.getgrnam(gid)[2]

    os.setgid(gid)
    os.setuid(uid)


def term_signal(signum, frame):
    global pidfilename
    try:
        os.unlink(pidfilename)
    except OSError:
        pass
    sys.exit()

def main(handler=DemoHandler):
    usage = """Usage: %s [options]

    -F -- stay in foreground (don't fork)
    -P -- PID filename
    -l -- log filename
    -m -- max children
    -p -- TCP port to listen on
    -s -- Unix Socket to listen on
    -u -- user id to run under
    """ % sys.argv[0]
    nofork = 0
    global pidfilename
    pidfilename = "/var/tmp/quixote-scgi.pid"
    logfilename = "/var/tmp/quixote-scgi.log"
    max_children = 5    # scgi default
    uid = "nobody"
    port = 4000
    host = "127.0.0.1"
    sockfilename = ""
    unix_socket = False
    
    try:
        opts, args = getopt.getopt(sys.argv[1:], 'FP:l:m:p:u:s:')
    except getopt.GetoptError, exc:
        print >>sys.stderr, exc
        print >>sys.stderr, usage
        sys.exit(1)
    for o, v in opts:
        if o == "-F":
            nofork = 1
        elif o == "-P":
            pidfilename = v
        elif o == "-l":
            logfilename = v
        elif o == "-m":
            max_children = int(v)
        elif o == "-p":
            port = int(v)
        elif o == "-u":
            uid = v
        elif o == "-s":
            sockfilename = str(v)
            unix_socket = True

    log = open(logfilename, "a", 1)
    os.dup2(log.fileno(), 1)
    os.dup2(log.fileno(), 2)
    os.close(0)

    if os.getuid() == 0:
        change_uid_gid(uid)
    
    if nofork:
        scgi_server.SCGIServer(handler, host=host, port=port,
                               max_children=max_children,
                               socketfile=sockfilename,
                               unix_socket=unix_socket).serve()
    else:
        pid = os.fork()
        if pid == 0:
            pid = os.getpid()
            pidfile = open(pidfilename, 'w')
            pidfile.write(str(pid))
            pidfile.close()
            signal.signal(signal.SIGTERM, term_signal)
            try:
                scgi_server.SCGIServer(handler, host=host, port=port,
                                       max_children=max_children,
                                       socketfile=sockfilename,
                                       unix_socket=unix_socket).serve()
            finally:
                # grandchildren get here too, don't let them unlink the pid
                if pid == os.getpid():
                    try:
                        os.unlink(pidfilename)
                    except OSError:
                        pass

if __name__ == '__main__':
    main()

#!/usr/bin/env python
"""
A pre-forking SCGI server that uses file descriptor passing to off-load
requests to child worker processes.
"""

import sys
import socket
import os
import select
import errno
import fcntl
import signal
from scgi import passfd

# netstring utility functions
def ns_read_size(input):
    size = ""
    while 1:
        c = input.read(1)
        if c == ':':
            break
        elif not c:
            raise IOError, 'short netstring read'
        size = size + c
    return long(size)

def ns_reads(input):
    size = ns_read_size(input)
    data = ""
    while size > 0:
        s = input.read(size)
        if not s:
            raise IOError, 'short netstring read'
        data = data + s
        size -= len(s)
    if input.read(1) != ',':
        raise IOError, 'missing netstring terminator'
    return data

def read_env(input):
    headers = ns_reads(input)
    items = headers.split("\0")
    items = items[:-1]
    assert len(items) % 2 == 0, "malformed headers"
    env = {}
    for i in range(0, len(items), 2):
        env[items[i]] = items[i+1]
    return env

class SCGIHandler:

    # Subclasses should override the handle_connection method.

    def __init__(self, parent_fd):
        self.parent_fd = parent_fd

    def serve(self):
        while 1:
            try:
                os.write(self.parent_fd, "1") # indicates that child is ready
                fd = passfd.recvfd(self.parent_fd)
            except (IOError, OSError):
                # parent probably exited  (EPIPE comes thru as OSError)
                raise SystemExit
            conn = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
            # Make sure the socket is blocking.  Apparently, on FreeBSD the
            # socket is non-blocking.  I think that's an OS bug but I don't
            # have the resources to track it down.
            conn.setblocking(1)
            os.close(fd)
            self.handle_connection(conn)


    def read_env(self, input):
        return read_env(input)

    def handle_connection(self, conn):
        input = conn.makefile("r")
        output = conn.makefile("w")

        env = self.read_env(input)
        output.write("Content-Type: text/plain\r\n")
        output.write("\r\n")
        for k, v in env.items():
            output.write("%s: %r\n" % (k, v))

        output.close()
        input.close()
        conn.close()


class SCGIServer:

    DEFAULT_PORT = 4000
    DEFAULT_SOCKET_FILE = "/tmp/scgi.socket"
    
    def __init__(self, handler_class=SCGIHandler, host="", port=DEFAULT_PORT,
                 max_children=5,socketfile=DEFAULT_SOCKET_FILE,unix_socket=False):
        self.handler_class = handler_class
        self.host = host
        self.port = port
        self.max_children = max_children
        self.children = {} # { pid : fd }
        self.spawn_child()
        self.restart = 0
        self.unix_socket = unix_socket
        self.unix_socket_filename = socketfile
        
    #
    # Deal with a hangup signal.  All we can really do here is
    # note that it happened.
    #
    def hup_signal(self, signum, frame):
        self.restart = 1

    def spawn_child(self, conn=None):
        parent_fd, child_fd = passfd.socketpair(socket.AF_UNIX,
                                                socket.SOCK_STREAM)
        # make child fd non-blocking
        flags = fcntl.fcntl(child_fd, fcntl.F_GETFL, 0)
        fcntl.fcntl(child_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
        pid = os.fork()
        if pid == 0:
            if conn:
                conn.close() # in the midst of handling a request, close
                             # the connection in the child 
            os.close(child_fd)
            self.handler_class(parent_fd).serve()
            sys.exit(0)
        else:
            os.close(parent_fd)
            self.children[pid] = child_fd

    def reap_children(self):
        while self.children:
            (pid, status) = os.waitpid(-1, os.WNOHANG)
            if pid <= 0:
                break
            os.close(self.children[pid])
            del self.children[pid]

    def do_restart(self):
        #
        # First close connections to the children, which will cause them
        # to exit after finishing what they are doing.
        #
        for fd in self.children.values():
            os.close(fd)
        #
        # Then do a blocking wait on each until we have cleared the
        # slate.
        #
        for pid in self.children.keys():
            (pid, status) = os.waitpid(pid, 0)
        self.children = {}
        #
        # Fire off a new child, we'll be wanting it soon.
        #
        self.spawn_child()
        self.restart = 0
        

    def delegate_request(self, conn):
        """Pass a request fd to a child process to handle.  This method
        blocks if all the children are busy and we have reached the
        max_children limit."""

        # There lots of subtleties here.  First, we can't use the write
        # status of the pipes to the child since select will return true
        # if the buffer is not filled.  Instead, each child writes one
        # byte of data when it is ready for a request.  The normal case
        # is that a child is ready for a request.  We want that case to
        # be fast.  Also, we want to pass requests to the same child if
        # possible.  Finally, we need to gracefully handle children
        # dying at any time.

        # If no children are ready and we haven't reached max_children
        # then we want another child to be started without delay.
        timeout = 0

        while 1:
            try:
                r, w, e = select.select(self.children.values(), [], [], timeout)
            except select.error, e:
                if e[0] == errno.EINTR:  # got a signal, try again
                    continue
                raise
            if r:
                # One or more children look like they are ready.  Sort
                # the file descriptions so that we keep preferring the
                # same child.
                r.sort()
                child_fd = r[0]

                # Try to read the single byte written by the child.
                # This can fail if the child died or the pipe really
                # wasn't ready (select returns a hint only).  The fd has
                # been made non-blocking by spawn_child.  If this fails
                # we fall through to the "reap_children" logic and will
                # retry the select call.
                try:
                    ready_byte = os.read(child_fd, 1)
                    if not ready_byte:
                        raise IOError # child died?
                    assert ready_byte == "1", repr(ready_byte)
                except socket.error, exc:
                    if exc[0]  == errno.EWOULDBLOCK:
                        pass # select was wrong
                    else:
                        raise
                except (OSError, IOError):
                    pass # child died?
                else:
                    # The byte was read okay, now we need to pass the fd
                    # of the request to the child.  This can also fail
                    # if the child died.  Again, if this fails we fall
                    # through to the "reap_children" logic and will
                    # retry the select call.
                    try:
                        passfd.sendfd(child_fd, conn.fileno())
                    except IOError, exc:
                        if exc.errno == errno.EPIPE:
                            pass # broken pipe, child died?
                        else:
                            raise
                    else:
                        # fd was apparently passed okay to the child.
                        # The child could die before completing the
                        # request but that's not our problem anymore.
                        return

            # didn't find any child, check if any died
            self.reap_children()
            
            # start more children if we haven't met max_children limit
            if len(self.children) < self.max_children:
                self.spawn_child(conn)
            
            # Start blocking inside select.  We might have reached
            # max_children limit and they are all busy.
            timeout = 2


    def serve(self):
        #jd
        if self.unix_socket:
            s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            s.bind((self.unix_socket_filename))
        else:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            s.bind((self.host, self.port))
        s.listen(40)
        signal.signal(signal.SIGHUP, self.hup_signal)
        while 1:
            try:
                conn, addr = s.accept()
                self.delegate_request(conn)
                conn.close()
            except socket.error, e:
                if e[0] != errno.EINTR:
                    raise  # something weird
            if self.restart:
                self.do_restart()


def main():
    if len(sys.argv) == 2:
        unix_socket = False
        try:
            port = int(sys.argv[1])
        except ValueError:
            try:
                port = str(sys.argv[1])
            except:
                pass
            else:
                unix_socket = True
    else:
        port = SCGIServer.DEFAULT_PORT
        
    if unix_socket:    
        SCGIServer(socketfile=port,unix_socket=unix_socket).serve()
    else:
        SCGIServer(port=port,unix_socket=unix_socket).serve()
if __name__ == "__main__":
    main()
_______________________________________________
Quixote-users mailing list
[email protected]
http://mail.mems-exchange.org/mailman/listinfo/quixote-users

Reply via email to