code wiki / (root) / nx_os_proc.nx

nx_os_proc.nx source

↩ module page · 333 lines · 16271 B

1// nx_os_proc.nx -- OS PROCESS-INTROSPECTION abstraction (the PORTABILITY SEAM). THE ONE place OS-specific 2// process access lives, so every tool above it (nx_heal, ...) stays OS-AGNOSTIC + portable. Interop by 3// construction: ONE source compiles to BOTH backends via the @ifdef target guard -- 4// LINUX backend = procfs (/proc/<pid>/{stat,cmdline}, getdents on /proc) [current NAS deploy] 5// NISHIOS backend = native process table (@ifdef TARGET_NISHI seam) -- superior: a direct kernel 6// query, NO text-parsing of /proc, NO USER_HZ guesswork. FAILS LOUD until wired, 7// so a NishiOS build never silently inherits Linux assumptions. 8// LAW (portability): NEVER scatter raw /proc, /sys, /dev, or hardcoded syscall numbers through the LOGIC 9// layer -- put OS-specifics behind an nx_os_* seam like this one. The sovereign core (seg_store, tool 10// logic) already only touches nx_syscalls (ABI-abstracted); this extends the same discipline to OS features. 11// license_tier: ORIGINAL 12import "nx_syscalls.nx" 13 14const OSP_HZ_LINUX: i64 = 100 // Linux USER_HZ: /proc/<pid>/stat starttime ticks/sec 15const OSP_PATH_CAP: i64 = 256 16const OSP_RD_CAP: i64 = 4096 17const OSP_STAT_CAP: i64 = 262144 // /proc/stat whole-file read cap (btime scan) 18const OSP_DENT_BUF: i64 = 65536 // getdents64 batch buffer (proven sizing) 19const OSP_F_PPID: i64 = 2 // /proc/<pid>/stat field after ')': state=1 ppid=2 ... utime=12 stime=13 ... starttime=20 20const OSP_F_START: i64 = 20 21const OSP_F_UTIME: i64 = 12 // user-mode CPU ticks (cumulative) 22const OSP_F_STIME: i64 = 13 // kernel-mode CPU ticks (cumulative) 23const OSP_ASCII_0: i64 = 48 24const OSP_ASCII_9: i64 = 57 25const OSP_SP: i64 = 32 26const OSP_NL: i64 = 10 27const OSP_RP: i64 = 41 // ')' 28const OSP_SLASH: i64 = 47 29const OSP_NUL: i64 = 0 30const OSP_SENTINEL: i64 = 0 - 1 // "not available on this OS backend" 31 32func osp_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (OSP_NUL as u8) { n = n + 1 } return n } 33// pid integer -> decimal string in out; return len 34func osp_itoa(v: i64, out: *u8) -> i64 { 35 if v == 0 { out[0] = OSP_ASCII_0 as u8; out[1] = 0 as u8; return 1 } 36 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0 37 while m > 0 { t[k] = (OSP_ASCII_0 + (m % 10)) as u8; m = m / 10; k = k + 1 } 38 var i: i64 = 0 39 while i < k { out[i] = t[k-1-i]; i = i + 1 } 40 out[k] = 0 as u8 41 return k 42} 43// parse leading integer at buf[off..]; endp[0] = position of the FIRST non-digit (NOT n) so a caller 44// walking fields can resume there. BUG-FIX 2026-07-16: the old `i = n` break jumped to end-of-buffer, 45// so osp_stat_field skipped every field after the first non-numeric one (the state char) and never 46// reached starttime (field 20) -- diagnose then dropped every real process. 47func osp_num(buf: *u8, n: i64, off: i64, endp: *i64) -> i64 { 48 var v: i64 = 0; var i: i64 = off; var any: i64 = 0; var go: i64 = 1 49 while go == 1 { 50 go = 0 51 if i < n { let c: i64 = buf[i] as i64; if c >= OSP_ASCII_0 { if c <= OSP_ASCII_9 { v = v*(10 as i64)+(c-OSP_ASCII_0); any = 1; i = i + 1; go = 1 } } } 52 } 53 endp[0] = i 54 if any == 0 { return OSP_SENTINEL } 55 return v 56} 57// basename (after last '/') of NUL-terminated s 58func osp_basename(s: *u8) -> *u8 { 59 var i: i64 = 0; var last: i64 = 0 60 while s[i] != (OSP_NUL as u8) { if s[i] == (OSP_SLASH as u8) { last = i + 1 } i = i + 1 } 61 return (s as i64 + last) as *u8 62} 63 64// ============================ LINUX BACKEND (procfs) ============================ 65// NOTE: the OS-target guard is currently a SOURCE-SWAP seam, not a compile-time @ifdef -- nx_cc's 66// preprocessor is arch-guard-only today (TARGET_X86_64); a custom TARGET_NISHI compiled BOTH branches 67// and the stub won (proven 2026-07-16). When nx_cc gains OS-target guards, wrap this in @ifdef 68// TARGET_LINUX and the NishiOS backend (spec at bottom) in @ifdef TARGET_NISHI. The portability WIN 69// stands regardless: every raw /proc lives HERE and nowhere else, so swapping the backend is a 70// single-file change with the whole LOGIC layer (nx_heal) untouched. 71// bounded read of a whole (small) proc/file into buf; returns len (0 on empty/absent) 72func osp_bread(path: *u8, buf: *u8, cap: i64) -> i64 { 73 let fd: i64 = sys_openat_rd(path) 74 if fd < 0 { return 0 } 75 var got: i64 = 0; var go: i64 = 1 76 while go == 1 { let r: i64 = sys_read(fd, (buf as i64 + got) as *u8, cap - got); if r > 0 { got = got + r; if got >= cap { go = 0 } } else { go = 0 } } 77 sys_close(fd) 78 return got 79} 80// build "/proc/<pid><leaf>" (leaf NUL-terminated, e.g. "/stat") into out 81func osp_ppath(out: *u8, pid: i64, leaf: *u8) -> i64 { 82 var o: i64 = 0 83 let pre: *u8 = "/proc/" as *u8 84 var i: i64 = 0 85 while pre[i] != (OSP_NUL as u8) { out[o] = pre[i]; o = o + 1; i = i + 1 } 86 o = o + osp_itoa(pid, (out as i64 + o) as *u8) 87 i = 0 88 while leaf[i] != (OSP_NUL as u8) { out[o] = leaf[i]; o = o + 1; i = i + 1 } 89 out[o] = 0 as u8 90 return o 91} 92// enumerate live pids into pids[0..cap); returns count 93func osp_list_pids(pids: *i64, cap: i64) -> i64 { 94 let fd: i64 = sys_openat_rd("/proc" as *u8) 95 if fd < 0 { return 0 } 96 let dbuf: *u8 = sys_mmap(OSP_DENT_BUF) 97 let ep: *i64 = sys_mmap(16) as *i64 98 var cnt: i64 = 0; var run: i64 = 1 99 while run == 1 { 100 let n: i64 = sys_getdents64(fd, dbuf, OSP_DENT_BUF) 101 if n <= 0 { run = 0 } else { 102 var off: i64 = 0 103 while off < n { 104 let rec: *u8 = ((dbuf as i64 + off) as *u8) 105 let reclen: i64 = dirent_reclen(rec) 106 if reclen <= 0 { off = n } else { 107 let name: *u8 = dirent_name(rec) 108 if name[0] >= (OSP_ASCII_0 as u8) { if name[0] <= (OSP_ASCII_9 as u8) { 109 if cnt < cap { let pid: i64 = osp_num(name, osp_slen(name), 0, ep); if pid > 0 { pids[cnt] = pid; cnt = cnt + 1 } } 110 } } 111 off = off + reclen 112 } 113 } 114 } 115 } 116 sys_close(fd) 117 return cnt 118} 119// count OPEN FILE DESCRIPTORS of pid = numeric entries in /proc/<pid>/fd. -1 if unreadable (gone/no perm). 120// The fd METER for anomaly detection: a socket/file-descriptor leak trends up HERE while VmSize can stay 121// flat (the fd table is not the address space) -- a leak shows in whatever resource disappears. Leak-free. 122func osp_fd_count_from_fd_result(fd: i64, cause: *i64) -> i64 { 123 if (cause as i64) != 0 { cause[0]=0 } 124 let dbuf: *u8=sys_mmap(OSP_DENT_BUF) 125 var count: i64=0 126 var running: i64=1 127 var failure: i64=0 128 while running == 1 { 129 let n: i64=sys_getdents64(fd,dbuf,OSP_DENT_BUF) 130 if n < 0 { failure=n; running=0 } else { 131 if n == 0 { running=0 } else { 132 var off: i64=0 133 while off < n { 134 let rec: *u8=((dbuf as i64)+off) as *u8 135 let reclen: i64=dirent_reclen(rec) 136 if reclen <= 0 { failure=OSP_SENTINEL; running=0; break } 137 let name: *u8=dirent_name(rec) 138 if name[0] >= OSP_ASCII_0 as u8 && name[0] <= OSP_ASCII_9 as u8 { count=count+1 } 139 off=off+reclen 140 } 141 } 142 } 143 } 144 sys_munmap(dbuf,OSP_DENT_BUF) 145 if failure < 0 { if (cause as i64) != 0 { cause[0]=failure }; return OSP_SENTINEL } 146 return count 147} 148func osp_fd_count_from_fd(fd: i64) -> i64 { 149 return osp_fd_count_from_fd_result(fd,0 as *i64) 150} 151func osp_fd_count(pid: i64) -> i64 { 152 let path: *u8=sys_mmap(OSP_PATH_CAP) 153 osp_ppath(path,pid,"/fd") 154 let fd: i64=sys_openat_rd(path) 155 sys_munmap(path,OSP_PATH_CAP) 156 if fd < 0 { return OSP_SENTINEL } 157 let count: i64=osp_fd_count_from_fd(fd) 158 sys_close(fd) 159 return count 160} 161 162// field after the last ')' in /proc/<pid>/stat: OSP_F_PPID or OSP_F_START; SENTINEL on fail 163func osp_stat_field(pid: i64, fidx: i64) -> i64 { 164 let p: *u8 = sys_mmap(OSP_PATH_CAP) 165 osp_ppath(p, pid, "/stat" as *u8) 166 let b: *u8 = sys_mmap(OSP_RD_CAP) 167 let n: i64 = osp_bread(p, b, OSP_RD_CAP - 1) 168 if n <= 0 { return OSP_SENTINEL } 169 var rp: i64 = 0 - 1; var i: i64 = 0 170 while i < n { if b[i] == (OSP_RP as u8) { rp = i } i = i + 1 } 171 if rp < 0 { return OSP_SENTINEL } 172 var f: i64 = 0; i = rp + 1 173 let ep: *i64 = sys_mmap(16) as *i64 174 while i < n { 175 if b[i] == (OSP_SP as u8) { i = i + 1 } else { 176 f = f + 1 177 let v: i64 = osp_num(b, n, i, ep) 178 if f == fidx { return v } 179 i = ep[0] 180 var go: i64 = 1 181 while go == 1 { go = 0; if i < n { if b[i] != (OSP_SP as u8) { i = i + 1; go = 1 } } } 182 } 183 } 184 return OSP_SENTINEL 185} 186func osp_ppid(pid: i64) -> i64 { return osp_stat_field(pid, OSP_F_PPID) } 187func osp_starttime_ticks(pid: i64) -> i64 { return osp_stat_field(pid, OSP_F_START) } 188// cumulative CPU ticks consumed by pid = utime + stime. The CPU METER for anomaly detection: sampled over 189// time, its RATE (Theil-Sen slope of the per-interval deltas) = the burn = "power disappearing" (a busy-loop 190// pegs a core). SENTINEL if unreadable. HZ ticks/sec via osp_hz(). 191func osp_cpu_ticks(pid: i64) -> i64 { 192 let u: i64 = osp_stat_field(pid, OSP_F_UTIME) 193 let s: i64 = osp_stat_field(pid, OSP_F_STIME) 194 if u == OSP_SENTINEL { return OSP_SENTINEL } 195 if s == OSP_SENTINEL { return OSP_SENTINEL } 196 return u + s 197} 198// argv0 basename of /proc/<pid>/cmdline into out; return len (0 if none) 199func osp_cmd_argv0(pid: i64, out: *u8, cap: i64) -> i64 { 200 let p: *u8 = sys_mmap(OSP_PATH_CAP) 201 osp_ppath(p, pid, "/cmdline" as *u8) 202 let cl: *u8 = sys_mmap(cap + 1) 203 let n: i64 = osp_bread(p, cl, cap) 204 if n <= 0 { out[0] = 0 as u8; return 0 } 205 cl[n] = 0 as u8 // argv0 = bytes up to the first NUL (already there) 206 let bn: *u8 = osp_basename(cl) 207 var o: i64 = 0 208 while bn[o] != (OSP_NUL as u8) { out[o] = bn[o]; o = o + 1 } 209 out[o] = 0 as u8 210 return o 211} 212func osp_hz() -> i64 { return OSP_HZ_LINUX } 213// where the HOST SUPERVISOR writes its log -- a deployment/OS question, so it lives in the seam. 214// Linux/NAS deploy: /tmp/supervisor.log (nx_hostctl supervise). NishiOS: its native supervisor journal. 215func osp_supervisor_log() -> *u8 { return "/tmp/supervisor.log" as *u8 } 216func osp_uptime_s() -> i64 { let ts: *i64 = sys_mmap(16) as *i64; sys_clock_gettime_mono(ts); return ts[0] } 217func osp_selfpid() -> i64 { 218 let b: *u8 = sys_mmap(OSP_RD_CAP) 219 let n: i64 = osp_bread("/proc/self/stat" as *u8, b, OSP_RD_CAP - 1) 220 if n <= 0 { return OSP_SENTINEL } 221 let ep: *i64 = sys_mmap(16) as *i64 222 return osp_num(b, n, 0, ep) 223} 224// wallclock epoch = /proc/stat btime + monotonic-since-boot 225func osp_boot_epoch() -> i64 { 226 let b: *u8 = sys_mmap(OSP_STAT_CAP) 227 let n: i64 = osp_bread("/proc/stat" as *u8, b, OSP_STAT_CAP - 1) 228 if n <= 0 { return 0 } 229 let needle: *u8 = "btime " as *u8 230 var i: i64 = 0 231 let ep: *i64 = sys_mmap(16) as *i64 232 while i < n { 233 var m: i64 = 1; var k: i64 = 0 234 while needle[k] != (OSP_NUL as u8) { if i+k >= n { m = 0 } else { if b[i+k] != needle[k] { m = 0 } } k = k + 1 } 235 if m == 1 { return osp_num(b, n, i + k, ep) } 236 i = i + 1 237 } 238 return 0 239} 240 241// ======================= NISHIOS BACKEND SPEC (the swap-in seam) ======================= 242// When NishiOS's native process API lands, REPLACE the Linux backend above (or @ifdef-branch it once 243// nx_cc has OS-target guards) with these ~8 functions over NishiOS's DIRECT kernel process table -- 244// superior to procfs: no /proc text-parsing, no USER_HZ, a real syscall query. The interface the LOGIC 245// layer depends on (and ALL it depends on) is exactly: 246// osp_list_pids(pids,cap)->count nishi_proc_enumerate (live pids) 247// osp_ppid(pid)->ppid nishi_proc_parent 248// osp_starttime_ticks(pid)->ticks nishi_proc_starttime (osp_hz() ticks/sec) 249// osp_cmd_argv0(pid,out,cap)->len nishi_proc_argv0 (basename of argv0) 250// osp_hz()->ticks_per_sec native rate (not the Linux-100 assumption) 251// osp_uptime_s()->secs sys_clock_gettime_mono (already OS-neutral) 252// osp_selfpid()->pid nishi_getpid 253// osp_boot_epoch()->epoch nishi_boot_epoch 254// Interop: NishiOS ships the superior backend; Linux stays supported for the current NAS deploy; the 255// LOGIC (nx_heal) compiles unchanged on both. THAT is the point of this file. 256 257// Direct-child enumeration does not depend on CONFIG_CHECKPOINT_RESTORE's 258// optional /proc/<pid>/task/<pid>/children file. The caller owns the output 259// capacity; overflow is an error, never a plausible partial child set. 260const OSP_DIRENT_NAME_OFFSET: i64 = 19 // Linux linux_dirent64 ABI 261func osp_stat_parent(buf: *u8, n: i64) -> i64 { 262 var last: i64=0-1;var i: i64=0 263 while i < n { if buf[i] == OSP_RP as u8 { last=i };i=i+1 } 264 if last < 0 { return 0-5 } 265 i=last+1 266 while i < n && buf[i] == OSP_SP as u8 { i=i+1 } 267 while i < n && buf[i] != OSP_SP as u8 { i=i+1 } 268 while i < n && buf[i] == OSP_SP as u8 { i=i+1 } 269 let first: i64=i;var parent: i64=0 270 while i < n && buf[i] >= OSP_ASCII_0 as u8 && buf[i] <= OSP_ASCII_9 as u8 { 271 parent=parent*10+(buf[i] as i64)-OSP_ASCII_0;i=i+1 272 } 273 if i == first || i == n || buf[i] != OSP_SP as u8 { return 0-5 } 274 return parent 275} 276func osp_children(parent: i64, children: *i64, capacity: i64) -> i64 { 277 if parent <= 0 || capacity < 0 { return 0-22 } 278 let directory: i64=sys_openat_directory("/proc") 279 if directory < 0 { return directory } 280 let batch: *u8=sys_mmap(OSP_DENT_BUF) 281 let path: *u8=sys_mmap(OSP_PATH_CAP) 282 let stat: *u8=sys_mmap(OSP_RD_CAP) 283 var count: i64=0;var failure: i64=0;var running: i64=1 284 while running == 1 { 285 let n: i64=sys_getdents64(directory,batch,OSP_DENT_BUF) 286 if n == (0-4) { continue } 287 if n <= 0 { failure=n;break } 288 var off: i64=0 289 while off < n { 290 if n-off <= OSP_DIRENT_NAME_OFFSET { failure=0-5;running=0;break } 291 let rec: *u8=batch+off 292 let size: i64=dirent_reclen(rec) 293 if size <= OSP_DIRENT_NAME_OFFSET || size > n-off { failure=0-5;running=0;break } 294 var i: i64=OSP_DIRENT_NAME_OFFSET;var pid: i64=0 295 while i < size && rec[i] >= OSP_ASCII_0 as u8 && rec[i] <= OSP_ASCII_9 as u8 { 296 pid=pid*10+(rec[i] as i64)-OSP_ASCII_0;i=i+1 297 } 298 if pid > 0 && i < size && rec[i] == OSP_NUL as u8 { 299 osp_ppath(path,pid,"/stat") 300 let fd: i64=sys_openat_rd(path) 301 // A process may disappear during enumeration; other failures 302 // make completeness unproven and must propagate. 303 if fd < 0 && fd != (0-2) { failure=fd;running=0;break } 304 if fd >= 0 { 305 var used: i64=0;var readrc: i64=1 306 while readrc > 0 && used < OSP_RD_CAP { 307 readrc=sys_read(fd,stat+used,OSP_RD_CAP-used) 308 if readrc == (0-4) { readrc=1;continue } 309 if readrc > 0 { used=used+readrc } 310 } 311 let closed: i64=sys_close(fd) 312 if readrc < 0 && readrc != (0-3) { failure=readrc;running=0;break } 313 if closed < 0 { failure=closed;running=0;break } 314 if used == OSP_RD_CAP { failure=0-75;running=0;break } 315 if used > 0 { 316 let observed: i64=osp_stat_parent(stat,used) 317 if observed < 0 { failure=observed;running=0;break } 318 if observed == parent { 319 if count >= capacity { failure=0-28;running=0;break } 320 children[count]=pid;count=count+1 321 } 322 } 323 } 324 } 325 off=off+size 326 } 327 } 328 let closed: i64=sys_close(directory) 329 sys_munmap(batch,OSP_DENT_BUF);sys_munmap(path,OSP_PATH_CAP);sys_munmap(stat,OSP_RD_CAP) 330 if failure < 0 { return failure } 331 if closed < 0 { return closed } 332 return count 333}