nx_torrent_keep_gate.nx source
↩ module page · 104 lines · 5597 B
1// nx_torrent_keep_gate.nx -- SOVEREIGN proof that the local keeper (nx_torrent_keep) supervises the media
2// daemon: with the keeper running, KILLING the daemon results in it being RESPAWNED with a NEW live pid.
3// This is the measured, re-runnable S-class regression guard for the workstation-local death-respawn
4// supervision that brings the local daemon to NAS-parity (the NAS already has nx_hostctl). The whole
5// scenario runs in ONE process so it is not affected by cross-call WSL VM recycling.
6//
7// Run: build the keeper elf first (nx_sov_build_run.elf nx_torrent_keep --build-only), then run this gate.
8// license_tier: ORIGINAL layer: gate over the L7 keeper
9// module: nishi-core.torrent.keep_gate
10// depends: nishi-core.syscalls
11import "nx_syscalls.nx"
12import "nx_gate_verdict.nx"
13
14func g_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
15func g_putn(v: i64) -> i64 {
16 let bb: *u8 = sys_mmap(28); var m: i64 = v; if m < 0 { m = 0 - m; sys_write(1, "-" as *u8, 1) }
17 let t: *u8 = sys_mmap(28); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 }
18 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
19 var i: i64 = 0; while i < k { bb[i] = t[k-1-i]; i = i + 1 }
20 sys_write(1, bb, k); return 0
21}
22func g_report(label: *u8, ok: i64) -> i64 {
23 if ok == 1 { g_puts(" [PASS] " as *u8) } else { g_puts(" [FAIL] " as *u8) }
24 g_puts(label); g_puts("\n" as *u8); return 0
25}
26func g_read_pid(path: *u8) -> i64 {
27 let fd: i64 = sys_openat_rd(path); if fd < 0 { return 0 - 1 }
28 let b: *u8 = sys_mmap(32); let n: i64 = sys_read(fd, b, 31); sys_close(fd)
29 if n <= 0 { return 0 - 1 }
30 var v: i64 = 0; var i: i64 = 0
31 while i < n { let c: i64 = b[i] as i64; if c >= 48 { if c <= 57 { v = v*10 + (c-48); i = i + 1 } else { i = n } } else { i = n } }
32 return v
33}
34func g_alive(pid: i64) -> i64 { if pid <= 0 { return 0 } if nx_kill(pid, 0) == 0 { return 1 } return 0 }
35// fork+exec the WOMB to (re)ensure the daemon up (idempotent); build/launch noise -> /dev/null; wait for it.
36func g_ensure_up(envp: *i64) -> i64 {
37 let womb: *u8 = "_offc/nx_sov_build_run.elf" as *u8
38 let pid: i64 = sys_fork()
39 if pid == 0 {
40 let dn: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4)
41 if dn >= 0 { sys_dup3(dn, 1, 0); sys_dup3(dn, 2, 0) }
42 let av: *i64 = sys_mmap(8*4) as *i64; av[0] = womb as i64; av[1] = "nx_torrent_up" as *u8 as i64; av[2] = 0
43 sys_execve(womb, av, envp); sys_exit(127)
44 }
45 let st: *i64 = sys_mmap(16) as *i64; sys_wait4(pid, st, 0)
46 return (st[0] >> 8) & 0xff
47}
48
49func main() -> i64 {
50 let pidfile: *u8 = "/tmp/nx_torrentd.pid" as *u8
51 let keepelf: *u8 = "/tmp/nx_torrent_keep.sov.elf" as *u8
52 let envp: *i64 = sys_mmap(8*4) as *i64; envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64; envp[1] = 0
53 var pass: i64 = 0; var tot: i64 = 0
54
55 // 1. ensure the daemon is up; capture its pid
56 g_ensure_up(envp)
57 let pid0: i64 = g_read_pid(pidfile)
58 var t1: i64 = 0; if pid0 > 0 { if g_alive(pid0) == 1 { t1 = 1 } }
59 tot = tot + 1; if t1 == 1 { pass = pass + 1 } g_report("daemon initially UP (pre-keeper)" as *u8, t1)
60 g_puts(" pid0=" as *u8); g_putn(pid0); g_puts("\n" as *u8)
61
62 // 2. launch the keeper as a child of this gate (so we can stop it deterministically afterward)
63 let kpid: i64 = sys_fork()
64 if kpid == 0 {
65 let dn: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4)
66 if dn >= 0 { sys_dup3(dn, 1, 0); sys_dup3(dn, 2, 0) }
67 let av: *i64 = sys_mmap(8*4) as *i64; av[0] = keepelf as i64; av[1] = 0
68 sys_execve(keepelf, av, envp); sys_exit(127)
69 }
70 sys_sleep_ms(3000) // let the keeper adopt the running daemon
71 var t2: i64 = 0; if g_alive(kpid) == 1 { t2 = 1 }
72 tot = tot + 1; if t2 == 1 { pass = pass + 1 } g_report("keeper launched + running" as *u8, t2)
73
74 // 3. KILL the daemon (simulate a crash). The keeper polls every 5s, so at +1.5s it is still down.
75 if pid0 > 0 { nx_kill(pid0, 9) }
76 sys_sleep_ms(1500)
77 var t3: i64 = 0; if g_alive(pid0) == 0 { t3 = 1 }
78 tot = tot + 1; if t3 == 1 { pass = pass + 1 } g_report("daemon killed (pid0 gone)" as *u8, t3)
79
80 // 4. poll up to 26s for the keeper's respawn: a NEW live pid in the pidfile
81 var newpid: i64 = 0 - 1; var waited: i64 = 0
82 while waited < 26000 {
83 sys_sleep_ms(2000); waited = waited + 2000
84 let p: i64 = g_read_pid(pidfile)
85 if p > 0 { if p != pid0 { if g_alive(p) == 1 { newpid = p; waited = 26000 } } }
86 }
87 var t4: i64 = 0; if newpid > 0 { t4 = 1 }
88 tot = tot + 1; if t4 == 1 { pass = pass + 1 } g_report("keeper RESPAWNED the daemon (new live pid != pid0)" as *u8, t4)
89 g_puts(" newpid=" as *u8); g_putn(newpid); g_puts("\n" as *u8)
90
91 // 5. cleanup: stop the gate's keeper child (the respawned daemon stays up -- it is the live one)
92 if kpid > 0 { nx_kill(kpid, 9) }
93
94 g_puts("TORRENT-KEEP-GATE authored=organ pass=" as *u8); g_putn(pass); g_puts("/" as *u8); g_putn(tot)
95 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check
96 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled
97 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify.
98 let ctr__dry: *i64 = gv_ctr()
99 ctr__dry[0] = pass
100 ctr__dry[1] = tot
101 let rc__dry: i64 = gv_verdict("TORRENT-KEEP-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8)
102 sys_exit(rc__dry)
103 return rc__dry
104}