_pidfile.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. # -*- test-case-name: twisted.application.runner.test.test_pidfile -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. PID file.
  6. """
  7. from __future__ import annotations
  8. import errno
  9. from os import getpid, kill, name as SYSTEM_NAME
  10. from types import TracebackType
  11. from typing import Any, Optional, Type
  12. from zope.interface import Interface, implementer
  13. from twisted.logger import Logger
  14. from twisted.python.filepath import FilePath
  15. class IPIDFile(Interface):
  16. """
  17. Manages a file that remembers a process ID.
  18. """
  19. def read() -> int:
  20. """
  21. Read the process ID stored in this PID file.
  22. @return: The contained process ID.
  23. @raise NoPIDFound: If this PID file does not exist.
  24. @raise EnvironmentError: If this PID file cannot be read.
  25. @raise ValueError: If this PID file's content is invalid.
  26. """
  27. def writeRunningPID() -> None:
  28. """
  29. Store the PID of the current process in this PID file.
  30. @raise EnvironmentError: If this PID file cannot be written.
  31. """
  32. def remove() -> None:
  33. """
  34. Remove this PID file.
  35. @raise EnvironmentError: If this PID file cannot be removed.
  36. """
  37. def isRunning() -> bool:
  38. """
  39. Determine whether there is a running process corresponding to the PID
  40. in this PID file.
  41. @return: True if this PID file contains a PID and a process with that
  42. PID is currently running; false otherwise.
  43. @raise EnvironmentError: If this PID file cannot be read.
  44. @raise InvalidPIDFileError: If this PID file's content is invalid.
  45. @raise StalePIDFileError: If this PID file's content refers to a PID
  46. for which there is no corresponding running process.
  47. """
  48. def __enter__() -> "IPIDFile":
  49. """
  50. Enter a context using this PIDFile.
  51. Writes the PID file with the PID of the running process.
  52. @raise AlreadyRunningError: A process corresponding to the PID in this
  53. PID file is already running.
  54. """
  55. def __exit__(
  56. excType: Optional[Type[BaseException]],
  57. excValue: Optional[BaseException],
  58. traceback: Optional[TracebackType],
  59. ) -> Optional[bool]:
  60. """
  61. Exit a context using this PIDFile.
  62. Removes the PID file.
  63. """
  64. @implementer(IPIDFile)
  65. class PIDFile:
  66. """
  67. Concrete implementation of L{IPIDFile}.
  68. This implementation is presently not supported on non-POSIX platforms.
  69. Specifically, calling L{PIDFile.isRunning} will raise
  70. L{NotImplementedError}.
  71. """
  72. _log = Logger()
  73. @staticmethod
  74. def _format(pid: int) -> bytes:
  75. """
  76. Format a PID file's content.
  77. @param pid: A process ID.
  78. @return: Formatted PID file contents.
  79. """
  80. return f"{int(pid)}\n".encode()
  81. def __init__(self, filePath: FilePath[Any]) -> None:
  82. """
  83. @param filePath: The path to the PID file on disk.
  84. """
  85. self.filePath = filePath
  86. def read(self) -> int:
  87. pidString = b""
  88. try:
  89. with self.filePath.open() as fh:
  90. for pidString in fh:
  91. break
  92. except OSError as e:
  93. if e.errno == errno.ENOENT: # No such file
  94. raise NoPIDFound("PID file does not exist")
  95. raise
  96. try:
  97. return int(pidString)
  98. except ValueError:
  99. raise InvalidPIDFileError(
  100. f"non-integer PID value in PID file: {pidString!r}"
  101. )
  102. def _write(self, pid: int) -> None:
  103. """
  104. Store a PID in this PID file.
  105. @param pid: A PID to store.
  106. @raise EnvironmentError: If this PID file cannot be written.
  107. """
  108. self.filePath.setContent(self._format(pid=pid))
  109. def writeRunningPID(self) -> None:
  110. self._write(getpid())
  111. def remove(self) -> None:
  112. self.filePath.remove()
  113. def isRunning(self) -> bool:
  114. try:
  115. pid = self.read()
  116. except NoPIDFound:
  117. return False
  118. if SYSTEM_NAME == "posix":
  119. return self._pidIsRunningPOSIX(pid)
  120. else:
  121. raise NotImplementedError(f"isRunning is not implemented on {SYSTEM_NAME}")
  122. @staticmethod
  123. def _pidIsRunningPOSIX(pid: int) -> bool:
  124. """
  125. POSIX implementation for running process check.
  126. Determine whether there is a running process corresponding to the given
  127. PID.
  128. @param pid: The PID to check.
  129. @return: True if the given PID is currently running; false otherwise.
  130. @raise EnvironmentError: If this PID file cannot be read.
  131. @raise InvalidPIDFileError: If this PID file's content is invalid.
  132. @raise StalePIDFileError: If this PID file's content refers to a PID
  133. for which there is no corresponding running process.
  134. """
  135. try:
  136. kill(pid, 0)
  137. except OSError as e:
  138. if e.errno == errno.ESRCH: # No such process
  139. raise StalePIDFileError("PID file refers to non-existing process")
  140. elif e.errno == errno.EPERM: # Not permitted to kill
  141. return True
  142. else:
  143. raise
  144. else:
  145. return True
  146. def __enter__(self) -> "PIDFile":
  147. try:
  148. if self.isRunning():
  149. raise AlreadyRunningError()
  150. except StalePIDFileError:
  151. self._log.info("Replacing stale PID file: {log_source}")
  152. self.writeRunningPID()
  153. return self
  154. def __exit__(
  155. self,
  156. excType: Optional[Type[BaseException]],
  157. excValue: Optional[BaseException],
  158. traceback: Optional[TracebackType],
  159. ) -> None:
  160. self.remove()
  161. return None
  162. @implementer(IPIDFile)
  163. class NonePIDFile:
  164. """
  165. PID file implementation that does nothing.
  166. This is meant to be used as a "active None" object in place of a PID file
  167. when no PID file is desired.
  168. """
  169. def __init__(self) -> None:
  170. pass
  171. def read(self) -> int:
  172. raise NoPIDFound("PID file does not exist")
  173. def _write(self, pid: int) -> None:
  174. """
  175. Store a PID in this PID file.
  176. @param pid: A PID to store.
  177. @raise EnvironmentError: If this PID file cannot be written.
  178. @note: This implementation always raises an L{EnvironmentError}.
  179. """
  180. raise OSError(errno.EPERM, "Operation not permitted")
  181. def writeRunningPID(self) -> None:
  182. self._write(0)
  183. def remove(self) -> None:
  184. raise OSError(errno.ENOENT, "No such file or directory")
  185. def isRunning(self) -> bool:
  186. return False
  187. def __enter__(self) -> "NonePIDFile":
  188. return self
  189. def __exit__(
  190. self,
  191. excType: Optional[Type[BaseException]],
  192. excValue: Optional[BaseException],
  193. traceback: Optional[TracebackType],
  194. ) -> None:
  195. return None
  196. nonePIDFile: IPIDFile = NonePIDFile()
  197. class AlreadyRunningError(Exception):
  198. """
  199. Process is already running.
  200. """
  201. class InvalidPIDFileError(Exception):
  202. """
  203. PID file contents are invalid.
  204. """
  205. class StalePIDFileError(Exception):
  206. """
  207. PID file contents are valid, but there is no process with the referenced
  208. PID.
  209. """
  210. class NoPIDFound(Exception):
  211. """
  212. No PID found in PID file.
  213. """