lesavka/scripts/manual/client_rct_clock_alignment.py

93 lines
3.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Sample client-to-RCT clock alignment for client-origin transport probes."""
from __future__ import annotations
import json
import pathlib
import shlex
import subprocess
import sys
import time
def sample_clock_alignment(host: str, ssh_opts_text: str) -> dict:
"""Return a midpoint clock-offset estimate between this client and RCT.
Inputs: SSH host plus the same SSH option string used by the manual probe.
Outputs: a JSON-serializable clock alignment record.
Why: client-origin freshness needs the final capture timestamps translated
into the client's clock without requiring NTP-level access or sudo.
"""
ssh_opts = shlex.split(ssh_opts_text)
remote_code = "import sys,time\nfor _ in sys.stdin:\n print(time.time_ns(), flush=True)\n"
proc = subprocess.Popen(
["ssh", *ssh_opts, host, "python3 -u -c " + shlex.quote(remote_code)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
rows: list[tuple[int, int]] = []
try:
assert proc.stdin is not None
assert proc.stdout is not None
for _ in range(9):
start = time.time_ns()
proc.stdin.write("sample\n")
proc.stdin.flush()
remote = proc.stdout.readline().strip()
end = time.time_ns()
if not remote:
raise RuntimeError("remote clock sampler stopped before returning data")
midpoint = (start + end) // 2
rows.append((end - start, int(remote) - midpoint))
time.sleep(0.05)
finally:
if proc.stdin is not None:
proc.stdin.close()
proc.terminate()
try:
proc.wait(timeout=2)
except subprocess.TimeoutExpired:
proc.kill()
rows.sort(key=lambda row: row[0])
best = rows[:5]
offset = round(sum(row[1] for row in best) / len(best))
uncertainty = max(row[0] for row in best) // 2
return {
"schema": "lesavka.client-rct-clock-alignment.v1",
"available": True,
"method": "persistent client-to-capture ssh midpoint",
"capture_host": host,
"capture_clock_offset_from_client_ns": offset,
"clock_uncertainty_ns": uncertainty,
"clock_uncertainty_ms": uncertainty / 1_000_000.0,
"samples": len(rows),
}
def main() -> int:
"""CLI entrypoint for the clock alignment helper."""
if len(sys.argv) != 4:
print(
"usage: client_rct_clock_alignment.py TETHYS_HOST SSH_OPTS OUTPUT_JSON",
file=sys.stderr,
)
return 2
host, ssh_opts_text, output_path = sys.argv[1:]
data = sample_clock_alignment(host, ssh_opts_text)
pathlib.Path(output_path).write_text(json.dumps(data, indent=2, sort_keys=True) + "\n")
offset = data["capture_clock_offset_from_client_ns"] / 1_000_000.0
uncertainty = data["clock_uncertainty_ms"]
print(f" ↪ tethys_from_client_offset_ms={offset:+.3f}")
print(f" ↪ clock_alignment_uncertainty_ms={uncertainty:.3f}")
return 0
if __name__ == "__main__":
raise SystemExit(main())