
'''Classes for putting a program in 'daemon' mode (detach from terminal and
run in the background) as well as redirecting output to syslog or a log file

Copyright 2003-2005 Dan Pascu
'''

import sys, os, errno, syslog, signal, time, atexit
import threading


class Redirect:
    def __init__(self, which, where, syslog=0):
        if not which in ['stdin', 'stdout', 'stderr']:
            raise ValueError, "Should be one of 'stdin', 'stdout' or 'stderr'"
        if which == 'stdin':
            syslog = 0
        if where == which:
            self.where = getattr(sys, '__%s__' % which)
        else:
            self.where = where
        self.lock = threading.Lock()
        self.which = which
        self.syslog = syslog

    def write(self, s):
        self.lock.acquire()
        try:
            if self.where:
                self.where.write(s)
                self.where.flush()
            if self.syslog:
                ## We want to print multilines one at a time, but ignore empty lines
                for line in [l for l in s.splitlines() if l]:
                    try:
                        syslog.syslog(line)
                    except TypeError:
                        ## This is a binary string containing \x00 which we need to escape 
                        if type(line) is unicode:
                            escape = 'unicode_escape'
                        else:
                            escape = 'string_escape'
                        syslog.syslog(line.strip().encode(escape))
        finally:
            self.lock.release()

    def read(self, size=-1):
        r = ''
        if self.where:
            self.lock.acquire()
            try:
                r = self.where.read(size)
            finally:
                self.lock.release()
        return r

    def flush(self):
        getattr(sys, '__%s__' % self.which).flush() # ? what's this for?

    def close(self):
        if self.where:
            self.where.close()


class DaemonError(Exception):
    pass

class DaemonQuit(Exception):
    pass


class Daemon:
    def __init__(self, name='<daemon>', pidfile=None, logFile=None, useSyslog=0):
        self.name    = name
        self.pidfile = pidfile
        self.logFilename = logFile
        self.syslog  = useSyslog
        
        ## Check if not already running
        if pidfile and os.path.isfile(pidfile):
            try:
                pf = open(pidfile, 'rb')
            except IOError, why:
                raise DaemonError, "Unable to read %s: %s" % (pidfile, str(why))
            else:
                try:
                    try:
                        pid = int(pf.readline().strip())
                    except IOError, why:
                        raise DaemonError, "Unable to read %s: %s" % (pidfile, str(why))
                    except ValueError:
                        pass
                    else:
                        ## Send the process a signal of zero (0)
                        try:
                            result = os.kill(pid, 0)
                            raise DaemonError, "Daemon already running on pid %d" % pid
                        except OSError, why:
                            if why[0] in (errno.EPERM, errno.EACCES):
                                raise DaemonError, "Another process running on pid %d" % pid
                finally:
                    pf.close()

        ## Do the Unix double fork magic
        try:
            pid = os.fork()
            if pid > 0:     ## Exit first parent
                sys.exit(0)
        except OSError, e:
            raise DaemonError, "Failed to fork %s: %d: %s" % (name, e.errno, e.strerror)
        
        ## Decouple from parent environment
        os.chdir("/")
        os.setsid()
        os.umask(022) # was 0. why?
            
        ## Do the second fork
        try:
            pid = os.fork()
            if pid > 0:     ## Exit first child too
                sys.exit(0)
        except OSError, e:
            raise DaemonError, "Failed to do 2nd fork: %d: %s" % (e.errno, e.strerror)

        if pidfile:
            try:
                pf = open(pidfile, "wb")
            except IOError, why:
                raise DaemonError, "Unable to write %s: %s" % (pidfile, str(why))
            else:
                try:
                    try:
                        print >>pf, os.getpid()
                    except IOError, why:
                        raise DaemonError, "Unable to write %s: %s" % (pidfile, str(why))
                finally:
                    pf.close()

        if useSyslog:
            syslog.openlog(name, syslog.LOG_PID, syslog.LOG_DAEMON)

        if logFile:
            try:
                log = open(logFile, 'a')
            except IOError:
                raise DaemonError, "unable to open log file for writing"
        else:
            log = None

        ## Close these at low level so the parent who started us should be able
        ## to exit and won't hang waiting for us
        os.close(0)
        os.close(1)
        os.close(2)

        sys.stdin  = Redirect('stdin', None)
        sys.stdout = Redirect('stdout', log, useSyslog)
        sys.stderr = Redirect('stderr', log, useSyslog)
        self.__logFile = log

        ## Ignore Terminal I/O Signals
        if hasattr(signal, 'SIGTTOU'):
            signal.signal(signal.SIGTTOU, signal.SIG_IGN)
        if hasattr(signal, 'SIGTTIN'):
            signal.signal(signal.SIGTTIN, signal.SIG_IGN)
        if hasattr(signal, 'SIGTSTP'):
            signal.signal(signal.SIGTSTP, signal.SIG_IGN)

        if hasattr(signal, 'SIGUSR1'):
            signal.signal(signal.SIGUSR1, signal.SIG_IGN)
        if hasattr(signal, 'SIGUSR2'):
            signal.signal(signal.SIGUSR2, signal.SIG_IGN)

        ## Catch TERM, HUP, INT signals and exit if got one
        if hasattr(signal, 'SIGTERM'):
            signal.signal(signal.SIGTERM, self.quit)
        if hasattr(signal, 'SIGINT'):
            signal.signal(signal.SIGINT, self.quit)
        if hasattr(signal, 'SIGHUP'):
            signal.signal(signal.SIGHUP, self.quit)

        atexit.register(self.close)

        ## Note that we started and return
        #print >>sys.stderr, "%s started." % name

    def close(self):
        if self.pidfile:
            try:
                os.unlink( self.pidfile )
            except OSError, why:
                print >>sys.stderr, "unable to delete %s: %s" % (pidfile, str(why))
        print >>sys.stderr, "exiting."
        if self.__logFile:
            self.__logFile.close()
        if self.syslog:
            syslog.closelog()

    def quit(self, *args):
        raise DaemonQuit, "received signal %d" % args[0]


