82 lines
3.2 KiB
Python
82 lines
3.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Trap 22 / N-6 trustworthy reproduction probe: rapid reconnect to a device listener.
|
|
#
|
|
# Failure under test (original report): after a rapid reconnect (<~1s) on a
|
|
# relistened socket, the chip completes the handshake but the MCU never gets
|
|
# standalone accept -> the socket stays LISTENING -> later the chip auto-closes it
|
|
# -> THE LISTENER DIES PERMANENTLY (all later SYNs refused, no self-recovery).
|
|
#
|
|
# Judgement is purely client-side (no firmware probes):
|
|
# * every round must (re)connect successfully within --timeout
|
|
# * on failure the probe keeps retrying for --recover-s to distinguish
|
|
# "transient" from "permanently dead listener"
|
|
#
|
|
# Usage: tcp_reconnect_probe.py [--host 192.168.1.100] [--port 21] [--rounds 50]
|
|
# [--gap-ms 600] [--timeout 3] [--recover-s 10]
|
|
# [--banner] (read one line after connect, e.g. FTP 220)
|
|
# Output:
|
|
# TS <epoch>
|
|
# ROUND <n> gap=<ms> connect=<OK|FAIL:reason> recovered=<na|yes|no>
|
|
# SUMMARY rounds=<n> ok=<n> fail=<n> unrecovered=<n>
|
|
# Exit: 0 all rounds connected / 1 any failure / 2 usage
|
|
import argparse, socket, sys, time
|
|
|
|
def log(m):
|
|
sys.stdout.write(m + "\n"); sys.stdout.flush()
|
|
|
|
def try_connect(host, port, timeout, banner):
|
|
s = socket.create_connection((host, port), timeout=timeout)
|
|
if banner:
|
|
s.settimeout(timeout)
|
|
try:
|
|
s.recv(256) # e.g. FTP "220 ..." greeting
|
|
except socket.timeout:
|
|
pass
|
|
return s
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--host", default="192.168.1.100")
|
|
ap.add_argument("--port", type=int, default=21)
|
|
ap.add_argument("--rounds", type=int, default=50)
|
|
ap.add_argument("--gap-ms", type=int, default=600)
|
|
ap.add_argument("--timeout", type=float, default=3.0)
|
|
ap.add_argument("--recover-s", type=float, default=10.0)
|
|
ap.add_argument("--banner", action="store_true")
|
|
a = ap.parse_args()
|
|
|
|
nok = nfail = nunrec = 0
|
|
for n in range(1, a.rounds + 1):
|
|
log("TS %d" % int(time.time()))
|
|
res = "OK"
|
|
recovered = "na"
|
|
try:
|
|
s = try_connect(a.host, a.port, a.timeout, a.banner)
|
|
s.close()
|
|
except Exception as e:
|
|
res = "FAIL:%s" % type(e).__name__
|
|
# keep probing: transient (relisten window) or permanently dead?
|
|
t0 = time.time()
|
|
recovered = "no"
|
|
while time.time() - t0 < a.recover_s:
|
|
time.sleep(0.5)
|
|
try:
|
|
s = try_connect(a.host, a.port, a.timeout, a.banner)
|
|
s.close()
|
|
recovered = "yes"
|
|
break
|
|
except Exception:
|
|
pass
|
|
ok = (res == "OK")
|
|
nok += 1 if ok else 0
|
|
nfail += 0 if ok else 1
|
|
if not ok and recovered != "yes":
|
|
nunrec += 1
|
|
log("ROUND %d gap=%d connect=%s recovered=%s" % (n, a.gap_ms, res, recovered))
|
|
if a.gap_ms > 0 and n < a.rounds:
|
|
time.sleep(a.gap_ms / 1000.0)
|
|
log("SUMMARY rounds=%d ok=%d fail=%d unrecovered=%d" % (a.rounds, nok, nfail, nunrec))
|
|
return 0 if nfail == 0 else 1
|
|
|
|
sys.exit(main())
|