AtlatestRepositorycourier
1
#!/usr/bin/env python32
"""Does the single-poller claim survive the PRODUCTION launch shape?4
claude-ops copies each courier to /tmp/mcp-bins/courier-<launcher-pid> and runs5
it from there, so every instance has a DIFFERENT /proc/<pid>/comm --6
"courier-21570", not "courier". Any liveness check that compares the claim7
owner's comm against our own therefore sees every other instance as a recycled8
pid, judges its live claim stale, breaks it, and starts a second poller.10
repro/single_poller_harness.py cannot see this: it runs both instances from one11
binary path, so their comms match. That is the whole point of this probe --12
production is the one shape the harness does not reproduce.14
Distinct names come from SYMLINKS rather than copies: a copied dev bundle15
cannot find its own library directory ("library not found: (courier main)"),16
while a symlink still resolves /proc/self/exe back to the real bundle AND17
still yields the symlink's basename as comm. Verified both.19
Usage: two_paths_probe.py --bin <courier>20
Exit 0 = exactly one poller (claim holds). Exit 1 = defect present.21
"""22
import argparse23
import json24
import os25
import shutil26
import subprocess27
import sys28
import threading29
import time30
import urllib.request32
PORT = int(os.environ.get("PORT", "8643"))33
BASE = f"http://127.0.0.1:{PORT}"34
RUN = "/tmp/courier-two-paths"37
def stats():38
with urllib.request.urlopen(BASE + "/_stats", timeout=10) as r:39
return json.load(r)42
def post(path, obj):43
data = json.dumps(obj).encode()44
req = urllib.request.Request(BASE + path, data=data,45
headers={"Content-Type": "application/json"})46
with urllib.request.urlopen(req, timeout=10) as r:47
return json.load(r)50
def start(binp, name):51
env = dict(os.environ)52
env.update({53
"COURIER_TELEGRAM_TOKEN": "TESTTOKEN-two-paths",54
"COURIER_TELEGRAM_CHAT_ID": "1",55
"COURIER_TELEGRAM_API_URL": BASE,56
"COURIER_RELAY_DIR": os.path.join(RUN, "relays"),57
})58
env.pop("COURIER_DISABLE_TELEGRAM_SEND", None)59
env.pop("COURIER_TELEGRAM_POLL_FORCE", None)60
p = subprocess.Popen([binp, "serve"], stdin=subprocess.PIPE,61
stdout=subprocess.PIPE,62
stderr=open(os.path.join(RUN, name + ".err"), "wb"),63
env=env, bufsize=0)64
p.stdin.write((json.dumps({65
"jsonrpc": "2.0", "id": 1, "method": "initialize",66
"params": {"protocolVersion": "2024-11-05", "capabilities": {},67
"clientInfo": {"name": "two-paths", "version": "0"}}}) + "\n").encode())68
p.stdin.flush()69
p.stdout.readline()70
p.stdin.write((json.dumps({"jsonrpc": "2.0",71
"method": "notifications/initialized",72
"params": {}}) + "\n").encode())73
p.stdin.flush()74
threading.Thread(target=lambda: [None for _ in p.stdout], daemon=True).start()75
return p78
def poller_children(pid):79
out = []80
for e in os.listdir("/proc"):81
if not e.isdigit():82
continue83
try:84
ppid = None85
for line in open(f"/proc/{e}/status"):86
if line.startswith("PPid:"):87
ppid = int(line.split()[1])88
break89
if ppid != pid:90
continue91
argv = open(f"/proc/{e}/cmdline", "rb").read().decode(92
"utf8", "replace").rstrip("\0").split("\0")93
exe = os.readlink(f"/proc/{e}/exe")94
except (FileNotFoundError, ProcessLookupError, PermissionError, OSError):95
continue96
# Exact last-argv match AND a real courier executable: a substring97
# search matches the searching process itself (the `pgrep -f`98
# self-match hazard).99
if argv and argv[-1] == "--telegram-poller" and \100
os.path.basename(exe).startswith("courier"):101
out.append(int(e))102
return out105
def main():106
ap = argparse.ArgumentParser()107
ap.add_argument("--bin", required=True)108
args = ap.parse_args()109
binp = os.path.abspath(args.bin)111
shutil.rmtree(RUN, ignore_errors=True)112
os.makedirs(os.path.join(RUN, "relays"), exist_ok=True)113
a_path = os.path.join(RUN, "courier-1111")114
b_path = os.path.join(RUN, "courier-2222")115
os.symlink(binp, a_path)116
os.symlink(binp, b_path)118
here = os.path.dirname(os.path.abspath(__file__))119
mock = subprocess.Popen([sys.executable, os.path.join(here, "mock_telegram.py"),120
str(PORT), os.path.join(RUN, "mocklog")],121
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)122
for _ in range(50):123
try:124
stats()125
break126
except Exception:127
time.sleep(0.1)128
post("/_seed", {"updates": []})129
post("/_dwell", {"seconds": 2.5})131
a = b = None132
try:133
a = start(a_path, "a")134
time.sleep(10)135
b = start(b_path, "b")136
time.sleep(25)138
ca = open(f"/proc/{a.pid}/comm").read().strip()139
cb = open(f"/proc/{b.pid}/comm").read().strip()140
pa, pb = poller_children(a.pid), poller_children(b.pid)141
claims = [f for f in os.listdir(RUN) if f.endswith(".claim")]142
owner = os.readlink(os.path.join(RUN, claims[0])) if claims else None143
s = stats()145
print(f"comm A = {ca!r} comm B = {cb!r} (distinct: {ca != cb})")146
print(f"poller children: A={pa} B={pb}")147
print(f"claim owner = {owner!r} (A={a.pid}, B={b.pid})")148
print(f"mock: conflicts={s['conflicts']} max_inflight={s['max_inflight']} "149
f"getupdates={s['getupdates']}")150
print()152
# The probe is only meaningful if the two names really did differ and153
# polling really happened -- otherwise "one poller" passes for free.154
ok_setup = (ca != cb) and s["getupdates"] >= 3155
if not ok_setup:156
print("PROBE INVALID: names not distinct, or no polling observed "157
f"(comms differ={ca != cb}, getupdates={s['getupdates']})")158
return 2160
total = len(pa) + len(pb)161
if total == 1 and s["conflicts"] == 0 and s["max_inflight"] <= 1:162
print("PASS: exactly one poller under the production launch shape")163
return 0164
print(f"FAIL: {total} pollers, {s['conflicts']} conflicts "165
f"-- the claim does not survive per-instance binary names")166
return 1167
finally:168
for p in (a, b):169
if p:170
p.kill()171
mock.terminate()174
if __name__ == "__main__":175
sys.exit(main())