diff options
Diffstat (limited to 'subplot/vendored/daemon.py')
-rw-r--r-- | subplot/vendored/daemon.py | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/subplot/vendored/daemon.py b/subplot/vendored/daemon.py new file mode 100644 index 0000000..febf392 --- /dev/null +++ b/subplot/vendored/daemon.py @@ -0,0 +1,139 @@ +import logging +import os +import signal +import socket +import subprocess +import time + + +# Start a daemon that will open a port on localhost. +def daemon_start_on_port(ctx, path=None, args=None, name=None, port=None): + daemon_start(ctx, path=path, args=args, name=name) + daemon_wait_for_port("localhost", port) + + +# Start a daeamon, get its PID. Don't wait for a port or anything. This is +# meant for background processes that don't have port. Useful for testing the +# lib/daemon library of Subplot, but not much else. +def daemon_start(ctx, path=None, args=None, name=None): + runcmd_run = globals()["runcmd_run"] + runcmd_exit_code_is = globals()["runcmd_exit_code_is"] + runcmd_get_exit_code = globals()["runcmd_get_exit_code"] + runcmd_get_stderr = globals()["runcmd_get_stderr"] + runcmd_prepend_to_path = globals()["runcmd_prepend_to_path"] + + argv = [path] + args.split() + + logging.debug(f"Starting daemon {name}") + logging.debug(f" ctx={ctx.as_dict()}") + logging.debug(f" name={name}") + logging.debug(f" path={path}") + logging.debug(f" args={args}") + logging.debug(f" argv={argv}") + + ns = ctx.declare("_daemon") + + this = ns[name] = { + "pid-file": f"{name}.pid", + "stderr": f"{name}.stderr", + "stdout": f"{name}.stdout", + } + + # Debian installs `daemonize` to /usr/sbin, which isn't part of the minimal + # environment that Subplot sets up. So we add /usr/sbin to the PATH. + runcmd_prepend_to_path(ctx, "/usr/sbin") + runcmd_run( + ctx, + [ + "daemonize", + "-c", + os.getcwd(), + "-p", + this["pid-file"], + "-e", + this["stderr"], + "-o", + this["stdout"], + ] + + argv, + ) + + # Check that daemonize has exited OK. If it hasn't, it didn't start the + # background process at all. If so, log the stderr in case there was + # something useful there for debugging. + exit = runcmd_get_exit_code(ctx) + if exit != 0: + stderr = runcmd_get_stderr(ctx) + logging.error(f"daemon {name} stderr: {stderr}") + runcmd_exit_code_is(ctx, 0) + + # Get the pid of the background process, from the pid file created by + # daemonize. We don't need to wait for it, since we know daemonize already + # exited. If it isn't there now, it's won't appear later. + if not os.path.exists(this["pid-file"]): + raise Exception("daemonize didn't create a PID file") + + this["pid"] = _daemon_wait_for_pid(this["pid-file"], 10.0) + + logging.debug(f"Started daemon {name}") + logging.debug(f" pid={this['pid']}") + logging.debug(f" ctx={ctx.as_dict()}") + + +def _daemon_wait_for_pid(filename, timeout): + start = time.time() + while time.time() < start + timeout: + with open(filename) as f: + data = f.read().strip() + if data: + return int(data) + raise Exception("daemonize created a PID file without a PID") + + +def daemon_wait_for_port(host, port, timeout=3.0): + addr = (host, port) + until = time.time() + timeout + while True: + try: + s = socket.create_connection(addr, timeout=timeout) + s.close() + return + except socket.timeout: + logging.error(f"daemon did not respond at port {port} within {timeout} seconds") + raise + except socket.error as e: + logging.info(f"could not connect to daemon at {port}: {e}") + pass + if time.time() >= until: + logging.error(f"could not connect to daemon at {port} within {timeout} seconds") + raise ConnectionRefusedError() + + +# Stop a daemon. +def daemon_stop(ctx, name=None): + logging.debug(f"Stopping daemon {name}") + ns = ctx.declare("_daemon") + logging.debug(f" ns={ns}") + pid = ns[name]["pid"] + signo = signal.SIGKILL + + logging.debug(f"Terminating process {pid} with signal {signo}") + try: + os.kill(pid, signo) + except ProcessLookupError: + logging.warning("Process did not actually exist (anymore?)") + + +def daemon_no_such_process(ctx, args=None): + assert not _daemon_pgrep(args) + + +def daemon_process_exists(ctx, args=None): + assert _daemon_pgrep(args) + + +def _daemon_pgrep(pattern): + logging.info(f"checking if process exists: pattern={pattern}") + exit = subprocess.call(["pgrep", "-laf", pattern]) + logging.info(f"exit code: {exit}") + return exit == 0 |