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}