code wiki / _hdl_build / nx_mgmt_snapshot.nx

nx_mgmt_snapshot.nx source

↩ module page · 181 lines · 8081 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] = 45 as u8; return ss_catn(d, o + 1, 0 - v) } 81 if v == 0 { d[o] = 48 as u8; return o + 1 } 82 let tmp: *u8 = sys_mmap(32) 83 var k: i64 = 0 84 var x: i64 = v 85 while x > 0 { let dgt: i64 = x - (x / 10) * 10; tmp[k] = (dgt + 48) as u8; x = x / 10; k = k + 1 } 86 var oo: i64 = o 87 while k > 0 { k = k - 1; d[oo] = tmp[k]; oo = oo + 1 } 88 return oo 89} 90 91// THE producer: feed bytes -> snapshot bytes. returns out_n. 92func ss_synth(feed: *u8, n: i64, out: *u8) -> i64 { 93 let svc_off: *i64 = sys_mmap(SS_MAXSVC * 8) as *i64 94 let svc_len: *i64 = sys_mmap(SS_MAXSVC * 8) as *i64 95 let svc_port: *i64 = sys_mmap(SS_MAXSVC * 8) as *i64 96 let svc_procs: *i64 = sys_mmap(SS_MAXSVC * 8) as *i64 97 let svc_rwin: *i64 = sys_mmap(SS_MAXSVC * 8) as *i64 98 var nsvc: i64 = 0 99 var sup: i64 = 0 100 let offs: *i64 = sys_mmap(64) as *i64 101 let lens: *i64 = sys_mmap(64) as *i64 102 103 // pass 1: SVCPORT declarations (data-driven service set; nothing hardcoded) 104 var cur: i64 = 0 105 while cur < n { 106 let le: i64 = ss_eol(feed, n, cur) 107 let nf: i64 = ss_split(feed, cur, le, offs, lens, 8) 108 if nf >= 3 { 109 if ss_tok_eq(feed, offs[0], lens[0], "SVCPORT" as *u8) == 1 { 110 if nsvc < SS_MAXSVC { 111 svc_off[nsvc] = offs[1]; svc_len[nsvc] = lens[1] 112 svc_port[nsvc] = ss_atoi(feed, offs[2], lens[2]) 113 svc_procs[nsvc] = 0; svc_rwin[nsvc] = 0 114 nsvc = nsvc + 1 115 } 116 } 117 } 118 cur = le + 1 119 } 120 121 // pass 2: PROC (lineage + per-svc proc count) + RESTART (per-svc window count) 122 cur = 0 123 while cur < n { 124 let le: i64 = ss_eol(feed, n, cur) 125 let nf: i64 = ss_split(feed, cur, le, offs, lens, 8) 126 if nf >= 1 { 127 if ss_tok_eq(feed, offs[0], lens[0], "PROC" as *u8) == 1 { 128 if nf >= 5 { 129 let pid: i64 = ss_atoi(feed, offs[1], lens[1]) 130 let sid: i64 = ss_atoi(feed, offs[3], lens[3]) 131 let no: i64 = offs[4] 132 let nl: i64 = lens[4] 133 if ss_tok_eq(feed, no, nl, "supervisor" as *u8) == 1 { 134 if pid == sid { sup = sup + 1 } // session-leader only -> keeper child (pid!=sid) excluded 135 } else { 136 var j: i64 = 0 137 while j < nsvc { 138 if ss_slice_eq(feed, no, nl, feed, svc_off[j], svc_len[j]) == 1 { svc_procs[j] = svc_procs[j] + 1 } 139 j = j + 1 140 } 141 } 142 } 143 } else { 144 if ss_tok_eq(feed, offs[0], lens[0], "RESTART" as *u8) == 1 { 145 if nf >= 2 { 146 var j: i64 = 0 147 while j < nsvc { 148 if ss_slice_eq(feed, offs[1], lens[1], feed, svc_off[j], svc_len[j]) == 1 { svc_rwin[j] = svc_rwin[j] + 1 } 149 j = j + 1 150 } 151 } 152 } 153 } 154 } 155 cur = le + 1 156 } 157 158 // emit the snapshot the API parses 159 var o: i64 = 0 160 o = ss_cat(out, o, "SUP " as *u8) 161 o = ss_catn(out, o, sup) 162 o = ss_cat(out, o, "\n" as *u8) 163 var k: i64 = 0 164 while k < nsvc { 165 o = ss_cat(out, o, "SVC " as *u8) 166 o = ss_cat_slice(out, o, feed, svc_off[k], svc_len[k]) 167 o = ss_cat(out, o, " " as *u8) 168 o = ss_catn(out, o, svc_port[k]) 169 o = ss_cat(out, o, " " as *u8) 170 if svc_procs[k] >= 1 { o = ss_cat(out, o, "UP" as *u8) } else { o = ss_cat(out, o, "DOWN" as *u8) } 171 o = ss_cat(out, o, " " as *u8) 172 o = ss_catn(out, o, svc_procs[k]) 173 o = ss_cat(out, o, " " as *u8) 174 o = ss_catn(out, o, svc_rwin[k]) 175 o = ss_cat(out, o, " " as *u8) 176 o = ss_catn(out, o, svc_rwin[k]) // rtot: window-scoped until the NAS adapter supplies a cumulative count 177 o = ss_cat(out, o, "\n" as *u8) 178 k = k + 1 179 } 180 return o 181}