Files
heartbeat/daemon/runner.py
T
2021-05-03 17:24:04 -04:00

323 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
# daemon/runner.py
# Part of python-daemon, an implementation of PEP 3143.
#
# Copyright © 20092015 Ben Finney <ben+python@benfinney.id.au>
# Copyright © 20072008 Robert Niederreiter, Jens Klein
# Copyright © 2003 Clark Evans
# Copyright © 2002 Noah Spurrier
# Copyright © 2001 Jürgen Hermann
#
# This is free software: you may copy, modify, and/or distribute this work
# under the terms of the Apache License, version 2.0 as published by the
# Apache Software Foundation.
# No warranty expressed or implied. See the file LICENSE.ASF-2 for details.
""" Daemon runner library.
"""
from __future__ import absolute_import, unicode_literals
import sys
import os
import signal
import errno
try:
# Python 3 standard library.
ProcessLookupError
except NameError:
# No such class in Python 2.
ProcessLookupError = NotImplemented
import lockfile
from . import pidfile
from .daemon import basestring, unicode
from .daemon import DaemonContext
from .daemon import _chain_exception_from_existing_exception_context
class DaemonRunnerError(Exception):
""" Abstract base class for errors from DaemonRunner. """
def __init__(self, *args, **kwargs):
self._chain_from_context()
super(DaemonRunnerError, self).__init__(*args, **kwargs)
def _chain_from_context(self):
_chain_exception_from_existing_exception_context(self, as_cause=True)
class DaemonRunnerInvalidActionError(DaemonRunnerError, ValueError):
""" Raised when specified action for DaemonRunner is invalid. """
def _chain_from_context(self):
# This exception is normally not caused by another.
_chain_exception_from_existing_exception_context(self, as_cause=False)
class DaemonRunnerStartFailureError(DaemonRunnerError, RuntimeError):
""" Raised when failure starting DaemonRunner. """
class DaemonRunnerStopFailureError(DaemonRunnerError, RuntimeError):
""" Raised when failure stopping DaemonRunner. """
class DaemonRunner:
""" Controller for a callable running in a separate background process.
The first command-line argument is the action to take:
* 'start': Become a daemon and call `app.run()`.
* 'stop': Exit the daemon process specified in the PID file.
* 'restart': Stop, then start.
"""
__metaclass__ = type
start_message = "started with pid {pid:d}"
def __init__(self, app):
""" Set up the parameters of a new runner.
:param app: The application instance; see below.
:return: ``None``.
The `app` argument must have the following attributes:
* `stdin_path`, `stdout_path`, `stderr_path`: Filesystem paths
to open and replace the existing `sys.stdin`, `sys.stdout`,
`sys.stderr`.
* `pidfile_path`: Absolute filesystem path to a file that will
be used as the PID file for the daemon. If ``None``, no PID
file will be used.
* `pidfile_timeout`: Used as the default acquisition timeout
value supplied to the runner's PID lock file.
* `run`: Callable that will be invoked when the daemon is
started.
"""
self.parse_args()
self.app = app
self.daemon_context = DaemonContext()
self.daemon_context.stdin = open(app.stdin_path, "rt")
self.daemon_context.stdout = open(app.stdout_path, "w+t")
self.daemon_context.stderr = open(app.stderr_path, "w+t", buffering=0)
self.pidfile = None
if app.pidfile_path is not None:
self.pidfile = make_pidlockfile(app.pidfile_path, app.pidfile_timeout)
self.daemon_context.pidfile = self.pidfile
def _usage_exit(self, argv):
""" Emit a usage message, then exit.
:param argv: The command-line arguments used to invoke the
program, as a sequence of strings.
:return: ``None``.
"""
progname = os.path.basename(argv[0])
usage_exit_code = 2
action_usage = "|".join(self.action_funcs.keys())
message = "usage: {progname} {usage}".format(
progname=progname, usage=action_usage
)
emit_message(message)
sys.exit(usage_exit_code)
def parse_args(self, argv=None):
""" Parse command-line arguments.
:param argv: The command-line arguments used to invoke the
program, as a sequence of strings.
:return: ``None``.
The parser expects the first argument as the program name, the
second argument as the action to perform.
If the parser fails to parse the arguments, emit a usage
message and exit the program.
"""
if argv is None:
argv = sys.argv
min_args = 2
if len(argv) < min_args:
self._usage_exit(argv)
self.action = unicode(argv[1])
if self.action not in self.action_funcs:
self._usage_exit(argv)
def _start(self):
""" Open the daemon context and run the application.
:return: ``None``.
:raises DaemonRunnerStartFailureError: If the PID file cannot
be locked by this process.
"""
if is_pidfile_stale(self.pidfile):
self.pidfile.break_lock()
try:
self.daemon_context.open()
except lockfile.AlreadyLocked:
error = DaemonRunnerStartFailureError(
"PID file {pidfile.path!r} already locked".format(pidfile=self.pidfile)
)
raise error
pid = os.getpid()
message = self.start_message.format(pid=pid)
emit_message(message)
self.app.run()
def _terminate_daemon_process(self):
""" Terminate the daemon process specified in the current PID file.
:return: ``None``.
:raises DaemonRunnerStopFailureError: If terminating the daemon
fails with an OS error.
"""
pid = self.pidfile.read_pid()
try:
os.kill(pid, signal.SIGTERM)
except OSError as exc:
error = DaemonRunnerStopFailureError(
"Failed to terminate {pid:d}: {exc}".format(pid=pid, exc=exc)
)
raise error
def _stop(self):
""" Exit the daemon process specified in the current PID file.
:return: ``None``.
:raises DaemonRunnerStopFailureError: If the PID file is not
already locked.
"""
if not self.pidfile.is_locked():
error = DaemonRunnerStopFailureError(
"PID file {pidfile.path!r} not locked".format(pidfile=self.pidfile)
)
raise error
if is_pidfile_stale(self.pidfile):
self.pidfile.break_lock()
else:
self._terminate_daemon_process()
def _restart(self):
""" Stop, then start.
"""
self._stop()
self._start()
action_funcs = {
"start": _start,
"stop": _stop,
"restart": _restart,
}
def _get_action_func(self):
""" Get the function for the specified action.
:return: The function object corresponding to the specified
action.
:raises DaemonRunnerInvalidActionError: if the action is
unknown.
The action is specified by the `action` attribute, which is set
during `parse_args`.
"""
try:
func = self.action_funcs[self.action]
except KeyError:
error = DaemonRunnerInvalidActionError(
"Unknown action: {action!r}".format(action=self.action)
)
raise error
return func
def do_action(self):
""" Perform the requested action.
:return: ``None``.
The action is specified by the `action` attribute, which is set
during `parse_args`.
"""
func = self._get_action_func()
func(self)
def emit_message(message, stream=None):
""" Emit a message to the specified stream (default `sys.stderr`). """
if stream is None:
stream = sys.stderr
stream.write("{message}\n".format(message=message))
stream.flush()
def make_pidlockfile(path, acquire_timeout):
""" Make a PIDLockFile instance with the given filesystem path. """
if not isinstance(path, basestring):
error = ValueError("Not a filesystem path: {path!r}".format(path=path))
raise error
if not os.path.isabs(path):
error = ValueError("Not an absolute path: {path!r}".format(path=path))
raise error
lockfile = pidfile.TimeoutPIDLockFile(path, acquire_timeout)
return lockfile
def is_pidfile_stale(pidfile):
""" Determine whether a PID file is stale.
:return: ``True`` iff the PID file is stale; otherwise ``False``.
The PID file is “stale” if its contents are valid but do not
match the PID of a currently-running process.
"""
result = False
pidfile_pid = pidfile.read_pid()
if pidfile_pid is not None:
try:
os.kill(pidfile_pid, signal.SIG_DFL)
except ProcessLookupError:
# The specified PID does not exist.
result = True
except OSError as exc:
if exc.errno == errno.ESRCH:
# Under Python 2, process lookup error is an OSError.
# The specified PID does not exist.
result = True
return result
# Local variables:
# coding: utf-8
# mode: python
# End:
# vim: fileencoding=utf-8 filetype=python :