_twistd_unix.py 16 KB

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