Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

procmon.py 13KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. # -*- test-case-name: twisted.runner.test.test_procmon -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Support for starting, monitoring, and restarting child process.
  6. """
  7. from typing import Dict, List, Optional
  8. import attr
  9. import incremental
  10. from twisted.application import service
  11. from twisted.internet import error, protocol, reactor as _reactor
  12. from twisted.logger import Logger
  13. from twisted.protocols import basic
  14. from twisted.python import deprecate
  15. @attr.s(frozen=True, auto_attribs=True)
  16. class _Process:
  17. """
  18. The parameters of a process to be restarted.
  19. @ivar args: command-line arguments (including name of command as first one)
  20. @type args: C{list}
  21. @ivar uid: user-id to run process as, or None (which means inherit uid)
  22. @type uid: C{int}
  23. @ivar gid: group-id to run process as, or None (which means inherit gid)
  24. @type gid: C{int}
  25. @ivar env: environment for process
  26. @type env: C{dict}
  27. @ivar cwd: initial working directory for process or None
  28. (which means inherit cwd)
  29. @type cwd: C{str}
  30. """
  31. args: List[str]
  32. uid: Optional[int] = None
  33. gid: Optional[int] = None
  34. env: Dict[str, str] = attr.ib(default=attr.Factory(dict))
  35. cwd: Optional[str] = None
  36. @deprecate.deprecated(incremental.Version("Twisted", 18, 7, 0))
  37. def toTuple(self):
  38. """
  39. Convert process to tuple.
  40. Convert process to tuple that looks like the legacy structure
  41. of processes, for potential users who inspected processes
  42. directly.
  43. This was only an accidental feature, and will be removed. If
  44. you need to remember what processes were added to a process monitor,
  45. keep track of that when they are added. The process list
  46. inside the process monitor is no longer a public API.
  47. This allows changing the internal structure of the process list,
  48. when warranted by bug fixes or additional features.
  49. @return: tuple representation of process
  50. """
  51. return (self.args, self.uid, self.gid, self.env)
  52. class DummyTransport:
  53. disconnecting = 0
  54. transport = DummyTransport()
  55. class LineLogger(basic.LineReceiver):
  56. tag = None
  57. stream = None
  58. delimiter = b"\n"
  59. service = None
  60. def lineReceived(self, line):
  61. try:
  62. line = line.decode("utf-8")
  63. except UnicodeDecodeError:
  64. line = repr(line)
  65. self.service.log.info(
  66. "[{tag}] {line}", tag=self.tag, line=line, stream=self.stream
  67. )
  68. class LoggingProtocol(protocol.ProcessProtocol):
  69. service = None
  70. name = None
  71. def connectionMade(self):
  72. self._output = LineLogger()
  73. self._output.tag = self.name
  74. self._output.stream = "stdout"
  75. self._output.service = self.service
  76. self._outputEmpty = True
  77. self._error = LineLogger()
  78. self._error.tag = self.name
  79. self._error.stream = "stderr"
  80. self._error.service = self.service
  81. self._errorEmpty = True
  82. self._output.makeConnection(transport)
  83. self._error.makeConnection(transport)
  84. def outReceived(self, data):
  85. self._output.dataReceived(data)
  86. self._outputEmpty = data[-1] == b"\n"
  87. def errReceived(self, data):
  88. self._error.dataReceived(data)
  89. self._errorEmpty = data[-1] == b"\n"
  90. def processEnded(self, reason):
  91. if not self._outputEmpty:
  92. self._output.dataReceived(b"\n")
  93. if not self._errorEmpty:
  94. self._error.dataReceived(b"\n")
  95. self.service.connectionLost(self.name)
  96. @property
  97. def output(self):
  98. return self._output
  99. @property
  100. def empty(self):
  101. return self._outputEmpty
  102. class ProcessMonitor(service.Service):
  103. """
  104. ProcessMonitor runs processes, monitors their progress, and restarts
  105. them when they die.
  106. The ProcessMonitor will not attempt to restart a process that appears to
  107. die instantly -- with each "instant" death (less than 1 second, by
  108. default), it will delay approximately twice as long before restarting
  109. it. A successful run will reset the counter.
  110. The primary interface is L{addProcess} and L{removeProcess}. When the
  111. service is running (that is, when the application it is attached to is
  112. running), adding a process automatically starts it.
  113. Each process has a name. This name string must uniquely identify the
  114. process. In particular, attempting to add two processes with the same
  115. name will result in a C{KeyError}.
  116. @type threshold: C{float}
  117. @ivar threshold: How long a process has to live before the death is
  118. considered instant, in seconds. The default value is 1 second.
  119. @type killTime: C{float}
  120. @ivar killTime: How long a process being killed has to get its affairs
  121. in order before it gets killed with an unmaskable signal. The
  122. default value is 5 seconds.
  123. @type minRestartDelay: C{float}
  124. @ivar minRestartDelay: The minimum time (in seconds) to wait before
  125. attempting to restart a process. Default 1s.
  126. @type maxRestartDelay: C{float}
  127. @ivar maxRestartDelay: The maximum time (in seconds) to wait before
  128. attempting to restart a process. Default 3600s (1h).
  129. @type _reactor: L{IReactorProcess} provider
  130. @ivar _reactor: A provider of L{IReactorProcess} and L{IReactorTime}
  131. which will be used to spawn processes and register delayed calls.
  132. @type log: L{Logger}
  133. @ivar log: The logger used to propagate log messages from spawned
  134. processes.
  135. """
  136. threshold = 1
  137. killTime = 5
  138. minRestartDelay = 1
  139. maxRestartDelay = 3600
  140. log = Logger()
  141. def __init__(self, reactor=_reactor):
  142. self._reactor = reactor
  143. self._processes = {}
  144. self.protocols = {}
  145. self.delay = {}
  146. self.timeStarted = {}
  147. self.murder = {}
  148. self.restart = {}
  149. @deprecate.deprecatedProperty(incremental.Version("Twisted", 18, 7, 0))
  150. def processes(self):
  151. """
  152. Processes as dict of tuples
  153. @return: Dict of process name to monitored processes as tuples
  154. """
  155. return {name: process.toTuple() for name, process in self._processes.items()}
  156. @deprecate.deprecated(incremental.Version("Twisted", 18, 7, 0))
  157. def __getstate__(self):
  158. dct = service.Service.__getstate__(self)
  159. del dct["_reactor"]
  160. dct["protocols"] = {}
  161. dct["delay"] = {}
  162. dct["timeStarted"] = {}
  163. dct["murder"] = {}
  164. dct["restart"] = {}
  165. del dct["_processes"]
  166. dct["processes"] = self.processes
  167. return dct
  168. def addProcess(self, name, args, uid=None, gid=None, env={}, cwd=None):
  169. """
  170. Add a new monitored process and start it immediately if the
  171. L{ProcessMonitor} service is running.
  172. Note that args are passed to the system call, not to the shell. If
  173. running the shell is desired, the common idiom is to use
  174. C{ProcessMonitor.addProcess("name", ['/bin/sh', '-c', shell_script])}
  175. @param name: A name for this process. This value must be
  176. unique across all processes added to this monitor.
  177. @type name: C{str}
  178. @param args: The argv sequence for the process to launch.
  179. @param uid: The user ID to use to run the process. If L{None},
  180. the current UID is used.
  181. @type uid: C{int}
  182. @param gid: The group ID to use to run the process. If L{None},
  183. the current GID is used.
  184. @type uid: C{int}
  185. @param env: The environment to give to the launched process. See
  186. L{IReactorProcess.spawnProcess}'s C{env} parameter.
  187. @type env: C{dict}
  188. @param cwd: The initial working directory of the launched process.
  189. The default of C{None} means inheriting the laucnhing process's
  190. working directory.
  191. @type env: C{dict}
  192. @raise KeyError: If a process with the given name already exists.
  193. """
  194. if name in self._processes:
  195. raise KeyError(f"remove {name} first")
  196. self._processes[name] = _Process(args, uid, gid, env, cwd)
  197. self.delay[name] = self.minRestartDelay
  198. if self.running:
  199. self.startProcess(name)
  200. def removeProcess(self, name):
  201. """
  202. Stop the named process and remove it from the list of monitored
  203. processes.
  204. @type name: C{str}
  205. @param name: A string that uniquely identifies the process.
  206. """
  207. self.stopProcess(name)
  208. del self._processes[name]
  209. def startService(self):
  210. """
  211. Start all monitored processes.
  212. """
  213. service.Service.startService(self)
  214. for name in list(self._processes):
  215. self.startProcess(name)
  216. def stopService(self):
  217. """
  218. Stop all monitored processes and cancel all scheduled process restarts.
  219. """
  220. service.Service.stopService(self)
  221. # Cancel any outstanding restarts
  222. for name, delayedCall in list(self.restart.items()):
  223. if delayedCall.active():
  224. delayedCall.cancel()
  225. for name in list(self._processes):
  226. self.stopProcess(name)
  227. def connectionLost(self, name):
  228. """
  229. Called when a monitored processes exits. If
  230. L{service.IService.running} is L{True} (ie the service is started), the
  231. process will be restarted.
  232. If the process had been running for more than
  233. L{ProcessMonitor.threshold} seconds it will be restarted immediately.
  234. If the process had been running for less than
  235. L{ProcessMonitor.threshold} seconds, the restart will be delayed and
  236. each time the process dies before the configured threshold, the restart
  237. delay will be doubled - up to a maximum delay of maxRestartDelay sec.
  238. @type name: C{str}
  239. @param name: A string that uniquely identifies the process
  240. which exited.
  241. """
  242. # Cancel the scheduled _forceStopProcess function if the process
  243. # dies naturally
  244. if name in self.murder:
  245. if self.murder[name].active():
  246. self.murder[name].cancel()
  247. del self.murder[name]
  248. del self.protocols[name]
  249. if self._reactor.seconds() - self.timeStarted[name] < self.threshold:
  250. # The process died too fast - backoff
  251. nextDelay = self.delay[name]
  252. self.delay[name] = min(self.delay[name] * 2, self.maxRestartDelay)
  253. else:
  254. # Process had been running for a significant amount of time
  255. # restart immediately
  256. nextDelay = 0
  257. self.delay[name] = self.minRestartDelay
  258. # Schedule a process restart if the service is running
  259. if self.running and name in self._processes:
  260. self.restart[name] = self._reactor.callLater(
  261. nextDelay, self.startProcess, name
  262. )
  263. def startProcess(self, name):
  264. """
  265. @param name: The name of the process to be started
  266. """
  267. # If a protocol instance already exists, it means the process is
  268. # already running
  269. if name in self.protocols:
  270. return
  271. process = self._processes[name]
  272. proto = LoggingProtocol()
  273. proto.service = self
  274. proto.name = name
  275. self.protocols[name] = proto
  276. self.timeStarted[name] = self._reactor.seconds()
  277. self._reactor.spawnProcess(
  278. proto,
  279. process.args[0],
  280. process.args,
  281. uid=process.uid,
  282. gid=process.gid,
  283. env=process.env,
  284. path=process.cwd,
  285. )
  286. def _forceStopProcess(self, proc):
  287. """
  288. @param proc: An L{IProcessTransport} provider
  289. """
  290. try:
  291. proc.signalProcess("KILL")
  292. except error.ProcessExitedAlready:
  293. pass
  294. def stopProcess(self, name):
  295. """
  296. @param name: The name of the process to be stopped
  297. """
  298. if name not in self._processes:
  299. raise KeyError(f"Unrecognized process name: {name}")
  300. proto = self.protocols.get(name, None)
  301. if proto is not None:
  302. proc = proto.transport
  303. try:
  304. proc.signalProcess("TERM")
  305. except error.ProcessExitedAlready:
  306. pass
  307. else:
  308. self.murder[name] = self._reactor.callLater(
  309. self.killTime, self._forceStopProcess, proc
  310. )
  311. def restartAll(self):
  312. """
  313. Restart all processes. This is useful for third party management
  314. services to allow a user to restart servers because of an outside change
  315. in circumstances -- for example, a new version of a library is
  316. installed.
  317. """
  318. for name in self._processes:
  319. self.stopProcess(name)
  320. def __repr__(self) -> str:
  321. lst = []
  322. for name, proc in self._processes.items():
  323. uidgid = ""
  324. if proc.uid is not None:
  325. uidgid = str(proc.uid)
  326. if proc.gid is not None:
  327. uidgid += ":" + str(proc.gid)
  328. if uidgid:
  329. uidgid = "(" + uidgid + ")"
  330. lst.append(f"{name!r}{uidgid}: {proc.args!r}")
  331. return "<" + self.__class__.__name__ + " " + " ".join(lst) + ">"