#!/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())