1# Copyright (c) Twisted Matrix Laboratories.
2# See LICENSE for details.
3
4"""
5Tests for L{twisted.application.app} and L{twisted.scripts.twistd}.
6"""
7
8
9import errno
10import inspect
11import os
12import pickle
13import signal
14import sys
15
16try:
17    import grp as _grp
18    import pwd as _pwd
19except ImportError:
20    pwd = None
21    grp = None
22else:
23    pwd = _pwd
24    grp = _grp
25
26from io import StringIO
27from unittest import skipIf
28
29from zope.interface import implementer
30from zope.interface.verify import verifyObject
31
32from twisted import internet, logger, plugin
33from twisted.application import app, reactors, service
34from twisted.application.service import IServiceMaker
35from twisted.internet.base import ReactorBase
36from twisted.internet.defer import Deferred
37from twisted.internet.interfaces import IReactorDaemonize, _ISupportsExitSignalCapturing
38from twisted.internet.test.modulehelpers import AlternateReactor
39from twisted.logger import ILogObserver, globalLogBeginner, globalLogPublisher
40from twisted.python import util
41from twisted.python.components import Componentized
42from twisted.python.fakepwd import UserDatabase
43from twisted.python.log import ILogObserver as LegacyILogObserver, textFromEventDict
44from twisted.python.reflect import requireModule
45from twisted.python.runtime import platformType
46from twisted.python.usage import UsageError
47from twisted.scripts import twistd
48from twisted.test.proto_helpers import MemoryReactor
49from twisted.test.test_process import MockOS
50from twisted.trial.unittest import TestCase
51
52_twistd_unix = requireModule("twistd.scripts._twistd_unix")
53if _twistd_unix:
54    from twisted.scripts._twistd_unix import (
55        UnixApplicationRunner,
56        UnixAppLogger,
57        checkPID,
58    )
59
60
61syslog = requireModule("twistd.python.syslog")
62profile = requireModule("profile")
63pstats = requireModule("pstats")
64cProfile = requireModule("cProfile")
65
66
67def patchUserDatabase(patch, user, uid, group, gid):
68    """
69    Patch L{pwd.getpwnam} so that it behaves as though only one user exists
70    and patch L{grp.getgrnam} so that it behaves as though only one group
71    exists.
72
73    @param patch: A function like L{TestCase.patch} which will be used to
74        install the fake implementations.
75
76    @type user: C{str}
77    @param user: The name of the single user which will exist.
78
79    @type uid: C{int}
80    @param uid: The UID of the single user which will exist.
81
82    @type group: C{str}
83    @param group: The name of the single user which will exist.
84
85    @type gid: C{int}
86    @param gid: The GID of the single group which will exist.
87    """
88    # Try not to be an unverified fake, but try not to depend on quirks of
89    # the system either (eg, run as a process with a uid and gid which
90    # equal each other, and so doesn't reliably test that uid is used where
91    # uid should be used and gid is used where gid should be used). -exarkun
92    pwent = pwd.getpwuid(os.getuid())
93    grent = grp.getgrgid(os.getgid())
94
95    database = UserDatabase()
96    database.addUser(
97        user, pwent.pw_passwd, uid, gid, pwent.pw_gecos, pwent.pw_dir, pwent.pw_shell
98    )
99
100    def getgrnam(name):
101        result = list(grent)
102        result[result.index(grent.gr_name)] = group
103        result[result.index(grent.gr_gid)] = gid
104        result = tuple(result)
105        return {group: result}[name]
106
107    patch(pwd, "getpwnam", database.getpwnam)
108    patch(grp, "getgrnam", getgrnam)
109    patch(pwd, "getpwuid", database.getpwuid)
110
111
112class MockServiceMaker:
113    """
114    A non-implementation of L{twisted.application.service.IServiceMaker}.
115    """
116
117    tapname = "ueoa"
118
119    def makeService(self, options):
120        """
121        Take a L{usage.Options} instance and return a
122        L{service.IService} provider.
123        """
124        self.options = options
125        self.service = service.Service()
126        return self.service
127
128
129class CrippledAppLogger(app.AppLogger):
130    """
131    @see: CrippledApplicationRunner.
132    """
133
134    def start(self, application):
135        pass
136
137
138class CrippledApplicationRunner(twistd._SomeApplicationRunner):
139    """
140    An application runner that cripples the platform-specific runner and
141    nasty side-effect-having code so that we can use it without actually
142    running any environment-affecting code.
143    """
144
145    loggerFactory = CrippledAppLogger
146
147    def preApplication(self):
148        pass
149
150    def postApplication(self):
151        pass
152
153
154class ServerOptionsTests(TestCase):
155    """
156    Non-platform-specific tests for the platform-specific ServerOptions class.
157    """
158
159    def test_subCommands(self):
160        """
161        subCommands is built from IServiceMaker plugins, and is sorted
162        alphabetically.
163        """
164
165        class FakePlugin:
166            def __init__(self, name):
167                self.tapname = name
168                self._options = "options for " + name
169                self.description = "description of " + name
170
171            def options(self):
172                return self._options
173
174        apple = FakePlugin("apple")
175        banana = FakePlugin("banana")
176        coconut = FakePlugin("coconut")
177        donut = FakePlugin("donut")
178
179        def getPlugins(interface):
180            self.assertEqual(interface, IServiceMaker)
181            yield coconut
182            yield banana
183            yield donut
184            yield apple
185
186        config = twistd.ServerOptions()
187        self.assertEqual(config._getPlugins, plugin.getPlugins)
188        config._getPlugins = getPlugins
189
190        # "subCommands is a list of 4-tuples of (command name, command
191        # shortcut, parser class, documentation)."
192        subCommands = config.subCommands
193        expectedOrder = [apple, banana, coconut, donut]
194
195        for subCommand, expectedCommand in zip(subCommands, expectedOrder):
196            name, shortcut, parserClass, documentation = subCommand
197            self.assertEqual(name, expectedCommand.tapname)
198            self.assertIsNone(shortcut)
199            self.assertEqual(parserClass(), expectedCommand._options),
200            self.assertEqual(documentation, expectedCommand.description)
201
202    def test_sortedReactorHelp(self):
203        """
204        Reactor names are listed alphabetically by I{--help-reactors}.
205        """
206
207        class FakeReactorInstaller:
208            def __init__(self, name):
209                self.shortName = "name of " + name
210                self.description = "description of " + name
211                self.moduleName = "twisted.internet.default"
212
213        apple = FakeReactorInstaller("apple")
214        banana = FakeReactorInstaller("banana")
215        coconut = FakeReactorInstaller("coconut")
216        donut = FakeReactorInstaller("donut")
217
218        def getReactorTypes():
219            yield coconut
220            yield banana
221            yield donut
222            yield apple
223
224        config = twistd.ServerOptions()
225        self.assertEqual(config._getReactorTypes, reactors.getReactorTypes)
226        config._getReactorTypes = getReactorTypes
227        config.messageOutput = StringIO()
228
229        self.assertRaises(SystemExit, config.parseOptions, ["--help-reactors"])
230        helpOutput = config.messageOutput.getvalue()
231        indexes = []
232        for reactor in apple, banana, coconut, donut:
233
234            def getIndex(s):
235                self.assertIn(s, helpOutput)
236                indexes.append(helpOutput.index(s))
237
238            getIndex(reactor.shortName)
239            getIndex(reactor.description)
240
241        self.assertEqual(
242            indexes,
243            sorted(indexes),
244            "reactor descriptions were not in alphabetical order: {!r}".format(
245                helpOutput
246            ),
247        )
248
249    def test_postOptionsSubCommandCausesNoSave(self):
250        """
251        postOptions should set no_save to True when a subcommand is used.
252        """
253        config = twistd.ServerOptions()
254        config.subCommand = "ueoa"
255        config.postOptions()
256        self.assertTrue(config["no_save"])
257
258    def test_postOptionsNoSubCommandSavesAsUsual(self):
259        """
260        If no sub command is used, postOptions should not touch no_save.
261        """
262        config = twistd.ServerOptions()
263        config.postOptions()
264        self.assertFalse(config["no_save"])
265
266    def test_listAllProfilers(self):
267        """
268        All the profilers that can be used in L{app.AppProfiler} are listed in
269        the help output.
270        """
271        config = twistd.ServerOptions()
272        helpOutput = str(config)
273        for profiler in app.AppProfiler.profilers:
274            self.assertIn(profiler, helpOutput)
275
276    @skipIf(not _twistd_unix, "twistd unix not available")
277    def test_defaultUmask(self):
278        """
279        The default value for the C{umask} option is L{None}.
280        """
281        config = twistd.ServerOptions()
282        self.assertIsNone(config["umask"])
283
284    @skipIf(not _twistd_unix, "twistd unix not available")
285    def test_umask(self):
286        """
287        The value given for the C{umask} option is parsed as an octal integer
288        literal.
289        """
290        config = twistd.ServerOptions()
291        config.parseOptions(["--umask", "123"])
292        self.assertEqual(config["umask"], 83)
293        config.parseOptions(["--umask", "0123"])
294        self.assertEqual(config["umask"], 83)
295
296    @skipIf(not _twistd_unix, "twistd unix not available")
297    def test_invalidUmask(self):
298        """
299        If a value is given for the C{umask} option which cannot be parsed as
300        an integer, L{UsageError} is raised by L{ServerOptions.parseOptions}.
301        """
302        config = twistd.ServerOptions()
303        self.assertRaises(UsageError, config.parseOptions, ["--umask", "abcdef"])
304
305    def test_unimportableConfiguredLogObserver(self):
306        """
307        C{--logger} with an unimportable module raises a L{UsageError}.
308        """
309        config = twistd.ServerOptions()
310        e = self.assertRaises(
311            UsageError, config.parseOptions, ["--logger", "no.such.module.I.hope"]
312        )
313        self.assertTrue(
314            e.args[0].startswith(
315                "Logger 'no.such.module.I.hope' could not be imported: "
316                "'no.such.module.I.hope' does not name an object"
317            )
318        )
319        self.assertNotIn("\n", e.args[0])
320
321    def test_badAttributeWithConfiguredLogObserver(self):
322        """
323        C{--logger} with a non-existent object raises a L{UsageError}.
324        """
325        config = twistd.ServerOptions()
326        e = self.assertRaises(
327            UsageError,
328            config.parseOptions,
329            ["--logger", "twisted.test.test_twistd.FOOBAR"],
330        )
331        self.assertTrue(
332            e.args[0].startswith(
333                "Logger 'twisted.test.test_twistd.FOOBAR' could not be "
334                "imported: module 'twisted.test.test_twistd' "
335                "has no attribute 'FOOBAR'"
336            )
337        )
338        self.assertNotIn("\n", e.args[0])
339
340    def test_version(self):
341        """
342        C{--version} prints the version.
343        """
344        from twisted import copyright
345
346        if platformType == "win32":
347            name = "(the Twisted Windows runner)"
348        else:
349            name = "(the Twisted daemon)"
350        expectedOutput = "twistd {} {}\n{}\n".format(
351            name, copyright.version, copyright.copyright
352        )
353
354        stdout = StringIO()
355        config = twistd.ServerOptions(stdout=stdout)
356        e = self.assertRaises(SystemExit, config.parseOptions, ["--version"])
357        self.assertIs(e.code, None)
358        self.assertEqual(stdout.getvalue(), expectedOutput)
359
360    def test_printSubCommandForUsageError(self):
361        """
362        Command is printed when an invalid option is requested.
363        """
364        stdout = StringIO()
365        config = twistd.ServerOptions(stdout=stdout)
366
367        self.assertRaises(UsageError, config.parseOptions, ["web --foo"])
368
369
370@skipIf(not _twistd_unix, "twistd unix not available")
371class CheckPIDTests(TestCase):
372    """
373    Tests for L{checkPID}.
374    """
375
376    def test_notExists(self):
377        """
378        Nonexistent PID file is not an error.
379        """
380        self.patch(os.path, "exists", lambda _: False)
381        checkPID("non-existent PID file")
382
383    def test_nonNumeric(self):
384        """
385        Non-numeric content in a PID file causes a system exit.
386        """
387        pidfile = self.mktemp()
388        with open(pidfile, "w") as f:
389            f.write("non-numeric")
390        e = self.assertRaises(SystemExit, checkPID, pidfile)
391        self.assertIn("non-numeric value", e.code)
392
393    def test_anotherRunning(self):
394        """
395        Another running twistd server causes a system exit.
396        """
397        pidfile = self.mktemp()
398        with open(pidfile, "w") as f:
399            f.write("42")
400
401        def kill(pid, sig):
402            pass
403
404        self.patch(os, "kill", kill)
405        e = self.assertRaises(SystemExit, checkPID, pidfile)
406        self.assertIn("Another twistd server", e.code)
407
408    def test_stale(self):
409        """
410        Stale PID file is removed without causing a system exit.
411        """
412        pidfile = self.mktemp()
413        with open(pidfile, "w") as f:
414            f.write(str(os.getpid() + 1))
415
416        def kill(pid, sig):
417            raise OSError(errno.ESRCH, "fake")
418
419        self.patch(os, "kill", kill)
420        checkPID(pidfile)
421        self.assertFalse(os.path.exists(pidfile))
422
423    def test_unexpectedOSError(self):
424        """
425        An unexpected L{OSError} when checking the validity of a
426        PID in a C{pidfile} terminates the process via L{SystemExit}.
427        """
428        pidfile = self.mktemp()
429        with open(pidfile, "w") as f:
430            f.write("3581")
431
432        def kill(pid, sig):
433            raise OSError(errno.EBADF, "fake")
434
435        self.patch(os, "kill", kill)
436        e = self.assertRaises(SystemExit, checkPID, pidfile)
437        self.assertIsNot(e.code, None)
438        self.assertTrue(e.args[0].startswith("Can't check status of PID"))
439
440
441class TapFileTests(TestCase):
442    """
443    Test twistd-related functionality that requires a tap file on disk.
444    """
445
446    def setUp(self):
447        """
448        Create a trivial Application and put it in a tap file on disk.
449        """
450        self.tapfile = self.mktemp()
451        with open(self.tapfile, "wb") as f:
452            pickle.dump(service.Application("Hi!"), f)
453
454    def test_createOrGetApplicationWithTapFile(self):
455        """
456        Ensure that the createOrGetApplication call that 'twistd -f foo.tap'
457        makes will load the Application out of foo.tap.
458        """
459        config = twistd.ServerOptions()
460        config.parseOptions(["-f", self.tapfile])
461        application = CrippledApplicationRunner(config).createOrGetApplication()
462        self.assertEqual(service.IService(application).name, "Hi!")
463
464
465class TestLoggerFactory:
466    """
467    A logger factory for L{TestApplicationRunner}.
468    """
469
470    def __init__(self, runner):
471        self.runner = runner
472
473    def start(self, application):
474        """
475        Save the logging start on the C{runner} instance.
476        """
477        self.runner.order.append("log")
478        self.runner.hadApplicationLogObserver = hasattr(self.runner, "application")
479
480    def stop(self):
481        """
482        Don't log anything.
483        """
484
485
486class TestApplicationRunner(app.ApplicationRunner):
487    """
488    An ApplicationRunner which tracks the environment in which its methods are
489    called.
490    """
491
492    def __init__(self, options):
493        app.ApplicationRunner.__init__(self, options)
494        self.order = []
495        self.logger = TestLoggerFactory(self)
496
497    def preApplication(self):
498        self.order.append("pre")
499        self.hadApplicationPreApplication = hasattr(self, "application")
500
501    def postApplication(self):
502        self.order.append("post")
503        self.hadApplicationPostApplication = hasattr(self, "application")
504
505
506class ApplicationRunnerTests(TestCase):
507    """
508    Non-platform-specific tests for the platform-specific ApplicationRunner.
509    """
510
511    def setUp(self):
512        config = twistd.ServerOptions()
513        self.serviceMaker = MockServiceMaker()
514        # Set up a config object like it's been parsed with a subcommand
515        config.loadedPlugins = {"test_command": self.serviceMaker}
516        config.subOptions = object()
517        config.subCommand = "test_command"
518        self.config = config
519
520    def test_applicationRunnerGetsCorrectApplication(self):
521        """
522        Ensure that a twistd plugin gets used in appropriate ways: it
523        is passed its Options instance, and the service it returns is
524        added to the application.
525        """
526        arunner = CrippledApplicationRunner(self.config)
527        arunner.run()
528
529        self.assertIs(
530            self.serviceMaker.options,
531            self.config.subOptions,
532            "ServiceMaker.makeService needs to be passed the correct "
533            "sub Command object.",
534        )
535        self.assertIs(
536            self.serviceMaker.service,
537            service.IService(arunner.application).services[0],
538            "ServiceMaker.makeService's result needs to be set as a child "
539            "of the Application.",
540        )
541
542    def test_preAndPostApplication(self):
543        """
544        Test thet preApplication and postApplication methods are
545        called by ApplicationRunner.run() when appropriate.
546        """
547        s = TestApplicationRunner(self.config)
548        s.run()
549        self.assertFalse(s.hadApplicationPreApplication)
550        self.assertTrue(s.hadApplicationPostApplication)
551        self.assertTrue(s.hadApplicationLogObserver)
552        self.assertEqual(s.order, ["pre", "log", "post"])
553
554    def _applicationStartsWithConfiguredID(self, argv, uid, gid):
555        """
556        Assert that given a particular command line, an application is started
557        as a particular UID/GID.
558
559        @param argv: A list of strings giving the options to parse.
560        @param uid: An integer giving the expected UID.
561        @param gid: An integer giving the expected GID.
562        """
563        self.config.parseOptions(argv)
564
565        events = []
566
567        class FakeUnixApplicationRunner(twistd._SomeApplicationRunner):
568            def setupEnvironment(self, chroot, rundir, nodaemon, umask, pidfile):
569                events.append("environment")
570
571            def shedPrivileges(self, euid, uid, gid):
572                events.append(("privileges", euid, uid, gid))
573
574            def startReactor(self, reactor, oldstdout, oldstderr):
575                events.append("reactor")
576
577            def removePID(self, pidfile):
578                pass
579
580        @implementer(service.IService, service.IProcess)
581        class FakeService:
582
583            parent = None
584            running = None
585            name = None
586            processName = None
587            uid = None
588            gid = None
589
590            def setName(self, name):
591                pass
592
593            def setServiceParent(self, parent):
594                pass
595
596            def disownServiceParent(self):
597                pass
598
599            def privilegedStartService(self):
600                events.append("privilegedStartService")
601
602            def startService(self):
603                events.append("startService")
604
605            def stopService(self):
606                pass
607
608        application = FakeService()
609        verifyObject(service.IService, application)
610        verifyObject(service.IProcess, application)
611
612        runner = FakeUnixApplicationRunner(self.config)
613        runner.preApplication()
614        runner.application = application
615        runner.postApplication()
616
617        self.assertEqual(
618            events,
619            [
620                "environment",
621                "privilegedStartService",
622                ("privileges", False, uid, gid),
623                "startService",
624                "reactor",
625            ],
626        )
627
628    @skipIf(
629        not getattr(os, "setuid", None),
630        "Platform does not support --uid/--gid twistd options.",
631    )
632    def test_applicationStartsWithConfiguredNumericIDs(self):
633        """
634        L{postApplication} should change the UID and GID to the values
635        specified as numeric strings by the configuration after running
636        L{service.IService.privilegedStartService} and before running
637        L{service.IService.startService}.
638        """
639        uid = 1234
640        gid = 4321
641        self._applicationStartsWithConfiguredID(
642            ["--uid", str(uid), "--gid", str(gid)], uid, gid
643        )
644
645    @skipIf(
646        not getattr(os, "setuid", None),
647        "Platform does not support --uid/--gid twistd options.",
648    )
649    def test_applicationStartsWithConfiguredNameIDs(self):
650        """
651        L{postApplication} should change the UID and GID to the values
652        specified as user and group names by the configuration after running
653        L{service.IService.privilegedStartService} and before running
654        L{service.IService.startService}.
655        """
656        user = "foo"
657        uid = 1234
658        group = "bar"
659        gid = 4321
660        patchUserDatabase(self.patch, user, uid, group, gid)
661        self._applicationStartsWithConfiguredID(
662            ["--uid", user, "--gid", group], uid, gid
663        )
664
665    def test_startReactorRunsTheReactor(self):
666        """
667        L{startReactor} calls L{reactor.run}.
668        """
669        reactor = DummyReactor()
670        runner = app.ApplicationRunner(
671            {"profile": False, "profiler": "profile", "debug": False}
672        )
673        runner.startReactor(reactor, None, None)
674        self.assertTrue(reactor.called, "startReactor did not call reactor.run()")
675
676    def test_applicationRunnerChoosesReactorIfNone(self):
677        """
678        L{ApplicationRunner} chooses a reactor if none is specified.
679        """
680        reactor = DummyReactor()
681        self.patch(internet, "reactor", reactor)
682        runner = app.ApplicationRunner(
683            {"profile": False, "profiler": "profile", "debug": False}
684        )
685        runner.startReactor(None, None, None)
686        self.assertTrue(reactor.called)
687
688    def test_applicationRunnerCapturesSignal(self):
689        """
690        If the reactor exits with a signal, the application runner caches
691        the signal.
692        """
693
694        class DummyReactorWithSignal(ReactorBase):
695            """
696            A dummy reactor, providing a C{run} method, and setting the
697            _exitSignal attribute to a nonzero value.
698            """
699
700            def installWaker(self):
701                """
702                Dummy method, does nothing.
703                """
704
705            def run(self):
706                """
707                A fake run method setting _exitSignal to a nonzero value
708                """
709                self._exitSignal = 2
710
711        reactor = DummyReactorWithSignal()
712        runner = app.ApplicationRunner(
713            {"profile": False, "profiler": "profile", "debug": False}
714        )
715        runner.startReactor(reactor, None, None)
716        self.assertEquals(2, runner._exitSignal)
717
718    def test_applicationRunnerIgnoresNoSignal(self):
719        """
720        The runner sets its _exitSignal instance attribute to None if
721        the reactor does not implement L{_ISupportsExitSignalCapturing}.
722        """
723
724        class DummyReactorWithExitSignalAttribute:
725            """
726            A dummy reactor, providing a C{run} method, and setting the
727            _exitSignal attribute to a nonzero value.
728            """
729
730            def installWaker(self):
731                """
732                Dummy method, does nothing.
733                """
734
735            def run(self):
736                """
737                A fake run method setting _exitSignal to a nonzero value
738                that should be ignored.
739                """
740                self._exitSignal = 2
741
742        reactor = DummyReactorWithExitSignalAttribute()
743        runner = app.ApplicationRunner(
744            {"profile": False, "profiler": "profile", "debug": False}
745        )
746        runner.startReactor(reactor, None, None)
747        self.assertEquals(None, runner._exitSignal)
748
749
750@skipIf(not _twistd_unix, "twistd unix not available")
751class UnixApplicationRunnerSetupEnvironmentTests(TestCase):
752    """
753    Tests for L{UnixApplicationRunner.setupEnvironment}.
754
755    @ivar root: The root of the filesystem, or C{unset} if none has been
756        specified with a call to L{os.chroot} (patched for this TestCase with
757        L{UnixApplicationRunnerSetupEnvironmentTests.chroot}).
758
759    @ivar cwd: The current working directory of the process, or C{unset} if
760        none has been specified with a call to L{os.chdir} (patched for this
761        TestCase with L{UnixApplicationRunnerSetupEnvironmentTests.chdir}).
762
763    @ivar mask: The current file creation mask of the process, or C{unset} if
764        none has been specified with a call to L{os.umask} (patched for this
765        TestCase with L{UnixApplicationRunnerSetupEnvironmentTests.umask}).
766
767    @ivar daemon: A boolean indicating whether daemonization has been performed
768        by a call to L{_twistd_unix.daemonize} (patched for this TestCase with
769        L{UnixApplicationRunnerSetupEnvironmentTests}.
770    """
771
772    unset = object()
773
774    def setUp(self):
775        self.root = self.unset
776        self.cwd = self.unset
777        self.mask = self.unset
778        self.daemon = False
779        self.pid = os.getpid()
780        self.patch(os, "chroot", lambda path: setattr(self, "root", path))
781        self.patch(os, "chdir", lambda path: setattr(self, "cwd", path))
782        self.patch(os, "umask", lambda mask: setattr(self, "mask", mask))
783        self.runner = UnixApplicationRunner(twistd.ServerOptions())
784        self.runner.daemonize = self.daemonize
785
786    def daemonize(self, reactor):
787        """
788        Indicate that daemonization has happened and change the PID so that the
789        value written to the pidfile can be tested in the daemonization case.
790        """
791        self.daemon = True
792        self.patch(os, "getpid", lambda: self.pid + 1)
793
794    def test_chroot(self):
795        """
796        L{UnixApplicationRunner.setupEnvironment} changes the root of the
797        filesystem if passed a non-L{None} value for the C{chroot} parameter.
798        """
799        self.runner.setupEnvironment("/foo/bar", ".", True, None, None)
800        self.assertEqual(self.root, "/foo/bar")
801
802    def test_noChroot(self):
803        """
804        L{UnixApplicationRunner.setupEnvironment} does not change the root of
805        the filesystem if passed L{None} for the C{chroot} parameter.
806        """
807        self.runner.setupEnvironment(None, ".", True, None, None)
808        self.assertIs(self.root, self.unset)
809
810    def test_changeWorkingDirectory(self):
811        """
812        L{UnixApplicationRunner.setupEnvironment} changes the working directory
813        of the process to the path given for the C{rundir} parameter.
814        """
815        self.runner.setupEnvironment(None, "/foo/bar", True, None, None)
816        self.assertEqual(self.cwd, "/foo/bar")
817
818    def test_daemonize(self):
819        """
820        L{UnixApplicationRunner.setupEnvironment} daemonizes the process if
821        C{False} is passed for the C{nodaemon} parameter.
822        """
823        with AlternateReactor(FakeDaemonizingReactor()):
824            self.runner.setupEnvironment(None, ".", False, None, None)
825        self.assertTrue(self.daemon)
826
827    def test_noDaemonize(self):
828        """
829        L{UnixApplicationRunner.setupEnvironment} does not daemonize the
830        process if C{True} is passed for the C{nodaemon} parameter.
831        """
832        self.runner.setupEnvironment(None, ".", True, None, None)
833        self.assertFalse(self.daemon)
834
835    def test_nonDaemonPIDFile(self):
836        """
837        L{UnixApplicationRunner.setupEnvironment} writes the process's PID to
838        the file specified by the C{pidfile} parameter.
839        """
840        pidfile = self.mktemp()
841        self.runner.setupEnvironment(None, ".", True, None, pidfile)
842        with open(pidfile, "rb") as f:
843            pid = int(f.read())
844        self.assertEqual(pid, self.pid)
845
846    def test_daemonPIDFile(self):
847        """
848        L{UnixApplicationRunner.setupEnvironment} writes the daemonized
849        process's PID to the file specified by the C{pidfile} parameter if
850        C{nodaemon} is C{False}.
851        """
852        pidfile = self.mktemp()
853        with AlternateReactor(FakeDaemonizingReactor()):
854            self.runner.setupEnvironment(None, ".", False, None, pidfile)
855        with open(pidfile, "rb") as f:
856            pid = int(f.read())
857        self.assertEqual(pid, self.pid + 1)
858
859    def test_umask(self):
860        """
861        L{UnixApplicationRunner.setupEnvironment} changes the process umask to
862        the value specified by the C{umask} parameter.
863        """
864        with AlternateReactor(FakeDaemonizingReactor()):
865            self.runner.setupEnvironment(None, ".", False, 123, None)
866        self.assertEqual(self.mask, 123)
867
868    def test_noDaemonizeNoUmask(self):
869        """
870        L{UnixApplicationRunner.setupEnvironment} doesn't change the process
871        umask if L{None} is passed for the C{umask} parameter and C{True} is
872        passed for the C{nodaemon} parameter.
873        """
874        self.runner.setupEnvironment(None, ".", True, None, None)
875        self.assertIs(self.mask, self.unset)
876
877    def test_daemonizedNoUmask(self):
878        """
879        L{UnixApplicationRunner.setupEnvironment} changes the process umask to
880        C{0077} if L{None} is passed for the C{umask} parameter and C{False} is
881        passed for the C{nodaemon} parameter.
882        """
883        with AlternateReactor(FakeDaemonizingReactor()):
884            self.runner.setupEnvironment(None, ".", False, None, None)
885        self.assertEqual(self.mask, 0o077)
886
887
888@skipIf(not _twistd_unix, "twistd unix not available")
889class UnixApplicationRunnerStartApplicationTests(TestCase):
890    """
891    Tests for L{UnixApplicationRunner.startApplication}.
892    """
893
894    def test_setupEnvironment(self):
895        """
896        L{UnixApplicationRunner.startApplication} calls
897        L{UnixApplicationRunner.setupEnvironment} with the chroot, rundir,
898        nodaemon, umask, and pidfile parameters from the configuration it is
899        constructed with.
900        """
901        options = twistd.ServerOptions()
902        options.parseOptions(
903            [
904                "--nodaemon",
905                "--umask",
906                "0070",
907                "--chroot",
908                "/foo/chroot",
909                "--rundir",
910                "/foo/rundir",
911                "--pidfile",
912                "/foo/pidfile",
913            ]
914        )
915        application = service.Application("test_setupEnvironment")
916        self.runner = UnixApplicationRunner(options)
917
918        args = []
919
920        def fakeSetupEnvironment(self, chroot, rundir, nodaemon, umask, pidfile):
921            args.extend((chroot, rundir, nodaemon, umask, pidfile))
922
923        # Sanity check
924        setupEnvironmentParameters = inspect.signature(
925            self.runner.setupEnvironment
926        ).parameters
927        fakeSetupEnvironmentParameters = inspect.signature(
928            fakeSetupEnvironment
929        ).parameters
930
931        # inspect.signature() does not return "self" in the signature of
932        # a class method, so we need to omit  it when comparing the
933        # the signature of a plain method
934        fakeSetupEnvironmentParameters = fakeSetupEnvironmentParameters.copy()
935        fakeSetupEnvironmentParameters.pop("self")
936
937        self.assertEqual(setupEnvironmentParameters, fakeSetupEnvironmentParameters)
938
939        self.patch(UnixApplicationRunner, "setupEnvironment", fakeSetupEnvironment)
940        self.patch(UnixApplicationRunner, "shedPrivileges", lambda *a, **kw: None)
941        self.patch(app, "startApplication", lambda *a, **kw: None)
942        self.runner.startApplication(application)
943
944        self.assertEqual(args, ["/foo/chroot", "/foo/rundir", True, 56, "/foo/pidfile"])
945
946    def test_shedPrivileges(self):
947        """
948        L{UnixApplicationRunner.shedPrivileges} switches the user ID
949        of the process.
950        """
951
952        def switchUIDPass(uid, gid, euid):
953            self.assertEqual(uid, 200)
954            self.assertEqual(gid, 54)
955            self.assertEqual(euid, 35)
956
957        self.patch(_twistd_unix, "switchUID", switchUIDPass)
958        runner = UnixApplicationRunner({})
959        runner.shedPrivileges(35, 200, 54)
960
961    def test_shedPrivilegesError(self):
962        """
963        An unexpected L{OSError} when calling
964        L{twisted.scripts._twistd_unix.shedPrivileges}
965        terminates the process via L{SystemExit}.
966        """
967
968        def switchUIDFail(uid, gid, euid):
969            raise OSError(errno.EBADF, "fake")
970
971        runner = UnixApplicationRunner({})
972        self.patch(_twistd_unix, "switchUID", switchUIDFail)
973        exc = self.assertRaises(SystemExit, runner.shedPrivileges, 35, 200, None)
974        self.assertEqual(exc.code, 1)
975
976    def _setUID(self, wantedUser, wantedUid, wantedGroup, wantedGid):
977        """
978        Common code for tests which try to pass the the UID to
979        L{UnixApplicationRunner}.
980        """
981        patchUserDatabase(self.patch, wantedUser, wantedUid, wantedGroup, wantedGid)
982
983        def initgroups(uid, gid):
984            self.assertEqual(uid, wantedUid)
985            self.assertEqual(gid, wantedGid)
986
987        def setuid(uid):
988            self.assertEqual(uid, wantedUid)
989
990        def setgid(gid):
991            self.assertEqual(gid, wantedGid)
992
993        self.patch(util, "initgroups", initgroups)
994        self.patch(os, "setuid", setuid)
995        self.patch(os, "setgid", setgid)
996
997        options = twistd.ServerOptions()
998        options.parseOptions(["--nodaemon", "--uid", str(wantedUid)])
999        application = service.Application("test_setupEnvironment")
1000        self.runner = UnixApplicationRunner(options)
1001        runner = UnixApplicationRunner(options)
1002        runner.startApplication(application)
1003
1004    def test_setUidWithoutGid(self):
1005        """
1006        Starting an application with L{UnixApplicationRunner} configured
1007        with a UID and no GUID will result in the GUID being
1008        set to the default GUID for that UID.
1009        """
1010        self._setUID("foo", 5151, "bar", 4242)
1011
1012    def test_setUidSameAsCurrentUid(self):
1013        """
1014        If the specified UID is the same as the current UID of the process,
1015        then a warning is displayed.
1016        """
1017        currentUid = os.getuid()
1018        self._setUID("morefoo", currentUid, "morebar", 4343)
1019
1020        warningsShown = self.flushWarnings()
1021        self.assertEqual(1, len(warningsShown))
1022        expectedWarning = (
1023            "tried to drop privileges and setuid {} but uid is already {}; "
1024            "should we be root? Continuing.".format(currentUid, currentUid)
1025        )
1026        self.assertEqual(expectedWarning, warningsShown[0]["message"])
1027
1028
1029@skipIf(not _twistd_unix, "twistd unix not available")
1030class UnixApplicationRunnerRemovePIDTests(TestCase):
1031    """
1032    Tests for L{UnixApplicationRunner.removePID}.
1033    """
1034
1035    def test_removePID(self):
1036        """
1037        L{UnixApplicationRunner.removePID} deletes the file the name of
1038        which is passed to it.
1039        """
1040        runner = UnixApplicationRunner({})
1041        path = self.mktemp()
1042        os.makedirs(path)
1043        pidfile = os.path.join(path, "foo.pid")
1044        open(pidfile, "w").close()
1045        runner.removePID(pidfile)
1046        self.assertFalse(os.path.exists(pidfile))
1047
1048    def test_removePIDErrors(self):
1049        """
1050        Calling L{UnixApplicationRunner.removePID} with a non-existent filename
1051        logs an OSError.
1052        """
1053        runner = UnixApplicationRunner({})
1054        runner.removePID("fakepid")
1055        errors = self.flushLoggedErrors(OSError)
1056        self.assertEqual(len(errors), 1)
1057        self.assertEqual(errors[0].value.errno, errno.ENOENT)
1058
1059
1060class FakeNonDaemonizingReactor:
1061    """
1062    A dummy reactor, providing C{beforeDaemonize} and C{afterDaemonize}
1063    methods, but not announcing this, and logging whether the methods have been
1064    called.
1065
1066    @ivar _beforeDaemonizeCalled: if C{beforeDaemonize} has been called or not.
1067    @type _beforeDaemonizeCalled: C{bool}
1068    @ivar _afterDaemonizeCalled: if C{afterDaemonize} has been called or not.
1069    @type _afterDaemonizeCalled: C{bool}
1070    """
1071
1072    def __init__(self):
1073        self._beforeDaemonizeCalled = False
1074        self._afterDaemonizeCalled = False
1075
1076    def beforeDaemonize(self):
1077        self._beforeDaemonizeCalled = True
1078
1079    def afterDaemonize(self):
1080        self._afterDaemonizeCalled = True
1081
1082    def addSystemEventTrigger(self, *args, **kw):
1083        """
1084        Skip event registration.
1085        """
1086
1087
1088@implementer(IReactorDaemonize)
1089class FakeDaemonizingReactor(FakeNonDaemonizingReactor):
1090    """
1091    A dummy reactor, providing C{beforeDaemonize} and C{afterDaemonize}
1092    methods, announcing this, and logging whether the methods have been called.
1093    """
1094
1095
1096class DummyReactor:
1097    """
1098    A dummy reactor, only providing a C{run} method and checking that it
1099    has been called.
1100
1101    @ivar called: if C{run} has been called or not.
1102    @type called: C{bool}
1103    """
1104
1105    called = False
1106
1107    def run(self):
1108        """
1109        A fake run method, checking that it's been called one and only time.
1110        """
1111        if self.called:
1112            raise RuntimeError("Already called")
1113        self.called = True
1114
1115
1116class AppProfilingTests(TestCase):
1117    """
1118    Tests for L{app.AppProfiler}.
1119    """
1120
1121    @skipIf(not profile, "profile module not available")
1122    def test_profile(self):
1123        """
1124        L{app.ProfileRunner.run} should call the C{run} method of the reactor
1125        and save profile data in the specified file.
1126        """
1127        config = twistd.ServerOptions()
1128        config["profile"] = self.mktemp()
1129        config["profiler"] = "profile"
1130        profiler = app.AppProfiler(config)
1131        reactor = DummyReactor()
1132
1133        profiler.run(reactor)
1134
1135        self.assertTrue(reactor.called)
1136        with open(config["profile"]) as f:
1137            data = f.read()
1138        self.assertIn("DummyReactor.run", data)
1139        self.assertIn("function calls", data)
1140
1141    def _testStats(self, statsClass, profile):
1142        out = StringIO()
1143
1144        # Patch before creating the pstats, because pstats binds self.stream to
1145        # sys.stdout early in 2.5 and newer.
1146        stdout = self.patch(sys, "stdout", out)
1147
1148        # If pstats.Stats can load the data and then reformat it, then the
1149        # right thing probably happened.
1150        stats = statsClass(profile)
1151        stats.print_stats()
1152        stdout.restore()
1153
1154        data = out.getvalue()
1155        self.assertIn("function calls", data)
1156        self.assertIn("(run)", data)
1157
1158    @skipIf(not profile, "profile module not available")
1159    def test_profileSaveStats(self):
1160        """
1161        With the C{savestats} option specified, L{app.ProfileRunner.run}
1162        should save the raw stats object instead of a summary output.
1163        """
1164        config = twistd.ServerOptions()
1165        config["profile"] = self.mktemp()
1166        config["profiler"] = "profile"
1167        config["savestats"] = True
1168        profiler = app.AppProfiler(config)
1169        reactor = DummyReactor()
1170
1171        profiler.run(reactor)
1172
1173        self.assertTrue(reactor.called)
1174        self._testStats(pstats.Stats, config["profile"])
1175
1176    def test_withoutProfile(self):
1177        """
1178        When the C{profile} module is not present, L{app.ProfilerRunner.run}
1179        should raise a C{SystemExit} exception.
1180        """
1181        savedModules = sys.modules.copy()
1182
1183        config = twistd.ServerOptions()
1184        config["profiler"] = "profile"
1185        profiler = app.AppProfiler(config)
1186
1187        sys.modules["profile"] = None
1188        try:
1189            self.assertRaises(SystemExit, profiler.run, None)
1190        finally:
1191            sys.modules.clear()
1192            sys.modules.update(savedModules)
1193
1194    @skipIf(not profile, "profile module not available")
1195    def test_profilePrintStatsError(self):
1196        """
1197        When an error happens during the print of the stats, C{sys.stdout}
1198        should be restored to its initial value.
1199        """
1200
1201        class ErroneousProfile(profile.Profile):
1202            def print_stats(self):
1203                raise RuntimeError("Boom")
1204
1205        self.patch(profile, "Profile", ErroneousProfile)
1206
1207        config = twistd.ServerOptions()
1208        config["profile"] = self.mktemp()
1209        config["profiler"] = "profile"
1210        profiler = app.AppProfiler(config)
1211        reactor = DummyReactor()
1212
1213        oldStdout = sys.stdout
1214        self.assertRaises(RuntimeError, profiler.run, reactor)
1215        self.assertIs(sys.stdout, oldStdout)
1216
1217    @skipIf(not cProfile, "cProfile module not available")
1218    def test_cProfile(self):
1219        """
1220        L{app.CProfileRunner.run} should call the C{run} method of the
1221        reactor and save profile data in the specified file.
1222        """
1223        config = twistd.ServerOptions()
1224        config["profile"] = self.mktemp()
1225        config["profiler"] = "cProfile"
1226        profiler = app.AppProfiler(config)
1227        reactor = DummyReactor()
1228
1229        profiler.run(reactor)
1230
1231        self.assertTrue(reactor.called)
1232        with open(config["profile"]) as f:
1233            data = f.read()
1234        self.assertIn("run", data)
1235        self.assertIn("function calls", data)
1236
1237    @skipIf(not cProfile, "cProfile module not available")
1238    def test_cProfileSaveStats(self):
1239        """
1240        With the C{savestats} option specified,
1241        L{app.CProfileRunner.run} should save the raw stats object
1242        instead of a summary output.
1243        """
1244        config = twistd.ServerOptions()
1245        config["profile"] = self.mktemp()
1246        config["profiler"] = "cProfile"
1247        config["savestats"] = True
1248        profiler = app.AppProfiler(config)
1249        reactor = DummyReactor()
1250
1251        profiler.run(reactor)
1252
1253        self.assertTrue(reactor.called)
1254        self._testStats(pstats.Stats, config["profile"])
1255
1256    def test_withoutCProfile(self):
1257        """
1258        When the C{cProfile} module is not present,
1259        L{app.CProfileRunner.run} should raise a C{SystemExit}
1260        exception and log the C{ImportError}.
1261        """
1262        savedModules = sys.modules.copy()
1263        sys.modules["cProfile"] = None
1264
1265        config = twistd.ServerOptions()
1266        config["profiler"] = "cProfile"
1267        profiler = app.AppProfiler(config)
1268        try:
1269            self.assertRaises(SystemExit, profiler.run, None)
1270        finally:
1271            sys.modules.clear()
1272            sys.modules.update(savedModules)
1273
1274    def test_unknownProfiler(self):
1275        """
1276        Check that L{app.AppProfiler} raises L{SystemExit} when given an
1277        unknown profiler name.
1278        """
1279        config = twistd.ServerOptions()
1280        config["profile"] = self.mktemp()
1281        config["profiler"] = "foobar"
1282
1283        error = self.assertRaises(SystemExit, app.AppProfiler, config)
1284        self.assertEqual(str(error), "Unsupported profiler name: foobar")
1285
1286    def test_defaultProfiler(self):
1287        """
1288        L{app.Profiler} defaults to the cprofile profiler if not specified.
1289        """
1290        profiler = app.AppProfiler({})
1291        self.assertEqual(profiler.profiler, "cprofile")
1292
1293    def test_profilerNameCaseInsentive(self):
1294        """
1295        The case of the profiler name passed to L{app.AppProfiler} is not
1296        relevant.
1297        """
1298        profiler = app.AppProfiler({"profiler": "CprOfile"})
1299        self.assertEqual(profiler.profiler, "cprofile")
1300
1301
1302def _patchTextFileLogObserver(patch):
1303    """
1304    Patch L{logger.textFileLogObserver} to record every call and keep a
1305    reference to the passed log file for tests.
1306
1307    @param patch: a callback for patching (usually L{TestCase.patch}).
1308
1309    @return: the list that keeps track of the log files.
1310    @rtype: C{list}
1311    """
1312    logFiles = []
1313    oldFileLogObserver = logger.textFileLogObserver
1314
1315    def observer(logFile, *args, **kwargs):
1316        logFiles.append(logFile)
1317        return oldFileLogObserver(logFile, *args, **kwargs)
1318
1319    patch(logger, "textFileLogObserver", observer)
1320    return logFiles
1321
1322
1323def _setupSyslog(testCase):
1324    """
1325    Make fake syslog, and return list to which prefix and then log
1326    messages will be appended if it is used.
1327    """
1328    logMessages = []
1329
1330    class fakesyslogobserver:
1331        def __init__(self, prefix):
1332            logMessages.append(prefix)
1333
1334        def emit(self, eventDict):
1335            logMessages.append(eventDict)
1336
1337    testCase.patch(syslog, "SyslogObserver", fakesyslogobserver)
1338    return logMessages
1339
1340
1341class AppLoggerTests(TestCase):
1342    """
1343    Tests for L{app.AppLogger}.
1344
1345    @ivar observers: list of observers installed during the tests.
1346    @type observers: C{list}
1347    """
1348
1349    def setUp(self):
1350        """
1351        Override L{globaLogBeginner.beginLoggingTo} so that we can trace the
1352        observers installed in C{self.observers}.
1353        """
1354        self.observers = []
1355
1356        def beginLoggingTo(observers):
1357            for observer in observers:
1358                self.observers.append(observer)
1359                globalLogPublisher.addObserver(observer)
1360
1361        self.patch(globalLogBeginner, "beginLoggingTo", beginLoggingTo)
1362
1363    def tearDown(self):
1364        """
1365        Remove all installed observers.
1366        """
1367        for observer in self.observers:
1368            globalLogPublisher.removeObserver(observer)
1369
1370    def _makeObserver(self):
1371        """
1372        Make a new observer which captures all logs sent to it.
1373
1374        @return: An observer that stores all logs sent to it.
1375        @rtype: Callable that implements L{ILogObserver}.
1376        """
1377
1378        @implementer(ILogObserver)
1379        class TestObserver:
1380            _logs = []
1381
1382            def __call__(self, event):
1383                self._logs.append(event)
1384
1385        return TestObserver()
1386
1387    def _checkObserver(self, observer):
1388        """
1389        Ensure that initial C{twistd} logs are written to logs.
1390
1391        @param observer: The observer made by L{self._makeObserver).
1392        """
1393        self.assertEqual(self.observers, [observer])
1394        self.assertIn("starting up", observer._logs[0]["log_format"])
1395        self.assertIn("reactor class", observer._logs[1]["log_format"])
1396
1397    def test_start(self):
1398        """
1399        L{app.AppLogger.start} calls L{globalLogBeginner.addObserver}, and then
1400        writes some messages about twistd and the reactor.
1401        """
1402        logger = app.AppLogger({})
1403        observer = self._makeObserver()
1404        logger._getLogObserver = lambda: observer
1405        logger.start(Componentized())
1406        self._checkObserver(observer)
1407
1408    def test_startUsesApplicationLogObserver(self):
1409        """
1410        When the L{ILogObserver} component is available on the application,
1411        that object will be used as the log observer instead of constructing a
1412        new one.
1413        """
1414        application = Componentized()
1415        observer = self._makeObserver()
1416        application.setComponent(ILogObserver, observer)
1417        logger = app.AppLogger({})
1418        logger.start(application)
1419        self._checkObserver(observer)
1420
1421    def _setupConfiguredLogger(
1422        self, application, extraLogArgs={}, appLogger=app.AppLogger
1423    ):
1424        """
1425        Set up an AppLogger which exercises the C{logger} configuration option.
1426
1427        @type application: L{Componentized}
1428        @param application: The L{Application} object to pass to
1429            L{app.AppLogger.start}.
1430        @type extraLogArgs: C{dict}
1431        @param extraLogArgs: extra values to pass to AppLogger.
1432        @type appLogger: L{AppLogger} class, or a subclass
1433        @param appLogger: factory for L{AppLogger} instances.
1434
1435        @rtype: C{list}
1436        @return: The logs accumulated by the log observer.
1437        """
1438        observer = self._makeObserver()
1439        logArgs = {"logger": lambda: observer}
1440        logArgs.update(extraLogArgs)
1441        logger = appLogger(logArgs)
1442        logger.start(application)
1443        return observer
1444
1445    def test_startUsesConfiguredLogObserver(self):
1446        """
1447        When the C{logger} key is specified in the configuration dictionary
1448        (i.e., when C{--logger} is passed to twistd), the initial log observer
1449        will be the log observer returned from the callable which the value
1450        refers to in FQPN form.
1451        """
1452        application = Componentized()
1453        self._checkObserver(self._setupConfiguredLogger(application))
1454
1455    def test_configuredLogObserverBeatsComponent(self):
1456        """
1457        C{--logger} takes precedence over a L{ILogObserver} component set on
1458        Application.
1459        """
1460        observer = self._makeObserver()
1461        application = Componentized()
1462        application.setComponent(ILogObserver, observer)
1463        self._checkObserver(self._setupConfiguredLogger(application))
1464        self.assertEqual(observer._logs, [])
1465
1466    def test_configuredLogObserverBeatsLegacyComponent(self):
1467        """
1468        C{--logger} takes precedence over a L{LegacyILogObserver} component
1469        set on Application.
1470        """
1471        nonlogs = []
1472        application = Componentized()
1473        application.setComponent(LegacyILogObserver, nonlogs.append)
1474        self._checkObserver(self._setupConfiguredLogger(application))
1475        self.assertEqual(nonlogs, [])
1476
1477    def test_loggerComponentBeatsLegacyLoggerComponent(self):
1478        """
1479        A L{ILogObserver} takes precedence over a L{LegacyILogObserver}
1480        component set on Application.
1481        """
1482        nonlogs = []
1483        observer = self._makeObserver()
1484        application = Componentized()
1485        application.setComponent(ILogObserver, observer)
1486        application.setComponent(LegacyILogObserver, nonlogs.append)
1487
1488        logger = app.AppLogger({})
1489        logger.start(application)
1490
1491        self._checkObserver(observer)
1492        self.assertEqual(nonlogs, [])
1493
1494    @skipIf(not _twistd_unix, "twistd unix not available")
1495    @skipIf(not syslog, "syslog not available")
1496    def test_configuredLogObserverBeatsSyslog(self):
1497        """
1498        C{--logger} takes precedence over a C{--syslog} command line
1499        argument.
1500        """
1501        logs = _setupSyslog(self)
1502        application = Componentized()
1503        self._checkObserver(
1504            self._setupConfiguredLogger(application, {"syslog": True}, UnixAppLogger)
1505        )
1506        self.assertEqual(logs, [])
1507
1508    def test_configuredLogObserverBeatsLogfile(self):
1509        """
1510        C{--logger} takes precedence over a C{--logfile} command line
1511        argument.
1512        """
1513        application = Componentized()
1514        path = self.mktemp()
1515        self._checkObserver(
1516            self._setupConfiguredLogger(application, {"logfile": "path"})
1517        )
1518        self.assertFalse(os.path.exists(path))
1519
1520    def test_getLogObserverStdout(self):
1521        """
1522        When logfile is empty or set to C{-}, L{app.AppLogger._getLogObserver}
1523        returns a log observer pointing at C{sys.stdout}.
1524        """
1525        logger = app.AppLogger({"logfile": "-"})
1526        logFiles = _patchTextFileLogObserver(self.patch)
1527
1528        logger._getLogObserver()
1529
1530        self.assertEqual(len(logFiles), 1)
1531        self.assertIs(logFiles[0], sys.stdout)
1532
1533        logger = app.AppLogger({"logfile": ""})
1534        logger._getLogObserver()
1535
1536        self.assertEqual(len(logFiles), 2)
1537        self.assertIs(logFiles[1], sys.stdout)
1538
1539    def test_getLogObserverFile(self):
1540        """
1541        When passing the C{logfile} option, L{app.AppLogger._getLogObserver}
1542        returns a log observer pointing at the specified path.
1543        """
1544        logFiles = _patchTextFileLogObserver(self.patch)
1545        filename = self.mktemp()
1546        sut = app.AppLogger({"logfile": filename})
1547
1548        observer = sut._getLogObserver()
1549        self.addCleanup(observer._outFile.close)
1550
1551        self.assertEqual(len(logFiles), 1)
1552        self.assertEqual(logFiles[0].path, os.path.abspath(filename))
1553
1554    def test_stop(self):
1555        """
1556        L{app.AppLogger.stop} removes the observer created in C{start}, and
1557        reinitialize its C{_observer} so that if C{stop} is called several
1558        times it doesn't break.
1559        """
1560        removed = []
1561        observer = object()
1562
1563        def remove(observer):
1564            removed.append(observer)
1565
1566        self.patch(globalLogPublisher, "removeObserver", remove)
1567        logger = app.AppLogger({})
1568        logger._observer = observer
1569        logger.stop()
1570        self.assertEqual(removed, [observer])
1571        logger.stop()
1572        self.assertEqual(removed, [observer])
1573        self.assertIsNone(logger._observer)
1574
1575    def test_legacyObservers(self):
1576        """
1577        L{app.AppLogger} using a legacy logger observer still works, wrapping
1578        it in a compat shim.
1579        """
1580        logs = []
1581        logger = app.AppLogger({})
1582
1583        @implementer(LegacyILogObserver)
1584        class LoggerObserver:
1585            """
1586            An observer which implements the legacy L{LegacyILogObserver}.
1587            """
1588
1589            def __call__(self, x):
1590                """
1591                Add C{x} to the logs list.
1592                """
1593                logs.append(x)
1594
1595        logger._observerFactory = lambda: LoggerObserver()
1596        logger.start(Componentized())
1597
1598        self.assertIn("starting up", textFromEventDict(logs[0]))
1599        warnings = self.flushWarnings([self.test_legacyObservers])
1600        self.assertEqual(len(warnings), 0, warnings)
1601
1602    def test_unmarkedObserversDeprecated(self):
1603        """
1604        L{app.AppLogger} using a logger observer which does not implement
1605        L{ILogObserver} or L{LegacyILogObserver} will be wrapped in a compat
1606        shim and raise a L{DeprecationWarning}.
1607        """
1608        logs = []
1609        logger = app.AppLogger({})
1610        logger._getLogObserver = lambda: logs.append
1611        logger.start(Componentized())
1612
1613        self.assertIn("starting up", textFromEventDict(logs[0]))
1614
1615        warnings = self.flushWarnings([self.test_unmarkedObserversDeprecated])
1616        self.assertEqual(len(warnings), 1, warnings)
1617        self.assertEqual(
1618            warnings[0]["message"],
1619            (
1620                "Passing a logger factory which makes log observers "
1621                "which do not implement twisted.logger.ILogObserver "
1622                "or twisted.python.log.ILogObserver to "
1623                "twisted.application.app.AppLogger was deprecated "
1624                "in Twisted 16.2. Please use a factory that "
1625                "produces twisted.logger.ILogObserver (or the "
1626                "legacy twisted.python.log.ILogObserver) "
1627                "implementing objects instead."
1628            ),
1629        )
1630
1631
1632@skipIf(not _twistd_unix, "twistd unix not available")
1633class UnixAppLoggerTests(TestCase):
1634    """
1635    Tests for L{UnixAppLogger}.
1636
1637    @ivar signals: list of signal handlers installed.
1638    @type signals: C{list}
1639    """
1640
1641    def setUp(self):
1642        """
1643        Fake C{signal.signal} for not installing the handlers but saving them
1644        in C{self.signals}.
1645        """
1646        self.signals = []
1647
1648        def fakeSignal(sig, f):
1649            self.signals.append((sig, f))
1650
1651        self.patch(signal, "signal", fakeSignal)
1652
1653    def test_getLogObserverStdout(self):
1654        """
1655        When non-daemonized and C{logfile} is empty or set to C{-},
1656        L{UnixAppLogger._getLogObserver} returns a log observer pointing at
1657        C{sys.stdout}.
1658        """
1659        logFiles = _patchTextFileLogObserver(self.patch)
1660
1661        logger = UnixAppLogger({"logfile": "-", "nodaemon": True})
1662        logger._getLogObserver()
1663        self.assertEqual(len(logFiles), 1)
1664        self.assertIs(logFiles[0], sys.stdout)
1665
1666        logger = UnixAppLogger({"logfile": "", "nodaemon": True})
1667        logger._getLogObserver()
1668        self.assertEqual(len(logFiles), 2)
1669        self.assertIs(logFiles[1], sys.stdout)
1670
1671    def test_getLogObserverStdoutDaemon(self):
1672        """
1673        When daemonized and C{logfile} is set to C{-},
1674        L{UnixAppLogger._getLogObserver} raises C{SystemExit}.
1675        """
1676        logger = UnixAppLogger({"logfile": "-", "nodaemon": False})
1677        error = self.assertRaises(SystemExit, logger._getLogObserver)
1678        self.assertEqual(str(error), "Daemons cannot log to stdout, exiting!")
1679
1680    def test_getLogObserverFile(self):
1681        """
1682        When C{logfile} contains a file name, L{app.AppLogger._getLogObserver}
1683        returns a log observer pointing at the specified path, and a signal
1684        handler rotating the log is installed.
1685        """
1686        logFiles = _patchTextFileLogObserver(self.patch)
1687        filename = self.mktemp()
1688        sut = UnixAppLogger({"logfile": filename})
1689
1690        observer = sut._getLogObserver()
1691        self.addCleanup(observer._outFile.close)
1692
1693        self.assertEqual(len(logFiles), 1)
1694        self.assertEqual(logFiles[0].path, os.path.abspath(filename))
1695
1696        self.assertEqual(len(self.signals), 1)
1697        self.assertEqual(self.signals[0][0], signal.SIGUSR1)
1698
1699        d = Deferred()
1700
1701        def rotate():
1702            d.callback(None)
1703
1704        logFiles[0].rotate = rotate
1705
1706        rotateLog = self.signals[0][1]
1707        rotateLog(None, None)
1708        return d
1709
1710    def test_getLogObserverDontOverrideSignalHandler(self):
1711        """
1712        If a signal handler is already installed,
1713        L{UnixAppLogger._getLogObserver} doesn't override it.
1714        """
1715
1716        def fakeGetSignal(sig):
1717            self.assertEqual(sig, signal.SIGUSR1)
1718            return object()
1719
1720        self.patch(signal, "getsignal", fakeGetSignal)
1721        filename = self.mktemp()
1722        sut = UnixAppLogger({"logfile": filename})
1723
1724        observer = sut._getLogObserver()
1725        self.addCleanup(observer._outFile.close)
1726
1727        self.assertEqual(self.signals, [])
1728
1729    def test_getLogObserverDefaultFile(self):
1730        """
1731        When daemonized and C{logfile} is empty, the observer returned by
1732        L{UnixAppLogger._getLogObserver} points at C{twistd.log} in the current
1733        directory.
1734        """
1735        logFiles = _patchTextFileLogObserver(self.patch)
1736        logger = UnixAppLogger({"logfile": "", "nodaemon": False})
1737        logger._getLogObserver()
1738
1739        self.assertEqual(len(logFiles), 1)
1740        self.assertEqual(logFiles[0].path, os.path.abspath("twistd.log"))
1741
1742    @skipIf(not _twistd_unix, "twistd unix not available")
1743    def test_getLogObserverSyslog(self):
1744        """
1745        If C{syslog} is set to C{True}, L{UnixAppLogger._getLogObserver} starts
1746        a L{syslog.SyslogObserver} with given C{prefix}.
1747        """
1748        logs = _setupSyslog(self)
1749        logger = UnixAppLogger({"syslog": True, "prefix": "test-prefix"})
1750        observer = logger._getLogObserver()
1751        self.assertEqual(logs, ["test-prefix"])
1752        observer({"a": "b"})
1753        self.assertEqual(logs, ["test-prefix", {"a": "b"}])
1754
1755
1756@skipIf(not _twistd_unix, "twistd unix support not available")
1757class DaemonizeTests(TestCase):
1758    """
1759    Tests for L{_twistd_unix.UnixApplicationRunner} daemonization.
1760    """
1761
1762    def setUp(self):
1763        self.mockos = MockOS()
1764        self.config = twistd.ServerOptions()
1765        self.patch(_twistd_unix, "os", self.mockos)
1766        self.runner = _twistd_unix.UnixApplicationRunner(self.config)
1767        self.runner.application = service.Application("Hi!")
1768        self.runner.oldstdout = sys.stdout
1769        self.runner.oldstderr = sys.stderr
1770        self.runner.startReactor = lambda *args: None
1771
1772    def test_success(self):
1773        """
1774        When double fork succeeded in C{daemonize}, the child process writes
1775        B{0} to the status pipe.
1776        """
1777        with AlternateReactor(FakeDaemonizingReactor()):
1778            self.runner.postApplication()
1779        self.assertEqual(
1780            self.mockos.actions,
1781            [
1782                ("chdir", "."),
1783                ("umask", 0o077),
1784                ("fork", True),
1785                "setsid",
1786                ("fork", True),
1787                ("write", -2, b"0"),
1788                ("unlink", "twistd.pid"),
1789            ],
1790        )
1791        self.assertEqual(self.mockos.closed, [-3, -2])
1792
1793    def test_successInParent(self):
1794        """
1795        The parent process initiating the C{daemonize} call reads data from the
1796        status pipe and then exit the process.
1797        """
1798        self.mockos.child = False
1799        self.mockos.readData = b"0"
1800        with AlternateReactor(FakeDaemonizingReactor()):
1801            self.assertRaises(SystemError, self.runner.postApplication)
1802        self.assertEqual(
1803            self.mockos.actions,
1804            [
1805                ("chdir", "."),
1806                ("umask", 0o077),
1807                ("fork", True),
1808                ("read", -1, 100),
1809                ("exit", 0),
1810                ("unlink", "twistd.pid"),
1811            ],
1812        )
1813        self.assertEqual(self.mockos.closed, [-1])
1814
1815    def test_successEINTR(self):
1816        """
1817        If the C{os.write} call to the status pipe raises an B{EINTR} error,
1818        the process child retries to write.
1819        """
1820        written = []
1821
1822        def raisingWrite(fd, data):
1823            written.append((fd, data))
1824            if len(written) == 1:
1825                raise OSError(errno.EINTR)
1826
1827        self.mockos.write = raisingWrite
1828        with AlternateReactor(FakeDaemonizingReactor()):
1829            self.runner.postApplication()
1830        self.assertEqual(
1831            self.mockos.actions,
1832            [
1833                ("chdir", "."),
1834                ("umask", 0o077),
1835                ("fork", True),
1836                "setsid",
1837                ("fork", True),
1838                ("unlink", "twistd.pid"),
1839            ],
1840        )
1841        self.assertEqual(self.mockos.closed, [-3, -2])
1842        self.assertEqual([(-2, b"0"), (-2, b"0")], written)
1843
1844    def test_successInParentEINTR(self):
1845        """
1846        If the C{os.read} call on the status pipe raises an B{EINTR} error, the
1847        parent child retries to read.
1848        """
1849        read = []
1850
1851        def raisingRead(fd, size):
1852            read.append((fd, size))
1853            if len(read) == 1:
1854                raise OSError(errno.EINTR)
1855            return b"0"
1856
1857        self.mockos.read = raisingRead
1858        self.mockos.child = False
1859        with AlternateReactor(FakeDaemonizingReactor()):
1860            self.assertRaises(SystemError, self.runner.postApplication)
1861        self.assertEqual(
1862            self.mockos.actions,
1863            [
1864                ("chdir", "."),
1865                ("umask", 0o077),
1866                ("fork", True),
1867                ("exit", 0),
1868                ("unlink", "twistd.pid"),
1869            ],
1870        )
1871        self.assertEqual(self.mockos.closed, [-1])
1872        self.assertEqual([(-1, 100), (-1, 100)], read)
1873
1874    def assertErrorWritten(self, raised, reported):
1875        """
1876        Assert L{UnixApplicationRunner.postApplication} writes
1877        C{reported} to its status pipe if the service raises an
1878        exception whose message is C{raised}.
1879        """
1880
1881        class FakeService(service.Service):
1882            def startService(self):
1883                raise RuntimeError(raised)
1884
1885        errorService = FakeService()
1886        errorService.setServiceParent(self.runner.application)
1887
1888        with AlternateReactor(FakeDaemonizingReactor()):
1889            self.assertRaises(RuntimeError, self.runner.postApplication)
1890        self.assertEqual(
1891            self.mockos.actions,
1892            [
1893                ("chdir", "."),
1894                ("umask", 0o077),
1895                ("fork", True),
1896                "setsid",
1897                ("fork", True),
1898                ("write", -2, reported),
1899                ("unlink", "twistd.pid"),
1900            ],
1901        )
1902        self.assertEqual(self.mockos.closed, [-3, -2])
1903
1904    def test_error(self):
1905        """
1906        If an error happens during daemonization, the child process writes the
1907        exception error to the status pipe.
1908        """
1909        self.assertErrorWritten(
1910            raised="Something is wrong", reported=b"1 RuntimeError: Something is wrong"
1911        )
1912
1913    def test_unicodeError(self):
1914        """
1915        If an error happens during daemonization, and that error's
1916        message is Unicode, the child encodes the message as ascii
1917        with backslash Unicode code points.
1918        """
1919        self.assertErrorWritten(raised="\u2022", reported=b"1 RuntimeError: \\u2022")
1920
1921    def assertErrorInParentBehavior(self, readData, errorMessage, mockOSActions):
1922        """
1923        Make L{os.read} appear to return C{readData}, and assert that
1924        L{UnixApplicationRunner.postApplication} writes
1925        C{errorMessage} to standard error and executes the calls
1926        against L{os} functions specified in C{mockOSActions}.
1927        """
1928        self.mockos.child = False
1929        self.mockos.readData = readData
1930        errorIO = StringIO()
1931        self.patch(sys, "__stderr__", errorIO)
1932        with AlternateReactor(FakeDaemonizingReactor()):
1933            self.assertRaises(SystemError, self.runner.postApplication)
1934        self.assertEqual(errorIO.getvalue(), errorMessage)
1935        self.assertEqual(self.mockos.actions, mockOSActions)
1936        self.assertEqual(self.mockos.closed, [-1])
1937
1938    def test_errorInParent(self):
1939        """
1940        When the child writes an error message to the status pipe
1941        during daemonization, the parent writes the repr of the
1942        message to C{stderr} and exits with non-zero status code.
1943        """
1944        self.assertErrorInParentBehavior(
1945            readData=b"1 Exception: An identified error",
1946            errorMessage=(
1947                "An error has occurred: b'Exception: An identified error'\n"
1948                "Please look at log file for more information.\n"
1949            ),
1950            mockOSActions=[
1951                ("chdir", "."),
1952                ("umask", 0o077),
1953                ("fork", True),
1954                ("read", -1, 100),
1955                ("exit", 1),
1956                ("unlink", "twistd.pid"),
1957            ],
1958        )
1959
1960    def test_nonASCIIErrorInParent(self):
1961        """
1962        When the child writes a non-ASCII error message to the status
1963        pipe during daemonization, the parent writes the repr of the
1964        message to C{stderr} and exits with a non-zero status code.
1965        """
1966        self.assertErrorInParentBehavior(
1967            readData=b"1 Exception: \xff",
1968            errorMessage=(
1969                "An error has occurred: b'Exception: \\xff'\n"
1970                "Please look at log file for more information.\n"
1971            ),
1972            mockOSActions=[
1973                ("chdir", "."),
1974                ("umask", 0o077),
1975                ("fork", True),
1976                ("read", -1, 100),
1977                ("exit", 1),
1978                ("unlink", "twistd.pid"),
1979            ],
1980        )
1981
1982    def test_errorInParentWithTruncatedUnicode(self):
1983        """
1984        When the child writes a non-ASCII error message to the status
1985        pipe during daemonization, and that message is too longer, the
1986        parent writes the repr of the truncated message to C{stderr}
1987        and exits with a non-zero status code.
1988        """
1989        truncatedMessage = b"1 RuntimeError: " + b"\\u2022" * 14
1990        # the escape sequence will appear to be escaped twice, because
1991        # we're getting the repr
1992        reportedMessage = "b'RuntimeError: {}'".format(r"\\u2022" * 14)
1993        self.assertErrorInParentBehavior(
1994            readData=truncatedMessage,
1995            errorMessage=(
1996                "An error has occurred: {}\n"
1997                "Please look at log file for more information.\n".format(
1998                    reportedMessage
1999                )
2000            ),
2001            mockOSActions=[
2002                ("chdir", "."),
2003                ("umask", 0o077),
2004                ("fork", True),
2005                ("read", -1, 100),
2006                ("exit", 1),
2007                ("unlink", "twistd.pid"),
2008            ],
2009        )
2010
2011    def test_errorMessageTruncated(self):
2012        """
2013        If an error occurs during daemonization and its message is too
2014        long, it's truncated by the child.
2015        """
2016        self.assertErrorWritten(
2017            raised="x" * 200, reported=b"1 RuntimeError: " + b"x" * 84
2018        )
2019
2020    def test_unicodeErrorMessageTruncated(self):
2021        """
2022        If an error occurs during daemonization and its message is
2023        unicode and too long, it's truncated by the child, even if
2024        this splits a unicode escape sequence.
2025        """
2026        self.assertErrorWritten(
2027            raised="\u2022" * 30,
2028            reported=b"1 RuntimeError: " + b"\\u2022" * 14,
2029        )
2030
2031    def test_hooksCalled(self):
2032        """
2033        C{daemonize} indeed calls L{IReactorDaemonize.beforeDaemonize} and
2034        L{IReactorDaemonize.afterDaemonize} if the reactor implements
2035        L{IReactorDaemonize}.
2036        """
2037        reactor = FakeDaemonizingReactor()
2038        self.runner.daemonize(reactor)
2039        self.assertTrue(reactor._beforeDaemonizeCalled)
2040        self.assertTrue(reactor._afterDaemonizeCalled)
2041
2042    def test_hooksNotCalled(self):
2043        """
2044        C{daemonize} does NOT call L{IReactorDaemonize.beforeDaemonize} or
2045        L{IReactorDaemonize.afterDaemonize} if the reactor does NOT implement
2046        L{IReactorDaemonize}.
2047        """
2048        reactor = FakeNonDaemonizingReactor()
2049        self.runner.daemonize(reactor)
2050        self.assertFalse(reactor._beforeDaemonizeCalled)
2051        self.assertFalse(reactor._afterDaemonizeCalled)
2052
2053
2054@implementer(_ISupportsExitSignalCapturing)
2055class SignalCapturingMemoryReactor(MemoryReactor):
2056    """
2057    MemoryReactor that implements the _ISupportsExitSignalCapturing interface,
2058    all other operations identical to MemoryReactor.
2059    """
2060
2061    @property
2062    def _exitSignal(self):
2063        return self._val
2064
2065    @_exitSignal.setter
2066    def _exitSignal(self, val):
2067        self._val = val
2068
2069
2070class StubApplicationRunnerWithSignal(twistd._SomeApplicationRunner):
2071    """
2072    An application runner that uses a SignalCapturingMemoryReactor and
2073    has a _signalValue attribute that it will set in the reactor.
2074
2075    @ivar _signalValue: The signal value to set on the reactor's _exitSignal
2076        attribute.
2077    """
2078
2079    loggerFactory = CrippledAppLogger
2080
2081    def __init__(self, config):
2082        super().__init__(config)
2083        self._signalValue = None
2084
2085    def preApplication(self):
2086        """
2087        Does nothing.
2088        """
2089
2090    def postApplication(self):
2091        """
2092        Instantiate a SignalCapturingMemoryReactor and start it
2093        in the runner.
2094        """
2095        reactor = SignalCapturingMemoryReactor()
2096        reactor._exitSignal = self._signalValue
2097        self.startReactor(reactor, sys.stdout, sys.stderr)
2098
2099
2100def stubApplicationRunnerFactoryCreator(signum):
2101    """
2102    Create a factory function to instantiate a
2103    StubApplicationRunnerWithSignal that will report signum as the captured
2104    signal..
2105
2106    @param signum: The integer signal number or None
2107    @type signum: C{int} or C{None}
2108
2109    @return: A factory function to create stub runners.
2110    @rtype: stubApplicationRunnerFactory
2111    """
2112
2113    def stubApplicationRunnerFactory(config):
2114        """
2115        Create a StubApplicationRunnerWithSignal using a reactor that
2116        implements _ISupportsExitSignalCapturing and whose _exitSignal
2117        attribute is set to signum.
2118
2119        @param config: The runner configuration, platform dependent.
2120        @type config: L{twisted.scripts.twistd.ServerOptions}
2121
2122        @return: A runner to use for the test.
2123        @rtype: twisted.test.test_twistd.StubApplicationRunnerWithSignal
2124        """
2125        runner = StubApplicationRunnerWithSignal(config)
2126        runner._signalValue = signum
2127        return runner
2128
2129    return stubApplicationRunnerFactory
2130
2131
2132class ExitWithSignalTests(TestCase):
2133
2134    """
2135    Tests for L{twisted.application.app._exitWithSignal}.
2136    """
2137
2138    def setUp(self):
2139        """
2140        Set up the server options and a fake for use by test cases.
2141        """
2142        self.config = twistd.ServerOptions()
2143        self.config.loadedPlugins = {"test_command": MockServiceMaker()}
2144        self.config.subOptions = object()
2145        self.config.subCommand = "test_command"
2146        self.fakeKillArgs = [None, None]
2147
2148        def fakeKill(pid, sig):
2149            """
2150            Fake method to capture arguments passed to os.kill.
2151
2152            @param pid: The pid of the process being killed.
2153
2154            @param sig: The signal sent to the process.
2155            """
2156            self.fakeKillArgs[0] = pid
2157            self.fakeKillArgs[1] = sig
2158
2159        self.patch(os, "kill", fakeKill)
2160
2161    def test_exitWithSignal(self):
2162        """
2163        exitWithSignal replaces the existing signal handler with the default
2164        handler and sends the replaced signal to the current process.
2165        """
2166
2167        fakeSignalArgs = [None, None]
2168
2169        def fake_signal(sig, handler):
2170            fakeSignalArgs[0] = sig
2171            fakeSignalArgs[1] = handler
2172
2173        self.patch(signal, "signal", fake_signal)
2174        app._exitWithSignal(signal.SIGINT)
2175
2176        self.assertEquals(fakeSignalArgs[0], signal.SIGINT)
2177        self.assertEquals(fakeSignalArgs[1], signal.SIG_DFL)
2178        self.assertEquals(self.fakeKillArgs[0], os.getpid())
2179        self.assertEquals(self.fakeKillArgs[1], signal.SIGINT)
2180
2181    def test_normalExit(self):
2182        """
2183        _exitWithSignal is not called if the runner does not exit with a
2184        signal.
2185        """
2186        self.patch(
2187            twistd, "_SomeApplicationRunner", stubApplicationRunnerFactoryCreator(None)
2188        )
2189        twistd.runApp(self.config)
2190        self.assertIsNone(self.fakeKillArgs[0])
2191        self.assertIsNone(self.fakeKillArgs[1])
2192
2193    def test_runnerExitsWithSignal(self):
2194        """
2195        _exitWithSignal is called when the runner exits with a signal.
2196        """
2197        self.patch(
2198            twistd,
2199            "_SomeApplicationRunner",
2200            stubApplicationRunnerFactoryCreator(signal.SIGINT),
2201        )
2202        twistd.runApp(self.config)
2203        self.assertEquals(self.fakeKillArgs[0], os.getpid())
2204        self.assertEquals(self.fakeKillArgs[1], signal.SIGINT)
2205