diff --git a/CouchPotato.py b/CouchPotato.py index 68922c62..e01d284b 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -1,7 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -'''Wrapper for the command line interface.''' - from os.path import dirname import os import sys @@ -44,8 +41,7 @@ def start(): return 0 from couchpotato.runner import runCouchPotato -if __name__ == '__main__': - +def main(): if os.environ.get('cp_main', 'false') == 'true': try: runCouchPotato(options, base_path, sys.argv[1:]) @@ -61,3 +57,16 @@ if __name__ == '__main__': fireEvent('app.crappy_shutdown', single = True) time.sleep(0.1) sys.exit() + +from daemon import Daemon +class CouchDaemon(Daemon): + def run(self): + os.environ['cp_main'] = 'true' + main() + +if __name__ == '__main__': + if options.daemon and options.pid_file: + daemon = CouchDaemon(options.pid_file) + daemon.start() + else: + main() diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 372b4ba1..fa41154c 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -27,6 +27,10 @@ def getOptions(base_path, args): dest = 'quiet', help = 'No console logging') parser.add_argument('--nogit', action = 'store_true', dest = 'nogit', help = 'No git available') + parser.add_argument('--daemon', action = 'store_true', + dest = 'daemon', help = 'Daemonize the app') + parser.add_argument('--pid_file', default = os.path.join(data_dir, 'couchpotato.pid'), + dest = 'pid_file', help = 'Path to pidfile needed for daemon') options = parser.parse_args(args) diff --git a/libs/daemon.py b/libs/daemon.py index 0842d33d..ed9f2d7b 100644 --- a/libs/daemon.py +++ b/libs/daemon.py @@ -1,181 +1,132 @@ -## {{{ http://code.activestate.com/recipes/278731/ (r6) -"""Disk And Execution MONitor (Daemon) +#!/usr/bin/env python -Configurable daemon behaviors: +from signal import SIGTERM +import sys +import os +import time +import atexit - 1.) The current working directory set to the "/" directory. - 2.) The current file creation mode mask set to 0. - 3.) Close all open files (1024). - 4.) Redirect standard I/O streams to "/dev/null". - -A failed call to fork() now raises an exception. - -References: - 1) Advanced Programming in the Unix Environment: W. Richard Stevens - 2) Unix Programming Frequently Asked Questions: - http://www.erlenstar.demon.co.uk/unix/faq_toc.html -""" - -__author__ = "Chad J. Schroeder" -__copyright__ = "Copyright (C) 2005 Chad J. Schroeder" - -__revision__ = "$Id$" -__version__ = "0.2" - -# Standard Python modules. -import os # Miscellaneous OS interfaces. -#import sys # System-specific parameters and functions. - -# Default daemon parameters. -# File mode creation mask of the daemon. -UMASK = 0 - -# Default working directory for the daemon. -WORKDIR = "/" - -# Default maximum for the number of available file descriptors. -MAXFD = 1024 - -# The standard I/O file descriptors are redirected to /dev/null by default. -if (hasattr(os, "devnull")): - REDIRECT_TO = os.devnull -else: - REDIRECT_TO = "/dev/null" - -def createDaemon(): - """Detach a process from the controlling terminal and run it in the - background as a daemon. +class Daemon(): """ + A generic daemon class. - try: - # Fork a child process so the parent can exit. This returns control to - # the command-line or shell. It also guarantees that the child will not - # be a process group leader, since the child receives a new process ID - # and inherits the parent's process group ID. This step is required - # to insure that the next call to os.setsid is successful. - pid = os.fork() - except OSError, e: - raise Exception, "%s [%d]" % (e.strerror, e.errno) - - if (pid == 0): # The first child. - # To become the session leader of this new session and the process group - # leader of the new process group, we call os.setsid(). The process is - # also guaranteed not to have a controlling terminal. - os.setsid() - - # Is ignoring SIGHUP necessary? - # - # It's often suggested that the SIGHUP signal should be ignored before - # the second fork to avoid premature termination of the process. The - # reason is that when the first child terminates, all processes, e.g. - # the second child, in the orphaned group will be sent a SIGHUP. - # - # "However, as part of the session management system, there are exactly - # two cases where SIGHUP is sent on the death of a process: - # - # 1) When the process that dies is the session leader of a session that - # is attached to a terminal device, SIGHUP is sent to all processes - # in the foreground process group of that terminal device. - # 2) When the death of a process causes a process group to become - # orphaned, and one or more processes in the orphaned group are - # stopped, then SIGHUP and SIGCONT are sent to all members of the - # orphaned group." [2] - # - # The first case can be ignored since the child is guaranteed not to have - # a controlling terminal. The second case isn't so easy to dismiss. - # The process group is orphaned when the first child terminates and - # POSIX.1 requires that every STOPPED process in an orphaned process - # group be sent a SIGHUP signal followed by a SIGCONT signal. Since the - # second child is not STOPPED though, we can safely forego ignoring the - # SIGHUP signal. In any case, there are no ill-effects if it is ignored. - # - # import signal # Set handlers for asynchronous events. - # signal.signal(signal.SIGHUP, signal.SIG_IGN) + Usage: subclass the Daemon class and override the run() method + """ + def __init__(self, pidfile, stdin = '/dev/null', stdout = '/dev/null', stderr = '/dev/null'): + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.pidfile = pidfile + def daemonize(self): + """ + do the UNIX double-fork magic, see Stevens' "Advanced + Programming in the UNIX Environment" for details (ISBN 0201563177) + http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + """ try: - # Fork a second child and exit immediately to prevent zombies. This - # causes the second child process to be orphaned, making the init - # process responsible for its cleanup. And, 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 (System V- - # based systems). This second fork guarantees that the child is no - # longer a session leader, preventing the daemon from ever acquiring - # a controlling terminal. - pid = os.fork() # Fork a second child. + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) except OSError, e: - raise Exception, "%s [%d]" % (e.strerror, e.errno) + sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) - if (pid == 0): # The second child. - # Since the current working directory may be a mounted filesystem, we - # avoid the issue of not being able to unmount the filesystem at - # shutdown time by changing it to the root directory. - os.chdir(WORKDIR) - # We probably don't want the file mode creation mask inherited from - # the parent, so we give the child complete control over permissions. - os.umask(UMASK) - else: - # exit() or _exit()? See below. - os._exit(0) # Exit parent (the first child) of the second child. - else: - # exit() or _exit()? - # _exit is like exit(), but it doesn't call any functions registered - # with atexit (and on_exit) or any registered signal handlers. It also - # closes any open file descriptors. Using exit() may cause all stdio - # streams to be flushed twice and any temporary files may be unexpectedly - # removed. It's therefore recommended that child branches of a fork() - # and the parent branch(es) of a daemon use _exit(). - os._exit(0) # Exit parent of the first child. + # decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) - # Close all open file descriptors. This prevents the child from keeping - # open any file descriptors inherited from the parent. There is a variety - # of methods to accomplish this task. Three are listed below. - # - # Try the system configuration variable, SC_OPEN_MAX, to obtain the maximum - # number of open file descriptors to close. If it doesn't exists, use - # the default value (configurable). - # - # try: - # maxfd = os.sysconf("SC_OPEN_MAX") - # except (AttributeError, ValueError): - # maxfd = MAXFD - # - # OR - # - # if (os.sysconf_names.has_key("SC_OPEN_MAX")): - # maxfd = os.sysconf("SC_OPEN_MAX") - # else: - # maxfd = MAXFD - # - # OR - # - # Use the getrlimit method to retrieve the maximum file descriptor number - # that can be opened by this process. If there is not limit on the - # resource, use the default value. - # - import resource # Resource usage information. - maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] - if (maxfd == resource.RLIM_INFINITY): - maxfd = MAXFD - - # Iterate through and close all file descriptors. - for fd in range(0, maxfd): + # do second fork try: - os.close(fd) - except OSError: # ERROR, fd wasn't open to begin with (ignored) - pass + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError, e: + sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) - # Redirect the standard I/O file descriptors to the specified file. Since - # the daemon has no controlling terminal, most daemons redirect stdin, - # stdout, and stderr to /dev/null. This is done to prevent side-effects - # from reads and writes to the standard I/O file descriptors. + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = file(self.stdin, 'r') + so = file(self.stdout, 'a+') + se = file(self.stderr, 'a+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) - # This call to open is guaranteed to return the lowest file descriptor, - # which will be 0 (stdin), since it was closed above. - os.open(REDIRECT_TO, os.O_RDWR) # standard input (0) + # write pidfile + atexit.register(self.delpid) + pid = str(os.getpid()) + file(self.pidfile, 'w+').write("%s\n" % pid) - # Duplicate standard input to standard output and standard error. - os.dup2(0, 1) # standard output (1) - os.dup2(0, 2) # standard error (2) + def delpid(self): + os.remove(self.pidfile) - return(0) + def start(self): + """ + Start the daemon + """ + # Check for a pidfile to see if the daemon already runs + try: + pf = file(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + if pid: + message = "pidfile %s already exist. Daemon already running?\n" + sys.stderr.write(message % self.pidfile) + sys.exit(1) + + # Start the daemon + self.daemonize() + self.run() + + def stop(self): + """ + Stop the daemon + """ + # Get the pid from the pidfile + try: + pf = file(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if not pid: + message = "pidfile %s does not exist. Daemon not running?\n" + sys.stderr.write(message % self.pidfile) + return # not an error in a restart + + # Try killing the daemon process + try: + while 1: + os.kill(pid, SIGTERM) + time.sleep(0.1) + except OSError, err: + err = str(err) + if err.find("No such process") > 0: + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + print str(err) + sys.exit(1) + + def restart(self): + """ + Restart the daemon + """ + self.stop() + self.start() + + def run(self): + """ + You should override this method when you subclass Daemon. It will be called after the process has been + daemonized by start() or restart(). + """