# -*- coding: utf-8 -*-
'''
***
Modified generic daemon class
***
Author: http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
License: http://creativecommons.org/licenses/by-sa/3.0/
'''
# Core modules
from __future__ import print_function
import atexit
import datetime
import errno
import logging
import os
import signal
import sys
import time
from datetime import timezone
if sys.version_info < (3, 8):
from importlib_metadata import version
else:
from importlib.metadata import version
VERSION = version('daemonizer')
UTC = datetime.datetime.now(timezone.utc)
[docs]
def timestamp():
"""
Make a UTC timestamp.
"""
sys.stdout.write(f'\nTIMESTAMP v{VERSION}: ')
sys.stdout.write('{:%Y-%m-%d %H:%M:%S %Z}\n'.format(UTC))
[docs]
class Daemon:
"""
A generic daemon class.
Usage: subclass the Daemon class and override the run() method.
"""
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,
use_cleanup=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
self.use_cleanup = use_cleanup
[docs]
def log(self, *args):
if self.verbose >= 1:
print(*args)
[docs]
def daemonize(self):
"""
Do the UNIX double-fork magic, see Stevens' "Advanced Programming
in the UNIX Environment" for details (ISBN 0201563177).
"""
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): # pylint: disable=W0613
if self.use_cleanup:
self.cleanup()
time.sleep(0.1)
self.daemon_alive = False
sys.exit()
if self.use_gevent:
import gevent
gevent.reinit()
gevent.signal_handler(signal.SIGTERM, sigtermhandler, signal.SIGTERM, None)
gevent.signal_handler(signal.SIGINT, sigtermhandler, signal.SIGINT, None)
else:
signal.signal(signal.SIGTERM, sigtermhandler)
signal.signal(signal.SIGINT, sigtermhandler)
if self.verbose:
timestamp()
self.log("Started")
logging.info('Started')
# Write pidfile
atexit.register(self.delpid) # Make sure pid file is removed if we quit
pid = str(os.getpid())
open(self.pidfile, 'w+', encoding='utf-8').write(f"{pid}\n")
[docs]
def delpid(self):
"""
Remove PID file if they are us.
"""
try:
# the process may fork itself again
pid = int(open(self.pidfile, 'r', encoding='utf-8').read().strip())
if pid == os.getpid():
os.remove(self.pidfile)
except OSError as e:
if e.errno == errno.ENOENT:
pass
else:
raise
[docs]
def start(self, *args, **kwargs):
"""
Start the daemon
"""
if self.verbose:
timestamp()
self.log("Starting...")
logging.debug('Starting...')
# Check for a pidfile to see if the daemon already runs
try:
pfile = open(self.pidfile, 'r', encoding='utf-8')
pid = int(pfile.read().strip())
pfile.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)
# Start the daemon
self.daemonize()
self.run(*args, **kwargs)
[docs]
def status(self):
"""
Get status from the daemon
"""
daemon_running = self.is_running()
if daemon_running:
message = "pidfile %s found, daemon PID is %d\n"
sys.stdout.write(message % (self.pidfile, self.get_pid()))
else:
message = "pidfile %s does not exist. Not running?\n"
sys.stderr.write(message % self.pidfile)
if self.verbose >= 1:
timestamp()
self.log(f"{__name__} status is: {self.is_running()}")
return daemon_running
[docs]
def stop(self):
"""
Stop the daemon
"""
if self.verbose >= 1:
timestamp()
self.log("Stopping...")
logging.debug('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 % 10 == 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)
if self.verbose >= 1:
timestamp()
self.log("Stopped")
logging.info('Stopped')
[docs]
def restart(self):
"""
Restart the daemon
"""
self.stop()
self.start()
[docs]
def cleanup(self):
"""
You should override this method if you need cleanup handlers on
shutdown (ie, prior to sigterm handling) and set use_cleanup to
``True`` when you subclass Daemon().
"""
raise NotImplementedError
[docs]
def get_pid(self):
"""
Get process ID.
:return pid: daemon process ID
:rtype int:
"""
try:
pfile = open(self.pidfile, 'r', encoding='utf-8')
pid = int(pfile.read().strip())
pfile.close()
except IOError:
pid = None
except SystemExit:
pid = None
return pid
[docs]
def is_running(self):
"""
Check whether the server is running.
:return: True if running, else False
"""
pid = self.get_pid()
if pid is None:
logging.debug('Process is stopped')
return False
if os.path.exists(f'/proc/{pid}'):
logging.info('Process (pid %d) is running...', pid)
return True
logging.debug('Process (pid %d) is killed', pid)
return False
[docs]
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