All checks were successful
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 8s
wai-app and Create / wai-app · does this change touch it (push) Successful in 17s
WAI Studio / does this change touch it (push) Successful in 19s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 29s
wai-fleet service / does this change touch it (push) Successful in 36s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 39s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 39s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 37s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 1m14s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 49s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 59s
wai-fleet service / service · macos (push) Successful in 54s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m5s
wai-fleet service / service · linux (push) Successful in 2m5s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 4m42s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 4m46s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 5m39s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 5m57s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 7m26s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m44s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 11m37s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 12m59s
WAI Studio / Studio in WebKit · linux (push) Successful in 13m38s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 16m34s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 16m37s
wai release binaries build on Windows / the seven release binaries · windows (push) Successful in 18m25s
wai-release-windows-build.yml's rehearsal failed at e694cb096 (run 28665): wai doctor, run from
the staged archive by archive_doctor.py, reported stack.wai-relay and stack.wai-room "not
checked: it did not finish within 6 s", and the port group too. No check hangs. The doctor runs
each group of checks on a thread of its own and takes what has finished by its 6 s for the whole
run; the three groups that did not finish are the three that start a program (wai-relay.exe and
wai-room.exe asked --version --json, and tasklist.exe for the holder of port 8080), and every
group that starts none finished. The first start of a program written moments ago waits on the
system's scan of a new executable: on cbio-device-0000, Microsoft Defender's real-time protection
on, a fresh copy of the 0.6.0 release's wai-relay.exe (11.8 MB) took 619 and 401 ms to start the
first time and 16 and 11 ms the second, wai-room.exe 425 and 398 ms against 11, wai-ingest.exe
672 and 615 ms against 11 to 13; the doctor from a fresh unpack took 2.5 s cold and 0.6 s warm.
The rehearsal stages debug builds, larger, and runs the doctor right after building them, the
host loaded: its quickstart's first starts of four of them took 4.6 to 9.2 s in the four runs
that passed (2.9 s for the release's), and in the run that failed the doctor's first starts of
two of them, with tasklist beside, passed 6 s. The doctor's spawn of a program is outside the 3 s
it gives the program to answer, so only the 6 s caught it.
The 6 s is the doctor's bound for a person at a prompt, and it stays. archive_doctor.py now starts
bin/wai-relay and bin/wai-room once each, as the doctor asks them, before the doctor, prints how
long each took, and fails when one does not end within --timeout (120 s): a program that hangs,
not a scan. Its exit status is the doctor's to judge, as before.
Checks: archive_doctor_test.py passes with two cases more (a slow first start, asked as the
doctor asks; a first start that does not end, failed at --timeout 2); the new archive_doctor.py
run on the Windows runner over a fresh unpack of wai-0.6.0-x86_64-pc-windows-msvc.zip prints
first starts of 0.5 s each and passes its six required checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
195 lines
8.9 KiB
Python
195 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
|
"""archive_doctor.py: `wai doctor` run from an unpacked quickstart archive, and held to what the
|
|
archive itself decides.
|
|
|
|
archive_doctor.py <unpacked-archive-dir> [--timeout S]
|
|
archive_doctor.py --wai <wai> --require <check-id> [--require <check-id>]... [--timeout S]
|
|
|
|
The first form runs <dir>/bin/wai (wai.exe on Windows) as `wai doctor --json`, with <dir>/bin first
|
|
on PATH, as an installed archive is, and requires every `install.*` and `stack.*` check to be `pass`:
|
|
this wai and what a prompt reaches, the relay and the room service beside it and of its version, and
|
|
the room client's and Studio's files. These six must be in the report:
|
|
install.version, install.path, stack.wai-relay, stack.wai-room, stack.web, stack.studio; and what
|
|
install.version and each of the four stack checks found must be in the archive, since `wai dev`
|
|
would otherwise take it from elsewhere on PATH or from the environment (WAI_RELAY_BIN, WAI_WEB, ...),
|
|
which would hide a piece the archive lacks. The other
|
|
checks (the ports, the browsers, the keystore, the keychain) depend on the machine it runs on: they
|
|
are printed and not required.
|
|
|
|
Before the doctor, the first form starts the two programs the doctor asks for their version,
|
|
bin/wai-relay and bin/wai-room, once each as the doctor asks them (`--version --json`), and prints
|
|
how long each took. The first start of a program written moments ago waits on the system's scan of
|
|
a new executable, and that wait would count against the doctor's 6 s for its whole run, a bound
|
|
for a person at a prompt and not one of the archive. On the Windows runner (Windows's
|
|
real-time protection on) the first start of a fresh copy of the 0.6.0 release's wai-relay.exe took
|
|
0.4 to 0.7 s, its second 11 to 16 ms; the rehearsal's larger debug programs, started by the doctor
|
|
on a host its build had just loaded, did not finish within the 6 s, and the doctor reported
|
|
stack.wai-relay and stack.wai-room not checked, while every group of checks that starts no program
|
|
finished. A first start that does not end within the timeout fails the check: that is a program
|
|
that hangs, not a scan.
|
|
|
|
The second form runs a `wai` as it is, in the environment as it is, and requires the checks named.
|
|
|
|
Every check is printed, one to a line, with its state. Exit 0 when every required check passed; 1
|
|
when one did not, is not in the report, or there is no report (wai did not run, did not answer in
|
|
time, or printed something else), or when a first start did not end in time; 2 for a usage error
|
|
or a directory with no bin/wai. --timeout bounds the doctor's run and each first start (120 s).
|
|
|
|
Standard library only; runs on Python 3.9.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
ARCHIVE_REQUIRED = ["install.version", "install.path", "stack.wai-relay", "stack.wai-room", "stack.web", "stack.studio"]
|
|
ARCHIVE_PREFIXES = ("install.", "stack.")
|
|
# The checks whose `facts.path` must be inside the archive, and under which of its directories.
|
|
ARCHIVE_INSIDE = {"install.version": "bin", "stack.wai-relay": "bin", "stack.wai-room": "bin", "stack.web": "share", "stack.studio": "share"}
|
|
# The programs the doctor asks for their version, started once before it (the module's text).
|
|
FIRST_STARTS = ["wai-relay", "wai-room"]
|
|
|
|
|
|
def inside(path, root):
|
|
"""Whether `path` is `root` or under it, both resolved."""
|
|
p, r = os.path.normcase(os.path.realpath(path)), os.path.normcase(os.path.realpath(root))
|
|
return p == r or p.startswith(r.rstrip(os.sep) + os.sep)
|
|
|
|
|
|
def usage(msg):
|
|
print(f"archive_doctor.py: {msg}", file=sys.stderr)
|
|
print(__doc__.split("\n\n")[1], file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
|
|
def first_start(exe, env, timeout):
|
|
"""`exe --version --json` started once, as the doctor asks it: how long it took to end, or why it
|
|
did not. Its exit status is the doctor's to judge."""
|
|
began = time.monotonic()
|
|
try:
|
|
subprocess.run([exe, "--version", "--json"], env=env, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL, timeout=timeout)
|
|
except subprocess.TimeoutExpired:
|
|
return None, f"did not end within {timeout:g} s"
|
|
except OSError as e:
|
|
return None, f"could not be started: {e}"
|
|
return time.monotonic() - began, None
|
|
|
|
|
|
def report(wai, env, timeout):
|
|
"""The envelope `wai doctor --json` prints, or why there is none."""
|
|
try:
|
|
p = subprocess.run([wai, "doctor", "--json"], env=env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE, timeout=timeout)
|
|
except subprocess.TimeoutExpired:
|
|
return None, f"{wai} doctor --json did not end within {timeout} s"
|
|
except OSError as e:
|
|
return None, f"{wai} could not be started: {e}"
|
|
try:
|
|
v = json.loads(p.stdout)
|
|
except ValueError:
|
|
err = p.stderr.decode("utf-8", "replace").strip().splitlines()[-1:] or [""]
|
|
return None, f"{wai} doctor --json exited {p.returncode} and printed no envelope: {err[0]}"
|
|
if not isinstance(v, dict) or v.get("wai") != "envelope/1" or v.get("op") != "toolchain.doctor":
|
|
return None, f"{wai} doctor --json printed another document: {str(v)[:200]}"
|
|
checks = v.get("data", {}).get("checks") if isinstance(v.get("data"), dict) else None
|
|
if not isinstance(checks, list):
|
|
return None, f"{wai} doctor --json printed an envelope with no checks: {str(v)[:200]}"
|
|
return v, None
|
|
|
|
|
|
def main(argv):
|
|
timeout = 120
|
|
wai = None
|
|
required = []
|
|
rest = []
|
|
i = 0
|
|
while i < len(argv):
|
|
a = argv[i]
|
|
if a in ("--timeout", "--wai", "--require"):
|
|
if i + 1 >= len(argv):
|
|
usage(f"{a} takes a value")
|
|
val = argv[i + 1]
|
|
if a == "--timeout":
|
|
try:
|
|
timeout = float(val)
|
|
except ValueError:
|
|
usage(f"--timeout {val}: a number of seconds")
|
|
elif a == "--wai":
|
|
wai = val
|
|
else:
|
|
required.append(val)
|
|
i += 2
|
|
continue
|
|
if a.startswith("--"):
|
|
usage(f"{a}: not an option")
|
|
rest.append(a)
|
|
i += 1
|
|
|
|
env = dict(os.environ)
|
|
if wai is None:
|
|
if len(rest) != 1 or required:
|
|
usage("name one unpacked archive, or --wai with --require")
|
|
root = rest[0]
|
|
exe = "wai.exe" if os.name == "nt" else "wai"
|
|
bin_dir = os.path.abspath(os.path.join(root, "bin"))
|
|
wai = os.path.join(bin_dir, exe)
|
|
if not os.path.isfile(wai):
|
|
usage(f"{root}: no bin/{exe} in it")
|
|
env["PATH"] = bin_dir + os.pathsep + env.get("PATH", "")
|
|
required = list(ARCHIVE_REQUIRED)
|
|
prefixes = ARCHIVE_PREFIXES
|
|
within = {cid: os.path.join(root, sub) for cid, sub in ARCHIVE_INSIDE.items()}
|
|
print(f"wai doctor from the archive {os.path.abspath(root)}, its bin first on PATH")
|
|
exe_ext = ".exe" if os.name == "nt" else ""
|
|
for name in FIRST_STARTS:
|
|
exe = os.path.join(bin_dir, name + exe_ext)
|
|
if not os.path.isfile(exe):
|
|
continue # the doctor's stack check says so
|
|
secs, why = first_start(exe, env, timeout)
|
|
if why is not None:
|
|
print(f"FAIL the first start of bin/{name}{exe_ext} --version --json {why}")
|
|
return 1
|
|
print(f"first start of bin/{name}{exe_ext}: {secs:.1f} s, before the doctor's own run")
|
|
else:
|
|
if rest or not required:
|
|
usage("--wai takes --require, and no archive")
|
|
prefixes = ()
|
|
within = {}
|
|
print(f"wai doctor from {wai}")
|
|
|
|
v, why = report(wai, env, timeout)
|
|
if v is None:
|
|
print(f"FAIL {why}")
|
|
return 1
|
|
checks = v["data"]["checks"]
|
|
states, paths = {}, {}
|
|
for c in checks:
|
|
cid, state = str(c.get("id")), str(c.get("state"))
|
|
states[cid] = state
|
|
facts = c.get("facts") if isinstance(c.get("facts"), dict) else {}
|
|
paths[cid] = facts.get("path")
|
|
need = cid in required or cid.startswith(prefixes)
|
|
print(f"{'required' if need else 'printed ':<9} {state:<15} {cid:<22} {c.get('message', '')}")
|
|
bad = []
|
|
for cid in required:
|
|
if cid not in states:
|
|
bad.append(f"{cid} is not in the report")
|
|
for cid, state in states.items():
|
|
if (cid in required or cid.startswith(prefixes)) and state != "pass":
|
|
bad.append(f"{cid} is {state}")
|
|
elif cid in within and not (isinstance(paths[cid], str) and inside(paths[cid], within[cid])):
|
|
bad.append(f"{cid} passed with {paths[cid]}, which is not in the archive's {os.path.basename(within[cid])}/")
|
|
if bad:
|
|
for b in bad:
|
|
print(f"FAIL {b}")
|
|
return 1
|
|
print(f"ok every required check passed ({len([c for c in states if c in required or c.startswith(prefixes)])})")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|