# --------------------------------------------------------------------------- # # Copyright (c) 2005, Greg Stein # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # --------------------------------------------------------------------------- # # This software lives at: # http://gstein.googlecode.com/svn/trunk/python/daemonize.py # import os import signal import sys import time import multiprocessing # requires Python 2.6 # possible return values from Daemon.daemonize() DAEMON_RUNNING = 'The daemon is running' DAEMON_NOT_RUNNING = 'The daemon is not running' DAEMON_COMPLETE = 'The daemon has completed its operations' DAEMON_STARTED = 'The daemon has been started' class Daemon(object): def __init__(self, logfile, pidfile): self.logfile = logfile self.pidfile = pidfile def foreground(self): "Run in the foreground." ### we should probably create a pidfile. other systems may try to detect ### the pidfile to see if this "daemon" is running. self.setup() self.run() ### remove the pidfile def daemonize_exit(self): try: result = self.daemonize() except (ChildFailed, DaemonFailed), e: # duplicate the exit code sys.exit(e.code) except (ChildTerminatedAbnormally, ChildForkFailed, DaemonTerminatedAbnormally, DaemonForkFailed), e: sys.stderr.write('ERROR: %s\n' % e) sys.exit(1) except ChildResumedIncorrectly: sys.stderr.write('ERROR: continued after receiving unknown signal.\n') sys.exit(1) if result == DAEMON_STARTED or result == DAEMON_COMPLETE: sys.exit(0) elif result == DAEMON_NOT_RUNNING: sys.stderr.write('ERROR: the daemon exited with a success code ' 'without signalling its startup.\n') sys.exit(1) # in original process. daemon is up and running. we're done. def daemonize(self): ### review error situations. map to backwards compat. ?? ### be mindful of daemonize_exit(). ### we should try and raise ChildFailed / ChildTerminatedAbnormally. ### ref: older revisions. OR: remove exceptions. child_is_ready = multiprocessing.Event() child_completed = multiprocessing.Event() p = multiprocessing.Process(target=self._first_child, args=(child_is_ready, child_completed)) p.start() # Wait for the child to finish setting things up (in case we need # to communicate with it). It will only exit when ready. ### use a timeout here! (parameterized, of course) p.join() ### need to propagate errors, to adjust the return codes if child_completed.is_set(): ### what was the exit status? return DAEMON_COMPLETE if child_is_ready.is_set(): return DAEMON_RUNNING ### how did we get here?! the immediate child should not exit without ### signalling ready/complete. some kind of error. return DAEMON_STARTED def _first_child(self, child_is_ready, child_completed): # we're in the child. ### NOTE: the original design was a bit bunk. Exceptions raised from ### this point are within the child processes. We need to signal the ### errors to the parent in other ways. # decouple from the parent process os.chdir('/') os.umask(0) os.setsid() # remember this pid so the second child can signal it. thispid = os.getpid() # if the daemon process exits before signalling readiness, then we # need to see the problem. trap SIGCHLD with a SignalCatcher. daemon_exit = SignalCatcher(signal.SIGCHLD) # perform the second fork try: pid = os.fork() except OSError, e: ### this won't make it to the parent process raise DaemonForkFailed(e.errno, e.strerror) if pid > 0: # in the parent. # Wait for the child to be ready for operation. while True: # The readiness event will invariably be signalled early/first. # If it *doesn't* get signalled because the child has prematurely # exited, then we will pause 10ms before noticing the exit. The # pause is acceptable since that is aberrant/unexpected behavior. ### is there a way to break this wait() on a signal such as SIGCHLD? ### parameterize this wait, in case the app knows children may ### fail quickly? if child_is_ready.wait(timeout=0.010): # The child signalled readiness. Yay! break if daemon_exit.signalled: # Whoops. The child exited without signalling :-( break # Python 2.6 compat: .wait() may exit when set, but return None if child_is_ready.is_set(): break # A simple timeout. The child is taking a while to prepare. Go # back and wait for readiness. if daemon_exit.signalled: # Tell the parent that the child has exited. ### we need to communicate the exit status, if possible. child_completed.set() # reap the daemon process, getting its exit code. bubble it up. cpid, status = os.waitpid(pid, 0) assert pid == cpid if os.WIFEXITED(status): code = os.WEXITSTATUS(status) if code: ### this won't make it to the parent process raise DaemonFailed(code) ### this return value is ignored return DAEMON_NOT_RUNNING # the daemon did not exit cleanly. ### this won't make it to the parent process raise DaemonTerminatedAbnormally(status) # child_is_ready got asserted. the daemon is up and running, so # save the pid and return success. if self.pidfile: # Be wary of symlink attacks try: os.remove(self.pidfile) except OSError: pass fd = os.open(self.pidfile, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0444) os.write(fd, '%d\n' % pid) os.close(fd) ### this return value is ignored return DAEMON_STARTED ### old code. what to do with this? throw ChildResumedIncorrectly ### or just toss this and the exception. # some other signal popped us out of the pause. the daemon might not # be running. ### this won't make it to the parent process raise ChildResumedIncorrectly() # we're a daemon now. get rid of the final remnants of the parent: # restore the signal handlers and switch std* to the proper files. signal.signal(signal.SIGUSR1, signal.SIG_DFL) signal.signal(signal.SIGCHLD, signal.SIG_DFL) sys.stdout.flush() sys.stderr.flush() si = open('/dev/null', 'r') so = open(self.logfile, 'a+') se = open(self.logfile, 'a+', 0) # unbuffered os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) # note: we could not inline the open() calls. after the fileno() completed, # the file would be closed, making the fileno invalid. gotta hold them # open until now: si.close() so.close() se.close() ### TEST: don't release the parent immediately. the whole parent stack ### should pause along with this sleep. #time.sleep(10) # everything is set up. call the initialization function. self.setup() ### TEST: exit before signalling. #sys.exit(0) #sys.exit(1) # the child is now ready for parent/anyone to communicate with it. child_is_ready.set() # start the daemon now. self.run() # The daemon is shutting down, so toss the pidfile. if self.pidfile: try: os.remove(self.pidfile) except OSError: pass ### this return value is ignored return DAEMON_COMPLETE def setup(self): raise NotImplementedError def run(self): raise NotImplementedError class _Detacher(Daemon): def __init__(self, target, logfile='/dev/null', pidfile=None, args=(), kwargs={}): Daemon.__init__(self, logfile, pidfile) self.target = target self.args = args self.kwargs = kwargs def setup(self): pass def run(self): self.target(*self.args, **self.kwargs) def run_detached(target, *args, **kwargs): """Simple function to run TARGET as a detached daemon. The additional arguments/keywords will be passed along. This function does not return -- sys.exit() will be called as appropriate. (capture SystemExit if logging/reporting is necessary) ### if needed, a variant of this func could be written to not exit """ d = _Detacher(target, args=args, kwargs=kwargs) d.daemonize_exit() class SignalCatcher(object): def __init__(self, signum): self.signalled = False signal.signal(signum, self.sig_handler) def sig_handler(self, signum, frame): self.signalled = True class ChildTerminatedAbnormally(Exception): "The child process terminated abnormally." def __init__(self, status): Exception.__init__(self, status) self.status = status def __str__(self): return 'child terminated abnormally (0x%04x)' % self.status class ChildFailed(Exception): "The child process exited with a failure code." def __init__(self, code): Exception.__init__(self, code) self.code = code def __str__(self): return 'child failed with exit code %d' % self.code class ChildForkFailed(Exception): "The child process could not be forked." def __init__(self, errno, strerror): Exception.__init__(self, errno, strerror) self.errno = errno self.strerror = strerror def __str__(self): return 'child fork failed with error %d (%s)' % self.args class ChildResumedIncorrectly(Exception): "The child resumed its operation incorrectly." class DaemonTerminatedAbnormally(Exception): "The daemon process terminated abnormally." def __init__(self, status): Exception.__init__(self, status) self.status = status def __str__(self): return 'daemon terminated abnormally (0x%04x)' % self.status class DaemonFailed(Exception): "The daemon process exited with a failure code." def __init__(self, code): Exception.__init__(self, code) self.code = code def __str__(self): return 'daemon failed with exit code %d' % self.code class DaemonForkFailed(Exception): "The daemon process could not be forked." def __init__(self, errno, strerror): Exception.__init__(self, errno, strerror) self.errno = errno self.strerror = strerror def __str__(self): return 'daemon fork failed with error %d (%s)' % self.args