_runner.py 5.6 KB

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