code wiki / (root) / nx_os_proc.nx

nx_os_proc.nx source

↩ module page · 243 lines · 12121 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(pid: i64) -> i64 { 123 let path: *u8 = sys_mmap(OSP_PATH_CAP) 124 osp_ppath(path, pid, "/fd" as *u8) 125 let fd: i64 = sys_openat_rd(path) 126 if fd < 0 { sys_munmap(path, OSP_PATH_CAP); return 0 - 1 } 127 let dbuf: *u8 = sys_mmap(OSP_DENT_BUF) 128 var cnt: i64 = 0 129 var run: i64 = 1 130 while run == 1 { 131 let n: i64 = sys_getdents64(fd, dbuf, OSP_DENT_BUF) 132 if n <= 0 { run = 0 } else { 133 var off: i64 = 0 134 while off < n { 135 let rec: *u8 = ((dbuf as i64 + off) as *u8) 136 let reclen: i64 = dirent_reclen(rec) 137 if reclen <= 0 { off = n } else { 138 let name: *u8 = dirent_name(rec) 139 if name[0] >= (OSP_ASCII_0 as u8) { if name[0] <= (OSP_ASCII_9 as u8) { cnt = cnt + 1 } } 140 off = off + reclen 141 } 142 } 143 } 144 } 145 sys_close(fd) 146 sys_munmap(path, OSP_PATH_CAP) 147 sys_munmap(dbuf, OSP_DENT_BUF) 148 return cnt 149} 150// field after the last ')' in /proc/<pid>/stat: OSP_F_PPID or OSP_F_START; SENTINEL on fail 151func osp_stat_field(pid: i64, fidx: i64) -> i64 { 152 let p: *u8 = sys_mmap(OSP_PATH_CAP) 153 osp_ppath(p, pid, "/stat" as *u8) 154 let b: *u8 = sys_mmap(OSP_RD_CAP) 155 let n: i64 = osp_bread(p, b, OSP_RD_CAP - 1) 156 if n <= 0 { return OSP_SENTINEL } 157 var rp: i64 = 0 - 1; var i: i64 = 0 158 while i < n { if b[i] == (OSP_RP as u8) { rp = i } i = i + 1 } 159 if rp < 0 { return OSP_SENTINEL } 160 var f: i64 = 0; i = rp + 1 161 let ep: *i64 = sys_mmap(16) as *i64 162 while i < n { 163 if b[i] == (OSP_SP as u8) { i = i + 1 } else { 164 f = f + 1 165 let v: i64 = osp_num(b, n, i, ep) 166 if f == fidx { return v } 167 i = ep[0] 168 var go: i64 = 1 169 while go == 1 { go = 0; if i < n { if b[i] != (OSP_SP as u8) { i = i + 1; go = 1 } } } 170 } 171 } 172 return OSP_SENTINEL 173} 174func osp_ppid(pid: i64) -> i64 { return osp_stat_field(pid, OSP_F_PPID) } 175func osp_starttime_ticks(pid: i64) -> i64 { return osp_stat_field(pid, OSP_F_START) } 176// cumulative CPU ticks consumed by pid = utime + stime. The CPU METER for anomaly detection: sampled over 177// time, its RATE (Theil-Sen slope of the per-interval deltas) = the burn = "power disappearing" (a busy-loop 178// pegs a core). SENTINEL if unreadable. HZ ticks/sec via osp_hz(). 179func osp_cpu_ticks(pid: i64) -> i64 { 180 let u: i64 = osp_stat_field(pid, OSP_F_UTIME) 181 let s: i64 = osp_stat_field(pid, OSP_F_STIME) 182 if u == OSP_SENTINEL { return OSP_SENTINEL } 183 if s == OSP_SENTINEL { return OSP_SENTINEL } 184 return u + s 185} 186// argv0 basename of /proc/<pid>/cmdline into out; return len (0 if none) 187func osp_cmd_argv0(pid: i64, out: *u8, cap: i64) -> i64 { 188 let p: *u8 = sys_mmap(OSP_PATH_CAP) 189 osp_ppath(p, pid, "/cmdline" as *u8) 190 let cl: *u8 = sys_mmap(cap + 1) 191 let n: i64 = osp_bread(p, cl, cap) 192 if n <= 0 { out[0] = 0 as u8; return 0 } 193 cl[n] = 0 as u8 // argv0 = bytes up to the first NUL (already there) 194 let bn: *u8 = osp_basename(cl) 195 var o: i64 = 0 196 while bn[o] != (OSP_NUL as u8) { out[o] = bn[o]; o = o + 1 } 197 out[o] = 0 as u8 198 return o 199} 200func osp_hz() -> i64 { return OSP_HZ_LINUX } 201// where the HOST SUPERVISOR writes its log -- a deployment/OS question, so it lives in the seam. 202// Linux/NAS deploy: /tmp/supervisor.log (nx_hostctl supervise). NishiOS: its native supervisor journal. 203func osp_supervisor_log() -> *u8 { return "/tmp/supervisor.log" as *u8 } 204func osp_uptime_s() -> i64 { let ts: *i64 = sys_mmap(16) as *i64; sys_clock_gettime_mono(ts); return ts[0] } 205func osp_selfpid() -> i64 { 206 let b: *u8 = sys_mmap(OSP_RD_CAP) 207 let n: i64 = osp_bread("/proc/self/stat" as *u8, b, OSP_RD_CAP - 1) 208 if n <= 0 { return OSP_SENTINEL } 209 let ep: *i64 = sys_mmap(16) as *i64 210 return osp_num(b, n, 0, ep) 211} 212// wallclock epoch = /proc/stat btime + monotonic-since-boot 213func osp_boot_epoch() -> i64 { 214 let b: *u8 = sys_mmap(OSP_STAT_CAP) 215 let n: i64 = osp_bread("/proc/stat" as *u8, b, OSP_STAT_CAP - 1) 216 if n <= 0 { return 0 } 217 let needle: *u8 = "btime " as *u8 218 var i: i64 = 0 219 let ep: *i64 = sys_mmap(16) as *i64 220 while i < n { 221 var m: i64 = 1; var k: i64 = 0 222 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 } 223 if m == 1 { return osp_num(b, n, i + k, ep) } 224 i = i + 1 225 } 226 return 0 227} 228 229// ======================= NISHIOS BACKEND SPEC (the swap-in seam) ======================= 230// When NishiOS's native process API lands, REPLACE the Linux backend above (or @ifdef-branch it once 231// nx_cc has OS-target guards) with these ~8 functions over NishiOS's DIRECT kernel process table -- 232// superior to procfs: no /proc text-parsing, no USER_HZ, a real syscall query. The interface the LOGIC 233// layer depends on (and ALL it depends on) is exactly: 234// osp_list_pids(pids,cap)->count nishi_proc_enumerate (live pids) 235// osp_ppid(pid)->ppid nishi_proc_parent 236// osp_starttime_ticks(pid)->ticks nishi_proc_starttime (osp_hz() ticks/sec) 237// osp_cmd_argv0(pid,out,cap)->len nishi_proc_argv0 (basename of argv0) 238// osp_hz()->ticks_per_sec native rate (not the Linux-100 assumption) 239// osp_uptime_s()->secs sys_clock_gettime_mono (already OS-neutral) 240// osp_selfpid()->pid nishi_getpid 241// osp_boot_epoch()->epoch nishi_boot_epoch 242// Interop: NishiOS ships the superior backend; Linux stays supported for the current NAS deploy; the 243// LOGIC (nx_heal) compiles unchanged on both. THAT is the point of this file.