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}