code wiki / _hdl_build / nx_lan_scan.nx
nx_lan_scan.nx
buildroot/runtime/_hdl_build/nx_lan_scan.nx
about
nx_lan_scan.nx -- shared sovereign LAN sweep primitive (no main).
WHY THIS EXISTS (rule 15, DRY through shared libraries): two organs now ask
the SAME question -- "is host H open on port P?" -- nx_printer_ctl (printer
discovery/survey) and nx_iot_ctl (home-electronics inventory). If each keeps
its own copy of the sweep, one gets fixed and the other silently does not,
and the two tools then DISAGREE about the same LAN while both look healthy.
The attribution rule they must agree on is the reason to share, not the
line count.
The implementation is lifted VERBATIM from the proven, gate-green
pctl_discover_scan / pctl_probe_host (nx_printer_ctl_lib), so behaviour is
identical BY CONSTRUCTION rather than by re-derivation.
SCALE LAW (learned the hard way, banked in the iot-printer-fabric memory):
any organ that fans out network I/O over MCP must bound total latency to the
tools-daemon fork-capture window, or the call returns status=0 with no output
while working fine on the CLI. Three rules, all encoded below:
1. BATCHED -- fire all 254 non-blocking connects first, then poll.
2. POLL-UNTIL-RESOLVED -- a single poll() returns as soon as ANY fd is
ready (the ~250 instantly-refused hosts) BEFORE a real host finishes its
handshake, so it finds NOTHING. Mark each fd done as its revents fire
(write 0xff over the pollfd fd bytes so poll ignores it next round) and
keep polling the still-connecting ones.
3. TIGHT BUDGET -- responsive devices answer in <50ms; unreachable hosts
must not be waited on.
Open iff POLLOUT && !POLLERR && !POLLHUP.
THE BATCH IS THE BOUND (2026-08-18, lane F box health -- MEASURED, not read).
A later edit replaced the sweep's raw non-blocking connect with nx_connect_bounded,
whose contract is to POLL THAT ONE FD for up to NX_CONN_DEFAULT_MS (6,000 ms) before
returning. Inside the fire-all-254 loop that turned rule 1 into its opposite: 254
serial 6 s waits (~25 min per port, ~100 min per `monitor` run) with every socket
held open until the final close loop. Live evidence: ~20 `nx_printer_ctl monitor`
beat instances piled up at RSS 4 kB with FDSize 512 and fd counts rising 14-26
per minute (nx_leak_check fleet, full population), each one a sweep still stuck in
its OPEN loop. Rules 1-3 above were correct; the primitive underneath them had been
swapped for one whose OWN bound was the thing being batched away.
FIX, by construction: the sweep issues the raw non-blocking connect (EINPROGRESS is
dependencies 2 imports · 3 importers
imports: nx_syscalls.nxnx_connect.nx
imported by: nx_iot_ctl_gate.nxnx_iot_ctl_lib.nxnx_printer_ctl_lib.nx
structs
| none |
consts
| 49 | const LSCAN_MAGIC_65535: i64 = 65535 |
| 51 | const LSCAN_SOCK_NONBLOCK: i64 = 2048 // SOCK_NONBLOCK |
| 52 | const LSCAN_POLLOUT: i64 = 4 |
| 53 | const LSCAN_POLLERR: i64 = 8 |
| 54 | const LSCAN_POLLHUP: i64 = 16 |
| 55 | const LSCAN_HOSTS: i64 = 254 // .1 .. .254 of a /24 |
| 56 | const LSCAN_ROUNDS: i64 = 12 // poll-until-resolved rounds |
| 57 | const LSCAN_MIN_PER_MS: i64 = 5 // floor on the per-round poll slice |
| 71 | const LSCAN_BUDGET_MS: i64 = 120 |
functions
| 75 | func lscan_parse_prefix(s: *u8) -> i64 |
| 114 | func lscan_parse_ip(s: *u8) -> i64 called by 1: main |
| 152 | func lscan_parse_port(s: *u8) -> i64 called by 1: main |
| 175 | func lscan_fill_sockaddr(addr: *u8, ipv4: i64, port: i64) -> i64 |
| 195 | func lscan_probe_host(ipv4: i64, port: i64, timeout_ms: i64) -> i64 called by 1: pctl_probe_host calls 5: sys_socketsys_mmaplscan_fill_sockaddrnx_connect_boundedsys_close |
| 212 | func lscan_sweep(base24: i64, port: i64, timeout_ms: i64, out_ips: *i64, max: i64) -> i64 |