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}