_runner.py 5.5 KB

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