runner.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. # -*- coding: utf-8 -*-
  2. # daemon/runner.py
  3. # Part of python-daemon, an implementation of PEP 3143.
  4. #
  5. # Copyright © 2009 Ben Finney <ben+python@benfinney.id.au>
  6. # Copyright © 2007–2008 Robert Niederreiter, Jens Klein
  7. # Copyright © 2003 Clark Evans
  8. # Copyright © 2002 Noah Spurrier
  9. # Copyright © 2001 Jürgen Hermann
  10. #
  11. # This is free software: you may copy, modify, and/or distribute this work
  12. # under the terms of the Python Software Foundation License, version 2 or
  13. # later as published by the Python Software Foundation.
  14. # No warranty expressed or implied. See the file LICENSE.PSF-2 for details.
  15. """ Daemon runner library.
  16. """
  17. import sys
  18. import os
  19. import signal
  20. import errno
  21. import pidlockfile
  22. from daemon import DaemonContext
  23. class DaemonRunnerError(Exception):
  24. """ Abstract base class for errors from DaemonRunner. """
  25. class DaemonRunnerInvalidActionError(ValueError, DaemonRunnerError):
  26. """ Raised when specified action for DaemonRunner is invalid. """
  27. class DaemonRunnerStartFailureError(RuntimeError, DaemonRunnerError):
  28. """ Raised when failure starting DaemonRunner. """
  29. class DaemonRunnerStopFailureError(RuntimeError, DaemonRunnerError):
  30. """ Raised when failure stopping DaemonRunner. """
  31. class DaemonRunner(object):
  32. """ Controller for a callable running in a separate background process.
  33. The first command-line argument is the action to take:
  34. * 'start': Become a daemon and call `app.run()`.
  35. * 'stop': Exit the daemon process specified in the PID file.
  36. * 'restart': Stop, then start.
  37. """
  38. start_message = "started with pid %(pid)d"
  39. def __init__(self, app):
  40. """ Set up the parameters of a new runner.
  41. The `app` argument must have the following attributes:
  42. * `stdin_path`, `stdout_path`, `stderr_path`: Filesystem
  43. paths to open and replace the existing `sys.stdin`,
  44. `sys.stdout`, `sys.stderr`.
  45. * `pidfile_path`: Absolute filesystem path to a file that
  46. will be used as the PID file for the daemon. If
  47. ``None``, no PID file will be used.
  48. * `pidfile_timeout`: Used as the default acquisition
  49. timeout value supplied to the runner's PID lock file.
  50. * `run`: Callable that will be invoked when the daemon is
  51. started.
  52. """
  53. self.parse_args()
  54. self.app = app
  55. self.daemon_context = DaemonContext()
  56. self.daemon_context.stdin = open(app.stdin_path, 'r')
  57. self.daemon_context.stdout = open(app.stdout_path, 'w+')
  58. self.daemon_context.stderr = open(
  59. app.stderr_path, 'w+', buffering=0)
  60. self.pidfile = None
  61. if app.pidfile_path is not None:
  62. self.pidfile = make_pidlockfile(
  63. app.pidfile_path, app.pidfile_timeout)
  64. self.daemon_context.pidfile = self.pidfile
  65. def _usage_exit(self, argv):
  66. """ Emit a usage message, then exit.
  67. """
  68. progname = os.path.basename(argv[0])
  69. usage_exit_code = 2
  70. action_usage = "|".join(self.action_funcs.keys())
  71. message = "usage: %(progname)s %(action_usage)s" % vars()
  72. emit_message(message)
  73. sys.exit(usage_exit_code)
  74. def parse_args(self, argv=None):
  75. """ Parse command-line arguments.
  76. """
  77. if argv is None:
  78. argv = sys.argv
  79. min_args = 2
  80. if len(argv) < min_args:
  81. self._usage_exit(argv)
  82. self.action = argv[1]
  83. if self.action not in self.action_funcs:
  84. self._usage_exit(argv)
  85. def _start(self):
  86. """ Open the daemon context and run the application.
  87. """
  88. if is_pidfile_stale(self.pidfile):
  89. self.pidfile.break_lock()
  90. try:
  91. self.daemon_context.open()
  92. except pidlockfile.AlreadyLocked:
  93. pidfile_path = self.pidfile.path
  94. raise DaemonRunnerStartFailureError(
  95. "PID file %(pidfile_path)r already locked" % vars())
  96. pid = os.getpid()
  97. message = self.start_message % vars()
  98. emit_message(message)
  99. self.app.run()
  100. def _terminate_daemon_process(self):
  101. """ Terminate the daemon process specified in the current PID file.
  102. """
  103. pid = self.pidfile.read_pid()
  104. try:
  105. os.kill(pid, signal.SIGTERM)
  106. except OSError, exc:
  107. raise DaemonRunnerStopFailureError(
  108. "Failed to terminate %(pid)d: %(exc)s" % vars())
  109. def _stop(self):
  110. """ Exit the daemon process specified in the current PID file.
  111. """
  112. if not self.pidfile.is_locked():
  113. pidfile_path = self.pidfile.path
  114. raise DaemonRunnerStopFailureError(
  115. "PID file %(pidfile_path)r not locked" % vars())
  116. if is_pidfile_stale(self.pidfile):
  117. self.pidfile.break_lock()
  118. else:
  119. self._terminate_daemon_process()
  120. def _restart(self):
  121. """ Stop, then start.
  122. """
  123. self._stop()
  124. self._start()
  125. action_funcs = {
  126. 'start': _start,
  127. 'stop': _stop,
  128. 'restart': _restart,
  129. }
  130. def _get_action_func(self):
  131. """ Return the function for the specified action.
  132. Raises ``DaemonRunnerInvalidActionError`` if the action is
  133. unknown.
  134. """
  135. try:
  136. func = self.action_funcs[self.action]
  137. except KeyError:
  138. raise DaemonRunnerInvalidActionError(
  139. "Unknown action: %(action)r" % vars(self))
  140. return func
  141. def do_action(self):
  142. """ Perform the requested action.
  143. """
  144. func = self._get_action_func()
  145. func(self)
  146. def emit_message(message, stream=None):
  147. """ Emit a message to the specified stream (default `sys.stderr`). """
  148. if stream is None:
  149. stream = sys.stderr
  150. stream.write("%(message)s\n" % vars())
  151. stream.flush()
  152. def make_pidlockfile(path, acquire_timeout):
  153. """ Make a PIDLockFile instance with the given filesystem path. """
  154. if not isinstance(path, basestring):
  155. error = ValueError("Not a filesystem path: %(path)r" % vars())
  156. raise error
  157. if not os.path.isabs(path):
  158. error = ValueError("Not an absolute path: %(path)r" % vars())
  159. raise error
  160. lockfile = pidlockfile.TimeoutPIDLockFile(path, acquire_timeout)
  161. return lockfile
  162. def is_pidfile_stale(pidfile):
  163. """ Determine whether a PID file is stale.
  164. Return ``True`` (“stale”) if the contents of the PID file are
  165. valid but do not match the PID of a currently-running process;
  166. otherwise return ``False``.
  167. """
  168. result = False
  169. pidfile_pid = pidfile.read_pid()
  170. if pidfile_pid is not None:
  171. try:
  172. os.kill(pidfile_pid, signal.SIG_DFL)
  173. except OSError, exc:
  174. if exc.errno == errno.ESRCH:
  175. # The specified PID does not exist
  176. result = True
  177. return result