"""
Python Sock tools: base_daemon.py - implementation of the base daemon class
Copyright (C) 2016 GarethNelson
This file is part of python-sock-tools
python-sock-tools is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
python-sock-tools is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with python-sock-tools. If not, see <http://www.gnu.org/licenses/>.
This module provides the base implementation for a unix daemon.
Daemons in socktools are implemented as a collection of protocols with appropriate infrastructure for handling logging etc.
Parts are inspired by or copied from python-daemon as found at https://github.com/martinrusev/python-daemon.
See docs/python-daemon-license.txt for the license for python-daemon.
Note:
This will almost certainly NOT work on windows - but if you're using windows to host a server you're too incompetent to bother to read this documentation anyway.
"""
import eventlet
eventlet.monkey_patch()
import logging
import logging.handlers
import sys
import atexit
import os
import errno
import signal
[docs]class BaseDaemon(object):
""" The base daemon class from which others inherit
This class can be used directly in theory, but it makes more sense to inherit from it or use one of the other daemon classes.
Keyword args:
pidfile (str): Path to the PID file the daemon will use
stdin (str): Path to the stdin device to use
stdout (str): Path to the stdout device
stderr (str): Path to the stderr device
"""
def __init__(self,pidfile='./daemon.pid',stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
self.pidfile = os.path.abspath(pidfile)
self.stdin = os.path.abspath(stdin)
self.stdout = os.path.abspath(stdout)
self.stderr = os.path.abspath(stderr)
self.active = False
self.logger = self.get_logger()
[docs] def get_logger(self):
""" Sets up the default logger
This method is used at startup to create a simple logger. The default implementation writes to syslog.
Returns:
logging.Logger: the logger
"""
self.logger = logging.getLogger(sys.argv[0])
if not self.logger.handlers:
handler = logging.handlers.SysLogHandler(address='/dev/log')
self.logger.addHandler(handler)
self.logger.setLevel(logging.DEBUG)
return self.logger
[docs] def handle_rc(self,argv=sys.argv):
""" Implements rc.d style style commands
To use a daemon in production you probably want a script to put into the init system, that's what this method is for. Simply invoke it and it'll handle start/stop/reload/restart/status.
If the arguments provided are not valid, this method will dump usage information to stdout and then exit.
Keyword args:
argv (list): List of command line arguments, generally this should just be sys.argv
"""
if len(argv)==2:
cmd = argv[1]
if cmd == 'start':
self.start()
elif cmd == 'stop':
self.stop()
elif cmd == 'reload':
self.do_reload()
elif cmd == 'restart':
self.stop()
self.start()
elif cmd == 'status':
if self.is_running():
print 'Daemon is running with PID %s' % self.get_pid()
else:
print 'Daemon is not running'
else:
print 'Unknown command!'
sys.exit(2)
else:
print 'Usage: %s start|stop|reload|restart|status' % argv[0]
sys.exit(2)
[docs] def is_running(self):
""" Check if the daemon is already running, either in this or in another process
This method will check first if the current process is running the daemon and if not will check the pidfile and check if the PID is still an active process.
Returns:
bool: True if the daemon is already running, otherwise False
"""
if self.active: return True
if not (self.get_pid() is None): return True
return False
[docs] def get_pid(self):
""" Get the pid of the active daemon process
If the daemon is running, this will give you the PID, otherwise it'll return None
Returns:
int: the PID of the active daemon process or None
"""
if self.active: return os.getpid()
pid = None
try:
pidfile_fd = open(self.pidfile,'r')
pid = int(pidfile_fd.readline().strip('\n'))
pidfile_fd.close()
except Exception,e:
pid = None
if pid is None: return None
try:
os.kill(pid,0)
except OSError,e:
if e.errno == errno.ESRCH:
pid = None
elif e.errno == errno.EPERM:
pid = None
return pid
[docs] def fork_me(self):
""" Used internally - fork me baby, fork me
Used internally to fork when detaching from parent process, fork me baby, fork me
"""
try:
pid = os.fork()
if pid>0: sys.exit(0)
except OSError,e:
sys.stderr.write('Fork failed with error number %s: %s\n' % (e.errno, e.strerror))
sys.exit(1)
[docs] def pre_stdout(self,pid=0):
""" Called just before we redirect the file descriptors
This method is called just before we redirect the file descriptors and make normal output no longer work - use it for announcing startup messages etc.
In the default implementation this outputs a message announcing the PID of the daemon.
Warning:
DO NOT use print to output here, use sys.stderr.write() and then sys.stderr.flush() otherwise bad things will happen.
Keyword args:
pid (int): The PID of the daemon process
"""
sys.stderr.write('Starting daemon with PID %s\n' % pid)
sys.stderr.flush()
[docs] def atexit_handler(self):
""" Handle cleanup before closing
This method is called if the daemon process closes and cleans up things like the PID file. If subclassing, you must make sure to call the parent method here.
Note:
This method should ONLY do cleanup, after it returns the process dies
"""
try:
os.remove(self.pidfile)
except:
pass
[docs] def run(self):
""" Main loop of the daemon process
This method is where the actual server (or whatever) code goes. The default implementation just runs eventlet.greenthread.sleep(3600) in an infinite loop.
The default implementation also spits out a log entry once per hour.
"""
while self.active:
self.get_logger().info('Default run() is spitting this message out')
eventlet.greenthread.sleep(3600)
[docs] def handle_hup(self,signum,frame):
""" Signal handler for SIGHUP
This method is the default signal handler for SIGHUP. The standard action to take upon a SIGHUP is reloading configuration, so the default implementation calls self.do_reload().
Args:
signum (int): Always signal.SIGHUP
frame: A stack frame, this will be returned to once the handler has finished running
"""
self.do_reload()
[docs] def do_reload(self):
""" Reload configuration
This method should be overridden when inheriting from the class. Your implementation should reload any configuration from disk.
In the default implementation this does nothing.
"""
pass
[docs] def daemonize(self):
""" Daemonize the process
This is where the magic happens, this method is called by start() to daemonize the process and start running.
You should not call this directly, instead use start().
Warning:
This method does not check if the daemon is already running or not, if it is already running then bad things will happen
"""
self.get_logger().info('Daemonize started')
self.fork_me() # fork me baby
os.chdir('/') # seperate ourselves from the parent
os.setsid()
os.umask(0)
self.fork_me() # fork me harder baby, after this we should be well and truly forked
pid = os.getpid()
self.pre_stdout(pid=pid) # display any startup messages
# write to the PID file
pidfile_fd = file(self.pidfile,'w+')
pidfile_fd.write('%s\n' % str(pid))
pidfile_fd.flush()
pidfile_fd.close()
# redirect file descriptors
_stdin = file(self.stdin, 'r')
_stdout = file(self.stdout, 'a+', 0)
_stderr = file(self.stderr, 'a+', 0)
os.dup2(_stdin.fileno(), sys.stdin.fileno())
os.dup2(_stdout.fileno(), sys.stdout.fileno())
os.dup2(_stderr.fileno(), sys.stderr.fileno())
# register atexit handler
atexit.register(self.atexit_handler)
# register SIGHUP handler
signal.signal(signal.SIGHUP, self.handle_hup)
# mark us as active
self.active = True
# run the actual server code or whatever
self.run()
[docs] def start(self):
""" Start the daemon up
Call this method to start the daemon, if it already exists this will cause the process to exit with a return value of 1
"""
pid = self.get_pid()
if not (pid is None):
sys.stderr.write('Daemon already running with PID %s\n' % pid)
sys.exit(1)
self.daemonize()
self.run()
[docs] def stop(self):
""" Stop the daemon
Call this method to stop the daemon (even if it's in another process). If the daemon is not running this method does nothing.
"""
if not self.is_running(): return
self.active = False
daemon_pid = self.get_pid()
while True:
try:
os.kill(daemon_pid, signal.SIGTERM)
eventlet.greenthread.sleep(1)
except OSError,e:
if e.errno == errno.ESRCH:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
sys.exit(0)
if __name__=='__main__':
d = BaseDaemon()
d.handle_rc()