code wiki / _hdl_build / nx_mgmt_snapshot.nx

nx_mgmt_snapshot.nx source

↩ module page · 180 lines · 8015 B

1// nx_mgmt_snapshot.nx -- R1b: the LIVE health-snapshot PRODUCER for the sovereign management plane. 2// It turns raw ground-truth (a proc/log FEED that the READ-ONLY `nx_aw_hostctl snapfeed` sub emits on the NAS) 3// into the canonical snapshot that nx_mgmt_api's /api/health + /api/services parse (md_* at nx_mgmt_api.nx): 4// SUP <n> 5// SVC <name> <port> <state UP|DOWN> <procs> <rwin> <rtot> 6// This is what makes /api/health REAL (reasons-backed OK/DEGRADED) instead of forever UNKNOWN/no-snapshot. 7// 8// THE false-positive fix (live-proven 2026-06-29): a supervisor LINEAGE is counted as a SESSION-LEADER 9// (pid == sid). The supervisor's reader-keeper child shares the `supervise` argv AND the parent's sid but has 10// pid != sid, so it is NOT miscounted as a 2nd supervisor -> the healthy state yields SUP 1, and mc_is_duel(1)=0. 11// A GENUINE duel = two distinct session leaders -> SUP 2 -> mc_is_duel(2)=1. We count lineages here (the DATA 12// layer's job) so the CORE rule mc_is_duel stays a pure threshold (see nx_mgmt_core.nx:15-18). 13// 14// PURE synth (ss_*, imports only nx_syscalls) => gates in ISOLATION with fixtures (the loose-coupling payoff). 15// The live wiring (hostctl `snapfeed` read-only sub -> ss_synth -> write the snapfile that /api/health reads) is 16// the deploy-coupled adapter; this organ is the logic it will call. license_tier: ORIGINAL 17// 18// FEED grammar (one record per line; the NAS adapter, which owns the canonical service<->proc map, emits it): 19// SVCPORT <canon> <port> -- declare a supervised service + the port it should serve 20// PROC <pid> <ppid> <sid> <canon> -- a live process tagged with its canonical service ("supervisor" for the supervisor lineage) 21// RESTART <canon> -- one guard-restart of <canon> seen within the crash-loop window 22import "nx_syscalls.nx" 23 24const SS_MAXSVC: i64 = 64 25 26// index of '\n' at-or-after start, or n. 27func ss_eol(b: *u8, n: i64, start: i64) -> i64 { 28 var i: i64 = start 29 var f: i64 = 0 30 while f == 0 { if i >= n { f = 1 } else { if (b[i] as i64) == 10 { f = 1 } else { i = i + 1 } } } 31 return i 32} 33 34// split b[ls..le) on spaces into up-to maxf (offs,lens) absolute slices. returns field count. 35func ss_split(b: *u8, ls: i64, le: i64, offs: *i64, lens: *i64, maxf: i64) -> i64 { 36 var nf: i64 = 0 37 var i: i64 = ls 38 while i < le { 39 var sk: i64 = 1 40 while sk == 1 { if i >= le { sk = 0 } else { if (b[i] as i64) == 32 { i = i + 1 } else { sk = 0 } } } 41 if i < le { 42 let st: i64 = i 43 var sc: i64 = 1 44 while sc == 1 { if i >= le { sc = 0 } else { if (b[i] as i64) == 32 { sc = 0 } else { i = i + 1 } } } 45 if nf < maxf { offs[nf] = st; lens[nf] = i - st; nf = nf + 1 } 46 } 47 } 48 return nf 49} 50 51func ss_atoi(b: *u8, off: i64, len: i64) -> i64 { 52 var v: i64 = 0 53 var i: i64 = 0 54 while i < len { let c: i64 = b[off + i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 } 55 return v 56} 57 58func ss_tok_eq(b: *u8, off: i64, len: i64, s: *u8) -> i64 { 59 var sl: i64 = 0 60 while s[sl] != (0 as u8) { sl = sl + 1 } 61 if sl != len { return 0 } 62 var i: i64 = 0 63 while i < len { if (b[off + i] as i64) != (s[i] as i64) { return 0 } i = i + 1 } 64 return 1 65} 66 67// two slices (into possibly-different buffers) byte-equal? 68func ss_slice_eq(a: *u8, ao: i64, al: i64, b: *u8, bo: i64, bl: i64) -> i64 { 69 if al != bl { return 0 } 70 var i: i64 = 0 71 while i < al { if (a[ao + i] as i64) != (b[bo + i] as i64) { return 0 } i = i + 1 } 72 return 1 73} 74 75func ss_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o] = s[i]; o = o + 1; i = i + 1 } return o } 76func ss_cat_slice(d: *u8, o: i64, src: *u8, off: i64, len: i64) -> i64 { var i: i64 = 0; while i < len { d[o] = src[off + i]; o = o + 1; i = i + 1 } return o } 77 78// append v as decimal (no '%' operator: digit = x - (x/10)*10). 79func ss_catn(d: *u8, o: i64, v: i64) -> i64 { 80 if v == 0 { d[o] = 48 as u8; return o + 1 } 81 let tmp: *u8 = sys_mmap(32) 82 var k: i64 = 0 83 var x: i64 = v 84 while x > 0 { let dgt: i64 = x - (x / 10) * 10; tmp[k] = (dgt + 48) as u8; x = x / 10; k = k + 1 } 85 var oo: i64 = o 86 while k > 0 { k = k - 1; d[oo] = tmp[k]; oo = oo + 1 } 87 return oo 88} 89 90// THE producer: feed bytes -> snapshot bytes. returns out_n. 91func ss_synth(feed: *u8, n: i64, out: *u8) -> i64 { 92 let svc_off: *i64 = sys_mmap(SS_MAXSVC * 8) as *i64 93 let svc_len: *i64 = sys_mmap(SS_MAXSVC * 8) as *i64 94 let svc_port: *i64 = sys_mmap(SS_MAXSVC * 8) as *i64 95 let svc_procs: *i64 = sys_mmap(SS_MAXSVC * 8) as *i64 96 let svc_rwin: *i64 = sys_mmap(SS_MAXSVC * 8) as *i64 97 var nsvc: i64 = 0 98 var sup: i64 = 0 99 let offs: *i64 = sys_mmap(64) as *i64 100 let lens: *i64 = sys_mmap(64) as *i64 101 102 // pass 1: SVCPORT declarations (data-driven service set; nothing hardcoded) 103 var cur: i64 = 0 104 while cur < n { 105 let le: i64 = ss_eol(feed, n, cur) 106 let nf: i64 = ss_split(feed, cur, le, offs, lens, 8) 107 if nf >= 3 { 108 if ss_tok_eq(feed, offs[0], lens[0], "SVCPORT" as *u8) == 1 { 109 if nsvc < SS_MAXSVC { 110 svc_off[nsvc] = offs[1]; svc_len[nsvc] = lens[1] 111 svc_port[nsvc] = ss_atoi(feed, offs[2], lens[2]) 112 svc_procs[nsvc] = 0; svc_rwin[nsvc] = 0 113 nsvc = nsvc + 1 114 } 115 } 116 } 117 cur = le + 1 118 } 119 120 // pass 2: PROC (lineage + per-svc proc count) + RESTART (per-svc window count) 121 cur = 0 122 while cur < n { 123 let le: i64 = ss_eol(feed, n, cur) 124 let nf: i64 = ss_split(feed, cur, le, offs, lens, 8) 125 if nf >= 1 { 126 if ss_tok_eq(feed, offs[0], lens[0], "PROC" as *u8) == 1 { 127 if nf >= 5 { 128 let pid: i64 = ss_atoi(feed, offs[1], lens[1]) 129 let sid: i64 = ss_atoi(feed, offs[3], lens[3]) 130 let no: i64 = offs[4] 131 let nl: i64 = lens[4] 132 if ss_tok_eq(feed, no, nl, "supervisor" as *u8) == 1 { 133 if pid == sid { sup = sup + 1 } // session-leader only -> keeper child (pid!=sid) excluded 134 } else { 135 var j: i64 = 0 136 while j < nsvc { 137 if ss_slice_eq(feed, no, nl, feed, svc_off[j], svc_len[j]) == 1 { svc_procs[j] = svc_procs[j] + 1 } 138 j = j + 1 139 } 140 } 141 } 142 } else { 143 if ss_tok_eq(feed, offs[0], lens[0], "RESTART" as *u8) == 1 { 144 if nf >= 2 { 145 var j: i64 = 0 146 while j < nsvc { 147 if ss_slice_eq(feed, offs[1], lens[1], feed, svc_off[j], svc_len[j]) == 1 { svc_rwin[j] = svc_rwin[j] + 1 } 148 j = j + 1 149 } 150 } 151 } 152 } 153 } 154 cur = le + 1 155 } 156 157 // emit the snapshot the API parses 158 var o: i64 = 0 159 o = ss_cat(out, o, "SUP " as *u8) 160 o = ss_catn(out, o, sup) 161 o = ss_cat(out, o, "\n" as *u8) 162 var k: i64 = 0 163 while k < nsvc { 164 o = ss_cat(out, o, "SVC " as *u8) 165 o = ss_cat_slice(out, o, feed, svc_off[k], svc_len[k]) 166 o = ss_cat(out, o, " " as *u8) 167 o = ss_catn(out, o, svc_port[k]) 168 o = ss_cat(out, o, " " as *u8) 169 if svc_procs[k] >= 1 { o = ss_cat(out, o, "UP" as *u8) } else { o = ss_cat(out, o, "DOWN" as *u8) } 170 o = ss_cat(out, o, " " as *u8) 171 o = ss_catn(out, o, svc_procs[k]) 172 o = ss_cat(out, o, " " as *u8) 173 o = ss_catn(out, o, svc_rwin[k]) 174 o = ss_cat(out, o, " " as *u8) 175 o = ss_catn(out, o, svc_rwin[k]) // rtot: window-scoped until the NAS adapter supplies a cumulative count 176 o = ss_cat(out, o, "\n" as *u8) 177 k = k + 1 178 } 179 return o 180}