_twistd_unix.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. # -*- test-case-name: twisted.test.test_twistd -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. import errno
  5. import os
  6. import pwd
  7. import sys
  8. import traceback
  9. from twisted import copyright, logger
  10. from twisted.application import app, service
  11. from twisted.internet.interfaces import IReactorDaemonize
  12. from twisted.python import log, logfile, usage
  13. from twisted.python.runtime import platformType
  14. from twisted.python.util import gidFromString, switchUID, uidFromString, untilConcludes
  15. if platformType == "win32":
  16. raise ImportError("_twistd_unix doesn't work on Windows.")
  17. def _umask(value):
  18. return int(value, 8)
  19. class ServerOptions(app.ServerOptions):
  20. synopsis = "Usage: twistd [options]"
  21. optFlags = [
  22. ["nodaemon", "n", "don't daemonize, don't use default umask of 0077"],
  23. ["originalname", None, "Don't try to change the process name"],
  24. ["syslog", None, "Log to syslog, not to file"],
  25. [
  26. "euid",
  27. "",
  28. "Set only effective user-id rather than real user-id. "
  29. "(This option has no effect unless the server is running as "
  30. "root, in which case it means not to shed all privileges "
  31. "after binding ports, retaining the option to regain "
  32. "privileges in cases such as spawning processes. "
  33. "Use with caution.)",
  34. ],
  35. ]
  36. optParameters = [
  37. ["prefix", None, "twisted", "use the given prefix when syslogging"],
  38. ["pidfile", "", "twistd.pid", "Name of the pidfile"],
  39. ["chroot", None, None, "Chroot to a supplied directory before running"],
  40. ["uid", "u", None, "The uid to run as.", uidFromString],
  41. [
  42. "gid",
  43. "g",
  44. None,
  45. "The gid to run as. If not specified, the default gid "
  46. "associated with the specified --uid is used.",
  47. gidFromString,
  48. ],
  49. ["umask", None, None, "The (octal) file creation mask to apply.", _umask],
  50. ]
  51. compData = usage.Completions(
  52. optActions={
  53. "pidfile": usage.CompleteFiles("*.pid"),
  54. "chroot": usage.CompleteDirs(descr="chroot directory"),
  55. "gid": usage.CompleteGroups(descr="gid to run as"),
  56. "uid": usage.CompleteUsernames(descr="uid to run as"),
  57. "prefix": usage.Completer(descr="syslog prefix"),
  58. },
  59. )
  60. def opt_version(self):
  61. """
  62. Print version information and exit.
  63. """
  64. print(f"twistd (the Twisted daemon) {copyright.version}", file=self.stdout)
  65. print(copyright.copyright, file=self.stdout)
  66. sys.exit()
  67. def postOptions(self):
  68. app.ServerOptions.postOptions(self)
  69. if self["pidfile"]:
  70. self["pidfile"] = os.path.abspath(self["pidfile"])
  71. def checkPID(pidfile):
  72. if not pidfile:
  73. return
  74. if os.path.exists(pidfile):
  75. try:
  76. with open(pidfile) as f:
  77. pid = int(f.read())
  78. except ValueError:
  79. sys.exit(f"Pidfile {pidfile} contains non-numeric value")
  80. try:
  81. os.kill(pid, 0)
  82. except OSError as why:
  83. if why.errno == errno.ESRCH:
  84. # The pid doesn't exist.
  85. log.msg(f"Removing stale pidfile {pidfile}", isError=True)
  86. os.remove(pidfile)
  87. else:
  88. sys.exit(
  89. "Can't check status of PID {} from pidfile {}: {}".format(
  90. pid, pidfile, why
  91. )
  92. )
  93. else:
  94. sys.exit(
  95. """\
  96. Another twistd server is running, PID {}\n
  97. This could either be a previously started instance of your application or a
  98. different application entirely. To start a new one, either run it in some other
  99. directory, or use the --pidfile and --logfile parameters to avoid clashes.
  100. """.format(
  101. pid
  102. )
  103. )
  104. class UnixAppLogger(app.AppLogger):
  105. """
  106. A logger able to log to syslog, to files, and to stdout.
  107. @ivar _syslog: A flag indicating whether to use syslog instead of file
  108. logging.
  109. @type _syslog: C{bool}
  110. @ivar _syslogPrefix: If C{sysLog} is C{True}, the string prefix to use for
  111. syslog messages.
  112. @type _syslogPrefix: C{str}
  113. @ivar _nodaemon: A flag indicating the process will not be daemonizing.
  114. @type _nodaemon: C{bool}
  115. """
  116. def __init__(self, options):
  117. app.AppLogger.__init__(self, options)
  118. self._syslog = options.get("syslog", False)
  119. self._syslogPrefix = options.get("prefix", "")
  120. self._nodaemon = options.get("nodaemon", False)
  121. def _getLogObserver(self):
  122. """
  123. Create and return a suitable log observer for the given configuration.
  124. The observer will go to syslog using the prefix C{_syslogPrefix} if
  125. C{_syslog} is true. Otherwise, it will go to the file named
  126. C{_logfilename} or, if C{_nodaemon} is true and C{_logfilename} is
  127. C{"-"}, to stdout.
  128. @return: An object suitable to be passed to C{log.addObserver}.
  129. """
  130. if self._syslog:
  131. from twisted.python import syslog
  132. return syslog.SyslogObserver(self._syslogPrefix).emit
  133. if self._logfilename == "-":
  134. if not self._nodaemon:
  135. sys.exit("Daemons cannot log to stdout, exiting!")
  136. logFile = sys.stdout
  137. elif self._nodaemon and not self._logfilename:
  138. logFile = sys.stdout
  139. else:
  140. if not self._logfilename:
  141. self._logfilename = "twistd.log"
  142. logFile = logfile.LogFile.fromFullPath(self._logfilename)
  143. try:
  144. import signal
  145. except ImportError:
  146. pass
  147. else:
  148. # Override if signal is set to None or SIG_DFL (0)
  149. if not signal.getsignal(signal.SIGUSR1):
  150. def rotateLog(signal, frame):
  151. from twisted.internet import reactor
  152. reactor.callFromThread(logFile.rotate)
  153. signal.signal(signal.SIGUSR1, rotateLog)
  154. return logger.textFileLogObserver(logFile)
  155. def launchWithName(name):
  156. if name and name != sys.argv[0]:
  157. exe = os.path.realpath(sys.executable)
  158. log.msg("Changing process name to " + name)
  159. os.execv(exe, [name, sys.argv[0], "--originalname"] + sys.argv[1:])
  160. class UnixApplicationRunner(app.ApplicationRunner):
  161. """
  162. An ApplicationRunner which does Unix-specific things, like fork,
  163. shed privileges, and maintain a PID file.
  164. """
  165. loggerFactory = UnixAppLogger
  166. def preApplication(self):
  167. """
  168. Do pre-application-creation setup.
  169. """
  170. checkPID(self.config["pidfile"])
  171. self.config["nodaemon"] = self.config["nodaemon"] or self.config["debug"]
  172. self.oldstdout = sys.stdout
  173. self.oldstderr = sys.stderr
  174. def _formatChildException(self, exception):
  175. """
  176. Format the C{exception} in preparation for writing to the
  177. status pipe. This does the right thing on Python 2 if the
  178. exception's message is Unicode, and in all cases limits the
  179. length of the message afte* encoding to 100 bytes.
  180. This means the returned message may be truncated in the middle
  181. of a unicode escape.
  182. @type exception: L{Exception}
  183. @param exception: The exception to format.
  184. @return: The formatted message, suitable for writing to the
  185. status pipe.
  186. @rtype: L{bytes}
  187. """
  188. # On Python 2 this will encode Unicode messages with the ascii
  189. # codec and the backslashreplace error handler.
  190. exceptionLine = traceback.format_exception_only(exception.__class__, exception)[
  191. -1
  192. ]
  193. # remove the trailing newline
  194. formattedMessage = f"1 {exceptionLine.strip()}"
  195. # On Python 3, encode the message the same way Python 2's
  196. # format_exception_only does
  197. formattedMessage = formattedMessage.encode("ascii", "backslashreplace")
  198. # By this point, the message has been encoded, if appropriate,
  199. # with backslashreplace on both Python 2 and Python 3.
  200. # Truncating the encoded message won't make it completely
  201. # unreadable, and the reader should print out the repr of the
  202. # message it receives anyway. What it will do, however, is
  203. # ensure that only 100 bytes are written to the status pipe,
  204. # ensuring that the child doesn't block because the pipe's
  205. # full. This assumes PIPE_BUF > 100!
  206. return formattedMessage[:100]
  207. def postApplication(self):
  208. """
  209. To be called after the application is created: start the application
  210. and run the reactor. After the reactor stops, clean up PID files and
  211. such.
  212. """
  213. try:
  214. self.startApplication(self.application)
  215. except Exception as ex:
  216. statusPipe = self.config.get("statusPipe", None)
  217. if statusPipe is not None:
  218. message = self._formatChildException(ex)
  219. untilConcludes(os.write, statusPipe, message)
  220. untilConcludes(os.close, statusPipe)
  221. self.removePID(self.config["pidfile"])
  222. raise
  223. else:
  224. statusPipe = self.config.get("statusPipe", None)
  225. if statusPipe is not None:
  226. untilConcludes(os.write, statusPipe, b"0")
  227. untilConcludes(os.close, statusPipe)
  228. self.startReactor(None, self.oldstdout, self.oldstderr)
  229. self.removePID(self.config["pidfile"])
  230. def removePID(self, pidfile):
  231. """
  232. Remove the specified PID file, if possible. Errors are logged, not
  233. raised.
  234. @type pidfile: C{str}
  235. @param pidfile: The path to the PID tracking file.
  236. """
  237. if not pidfile:
  238. return
  239. try:
  240. os.unlink(pidfile)
  241. except OSError as e:
  242. if e.errno == errno.EACCES or e.errno == errno.EPERM:
  243. log.msg("Warning: No permission to delete pid file")
  244. else:
  245. log.err(e, "Failed to unlink PID file:")
  246. except BaseException:
  247. log.err(None, "Failed to unlink PID file:")
  248. def setupEnvironment(self, chroot, rundir, nodaemon, umask, pidfile):
  249. """
  250. Set the filesystem root, the working directory, and daemonize.
  251. @type chroot: C{str} or L{None}
  252. @param chroot: If not None, a path to use as the filesystem root (using
  253. L{os.chroot}).
  254. @type rundir: C{str}
  255. @param rundir: The path to set as the working directory.
  256. @type nodaemon: C{bool}
  257. @param nodaemon: A flag which, if set, indicates that daemonization
  258. should not be done.
  259. @type umask: C{int} or L{None}
  260. @param umask: The value to which to change the process umask.
  261. @type pidfile: C{str} or L{None}
  262. @param pidfile: If not L{None}, the path to a file into which to put
  263. the PID of this process.
  264. """
  265. daemon = not nodaemon
  266. if chroot is not None:
  267. os.chroot(chroot)
  268. if rundir == ".":
  269. rundir = "/"
  270. os.chdir(rundir)
  271. if daemon and umask is None:
  272. umask = 0o077
  273. if umask is not None:
  274. os.umask(umask)
  275. if daemon:
  276. from twisted.internet import reactor
  277. self.config["statusPipe"] = self.daemonize(reactor)
  278. if pidfile:
  279. with open(pidfile, "wb") as f:
  280. f.write(b"%d" % (os.getpid(),))
  281. def daemonize(self, reactor):
  282. """
  283. Daemonizes the application on Unix. This is done by the usual double
  284. forking approach.
  285. @see: U{http://code.activestate.com/recipes/278731/}
  286. @see: W. Richard Stevens,
  287. "Advanced Programming in the Unix Environment",
  288. 1992, Addison-Wesley, ISBN 0-201-56317-7
  289. @param reactor: The reactor in use. If it provides
  290. L{IReactorDaemonize}, its daemonization-related callbacks will be
  291. invoked.
  292. @return: A writable pipe to be used to report errors.
  293. @rtype: C{int}
  294. """
  295. # If the reactor requires hooks to be called for daemonization, call
  296. # them. Currently the only reactor which provides/needs that is
  297. # KQueueReactor.
  298. if IReactorDaemonize.providedBy(reactor):
  299. reactor.beforeDaemonize()
  300. r, w = os.pipe()
  301. if os.fork(): # launch child and...
  302. code = self._waitForStart(r)
  303. os.close(r)
  304. os._exit(code) # kill off parent
  305. os.setsid()
  306. if os.fork(): # launch child and...
  307. os._exit(0) # kill off parent again.
  308. null = os.open("/dev/null", os.O_RDWR)
  309. for i in range(3):
  310. try:
  311. os.dup2(null, i)
  312. except OSError as e:
  313. if e.errno != errno.EBADF:
  314. raise
  315. os.close(null)
  316. if IReactorDaemonize.providedBy(reactor):
  317. reactor.afterDaemonize()
  318. return w
  319. def _waitForStart(self, readPipe: int) -> int:
  320. """
  321. Wait for the daemonization success.
  322. @param readPipe: file descriptor to read start information from.
  323. @type readPipe: C{int}
  324. @return: code to be passed to C{os._exit}: 0 for success, 1 for error.
  325. @rtype: C{int}
  326. """
  327. data = untilConcludes(os.read, readPipe, 100)
  328. dataRepr = repr(data[2:])
  329. if data != b"0":
  330. msg = (
  331. "An error has occurred: {}\nPlease look at log "
  332. "file for more information.\n".format(dataRepr)
  333. )
  334. untilConcludes(sys.__stderr__.write, msg)
  335. return 1
  336. return 0
  337. def shedPrivileges(self, euid, uid, gid):
  338. """
  339. Change the UID and GID or the EUID and EGID of this process.
  340. @type euid: C{bool}
  341. @param euid: A flag which, if set, indicates that only the I{effective}
  342. UID and GID should be set.
  343. @type uid: C{int} or L{None}
  344. @param uid: If not L{None}, the UID to which to switch.
  345. @type gid: C{int} or L{None}
  346. @param gid: If not L{None}, the GID to which to switch.
  347. """
  348. if uid is not None or gid is not None:
  349. extra = euid and "e" or ""
  350. desc = f"{extra}uid/{extra}gid {uid}/{gid}"
  351. try:
  352. switchUID(uid, gid, euid)
  353. except OSError as e:
  354. log.msg(
  355. "failed to set {}: {} (are you root?) -- "
  356. "exiting.".format(desc, e)
  357. )
  358. sys.exit(1)
  359. else:
  360. log.msg(f"set {desc}")
  361. def startApplication(self, application):
  362. """
  363. Configure global process state based on the given application and run
  364. the application.
  365. @param application: An object which can be adapted to
  366. L{service.IProcess} and L{service.IService}.
  367. """
  368. process = service.IProcess(application)
  369. if not self.config["originalname"]:
  370. launchWithName(process.processName)
  371. self.setupEnvironment(
  372. self.config["chroot"],
  373. self.config["rundir"],
  374. self.config["nodaemon"],
  375. self.config["umask"],
  376. self.config["pidfile"],
  377. )
  378. service.IService(application).privilegedStartService()
  379. uid, gid = self.config["uid"], self.config["gid"]
  380. if uid is None:
  381. uid = process.uid
  382. if gid is None:
  383. gid = process.gid
  384. if uid is not None and gid is None:
  385. gid = pwd.getpwuid(uid).pw_gid
  386. self.shedPrivileges(self.config["euid"], uid, gid)
  387. app.startApplication(application, not self.config["no_save"])