Skip to content

Instantly share code, notes, and snippets.

@colinhunt
Created May 29, 2017 03:43
Show Gist options
  • Save colinhunt/832027827a4cf00bc703b82b80b3e7e2 to your computer and use it in GitHub Desktop.
Save colinhunt/832027827a4cf00bc703b82b80b3e7e2 to your computer and use it in GitHub Desktop.
'''
***
Modified generic daemon class
***
Author: http://www.jejik.com/articles/2007/02/
a_simple_unix_linux_daemon_in_python/www.boxedice.com
License: http://creativecommons.org/licenses/by-sa/3.0/
Changes: 23rd Jan 2009 (David Mytton <[email protected]>)
- Replaced hard coded '/dev/null in __init__ with os.devnull
- Added OS check to conditionally remove code that doesn't
work on OS X
- Added output to console on completion
- Tidied up formatting
11th Mar 2009 (David Mytton <[email protected]>)
- Fixed problem with daemon exiting on Python 2.4
(before SystemExit was part of the Exception base)
13th Aug 2010 (David Mytton <[email protected]>
- Fixed unhandled exception if PID file is empty
24th May 2017 (Colin Hunt <[email protected]>)
- Added 'main' utility
- Stopping daemon properly on unhandled exits or exceptions
'''
# Core modules
from __future__ import print_function
import atexit
import errno
import os
import sys
import time
import signal
import traceback
class Daemon(object):
"""
A generic daemon class.
Usage: subclass the Daemon class and override the init() and run() methods
"""
def __init__(self, pidfile, stdin=os.devnull,
stdout=os.devnull, stderr=os.devnull,
home_dir='.', umask=0o22, verbose=1,
use_gevent=False, use_eventlet=False):
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
self.pidfile = pidfile
self.home_dir = home_dir
self.verbose = verbose
self.umask = umask
self.daemon_alive = True
self.use_gevent = use_gevent
self.use_eventlet = use_eventlet
def log(self, *args):
if self.verbose >= 1:
print(*args)
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
"""
if self.use_eventlet:
import eventlet.tpool
eventlet.tpool.killall()
try:
pid = os.fork()
if pid > 0:
# Exit first parent
sys.exit(0)
except OSError as e:
sys.stderr.write(
"fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
sys.exit(1)
# Decouple from parent environment
os.chdir(self.home_dir)
os.setsid()
os.umask(self.umask)
# Do second fork
try:
pid = os.fork()
if pid > 0:
# Exit from second parent
sys.exit(0)
except OSError as e:
sys.stderr.write(
"fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
sys.exit(1)
if sys.platform != 'darwin': # This block breaks on OS X
# Redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = open(self.stdin, 'r')
so = open(self.stdout, 'a+')
if self.stderr:
try:
se = open(self.stderr, 'a+', 0)
except ValueError:
# Python 3 can't have unbuffered text I/O
se = open(self.stderr, 'a+', 1)
else:
se = so
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
def sigtermhandler(signum, frame):
self.daemon_alive = False
sys.exit()
if self.use_gevent:
import gevent
gevent.reinit()
gevent.signal(signal.SIGTERM, sigtermhandler, signal.SIGTERM, None)
gevent.signal(signal.SIGINT, sigtermhandler, signal.SIGINT, None)
else:
signal.signal(signal.SIGTERM, sigtermhandler)
signal.signal(signal.SIGINT, sigtermhandler)
self.log("Started")
# Write pidfile
atexit.register(
self.delpid) # Make sure pid file is removed if we quit
pid = str(os.getpid())
open(self.pidfile, 'w+').write("%s\n" % pid)
def delpid(self):
try:
# the process may fork itself again
pid = int(open(self.pidfile, 'r').read().strip())
if pid == os.getpid():
os.remove(self.pidfile)
except OSError as e:
if e.errno == errno.ENOENT:
pass
else:
raise
def start(self, *args, **kwargs):
"""
Start the daemon
"""
self.log("Starting...")
# Check for a pidfile to see if the daemon already runs
try:
pf = open(self.pidfile, 'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None
except SystemExit:
pid = None
if pid:
message = "pidfile %s already exists. Is it already running?\n"
sys.stderr.write(message % self.pidfile)
sys.exit(1)
try:
self.init()
except:
self.log("Failed to start.")
raise
# Start the daemon
self.daemonize()
self._run(*args, **kwargs)
def stop(self):
"""
Stop the daemon
"""
if self.verbose >= 1:
self.log("Stopping...")
# Get the pid from the pidfile
pid = self.get_pid()
if not pid:
message = "pidfile %s does not exist. Not running?\n"
sys.stderr.write(message % self.pidfile)
# Just to be sure. A ValueError might occur if the PID file is
# empty but does actually exist
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
return # Not an error in a restart
# Try killing the daemon process
try:
i = 0
while 1:
os.kill(pid, signal.SIGTERM)
time.sleep(0.1)
i = i + 1
if i % 20 == 0:
os.kill(pid, signal.SIGHUP)
except OSError as err:
if err.errno == errno.ESRCH:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
print(str(err))
sys.exit(1)
self.log("Stopped")
def restart(self):
"""
Restart the daemon
"""
self.stop()
self.start()
def get_pid(self):
try:
pf = open(self.pidfile, 'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None
except SystemExit:
pid = None
return pid
def is_running(self):
pid = self.get_pid()
if pid is None:
self.log('Process is stopped')
return False
elif os.path.exists('/proc/%d' % pid):
self.log('Process (pid %d) is running...' % pid)
return True
else:
self.log('Process (pid %d) is killed' % pid)
return False
def _run(self):
try:
self.run()
except SystemExit as e:
self.log('SystemExit received...')
except:
self.log("Unhandled exception occurred ('%s')" % sys.exc_info()[1])
self.log("---\n'%s'\n---" % traceback.format_exc())
finally:
self.stop()
def init(self):
"""
You may override this method when you subclass Daemon.
It will be called before the process is
daemonized by start() or restart().
"""
pass
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().
"""
raise NotImplementedError
def usage():
print("usage: %s start|stop|restart|status" % sys.argv[0])
sys.exit(2)
def main(daemon):
if len(sys.argv) >= 2:
cmd = sys.argv[1]
del sys.argv[1] # so it doesn't interfere with argparse
if 'start' == cmd:
daemon.start()
elif 'stop' == cmd:
daemon.stop()
elif 'restart' == cmd:
daemon.restart()
elif 'status' == cmd:
daemon.is_running()
else:
print("Unknown command '%s'" % cmd)
usage()
sys.exit(0)
else:
usage()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment