#!/usr/bin/env python # $Id$ # # Copyright (C) 2015--2016 Parsons Government Services ("PARSONS") # Portions copyright (C) 2014 Dragon Research Labs ("DRL") # Portions copyright (C) 2009--2013 Internet Systems Consortium ("ISC") # Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN") # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notices and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND PARSONS, DRL, ISC, AND ARIN # DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT # SHALL PARSONS, DRL, ISC, OR ARIN BE LIABLE FOR ANY SPECIAL, DIRECT, # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ Start servers, using config file to figure out which servers the user wants started. """ import os import pwd import sys import time import signal import logging import argparse import subprocess import rpki.log import rpki.config import rpki.autoconf import rpki.daemonize from logging.handlers import SysLogHandler logger = logging.getLogger(__name__) signames = dict((getattr(signal, sig), sig) for sig in dir(signal) if sig.startswith("SIG") and sig.isalnum() and sig.isupper() and isinstance(getattr(signal, sig), int)) class Daemon(object): """ Representation and control of one daemon under our care. """ def __init__(self, name): self.name = name self.proc = None self.next_restart = 0 if cfg.getboolean(option = "start_" + name, section = "myrpki", default = False): self.cmd = (os.path.join(rpki.autoconf.libexecdir, name), "--foreground") else: self.cmd = () def start_maybe(self, output): if self.cmd and self.proc is None and time.time() > self.next_restart: try: self.proc = subprocess.Popen(self.cmd, stdout = output, stderr = output) self.next_restart = int(time.time() + args.restart_delay) logger.debug("Started %s[%s]", self.name, self.proc.pid) except: logger.exception("Trouble starting %s", self.name) def terminate(self): if self.proc is not None: try: logger.debug("Terminating daemon %s[%s]", self.name, self.proc.pid) self.proc.terminate() except: logger.exception("Trouble terminating %s[%s]", self.name, self.proc.pid) def delay(self): return max(0, int(self.next_restart - time.time())) if self.cmd and self.proc is None else 0 def reap(self): if self.proc is not None and self.proc.poll() is not None: code = self.proc.wait() if code < 0: logger.warn("%s[%s] exited on signal %s", self.name, self.proc.pid, signames.get(-code, "???")) else: logger.warn("%s[%s] exited with status %s", self.name, self.proc.pid, code) self.proc = None class Signals(object): """ Convert POSIX signals into something we can use in a loop at main program level. Assumes that we use signal.pause() to block, so simply receiving the signal is enough to wake us up. Calling the constructed Signals object with one or more signal numbers returns True if any of those signals have been received, and clears the internal flag for the first such signal. """ def __init__(self, *sigs): self._active = set() for sig in sigs: signal.signal(sig, self._handler) def _handler(self, sig, frame): self._active.add(sig) #logger.debug("Received %s", signames.get(sig, "???")) def __call__(self, *sigs): for sig in sigs: try: self._active.remove(sig) return True except KeyError: pass return False def non_negative_integer(s): if int(s) < 0: raise ValueError return s def positive_integer(s): if int(s) <= 0: raise ValueError return s if __name__ == "__main__": os.environ.update(TZ = "UTC") time.tzset() cfg = rpki.config.argparser(section = "rpki-nanny", doc = __doc__) cfg.add_argument("--restart-delay", type = positive_integer, default = 60, help = "how long to wait before restarting a crashed daemon") cfg.add_argument("--pidfile", default = os.path.join(rpki.daemonize.default_pid_directory, "rpki-nanny.pid"), help = "override default location of pid file") cfg.add_boolean_argument("--foreground", default = False, help = "whether to stay in foreground rather than daemonizing") cfg.add_boolean_argument("--capture-stdout-stderr", default = True, help = "whether to capture output incorrectly sent to stdout/stderr") cfg.add_logging_arguments() args = cfg.argparser.parse_args() # Drop privs before daemonizing or opening log file pw = pwd.getpwnam(rpki.autoconf.RPKI_USER) os.setgid(pw.pw_gid) os.setuid(pw.pw_uid) cfg.configure_logging(ident = "rpki-nanny", args = args) if not args.foreground: rpki.daemonize.daemon(pidfile = args.pidfile) if args.capture_stdout_stderr: try: logger_pipe = os.pipe() logger_pid = os.fork() if logger_pid == 0: os.close(logger_pipe[1]) with os.fdopen(logger_pipe[0]) as f: for line in f: logger.warn("Captured: %s", line.rstrip()) # Should never get here, but just in case logger.error("[Unexpected EOF in stdout/stderr capture logger]") sys.exit(1) else: os.close(logger_pipe[0]) except: logger.exception("Trouble setting up stdout/stderr capture process") sys.exit(1) daemon_output = logger_pipe[1] if args.capture_stdout_stderr else None signals = Signals(signal.SIGALRM, signal.SIGCHLD, signal.SIGTERM, signal.SIGINT) daemons = [Daemon(name) for name in ("irdbd", "rpkid", "pubd", "rootd")] exiting = False try: while not exiting or not all(daemon.proc is None for daemon in daemons): if not exiting and signals(signal.SIGTERM, signal.SIGINT): logger.info("Received exit signal") exiting = True for daemon in daemons: daemon.terminate() if not exiting: for daemon in daemons: daemon.start_maybe(daemon_output) alarms = tuple(daemon.delay() for daemon in daemons) signal.alarm(min(a for a in alarms if a > 0) + 1 if any(alarms) else 0) if not signals(signal.SIGCHLD, signal.SIGALRM): signal.pause() for daemon in daemons: daemon.reap() if args.capture_stdout_stderr: os.kill(logger_pid, signal.SIGTERM) except: logger.exception("Unhandled exception in main loop") for daemon in daemons: daemon.terminate() sys.exit(1)