nx_run_timeout.nx source
↩ module page · 67 lines · 3236 B
1// nx_run_timeout.nx -- sovereign RUN-WITH-TIMEOUT primitive (bits-up, reusable).
2//
3// REASON TO EXIST: the engineer's cockpit (nx_engineer.nx) runs gates with a
4// BLOCKING sys_wait4(pid, st, 0) -- so a gate that HANGS (infinite loop) hangs
5// the engineer itself. It can clock SLOW but cannot detect a HANG. This is the
6// bits-up capability gap. nx_run_timeout closes it: fork -> exec -> poll-wait
7// against a monotonic deadline -> KILL on overrun. Returns the child's exit
8// code, or NX_RT_TIMEOUT if it had to be killed (a true infinite loop).
9//
10// Reuse set: nx_engineer (detect hanging gates, not just slow), the compiler
11// loop-detector (nx_cc_probe), any supervised spawn that must not block forever.
12// NishiLang-native -- replaces `timeout N cmd` shell scaffolding.
13// license_tier: ORIGINAL
14
15import "nx_syscalls.nx"
16
17// kill(2) number: x86_64=62, rv64=129 (asm-generic). Matches nx_syscalls' own
18// @ifdef-selected process-control consts, which are NOT backend-translated.
19// alarm(2): x86_64 = 37. (rv64 has no plain alarm -> setitimer; dev box is x86.)
20@ifdef TARGET_X86_64
21const NX_RT_SYS_ALARM: i64 = 37
22@endif
23@ifndef TARGET_X86_64
24const NX_RT_SYS_ALARM: i64 = 37
25@endif
26
27const NX_RT_TIMEOUT: i64 = 1000 // child SIGALRM'd at the deadline (a HANG/loop)
28const NX_RT_FORK_FAIL: i64 = 1001 // fork or wait anomaly
29const NX_RT_SIGALRM: i64 = 14 // the deadline signal (default action = terminate)
30
31// arm a self-timeout on the CURRENT process: SIGALRM after `sec` seconds. The
32// child arms this just before execve, so a hung child terminates itself -- no
33// fragile parent-side poll/kill (which failed to stop a truly-looping child).
34func nx_rt_alarm(sec: i64) -> i64 {
35 return __syscall(NX_RT_SYS_ALARM, sec, 0, 0, 0, 0, 0)
36}
37
38// Run `path` with `argv`/`envp`, stdout+stderr -> /dev/null, killing it if it
39// runs past `timeout_ms`. Writes wall-time (ms) to *elapsed_ms_out if non-null.
40// Returns child exit code (0..255), NX_RT_TIMEOUT (killed), or NX_RT_FORK_FAIL.
41func nx_run_timeout(path: *u8, argv: *i64, envp: *i64,
42 timeout_ms: i64, elapsed_ms_out: *i64) -> i64 {
43 let t0: i64 = sys_now_ms()
44 var tsec: i64 = (timeout_ms + 999) / 1000 // ceil to whole seconds
45 if tsec < 1 { tsec = 1 }
46 let pid: i64 = sys_fork()
47 if pid == 0 {
48 let dn: i64 = sys_openat_wr("/dev/null" as *u8, 0)
49 if dn >= 0 { sys_dup3(dn, 1, 0); sys_dup3(dn, 2, 0) }
50 nx_rt_alarm(tsec) // self-deadline: SIGALRM terminates a hung child
51 sys_execve(path, argv, envp)
52 sys_exit(127)
53 }
54 if pid < 0 {
55 if (elapsed_ms_out as i64) != 0 { elapsed_ms_out[0] = 0 }
56 return NX_RT_FORK_FAIL
57 }
58 let st: *i64 = sys_mmap(16) as *i64
59 let r: i64 = sys_wait4(pid, st, 0) // BLOCKING wait -- robust, proven in nx_engineer
60 if (elapsed_ms_out as i64) != 0 { elapsed_ms_out[0] = sys_now_ms() - t0 }
61 if r < 0 { return NX_RT_FORK_FAIL }
62 let status: i64 = st[0]
63 let termsig: i64 = status & 0x7f
64 if termsig == NX_RT_SIGALRM { return NX_RT_TIMEOUT } // deadline hit = HANG/loop
65 if termsig != 0 { return 1000 + termsig } // other signal = crashed (not healthy)
66 return wait_exit_code(status)
67}