Re: Re: scgi server start
"Mike Orr" <[email protected]> Mon, 26 Mar 2007 16:00:52 -0700
| Newsgroups | gmane.comp.web.quixote.user |
|---|---|
| Message-ID | <[email protected]> |
Here's a module I used to use on Mac OS X, which doesn't have start-stop-daemon (or at least didn't at the time). Now I have a Linux server so I don't need it, but it should still work. -- Mike Orr <[email protected]> _______________________________________________ Quixote-users mailing list [email protected] http://mail.mems-exchange.org/mailman/listinfo/quixote-users
toplevel.py
(application/x-python, 14.9 KB)
"""Daemonizer with command-line front end; with optional Quixote support.
This module provides generic functions to daemonize and change user, a
TopLevel class to manage the command-line options for these, and a
QuixoteTopLevel class for SCGI and SimpleServer applications. Many Linux
distributions come with a start-stop-daemon program that does most of this, but
Mac OS X does not, hence the need for a Python version.
At the lowest level is the daemonize() function by Chad Schroeder, borrowed
from the Python cookbook.
TopLevel gives your application the following command-line options:
-h, --help Show this help message and exit
--daemon Run as system service (daemon) (requires --pidfile)
--stop Stop a running daemon (requires --pidfile)
--pidfile=PIDFILE Record daemon PID in this file
--user=USER Run as this user (name or UID)
--group=GROUP Run as this group (name or GID)
QuixoteTopLevel adds the following options
--simple Run under the simple server
--scgi Run under the SCGI server
GENERIC USAGE ==============================================================
import toplevel
opts, args = toplevel.TopLevel().main()
#
# We may have forked or changed user. You can examine 'opts' if you
# care. Depending on the OS, any open files or database connections may
# have been closed by the fork.
#
# Put your application-specific code here.
If you need additional command-line options, subclass TopLevel and override
.get_parser() and perhaps .check_usage() . Remember to call the superclass
methods!
Functions change_user(), daemon(), stop(), daemonize(), etc. can be used
standalone.
QUIXOTE USAGE ==========================================================
import toplevel
from quixote.publish import Publisher
from my_directory import MyDirectory
class MyTopLevel(toplevel.QuixoteTopLevel):
simple_host = '127.0.0.1'
scgi_max_children = 1
# Override other class attributes as needed.
def create_publisher(self):
app = MyDirectory()
return Publisher(app)
# You could return a different publisher depending on self.opts.
MyTopLevel.main()
NOTES ON QuixoteTopLevel CLASS ATTRIBUTES =================================
.simple_host and .scgi_host should be '' to listen on all interfaces. Or
specify the domain or IP to listen on one interface. Note that some versions
of Mac OS X do not recognize "localhost", use "127.0.0.1" instead.
If you are using mod_scgi's SCGIMount directive (recommended), you can leave
.scgi_script_name at the default. If you're using SCGIServer,
.scgi_script_name **must** match the <Location> directive the SCGIServer is
contained in. Otherwise your application won't work.
QuixoteTopLevel will raise RuntimeError if the user chooses --scgi and
.scgi_max_children is greater than 1. If you know your application is
multiprocess safe, you can override .is_multiprocess_safe() and return True.
Although multiple children do not have the shared-memory problem of threads,
you can still get into trouble with files or sessions. Use file locking if you
write to any files during a request. Quixote's default session store (a
dictionary) is not safe: each child has its own dictionary, and requests go to
different children at random. If the application keeps "forgetting" your
session, this is probably why. The session2 package at
http://quixote.idyll.org/session2/ has multiprocess-safe persistent session
stores using the filesystem, Durus, MySQL, PostgreSQL, and others.
If .simple_banner is true, a message is printed on stdout telling which URL
browsers should connect to.
I've never used .simple_https so I'm not sure where you put the socket into SSL
mode. I'm also not sure the simple_banner message is correct in this case.
QuixoteTopLevel is provided as a convenience. It may not be suitable for all
Quixote applications.
CHANGELOG:
- 2005-08-31: add os._urandomfd kludge. Docstring.
- 2005-09-22: add TopLevel and QuixoteTopLevel classes, remove corresponding public
functions and the esoteric 'config' object.
KLUDGES:
- After the daemonize() call in daemon(), do "os._urandomfd = None". This is to
avoid a "bad file descriptor" exception (observed on Mac OS X).
Apparently Python or Quixote calls os.urandom() before the fork, and the file
descriptor for /dev/urandom is invalid in the child, so we have to make the
'os' module reopen the file.
"""
import errno, grp, os, pwd, signal, sys
try:
import grp, pwd
except ImportError: # Modules not available on Windows.
grp = pwd = None
from optparse import OptionParser
from quixote.publish import Publisher
#### GENERIC FRONT END FOR APPLICATIONS ####
class TopLevel(object):
usage = None
description = None
def main(self):
parser = self.get_parser()
self.opts, self.args = opts, args = parser.parse_args()
errmsg = self.check_usage()
if errmsg:
print "ERROR:", errmsg
parser.print_help()
sys.exit(1)
if hasattr(opts, "user"):
change_user(opts.user, opts.group)
if opts.stop:
stop(opts.pidfile) # Exits program, does not return.
if opts.daemon:
daemon(opts.pidfile)
return opts, args
def get_parser(self):
parser = OptionParser(usage=self.usage, description=self.description)
self.add_parser_options(parser)
return parser
def add_parser_options(self, parser):
pao = parser.add_option
pao('--daemon', action="store_true", dest="daemon",
help="Run as system service (daemon) (requires --pidfile)")
pao('--stop', action="store_true", dest="stop",
help="Stop a running daemon (requires --pidfile)")
pao('--pidfile', action="store", dest="pidfile",
help="Record daemon PID in this file")
if pwd:
pao('--user', action="store", dest="user",
help="Run as this user (name or UID)")
if grp:
pao('--group', action="store", dest="group",
help="Run as this group (name or GID)")
def check_usage(self):
opts = self.opts
if opts.daemon and opts.stop:
return "can't specify both --daemon and --stop"
if opts.daemon and not opts.pidfile:
return "--daemon requires --pidfile"
if opts.stop and not opts.pidfile:
return "--stop requires --pidfile"
if opts.stop and opts.pidfile and not os.path.exists(opts.pidfile):
return "--pidfile refers to non-existent file"
return None
#### FRONT END FOR QUIXOTE APPLICATIONS ####
class QuixoteTopLevel(TopLevel):
scgi_host = '127.0.0.1'
scgi_port = 3000
scgi_script_name = None
scgi_max_children = 1
simple_host = ''
simple_port = 8080
simple_https = False
simple_banner = True
title = "Quixote application"
def main(self):
super(QuixoteTopLevel, self).main()
if self.opts.scgi:
self.run_scgi()
else:
self.run_simple()
def run_scgi(self):
from quixote.server import scgi_server
if self.scgi_max_children > 1 and not self.is_multiprocess_safe():
m = "configured application is unsafe for multiple SCGI children"
raise RuntimeError(m)
scgi_server.run(self.create_publisher,
host=self.scgi_host,
port=self.scgi_port,
script_name=self.scgi_script_name,
max_children=self.scgi_max_children,
)
def run_simple(self):
from quixote.server import simple_server
if self.simple_banner:
title = self.title
scheme = self.simple_https and "https" or "http"
host = self.simple_host or "[ALL INTERFACES]"
port = self.simple_port
print "%s listening on %s://%s:%s/" % (title, scheme, host, port)
simple_server.run(self.create_publisher,
host=self.simple_host,
port=self.simple_port,
)
def get_parser(self):
parser = super(QuixoteTopLevel, self).get_parser()
pao = parser.add_option
pao('--simple', action="store_true", dest="simple",
help="Run under the simple server")
pao('--scgi', action="store_true", dest="scgi",
help="Run under the SCGI server")
return parser
def check_usage(self):
errmsg = super(QuixoteTopLevel, self).check_usage()
if errmsg:
return errmsg
opts = self.opts
if (not opts.stop) and count_true(opts.scgi, opts.simple) != 1:
return "must specify --simple or --scgi flag (but not both)"
return None
def create_publisher(self):
raise NotImplementedError("subclass responsibility")
def is_multiprocess_safe(self):
return False
#### GENERIC FUNCTIONS ####
def count_true(*values):
"""Return the number of true values."""
return len(filter(None, values))
#### USER-CHANGING FUNCTIONS ####
def change_user(user=None, group=None):
# JJ Behrens says we must set the group first, so we do.
if group is not None:
assert grp is not None, "Can't change group on %r platform." % sys.platform
if group.isdigit():
gid = int(group)
else:
gid = grp.getgrnam(group)[2]
os.setegid(gid)
if user is not None:
assert grp is not None, "Can't change user on %r platform." % sys.platform
if user.isdigit():
uid = int(user)
else:
uid = pwd.getpwnam(user)[2]
os.seteuid(uid)
#### DAEMON FUNCTIONS ####
def daemon(pidfile):
if os.path.exists(pidfile):
pid = read_pidfile(pidfile)
if process_exists(pid):
msg = "daemon already running (PID %s, file %s)" % (pid, pidfile)
sys.exit(msg)
daemonize()
os._urandomfd = None # @@MO: Workaround for Mac OS X (see module docstring).
write_pidfile(pidfile)
def stop(pidfile):
pid = read_pidfile(pidfile)
try:
os.kill(pid, signal.SIGTERM)
except OSError, e:
if e.errno == errno.ESRCH: # No such process.
sys.exit(1)
else:
raise
# Signal delivered, delete pidfile and exit program.
os.remove(pidfile)
sys.exit(0)
#### DAEMON UTILITY FUNCTIONS ####
def read_pidfile(pidfile):
f = open(pidfile, 'r')
pid = f.read()
f.close()
try:
pid = int(pid)
except ValueError:
msg = "corrupt PID file %s (content not numeric)" % pidfile
raise RuntimeError(message)
return pid
def write_pidfile(pidfile):
pid = os.getpid()
f = open(pidfile, 'w')
print >>f, pid
f.close()
def process_exists(pid):
try:
os.kill(pid, 0)
except OSError, e:
if e.errno == errno.ESRCH:
return False
else:
raise
return True
def daemonize():
"""Detach a process from the controlling terminal and run it in the
background as a daemon. A Python Cookbook recipe by Chad J Schroeder.
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731
Original documentation follows.
Disk And Execution MONitor (Daemon)
Default daemon behaviors (they can be modified):
1.) Ignore SIGHUP signals.
2.) Default current working directory to the "/" directory.
# 3.) Set the current file creation mode mask to 0.
4.) Close all open files (0 to [SC_OPEN_MAX or 256]).
5.) Redirect standard I/O streams to "/dev/null".
[# indicates behaviors disabled in this version.]
Failed fork() calls will return a tuple: (errno, strerror). This
behavior can be modified to meet your program's needs.
Resources:
Advanced Programming in the Unix Environment: W. Richard Stevens
Unix Network Programming (Volume 1): W. Richard Stevens
http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
"""
try:
# Fork a child process so the parent can exit. This will return control
# to the command line or shell. This is required so that the new process
# is guaranteed not to be a process group leader. We have this guarantee
# because the process GID of the parent is inherited by the child, but
# the child gets a new PID, making it impossible for its PID to equal its
# PGID.
pid = os.fork()
except OSError, e:
return((e.errno, e.strerror)) # ERROR (return a tuple)
if (pid == 0): # The first child.
# Next we call os.setsid() to become the session leader of this new
# session. The process also becomes the process group leader of the
# new process group. Since a controlling terminal is associated with a
# session, and this new session has not yet acquired a controlling
# terminal our process now has no controlling terminal. This shouldn't
# fail, since we're guaranteed that the child is not a process group
# leader.
os.setsid()
# When the first child terminates, all processes in the second child
# are sent a SIGHUP, so it's ignored.
signal.signal(signal.SIGHUP, signal.SIG_IGN)
try:
# Fork a second child to prevent zombies. Since the first child is
# a session leader without a controlling terminal, it's possible for
# it to acquire one by opening a terminal in the future. This second
# fork guarantees that the child is no longer a session leader, thus
# preventing the daemon from ever acquiring a controlling terminal.
pid = os.fork() # Fork a second child.
except OSError, e:
return((e.errno, e.strerror)) # ERROR (return a tuple)
if (pid == 0): # The second child.
# Ensure that the daemon doesn't keep any directory in use. Failure
# to do this could make a filesystem unmountable.
os.chdir("/")
# Give the child complete control over permissions.
os.umask(0)
else:
os._exit(0) # Exit parent (the first child) of the second child.
else:
os._exit(0) # Exit parent of the first child.
# Close all open files. Try the system configuration variable, SC_OPEN_MAX,
# for the maximum number of open files to close. If it doesn't exist, use
# the default value (configurable).
try:
maxfd = os.sysconf("SC_OPEN_MAX")
except (AttributeError, ValueError):
maxfd = 256 # default maximum
for fd in range(0, maxfd):
try:
os.close(fd)
except OSError: # ERROR (ignore)
pass
# Redirect the standard file descriptors to /dev/null.
os.open("/dev/null", os.O_RDONLY) # standard input (0)
os.open("/dev/null", os.O_RDWR) # standard output (1)
os.open("/dev/null", os.O_RDWR) # standard error (2)
return(0)