#!/usr/bin/python
#
# Copyright 2001 by Object Craft P/L, Melbourne, Australia.
#
# LICENCE - see LICENCE file distributed with this software for details.
#
#
# Run the session server as a daemon
#
# This won't work under Windows as the daemonisation process is specific
# to unix. Hints on how to make it work gratefully received.
#
# $Id: al-session-daemon 8003 2006-11-20 01:05:51Z andrewm $
#

# standard libraries
import getopt
import os
import sys
import signal
from errno import *
import time
import traceback

# our modules
from albatross import simpleserver
from albatross import pidfile


ourname = "al-session-daemon"

EXIT_OK, EXIT_ERROR, EXIT_USAGE = range(3)


class Options:

    def __init__(self):
        # Defaults
        self.debug = 0
        self.logfile_name = "/var/log/al-session-daemon.log"
        self.ourname = ourname
        self.pidfile_name = "/var/run/%s.pid" % ourname
        self.port = 34343


class Pipe:

    """
    A simple wrapper around a unix pipe, intended to be used to
    communicate between a parent and a child process.

    One process holds the read end, the other process holds the write end.
    The first use decides which direction the pipe runs in, with the
    unused file descriptors being closed (so the other end can detect
    EOF).

    On destruction of the object, any open file descriptors are closed.
    """

    def __init__(self):
        self.read_fd, self.write_fd = os.pipe()

    def write(self, buf):
        if self.read_fd >= 0:
            try:
                os.close(self.read_fd)
            except:
                pass
            self.read_fd = -1
        return os.write(self.write_fd, buf)

    def read(self, count):
        if self.write_fd >= 0:
            try:
                os.close(self.write_fd)
            except:
                pass
            self.write_fd = -1
        return os.read(self.read_fd, count)

    def __del__(self):
        if self.read_fd >= 0:
            try:
                os.close(self.read_fd)
            except:
                pass
        if self.write_fd >= 0:
            try:
                os.close(self.write_fd)
            except:
                pass


class Daemon:

    def __init__(self, port, logfile_name, pidfile_name, debug = 0):
        self.port = port
        if logfile_name:
            self.logfile = open(logfile_name, "a", 1)
        else:
            self.logfile = None
        self.pidfile = pidfile.PidFile(pidfile_name)
        if debug:
            self.debugfile = self.logfile
        else:
            self.debugfile = None

    def start(self):

        # We want the parent to be able to return useful exit codes to
        # whoever called it, so we use a pipe to pass status from the
        # child back to the parent.
        pipe = Pipe()
        pid = os.fork()
        if pid:
            # Parent
            status = pipe.read(3)
            if status == str(EXIT_OK):
                print "session-server pid %d" % pid
                return EXIT_OK
            else:
                return EXIT_ERROR

        # Child
        del pid

        try:
            self.pidfile.start()
        except pidfile.PidFileError, msg:
            sys.stderr.write("pidfile: %s\n" % msg)
            sys.exit(EXIT_ERROR)

        try:
            self.server = simpleserver.Server(self.port, self.debugfile)

            # Divorce us from the parents controlling TTY (so we don't
            # receive their signals, etc). It's somewhat rude to change
            # these file descriptors from under the python libraries, but
            # it should be safe enough.
            devnull = os.open("/dev/null", os.O_RDWR)
            if not self.logfile:
                opfile = devnull
            else:
                opfile = self.logfile.fileno()
            os.dup2(devnull, 0)
            os.dup2(opfile, 1)
            os.dup2(opfile, 2)
            os.close(devnull)
            del opfile, devnull
            try:
                os.setsid()
            except:
                pass

            signal.signal(signal.SIGTERM, self.sig_stop)

            if self.logfile:
                self.logfile.write("\nServer starting at %s\n" % self.asctime())
                self.logfile.flush()

            pipe.write(str(EXIT_OK))            # Signal parent that all is ok
            del pipe

            self.server.select_loop()

        except:
            traceback.print_exc()

        if self.logfile:
            self.logfile.write("Server stopped at %s\n" % self.asctime())

        try:
            self.pidfile.stop()
        except pidfile.PidFileError, msg:
            sys.stderr.write("pidfile: %s\n" % msg)

        sys.exit(EXIT_OK)

    def sig_stop(self, sig, stack):
        self.server.stop()

    def stop(self):
        tries = 5
        try:
            pid = self.pidfile.getpid()
        except pidfile.PidFileError, msg:
            sys.stderr.write("pidfile: %s\n" % msg)
            return EXIT_ERROR
        if pid:
            print "stopping %d" % pid,
            while self.pidfile.is_running() and tries:
                try:
                    self.pidfile.kill()
                except pidfile.PidFileError, msg:
                    sys.stderr.write("pidfile: %s\n" % msg)
                    return EXIT_ERROR
                print ".",
                sys.stdout.flush()
                tries = tries - 1
                time.sleep(1)
            if tries:
                print "done"
                return EXIT_OK
            else:
                print "failed to kill %s" % pid
        else:
            print "daemon not running"
        return EXIT_ERROR

    def status(self):
        pid =  self.pidfile.getpid()
        if pid:
            print "Daemon pid is %d" % pid
            return EXIT_OK
        else:
            print "Daemon is not running"
            return EXIT_ERROR

    def command(self, cmd_name):
        cmds = {
            "start": self.start,
            "stop": self.stop,
            "status": self.status,
        }
        try:
            cmd = cmds[cmd_name]
        except KeyError:
            return EXIT_USAGE

        return cmd()

    def asctime(self):
        # Python 1.5.2's time.asctime() wasn't as enlightened as 2.0, so we 
        # roll our own
        return time.asctime(time.localtime(time.time()))


def usage():
    sys.stderr.write("Usage: %s [OPTIONS]... <command>\n" % ourname)
    sys.stderr.write("Try '%s --help' for more information\n" % ourname)
    sys.exit(EXIT_USAGE)


def help():
    print """\
Usage: %(ourname)s [options]... <command>

Where [options] are:
  -D, --debug                   Write debugging to log
  -h, --help                    Display this help and exit
  -k <pid-file>,                Record server pid in <pid-file>, default is 
    --pidfile=<pid-file>        %(pidfile_name)s
  -p <port>, --port=<port>      Listen on <port>, default is %(port)s
  -l <log-file>,                Write log to <log-file>, default is
    --log=<log-file>            %(logfile_name)s

<command> is one of:
  start    start a new daemon
  stop     kill the current daemon
  status   request the daemon's status
""" % Options().__dict__
    sys.exit(EXIT_OK)


def main(argv):
    options = Options()

    try:
        opts, args = getopt.getopt(argv, 
                                   "Dhk:p:l:", 
                                   ["debug", "help", "pidfile=", 
                                    "port=", "log="])
    except getopt.GetoptError:
        usage()
    for opt, arg in opts:
        if opt in ("-D", "--debug"):
            options.debug = not options.debug
        elif opt in ("-h", "--help"):
            help()
        elif opt in ("-k", "--pidfile"):
            options.pidfile_name = arg
        elif opt in ("-p", "--port"):
            try:
                options.port = int(arg)
            except ValueError:
                usage()
        elif opt in ("-l", "--log"):
            options.logfile_name = arg

    if not args:
        usage()

    daemon = Daemon(port = options.port,
                    logfile_name = options.logfile_name,
                    pidfile_name = options.pidfile_name,
                    debug = options.debug)

    exit_code = daemon.command(args[0])
    if exit_code == EXIT_USAGE:
        usage()

    sys.exit(exit_code)


if __name__ == '__main__':
    main(sys.argv[1:])
