OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / swarmd / test / test_ctl.py

115 lines · python ref:
#!/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())