test_twistd.py 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tests for L{twisted.application.app} and L{twisted.scripts.twistd}.
  5. """
  6. from __future__ import absolute_import, division
  7. import errno
  8. import inspect
  9. import signal
  10. import os
  11. import sys
  12. try:
  13. import pwd
  14. import grp
  15. except ImportError:
  16. pwd = grp = None
  17. try:
  18. import cPickle as pickle
  19. except ImportError:
  20. import pickle
  21. from zope.interface import implementer
  22. from zope.interface.verify import verifyObject
  23. from twisted.trial import unittest
  24. from twisted.test.test_process import MockOS
  25. from twisted import plugin, logger
  26. from twisted.application.service import IServiceMaker
  27. from twisted.application import service, app, reactors
  28. from twisted.scripts import twistd
  29. from twisted.python.compat import NativeStringIO, _PY3
  30. from twisted.python.usage import UsageError
  31. from twisted.python.log import (ILogObserver as LegacyILogObserver,
  32. textFromEventDict)
  33. from twisted.python.components import Componentized
  34. from twisted.internet.defer import Deferred
  35. from twisted.internet.interfaces import IReactorDaemonize
  36. from twisted.internet.test.modulehelpers import AlternateReactor
  37. from twisted.python.fakepwd import UserDatabase
  38. from twisted.logger import globalLogBeginner, globalLogPublisher, ILogObserver
  39. try:
  40. from twisted.scripts import _twistd_unix
  41. except ImportError:
  42. _twistd_unix = None
  43. else:
  44. from twisted.scripts._twistd_unix import checkPID
  45. from twisted.scripts._twistd_unix import UnixApplicationRunner
  46. from twisted.scripts._twistd_unix import UnixAppLogger
  47. try:
  48. from twisted.python import syslog
  49. except ImportError:
  50. syslog = None
  51. try:
  52. import profile
  53. except ImportError:
  54. profile = None
  55. try:
  56. import pstats
  57. import cProfile
  58. except ImportError:
  59. cProfile = None
  60. if getattr(os, 'setuid', None) is None:
  61. setuidSkip = "Platform does not support --uid/--gid twistd options."
  62. else:
  63. setuidSkip = None
  64. def patchUserDatabase(patch, user, uid, group, gid):
  65. """
  66. Patch L{pwd.getpwnam} so that it behaves as though only one user exists
  67. and patch L{grp.getgrnam} so that it behaves as though only one group
  68. exists.
  69. @param patch: A function like L{TestCase.patch} which will be used to
  70. install the fake implementations.
  71. @type user: C{str}
  72. @param user: The name of the single user which will exist.
  73. @type uid: C{int}
  74. @param uid: The UID of the single user which will exist.
  75. @type group: C{str}
  76. @param group: The name of the single user which will exist.
  77. @type gid: C{int}
  78. @param gid: The GID of the single group which will exist.
  79. """
  80. # Try not to be an unverified fake, but try not to depend on quirks of
  81. # the system either (eg, run as a process with a uid and gid which
  82. # equal each other, and so doesn't reliably test that uid is used where
  83. # uid should be used and gid is used where gid should be used). -exarkun
  84. pwent = pwd.getpwuid(os.getuid())
  85. grent = grp.getgrgid(os.getgid())
  86. database = UserDatabase()
  87. database.addUser(
  88. user, pwent.pw_passwd, uid, pwent.pw_gid,
  89. pwent.pw_gecos, pwent.pw_dir, pwent.pw_shell)
  90. def getgrnam(name):
  91. result = list(grent)
  92. result[result.index(grent.gr_name)] = group
  93. result[result.index(grent.gr_gid)] = gid
  94. result = tuple(result)
  95. return {group: result}[name]
  96. patch(pwd, "getpwnam", database.getpwnam)
  97. patch(grp, "getgrnam", getgrnam)
  98. class MockServiceMaker(object):
  99. """
  100. A non-implementation of L{twisted.application.service.IServiceMaker}.
  101. """
  102. tapname = 'ueoa'
  103. def makeService(self, options):
  104. """
  105. Take a L{usage.Options} instance and return a
  106. L{service.IService} provider.
  107. """
  108. self.options = options
  109. self.service = service.Service()
  110. return self.service
  111. class CrippledAppLogger(app.AppLogger):
  112. """
  113. @see: CrippledApplicationRunner.
  114. """
  115. def start(self, application):
  116. pass
  117. class CrippledApplicationRunner(twistd._SomeApplicationRunner):
  118. """
  119. An application runner that cripples the platform-specific runner and
  120. nasty side-effect-having code so that we can use it without actually
  121. running any environment-affecting code.
  122. """
  123. loggerFactory = CrippledAppLogger
  124. def preApplication(self):
  125. pass
  126. def postApplication(self):
  127. pass
  128. class ServerOptionsTests(unittest.TestCase):
  129. """
  130. Non-platform-specific tests for the platform-specific ServerOptions class.
  131. """
  132. def test_subCommands(self):
  133. """
  134. subCommands is built from IServiceMaker plugins, and is sorted
  135. alphabetically.
  136. """
  137. class FakePlugin(object):
  138. def __init__(self, name):
  139. self.tapname = name
  140. self._options = 'options for ' + name
  141. self.description = 'description of ' + name
  142. def options(self):
  143. return self._options
  144. apple = FakePlugin('apple')
  145. banana = FakePlugin('banana')
  146. coconut = FakePlugin('coconut')
  147. donut = FakePlugin('donut')
  148. def getPlugins(interface):
  149. self.assertEqual(interface, IServiceMaker)
  150. yield coconut
  151. yield banana
  152. yield donut
  153. yield apple
  154. config = twistd.ServerOptions()
  155. self.assertEqual(config._getPlugins, plugin.getPlugins)
  156. config._getPlugins = getPlugins
  157. # "subCommands is a list of 4-tuples of (command name, command
  158. # shortcut, parser class, documentation)."
  159. subCommands = config.subCommands
  160. expectedOrder = [apple, banana, coconut, donut]
  161. for subCommand, expectedCommand in zip(subCommands, expectedOrder):
  162. name, shortcut, parserClass, documentation = subCommand
  163. self.assertEqual(name, expectedCommand.tapname)
  164. self.assertIsNone(shortcut)
  165. self.assertEqual(parserClass(), expectedCommand._options),
  166. self.assertEqual(documentation, expectedCommand.description)
  167. def test_sortedReactorHelp(self):
  168. """
  169. Reactor names are listed alphabetically by I{--help-reactors}.
  170. """
  171. class FakeReactorInstaller(object):
  172. def __init__(self, name):
  173. self.shortName = 'name of ' + name
  174. self.description = 'description of ' + name
  175. self.moduleName = 'twisted.internet.default'
  176. apple = FakeReactorInstaller('apple')
  177. banana = FakeReactorInstaller('banana')
  178. coconut = FakeReactorInstaller('coconut')
  179. donut = FakeReactorInstaller('donut')
  180. def getReactorTypes():
  181. yield coconut
  182. yield banana
  183. yield donut
  184. yield apple
  185. config = twistd.ServerOptions()
  186. self.assertEqual(config._getReactorTypes, reactors.getReactorTypes)
  187. config._getReactorTypes = getReactorTypes
  188. config.messageOutput = NativeStringIO()
  189. self.assertRaises(SystemExit, config.parseOptions, ['--help-reactors'])
  190. helpOutput = config.messageOutput.getvalue()
  191. indexes = []
  192. for reactor in apple, banana, coconut, donut:
  193. def getIndex(s):
  194. self.assertIn(s, helpOutput)
  195. indexes.append(helpOutput.index(s))
  196. getIndex(reactor.shortName)
  197. getIndex(reactor.description)
  198. self.assertEqual(
  199. indexes, sorted(indexes),
  200. 'reactor descriptions were not in alphabetical order: %r' % (
  201. helpOutput,))
  202. def test_postOptionsSubCommandCausesNoSave(self):
  203. """
  204. postOptions should set no_save to True when a subcommand is used.
  205. """
  206. config = twistd.ServerOptions()
  207. config.subCommand = 'ueoa'
  208. config.postOptions()
  209. self.assertTrue(config['no_save'])
  210. def test_postOptionsNoSubCommandSavesAsUsual(self):
  211. """
  212. If no sub command is used, postOptions should not touch no_save.
  213. """
  214. config = twistd.ServerOptions()
  215. config.postOptions()
  216. self.assertFalse(config['no_save'])
  217. def test_listAllProfilers(self):
  218. """
  219. All the profilers that can be used in L{app.AppProfiler} are listed in
  220. the help output.
  221. """
  222. config = twistd.ServerOptions()
  223. helpOutput = str(config)
  224. for profiler in app.AppProfiler.profilers:
  225. self.assertIn(profiler, helpOutput)
  226. def test_defaultUmask(self):
  227. """
  228. The default value for the C{umask} option is L{None}.
  229. """
  230. config = twistd.ServerOptions()
  231. self.assertIsNone(config['umask'])
  232. def test_umask(self):
  233. """
  234. The value given for the C{umask} option is parsed as an octal integer
  235. literal.
  236. """
  237. config = twistd.ServerOptions()
  238. config.parseOptions(['--umask', '123'])
  239. self.assertEqual(config['umask'], 83)
  240. config.parseOptions(['--umask', '0123'])
  241. self.assertEqual(config['umask'], 83)
  242. def test_invalidUmask(self):
  243. """
  244. If a value is given for the C{umask} option which cannot be parsed as
  245. an integer, L{UsageError} is raised by L{ServerOptions.parseOptions}.
  246. """
  247. config = twistd.ServerOptions()
  248. self.assertRaises(UsageError, config.parseOptions,
  249. ['--umask', 'abcdef'])
  250. if _twistd_unix is None:
  251. msg = "twistd unix not available"
  252. test_defaultUmask.skip = test_umask.skip = test_invalidUmask.skip = msg
  253. def test_unimportableConfiguredLogObserver(self):
  254. """
  255. C{--logger} with an unimportable module raises a L{UsageError}.
  256. """
  257. config = twistd.ServerOptions()
  258. e = self.assertRaises(
  259. UsageError, config.parseOptions,
  260. ['--logger', 'no.such.module.I.hope'])
  261. self.assertTrue(
  262. e.args[0].startswith(
  263. "Logger 'no.such.module.I.hope' could not be imported: "
  264. "'no.such.module.I.hope' does not name an object"))
  265. self.assertNotIn('\n', e.args[0])
  266. def test_badAttributeWithConfiguredLogObserver(self):
  267. """
  268. C{--logger} with a non-existent object raises a L{UsageError}.
  269. """
  270. config = twistd.ServerOptions()
  271. e = self.assertRaises(UsageError, config.parseOptions,
  272. ["--logger", "twisted.test.test_twistd.FOOBAR"])
  273. if sys.version_info <= (3, 5):
  274. self.assertTrue(
  275. e.args[0].startswith(
  276. "Logger 'twisted.test.test_twistd.FOOBAR' could not be "
  277. "imported: 'module' object has no attribute 'FOOBAR'"))
  278. else:
  279. self.assertTrue(
  280. e.args[0].startswith(
  281. "Logger 'twisted.test.test_twistd.FOOBAR' could not be "
  282. "imported: module 'twisted.test.test_twistd' "
  283. "has no attribute 'FOOBAR'"))
  284. self.assertNotIn('\n', e.args[0])
  285. class CheckPIDTests(unittest.TestCase):
  286. """
  287. Tests for L{checkPID}.
  288. """
  289. if _twistd_unix is None:
  290. skip = "twistd unix not available"
  291. def test_notExists(self):
  292. """
  293. Nonexistent PID file is not an error.
  294. """
  295. self.patch(os.path, "exists", lambda _: False)
  296. checkPID("non-existent PID file")
  297. def test_nonNumeric(self):
  298. """
  299. Non-numeric content in a PID file causes a system exit.
  300. """
  301. pidfile = self.mktemp()
  302. with open(pidfile, "w") as f:
  303. f.write("non-numeric")
  304. e = self.assertRaises(SystemExit, checkPID, pidfile)
  305. self.assertIn("non-numeric value", e.code)
  306. def test_anotherRunning(self):
  307. """
  308. Another running twistd server causes a system exit.
  309. """
  310. pidfile = self.mktemp()
  311. with open(pidfile, "w") as f:
  312. f.write("42")
  313. def kill(pid, sig):
  314. pass
  315. self.patch(os, "kill", kill)
  316. e = self.assertRaises(SystemExit, checkPID, pidfile)
  317. self.assertIn("Another twistd server", e.code)
  318. def test_stale(self):
  319. """
  320. Stale PID file is removed without causing a system exit.
  321. """
  322. pidfile = self.mktemp()
  323. with open(pidfile, "w") as f:
  324. f.write(str(os.getpid() + 1))
  325. def kill(pid, sig):
  326. raise OSError(errno.ESRCH, "fake")
  327. self.patch(os, "kill", kill)
  328. checkPID(pidfile)
  329. self.assertFalse(os.path.exists(pidfile))
  330. class TapFileTests(unittest.TestCase):
  331. """
  332. Test twistd-related functionality that requires a tap file on disk.
  333. """
  334. def setUp(self):
  335. """
  336. Create a trivial Application and put it in a tap file on disk.
  337. """
  338. self.tapfile = self.mktemp()
  339. with open(self.tapfile, 'wb') as f:
  340. pickle.dump(service.Application("Hi!"), f)
  341. def test_createOrGetApplicationWithTapFile(self):
  342. """
  343. Ensure that the createOrGetApplication call that 'twistd -f foo.tap'
  344. makes will load the Application out of foo.tap.
  345. """
  346. config = twistd.ServerOptions()
  347. config.parseOptions(['-f', self.tapfile])
  348. application = CrippledApplicationRunner(
  349. config).createOrGetApplication()
  350. self.assertEqual(service.IService(application).name, 'Hi!')
  351. class TestLoggerFactory(object):
  352. """
  353. A logger factory for L{TestApplicationRunner}.
  354. """
  355. def __init__(self, runner):
  356. self.runner = runner
  357. def start(self, application):
  358. """
  359. Save the logging start on the C{runner} instance.
  360. """
  361. self.runner.order.append("log")
  362. self.runner.hadApplicationLogObserver = hasattr(self.runner,
  363. 'application')
  364. def stop(self):
  365. """
  366. Don't log anything.
  367. """
  368. class TestApplicationRunner(app.ApplicationRunner):
  369. """
  370. An ApplicationRunner which tracks the environment in which its methods are
  371. called.
  372. """
  373. def __init__(self, options):
  374. app.ApplicationRunner.__init__(self, options)
  375. self.order = []
  376. self.logger = TestLoggerFactory(self)
  377. def preApplication(self):
  378. self.order.append("pre")
  379. self.hadApplicationPreApplication = hasattr(self, 'application')
  380. def postApplication(self):
  381. self.order.append("post")
  382. self.hadApplicationPostApplication = hasattr(self, 'application')
  383. class ApplicationRunnerTests(unittest.TestCase):
  384. """
  385. Non-platform-specific tests for the platform-specific ApplicationRunner.
  386. """
  387. def setUp(self):
  388. config = twistd.ServerOptions()
  389. self.serviceMaker = MockServiceMaker()
  390. # Set up a config object like it's been parsed with a subcommand
  391. config.loadedPlugins = {'test_command': self.serviceMaker}
  392. config.subOptions = object()
  393. config.subCommand = 'test_command'
  394. self.config = config
  395. def test_applicationRunnerGetsCorrectApplication(self):
  396. """
  397. Ensure that a twistd plugin gets used in appropriate ways: it
  398. is passed its Options instance, and the service it returns is
  399. added to the application.
  400. """
  401. arunner = CrippledApplicationRunner(self.config)
  402. arunner.run()
  403. self.assertIs(
  404. self.serviceMaker.options, self.config.subOptions,
  405. "ServiceMaker.makeService needs to be passed the correct "
  406. "sub Command object.")
  407. self.assertIs(
  408. self.serviceMaker.service,
  409. service.IService(arunner.application).services[0],
  410. "ServiceMaker.makeService's result needs to be set as a child "
  411. "of the Application.")
  412. def test_preAndPostApplication(self):
  413. """
  414. Test thet preApplication and postApplication methods are
  415. called by ApplicationRunner.run() when appropriate.
  416. """
  417. s = TestApplicationRunner(self.config)
  418. s.run()
  419. self.assertFalse(s.hadApplicationPreApplication)
  420. self.assertTrue(s.hadApplicationPostApplication)
  421. self.assertTrue(s.hadApplicationLogObserver)
  422. self.assertEqual(s.order, ["pre", "log", "post"])
  423. def _applicationStartsWithConfiguredID(self, argv, uid, gid):
  424. """
  425. Assert that given a particular command line, an application is started
  426. as a particular UID/GID.
  427. @param argv: A list of strings giving the options to parse.
  428. @param uid: An integer giving the expected UID.
  429. @param gid: An integer giving the expected GID.
  430. """
  431. self.config.parseOptions(argv)
  432. events = []
  433. class FakeUnixApplicationRunner(twistd._SomeApplicationRunner):
  434. def setupEnvironment(self, chroot, rundir, nodaemon, umask,
  435. pidfile):
  436. events.append('environment')
  437. def shedPrivileges(self, euid, uid, gid):
  438. events.append(('privileges', euid, uid, gid))
  439. def startReactor(self, reactor, oldstdout, oldstderr):
  440. events.append('reactor')
  441. def removePID(self, pidfile):
  442. pass
  443. @implementer(service.IService, service.IProcess)
  444. class FakeService(object):
  445. processName = None
  446. uid = None
  447. gid = None
  448. def setName(self, name):
  449. pass
  450. def setServiceParent(self, parent):
  451. pass
  452. def disownServiceParent(self):
  453. pass
  454. def privilegedStartService(self):
  455. events.append('privilegedStartService')
  456. def startService(self):
  457. events.append('startService')
  458. def stopService(self):
  459. pass
  460. application = FakeService()
  461. verifyObject(service.IService, application)
  462. verifyObject(service.IProcess, application)
  463. runner = FakeUnixApplicationRunner(self.config)
  464. runner.preApplication()
  465. runner.application = application
  466. runner.postApplication()
  467. self.assertEqual(
  468. events,
  469. ['environment', 'privilegedStartService',
  470. ('privileges', False, uid, gid), 'startService', 'reactor'])
  471. def test_applicationStartsWithConfiguredNumericIDs(self):
  472. """
  473. L{postApplication} should change the UID and GID to the values
  474. specified as numeric strings by the configuration after running
  475. L{service.IService.privilegedStartService} and before running
  476. L{service.IService.startService}.
  477. """
  478. uid = 1234
  479. gid = 4321
  480. self._applicationStartsWithConfiguredID(
  481. ["--uid", str(uid), "--gid", str(gid)], uid, gid)
  482. test_applicationStartsWithConfiguredNumericIDs.skip = setuidSkip
  483. def test_applicationStartsWithConfiguredNameIDs(self):
  484. """
  485. L{postApplication} should change the UID and GID to the values
  486. specified as user and group names by the configuration after running
  487. L{service.IService.privilegedStartService} and before running
  488. L{service.IService.startService}.
  489. """
  490. user = "foo"
  491. uid = 1234
  492. group = "bar"
  493. gid = 4321
  494. patchUserDatabase(self.patch, user, uid, group, gid)
  495. self._applicationStartsWithConfiguredID(
  496. ["--uid", user, "--gid", group], uid, gid)
  497. test_applicationStartsWithConfiguredNameIDs.skip = setuidSkip
  498. def test_startReactorRunsTheReactor(self):
  499. """
  500. L{startReactor} calls L{reactor.run}.
  501. """
  502. reactor = DummyReactor()
  503. runner = app.ApplicationRunner({
  504. "profile": False,
  505. "profiler": "profile",
  506. "debug": False})
  507. runner.startReactor(reactor, None, None)
  508. self.assertTrue(
  509. reactor.called, "startReactor did not call reactor.run()")
  510. class UnixApplicationRunnerSetupEnvironmentTests(unittest.TestCase):
  511. """
  512. Tests for L{UnixApplicationRunner.setupEnvironment}.
  513. @ivar root: The root of the filesystem, or C{unset} if none has been
  514. specified with a call to L{os.chroot} (patched for this TestCase with
  515. L{UnixApplicationRunnerSetupEnvironmentTests.chroot}).
  516. @ivar cwd: The current working directory of the process, or C{unset} if
  517. none has been specified with a call to L{os.chdir} (patched for this
  518. TestCase with L{UnixApplicationRunnerSetupEnvironmentTests.chdir}).
  519. @ivar mask: The current file creation mask of the process, or C{unset} if
  520. none has been specified with a call to L{os.umask} (patched for this
  521. TestCase with L{UnixApplicationRunnerSetupEnvironmentTests.umask}).
  522. @ivar daemon: A boolean indicating whether daemonization has been performed
  523. by a call to L{_twistd_unix.daemonize} (patched for this TestCase with
  524. L{UnixApplicationRunnerSetupEnvironmentTests}.
  525. """
  526. if _twistd_unix is None:
  527. skip = "twistd unix not available"
  528. unset = object()
  529. def setUp(self):
  530. self.root = self.unset
  531. self.cwd = self.unset
  532. self.mask = self.unset
  533. self.daemon = False
  534. self.pid = os.getpid()
  535. self.patch(os, 'chroot', lambda path: setattr(self, 'root', path))
  536. self.patch(os, 'chdir', lambda path: setattr(self, 'cwd', path))
  537. self.patch(os, 'umask', lambda mask: setattr(self, 'mask', mask))
  538. self.runner = UnixApplicationRunner(twistd.ServerOptions())
  539. self.runner.daemonize = self.daemonize
  540. def daemonize(self, reactor):
  541. """
  542. Indicate that daemonization has happened and change the PID so that the
  543. value written to the pidfile can be tested in the daemonization case.
  544. """
  545. self.daemon = True
  546. self.patch(os, 'getpid', lambda: self.pid + 1)
  547. def test_chroot(self):
  548. """
  549. L{UnixApplicationRunner.setupEnvironment} changes the root of the
  550. filesystem if passed a non-L{None} value for the C{chroot} parameter.
  551. """
  552. self.runner.setupEnvironment("/foo/bar", ".", True, None, None)
  553. self.assertEqual(self.root, "/foo/bar")
  554. def test_noChroot(self):
  555. """
  556. L{UnixApplicationRunner.setupEnvironment} does not change the root of
  557. the filesystem if passed L{None} for the C{chroot} parameter.
  558. """
  559. self.runner.setupEnvironment(None, ".", True, None, None)
  560. self.assertIs(self.root, self.unset)
  561. def test_changeWorkingDirectory(self):
  562. """
  563. L{UnixApplicationRunner.setupEnvironment} changes the working directory
  564. of the process to the path given for the C{rundir} parameter.
  565. """
  566. self.runner.setupEnvironment(None, "/foo/bar", True, None, None)
  567. self.assertEqual(self.cwd, "/foo/bar")
  568. def test_daemonize(self):
  569. """
  570. L{UnixApplicationRunner.setupEnvironment} daemonizes the process if
  571. C{False} is passed for the C{nodaemon} parameter.
  572. """
  573. with AlternateReactor(FakeDaemonizingReactor()):
  574. self.runner.setupEnvironment(None, ".", False, None, None)
  575. self.assertTrue(self.daemon)
  576. def test_noDaemonize(self):
  577. """
  578. L{UnixApplicationRunner.setupEnvironment} does not daemonize the
  579. process if C{True} is passed for the C{nodaemon} parameter.
  580. """
  581. self.runner.setupEnvironment(None, ".", True, None, None)
  582. self.assertFalse(self.daemon)
  583. def test_nonDaemonPIDFile(self):
  584. """
  585. L{UnixApplicationRunner.setupEnvironment} writes the process's PID to
  586. the file specified by the C{pidfile} parameter.
  587. """
  588. pidfile = self.mktemp()
  589. self.runner.setupEnvironment(None, ".", True, None, pidfile)
  590. with open(pidfile, 'rb') as f:
  591. pid = int(f.read())
  592. self.assertEqual(pid, self.pid)
  593. def test_daemonPIDFile(self):
  594. """
  595. L{UnixApplicationRunner.setupEnvironment} writes the daemonized
  596. process's PID to the file specified by the C{pidfile} parameter if
  597. C{nodaemon} is C{False}.
  598. """
  599. pidfile = self.mktemp()
  600. with AlternateReactor(FakeDaemonizingReactor()):
  601. self.runner.setupEnvironment(None, ".", False, None, pidfile)
  602. with open(pidfile, 'rb') as f:
  603. pid = int(f.read())
  604. self.assertEqual(pid, self.pid + 1)
  605. def test_umask(self):
  606. """
  607. L{UnixApplicationRunner.setupEnvironment} changes the process umask to
  608. the value specified by the C{umask} parameter.
  609. """
  610. with AlternateReactor(FakeDaemonizingReactor()):
  611. self.runner.setupEnvironment(None, ".", False, 123, None)
  612. self.assertEqual(self.mask, 123)
  613. def test_noDaemonizeNoUmask(self):
  614. """
  615. L{UnixApplicationRunner.setupEnvironment} doesn't change the process
  616. umask if L{None} is passed for the C{umask} parameter and C{True} is
  617. passed for the C{nodaemon} parameter.
  618. """
  619. self.runner.setupEnvironment(None, ".", True, None, None)
  620. self.assertIs(self.mask, self.unset)
  621. def test_daemonizedNoUmask(self):
  622. """
  623. L{UnixApplicationRunner.setupEnvironment} changes the process umask to
  624. C{0077} if L{None} is passed for the C{umask} parameter and C{False} is
  625. passed for the C{nodaemon} parameter.
  626. """
  627. with AlternateReactor(FakeDaemonizingReactor()):
  628. self.runner.setupEnvironment(None, ".", False, None, None)
  629. self.assertEqual(self.mask, 0o077)
  630. class UnixApplicationRunnerStartApplicationTests(unittest.TestCase):
  631. """
  632. Tests for L{UnixApplicationRunner.startApplication}.
  633. """
  634. if _twistd_unix is None:
  635. skip = "twistd unix not available"
  636. def test_setupEnvironment(self):
  637. """
  638. L{UnixApplicationRunner.startApplication} calls
  639. L{UnixApplicationRunner.setupEnvironment} with the chroot, rundir,
  640. nodaemon, umask, and pidfile parameters from the configuration it is
  641. constructed with.
  642. """
  643. options = twistd.ServerOptions()
  644. options.parseOptions([
  645. '--nodaemon',
  646. '--umask', '0070',
  647. '--chroot', '/foo/chroot',
  648. '--rundir', '/foo/rundir',
  649. '--pidfile', '/foo/pidfile'])
  650. application = service.Application("test_setupEnvironment")
  651. self.runner = UnixApplicationRunner(options)
  652. args = []
  653. def fakeSetupEnvironment(self, chroot, rundir, nodaemon, umask,
  654. pidfile):
  655. args.extend((chroot, rundir, nodaemon, umask, pidfile))
  656. # Sanity check
  657. if _PY3:
  658. setupEnvironmentParameters = \
  659. inspect.signature(self.runner.setupEnvironment).parameters
  660. fakeSetupEnvironmentParameters = \
  661. inspect.signature(fakeSetupEnvironment).parameters
  662. # inspect.signature() does not return "self" in the signature of
  663. # a class method, so we need to omit it when comparing the
  664. # the signature of a plain method
  665. fakeSetupEnvironmentParameters = fakeSetupEnvironmentParameters.copy()
  666. fakeSetupEnvironmentParameters.pop("self")
  667. self.assertEqual(setupEnvironmentParameters,
  668. fakeSetupEnvironmentParameters)
  669. else:
  670. self.assertEqual(
  671. inspect.getargspec(self.runner.setupEnvironment),
  672. inspect.getargspec(fakeSetupEnvironment))
  673. self.patch(UnixApplicationRunner, 'setupEnvironment',
  674. fakeSetupEnvironment)
  675. self.patch(UnixApplicationRunner, 'shedPrivileges',
  676. lambda *a, **kw: None)
  677. self.patch(app, 'startApplication', lambda *a, **kw: None)
  678. self.runner.startApplication(application)
  679. self.assertEqual(
  680. args,
  681. ['/foo/chroot', '/foo/rundir', True, 56, '/foo/pidfile'])
  682. class UnixApplicationRunnerRemovePIDTests(unittest.TestCase):
  683. """
  684. Tests for L{UnixApplicationRunner.removePID}.
  685. """
  686. if _twistd_unix is None:
  687. skip = "twistd unix not available"
  688. def test_removePID(self):
  689. """
  690. L{UnixApplicationRunner.removePID} deletes the file the name of
  691. which is passed to it.
  692. """
  693. runner = UnixApplicationRunner({})
  694. path = self.mktemp()
  695. os.makedirs(path)
  696. pidfile = os.path.join(path, "foo.pid")
  697. open(pidfile, "w").close()
  698. runner.removePID(pidfile)
  699. self.assertFalse(os.path.exists(pidfile))
  700. def test_removePIDErrors(self):
  701. """
  702. Calling L{UnixApplicationRunner.removePID} with a non-existent filename
  703. logs an OSError.
  704. """
  705. runner = UnixApplicationRunner({})
  706. runner.removePID("fakepid")
  707. errors = self.flushLoggedErrors(OSError)
  708. self.assertEqual(len(errors), 1)
  709. self.assertEqual(errors[0].value.errno, errno.ENOENT)
  710. class FakeNonDaemonizingReactor(object):
  711. """
  712. A dummy reactor, providing C{beforeDaemonize} and C{afterDaemonize}
  713. methods, but not announcing this, and logging whether the methods have been
  714. called.
  715. @ivar _beforeDaemonizeCalled: if C{beforeDaemonize} has been called or not.
  716. @type _beforeDaemonizeCalled: C{bool}
  717. @ivar _afterDaemonizeCalled: if C{afterDaemonize} has been called or not.
  718. @type _afterDaemonizeCalled: C{bool}
  719. """
  720. def __init__(self):
  721. self._beforeDaemonizeCalled = False
  722. self._afterDaemonizeCalled = False
  723. def beforeDaemonize(self):
  724. self._beforeDaemonizeCalled = True
  725. def afterDaemonize(self):
  726. self._afterDaemonizeCalled = True
  727. def addSystemEventTrigger(self, *args, **kw):
  728. """
  729. Skip event registration.
  730. """
  731. @implementer(IReactorDaemonize)
  732. class FakeDaemonizingReactor(FakeNonDaemonizingReactor):
  733. """
  734. A dummy reactor, providing C{beforeDaemonize} and C{afterDaemonize}
  735. methods, announcing this, and logging whether the methods have been called.
  736. """
  737. class DummyReactor(object):
  738. """
  739. A dummy reactor, only providing a C{run} method and checking that it
  740. has been called.
  741. @ivar called: if C{run} has been called or not.
  742. @type called: C{bool}
  743. """
  744. called = False
  745. def run(self):
  746. """
  747. A fake run method, checking that it's been called one and only time.
  748. """
  749. if self.called:
  750. raise RuntimeError("Already called")
  751. self.called = True
  752. class AppProfilingTests(unittest.TestCase):
  753. """
  754. Tests for L{app.AppProfiler}.
  755. """
  756. def test_profile(self):
  757. """
  758. L{app.ProfileRunner.run} should call the C{run} method of the reactor
  759. and save profile data in the specified file.
  760. """
  761. config = twistd.ServerOptions()
  762. config["profile"] = self.mktemp()
  763. config["profiler"] = "profile"
  764. profiler = app.AppProfiler(config)
  765. reactor = DummyReactor()
  766. profiler.run(reactor)
  767. self.assertTrue(reactor.called)
  768. with open(config["profile"]) as f:
  769. data = f.read()
  770. self.assertIn("DummyReactor.run", data)
  771. self.assertIn("function calls", data)
  772. if profile is None:
  773. test_profile.skip = "profile module not available"
  774. def _testStats(self, statsClass, profile):
  775. out = NativeStringIO()
  776. # Patch before creating the pstats, because pstats binds self.stream to
  777. # sys.stdout early in 2.5 and newer.
  778. stdout = self.patch(sys, 'stdout', out)
  779. # If pstats.Stats can load the data and then reformat it, then the
  780. # right thing probably happened.
  781. stats = statsClass(profile)
  782. stats.print_stats()
  783. stdout.restore()
  784. data = out.getvalue()
  785. self.assertIn("function calls", data)
  786. self.assertIn("(run)", data)
  787. def test_profileSaveStats(self):
  788. """
  789. With the C{savestats} option specified, L{app.ProfileRunner.run}
  790. should save the raw stats object instead of a summary output.
  791. """
  792. config = twistd.ServerOptions()
  793. config["profile"] = self.mktemp()
  794. config["profiler"] = "profile"
  795. config["savestats"] = True
  796. profiler = app.AppProfiler(config)
  797. reactor = DummyReactor()
  798. profiler.run(reactor)
  799. self.assertTrue(reactor.called)
  800. self._testStats(pstats.Stats, config['profile'])
  801. if profile is None:
  802. test_profileSaveStats.skip = "profile module not available"
  803. def test_withoutProfile(self):
  804. """
  805. When the C{profile} module is not present, L{app.ProfilerRunner.run}
  806. should raise a C{SystemExit} exception.
  807. """
  808. savedModules = sys.modules.copy()
  809. config = twistd.ServerOptions()
  810. config["profiler"] = "profile"
  811. profiler = app.AppProfiler(config)
  812. sys.modules["profile"] = None
  813. try:
  814. self.assertRaises(SystemExit, profiler.run, None)
  815. finally:
  816. sys.modules.clear()
  817. sys.modules.update(savedModules)
  818. def test_profilePrintStatsError(self):
  819. """
  820. When an error happens during the print of the stats, C{sys.stdout}
  821. should be restored to its initial value.
  822. """
  823. class ErroneousProfile(profile.Profile):
  824. def print_stats(self):
  825. raise RuntimeError("Boom")
  826. self.patch(profile, "Profile", ErroneousProfile)
  827. config = twistd.ServerOptions()
  828. config["profile"] = self.mktemp()
  829. config["profiler"] = "profile"
  830. profiler = app.AppProfiler(config)
  831. reactor = DummyReactor()
  832. oldStdout = sys.stdout
  833. self.assertRaises(RuntimeError, profiler.run, reactor)
  834. self.assertIs(sys.stdout, oldStdout)
  835. if profile is None:
  836. test_profilePrintStatsError.skip = "profile module not available"
  837. def test_cProfile(self):
  838. """
  839. L{app.CProfileRunner.run} should call the C{run} method of the
  840. reactor and save profile data in the specified file.
  841. """
  842. config = twistd.ServerOptions()
  843. config["profile"] = self.mktemp()
  844. config["profiler"] = "cProfile"
  845. profiler = app.AppProfiler(config)
  846. reactor = DummyReactor()
  847. profiler.run(reactor)
  848. self.assertTrue(reactor.called)
  849. with open(config["profile"]) as f:
  850. data = f.read()
  851. self.assertIn("run", data)
  852. self.assertIn("function calls", data)
  853. if cProfile is None:
  854. test_cProfile.skip = "cProfile module not available"
  855. def test_cProfileSaveStats(self):
  856. """
  857. With the C{savestats} option specified,
  858. L{app.CProfileRunner.run} should save the raw stats object
  859. instead of a summary output.
  860. """
  861. config = twistd.ServerOptions()
  862. config["profile"] = self.mktemp()
  863. config["profiler"] = "cProfile"
  864. config["savestats"] = True
  865. profiler = app.AppProfiler(config)
  866. reactor = DummyReactor()
  867. profiler.run(reactor)
  868. self.assertTrue(reactor.called)
  869. self._testStats(pstats.Stats, config['profile'])
  870. if cProfile is None:
  871. test_cProfileSaveStats.skip = "cProfile module not available"
  872. def test_withoutCProfile(self):
  873. """
  874. When the C{cProfile} module is not present,
  875. L{app.CProfileRunner.run} should raise a C{SystemExit}
  876. exception and log the C{ImportError}.
  877. """
  878. savedModules = sys.modules.copy()
  879. sys.modules["cProfile"] = None
  880. config = twistd.ServerOptions()
  881. config["profiler"] = "cProfile"
  882. profiler = app.AppProfiler(config)
  883. try:
  884. self.assertRaises(SystemExit, profiler.run, None)
  885. finally:
  886. sys.modules.clear()
  887. sys.modules.update(savedModules)
  888. def test_unknownProfiler(self):
  889. """
  890. Check that L{app.AppProfiler} raises L{SystemExit} when given an
  891. unknown profiler name.
  892. """
  893. config = twistd.ServerOptions()
  894. config["profile"] = self.mktemp()
  895. config["profiler"] = "foobar"
  896. error = self.assertRaises(SystemExit, app.AppProfiler, config)
  897. self.assertEqual(str(error), "Unsupported profiler name: foobar")
  898. def test_defaultProfiler(self):
  899. """
  900. L{app.Profiler} defaults to the cprofile profiler if not specified.
  901. """
  902. profiler = app.AppProfiler({})
  903. self.assertEqual(profiler.profiler, "cprofile")
  904. def test_profilerNameCaseInsentive(self):
  905. """
  906. The case of the profiler name passed to L{app.AppProfiler} is not
  907. relevant.
  908. """
  909. profiler = app.AppProfiler({"profiler": "CprOfile"})
  910. self.assertEqual(profiler.profiler, "cprofile")
  911. def _patchTextFileLogObserver(patch):
  912. """
  913. Patch L{logger.textFileLogObserver} to record every call and keep a
  914. reference to the passed log file for tests.
  915. @param patch: a callback for patching (usually L{unittest.TestCase.patch}).
  916. @return: the list that keeps track of the log files.
  917. @rtype: C{list}
  918. """
  919. logFiles = []
  920. oldFileLogObserver = logger.textFileLogObserver
  921. def observer(logFile, *args, **kwargs):
  922. logFiles.append(logFile)
  923. return oldFileLogObserver(logFile, *args, **kwargs)
  924. patch(logger, 'textFileLogObserver', observer)
  925. return logFiles
  926. def _setupSyslog(testCase):
  927. """
  928. Make fake syslog, and return list to which prefix and then log
  929. messages will be appended if it is used.
  930. """
  931. logMessages = []
  932. class fakesyslogobserver(object):
  933. def __init__(self, prefix):
  934. logMessages.append(prefix)
  935. def emit(self, eventDict):
  936. logMessages.append(eventDict)
  937. testCase.patch(syslog, "SyslogObserver", fakesyslogobserver)
  938. return logMessages
  939. class AppLoggerTests(unittest.TestCase):
  940. """
  941. Tests for L{app.AppLogger}.
  942. @ivar observers: list of observers installed during the tests.
  943. @type observers: C{list}
  944. """
  945. def setUp(self):
  946. """
  947. Override L{globaLogBeginner.beginLoggingTo} so that we can trace the
  948. observers installed in C{self.observers}.
  949. """
  950. self.observers = []
  951. def beginLoggingTo(observers):
  952. for observer in observers:
  953. self.observers.append(observer)
  954. globalLogPublisher.addObserver(observer)
  955. self.patch(globalLogBeginner, 'beginLoggingTo', beginLoggingTo)
  956. def tearDown(self):
  957. """
  958. Remove all installed observers.
  959. """
  960. for observer in self.observers:
  961. globalLogPublisher.removeObserver(observer)
  962. def _makeObserver(self):
  963. """
  964. Make a new observer which captures all logs sent to it.
  965. @return: An observer that stores all logs sent to it.
  966. @rtype: Callable that implements L{ILogObserver}.
  967. """
  968. @implementer(ILogObserver)
  969. class TestObserver(object):
  970. _logs = []
  971. def __call__(self, event):
  972. self._logs.append(event)
  973. return TestObserver()
  974. def _checkObserver(self, observer):
  975. """
  976. Ensure that initial C{twistd} logs are written to logs.
  977. @param observer: The observer made by L{self._makeObserver).
  978. """
  979. self.assertEqual(self.observers, [observer])
  980. self.assertIn("starting up", observer._logs[0]["log_format"])
  981. self.assertIn("reactor class", observer._logs[1]["log_format"])
  982. def test_start(self):
  983. """
  984. L{app.AppLogger.start} calls L{globalLogBeginner.addObserver}, and then
  985. writes some messages about twistd and the reactor.
  986. """
  987. logger = app.AppLogger({})
  988. observer = self._makeObserver()
  989. logger._getLogObserver = lambda: observer
  990. logger.start(Componentized())
  991. self._checkObserver(observer)
  992. def test_startUsesApplicationLogObserver(self):
  993. """
  994. When the L{ILogObserver} component is available on the application,
  995. that object will be used as the log observer instead of constructing a
  996. new one.
  997. """
  998. application = Componentized()
  999. observer = self._makeObserver()
  1000. application.setComponent(ILogObserver, observer)
  1001. logger = app.AppLogger({})
  1002. logger.start(application)
  1003. self._checkObserver(observer)
  1004. def _setupConfiguredLogger(self, application, extraLogArgs={},
  1005. appLogger=app.AppLogger):
  1006. """
  1007. Set up an AppLogger which exercises the C{logger} configuration option.
  1008. @type application: L{Componentized}
  1009. @param application: The L{Application} object to pass to
  1010. L{app.AppLogger.start}.
  1011. @type extraLogArgs: C{dict}
  1012. @param extraLogArgs: extra values to pass to AppLogger.
  1013. @type appLogger: L{AppLogger} class, or a subclass
  1014. @param appLogger: factory for L{AppLogger} instances.
  1015. @rtype: C{list}
  1016. @return: The logs accumulated by the log observer.
  1017. """
  1018. observer = self._makeObserver()
  1019. logArgs = {"logger": lambda: observer}
  1020. logArgs.update(extraLogArgs)
  1021. logger = appLogger(logArgs)
  1022. logger.start(application)
  1023. return observer
  1024. def test_startUsesConfiguredLogObserver(self):
  1025. """
  1026. When the C{logger} key is specified in the configuration dictionary
  1027. (i.e., when C{--logger} is passed to twistd), the initial log observer
  1028. will be the log observer returned from the callable which the value
  1029. refers to in FQPN form.
  1030. """
  1031. application = Componentized()
  1032. self._checkObserver(self._setupConfiguredLogger(application))
  1033. def test_configuredLogObserverBeatsComponent(self):
  1034. """
  1035. C{--logger} takes precedence over a L{ILogObserver} component set on
  1036. Application.
  1037. """
  1038. observer = self._makeObserver()
  1039. application = Componentized()
  1040. application.setComponent(ILogObserver, observer)
  1041. self._checkObserver(self._setupConfiguredLogger(application))
  1042. self.assertEqual(observer._logs, [])
  1043. def test_configuredLogObserverBeatsLegacyComponent(self):
  1044. """
  1045. C{--logger} takes precedence over a L{LegacyILogObserver} component
  1046. set on Application.
  1047. """
  1048. nonlogs = []
  1049. application = Componentized()
  1050. application.setComponent(LegacyILogObserver, nonlogs.append)
  1051. self._checkObserver(self._setupConfiguredLogger(application))
  1052. self.assertEqual(nonlogs, [])
  1053. def test_loggerComponentBeatsLegacyLoggerComponent(self):
  1054. """
  1055. A L{ILogObserver} takes precedence over a L{LegacyILogObserver}
  1056. component set on Application.
  1057. """
  1058. nonlogs = []
  1059. observer = self._makeObserver()
  1060. application = Componentized()
  1061. application.setComponent(ILogObserver, observer)
  1062. application.setComponent(LegacyILogObserver, nonlogs.append)
  1063. logger = app.AppLogger({})
  1064. logger.start(application)
  1065. self._checkObserver(observer)
  1066. self.assertEqual(nonlogs, [])
  1067. def test_configuredLogObserverBeatsSyslog(self):
  1068. """
  1069. C{--logger} takes precedence over a C{--syslog} command line
  1070. argument.
  1071. """
  1072. logs = _setupSyslog(self)
  1073. application = Componentized()
  1074. self._checkObserver(self._setupConfiguredLogger(application,
  1075. {"syslog": True},
  1076. UnixAppLogger))
  1077. self.assertEqual(logs, [])
  1078. if _twistd_unix is None or syslog is None:
  1079. test_configuredLogObserverBeatsSyslog.skip = (
  1080. "Not on POSIX, or syslog not available."
  1081. )
  1082. def test_configuredLogObserverBeatsLogfile(self):
  1083. """
  1084. C{--logger} takes precedence over a C{--logfile} command line
  1085. argument.
  1086. """
  1087. application = Componentized()
  1088. path = self.mktemp()
  1089. self._checkObserver(self._setupConfiguredLogger(application,
  1090. {"logfile": "path"}))
  1091. self.assertFalse(os.path.exists(path))
  1092. def test_getLogObserverStdout(self):
  1093. """
  1094. When logfile is empty or set to C{-}, L{app.AppLogger._getLogObserver}
  1095. returns a log observer pointing at C{sys.stdout}.
  1096. """
  1097. logger = app.AppLogger({"logfile": "-"})
  1098. logFiles = _patchTextFileLogObserver(self.patch)
  1099. logger._getLogObserver()
  1100. self.assertEqual(len(logFiles), 1)
  1101. self.assertIs(logFiles[0], sys.stdout)
  1102. logger = app.AppLogger({"logfile": ""})
  1103. logger._getLogObserver()
  1104. self.assertEqual(len(logFiles), 2)
  1105. self.assertIs(logFiles[1], sys.stdout)
  1106. def test_getLogObserverFile(self):
  1107. """
  1108. When passing the C{logfile} option, L{app.AppLogger._getLogObserver}
  1109. returns a log observer pointing at the specified path.
  1110. """
  1111. logFiles = _patchTextFileLogObserver(self.patch)
  1112. filename = self.mktemp()
  1113. logger = app.AppLogger({"logfile": filename})
  1114. logger._getLogObserver()
  1115. self.assertEqual(len(logFiles), 1)
  1116. self.assertEqual(logFiles[0].path,
  1117. os.path.abspath(filename))
  1118. def test_stop(self):
  1119. """
  1120. L{app.AppLogger.stop} removes the observer created in C{start}, and
  1121. reinitialize its C{_observer} so that if C{stop} is called several
  1122. times it doesn't break.
  1123. """
  1124. removed = []
  1125. observer = object()
  1126. def remove(observer):
  1127. removed.append(observer)
  1128. self.patch(globalLogPublisher, 'removeObserver', remove)
  1129. logger = app.AppLogger({})
  1130. logger._observer = observer
  1131. logger.stop()
  1132. self.assertEqual(removed, [observer])
  1133. logger.stop()
  1134. self.assertEqual(removed, [observer])
  1135. self.assertIsNone(logger._observer)
  1136. def test_legacyObservers(self):
  1137. """
  1138. L{app.AppLogger} using a legacy logger observer still works, wrapping
  1139. it in a compat shim.
  1140. """
  1141. logs = []
  1142. logger = app.AppLogger({})
  1143. @implementer(LegacyILogObserver)
  1144. class LoggerObserver(object):
  1145. """
  1146. An observer which implements the legacy L{LegacyILogObserver}.
  1147. """
  1148. def __call__(self, x):
  1149. """
  1150. Add C{x} to the logs list.
  1151. """
  1152. logs.append(x)
  1153. logger._observerFactory = lambda: LoggerObserver()
  1154. logger.start(Componentized())
  1155. self.assertIn("starting up", textFromEventDict(logs[0]))
  1156. warnings = self.flushWarnings(
  1157. [self.test_legacyObservers])
  1158. self.assertEqual(len(warnings), 0)
  1159. def test_unmarkedObserversDeprecated(self):
  1160. """
  1161. L{app.AppLogger} using a logger observer which does not implement
  1162. L{ILogObserver} or L{LegacyILogObserver} will be wrapped in a compat
  1163. shim and raise a L{DeprecationWarning}.
  1164. """
  1165. logs = []
  1166. logger = app.AppLogger({})
  1167. logger._getLogObserver = lambda: logs.append
  1168. logger.start(Componentized())
  1169. self.assertIn("starting up", textFromEventDict(logs[0]))
  1170. warnings = self.flushWarnings(
  1171. [self.test_unmarkedObserversDeprecated])
  1172. self.assertEqual(len(warnings), 1)
  1173. self.assertEqual(warnings[0]["message"],
  1174. ("Passing a logger factory which makes log observers "
  1175. "which do not implement twisted.logger.ILogObserver "
  1176. "or twisted.python.log.ILogObserver to "
  1177. "twisted.application.app.AppLogger was deprecated "
  1178. "in Twisted 16.2. Please use a factory that "
  1179. "produces twisted.logger.ILogObserver (or the "
  1180. "legacy twisted.python.log.ILogObserver) "
  1181. "implementing objects instead."))
  1182. class UnixAppLoggerTests(unittest.TestCase):
  1183. """
  1184. Tests for L{UnixAppLogger}.
  1185. @ivar signals: list of signal handlers installed.
  1186. @type signals: C{list}
  1187. """
  1188. if _twistd_unix is None:
  1189. skip = "twistd unix not available"
  1190. def setUp(self):
  1191. """
  1192. Fake C{signal.signal} for not installing the handlers but saving them
  1193. in C{self.signals}.
  1194. """
  1195. self.signals = []
  1196. def fakeSignal(sig, f):
  1197. self.signals.append((sig, f))
  1198. self.patch(signal, "signal", fakeSignal)
  1199. def test_getLogObserverStdout(self):
  1200. """
  1201. When non-daemonized and C{logfile} is empty or set to C{-},
  1202. L{UnixAppLogger._getLogObserver} returns a log observer pointing at
  1203. C{sys.stdout}.
  1204. """
  1205. logFiles = _patchTextFileLogObserver(self.patch)
  1206. logger = UnixAppLogger({"logfile": "-", "nodaemon": True})
  1207. logger._getLogObserver()
  1208. self.assertEqual(len(logFiles), 1)
  1209. self.assertIs(logFiles[0], sys.stdout)
  1210. logger = UnixAppLogger({"logfile": "", "nodaemon": True})
  1211. logger._getLogObserver()
  1212. self.assertEqual(len(logFiles), 2)
  1213. self.assertIs(logFiles[1], sys.stdout)
  1214. def test_getLogObserverStdoutDaemon(self):
  1215. """
  1216. When daemonized and C{logfile} is set to C{-},
  1217. L{UnixAppLogger._getLogObserver} raises C{SystemExit}.
  1218. """
  1219. logger = UnixAppLogger({"logfile": "-", "nodaemon": False})
  1220. error = self.assertRaises(SystemExit, logger._getLogObserver)
  1221. self.assertEqual(str(error), "Daemons cannot log to stdout, exiting!")
  1222. def test_getLogObserverFile(self):
  1223. """
  1224. When C{logfile} contains a file name, L{app.AppLogger._getLogObserver}
  1225. returns a log observer pointing at the specified path, and a signal
  1226. handler rotating the log is installed.
  1227. """
  1228. logFiles = _patchTextFileLogObserver(self.patch)
  1229. filename = self.mktemp()
  1230. logger = UnixAppLogger({"logfile": filename})
  1231. logger._getLogObserver()
  1232. self.assertEqual(len(logFiles), 1)
  1233. self.assertEqual(logFiles[0].path, os.path.abspath(filename))
  1234. self.assertEqual(len(self.signals), 1)
  1235. self.assertEqual(self.signals[0][0], signal.SIGUSR1)
  1236. d = Deferred()
  1237. def rotate():
  1238. d.callback(None)
  1239. logFiles[0].rotate = rotate
  1240. rotateLog = self.signals[0][1]
  1241. rotateLog(None, None)
  1242. return d
  1243. def test_getLogObserverDontOverrideSignalHandler(self):
  1244. """
  1245. If a signal handler is already installed,
  1246. L{UnixAppLogger._getLogObserver} doesn't override it.
  1247. """
  1248. def fakeGetSignal(sig):
  1249. self.assertEqual(sig, signal.SIGUSR1)
  1250. return object()
  1251. self.patch(signal, "getsignal", fakeGetSignal)
  1252. filename = self.mktemp()
  1253. logger = UnixAppLogger({"logfile": filename})
  1254. logger._getLogObserver()
  1255. self.assertEqual(self.signals, [])
  1256. def test_getLogObserverDefaultFile(self):
  1257. """
  1258. When daemonized and C{logfile} is empty, the observer returned by
  1259. L{UnixAppLogger._getLogObserver} points at C{twistd.log} in the current
  1260. directory.
  1261. """
  1262. logFiles = _patchTextFileLogObserver(self.patch)
  1263. logger = UnixAppLogger({"logfile": "", "nodaemon": False})
  1264. logger._getLogObserver()
  1265. self.assertEqual(len(logFiles), 1)
  1266. self.assertEqual(logFiles[0].path, os.path.abspath("twistd.log"))
  1267. def test_getLogObserverSyslog(self):
  1268. """
  1269. If C{syslog} is set to C{True}, L{UnixAppLogger._getLogObserver} starts
  1270. a L{syslog.SyslogObserver} with given C{prefix}.
  1271. """
  1272. logs = _setupSyslog(self)
  1273. logger = UnixAppLogger({"syslog": True, "prefix": "test-prefix"})
  1274. observer = logger._getLogObserver()
  1275. self.assertEqual(logs, ["test-prefix"])
  1276. observer({"a": "b"})
  1277. self.assertEqual(logs, ["test-prefix", {"a": "b"}])
  1278. if syslog is None:
  1279. test_getLogObserverSyslog.skip = "Syslog not available"
  1280. class DaemonizeTests(unittest.TestCase):
  1281. """
  1282. Tests for L{_twistd_unix.UnixApplicationRunner} daemonization.
  1283. """
  1284. def setUp(self):
  1285. self.mockos = MockOS()
  1286. self.config = twistd.ServerOptions()
  1287. self.patch(_twistd_unix, 'os', self.mockos)
  1288. self.runner = _twistd_unix.UnixApplicationRunner(self.config)
  1289. self.runner.application = service.Application("Hi!")
  1290. self.runner.oldstdout = sys.stdout
  1291. self.runner.oldstderr = sys.stderr
  1292. self.runner.startReactor = lambda *args: None
  1293. def test_success(self):
  1294. """
  1295. When double fork succeeded in C{daemonize}, the child process writes
  1296. B{0} to the status pipe.
  1297. """
  1298. with AlternateReactor(FakeDaemonizingReactor()):
  1299. self.runner.postApplication()
  1300. self.assertEqual(
  1301. self.mockos.actions,
  1302. [('chdir', '.'), ('umask', 0o077), ('fork', True), 'setsid',
  1303. ('fork', True), ('write', -2, b'0'), ('unlink', 'twistd.pid')])
  1304. self.assertEqual(self.mockos.closed, [-3, -2])
  1305. def test_successInParent(self):
  1306. """
  1307. The parent process initiating the C{daemonize} call reads data from the
  1308. status pipe and then exit the process.
  1309. """
  1310. self.mockos.child = False
  1311. self.mockos.readData = b"0"
  1312. with AlternateReactor(FakeDaemonizingReactor()):
  1313. self.assertRaises(SystemError, self.runner.postApplication)
  1314. self.assertEqual(
  1315. self.mockos.actions,
  1316. [('chdir', '.'), ('umask', 0o077), ('fork', True),
  1317. ('read', -1, 100), ('exit', 0), ('unlink', 'twistd.pid')])
  1318. self.assertEqual(self.mockos.closed, [-1])
  1319. def test_successEINTR(self):
  1320. """
  1321. If the C{os.write} call to the status pipe raises an B{EINTR} error,
  1322. the process child retries to write.
  1323. """
  1324. written = []
  1325. def raisingWrite(fd, data):
  1326. written.append((fd, data))
  1327. if len(written) == 1:
  1328. raise IOError(errno.EINTR)
  1329. self.mockos.write = raisingWrite
  1330. with AlternateReactor(FakeDaemonizingReactor()):
  1331. self.runner.postApplication()
  1332. self.assertEqual(
  1333. self.mockos.actions,
  1334. [('chdir', '.'), ('umask', 0o077), ('fork', True), 'setsid',
  1335. ('fork', True), ('unlink', 'twistd.pid')])
  1336. self.assertEqual(self.mockos.closed, [-3, -2])
  1337. self.assertEqual([(-2, b'0'), (-2, b'0')], written)
  1338. def test_successInParentEINTR(self):
  1339. """
  1340. If the C{os.read} call on the status pipe raises an B{EINTR} error, the
  1341. parent child retries to read.
  1342. """
  1343. read = []
  1344. def raisingRead(fd, size):
  1345. read.append((fd, size))
  1346. if len(read) == 1:
  1347. raise IOError(errno.EINTR)
  1348. return b"0"
  1349. self.mockos.read = raisingRead
  1350. self.mockos.child = False
  1351. with AlternateReactor(FakeDaemonizingReactor()):
  1352. self.assertRaises(SystemError, self.runner.postApplication)
  1353. self.assertEqual(
  1354. self.mockos.actions,
  1355. [('chdir', '.'), ('umask', 0o077), ('fork', True),
  1356. ('exit', 0), ('unlink', 'twistd.pid')])
  1357. self.assertEqual(self.mockos.closed, [-1])
  1358. self.assertEqual([(-1, 100), (-1, 100)], read)
  1359. def assertErrorWritten(self, raised, reported):
  1360. """
  1361. Assert L{UnixApplicationRunner.postApplication} writes
  1362. C{reported} to its status pipe if the service raises an
  1363. exception whose message is C{raised}.
  1364. """
  1365. class FakeService(service.Service):
  1366. def startService(self):
  1367. raise RuntimeError(raised)
  1368. errorService = FakeService()
  1369. errorService.setServiceParent(self.runner.application)
  1370. with AlternateReactor(FakeDaemonizingReactor()):
  1371. self.assertRaises(RuntimeError, self.runner.postApplication)
  1372. self.assertEqual(
  1373. self.mockos.actions,
  1374. [('chdir', '.'), ('umask', 0o077), ('fork', True), 'setsid',
  1375. ('fork', True), ('write', -2, reported),
  1376. ('unlink', 'twistd.pid')])
  1377. self.assertEqual(self.mockos.closed, [-3, -2])
  1378. def test_error(self):
  1379. """
  1380. If an error happens during daemonization, the child process writes the
  1381. exception error to the status pipe.
  1382. """
  1383. self.assertErrorWritten(raised="Something is wrong",
  1384. reported=b'1 RuntimeError: Something is wrong')
  1385. def test_unicodeError(self):
  1386. """
  1387. If an error happens during daemonization, and that error's
  1388. message is Unicode, the child encodes the message as ascii
  1389. with backslash Unicode code points.
  1390. """
  1391. self.assertErrorWritten(raised=u"\u2022",
  1392. reported=b'1 RuntimeError: \\u2022')
  1393. def assertErrorInParentBehavior(self, readData, errorMessage,
  1394. mockOSActions):
  1395. """
  1396. Make L{os.read} appear to return C{readData}, and assert that
  1397. L{UnixApplicationRunner.postApplication} writes
  1398. C{errorMessage} to standard error and executes the calls
  1399. against L{os} functions specified in C{mockOSActions}.
  1400. """
  1401. self.mockos.child = False
  1402. self.mockos.readData = readData
  1403. errorIO = NativeStringIO()
  1404. self.patch(sys, '__stderr__', errorIO)
  1405. with AlternateReactor(FakeDaemonizingReactor()):
  1406. self.assertRaises(SystemError, self.runner.postApplication)
  1407. self.assertEqual(errorIO.getvalue(), errorMessage)
  1408. self.assertEqual(self.mockos.actions, mockOSActions)
  1409. self.assertEqual(self.mockos.closed, [-1])
  1410. def test_errorInParent(self):
  1411. """
  1412. When the child writes an error message to the status pipe
  1413. during daemonization, the parent writes the repr of the
  1414. message to C{stderr} and exits with non-zero status code.
  1415. """
  1416. self.assertErrorInParentBehavior(
  1417. readData=b"1 Exception: An identified error",
  1418. errorMessage=(
  1419. "An error has occurred: b'Exception: An identified error'\n"
  1420. "Please look at log file for more information.\n"),
  1421. mockOSActions=[
  1422. ('chdir', '.'), ('umask', 0o077), ('fork', True),
  1423. ('read', -1, 100), ('exit', 1), ('unlink', 'twistd.pid'),
  1424. ],
  1425. )
  1426. def test_nonASCIIErrorInParent(self):
  1427. """
  1428. When the child writes a non-ASCII error message to the status
  1429. pipe during daemonization, the parent writes the repr of the
  1430. message to C{stderr} and exits with a non-zero status code.
  1431. """
  1432. self.assertErrorInParentBehavior(
  1433. readData=b"1 Exception: \xff",
  1434. errorMessage=(
  1435. "An error has occurred: b'Exception: \\xff'\n"
  1436. "Please look at log file for more information.\n"
  1437. ),
  1438. mockOSActions=[
  1439. ('chdir', '.'), ('umask', 0o077), ('fork', True),
  1440. ('read', -1, 100), ('exit', 1), ('unlink', 'twistd.pid'),
  1441. ],
  1442. )
  1443. def test_errorInParentWithTruncatedUnicode(self):
  1444. """
  1445. When the child writes a non-ASCII error message to the status
  1446. pipe during daemonization, and that message is too longer, the
  1447. parent writes the repr of the truncated message to C{stderr}
  1448. and exits with a non-zero status code.
  1449. """
  1450. truncatedMessage = b'1 RuntimeError: ' + b'\\u2022' * 14
  1451. # the escape sequence will appear to be escaped twice, because
  1452. # we're getting the repr
  1453. reportedMessage = "b'RuntimeError: {}'".format(r'\\u2022' * 14)
  1454. self.assertErrorInParentBehavior(
  1455. readData=truncatedMessage,
  1456. errorMessage=(
  1457. "An error has occurred: {}\n"
  1458. "Please look at log file for more information.\n".format(
  1459. reportedMessage)
  1460. ),
  1461. mockOSActions=[
  1462. ('chdir', '.'), ('umask', 0o077), ('fork', True),
  1463. ('read', -1, 100), ('exit', 1), ('unlink', 'twistd.pid'),
  1464. ],
  1465. )
  1466. def test_errorMessageTruncated(self):
  1467. """
  1468. If an error occurs during daemonization and its message is too
  1469. long, it's truncated by the child.
  1470. """
  1471. self.assertErrorWritten(
  1472. raised="x" * 200,
  1473. reported=b'1 RuntimeError: ' + b'x' * 84)
  1474. def test_unicodeErrorMessageTruncated(self):
  1475. """
  1476. If an error occurs during daemonization and its message is
  1477. unicode and too long, it's truncated by the child, even if
  1478. this splits a unicode escape sequence.
  1479. """
  1480. self.assertErrorWritten(
  1481. raised=u"\u2022" * 30,
  1482. reported=b'1 RuntimeError: ' + b'\\u2022' * 14,
  1483. )
  1484. def test_hooksCalled(self):
  1485. """
  1486. C{daemonize} indeed calls L{IReactorDaemonize.beforeDaemonize} and
  1487. L{IReactorDaemonize.afterDaemonize} if the reactor implements
  1488. L{IReactorDaemonize}.
  1489. """
  1490. reactor = FakeDaemonizingReactor()
  1491. self.runner.daemonize(reactor)
  1492. self.assertTrue(reactor._beforeDaemonizeCalled)
  1493. self.assertTrue(reactor._afterDaemonizeCalled)
  1494. def test_hooksNotCalled(self):
  1495. """
  1496. C{daemonize} does NOT call L{IReactorDaemonize.beforeDaemonize} or
  1497. L{IReactorDaemonize.afterDaemonize} if the reactor does NOT implement
  1498. L{IReactorDaemonize}.
  1499. """
  1500. reactor = FakeNonDaemonizingReactor()
  1501. self.runner.daemonize(reactor)
  1502. self.assertFalse(reactor._beforeDaemonizeCalled)
  1503. self.assertFalse(reactor._afterDaemonizeCalled)
  1504. if _twistd_unix is None:
  1505. DaemonizeTests.skip = "twistd unix support not available"