• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / zdaemon / 12825577578

17 Jan 2025 08:41AM UTC coverage: 79.914% (-0.08%) from 79.992%
12825577578

Pull #37

github

web-flow
Trigger CI
Pull Request #37: Update Python version support.

361 of 512 branches covered (70.51%)

Branch coverage included in aggregate %.

5 of 15 new or added lines in 5 files covered. (33.33%)

5 existing lines in 4 files now uncovered.

1676 of 2037 relevant lines covered (82.28%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.8
/src/zdaemon/zdrun.py
1
#!python
2
##############################################################################
3
#
4
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
5
# All Rights Reserved.
6
#
7
# This software is subject to the provisions of the Zope Public License,
8
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
9
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
10
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
11
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
12
# FOR A PARTICULAR PURPOSE
13
#
14
##############################################################################
15
"""zrdun -- run an application as a daemon.
16

17
Usage: python zrdun.py [zrdun-options] program [program-arguments]
18
"""
19

20
import errno
1✔
21
import fcntl
1✔
22
import logging
1✔
23
import os
1✔
24
import select
1✔
25
import signal
1✔
26
import socket
1✔
27
import subprocess
1✔
28
import sys
1✔
29
import threading
1✔
30
import time
1✔
31
from stat import ST_MODE
1✔
32

33

34
if __name__ == "__main__":
1!
35
    # Add the parent of the script directory to the module search path
36
    # (but only when the script is run from inside the zdaemon package)
37
    from os.path import abspath
×
38
    from os.path import basename
×
39
    from os.path import dirname
×
40
    from os.path import normpath
×
41
    scriptdir = dirname(normpath(abspath(sys.argv[0])))
×
42
    if basename(scriptdir).lower() == "zdaemon":
×
43
        sys.path.append(dirname(scriptdir))
×
44
    here = os.path.dirname(os.path.realpath(__file__))
×
45
    swhome = os.path.dirname(here)
×
46
    for parts in [("src",), ("lib", "python"), ("Lib", "site-packages")]:
×
47
        d = os.path.join(swhome, *(parts + ("zdaemon",)))
×
48
        if os.path.isdir(d):
×
49
            d = os.path.join(swhome, *parts)
×
50
            sys.path.insert(0, d)
×
51
            break
×
52

53
from ZConfig.components.logger.loghandler import reopenFiles
1✔
54

55
from zdaemon.zdoptions import RunnerOptions
1✔
56

57

58
def string_list(arg):
1✔
59
    return arg.split()
×
60

61

62
class ZDRunOptions(RunnerOptions):
1✔
63

64
    __doc__ = __doc__
1✔
65

66
    positional_args_allowed = 1
1✔
67
    logsectionname = "runner.eventlog"
1✔
68
    program = None
1✔
69

70
    def __init__(self):
1✔
71
        RunnerOptions.__init__(self)
1✔
72
        self.add("schemafile", short="S:", long="schema=",
1✔
73
                 default="schema.xml",
74
                 handler=self.set_schemafile)
75
        self.add("stoptimeut", "runner.stop_timeout")
1✔
76
        self.add("starttestprogram", "runner.start_test_program")
1✔
77

78
    def set_schemafile(self, file):
1✔
79
        self.schemafile = file
1✔
80

81
    def realize(self, *args, **kwds):
1✔
82
        RunnerOptions.realize(self, *args, **kwds)
1✔
83
        if self.args:
1!
84
            self.program = self.args
1✔
85
        if not self.program:
1!
86
            self.usage("no program specified (use positional args)")
×
87
        if self.sockname:
1!
88
            # Convert socket name to absolute path
89
            self.sockname = os.path.abspath(self.sockname)
1✔
90
        if self.config_logger is None:
1✔
91
            # This doesn't perform any configuration of the logging
92
            # package, but that's reasonable in this case.
93
            self.logger = logging.getLogger()
1✔
94
        else:
95
            self.logger = self.config_logger()
1✔
96

97
    def load_logconf(self, sectname):
1✔
98
        """Load alternate eventlog if the specified section isn't present."""
99
        RunnerOptions.load_logconf(self, sectname)
1✔
100
        if self.config_logger is None and sectname != "eventlog":
1✔
101
            RunnerOptions.load_logconf(self, "eventlog")
1✔
102

103

104
class Subprocess:
1✔
105

106
    """A class to manage a subprocess."""
107

108
    # Initial state; overridden by instance variables
109
    pid = 0  # Subprocess pid; 0 when not running
1✔
110
    lasttime = 0  # Last time the subprocess was started; 0 if never
1✔
111

112
    def __init__(self, options, args=None, child_exits=None):
1✔
113
        """Constructor.
114

115
        Arguments are a ZDRunOptions instance and a list of program
116
        arguments; the latter's first item must be the program name.
117
        """
118
        if args is None:
1!
119
            args = options.args
1✔
120
        if not args:
1!
121
            options.usage("missing 'program' argument")
×
122
        self.options = options
1✔
123
        self.args = args
1✔
124
        self.testing = set()
1✔
125
        self.child_exits = child_exits
1✔
126
        self._set_filename(args[0])
1✔
127

128
    def _set_filename(self, program):
1✔
129
        """Internal: turn a program name into a file name, using $PATH."""
130
        if "/" in program:
1✔
131
            filename = program
1✔
132
            try:
1✔
133
                st = os.stat(filename)
1✔
NEW
134
            except OSError:
×
135
                self.options.usage("can't stat program %r" % program)
×
136
        else:
137
            path = get_path()
1✔
138
            for dir in path:
1✔
139
                filename = os.path.join(dir, program)
1✔
140
                try:
1✔
141
                    st = os.stat(filename)
1✔
142
                except OSError:
1✔
143
                    continue
1✔
144
                mode = st[ST_MODE]
1✔
145
                if mode & 0o111:
1!
146
                    break
1✔
147
            else:
148
                self.options.usage("can't find program %r on PATH %s" %
1✔
149
                                   (program, path))
150
        if not os.access(filename, os.X_OK):
1!
151
            self.options.usage("no permission to run program %r" % filename)
×
152
        self.filename = filename
1✔
153

154
    def test(self, pid):
1✔
155
        starttestprogram = self.options.starttestprogram
1✔
156
        logger = self.options.logger
1✔
157
        try:
1✔
158
            while self.pid == pid:
1!
159
                try:
1✔
160
                    with subprocess.Popen(starttestprogram) as p:
1✔
161
                        # uncomment the following line to force
162
                        # (for testing purposes) a race between
163
                        # the ``wait`` below and the ``SIGCHLD`` handler
164
                        # import time; time.sleep(0.01)
165
                        sts = p.wait()
1✔
166
                        # ``sts`` is usually the return status.
167
                        # However, the true return status may have been
168
                        # captured by the ``SIGCHLD`` handler.
169
                        # In this case, ``str == 0`` and the
170
                        # true return status can be found via ``child_exits``.
171
                        if not sts and not self.child_exits.fetch(p.pid):
1✔
172
                            logger.debug("start test succeeded")
1✔
173
                            break
1✔
174
                        logger.debug("start test failed")
1✔
175
                except Exception:  # pragma: nocover
176
                    logger.critical("start test raised",
177
                                    exc_info=True)
178
                    break  # likely a permanent error
179
                time.sleep(1)
1✔
180
        finally:
181
            self.testing.remove(pid)
1✔
182

183
    def spawn(self):
1✔
184
        """Start the subprocess.  It must not be running already.
185

186
        Return the process id.  If the fork() call fails, return 0.
187
        """
188
        assert not self.pid
1✔
189
        self.lasttime = time.time()
1✔
190
        try:
1✔
191
            pid = os.fork()
1✔
NEW
192
        except OSError:
×
193
            return 0
×
194
        if pid != 0:
1✔
195
            # Parent
196
            self.pid = pid
1✔
197
            if self.options.starttestprogram:
1✔
198
                self.testing.add(pid)
1✔
199
                thread = threading.Thread(target=self.test, args=(pid,))
1✔
200
                thread.setDaemon(True)
1✔
201
                thread.start()
1✔
202

203
            self.options.logger.info("spawned process pid=%d" % pid)
1✔
204
            return pid
1✔
205
        else:  # pragma: nocover
206
            # Child
207
            try:
208
                # Close file descriptors except std{in,out,err}.
209
                # XXX We don't know how many to close; hope 100 is plenty.
210
                for i in range(3, 100):
211
                    try:
212
                        os.close(i)
213
                    except OSError:
214
                        pass
215
                try:
216
                    os.execv(self.filename, self.args)
217
                except OSError as err:
218
                    sys.stderr.write("can't exec %r: %s\n" %
219
                                     (self.filename, err))
220
                    sys.stderr.flush()  # just in case
221
            finally:
222
                os._exit(127)
223
            # Does not return
224

225
    def kill(self, sig):
1✔
226
        """Send a signal to the subprocess.  This may or may not kill it.
227

228
        Return None if the signal was sent, or an error message string
229
        if an error occurred or if the subprocess is not running.
230
        """
231
        if not self.pid:
1!
232
            return "no subprocess running"
×
233
        try:
1✔
234
            os.kill(self.pid, sig)
1✔
NEW
235
        except OSError as msg:
×
236
            return str(msg)
×
237
        return None
1✔
238

239
    def setstatus(self, sts):
1✔
240
        """Set process status returned by wait() or waitpid().
241

242
        This simply notes the fact that the subprocess is no longer
243
        running by setting self.pid to 0.
244
        """
245
        self.pid = 0
1✔
246

247

248
class Daemonizer:
1✔
249

250
    def main(self, args=None):
1✔
251
        self.options = ZDRunOptions()
1✔
252
        self.options.realize(args)
1✔
253
        self.logger = self.options.logger
1✔
254
        self.run()
1✔
255

256
    def run(self):
1✔
257
        self.child_exits = _ChildExits()
1✔
258
        self.proc = Subprocess(self.options, child_exits=self.child_exits)
1✔
259
        self.opensocket()
1✔
260
        try:
1✔
261
            self.setsignals()
1✔
262
            if self.options.daemon:
1✔
263
                self.daemonize()
1✔
264
            try:
1✔
265
                self.runforever()
1✔
266
            except Exception:  # pragma: nocover
267
                self.logger.critical("runforever raised", exc_info=True)
268
        finally:
269
            try:
1✔
270
                os.unlink(self.options.sockname)
1✔
NEW
271
            except OSError:
×
272
                pass
×
273

274
    mastersocket = None
1✔
275
    commandsocket = None
1✔
276

277
    def opensocket(self):
1✔
278
        sockname = self.options.sockname
1✔
279
        tempname = "%s.%d" % (sockname, os.getpid())
1✔
280
        self.unlink_quietly(tempname)
1✔
281
        while True:
1✔
282
            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1✔
283
            try:
1✔
284
                sock.bind(tempname)
1✔
285
                os.chmod(tempname, 0o700)
1✔
286
                try:
1✔
287
                    os.link(tempname, sockname)
1✔
288
                    break
1✔
NEW
289
                except OSError:
×
290
                    # Lock contention, or stale socket.
291
                    self.checkopen()
×
292
                    # Stale socket -- delete, sleep, and try again.
293
                    msg = "Unlinking stale socket %s; sleep 1" % sockname
×
294
                    sys.stderr.write(msg + "\n")
×
295
                    sys.stderr.flush()  # just in case
×
296
                    self.logger.warn(msg)
×
297
                    self.unlink_quietly(sockname)
×
298
                    sock.close()
×
299
                    time.sleep(1)
×
300
                    continue
×
301
            finally:
302
                self.unlink_quietly(tempname)
1✔
303
        sock.listen(1)
1✔
304
        sock.setblocking(0)
1✔
305
        try:  # PEP 446, Python >= 3.4
1✔
306
            sock.set_inheritable(True)
1✔
307
        except AttributeError:
×
308
            pass
×
309
        self.mastersocket = sock
1✔
310

311
    def unlink_quietly(self, filename):
1✔
312
        try:
1✔
313
            os.unlink(filename)
1✔
314
        except OSError:
1✔
315
            pass
1✔
316

317
    def checkopen(self):
1✔
318
        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
×
319
        try:
×
320
            s.connect(self.options.sockname)
×
321
            s.send(b"status\n")
×
322
            data = s.recv(1000).decode()
×
323
            s.close()
×
324
        except OSError:
×
325
            pass
×
326
        else:
327
            data = data.rstrip("\n")
×
328
            msg = ("Another zrdun is already up using socket %r:\n%s" %
×
329
                   (self.options.sockname, data))
330
            sys.stderr.write(msg + "\n")
×
331
            sys.stderr.flush()  # just in case
×
332
            self.logger.critical(msg)
×
333
            sys.exit(1)
×
334

335
    def setsignals(self):
1✔
336
        signal.signal(signal.SIGTERM, self.sigexit)
1✔
337
        signal.signal(signal.SIGHUP, self.sigexit)
1✔
338
        signal.signal(signal.SIGINT, self.sigexit)
1✔
339
        signal.signal(signal.SIGCHLD, self.sigchild)
1✔
340

341
    def sigexit(self, sig, frame):
1✔
342
        self.logger.critical("daemon manager killed by %s" % signame(sig))
×
343
        sys.exit(1)
×
344

345
    waitstatus = None
1✔
346

347
    def sigchild(self, sig, frame):
1✔
348
        try:
1✔
349
            pid, sts = os.waitpid(-1, os.WNOHANG)
1✔
NEW
350
        except OSError:
×
351
            return
×
352
        if pid:
1✔
353
            if pid == self.proc.pid:
1✔
354
                self.logger.debug("controlled process %s exited", pid)
1✔
355
                self.waitstatus = pid, sts
1✔
356
            else:  # pragma: nocover
357
                # this indicates a race between this ``SIGCHLD`` handler
358
                # and a ``wait``.
359
                # Record in ``child_exits`` to allow the ``wait`` caller
360
                # to get the correct exit status
361
                self.logger.debug("unknown process %s exited", pid)
362
                self.child_exits[pid] = sts
363

364
    transcript = None
1✔
365

366
    def daemonize(self):
1✔
367

368
        # To daemonize, we need to become the leader of our own session
369
        # (process) group.  If we do not, signals sent to our
370
        # parent process will also be sent to us.   This might be bad because
371
        # signals such as SIGINT can be sent to our parent process during
372
        # normal (uninteresting) operations such as when we press Ctrl-C in the
373
        # parent terminal window to escape from a logtail command.
374
        # To disassociate ourselves from our parent's session group we use
375
        # os.setsid.  It means "set session id", which has the effect of
376
        # disassociating a process from is current session and process group
377
        # and setting itself up as a new session leader.
378
        #
379
        # Unfortunately we cannot call setsid if we're already a session group
380
        # leader, so we use "fork" to make a copy of ourselves that is
381
        # guaranteed to not be a session group leader.
382
        #
383
        # We also change directories, set stderr and stdout to null, and
384
        # change our umask.
385
        #
386
        # This explanation was (gratefully) garnered from
387
        # http://www.hawklord.uklinux.net/system/daemons/d3.htm
388

389
        pid = os.fork()
1✔
390
        if pid != 0:  # pragma: nocover
391
            # Parent
392
            self.logger.debug("daemon manager forked; parent exiting")
393
            os._exit(0)
394
        # Child
395
        self.logger.info("daemonizing the process")
1✔
396
        if self.options.directory:
1!
397
            try:
×
398
                os.chdir(self.options.directory)
×
NEW
399
            except OSError as err:
×
400
                self.logger.warn("can't chdir into %r: %s"
×
401
                                 % (self.options.directory, err))
402
            else:
403
                self.logger.info("set current directory: %r"
×
404
                                 % self.options.directory)
405
        os.close(0)
1✔
406
        sys.stdin = sys.__stdin__ = open("/dev/null")
1✔
407
        self.transcript = Transcript(self.options.transcript)
1✔
408
        os.setsid()
1✔
409
        os.umask(self.options.umask)
1✔
410
        # XXX Stevens, in his Advanced Unix book, section 13.3 (page
411
        # 417) recommends calling umask(0) and closing unused
412
        # file descriptors.  In his Network Programming book, he
413
        # additionally recommends ignoring SIGHUP and forking again
414
        # after the setsid() call, for obscure SVR4 reasons.
415

416
    should_be_up = True
1✔
417
    delay = 0  # If nonzero, delay starting or killing until this time
1✔
418
    killing = 0  # If true, send SIGKILL when delay expires
1✔
419
    proc = None  # Subprocess instance
1✔
420

421
    def runforever(self):
1✔
422
        sig_r, sig_w = os.pipe()
1✔
423
        fcntl.fcntl(
1✔
424
            sig_r, fcntl.F_SETFL, fcntl.fcntl(
425
                sig_r, fcntl.F_GETFL) | os.O_NONBLOCK)
426
        fcntl.fcntl(
1✔
427
            sig_w, fcntl.F_SETFL, fcntl.fcntl(
428
                sig_w, fcntl.F_GETFL) | os.O_NONBLOCK)
429
        signal.set_wakeup_fd(sig_w)
1✔
430
        self.logger.info("daemon manager started")
1✔
431
        while self.should_be_up or self.proc.pid:
1✔
432
            if self.should_be_up and not self.proc.pid and not self.delay:
1✔
433
                pid = self.proc.spawn()
1✔
434
                if not pid:
1!
435
                    # Can't fork.  Try again later...
436
                    self.delay = time.time() + self.backofflimit
×
437
            if self.waitstatus:
1✔
438
                self.reportstatus()
1✔
439
            r, w, x = [self.mastersocket, sig_r], [], []
1✔
440
            if self.commandsocket:
1✔
441
                r.append(self.commandsocket)
1✔
442
            timeout = self.options.backofflimit
1✔
443
            if self.delay:
1✔
444
                timeout = max(0, min(timeout, self.delay - time.time()))
1✔
445
                if timeout <= 0:
1✔
446
                    self.delay = 0
1✔
447
                    if self.killing and self.proc.pid:
1✔
448
                        self.proc.kill(signal.SIGKILL)
1✔
449
                        self.delay = time.time() + self.options.backofflimit
1✔
450
            try:
1✔
451
                r, w, x = select.select(r, w, x, timeout)
1✔
452
            except OSError as err:
×
453
                if err.args[0] != errno.EINTR:
×
454
                    raise
×
455
                r = w = x = []
×
456
            if self.waitstatus:
1✔
457
                self.reportstatus()
1✔
458
            if self.commandsocket and self.commandsocket in r:
1✔
459
                try:
1✔
460
                    self.dorecv()
1✔
461
                except OSError as msg:
×
462
                    self.logger.exception("socket.error in dorecv(): %s"
×
463
                                          % str(msg))
464
                    self.commandsocket = None
×
465
            if self.mastersocket in r:
1✔
466
                try:
1✔
467
                    self.doaccept()
1✔
468
                except OSError as msg:
×
469
                    self.logger.exception("socket.error in doaccept(): %s"
×
470
                                          % str(msg))
471
                    self.commandsocket = None
×
472
            if sig_r in r:
1✔
473
                os.read(sig_r, 1)  # don't let the buffer fill up
1✔
474
        self.logger.info("Exiting")
1✔
475
        sys.exit(0)
1✔
476

477
    def reportstatus(self):
1✔
478
        pid, sts = self.waitstatus
1✔
479
        self.waitstatus = None
1✔
480
        es, msg = decode_wait_status(sts)
1✔
481
        msg = "pid %d: " % pid + msg
1✔
482
        if pid != self.proc.pid:
1!
483
            msg = "unknown(!=%s) " % self.proc.pid + msg
×
484
            self.logger.warn(msg)
×
485
        else:
486
            killing = self.killing
1✔
487
            if killing:
1✔
488
                self.killing = 0
1✔
489
                self.delay = 0
1✔
490
            else:
491
                self.governor()
1✔
492
            self.proc.setstatus(sts)
1✔
493
            if es in self.options.exitcodes and not killing:
1✔
494
                msg = msg + "; exiting now"
1✔
495
                self.logger.info(msg)
1✔
496
                sys.exit(es)
1✔
497
            self.logger.info(msg)
1✔
498

499
    backoff = 0
1✔
500

501
    def governor(self):
1✔
502
        # Back off if respawning too frequently
503
        now = time.time()
1✔
504
        if not self.proc.lasttime:
1!
505
            pass
×
506
        elif now - self.proc.lasttime < self.options.backofflimit:
1✔
507
            # Exited rather quickly; slow down the restarts
508
            self.backoff += 1
1✔
509
            if self.backoff >= self.options.backofflimit:
1✔
510
                if self.options.forever:
1!
511
                    self.backoff = self.options.backofflimit
×
512
                else:
513
                    self.logger.critical("restarting too frequently; quit")
1✔
514
                    sys.exit(1)
1✔
515
            self.logger.info("sleep %s to avoid rapid restarts" % self.backoff)
1✔
516
            self.delay = now + self.backoff
1✔
517
        else:
518
            # Reset the backoff timer
519
            self.backoff = 0
1✔
520
            self.delay = 0
1✔
521

522
    def doaccept(self):
1✔
523
        if self.commandsocket:
1!
524
            # Give up on previous command socket!
525
            self.sendreply("Command superseded by new command")
×
526
            self.commandsocket.close()
×
527
            self.commandsocket = None
×
528
        self.commandsocket, addr = self.mastersocket.accept()
1✔
529
        try:  # PEP 446, Python >= 3.4
1✔
530
            self.commandsocket.set_inheritable(True)
1✔
531
        except AttributeError:
×
532
            pass
×
533
        self.commandbuffer = b""
1✔
534

535
    def dorecv(self):
1✔
536
        data = self.commandsocket.recv(1000)
1✔
537
        if not data:
1!
538
            self.sendreply("Command not terminated by newline")
×
539
            self.commandsocket.close()
×
540
            self.commandsocket = None
×
541
        self.commandbuffer += data
1✔
542
        if b"\n" in self.commandbuffer:
1!
543
            self.docommand()
1✔
544
            self.commandsocket.close()
1✔
545
            self.commandsocket = None
1✔
546
        elif len(self.commandbuffer) > 10000:
×
547
            self.sendreply("Command exceeds 10 KB")
×
548
            self.commandsocket.close()
×
549
            self.commandsocket = None
×
550

551
    def docommand(self):
1✔
552
        lines = self.commandbuffer.split(b"\n")
1✔
553
        args = lines[0].split()
1✔
554
        if not args:
1!
555
            self.sendreply("Empty command")
×
556
            return
×
557
        command = args[0].decode()
1✔
558
        methodname = "cmd_" + command
1✔
559
        method = getattr(self, methodname, None)
1✔
560
        if method:
1!
561
            method([a.decode() for a in args])
1✔
562
        else:
563
            self.sendreply("Unknown command %r; 'help' for a list" % command)
×
564

565
    def cmd_start(self, args):
1✔
566
        self.should_be_up = True
×
567
        self.backoff = 0
×
568
        self.delay = 0
×
569
        self.killing = 0
×
570
        if not self.proc.pid:
×
571
            self.proc.spawn()
×
572
            self.sendreply("Application started")
×
573
        else:
574
            self.sendreply("Application already started")
×
575

576
    def cmd_stop(self, args):
1✔
577
        self.should_be_up = False
1✔
578
        self.backoff = 0
1✔
579
        self.delay = 0
1✔
580
        self.killing = 0
1✔
581
        if self.proc.pid:
1✔
582
            self.proc.kill(signal.SIGTERM)
1✔
583
            self.sendreply("Sent SIGTERM")
1✔
584
            self.killing = 1
1✔
585
            if self.options.stoptimeut:
1✔
586
                self.delay = time.time() + self.options.stoptimeut
1✔
587
        else:
588
            self.sendreply("Application already stopped")
1✔
589

590
    def cmd_restart(self, args):
1✔
591
        self.should_be_up = True
1✔
592
        self.backoff = 0
1✔
593
        self.delay = 0
1✔
594
        self.killing = 0
1✔
595
        if self.proc.pid:
1!
596
            self.proc.kill(signal.SIGTERM)
1✔
597
            self.sendreply("Sent SIGTERM; will restart later")
1✔
598
            self.killing = 1
1✔
599
            if self.options.stoptimeut:
1!
600
                self.delay = time.time() + self.options.stoptimeut
1✔
601
        else:
602
            self.proc.spawn()
×
603
            self.sendreply("Application started")
×
604

605
    def cmd_kill(self, args):
1✔
606
        if args[1:]:
×
607
            try:
×
608
                sig = int(args[1])
×
609
            except BaseException:
×
610
                self.sendreply("Bad signal %r" % args[1])
×
611
                return
×
612
        else:
613
            sig = signal.SIGTERM
×
614
        if not self.proc.pid:
×
615
            self.sendreply("Application not running")
×
616
        else:
617
            msg = self.proc.kill(sig)
×
618
            if msg:
×
619
                self.sendreply("Kill %d failed: %s" % (sig, msg))
×
620
            else:
621
                self.sendreply("Signal %d sent" % sig)
×
622

623
    def cmd_status(self, args):
1✔
624
        if not self.proc.pid:
1✔
625
            status = "stopped"
1✔
626
        else:
627
            status = "running"
1✔
628
        self.sendreply("status=%s\n" % status +
1✔
629
                       "now=%r\n" % time.time() +
630
                       "should_be_up=%d\n" % self.should_be_up +
631
                       "delay=%r\n" % self.delay +
632
                       "backoff=%r\n" % self.backoff +
633
                       "lasttime=%r\n" % self.proc.lasttime +
634
                       "application=%r\n" % self.proc.pid +
635
                       "testing=%d\n" % bool(self.proc.testing) +
636
                       "manager=%r\n" % os.getpid() +
637
                       "backofflimit=%r\n" % self.options.backofflimit +
638
                       "filename=%r\n" % self.proc.filename +
639
                       "args=%r\n" % self.proc.args)
640

641
    def cmd_reopen_transcript(self, args):
1✔
642
        reopenFiles()
1✔
643
        if self.transcript is not None:
1!
644
            self.transcript.reopen()
1✔
645

646
    def sendreply(self, msg):
1✔
647
        try:
1✔
648
            if not msg.endswith("\n"):
1✔
649
                msg = msg + "\n"
1✔
650
            msg = msg.encode()
1✔
651
            if hasattr(self.commandsocket, "sendall"):
1✔
652
                self.commandsocket.sendall(msg)
1✔
653
            else:  # pragma: nocover
654
                # This is quadratic, but msg is rarely more than 100 bytes :-)
655
                while msg:
656
                    sent = self.commandsocket.send(msg)
657
                    msg = msg[sent:]
658
        except OSError as msg:
×
659
            self.logger.warn("Error sending reply: %s" % str(msg))
×
660

661

662
class Transcript:
1✔
663

664
    def __init__(self, filename):
1✔
665
        self.read_from, w = os.pipe()
1✔
666
        os.dup2(w, 1)
1✔
667
        sys.stdout = sys.__stdout__ = os.fdopen(1, "w", 1)
1✔
668
        os.dup2(w, 2)
1✔
669
        sys.stderr = sys.__stderr__ = os.fdopen(2, "w", 1)
1✔
670
        self.filename = filename
1✔
671
        self.file = open(filename, 'ab', 0)
1✔
672
        self.write = self.file.write
1✔
673
        self.lock = threading.Lock()
1✔
674
        thread = threading.Thread(target=self.copy)
1✔
675
        thread.setDaemon(True)
1✔
676
        thread.start()
1✔
677

678
    def copy(self):
1✔
679
        try:
1✔
680
            lock = self.lock
1✔
681
            i = [self.read_from]
1✔
682
            o = e = []
1✔
683
            while True:
1✔
684
                ii, oo, ee = select.select(i, o, e)
1✔
685
                with lock:
1✔
686
                    for fd in ii:
1✔
687
                        self.write(os.read(fd, 8192))
1✔
688
        finally:
689
            # since there's no reader from this pipe we want the other side to
690
            # get a SIGPIPE as soon as it tries to write to it, instead of
691
            # deadlocking when the pipe buffer becomes full.
692
            os.close(self.read_from)
×
693

694
    def reopen(self):
1✔
695
        new_file = open(self.filename, 'ab', 0)
1✔
696
        with self.lock:
1✔
697
            self.file.close()
1✔
698
            self.file = new_file
1✔
699
            self.write = self.file.write
1✔
700

701

702
# Helpers for dealing with signals and exit status
703

704
def decode_wait_status(sts):
1✔
705
    """Decode the status returned by wait() or waitpid().
706

707
    Return a tuple (exitstatus, message) where exitstatus is the exit
708
    status, or -1 if the process was killed by a signal; and message
709
    is a message telling what happened.  It is the caller's
710
    responsibility to display the message.
711
    """
712
    if os.WIFEXITED(sts):
1✔
713
        es = os.WEXITSTATUS(sts) & 0xffff
1✔
714
        msg = "exit status %s" % es
1✔
715
        return es, msg
1✔
716
    elif os.WIFSIGNALED(sts):
1!
717
        sig = os.WTERMSIG(sts)
1✔
718
        msg = "terminated by %s" % signame(sig)
1✔
719
        if hasattr(os, "WCOREDUMP"):
1!
720
            iscore = os.WCOREDUMP(sts)
1✔
721
        else:
722
            iscore = sts & 0x80
×
723
        if iscore:
1!
724
            msg += " (core dumped)"
×
725
        return -1, msg
1✔
726
    else:
727
        msg = "unknown termination cause 0x%04x" % sts
×
728
        return -1, msg
×
729

730

731
_signames = None
1✔
732

733

734
def signame(sig):
1✔
735
    """Return a symbolic name for a signal.
736

737
    Return "signal NNN" if there is no corresponding SIG name in the
738
    signal module.
739
    """
740

741
    if _signames is None:
1✔
742
        _init_signames()
1✔
743
    return _signames.get(sig) or "signal %d" % sig
1✔
744

745

746
def _init_signames():
1✔
747
    global _signames
748
    d = {}
1✔
749
    for k, v in signal.__dict__.items():
1✔
750
        k_startswith = getattr(k, "startswith", None)
1✔
751
        if k_startswith is None:  # pragma: nocover
752
            continue
753
        if k_startswith("SIG") and not k_startswith("SIG_"):
1✔
754
            d[v] = k
1✔
755
    _signames = d
1✔
756

757

758
def get_path():
1✔
759
    """Return a list corresponding to $PATH, or a default."""
760
    path = ["/bin", "/usr/bin", "/usr/local/bin"]
1✔
761
    if "PATH" in os.environ:
1!
762
        p = os.environ["PATH"]
1✔
763
        if p:
1!
764
            path = p.split(os.pathsep)
1✔
765
    return path
1✔
766

767

768
class _ChildExits(dict):
1✔
769
    """map ``pid`` to exit status or ``None``."""
770

771
    def fetch(self, pid):
1✔
772
        """fetch and reset status for *pid*."""
773
        st = self.get(pid)
1✔
774
        if st is not None:
1✔
775
            # there is only a negligable race risk
776
            del self[pid]
1✔
777
        return st
1✔
778

779

780
# Main program
781

782
def main(args=None):
1✔
783
    assert os.name == "posix", "This code makes many Unix-specific assumptions"
1✔
784

785
    d = Daemonizer()
1✔
786
    d.main(args)
1✔
787

788

789
if __name__ == '__main__':
790
    main()
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc