blob: ec08f929b4a2622252ef1878c9ae7ccebe2b3670 [file] [log] [blame]
# ---------------------------------------------------------------------------
#
# 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 stat
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) as e:
# duplicate the exit code
sys.exit(e.code)
except (ChildTerminatedAbnormally, ChildForkFailed,
DaemonTerminatedAbnormally, DaemonForkFailed) as 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 as 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,
stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
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