1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
# -*- test-case-name: twisted.application.runner.test.test_runner -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Twisted application runner.
"""
from os import kill
from signal import SIGTERM
from sys import stderr
from typing import Any, Callable, Mapping, TextIO
from attr import Factory, attrib, attrs
from constantly import NamedConstant # type: ignore[import]
from twisted.internet.interfaces import IReactorCore
from twisted.logger import (
FileLogObserver,
FilteringLogObserver,
Logger,
LogLevel,
LogLevelFilterPredicate,
globalLogBeginner,
textFileLogObserver,
)
from ._exit import ExitStatus, exit
from ._pidfile import AlreadyRunningError, InvalidPIDFileError, IPIDFile, nonePIDFile
@attrs(frozen=True)
class Runner:
"""
Twisted application runner.
@cvar _log: The logger attached to this class.
@ivar _reactor: The reactor to start and run the application in.
@ivar _pidFile: The file to store the running process ID in.
@ivar _kill: Whether this runner should kill an existing running
instance of the application.
@ivar _defaultLogLevel: The default log level to start the logging
system with.
@ivar _logFile: A file stream to write logging output to.
@ivar _fileLogObserverFactory: A factory for the file log observer to
use when starting the logging system.
@ivar _whenRunning: Hook to call after the reactor is running;
this is where the application code that relies on the reactor gets
called.
@ivar _whenRunningArguments: Keyword arguments to pass to
C{whenRunning} when it is called.
@ivar _reactorExited: Hook to call after the reactor exits.
@ivar _reactorExitedArguments: Keyword arguments to pass to
C{reactorExited} when it is called.
"""
_log = Logger()
_reactor = attrib(type=IReactorCore)
_pidFile = attrib(type=IPIDFile, default=nonePIDFile)
_kill = attrib(type=bool, default=False)
_defaultLogLevel = attrib(type=NamedConstant, default=LogLevel.info)
_logFile = attrib(type=TextIO, default=stderr)
_fileLogObserverFactory = attrib(
type=Callable[[TextIO], FileLogObserver], default=textFileLogObserver
)
_whenRunning = attrib(type=Callable[..., None], default=lambda **_: None)
_whenRunningArguments = attrib(type=Mapping[str, Any], default=Factory(dict))
_reactorExited = attrib(type=Callable[..., None], default=lambda **_: None)
_reactorExitedArguments = attrib(type=Mapping[str, Any], default=Factory(dict))
def run(self) -> None:
"""
Run this command.
"""
pidFile = self._pidFile
self.killIfRequested()
try:
with pidFile:
self.startLogging()
self.startReactor()
self.reactorExited()
except AlreadyRunningError:
exit(ExitStatus.EX_CONFIG, "Already running.")
# When testing, patched exit doesn't exit
return # type: ignore[unreachable]
def killIfRequested(self) -> None:
"""
If C{self._kill} is true, attempt to kill a running instance of the
application.
"""
pidFile = self._pidFile
if self._kill:
if pidFile is nonePIDFile:
exit(ExitStatus.EX_USAGE, "No PID file specified.")
# When testing, patched exit doesn't exit
return # type: ignore[unreachable]
try:
pid = pidFile.read()
except OSError:
exit(ExitStatus.EX_IOERR, "Unable to read PID file.")
# When testing, patched exit doesn't exit
return # type: ignore[unreachable]
except InvalidPIDFileError:
exit(ExitStatus.EX_DATAERR, "Invalid PID file.")
# When testing, patched exit doesn't exit
return # type: ignore[unreachable]
self.startLogging()
self._log.info("Terminating process: {pid}", pid=pid)
kill(pid, SIGTERM)
exit(ExitStatus.EX_OK)
# When testing, patched exit doesn't exit
return # type: ignore[unreachable]
def startLogging(self) -> None:
"""
Start the L{twisted.logger} logging system.
"""
logFile = self._logFile
fileLogObserverFactory = self._fileLogObserverFactory
fileLogObserver = fileLogObserverFactory(logFile)
logLevelPredicate = LogLevelFilterPredicate(
defaultLogLevel=self._defaultLogLevel
)
filteringObserver = FilteringLogObserver(fileLogObserver, [logLevelPredicate])
globalLogBeginner.beginLoggingTo([filteringObserver])
def startReactor(self) -> None:
"""
Register C{self._whenRunning} with the reactor so that it is called
once the reactor is running, then start the reactor.
"""
self._reactor.callWhenRunning(self.whenRunning)
self._log.info("Starting reactor...")
self._reactor.run()
def whenRunning(self) -> None:
"""
Call C{self._whenRunning} with C{self._whenRunningArguments}.
@note: This method is called after the reactor starts running.
"""
self._whenRunning(**self._whenRunningArguments)
def reactorExited(self) -> None:
"""
Call C{self._reactorExited} with C{self._reactorExitedArguments}.
@note: This method is called after the reactor exits.
"""
self._reactorExited(**self._reactorExitedArguments)
|