# -*- coding: utf-8 -*-
# cython: language_level=3, always_allow_keywords=True
## Copyright 2007-2024 by LivingLogic AG, Bayreuth/Germany
## Copyright 2007-2024 by Walter Dörwald
##
## All Rights Reserved
##
## See ll/xist/__init__.py for the license
r"""
This module can be used on UNIX to fork a daemon process. It is based on
`Jürgen Hermann's Cookbook recipe`__.
__ http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
An example script might look like this::
from ll import daemon
counter = daemon.Daemon(
stdin="/dev/null",
stdout="/tmp/daemon.log",
stderr="/tmp/daemon.log",
pidfile="/var/run/counter/counter.pid",
user="nobody"
)
if __name__ == "__main__":
if counter.service():
import sys, os, time
sys.stdout.write(f"Daemon started with pid {os.getpid()}\n")
sys.stdout.write(f"Daemon stdout output\n")
sys.stderr.write(f"Daemon stderr output\n")
c = 0
while True:
sys.stdout.write(f"{c}: {time.ctime(time.time())}\n")
sys.stdout.flush()
c += 1
time.sleep(1)
"""
import sys, os, signal, pwd, grp, argparse
__docformat__ = "reStructuredText"
[docs]
class Daemon:
"""
The :class:`Daemon` class provides methods for starting and stopping a
daemon process as well as handling command line arguments.
"""
[docs]
def __init__(self, stdin="/dev/null", stdout="/dev/null", stderr="/dev/null", pidfile=None, user=None, group=None):
"""
The ``stdin``, ``stdout``, and ``stderr`` arguments are file names
that will be opened and be used to replace the standard file descriptors
in ``sys.stdin``, ``sys.stdout``, and ``sys.stderr``. These arguments
are optional and default to ``"/dev/null"``. Note that stderr is opened
unbuffered, so if it shares a file with stdout then interleaved output
may not appear in the order that you expect.
``pidfile`` must be the name of a file. :meth:`start` will write the pid
of the newly forked daemon to this file. :meth:`stop` uses this file to
kill the daemon.
``user`` can be the name or uid of a user. :meth:`start` will switch
to this user for running the service. If ``user`` is :const:`None` no
user switching will be done.
In the same way ``group`` can be the name or gid of a group.
:meth:`start` will switch to this group.
"""
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
self.pidfile = pidfile
self.user = user
self.group = group
[docs]
def openstreams(self):
"""
Open the standard file descriptors stdin, stdout and stderr as specified
in the constructor.
"""
si = open(self.stdin, "rb")
so = open(self.stdout, "ab+")
se = open(self.stderr, "ab+", 0)
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
[docs]
def handlesighup(self, signum, frame):
"""
Handle a ``SIG_HUP`` signal: Reopen standard file descriptors.
"""
self.openstreams()
[docs]
def handlesigterm(self, signum, frame):
"""
Handle a ``SIG_TERM`` signal: Remove the pid file and exit.
"""
if self.pidfile is not None:
try:
os.remove(self.pidfile)
except Exception:
pass
sys.exit(0)
[docs]
def switchuser(self, user, group):
"""
Switch the effective user and group. If ``user`` and ``group`` are
both :const:`None` nothing will be done. ``user`` and ``group``
can be an :class:`int` (i.e. a user/group id) or :class:`str`
(a user/group name).
"""
if group is not None:
if isinstance(group, str):
group = grp.getgrnam(group).gr_gid
os.setegid(group)
if user is not None:
if isinstance(user, str):
user = pwd.getpwnam(user).pw_uid
os.seteuid(user)
if "HOME" in os.environ:
os.environ["HOME"] = pwd.getpwuid(user).pw_dir
[docs]
def start(self):
"""
Daemonize the running script. When this method returns the process is
completely decoupled from the parent environment.
"""
# Finish up with the current stdout/stderr
sys.stdout.flush()
sys.stderr.flush()
# Do first fork
try:
pid = os.fork()
if pid > 0:
sys.exit(0) # Exit first parent
except OSError as exc:
sys.exit(f"{sys.argv[0]}: fork #1 failed: ({exc.errno}) {exc.strerror}\n")
# Decouple from parent environment
os.chdir("/")
os.umask(0)
os.setsid()
# Do second fork
try:
pid = os.fork()
if pid > 0:
sys.exit(0) # Exit second parent
except OSError as exc:
sys.exit(f"{sys.argv[0]}: fork #2 failed: ({exc.errno}) {exc.strerror}\n")
# Now I am a daemon!
# Switch user
self.switchuser(self.user, self.group)
# Redirect standard file descriptors (will belong to the new user)
self.openstreams()
# Write pid file (will belong to the new user)
if self.pidfile is not None:
with open(self.pidfile, "wb") as f:
f.write(str(os.getpid()).encode("utf-8"))
# Reopen file descriptors on SIGHUP
signal.signal(signal.SIGHUP, self.handlesighup)
# Remove pid file and exit on SIGTERM
signal.signal(signal.SIGTERM, self.handlesigterm)
[docs]
def stop(self):
"""
Send a ``SIGTERM`` signal to a running daemon. The pid of the daemon
will be read from the pidfile specified in the constructor.
"""
if self.pidfile is None:
sys.exit("no pidfile specified")
try:
with open(self.pidfile, "rb") as f:
data = f.read()
except IOError as exc:
sys.exit(f"can't open pidfile {self.pidfile}: {exc}")
try:
pid = int(data.decode("utf-8"))
except ValueError:
sys.exit(f"mangled pidfile {self.pidfile}: {data}")
os.kill(pid, signal.SIGTERM)
[docs]
def argparser(self):
"""
Return an :mod:`argparse` parser for parsing the command line arguments.
This can be overwritten in subclasses to add more arguments.
"""
p = argparse.ArgumentParser(description="Start, stop or restart a daemon process")
p.add_argument("action", help="Action to execute", choices=("start", "stop", "restart", "run"))
p.add_argument("--pidfile", dest="pidfile", help="PID filename (default %(default)s)", default=self.pidfile)
p.add_argument("--stdin", dest="stdin", help="stdin filename (default %(default)s)", default=self.stdin)
p.add_argument("--stdout", dest="stdout", help="stdout filename (default %(default)s)", default=self.stdout)
p.add_argument("--stderr", dest="stderr", help="stderr filename (default %(default)s)", default=self.stderr)
p.add_argument("--user", dest="user", help="user name or id (default %(default)s)", default=self.user)
p.add_argument("--group", dest="group", help="group name or id (default %(default)s)", default=self.group)
return p
[docs]
def parseargs(self, parser, args=None):
"""
Use the parser returned by :meth:`argparser` to parse the argument
sequence ``args``, modify ``self`` accordingly and return
the result of the parsers :meth:`parse_args` call.
"""
args = parser.parse_args(args)
self.stdin = args.stdin
self.stdout = args.stdout
self.stderr = args.stderr
self.pidfile = args.pidfile
self.user = args.user
self.group = args.group
return args
[docs]
def service(self, args=None):
"""
Handle command line arguments and start or stop the daemon accordingly.
``args`` must be a list of command line arguments (including the
program name in ``args[0]``). If ``args`` is :const:`None` or
unspecified ``sys.argv`` is used.
The return value is true when a starting option has been specified as the
command line argument, i.e. if the daemon should be started.
The :mod:`argparse` arguments are available afterwards as ``self.args``.
"""
p = self.argparser()
args = self.parseargs(p, args)
if args.action == "run":
return True
elif args.action == "restart":
self.stop()
self.start()
return True
elif args.action == "start":
self.start()
return True
elif args.action == "stop":
self.stop()
return False