| # -*- 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 |