#!/usr/bin/env python3
"""Gate: a running daemon can be inspected, retargeted and stopped — by NAME.
python3 -B test/test_ctl.py [--bootstrap host:port]
⛔ WHY THIS IS A GATE AND NOT A CONVENIENCE. Before swarmd-ctl the only way to
stop a daemon was to match its command line, and a pattern that matches a
daemon's argv also matches the SHELL that typed it: `pkill -f "session-daemon
--topic mountain"` over ssh killed the ssh session's own remote shell, twice in
this project — once in the session that wrote this file. It is also
indiscriminate, since two daemons on two topics share every substring worth
matching on.
And the topic was a START-UP ARGUMENT and nothing else, so changing session meant
killing a process. That is why killing processes was the interface. The
swarm-join / swarm-leave RPCs had existed since the control plane was written;
nothing exposed them.
"""
import os, subprocess, sys, tempfile, time
HERE = os.path.dirname(os.path.realpath(__file__))
ROOT = os.path.dirname(HERE)
SWARMD = os.path.join(ROOT, "swarmd")
CTL = os.path.join(ROOT, "tools", "swarmd-ctl")
passed = failed = 0
def ck(name, cond, detail=""):
global passed, failed
if cond:
passed += 1
print(" PASS: %-52s %s" % (name, detail))
else:
failed += 1
print(" FAIL: %-52s %s" % (name, detail))
def ctl(sock, *args):
r = subprocess.run([CTL, "--socket", sock] + list(args),
capture_output=True, text=True, timeout=30)
return r.returncode, (r.stdout + r.stderr).strip()
def main():
bootstrap = (sys.argv[sys.argv.index("--bootstrap") + 1]
if "--bootstrap" in sys.argv else None)
for p in (SWARMD, CTL):
if not os.path.isfile(p):
print("missing %s — run ./build.sh" % p)
return 2
tmp = tempfile.mkdtemp(prefix="swarmd-ctl-")
sock = os.path.join(tmp, "d.sock")
argv = [SWARMD, "--socket", sock]
if bootstrap:
argv += ["--bootstrap", bootstrap]
log = open(os.path.join(tmp, "d.log"), "wb")
proc = subprocess.Popen(argv, stdout=log, stderr=subprocess.STDOUT)
try:
deadline = time.time() + 30
while time.time() < deadline and not os.path.exists(sock):
time.sleep(0.2)
ck("the daemon bound its socket", os.path.exists(sock))
pidfile = sock + ".pid"
ck("it wrote a pidfile beside the socket", os.path.exists(pidfile))
pid = int(open(pidfile).read().strip())
ck("the pidfile names the daemon's own process", pid == proc.pid,
"%d" % pid)
rc, out = ctl(sock, "status")
ck("status answers", rc == 0 and "dht=" in out, out)
ck("and counts the ctl client that asked", "local=1" in out, out)
# ⛔ THE POINT: a topic can be changed on a daemon that is ALREADY RUNNING.
rc, out = ctl(sock, "join", "ctl-gate-topic")
ck("join reaches a running daemon", rc == 0 and "joined" in out, out)
rc, out = ctl(sock, "leave", "ctl-gate-topic")
ck("leave does too", rc == 0 and "left" in out, out)
rc, out = ctl(sock, "join")
ck("join with no topic is refused, not guessed", rc != 0)
# ---- stop, by pid, from the daemon's own file ----
rc, out = ctl(sock, "stop")
ck("stop reports the pid it signalled", rc == 0 and str(pid) in out, out)
try:
proc.wait(timeout=15)
gone = True
except subprocess.TimeoutExpired:
gone = False
ck("the daemon actually exited", gone)
ck("and took its pidfile with it", not os.path.exists(pidfile))
# ---- and it refuses what it cannot name ----
rc, out = ctl(os.path.join(tmp, "nothing.sock"), "stop")
ck("stop on a socket with no pidfile fails clearly",
rc != 0 and "pidfile" in out, out)
finally:
if proc.poll() is None:
proc.terminate()
try: proc.wait(timeout=5)
except subprocess.TimeoutExpired: proc.kill()
log.close()
print("(logs: %s)" % tmp)
print("\n%d/%d passed" % (passed, passed + failed))
return 1 if failed else 0
sys.exit(main())