33
|
1 # -*- coding: utf-8 -*-
|
|
2
|
|
3 # daemon/runner.py
|
|
4 # Part of ‘python-daemon’, an implementation of PEP 3143.
|
|
5 #
|
|
6 # Copyright © 2009–2015 Ben Finney <ben+python@benfinney.id.au>
|
|
7 # Copyright © 2007–2008 Robert Niederreiter, Jens Klein
|
|
8 # Copyright © 2003 Clark Evans
|
|
9 # Copyright © 2002 Noah Spurrier
|
|
10 # Copyright © 2001 Jürgen Hermann
|
|
11 #
|
|
12 # This is free software: you may copy, modify, and/or distribute this work
|
|
13 # under the terms of the Apache License, version 2.0 as published by the
|
|
14 # Apache Software Foundation.
|
|
15 # No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details.
|
|
16
|
|
17 """ Daemon runner library.
|
|
18 """
|
|
19
|
|
20 from __future__ import (absolute_import, unicode_literals)
|
|
21
|
|
22 import sys
|
|
23 import os
|
|
24 import signal
|
|
25 import errno
|
|
26 try:
|
|
27 # Python 3 standard library.
|
|
28 ProcessLookupError
|
|
29 except NameError:
|
|
30 # No such class in Python 2.
|
|
31 ProcessLookupError = NotImplemented
|
|
32
|
|
33 import lockfile
|
|
34
|
|
35 from . import pidfile
|
|
36 from .daemon import (basestring, unicode)
|
|
37 from .daemon import DaemonContext
|
|
38 from .daemon import _chain_exception_from_existing_exception_context
|
|
39
|
|
40
|
|
41 class DaemonRunnerError(Exception):
|
|
42 """ Abstract base class for errors from DaemonRunner. """
|
|
43
|
|
44 def __init__(self, *args, **kwargs):
|
|
45 self._chain_from_context()
|
|
46
|
|
47 super(DaemonRunnerError, self).__init__(*args, **kwargs)
|
|
48
|
|
49 def _chain_from_context(self):
|
|
50 _chain_exception_from_existing_exception_context(self, as_cause=True)
|
|
51
|
|
52
|
|
53 class DaemonRunnerInvalidActionError(DaemonRunnerError, ValueError):
|
|
54 """ Raised when specified action for DaemonRunner is invalid. """
|
|
55
|
|
56 def _chain_from_context(self):
|
|
57 # This exception is normally not caused by another.
|
|
58 _chain_exception_from_existing_exception_context(self, as_cause=False)
|
|
59
|
|
60
|
|
61 class DaemonRunnerStartFailureError(DaemonRunnerError, RuntimeError):
|
|
62 """ Raised when failure starting DaemonRunner. """
|
|
63
|
|
64
|
|
65 class DaemonRunnerStopFailureError(DaemonRunnerError, RuntimeError):
|
|
66 """ Raised when failure stopping DaemonRunner. """
|
|
67
|
|
68
|
|
69 class DaemonRunner:
|
|
70 """ Controller for a callable running in a separate background process.
|
|
71
|
|
72 The first command-line argument is the action to take:
|
|
73
|
|
74 * 'start': Become a daemon and call `app.run()`.
|
|
75 * 'stop': Exit the daemon process specified in the PID file.
|
|
76 * 'restart': Stop, then start.
|
|
77
|
|
78 """
|
|
79
|
|
80 __metaclass__ = type
|
|
81
|
|
82 start_message = "started with pid {pid:d}"
|
|
83
|
|
84 def __init__(self, app):
|
|
85 """ Set up the parameters of a new runner.
|
|
86
|
|
87 :param app: The application instance; see below.
|
|
88 :return: ``None``.
|
|
89
|
|
90 The `app` argument must have the following attributes:
|
|
91
|
|
92 * `stdin_path`, `stdout_path`, `stderr_path`: Filesystem paths
|
|
93 to open and replace the existing `sys.stdin`, `sys.stdout`,
|
|
94 `sys.stderr`.
|
|
95
|
|
96 * `pidfile_path`: Absolute filesystem path to a file that will
|
|
97 be used as the PID file for the daemon. If ``None``, no PID
|
|
98 file will be used.
|
|
99
|
|
100 * `pidfile_timeout`: Used as the default acquisition timeout
|
|
101 value supplied to the runner's PID lock file.
|
|
102
|
|
103 * `run`: Callable that will be invoked when the daemon is
|
|
104 started.
|
|
105
|
|
106 """
|
|
107 self.parse_args()
|
|
108 self.app = app
|
|
109 self.daemon_context = DaemonContext()
|
|
110 self.daemon_context.stdin = open(app.stdin_path, 'rt')
|
|
111 self.daemon_context.stdout = open(app.stdout_path, 'w+t')
|
|
112 self.daemon_context.stderr = open(
|
|
113 app.stderr_path, 'w+t', buffering=0)
|
|
114
|
|
115 self.pidfile = None
|
|
116 if app.pidfile_path is not None:
|
|
117 self.pidfile = make_pidlockfile(
|
|
118 app.pidfile_path, app.pidfile_timeout)
|
|
119 self.daemon_context.pidfile = self.pidfile
|
|
120
|
|
121 def _usage_exit(self, argv):
|
|
122 """ Emit a usage message, then exit.
|
|
123
|
|
124 :param argv: The command-line arguments used to invoke the
|
|
125 program, as a sequence of strings.
|
|
126 :return: ``None``.
|
|
127
|
|
128 """
|
|
129 progname = os.path.basename(argv[0])
|
|
130 usage_exit_code = 2
|
|
131 action_usage = "|".join(self.action_funcs.keys())
|
|
132 message = "usage: {progname} {usage}".format(
|
|
133 progname=progname, usage=action_usage)
|
|
134 emit_message(message)
|
|
135 sys.exit(usage_exit_code)
|
|
136
|
|
137 def parse_args(self, argv=None):
|
|
138 """ Parse command-line arguments.
|
|
139
|
|
140 :param argv: The command-line arguments used to invoke the
|
|
141 program, as a sequence of strings.
|
|
142
|
|
143 :return: ``None``.
|
|
144
|
|
145 The parser expects the first argument as the program name, the
|
|
146 second argument as the action to perform.
|
|
147
|
|
148 If the parser fails to parse the arguments, emit a usage
|
|
149 message and exit the program.
|
|
150
|
|
151 """
|
|
152 if argv is None:
|
|
153 argv = sys.argv
|
|
154
|
|
155 min_args = 2
|
|
156 if len(argv) < min_args:
|
|
157 self._usage_exit(argv)
|
|
158
|
|
159 self.action = unicode(argv[1])
|
|
160 if self.action not in self.action_funcs:
|
|
161 self._usage_exit(argv)
|
|
162
|
|
163 def _start(self):
|
|
164 """ Open the daemon context and run the application.
|
|
165
|
|
166 :return: ``None``.
|
|
167 :raises DaemonRunnerStartFailureError: If the PID file cannot
|
|
168 be locked by this process.
|
|
169
|
|
170 """
|
|
171 if is_pidfile_stale(self.pidfile):
|
|
172 self.pidfile.break_lock()
|
|
173
|
|
174 try:
|
|
175 self.daemon_context.open()
|
|
176 except lockfile.AlreadyLocked:
|
|
177 error = DaemonRunnerStartFailureError(
|
|
178 "PID file {pidfile.path!r} already locked".format(
|
|
179 pidfile=self.pidfile))
|
|
180 raise error
|
|
181
|
|
182 pid = os.getpid()
|
|
183 message = self.start_message.format(pid=pid)
|
|
184 emit_message(message)
|
|
185
|
|
186 self.app.run()
|
|
187
|
|
188 def _terminate_daemon_process(self):
|
|
189 """ Terminate the daemon process specified in the current PID file.
|
|
190
|
|
191 :return: ``None``.
|
|
192 :raises DaemonRunnerStopFailureError: If terminating the daemon
|
|
193 fails with an OS error.
|
|
194
|
|
195 """
|
|
196 pid = self.pidfile.read_pid()
|
|
197 try:
|
|
198 os.kill(pid, signal.SIGTERM)
|
|
199 except OSError as exc:
|
|
200 error = DaemonRunnerStopFailureError(
|
|
201 "Failed to terminate {pid:d}: {exc}".format(
|
|
202 pid=pid, exc=exc))
|
|
203 raise error
|
|
204
|
|
205 def _stop(self):
|
|
206 """ Exit the daemon process specified in the current PID file.
|
|
207
|
|
208 :return: ``None``.
|
|
209 :raises DaemonRunnerStopFailureError: If the PID file is not
|
|
210 already locked.
|
|
211
|
|
212 """
|
|
213 if not self.pidfile.is_locked():
|
|
214 error = DaemonRunnerStopFailureError(
|
|
215 "PID file {pidfile.path!r} not locked".format(
|
|
216 pidfile=self.pidfile))
|
|
217 raise error
|
|
218
|
|
219 if is_pidfile_stale(self.pidfile):
|
|
220 self.pidfile.break_lock()
|
|
221 else:
|
|
222 self._terminate_daemon_process()
|
|
223
|
|
224 def _restart(self):
|
|
225 """ Stop, then start.
|
|
226 """
|
|
227 self._stop()
|
|
228 self._start()
|
|
229
|
|
230 action_funcs = {
|
|
231 'start': _start,
|
|
232 'stop': _stop,
|
|
233 'restart': _restart,
|
|
234 }
|
|
235
|
|
236 def _get_action_func(self):
|
|
237 """ Get the function for the specified action.
|
|
238
|
|
239 :return: The function object corresponding to the specified
|
|
240 action.
|
|
241 :raises DaemonRunnerInvalidActionError: if the action is
|
|
242 unknown.
|
|
243
|
|
244 The action is specified by the `action` attribute, which is set
|
|
245 during `parse_args`.
|
|
246
|
|
247 """
|
|
248 try:
|
|
249 func = self.action_funcs[self.action]
|
|
250 except KeyError:
|
|
251 error = DaemonRunnerInvalidActionError(
|
|
252 "Unknown action: {action!r}".format(
|
|
253 action=self.action))
|
|
254 raise error
|
|
255 return func
|
|
256
|
|
257 def do_action(self):
|
|
258 """ Perform the requested action.
|
|
259
|
|
260 :return: ``None``.
|
|
261
|
|
262 The action is specified by the `action` attribute, which is set
|
|
263 during `parse_args`.
|
|
264
|
|
265 """
|
|
266 func = self._get_action_func()
|
|
267 func(self)
|
|
268
|
|
269
|
|
270 def emit_message(message, stream=None):
|
|
271 """ Emit a message to the specified stream (default `sys.stderr`). """
|
|
272 if stream is None:
|
|
273 stream = sys.stderr
|
|
274 stream.write("{message}\n".format(message=message))
|
|
275 stream.flush()
|
|
276
|
|
277
|
|
278 def make_pidlockfile(path, acquire_timeout):
|
|
279 """ Make a PIDLockFile instance with the given filesystem path. """
|
|
280 if not isinstance(path, basestring):
|
|
281 error = ValueError("Not a filesystem path: {path!r}".format(
|
|
282 path=path))
|
|
283 raise error
|
|
284 if not os.path.isabs(path):
|
|
285 error = ValueError("Not an absolute path: {path!r}".format(
|
|
286 path=path))
|
|
287 raise error
|
|
288 lockfile = pidfile.TimeoutPIDLockFile(path, acquire_timeout)
|
|
289
|
|
290 return lockfile
|
|
291
|
|
292
|
|
293 def is_pidfile_stale(pidfile):
|
|
294 """ Determine whether a PID file is stale.
|
|
295
|
|
296 :return: ``True`` iff the PID file is stale; otherwise ``False``.
|
|
297
|
|
298 The PID file is “stale” if its contents are valid but do not
|
|
299 match the PID of a currently-running process.
|
|
300
|
|
301 """
|
|
302 result = False
|
|
303
|
|
304 pidfile_pid = pidfile.read_pid()
|
|
305 if pidfile_pid is not None:
|
|
306 try:
|
|
307 os.kill(pidfile_pid, signal.SIG_DFL)
|
|
308 except ProcessLookupError:
|
|
309 # The specified PID does not exist.
|
|
310 result = True
|
|
311 except OSError as exc:
|
|
312 if exc.errno == errno.ESRCH:
|
|
313 # Under Python 2, process lookup error is an OSError.
|
|
314 # The specified PID does not exist.
|
|
315 result = True
|
|
316
|
|
317 return result
|
|
318
|
|
319
|
|
320 # Local variables:
|
|
321 # coding: utf-8
|
|
322 # mode: python
|
|
323 # End:
|
|
324 # vim: fileencoding=utf-8 filetype=python :
|