Skip to content

Instantly share code, notes, and snippets.

@tspspi
Created November 9, 2021 15:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tspspi/4297eee1cdf1ed3caca5d0a6f467f376 to your computer and use it in GitHub Desktop.
Save tspspi/4297eee1cdf1ed3caca5d0a6f467f376 to your computer and use it in GitHub Desktop.
A simple daemon skeleton for Python 3.x

Simple daemon skeleton for Python 3.x

This is a simple daemon skeleton that:

  • parses CLI arguments
  • Sets up logging
  • Daemonizes according to CLI arguments
  • Captures SIGINT/SIGTERM as termination requests
  • Captures SIGHUP in case one should re-read the configuration

This does not use PEP-3143 but daemonize

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain this list of conditions and the following disclaimer.
  • Redistributions in binary form must reproduce this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  • Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import argparse
import sys
import logging
import signal, lockfile, grp, os
from pwd import getpwnam
from daemonize import Daemonize
class ExampleDaemon:
def __init__(self, args, logger):
self.args = args
self.logger = logger
self.terminate = False
self.rereadConfig = True
def signalSigHup(self, *args):
self.rereadConfig = True
def signalTerm(self, *args):
self.terminate = True
def __enter__(self):
return self
def __exit__(self, type, value, tb):
pass
def run(self):
signal.signal(signal.SIGHUP, self.signalSigHup)
signal.signal(signal.SIGTERM, self.signalTerm)
signal.signal(signal.SIGINT, self.signalTerm)
self.logger.info("Service running")
while True:
# Do whatever is required to be done ...
if self.terminate:
break
self.logger.info("Shutting down due to user request")
def mainDaemon():
parg = parseArguments()
args = parg['args']
logger = parg['logger']
logger.debug("Daemon starting ...")
with ExampleDaemon(args, logger) as sampleDaemon:
sampleDaemon.run()
def parseArguments():
ap = argparse.ArgumentParser(description = 'Example daemon')
ap.add_argument('-f', '--foreground', action='store_true', help="Do not daemonize - stay in foreground and dump debug information to the terminal")
ap.add_argument('--uid', type=str, required=False, default=None, help="User ID to impersonate when launching as root")
ap.add_argument('--gid', type=str, required=False, default=None, help="Group ID to impersonate when launching as root")
ap.add_argument('--chroot', type=str, required=False, default=None, help="Chroot directory that should be switched into")
ap.add_argument('--pidfile', type=str, required=False, default="/var/run/sampledaemon.pid", help="PID file to keep only one daemon instance running")
ap.add_argument('--loglevel', type=str, required=False, default="error", help="Loglevel to use (debug, info, warning, error, critical). Default: error")
ap.add_argument('--logfile', type=str, required=False, default="/var/log/sampledaemon.log", help="Logfile that should be used as target for log messages")
args = ap.parse_args()
loglvls = {
"DEBUG" : logging.DEBUG,
"INFO" : logging.INFO,
"WARNING" : logging.WARNING,
"ERROR" : logging.ERROR,
"CRITICAL" : logging.CRITICAL
}
if not args.loglevel.upper() in loglvls:
print("Unknown log level {}".format(args.loglevel.upper()))
sys.exit(1)
logger = logging.getLogger()
logger.setLevel(loglvls[args.loglevel.upper()])
if args.logfile:
fileHandleLog = logging.FileHandler(args.logfile)
logger.addHandler(fileHandleLog)
return { 'args' : args, 'logger' : logger }
# Entry function for CLI program
# This also configures the daemon properties
def mainStartup():
parg = parseArguments()
args = parg['args']
logger = parg['logger']
daemonPidfile = args.pidfile
daemonUid = None
daemonGid = None
daemonChroot = "/"
if args.uid:
try:
args.uid = int(args.uid)
except ValueError:
try:
args.uid = getpwnam(args.uid).pw_uid
except KeyError:
logger.critical("Unknown user {}".format(args.uid))
print("Unknown user {}".format(args.uid))
sys.exit(1)
daemonUid = args.uid
if args.gid:
try:
args.gid = int(args.gid)
except ValueError:
try:
args.gid = grp.getgrnam(args.gid)[2]
except KeyError:
logger.critical("Unknown group {}".format(args.gid))
print("Unknown group {}".format(args.gid))
sys.exit(1)
daemonGid = args.gid
if args.chroot:
if not os.path.isdir(args.chroot):
logger.critical("Non existing chroot directors {}".format(args.chroot))
print("Non existing chroot directors {}".format(args.chroot))
sys.exit(1)
daemonChroot = args.chroot
if args.foreground:
logger.debug("Launching in foreground")
with ExampleDaemon(args, logger) as sampleDaemon:
sampleDaemon.run()
else:
logger.debug("Daemonizing ...")
daemon = Daemonize(
app="ExampleDaemon",
action=mainDaemon,
pid=daemonPidfile,
user=daemonUid,
group=daemonGid,
chdir=daemonChroot
)
daemon.start()
if __name__ == "__main__":
mainStartup()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment