230 lines
6.9 KiB
Python
230 lines
6.9 KiB
Python
![]() |
# -*- coding: utf-8 -*-
|
|||
|
|
|||
|
# daemon/runner.py
|
|||
|
# Part of python-daemon, an implementation of PEP 3143.
|
|||
|
#
|
|||
|
# Copyright © 2009–2010 Ben Finney <ben+python@benfinney.id.au>
|
|||
|
# Copyright © 2007–2008 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 Python Software Foundation License, version 2 or
|
|||
|
# later as published by the Python Software Foundation.
|
|||
|
# No warranty expressed or implied. See the file LICENSE.PSF-2 for details.
|
|||
|
|
|||
|
""" Daemon runner library.
|
|||
|
"""
|
|||
|
|
|||
|
import sys
|
|||
|
import os
|
|||
|
import signal
|
|||
|
import errno
|
|||
|
|
|||
|
import pidlockfile
|
|||
|
|
|||
|
from daemon import DaemonContext
|
|||
|
|
|||
|
|
|||
|
class DaemonRunnerError(Exception):
|
|||
|
""" Abstract base class for errors from DaemonRunner. """
|
|||
|
|
|||
|
class DaemonRunnerInvalidActionError(ValueError, DaemonRunnerError):
|
|||
|
""" Raised when specified action for DaemonRunner is invalid. """
|
|||
|
|
|||
|
class DaemonRunnerStartFailureError(RuntimeError, DaemonRunnerError):
|
|||
|
""" Raised when failure starting DaemonRunner. """
|
|||
|
|
|||
|
class DaemonRunnerStopFailureError(RuntimeError, DaemonRunnerError):
|
|||
|
""" Raised when failure stopping DaemonRunner. """
|
|||
|
|
|||
|
|
|||
|
class DaemonRunner(object):
|
|||
|
""" 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.
|
|||
|
|
|||
|
"""
|
|||
|
|
|||
|
start_message = "started with pid %(pid)d"
|
|||
|
|
|||
|
def __init__(self, app):
|
|||
|
""" Set up the parameters of a new runner.
|
|||
|
|
|||
|
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, 'r')
|
|||
|
self.daemon_context.stdout = open(app.stdout_path, 'w+')
|
|||
|
self.daemon_context.stderr = open(
|
|||
|
app.stderr_path, 'w+', 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.
|
|||
|
"""
|
|||
|
progname = os.path.basename(argv[0])
|
|||
|
usage_exit_code = 2
|
|||
|
action_usage = "|".join(self.action_funcs.keys())
|
|||
|
message = "usage: %(progname)s %(action_usage)s" % vars()
|
|||
|
emit_message(message)
|
|||
|
sys.exit(usage_exit_code)
|
|||
|
|
|||
|
def parse_args(self, argv=None):
|
|||
|
""" Parse command-line arguments.
|
|||
|
"""
|
|||
|
if argv is None:
|
|||
|
argv = sys.argv
|
|||
|
|
|||
|
min_args = 2
|
|||
|
if len(argv) < min_args:
|
|||
|
self._usage_exit(argv)
|
|||
|
|
|||
|
self.action = 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.
|
|||
|
"""
|
|||
|
if is_pidfile_stale(self.pidfile):
|
|||
|
self.pidfile.break_lock()
|
|||
|
|
|||
|
try:
|
|||
|
self.daemon_context.open()
|
|||
|
except pidlockfile.AlreadyLocked:
|
|||
|
pidfile_path = self.pidfile.path
|
|||
|
raise DaemonRunnerStartFailureError(
|
|||
|
"PID file %(pidfile_path)r already locked" % vars())
|
|||
|
|
|||
|
pid = os.getpid()
|
|||
|
message = self.start_message % vars()
|
|||
|
emit_message(message)
|
|||
|
|
|||
|
self.app.run()
|
|||
|
|
|||
|
def _terminate_daemon_process(self):
|
|||
|
""" Terminate the daemon process specified in the current PID file.
|
|||
|
"""
|
|||
|
pid = self.pidfile.read_pid()
|
|||
|
try:
|
|||
|
os.kill(pid, signal.SIGTERM)
|
|||
|
except OSError, exc:
|
|||
|
raise DaemonRunnerStopFailureError(
|
|||
|
"Failed to terminate %(pid)d: %(exc)s" % vars())
|
|||
|
|
|||
|
def _stop(self):
|
|||
|
""" Exit the daemon process specified in the current PID file.
|
|||
|
"""
|
|||
|
if not self.pidfile.is_locked():
|
|||
|
pidfile_path = self.pidfile.path
|
|||
|
raise DaemonRunnerStopFailureError(
|
|||
|
"PID file %(pidfile_path)r not locked" % vars())
|
|||
|
|
|||
|
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):
|
|||
|
""" Return the function for the specified action.
|
|||
|
|
|||
|
Raises ``DaemonRunnerInvalidActionError`` if the action is
|
|||
|
unknown.
|
|||
|
|
|||
|
"""
|
|||
|
try:
|
|||
|
func = self.action_funcs[self.action]
|
|||
|
except KeyError:
|
|||
|
raise DaemonRunnerInvalidActionError(
|
|||
|
"Unknown action: %(action)r" % vars(self))
|
|||
|
return func
|
|||
|
|
|||
|
def do_action(self):
|
|||
|
""" Perform the requested action.
|
|||
|
"""
|
|||
|
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)s\n" % vars())
|
|||
|
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" % vars())
|
|||
|
raise error
|
|||
|
if not os.path.isabs(path):
|
|||
|
error = ValueError("Not an absolute path: %(path)r" % vars())
|
|||
|
raise error
|
|||
|
lockfile = pidlockfile.TimeoutPIDLockFile(path, acquire_timeout)
|
|||
|
|
|||
|
return lockfile
|
|||
|
|
|||
|
|
|||
|
def is_pidfile_stale(pidfile):
|
|||
|
""" Determine whether a PID file is stale.
|
|||
|
|
|||
|
Return ``True`` (“stale”) if the contents of the PID file are
|
|||
|
valid but do not match the PID of a currently-running process;
|
|||
|
otherwise return ``False``.
|
|||
|
|
|||
|
"""
|
|||
|
result = False
|
|||
|
|
|||
|
pidfile_pid = pidfile.read_pid()
|
|||
|
if pidfile_pid is not None:
|
|||
|
try:
|
|||
|
os.kill(pidfile_pid, signal.SIG_DFL)
|
|||
|
except OSError, exc:
|
|||
|
if exc.errno == errno.ESRCH:
|
|||
|
# The specified PID does not exist
|
|||
|
result = True
|
|||
|
|
|||
|
return result
|