code wiki / (root) / nx_node_beacon.nx

nx_node_beacon.nx source

↩ module page · 323 lines · 12218 B

1// nx_node_beacon.nx -- SOVEREIGN per-node resource beacon: the swarm MONITOR keystone. 2// 3// Reads LIVE /proc utilization with BOUNDED reads (openat + single read + close) and NEVER 4// sys_read_file -- that call mmaps 4 GiB PER CALL and never frees, which is the documented 5// supervisor-ENOMEM F-class root (nx_hostctl.nx:311). A beacon is fork-per-pass and short-lived, 6// so its mmaps free on exit; the bounded reader keeps a single pass cheap regardless. 7// 8// Composes proven prior art: the /proc parsers from nx_sysload (loadavg/meminfo/conns), the 9// bounded reader idiom from nx_zombie_audit (za_readfile), and the record assembly helpers from 10// nx_framed_append (fa_cat/fa_catn/fa_len). 11// 12// EMITS one POSITIONAL row the mgmt-API /api/nodes emitter parses (mirrors the SVC-row shape): 13// NODE <name> <cpu_pct> <load_milli> <ncpu> <load_per_core_milli> <mem_used_pct> \ 14// <mem_avail_mb> <mem_total_mb> <conns8443> <ts_us> <temp_mc> <batt_pct> 15// v2 (SF-R1, swarm-fabric energy groundwork): temp_mc (max millidegC across /sys/class/thermal 16// zones) + batt_pct (/sys/class/power_supply/BAT*/capacity) APPENDED so the v1 positional prefix 17// is unchanged (rule 19 additive contract). -1 = source absent on this host (e.g. WSL VM exposes 18// no thermal/battery) -- honest-absent, never fabricated. 19// 20// Run: 21// nx_node_beacon -> SELF-GATE: sample local /proc, print row + verdict, exit 0 22// nx_node_beacon <name> <path> -> write the NODE row (labelled <name>) to <path> (the snapshot 23// /api/nodes reads) AND echo it to stdout for hostctl capture 24// license_tier: ORIGINAL 25import "nx_syscalls.nx" 26import "nx_framed_append.nx" 27const NB_MAGIC_1024: i64 = 1024 28const NB_MAGIC_150000: i64 = 150000 29 30const NB_ROW_CAP: i64 = 512 31const NB_SMALL: i64 = 8192 32const NB_BIG: i64 = 262144 33const NB_CPU_GAP_MS: i64 = 200 34 35// bounded /proc read: openat + one read + close, NUL-terminates buf. returns bytes, -1 on open fail. 36func nb_readproc(path: *u8, buf: *u8, cap: i64) -> i64 { 37 let fd: i64 = sys_openat_rd(path) 38 if fd < 0 { return 0 - 1 } 39 let r: i64 = sys_read(fd, buf, cap - 1) 40 sys_close(fd) 41 if r > 0 { buf[r] = 0 as u8 } else { buf[0] = 0 as u8 } 42 return r 43} 44 45// skip ASCII spaces from p; return first non-space index (or n). 46func nb_skip_sp(buf: *u8, n: i64, p: i64) -> i64 { 47 var i: i64 = p 48 var go: i64 = 1 49 while go == 1 { 50 if i >= n { go = 0 } else { 51 let c: i64 = buf[i] as i64 52 if c == 32 { i = i + 1 } else { go = 0 } 53 } 54 } 55 return i 56} 57 58// parse a non-negative decimal at p; write the end index to pend[0]; return the value. 59func nb_pdec(buf: *u8, n: i64, p: i64, pend: *i64) -> i64 { 60 var i: i64 = p 61 var v: i64 = 0 62 var go: i64 = 1 63 while go == 1 { 64 if i >= n { go = 0 } else { 65 let c: i64 = buf[i] as i64 66 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); i = i + 1 } else { go = 0 } } else { go = 0 } 67 } 68 } 69 pend[0] = i 70 return v 71} 72 73// count non-overlapping occurrences of needle (len nl) in buf (len n). 74func nb_count_sub(buf: *u8, n: i64, needle: *u8, nl: i64) -> i64 { 75 var c: i64 = 0 76 var i: i64 = 0 77 while i <= n - nl { 78 var j: i64 = 0 79 var ok: i64 = 1 80 while j < nl { 81 if buf[i + j] != needle[j] { ok = 0; j = nl } else { j = j + 1 } 82 } 83 if ok == 1 { c = c + 1; i = i + nl } else { i = i + 1 } 84 } 85 return c 86} 87 88// return index AFTER the first occurrence of key (len klen) in buf, or -1. 89func nb_find_after(buf: *u8, n: i64, key: *u8, klen: i64) -> i64 { 90 var i: i64 = 0 91 let last: i64 = n - klen 92 while i <= last { 93 var j: i64 = 0 94 var ok: i64 = 1 95 while j < klen { 96 if buf[i + j] != key[j] { ok = 0; j = klen } else { j = j + 1 } 97 } 98 if ok == 1 { return i + klen } 99 i = i + 1 100 } 101 return 0 - 1 102} 103 104// /proc/stat first "cpu " aggregate line -> busy + total jiffies. busy=total-(idle+iowait). 105// writes busy to bo[0], total to to[0]; returns 0 ok / -1 fail. 106func nb_cpu_read(buf: *u8, cap: i64, bo: *i64, to: *i64) -> i64 { 107 let r: i64 = nb_readproc("/proc/stat" as *u8, buf, cap) 108 if r <= 0 { return 0 - 1 } 109 let pend: *i64 = sys_mmap(16) as *i64 110 var p: i64 = 3 // line begins "cpu " -> first number after index 3 111 var total: i64 = 0 112 var idle: i64 = 0 113 var k: i64 = 0 114 while k < 8 { 115 p = nb_skip_sp(buf, r, p) 116 let v: i64 = nb_pdec(buf, r, p, pend) 117 p = pend[0] 118 total = total + v 119 if k == 3 { idle = idle + v } // idle 120 if k == 4 { idle = idle + v } // iowait 121 k = k + 1 122 } 123 bo[0] = total - idle 124 to[0] = total 125 return 0 126} 127 128// /proc/loadavg first field -> MILLI (e.g. "0.52 ..." -> 520). 0 on failure. 129func nb_loadavg_milli(buf: *u8) -> i64 { 130 let r: i64 = nb_readproc("/proc/loadavg" as *u8, buf, 256) 131 if r <= 0 { return 0 } 132 let pend: *i64 = sys_mmap(16) as *i64 133 let ip: i64 = nb_pdec(buf, r, 0, pend) 134 var frac: i64 = 0 135 var p: i64 = pend[0] 136 if p < r { 137 if (buf[p] as i64) == 46 { // '.' 138 p = p + 1 139 var dd: i64 = 0 140 var go: i64 = 1 141 while go == 1 { 142 if dd >= 2 { go = 0 } else { 143 if p >= r { go = 0 } else { 144 let c: i64 = buf[p] as i64 145 if c >= 48 { if c <= 57 { frac = frac * 10 + (c - 48); p = p + 1; dd = dd + 1 } else { go = 0 } } else { go = 0 } 146 } 147 } 148 } 149 while dd < 2 { frac = frac * 10; dd = dd + 1 } 150 } 151 } 152 return ip * 1000 + frac * 10 153} 154 155// count "processor" lines in /proc/cpuinfo (>=1). 156func nb_ncpu(buf: *u8, cap: i64) -> i64 { 157 let r: i64 = nb_readproc("/proc/cpuinfo" as *u8, buf, cap) 158 if r <= 0 { return 1 } 159 let c: i64 = nb_count_sub(buf, r, "processor" as *u8, 9) 160 if c < 1 { return 1 } 161 return c 162} 163 164// parse the kB integer following `key` in an already-read /proc/meminfo buffer. -1 if absent. 165func nb_meminfo_kb(buf: *u8, n: i64, key: *u8, klen: i64) -> i64 { 166 let at: i64 = nb_find_after(buf, n, key, klen) 167 if at < 0 { return 0 - 1 } 168 let s: i64 = nb_skip_sp(buf, n, at) 169 let pend: *i64 = sys_mmap(16) as *i64 170 return nb_pdec(buf, n, s, pend) 171} 172 173// max temperature across /sys/class/thermal/thermal_zone0..15/temp in millidegC; -1 if no zone 174// readable (WSL VM / stripped kernels have none -- report absent, never fabricate). 175func nb_thermal_max_mc(buf: *u8) -> i64 { 176 var best: i64 = 0 - 1 177 let path: *u8 = sys_mmap(64) 178 let pend: *i64 = sys_mmap(16) as *i64 179 var z: i64 = 0 180 while z < 16 { 181 var o: i64 = 0 182 o = fa_cat(path, o, "/sys/class/thermal/thermal_zone" as *u8) 183 o = fa_catn(path, o, z) 184 o = fa_cat(path, o, "/temp" as *u8) 185 path[o] = 0 as u8 186 let r: i64 = nb_readproc(path, buf, 64) 187 if r > 0 { 188 let v: i64 = nb_pdec(buf, r, 0, pend) 189 if v > best { best = v } 190 } 191 z = z + 1 192 } 193 return best 194} 195 196// first readable battery capacity 0..100 from /sys/class/power_supply/BAT0..3/capacity; -1 if 197// none (desktops/VMs have no battery -- report absent). 198func nb_batt_pct(buf: *u8) -> i64 { 199 let path: *u8 = sys_mmap(64) 200 let pend: *i64 = sys_mmap(16) as *i64 201 var b: i64 = 0 202 while b < 4 { 203 var o: i64 = 0 204 o = fa_cat(path, o, "/sys/class/power_supply/BAT" as *u8) 205 o = fa_catn(path, o, b) 206 o = fa_cat(path, o, "/capacity" as *u8) 207 path[o] = 0 as u8 208 let r: i64 = nb_readproc(path, buf, 64) 209 if r > 0 { 210 let v: i64 = nb_pdec(buf, r, 0, pend) 211 if v >= 0 { if v <= 100 { return v } } 212 } 213 b = b + 1 214 } 215 return 0 - 1 216} 217 218// assemble the positional NODE row into `row`; returns byte length (no trailing newline). 219func nb_build_row(row: *u8, name: *u8, cpu_pct: i64, load_milli: i64, ncpu: i64, lpc: i64, mem_used_pct: i64, mem_avail_mb: i64, mem_total_mb: i64, conns: i64, ts: i64, temp_mc: i64, batt_pct: i64) -> i64 { 220 var o: i64 = 0 221 o = fa_cat(row, o, "NODE " as *u8) 222 o = fa_cat(row, o, name) 223 o = fa_cat(row, o, " " as *u8); o = fa_catn(row, o, cpu_pct) 224 o = fa_cat(row, o, " " as *u8); o = fa_catn(row, o, load_milli) 225 o = fa_cat(row, o, " " as *u8); o = fa_catn(row, o, ncpu) 226 o = fa_cat(row, o, " " as *u8); o = fa_catn(row, o, lpc) 227 o = fa_cat(row, o, " " as *u8); o = fa_catn(row, o, mem_used_pct) 228 o = fa_cat(row, o, " " as *u8); o = fa_catn(row, o, mem_avail_mb) 229 o = fa_cat(row, o, " " as *u8); o = fa_catn(row, o, mem_total_mb) 230 o = fa_cat(row, o, " " as *u8); o = fa_catn(row, o, conns) 231 o = fa_cat(row, o, " " as *u8); o = fa_catn(row, o, ts) 232 o = fa_cat(row, o, " " as *u8); o = fa_catn(row, o, temp_mc) 233 o = fa_cat(row, o, " " as *u8); o = fa_catn(row, o, batt_pct) 234 return o 235} 236 237func main(argc: i64, argv: *i64) -> i64 { 238 let small: *u8 = sys_mmap(NB_SMALL) 239 let big: *u8 = sys_mmap(NB_BIG) 240 241 // --- CPU%: two /proc/stat samples across a short gap --- 242 let bo1: *i64 = sys_mmap(16) as *i64 243 let to1: *i64 = sys_mmap(16) as *i64 244 let bo2: *i64 = sys_mmap(16) as *i64 245 let to2: *i64 = sys_mmap(16) as *i64 246 nb_cpu_read(big, NB_BIG, bo1, to1) 247 sys_sleep_ms(NB_CPU_GAP_MS) 248 nb_cpu_read(big, NB_BIG, bo2, to2) 249 var cpu_pct: i64 = 0 250 let dt: i64 = to2[0] - to1[0] 251 if dt > 0 { cpu_pct = (bo2[0] - bo1[0]) * 100 / dt } 252 if cpu_pct < 0 { cpu_pct = 0 } 253 if cpu_pct > 100 { cpu_pct = 100 } 254 255 // --- load / cpu count --- 256 let load_milli: i64 = nb_loadavg_milli(small) 257 let ncpu: i64 = nb_ncpu(big, NB_BIG) 258 var lpc: i64 = load_milli 259 if ncpu > 0 { lpc = load_milli / ncpu } 260 261 // --- memory --- 262 let rmem: i64 = nb_readproc("/proc/meminfo" as *u8, small, NB_SMALL) 263 var mem_total_kb: i64 = nb_meminfo_kb(small, rmem, "MemTotal:" as *u8, 9) 264 var mem_avail_kb: i64 = nb_meminfo_kb(small, rmem, "MemAvailable:" as *u8, 13) 265 if mem_total_kb < 1 { mem_total_kb = 1 } 266 if mem_avail_kb < 0 { mem_avail_kb = 0 } 267 let mem_total_mb: i64 = mem_total_kb / NB_MAGIC_1024 268 let mem_avail_mb: i64 = mem_avail_kb / NB_MAGIC_1024 269 let mem_used_pct: i64 = (mem_total_kb - mem_avail_kb) * 100 / mem_total_kb 270 271 // --- serving load: ESTABLISHED-ish conns on :8443 (0x20FB) --- 272 let rtcp: i64 = nb_readproc("/proc/net/tcp" as *u8, big, NB_BIG) 273 var conns: i64 = 0 274 if rtcp > 0 { 275 let cc: i64 = nb_count_sub(big, rtcp, ":20FB" as *u8, 5) 276 if cc > 1 { conns = cc - 1 } 277 } 278 279 // --- energy axes (v2, honest-absent -1) --- 280 let temp_mc: i64 = nb_thermal_max_mc(small) 281 let batt_pct: i64 = nb_batt_pct(small) 282 283 let ts: i64 = sys_now_us() 284 285 // --- label + destination from argv --- 286 var name: *u8 = "nas" as *u8 287 var path: *u8 = 0 as *u8 288 if argc >= 2 { name = argv[1] as *u8 } 289 if argc >= 3 { path = argv[2] as *u8 } 290 291 let row: *u8 = sys_mmap(NB_ROW_CAP) 292 let rl: i64 = nb_build_row(row, name, cpu_pct, load_milli, ncpu, lpc, mem_used_pct, mem_avail_mb, mem_total_mb, conns, ts, temp_mc, batt_pct) 293 294 if (path as i64) != 0 { 295 row[rl] = 10 as u8 // '\n' 296 let fd: i64 = sys_openat_wr(path, 0x1a4) 297 if fd < 0 { return 20 } 298 sys_write(fd, row, rl + 1) 299 sys_close(fd) 300 sys_write(1, row, rl + 1) // echo for hostctl capture 301 return 0 302 } 303 304 // SELF-GATE 305 sys_write(1, row, rl) 306 sys_write(1, "\n" as *u8, 1) 307 var ok: i64 = 1 308 if ncpu < 1 { ok = 0 } 309 if mem_total_mb < 1 { ok = 0 } 310 if cpu_pct < 0 { ok = 0 } 311 if cpu_pct > 100 { ok = 0 } 312 if load_milli < 0 { ok = 0 } 313 if temp_mc < (0 - 1) { ok = 0 } 314 if temp_mc > NB_MAGIC_150000 { ok = 0 } 315 if batt_pct < (0 - 1) { ok = 0 } 316 if batt_pct > 100 { ok = 0 } 317 if ok == 1 { 318 sys_write(1, "NODEBEACONGATE verdict=GREEN\n" as *u8, fa_len("NODEBEACONGATE verdict=GREEN\n" as *u8)) 319 } else { 320 sys_write(1, "NODEBEACONGATE verdict=RED\n" as *u8, fa_len("NODEBEACONGATE verdict=RED\n" as *u8)) 321 } 322 return 0 323}