code wiki / _hdl_build / nx_mp_serve.nx
nx_mp_serve.nx source
↩ module page · 132 lines · 7731 B
1// nx_mp_serve.nx -- the RUNNABLE sovereign multiplayer daemon: serves the game (pets3d.html + wasm) AND the
2// player-state relay (/pstate POST, /roster GET) on one port. Socket/bind/listen + accept/FORK loop (mirrors
3// nx_arcade_serve, proven live), with the relay in SHARED mmap so every forked handler sees the same rooms.
4// All request logic = nx_mp_daemon (gate-proven). The browser host only memcpy's + calls gi_net_* -- no JS logic.
5// Build: ./_offc/nx_sov_build_run.elf nx_mp_serve (it backgrounds -- the server loops). license_tier: ORIGINAL
6import "nx_syscalls.nx"
7import "nx_mp_daemon.nx" // brings nx_mp_relay + nx_room_relay (rr_*) transitively
8const MPS_MAGIC_7702: i64 = 7702
9const MPS_MAGIC_8192: i64 = 8192
10const MPS_MAGIC_1024: i64 = 1024
11const MPS_MAGIC_8000: i64 = 8000
12
13// HOST-PROTECTION CAP (debt 322 fix, 2026-07-20). This daemon forks one handler per connection.
14// WITHOUT a bound on concurrent LIVE children, a connection burst (e.g. headless probe pairs) forks
15// until the box runs out of processes/memory -- exactly the cascade that took the whole NAS down
16// (sovereign services died -> sshd could not fork -> DSM died -> reboot). At the cap the accept loop
17// BLOCK-REAPS a slot before forking (backpressure), so a burst DEGRADES -- queues in the listen
18// backlog, then the kernel refuses -- instead of killing the host (rule 14 graceful degradation,
19// rule 26 never take the box down). 32 = conservative for a secondary game relay: the primary sites
20// daemon caps at 64, and these handlers are plain-HTTP + short-lived, so 32 is ample for family co-op.
21const MPS_MAX_CHILDREN: i64 = 32
22
23func mps_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
24func mps_wstr(buf: *u8, off: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { buf[off + i] = s[i]; i = i + 1 } return off + i }
25func mps_wn(buf: *u8, off: i64, v: i64) -> i64 {
26 var m: i64 = v; let tmp: *u8 = sys_mmap(32); var k: i64 = 0
27 if m == 0 { tmp[0] = 48 as u8; k = 1 }
28 while m > 0 { tmp[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
29 var i: i64 = 0; while i < k { buf[off + i] = tmp[k - 1 - i]; i = i + 1 }
30 return off + k
31}
32func mps_eq(a: *u8, b: *u8) -> i64 {
33 var i: i64 = 0
34 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
35 if b[i] != (0 as u8) { return 0 }
36 return 1
37}
38// serve a disk file with a content-type; 0 ok / -1 missing
39func mps_serve(cfd: i64, fpath: *u8, ctype: *u8) -> i64 {
40 let lenp: *i64 = sys_mmap(16) as *i64
41 let body: *u8 = sys_read_file(fpath, lenp)
42 if body == (0 as i64) as *u8 { return 0 - 1 }
43 let blen: i64 = lenp[0]
44 let hdr: *u8 = sys_mmap(512)
45 var o: i64 = 0
46 o = mps_wstr(hdr, o, "HTTP/1.1 200 OK\r\nContent-Type: " as *u8)
47 o = mps_wstr(hdr, o, ctype)
48 o = mps_wstr(hdr, o, "\r\nContent-Length: " as *u8)
49 o = mps_wn(hdr, o, blen)
50 o = mps_wstr(hdr, o, "\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\n\r\n" as *u8)
51 sys_write(cfd, hdr, o)
52 sys_write(cfd, body, blen)
53 return 0
54}
55
56func main() -> i64 {
57 let port: i64 = MPS_MAGIC_7702
58 let N: i64 = 64 // relay slots (shared across rooms; plenty for co-op sessions)
59 let SB: i64 = 64
60 // SHARED relay state so every forked connection-handler sees the same rooms
61 let st: *i64 = sys_mmap_shared((2 + 5*N) * 8) as *i64
62 let arena: *u8 = sys_mmap_shared(N * SB)
63 rr_init(st, N, SB)
64
65 let addr: *u8 = sys_mmap(16)
66 addr[0] = 2 as u8; addr[1] = 0 as u8
67 addr[2] = ((port >> 8) & 0xff) as u8; addr[3] = (port & 0xff) as u8
68 addr[4] = 0 as u8; addr[5] = 0 as u8; addr[6] = 0 as u8; addr[7] = 0 as u8 // 0.0.0.0 (behind nginx/LAN)
69 var z: i64 = 8
70 while z < 16 { addr[z] = 0 as u8; z = z + 1 }
71 let lfd: i64 = sys_socket(2, 1, 0)
72 if lfd < 0 { sys_write(1, "socket failed\n" as *u8, 14); sys_exit(1); return 1 }
73 let optval: *u8 = sys_mmap(4)
74 optval[0] = 1 as u8; optval[1] = 0 as u8; optval[2] = 0 as u8; optval[3] = 0 as u8
75 sys_setsockopt(lfd, 1, 2, optval, 4)
76 if sys_bind(lfd, addr, 16) < 0 { sys_write(1, "bind failed\n" as *u8, 12); sys_exit(1); return 1 }
77 if sys_listen(lfd, 64) < 0 { sys_write(1, "listen failed\n" as *u8, 14); sys_exit(1); return 1 }
78 sys_write(1, "NISHI MP DAEMON (sovereign) on 0.0.0.0:7702 -- /pets3d game + /pstate + /roster relay\n" as *u8, 85)
79
80 let req: *u8 = sys_mmap(MPS_MAGIC_8192)
81 let path: *u8 = sys_mmap(MPS_MAGIC_1024)
82 let out: *u8 = sys_mmap(MPS_MAGIC_8192)
83 let nf: *u8 = "HTTP/1.1 404 Not Found\r\nContent-Length: 3\r\nConnection: close\r\n\r\n404" as *u8
84 let rst: *i64 = sys_mmap(16) as *i64 // wait4 status buffer for reaping exited connection handlers
85 sys_set_socket_timeout(lfd, 5) // idle accept-timeout so the reap loop runs even with no traffic
86 var live: i64 = 0 // concurrent live children -- host-protection cap (debt 322)
87 var go: i64 = 1
88 while go == 1 {
89 // Reap exited handlers (WNOHANG) every iteration -- including idle accept-timeouts -- so
90 // zombies never accumulate and `live` tracks true concurrency.
91 while sys_wait4(0 - 1, rst, 1) > 0 { live = live - 1 }
92 let cfd: i64 = sys_accept(lfd)
93 if cfd >= 0 {
94 // BACKPRESSURE (debt 322 fix): at the cap, BLOCK-REAP one slot before forking so a
95 // connection burst can never fork the host to death -- it degrades (listen backlog,
96 // then kernel refuse) instead. rule 14 (graceful degradation) + rule 26 (never take box down).
97 if live >= MPS_MAX_CHILDREN {
98 if sys_wait4(0 - 1, rst, 0) > 0 { live = live - 1 }
99 }
100 let pid: i64 = sys_fork()
101 if pid == 0 {
102 sys_close(lfd)
103 sys_set_socket_timeout(cfd, 10) // anti-pin: a stuck client must not hold a slot forever
104 let rn: i64 = sys_read(cfd, req, MPS_MAGIC_8192)
105 mpd_path(req, rn, path)
106 let route: i64 = mpd_route(path)
107 let now: i64 = sys_now_ms()
108 if route == 1 {
109 let rl: i64 = mpd_pstate(req, rn, path, st, arena, SB, now, out)
110 sys_write(cfd, out, rl)
111 } else { if route == 2 {
112 let rl: i64 = mpd_roster(path, st, arena, SB, now, MPS_MAGIC_8000, out)
113 sys_write(cfd, out, rl)
114 } else {
115 var served: i64 = 0
116 if mps_eq(path, "/" as *u8) == 1 { served = 1; mps_serve(cfd, "web_assets/_game_build/pets3d_mp.html" as *u8, "text/html; charset=utf-8" as *u8) }
117 if mps_eq(path, "/pets3d" as *u8) == 1 { served = 1; mps_serve(cfd, "web_assets/_game_build/pets3d_mp.html" as *u8, "text/html; charset=utf-8" as *u8) }
118 if mps_eq(path, "/pets3d.wasm" as *u8) == 1 { served = 1; mps_serve(cfd, "web_assets/_game_build/pets3d.wasm" as *u8, "application/wasm" as *u8) }
119 if mps_eq(path, "/mineworld" as *u8) == 1 { served = 1; mps_serve(cfd, "web_assets/_game_build/mineworld_mp.html" as *u8, "text/html; charset=utf-8" as *u8) }
120 if mps_eq(path, "/mineworld-mp" as *u8) == 1 { served = 1; mps_serve(cfd, "web_assets/_game_build/mineworld_mp.html" as *u8, "text/html; charset=utf-8" as *u8) }
121 if served == 0 { sys_write(cfd, nf, mps_slen(nf)) }
122 } }
123 sys_close(cfd)
124 sys_exit(0)
125 }
126 live = live + 1 // parent: count the new handler (reaped at the loop top)
127 sys_close(cfd)
128 }
129 }
130 sys_close(lfd)
131 return 0
132}