Writing daemons in python

09 Nov 2021 - tsp
Last update 09 Nov 2021
Reading time 4 mins

So I’m not a huge fan of Python for some particular reasons that are out of scope for this blog entry but it’s a language that exists and that is often used so sometimes on has to use it (by the way I like it for playing around with algorithms, fast hacks and as some advanced scripting language anyways). And then there are moments where one has to develop services around an extensive Python codebase - then it makes simply no sense to rewrite everything in a more sane and stable language than to use Python for the remaining parts of course.

Services on Unix like systems are usually (except for the new systemd area) realized as so called daemon processes. These are processes that are launched just like ordinary processes but then detach from their terminal and thus also from their process hierarchy and run in background. The Windows analogon are of course services - but they work somewhat different. On Unix like systems it’s the responsibility of the daemon to start up, detach itself from the terminal and optionally (especially important when launched as root) limit it’s own execution environment as far as possible to reduce attack surface.

Usually the daemonization process is the execution of the following steps:

On some Unices the applications might even limit themselves more - for example on FreeBSD applications can drop their permissions to gain access to network ports, prevent access to filesystems and limit access to certain system calls. This is of course operating system specific.

Since these steps are security critical and tedious to do right there are libraries that allow applications to daemonize themselves. For Python there is the new PEP-3143 - the standard daemon process library that’s found in the python-daemon PyPi package. Unfortunately I never really got that working - the following recipe uses also an external library called daemonize. As usual I don’t really feel good when using an external dependency from PyPi but that’s the Python way as it seems. This package can be references in setup.cfg as daemonize and can be manually installed using

pip install daemonize

The mini recipe that I’m referencing here performs the following steps:

The code

The whole code is available as a GitHub GIST

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

view raw README.md hosted with ❤ by GitHub

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.

view raw LICENSE.md hosted with ❤ by GitHub
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()
view raw sampledaemon.py hosted with ❤ by GitHub

This article is tagged:


Data protection policy

Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)

This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/

Valid HTML 4.01 Strict Powered by FreeBSD IPv6 support