code wiki / _hdl_build / nx_jobserver.nx

nx_jobserver.nx source

↩ module page · 138 lines · 7082 B

1// nx_jobserver.nx -- PERSISTENT sovereign job-server: ONE long-lived Nishi process that runs queued commands, 2// so the agent loop stops spawning a fresh wsl.exe + sh -c per command (the per-command churn that eats memory 3// and triggers the .wslconfig crashes). Single-slot file queue (no shell, no quoting): 4// - poll <dir>/nxjob.req (line1 = elf path, lines2.. = one arg each, VERBATIM) 5// - on arrival: consume it (unlink first so it never re-runs), fork/exec the organ DIRECTLY with a WATCHDOG 6// (kill -9 at <watchdog> seconds so a runaway can't hang/eat memory), capture stdout+stderr -> nxjob.out, 7// write the exit code -> nxjob.res (the DONE signal, written LAST). The caller writes nxjob.req atomically 8// (write tmp -> rename) and waits for nxjob.res. One process, file-driven, watchdogged, NO bash/PowerShell/sh. 9// usage: nx_jobserver <queuedir> <pollbudget> <watchdog-sec> (pollbudget bounds the run for testing; a real 10// daemon passes a large budget or is restarted by the supervisor). exit codes in res: 124=watchdog-killed, 11// 127=execve-failed, 125=empty job, else the child's code. 12// Sovereign: nx_syscalls only (fork/execve/wait4/dup3/clock_nanosleep/unlinkat). license_tier: ORIGINAL 13import "nx_syscalls.nx" 14import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 15const K_MAGIC_1000000: i64 = 1000000 16const K_MAGIC_1024: i64 = 1024 17 18func js_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 19// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 20// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 21// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 22// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 23func js_wn(v: i64) -> i64 { nxi_out(v); return 0 } 24func js_atoi(s: *u8) -> i64 { var v: i64 = 0; var i: i64 = 0; while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 } return v } 25// build dir + "/" + name -> out (null-terminated) 26func js_path(dir: *u8, name: *u8, out: *u8) -> i64 { 27 var o: i64 = 0; var i: i64 = 0 28 while dir[i] != (0 as u8) { out[o] = dir[i]; o = o + 1; i = i + 1 } 29 out[o] = 47 as u8; o = o + 1 30 i = 0 31 while name[i] != (0 as u8) { out[o] = name[i]; o = o + 1; i = i + 1 } 32 out[o] = 0 as u8 33 return o 34} 35func js_unlink(path: *u8) -> i64 { __syscall(263, AT_FDCWD, path, 0, 0, 0, 0) return 0 } 36func js_sleep_ms(ms: i64) -> i64 { 37 let ts: *i64 = sys_mmap(16) as *i64 38 ts[0] = ms / 1000 39 ts[1] = (ms % 1000) * K_MAGIC_1000000 40 return __syscall(SYS_CLOCK_NANOSLEEP, 0, 0, ts, 0, 0, 0) 41} 42// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 43// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 44// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 45// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 46func js_wn_fd(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 } 47 48// parse a job buffer -> argv, fork/exec the organ with output->out_path and a watchdog. returns exit code. 49func js_run_job(reqbuf: *u8, n: i64, out_path: *u8, wdog_sec: i64) -> i64 { 50 let buf: *u8 = sys_mmap(n + 8) 51 var ci: i64 = 0 52 while ci < n { buf[ci] = reqbuf[ci]; ci = ci + 1 } 53 buf[n] = 10 as u8 54 let cargv: *i64 = sys_mmap(8 * 128) as *i64 55 var ac: i64 = 0; var i: i64 = 0; var start: i64 = 0 56 while i <= n { 57 if (buf[i] as i64) == 10 { 58 var end: i64 = i 59 if end > start { if (buf[end - 1] as i64) == 13 { end = end - 1 } } 60 if end > start { buf[end] = 0 as u8; if ac < 127 { cargv[ac] = ((buf as i64) + start) as i64; ac = ac + 1 } } 61 start = i + 1 62 } 63 i = i + 1 64 } 65 cargv[ac] = 0 66 if ac == 0 { return 125 } 67 let out_fd: i64 = sys_openat_wr(out_path, 0x180) 68 if out_fd < 0 { return 126 } 69 let pid: i64 = sys_fork() 70 if pid == 0 { 71 sys_dup3(out_fd, 1, 0) 72 sys_dup3(out_fd, 2, 0) 73 let envp: *i64 = sys_mmap(16) as *i64 74 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64 75 envp[1] = 0 76 sys_execve(cargv[0] as *u8, cargv, envp) 77 sys_exit(127) 78 } 79 let st: *i64 = sys_mmap(16) as *i64 80 var ticks: i64 = 0 81 let deadline: i64 = wdog_sec * 5 // 200ms ticks 82 var rc: i64 = 124 83 var waiting: i64 = 1 84 while waiting == 1 { 85 let w: i64 = sys_wait4(pid, st, 1) // WNOHANG 86 if w == pid { 87 waiting = 0 88 if (st[0] % 128) != 0 { rc = 128 + (st[0] % 128) } else { rc = (st[0] >> 8) & 0xff } 89 } else { 90 ticks = ticks + 1 91 if ticks >= deadline { 92 waiting = 0 93 nx_kill(pid, 9) 94 sys_wait4(pid, st, 0) 95 rc = 124 96 } else { js_sleep_ms(200) } 97 } 98 } 99 sys_close(out_fd) 100 return rc 101} 102 103func main(argc: i64, argv: *i64) -> i64 { 104 if argc < 4 { sys_write(2, "usage: nx_jobserver <queuedir> <pollbudget> <watchdog-sec>\n" as *u8, 58); return 1 } 105 let dir: *u8 = argv[1] as *u8 106 let budget: i64 = js_atoi(argv[2] as *u8) 107 let wdog: i64 = js_atoi(argv[3] as *u8) 108 let req_path: *u8 = sys_mmap(K_MAGIC_1024); js_path(dir, "nxjob.req" as *u8, req_path) 109 let out_path: *u8 = sys_mmap(K_MAGIC_1024); js_path(dir, "nxjob.out" as *u8, out_path) 110 let res_path: *u8 = sys_mmap(K_MAGIC_1024); js_path(dir, "nxjob.res" as *u8, res_path) 111 let stop_path: *u8 = sys_mmap(K_MAGIC_1024); js_path(dir, "nxjob.stop" as *u8, stop_path) 112 js_w("nx_jobserver: watching " as *u8); js_w(dir); js_w(" (ONE persistent process; stop via nxjob.stop; budget=0 means forever)\n" as *u8) 113 var polls: i64 = 0 114 var ran: i64 = 0 115 var keep: i64 = 1 116 while keep == 1 { 117 let lenbox: *i64 = sys_mmap(16) as *i64; lenbox[0] = 0 118 let buf: *u8 = sys_read_file(req_path, lenbox) 119 if (buf as i64) != 0 { 120 if lenbox[0] > 0 { 121 js_unlink(req_path) 122 let code: i64 = js_run_job(buf, lenbox[0], out_path, wdog) 123 let rfd: i64 = sys_openat_wr(res_path, 0x180) 124 if rfd >= 0 { js_wn_fd(rfd, code); sys_write(rfd, "\n" as *u8, 1); sys_close(rfd) } 125 ran = ran + 1 126 js_w(" JOB " as *u8); js_wn(ran); js_w(" done exit=" as *u8); js_wn(code); js_w("\n" as *u8) 127 } 128 } 129 // stop signal (a real daemon needs a clean shutdown, not just a budget) 130 let sbox: *i64 = sys_mmap(16) as *i64; sbox[0] = 0 131 let sf: *u8 = sys_read_file(stop_path, sbox) 132 if (sf as i64) != 0 { keep = 0; js_unlink(stop_path) } 133 if keep == 1 { if budget > 0 { polls = polls + 1; if polls >= budget { keep = 0 } } } 134 if keep == 1 { js_sleep_ms(200) } 135 } 136 js_w("nx_jobserver: exit (jobs run=" as *u8); js_wn(ran); js_w(")\n" as *u8) 137 return 0 138}