summaryrefslogtreecommitdiff
path: root/share
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-01-20 08:28:15 +0200
committerLars Wirzenius <liw@liw.fi>2021-01-20 12:03:41 +0200
commitda174232d510a343babb8efc2da8c5659acec25d (patch)
treed096e10cd9231adfa441b12234ffbd5e902b5319 /share
parent53c60f6387dff0d42eeb8079760790fdcea98984 (diff)
downloadsubplot-da174232d510a343babb8efc2da8c5659acec25d.tar.gz
feat: lib/daemon.py can return daemon stdout/stderr content so far
Diffstat (limited to 'share')
-rw-r--r--share/python/lib/daemon.md35
-rw-r--r--share/python/lib/daemon.py71
-rw-r--r--share/python/lib/daemon.yaml10
3 files changed, 114 insertions, 2 deletions
diff --git a/share/python/lib/daemon.md b/share/python/lib/daemon.md
index 4d91bd1..9484926 100644
--- a/share/python/lib/daemon.md
+++ b/share/python/lib/daemon.md
@@ -69,6 +69,41 @@ then there is no "/bin/sleep 12765" process
~~~
+# Daemon stdout and stderr are retrievable
+
+Sometimes it's useful for the step functions to be able to retrieve
+the stdout or stderr of of the daemon, after it's started, or even
+after it's terminated. This scenario verifies that `lib/daemon` can do
+that.
+
+~~~scenario
+given a daemon helper shell script chatty-daemon.sh
+given there is no "chatty-daemon" process
+when I start "./chatty-daemon.sh" as a background process as chatty-daemon
+when daemon chatty-daemon has produced output
+when I stop background process chatty-daemon
+then there is no "chatty-daemon" process
+then daemon chatty-daemon stdout is "hi there\n"
+then daemon chatty-daemon stderr is "hola\n"
+~~~
+
+We make for the daemon to exit, to work around a race condition: if
+the test program retrieves the daemon's output too fast, it may not
+have had time to produce it yet.
+
+
+~~~{#chatty-daemon.sh .file .sh .numberLines}
+#!/bin/bash
+
+set -euo pipefail
+
+trap 'exit 0' TERM
+
+echo hola 1>&2
+echo hi there
+~~~
+
+
---
title: Acceptance criteria for the lib/daemon Subplot library
author: The Subplot project
diff --git a/share/python/lib/daemon.py b/share/python/lib/daemon.py
index 4ba1987..160da0a 100644
--- a/share/python/lib/daemon.py
+++ b/share/python/lib/daemon.py
@@ -62,6 +62,7 @@ def _daemon_start(ctx, path=None, args=None, name=None):
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}")
@@ -154,10 +155,17 @@ def daemon_wait_for_port(host, port, timeout=5.0):
# 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
+ 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:
@@ -165,6 +173,20 @@ def daemon_stop(ctx, name=None):
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)
@@ -176,7 +198,7 @@ def daemon_process_exists(ctx, args=None):
def _daemon_pgrep(pattern):
logging.info(f"checking if process exists: pattern={pattern}")
- exit = subprocess.call(["pgrep", "-laf", pattern])
+ exit = subprocess.call(["pgrep", "-laf", pattern], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
logging.info(f"exit code: {exit}")
return exit == 0
@@ -187,3 +209,48 @@ def daemon_start_fails_with(ctx, message=None):
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/share/python/lib/daemon.yaml b/share/python/lib/daemon.yaml
index f72ba1a..4fab1f6 100644
--- a/share/python/lib/daemon.yaml
+++ b/share/python/lib/daemon.yaml
@@ -17,6 +17,9 @@
- 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
@@ -25,3 +28,10 @@
- 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
+