# Copyright (c) 2022 Johannes Findeisen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is furnished # to do so, subject to the following conditions: # # The above copyright notice and this permission notice (including the next # paragraph) shall be included in all copies or substantial portions of the # Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS # OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF # OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import atexit import os import signal import sys import time from logging import getLogger logger = getLogger(__name__) class Daemon: """ Usage: subclass the daemon class and override the run() method. """ def __init__(self, pid_file): self.pid_file = pid_file def daemonize(self): """ Daemonize the class using the UNIX double fork mechanism. """ # Do first fork try: pid = os.fork() if pid > 0: # Exit first parent sys.exit(0) except OSError as err: logger.error(str("fork #1 failed: {0}".format(err))) sys.exit(1) # Decouple from parent environment os.chdir('/') os.setsid() os.umask(0) # Do second fork try: pid = os.fork() if pid > 0: # Exit from second parent sys.exit(0) except OSError as err: logger.error(str("fork #2 failed: {0}".format(err))) sys.exit(1) # Redirect standard file descriptors sys.stdout.flush() sys.stderr.flush() si = open(os.devnull, 'r') so = open(os.devnull, 'a+') se = open(os.devnull, 'a+') os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) # Write pidfile atexit.register(self.del_pid) pid = str(os.getpid()) with open(self.pid_file, 'w+') as f: f.write(pid + '\n') def del_pid(self): os.remove(self.pid_file) def start(self): """ Start the daemon. """ # Check for a pidfile to see if the daemon already runs try: with open(self.pid_file, 'r') as pf: pid = int(pf.read().strip()) except IOError: pid = None if pid: message = "pid_file {0} already exist. daemon already running?" logger.error(str(message.format(self.pid_file))) sys.exit(1) # Start the daemon self.daemonize() self.run() def stop(self): """ Stop the daemon. """ # Get the pid from the pid_file try: with open(self.pid_file, 'r') as pf: pid = int(pf.read().strip()) except IOError: pid = None if not pid: message = "pid_file {0} does not exist. daemon not running?" logger.error(str(message.format(self.pid_file))) return # not an error in a restart # Try killing the daemon process try: while 1: os.kill(pid, signal.SIGTERM) time.sleep(0.1) except OSError as err: e = str(err.args) if e.find("No such process") > 0: if os.path.exists(self.pid_file): os.remove(self.pid_file) else: logger.error(str(err.args)) sys.exit(1) def restart(self): """ Restart the daemon. """ self.stop() self.start() 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(). """