summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-01-30 10:12:43 +0200
committerLars Wirzenius <liw@liw.fi>2021-01-30 11:52:11 +0200
commit80bdaa2e4a12a037d8f6b3a7658289b02df1cd1b (patch)
tree4f12f76afc6fbd7f21890fb1ffd5b5bd5708a3b9
parent652cfdf094736d8a53b9176e9a2d574c8c73cc9f (diff)
downloadewww-80bdaa2e4a12a037d8f6b3a7658289b02df1cd1b.tar.gz
chore: vendor lib/daemon from Subplot
-rw-r--r--ewww.md3
-rw-r--r--subplot/vendored/daemon.py256
-rw-r--r--subplot/vendored/daemon.yaml37
3 files changed, 295 insertions, 1 deletions
diff --git a/ewww.md b/ewww.md
index e33f5eb..6352c02 100644
--- a/ewww.md
+++ b/ewww.md
@@ -146,11 +146,12 @@ author: Lars Wirzenius
template: python
bindings:
- subplot/ewww.yaml
+ - subplot/vendored/daemon.yaml
- subplot/vendored/runcmd.yaml
functions:
- subplot/ewww.py
- - subplot/daemon.py
- subplot/http.py
+ - subplot/vendored/daemon.py
- subplot/vendored/runcmd.py
classes:
- scenario-disabled
diff --git a/subplot/vendored/daemon.py b/subplot/vendored/daemon.py
new file mode 100644
index 0000000..d436b4f
--- /dev/null
+++ b/subplot/vendored/daemon.py
@@ -0,0 +1,256 @@
+import logging
+import os
+import signal
+import socket
+import subprocess
+import time
+
+
+# A helper function for testing lib/daemon itself.
+def _daemon_shell_script(ctx, filename=None):
+ get_file = globals()["get_file"]
+ data = get_file(filename)
+ with open(filename, "wb") as f:
+ f.write(data)
+ os.chmod(filename, 0o755)
+
+
+# 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 daemon after a little wait. This is used only for testing the
+# port-waiting code.
+def _daemon_start_soonish(ctx, path=None, args=None, name=None, port=None):
+ _daemon_start(ctx, path=os.path.abspath(path), args=args, name=name)
+ daemon = ctx.declare("_daemon")
+
+ # Store the PID of the process we just started so that _daemon_stop_soonish
+ # can kill it during the cleanup phase. This works around the Subplot
+ # Python template not giving the step captures to cleanup functions. Note
+ # that this code assume at most one _soonish function is called.
+ daemon["_soonish"] = daemon[name]["pid"]
+
+ try:
+ daemon_wait_for_port("localhost", port)
+ except Exception as e:
+ daemon["_start_error"] = repr(e)
+
+ logging.info("pgrep: %r", _daemon_pgrep(path))
+
+
+def _daemon_stop_soonish(ctx, path=None, args=None, name=None, port=None):
+ ns = ctx.declare("_daemon")
+ pid = ns["_soonish"]
+ logging.debug(f"Stopping soonishly-started daemon, {pid}")
+ signo = signal.SIGKILL
+ try:
+ os.kill(pid, signo)
+ except ProcessLookupError:
+ logging.warning("Process did not actually exist (anymore?)")
+
+
+# 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"]
+
+ path = os.path.abspath(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=5.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()
+ # Sleep a bit to avoid consuming too much CPU while busy-waiting.
+ time.sleep(0.1)
+
+
+# Stop a daemon.
+def daemon_stop(ctx, path=None, args=None, name=None):
+ logging.debug(f"Stopping daemon {name}")
+
+ ns = ctx.declare("_daemon")
+ logging.debug(f" ns={ns}")
+ pid = ns[name]["pid"]
+ signo = signal.SIGTERM
+
+ this = ns[name]
+ data = open(this["stdout"]).read()
+ logging.debug(f"{name} stdout, before: {data!r}")
+ data = open(this["stderr"]).read()
+ logging.debug(f"{name} stderr, before: {data!r}")
+
+ logging.debug(f"Terminating process {pid} with signal {signo}")
+ try:
+ os.kill(pid, signo)
+ except ProcessLookupError:
+ logging.warning("Process did not actually exist (anymore?)")
+
+ while True:
+ try:
+ os.kill(pid, 0)
+ logging.debug(f"Daemon {name}, pid {pid} still exists")
+ time.sleep(1)
+ except ProcessLookupError:
+ break
+ logging.debug(f"Daemon {name} is gone")
+
+ data = open(this["stdout"]).read()
+ logging.debug(f"{name} stdout, after: {data!r}")
+ data = open(this["stderr"]).read()
+ logging.debug(f"{name} stderr, after: {data!r}")
+
+
+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], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ logging.info(f"exit code: {exit}")
+ return exit == 0
+
+
+def daemon_start_fails_with(ctx, message=None):
+ daemon = ctx.declare("_daemon")
+ error = daemon["_start_error"]
+ logging.debug(f"daemon_start_fails_with: error={error!r}")
+ logging.debug(f"daemon_start_fails_with: message={message!r}")
+ assert message.lower() in error.lower()
+
+
+def daemon_get_stdout(ctx, name):
+ return _daemon_get_output(ctx, name, "stdout")
+
+
+def daemon_get_stderr(ctx, name):
+ return _daemon_get_output(ctx, name, "stderr")
+
+
+def _daemon_get_output(ctx, name, which):
+ ns = ctx.declare("_daemon")
+ this = ns[name]
+ filename = this[which]
+ data = open(filename).read()
+ logging.debug(f"Read {which} of daemon {name} from {filename}: {data!r}")
+ return data
+
+
+def daemon_has_produced_output(ctx, name=None):
+ started = time.time()
+ timeout = 5.0
+ while time.time() < started + timeout:
+ stdout = daemon_get_stdout(ctx, name)
+ stderr = daemon_get_stderr(ctx, name)
+ if stdout and stderr:
+ break
+ time.sleep(0.1)
+
+
+def daemon_stdout_is(ctx, name=None, text=None):
+ daemon_get_stdout = globals()["daemon_get_stdout"]
+ _daemon_output_is(ctx, name, text, daemon_get_stdout)
+
+
+def daemon_stderr_is(ctx, name=None, text=None):
+ daemon_get_stderr = globals()["daemon_get_stderr"]
+ _daemon_output_is(ctx, name, text, daemon_get_stderr)
+
+
+def _daemon_output_is(ctx, name, text, getter):
+ assert_eq = globals()["assert_eq"]
+ text = bytes(text, "UTF-8").decode("unicode_escape")
+ output = getter(ctx, name)
+ assert_eq(text, output)
diff --git a/subplot/vendored/daemon.yaml b/subplot/vendored/daemon.yaml
new file mode 100644
index 0000000..4fab1f6
--- /dev/null
+++ b/subplot/vendored/daemon.yaml
@@ -0,0 +1,37 @@
+- given: there is no "{args:text}" process
+ function: daemon_no_such_process
+
+- given: a daemon helper shell script {filename}
+ function: _daemon_shell_script
+
+- when: I start "{path}{args:text}" as a background process as {name}, on port {port}
+ function: daemon_start_on_port
+
+- when: I try to start "{path}{args:text}" as {name}, on port {port}
+ function: _daemon_start_soonish
+ cleanup: _daemon_stop_soonish
+
+- when: I start "{path}{args:text}" as a background process as {name}
+ function: _daemon_start
+
+- when: I stop background process {name}
+ function: daemon_stop
+
+- when: daemon {name} has produced output
+ function: daemon_has_produced_output
+
+- then: a process "{args:text}" is running
+ function: daemon_process_exists
+
+- then: there is no "{args:text}" process
+ function: daemon_no_such_process
+
+- then: starting daemon fails with "{message:text}"
+ function: daemon_start_fails_with
+
+- then: daemon {name} stdout is "{text:text}"
+ function: daemon_stdout_is
+
+- then: daemon {name} stderr is "{text:text}"
+ function: daemon_stderr_is
+