aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/Twisted/py3/twisted/scripts/_twistd_unix.py
blob: 295a32fa01ee6876019420b4871feba952ae53a4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# -*- test-case-name: twisted.test.test_twistd -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.


import errno
import os
import pwd
import sys
import traceback

from twisted import copyright, logger
from twisted.application import app, service
from twisted.internet.interfaces import IReactorDaemonize
from twisted.python import log, logfile, usage
from twisted.python.runtime import platformType
from twisted.python.util import gidFromString, switchUID, uidFromString, untilConcludes

if platformType == "win32":
    raise ImportError("_twistd_unix doesn't work on Windows.")


def _umask(value):
    return int(value, 8)


class ServerOptions(app.ServerOptions):
    synopsis = "Usage: twistd [options]"

    optFlags = [
        ["nodaemon", "n", "don't daemonize, don't use default umask of 0077"],
        ["originalname", None, "Don't try to change the process name"],
        ["syslog", None, "Log to syslog, not to file"],
        [
            "euid",
            "",
            "Set only effective user-id rather than real user-id. "
            "(This option has no effect unless the server is running as "
            "root, in which case it means not to shed all privileges "
            "after binding ports, retaining the option to regain "
            "privileges in cases such as spawning processes. "
            "Use with caution.)",
        ],
    ]

    optParameters = [
        ["prefix", None, "twisted", "use the given prefix when syslogging"],
        ["pidfile", "", "twistd.pid", "Name of the pidfile"],
        ["chroot", None, None, "Chroot to a supplied directory before running"],
        ["uid", "u", None, "The uid to run as.", uidFromString],
        [
            "gid",
            "g",
            None,
            "The gid to run as.  If not specified, the default gid "
            "associated with the specified --uid is used.",
            gidFromString,
        ],
        ["umask", None, None, "The (octal) file creation mask to apply.", _umask],
    ]

    compData = usage.Completions(
        optActions={
            "pidfile": usage.CompleteFiles("*.pid"),
            "chroot": usage.CompleteDirs(descr="chroot directory"),
            "gid": usage.CompleteGroups(descr="gid to run as"),
            "uid": usage.CompleteUsernames(descr="uid to run as"),
            "prefix": usage.Completer(descr="syslog prefix"),
        },
    )

    def opt_version(self):
        """
        Print version information and exit.
        """
        print(f"twistd (the Twisted daemon) {copyright.version}", file=self.stdout)
        print(copyright.copyright, file=self.stdout)
        sys.exit()

    def postOptions(self):
        app.ServerOptions.postOptions(self)
        if self["pidfile"]:
            self["pidfile"] = os.path.abspath(self["pidfile"])


def checkPID(pidfile):
    if not pidfile:
        return
    if os.path.exists(pidfile):
        try:
            with open(pidfile) as f:
                pid = int(f.read())
        except ValueError:
            sys.exit(f"Pidfile {pidfile} contains non-numeric value")
        try:
            os.kill(pid, 0)
        except OSError as why:
            if why.errno == errno.ESRCH:
                # The pid doesn't exist.
                log.msg(f"Removing stale pidfile {pidfile}", isError=True)
                os.remove(pidfile)
            else:
                sys.exit(
                    "Can't check status of PID {} from pidfile {}: {}".format(
                        pid, pidfile, why
                    )
                )
        else:
            sys.exit(
                """\
Another twistd server is running, PID {}\n
This could either be a previously started instance of your application or a
different application entirely. To start a new one, either run it in some other
directory, or use the --pidfile and --logfile parameters to avoid clashes.
""".format(
                    pid
                )
            )


class UnixAppLogger(app.AppLogger):
    """
    A logger able to log to syslog, to files, and to stdout.

    @ivar _syslog: A flag indicating whether to use syslog instead of file
        logging.
    @type _syslog: C{bool}

    @ivar _syslogPrefix: If C{sysLog} is C{True}, the string prefix to use for
        syslog messages.
    @type _syslogPrefix: C{str}

    @ivar _nodaemon: A flag indicating the process will not be daemonizing.
    @type _nodaemon: C{bool}
    """

    def __init__(self, options):
        app.AppLogger.__init__(self, options)
        self._syslog = options.get("syslog", False)
        self._syslogPrefix = options.get("prefix", "")
        self._nodaemon = options.get("nodaemon", False)

    def _getLogObserver(self):
        """
        Create and return a suitable log observer for the given configuration.

        The observer will go to syslog using the prefix C{_syslogPrefix} if
        C{_syslog} is true.  Otherwise, it will go to the file named
        C{_logfilename} or, if C{_nodaemon} is true and C{_logfilename} is
        C{"-"}, to stdout.

        @return: An object suitable to be passed to C{log.addObserver}.
        """
        if self._syslog:
            from twisted.python import syslog

            return syslog.SyslogObserver(self._syslogPrefix).emit

        if self._logfilename == "-":
            if not self._nodaemon:
                sys.exit("Daemons cannot log to stdout, exiting!")
            logFile = sys.stdout
        elif self._nodaemon and not self._logfilename:
            logFile = sys.stdout
        else:
            if not self._logfilename:
                self._logfilename = "twistd.log"
            logFile = logfile.LogFile.fromFullPath(self._logfilename)
            try:
                import signal
            except ImportError:
                pass
            else:
                # Override if signal is set to None or SIG_DFL (0)
                if not signal.getsignal(signal.SIGUSR1):

                    def rotateLog(signal, frame):
                        from twisted.internet import reactor

                        reactor.callFromThread(logFile.rotate)

                    signal.signal(signal.SIGUSR1, rotateLog)
        return logger.textFileLogObserver(logFile)


def launchWithName(name):
    if name and name != sys.argv[0]:
        exe = os.path.realpath(sys.executable)
        log.msg("Changing process name to " + name)
        os.execv(exe, [name, sys.argv[0], "--originalname"] + sys.argv[1:])


class UnixApplicationRunner(app.ApplicationRunner):
    """
    An ApplicationRunner which does Unix-specific things, like fork,
    shed privileges, and maintain a PID file.
    """

    loggerFactory = UnixAppLogger

    def preApplication(self):
        """
        Do pre-application-creation setup.
        """
        checkPID(self.config["pidfile"])
        self.config["nodaemon"] = self.config["nodaemon"] or self.config["debug"]
        self.oldstdout = sys.stdout
        self.oldstderr = sys.stderr

    def _formatChildException(self, exception):
        """
        Format the C{exception} in preparation for writing to the
        status pipe.  This does the right thing on Python 2 if the
        exception's message is Unicode, and in all cases limits the
        length of the message afte* encoding to 100 bytes.

        This means the returned message may be truncated in the middle
        of a unicode escape.

        @type exception: L{Exception}
        @param exception: The exception to format.

        @return: The formatted message, suitable for writing to the
            status pipe.
        @rtype: L{bytes}
        """
        # On Python 2 this will encode Unicode messages with the ascii
        # codec and the backslashreplace error handler.
        exceptionLine = traceback.format_exception_only(exception.__class__, exception)[
            -1
        ]
        # remove the trailing newline
        formattedMessage = f"1 {exceptionLine.strip()}"
        # On Python 3, encode the message the same way Python 2's
        # format_exception_only does
        formattedMessage = formattedMessage.encode("ascii", "backslashreplace")
        # By this point, the message has been encoded, if appropriate,
        # with backslashreplace on both Python 2 and Python 3.
        # Truncating the encoded message won't make it completely
        # unreadable, and the reader should print out the repr of the
        # message it receives anyway.  What it will do, however, is
        # ensure that only 100 bytes are written to the status pipe,
        # ensuring that the child doesn't block because the pipe's
        # full.  This assumes PIPE_BUF > 100!
        return formattedMessage[:100]

    def postApplication(self):
        """
        To be called after the application is created: start the application
        and run the reactor. After the reactor stops, clean up PID files and
        such.
        """
        try:
            self.startApplication(self.application)
        except Exception as ex:
            statusPipe = self.config.get("statusPipe", None)
            if statusPipe is not None:
                message = self._formatChildException(ex)
                untilConcludes(os.write, statusPipe, message)
                untilConcludes(os.close, statusPipe)
            self.removePID(self.config["pidfile"])
            raise
        else:
            statusPipe = self.config.get("statusPipe", None)
            if statusPipe is not None:
                untilConcludes(os.write, statusPipe, b"0")
                untilConcludes(os.close, statusPipe)
        self.startReactor(None, self.oldstdout, self.oldstderr)
        self.removePID(self.config["pidfile"])

    def removePID(self, pidfile):
        """
        Remove the specified PID file, if possible.  Errors are logged, not
        raised.

        @type pidfile: C{str}
        @param pidfile: The path to the PID tracking file.
        """
        if not pidfile:
            return
        try:
            os.unlink(pidfile)
        except OSError as e:
            if e.errno == errno.EACCES or e.errno == errno.EPERM:
                log.msg("Warning: No permission to delete pid file")
            else:
                log.err(e, "Failed to unlink PID file:")
        except BaseException:
            log.err(None, "Failed to unlink PID file:")

    def setupEnvironment(self, chroot, rundir, nodaemon, umask, pidfile):
        """
        Set the filesystem root, the working directory, and daemonize.

        @type chroot: C{str} or L{None}
        @param chroot: If not None, a path to use as the filesystem root (using
            L{os.chroot}).

        @type rundir: C{str}
        @param rundir: The path to set as the working directory.

        @type nodaemon: C{bool}
        @param nodaemon: A flag which, if set, indicates that daemonization
            should not be done.

        @type umask: C{int} or L{None}
        @param umask: The value to which to change the process umask.

        @type pidfile: C{str} or L{None}
        @param pidfile: If not L{None}, the path to a file into which to put
            the PID of this process.
        """
        daemon = not nodaemon

        if chroot is not None:
            os.chroot(chroot)
            if rundir == ".":
                rundir = "/"
        os.chdir(rundir)
        if daemon and umask is None:
            umask = 0o077
        if umask is not None:
            os.umask(umask)
        if daemon:
            from twisted.internet import reactor

            self.config["statusPipe"] = self.daemonize(reactor)
        if pidfile:
            with open(pidfile, "wb") as f:
                f.write(b"%d" % (os.getpid(),))

    def daemonize(self, reactor):
        """
        Daemonizes the application on Unix. This is done by the usual double
        forking approach.

        @see: U{http://code.activestate.com/recipes/278731/}
        @see: W. Richard Stevens,
            "Advanced Programming in the Unix Environment",
            1992, Addison-Wesley, ISBN 0-201-56317-7

        @param reactor: The reactor in use.  If it provides
            L{IReactorDaemonize}, its daemonization-related callbacks will be
            invoked.

        @return: A writable pipe to be used to report errors.
        @rtype: C{int}
        """
        # If the reactor requires hooks to be called for daemonization, call
        # them. Currently the only reactor which provides/needs that is
        # KQueueReactor.
        if IReactorDaemonize.providedBy(reactor):
            reactor.beforeDaemonize()
        r, w = os.pipe()
        if os.fork():  # launch child and...
            code = self._waitForStart(r)
            os.close(r)
            os._exit(code)  # kill off parent
        os.setsid()
        if os.fork():  # launch child and...
            os._exit(0)  # kill off parent again.
        null = os.open("/dev/null", os.O_RDWR)
        for i in range(3):
            try:
                os.dup2(null, i)
            except OSError as e:
                if e.errno != errno.EBADF:
                    raise
        os.close(null)

        if IReactorDaemonize.providedBy(reactor):
            reactor.afterDaemonize()

        return w

    def _waitForStart(self, readPipe: int) -> int:
        """
        Wait for the daemonization success.

        @param readPipe: file descriptor to read start information from.
        @type readPipe: C{int}

        @return: code to be passed to C{os._exit}: 0 for success, 1 for error.
        @rtype: C{int}
        """
        data = untilConcludes(os.read, readPipe, 100)
        dataRepr = repr(data[2:])
        if data != b"0":
            msg = (
                "An error has occurred: {}\nPlease look at log "
                "file for more information.\n".format(dataRepr)
            )
            untilConcludes(sys.__stderr__.write, msg)
            return 1
        return 0

    def shedPrivileges(self, euid, uid, gid):
        """
        Change the UID and GID or the EUID and EGID of this process.

        @type euid: C{bool}
        @param euid: A flag which, if set, indicates that only the I{effective}
            UID and GID should be set.

        @type uid: C{int} or L{None}
        @param uid: If not L{None}, the UID to which to switch.

        @type gid: C{int} or L{None}
        @param gid: If not L{None}, the GID to which to switch.
        """
        if uid is not None or gid is not None:
            extra = euid and "e" or ""
            desc = f"{extra}uid/{extra}gid {uid}/{gid}"
            try:
                switchUID(uid, gid, euid)
            except OSError as e:
                log.msg(
                    "failed to set {}: {} (are you root?) -- "
                    "exiting.".format(desc, e)
                )
                sys.exit(1)
            else:
                log.msg(f"set {desc}")

    def startApplication(self, application):
        """
        Configure global process state based on the given application and run
        the application.

        @param application: An object which can be adapted to
            L{service.IProcess} and L{service.IService}.
        """
        process = service.IProcess(application)
        if not self.config["originalname"]:
            launchWithName(process.processName)
        self.setupEnvironment(
            self.config["chroot"],
            self.config["rundir"],
            self.config["nodaemon"],
            self.config["umask"],
            self.config["pidfile"],
        )

        service.IService(application).privilegedStartService()

        uid, gid = self.config["uid"], self.config["gid"]
        if uid is None:
            uid = process.uid
        if gid is None:
            gid = process.gid
        if uid is not None and gid is None:
            gid = pwd.getpwuid(uid).pw_gid

        self.shedPrivileges(self.config["euid"], uid, gid)
        app.startApplication(application, not self.config["no_save"])