code wiki / (root) / nx_http_probe_lib.nx

nx_http_probe_lib.nx source

↩ module page · 414 lines · 20417 B

1// nx_http_probe_lib.nx -- THE BOUNDED LOOPBACK HTTP LIVENESS PROBE + DEAD-MAN HEARTBEAT STAMPER. 2// 3// WHY THIS EXISTS (2026-08-20). The estate's only compensating control over a FAIL-OPEN authentication 4// path was a curl one-liner in cron.reg: 5// curl -s --max-time 8 http://127.0.0.1:18096/api/cap/status | grep -q '"secure":true' \ 6// && date +ts=%s > knowledge/status/capplane.log 7// The cron row FIRED and the command FAILED, and because the row DISCARDS ALL OUTPUT its own failure was 8// unobservable: 7 of 8 conjuncts were proven true and the 8th ("does curl run") was structurally 9// unmeasurable. The cap-plane watch went stale at 10.9x its budget and nobody could say which link broke. 10// **** A PROBE THAT FAILS SILENTLY IS NOT A CONTROL, IT IS A DECORATION. **** 11// So the single disqualifying property for the replacement is a silent failure mode. EVERY exit path here 12// carries a NAMED reason, printed on stdout AND stderr, and the verdict line is LAST so a positional 13// reader (gv_last_line, nx_gate_green) can anchor on it without matching text anywhere else. 14// 15// WHY A LIB AND NOT ONE FILE: the CLI (nx_http_probe.nx) and the gate (nx_http_probe_gate.nx) both need 16// the same logic, and a gate that re-implements its subject proves nothing. One ruler, two callers. 17// 18// LOOPBACK-ONLY BY CONSTRUCTION -- THIS IS A SAFETY PROPERTY, NOT A LIMITATION. The destination address 19// is hard-pinned to 127.0.0.1 and cannot be passed in. cron.reg's own cap-plane comment records why: 20// during the 2026-08-06 outage the PUBLIC url returned insecure_placeholder/secure=false while loopback 21// :18096 returned provisioned/secure=true, because the public :443 answer came from a DIFFERENT BINARY 22// (DSM nginx co-squat, D008) -- and that sent the diagnosis down a false path for an hour. 23// **** A HEALTH ENDPOINT ANSWERS FOR THE PROCESS THAT SERVED IT, NEVER FOR THE SERVICE. **** 24// A sibling seat had to re-derive that this session after taking the convenient answer. Pinning the 25// address makes the mistake unrepresentable rather than merely documented. 26// 27// COMPOSED, NOT RE-IMPLEMENTED. Three proven primitives do the work: 28// nx_connect_bounded (nx_connect.nx) -- SO_RCVTIMEO does NOT bound connect(); this does 29// nx_http_client_build_request(nx_http_client.nx) -- the ONE request-format definition in the estate 30// sys_set_socket_timeout (nx_syscalls.nx) -- SO_RCVTIMEO/SO_SNDTIMEO 31// The only new code is a DEADLINE-AWARE drain. nx_http_client_get's own _drain has no read bound, so a 32// daemon that accepts and then wedges -- WHICH IS EXACTLY THE 2026-08-06 INCIDENT -- parks the probe 33// forever. On a */5 beat that piles up processes, which is a resource bug even when the feature works. 34// nx_http_client_get is otherwise the right organ and is deliberately left byte-untouched: editing a lib 35// with ~100 consumers to add one caller's timeout is a fleet-wide blast radius for a local need. 36// 37// THE BUDGET IS AN ARGUMENT, NOT A CONSTANT. Rule 11: no magic numbers. The caller states the budget and 38// states its derivation at the call site (cron.reg). Worst-case wall time is ~2x budget: the connect leg 39// and the receive leg each get the full budget, deliberately, so that "slow to connect" and "slow to 40// answer" are separately diagnosable instead of sharing one squeezed allowance. Pick budget <= beat/2. 41// 42// NO SILENT CAPS. HP_BODY_CAP bounds a NETWORK body, whose size is genuinely unknowable in advance -- 43// the one case the no-buffer-caps law permits a bound. It is named for that single purpose and its 44// truncation ANNOUNCES: a pattern that is absent from a TRUNCATED body is UNPROVEN, not absent, and it 45// gets its own exit code rather than being reported as a clean miss. 46// 47// expect_exit: 0 license_tier: ORIGINAL No hw writes (Rule 26). 48// Imports are named EXPLICITLY rather than relied on transitively. nx_http_client.nx does pull in 49// nx_syscalls.nx and nx_connect.nx today, but a consumer that depends on somebody else's import list is 50// one refactor away from a build break it did not cause. 51import "nx_syscalls.nx" 52import "nx_connect.nx" 53import "nx_http_client.nx" 54import "nx_gate.nx" 55 56// ---- NAMED EXIT CODES. Every one of these is a DIAGNOSIS, never a bare failure. ------------------- 57// 0/1/2/3 are the values the pre-2026-08-20 organ already returned; keeping them is Rule 19 (additive). 58const HP_EXIT_OK: i64 = 0 59const HP_EXIT_USAGE: i64 = 1 60const HP_EXIT_CONNECT_FAILED: i64 = 2 61const HP_EXIT_PATTERN_ABSENT: i64 = 3 62const HP_EXIT_STATUS_NOT_2XX: i64 = 4 63const HP_EXIT_STAMP_WRITE_FAILED:i64 = 5 64const HP_EXIT_RECV_TIMEOUT: i64 = 6 65const HP_EXIT_SEND_FAILED: i64 = 7 66const HP_EXIT_BODY_TRUNCATED: i64 = 8 67const HP_EXIT_PATH_TOO_LONG: i64 = 9 68const HP_EXIT_SOCKET_FAILED: i64 = 10 69const HP_EXIT_EMPTY_RESPONSE: i64 = 11 70const HP_EXIT_NOT_HTTP: i64 = 12 71 72// The legacy positional form defaulted to this port (redirect.elf). Preserved so an existing caller of 73// the two-and-three-argument form keeps its meaning; the `watch` verb takes the port explicitly. 74const HP_LEGACY_DEFAULT_PORT: i64 = 8080 75 76// A NETWORK BODY IS THE ONE SIZE YOU CANNOT KNOW IN ADVANCE. Bounded, named for this one purpose, and 77// truncation is ANNOUNCED and given its own exit code -- never a silent short read. 78const HP_BODY_CAP: i64 = 4194304 79 80// Matches the request buffer nx_http_client_get itself allocates for the SAME builder. This is adopting 81// the incumbent's declared bound, not making a second independent guess about the same thing. 82const HP_REQ_CAP: i64 = 4096 83// Half of HP_REQ_CAP, so the builder's fixed header block (request line + Host + UA + derived Accept 84// caps + Connection: close, a few hundred bytes) can never push a legal path past the buffer. A path 85// longer than this is REFUSED BY NAME rather than silently overflowing. 86const HP_PATH_MAX: i64 = 2048 87 88const HP_ADDR_BYTES: i64 = 16 89const HP_STAMP_CAP: i64 = 64 90const HP_MODE_0644: i64 = 420 91const HP_FD_STDOUT: i64 = 1 92const HP_FD_STDERR: i64 = 2 93const HP_ASCII_ZERO: i64 = 48 94const HP_ASCII_NINE: i64 = 57 95const HP_ASCII_SPACE: i64 = 32 96const HP_ASCII_NL: i64 = 10 97const HP_B10: i64 = 10 98const HP_STATUS_DIGITS: i64 = 3 99const HP_STATUS_2XX_LO: i64 = 200 100const HP_STATUS_2XX_HI: i64 = 300 101const HP_MS_PER_SEC: i64 = 1000 102const HP_FLAG_SLOTS: i64 = 32 103// The destination, pinned. See the LOOPBACK-ONLY paragraph in the header. 104const HP_LOOPBACK_A: i64 = 127 105const HP_LOOPBACK_B: i64 = 0 106const HP_LOOPBACK_C: i64 = 0 107const HP_LOOPBACK_D: i64 = 1 108 109// drain outcome slots, named so a reader never has to count array indices 110const HP_FL_CLEAN_EOF: i64 = 0 111const HP_FL_CAP_HIT: i64 = 1 112const HP_FL_DEADLINE: i64 = 2 113const HP_FL_READS: i64 = 3 114 115func hp_strlen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 116 117func hp_werr(s: *u8) -> i64 { sys_write(HP_FD_STDERR, s, hp_strlen(s)); return 0 } 118 119// exact NUL-terminated equality. Used to recognise the `watch` verb: a SUBSTRING test would accept any 120// argument containing the word, which is the loose-matching class this estate keeps paying for. 121func hp_streq(a: *u8, b: *u8) -> i64 { 122 var i: i64 = 0 123 while a[i] != (0 as u8) { 124 if b[i] != a[i] { return 0 } 125 i = i + 1 126 } 127 if b[i] != (0 as u8) { return 0 } 128 return 1 129} 130 131func hp_nerr(v: i64) -> i64 { 132 let t: *u8 = sys_mmap(HP_STAMP_CAP) 133 let o: *u8 = sys_mmap(HP_STAMP_CAP) 134 var m: i64 = v 135 var w: i64 = 0 136 if m < 0 { o[0] = 45 as u8; w = 1; m = 0 - m } 137 var k: i64 = 0 138 if m == 0 { t[0] = HP_ASCII_ZERO as u8; k = 1 } 139 while m > 0 { t[k] = (HP_ASCII_ZERO + (m % HP_B10)) as u8; m = m / HP_B10; k = k + 1 } 140 var q: i64 = k - 1 141 while q >= 0 { o[w] = t[q]; w = w + 1; q = q - 1 } 142 sys_write(HP_FD_STDERR, o, w) 143 sys_munmap(t, HP_STAMP_CAP) 144 sys_munmap(o, HP_STAMP_CAP) 145 return 0 146} 147 148// parse up to `len` decimal digits at buf[off..]; stops at the first non-digit 149func hp_atoi(buf: *u8, off: i64, len: i64) -> i64 { 150 var v: i64 = 0 151 var i: i64 = 0 152 while i < len { 153 let d: i64 = buf[off + i] as i64 154 if d >= HP_ASCII_ZERO { 155 if d <= HP_ASCII_NINE { v = v * HP_B10 + (d - HP_ASCII_ZERO) } else { i = len } 156 } else { i = len } 157 i = i + 1 158 } 159 return v 160} 161 162// 1 if needle occurs in buf[0..n). An empty needle trivially occurs (callers gate on length first). 163func hp_contains(buf: *u8, n: i64, needle: *u8) -> i64 { 164 let nl: i64 = hp_strlen(needle) 165 if nl == 0 { return 1 } 166 var i: i64 = 0 167 while i + nl <= n { 168 var m: i64 = 1 169 var j: i64 = 0 170 while j < nl { if buf[i+j] != needle[j] { m = 0; j = nl } else { j = j + 1 } } 171 if m == 1 { return 1 } 172 i = i + 1 173 } 174 return 0 175} 176 177// THE STATUS CODE, DERIVED -- never a hand-counted offset beside a string literal. The old organ read 178// buf[9..12] with 9 hand-counted from "HTTP/1.1 ": change the prefix, forget the number, and the parser 179// silently reads the wrong window while still compiling. This binds the prefix once and scans for the 180// separator, so it also accepts HTTP/1.0. Returns the code, or -1 if this is not an HTTP status line. 181func hp_status_of(buf: *u8, n: i64) -> i64 { 182 let pfx: *u8 = "HTTP/" as *u8 183 let pl: i64 = hp_strlen(pfx) 184 if n < pl { return 0 - 1 } 185 var i: i64 = 0 186 while i < pl { if buf[i] != pfx[i] { return 0 - 1 } i = i + 1 } 187 var s: i64 = pl 188 var at: i64 = 0 - 1 189 while s < n { 190 if buf[s] == (HP_ASCII_SPACE as u8) { at = s + 1; s = n } else { s = s + 1 } 191 } 192 if at < 0 { return 0 - 1 } 193 if at + HP_STATUS_DIGITS > n { return 0 - 1 } 194 var v: i64 = 0 195 var k: i64 = 0 196 while k < HP_STATUS_DIGITS { 197 let d: i64 = buf[at + k] as i64 198 if d < HP_ASCII_ZERO { return 0 - 1 } 199 if d > HP_ASCII_NINE { return 0 - 1 } 200 v = v * HP_B10 + (d - HP_ASCII_ZERO) 201 k = k + 1 202 } 203 return v 204} 205 206// THE DEADLINE-AWARE DRAIN -- the only genuinely new code in this organ, and the reason it exists. 207// nx_http_client's _drain reads until EOF with no bound: a daemon that ACCEPTS and then wedges parks the 208// caller forever, which is precisely the 2026-08-06 failure this probe is the detector for. 209// Distinguishes the three ways a read loop can end, because they have different remedies: 210// CLEAN_EOF peer closed -> the response is COMPLETE (the builder sends Connection: close) 211// CAP_HIT buffer full -> the response is TRUNCATED; an absent pattern is UNPROVEN, not absent 212// DEADLINE budget spent -> the response is INCOMPLETE; nothing about the body can be concluded 213func hp_drain(fd: i64, buf: *u8, cap: i64, deadline_sec: i64, fl: *i64) -> i64 { 214 fl[HP_FL_CLEAN_EOF] = 0 215 fl[HP_FL_CAP_HIT] = 0 216 fl[HP_FL_DEADLINE] = 0 217 fl[HP_FL_READS] = 0 218 var off: i64 = 0 219 var keep: i64 = 1 220 while keep == 1 { 221 if off >= cap { fl[HP_FL_CAP_HIT] = 1; keep = 0 } 222 else { 223 if sys_now_realtime_sec() >= deadline_sec { fl[HP_FL_DEADLINE] = 1; keep = 0 } 224 else { 225 let r: i64 = sys_read(fd, ((buf as i64) + off) as *u8, cap - off) 226 fl[HP_FL_READS] = fl[HP_FL_READS] + 1 227 if r == 0 { fl[HP_FL_CLEAN_EOF] = 1; keep = 0 } 228 else { 229 // r < 0 is SO_RCVTIMEO expiry (EAGAIN) or a socket error. Either way this read loop 230 // did not observe a complete response, so it is reported as budget-exhausted rather 231 // than quietly returning a partial body that reads like a whole one. 232 if r < 0 { fl[HP_FL_DEADLINE] = 1; keep = 0 } else { off = off + r } 233 } 234 } 235 } 236 } 237 return off 238} 239 240// THE HEARTBEAT. Format is `ts=<epoch>\n`, byte-compatible with the `date +ts=%s` the cron one-liner 241// wrote, because nx_cron_watch's cw_num_after parses the marker `ts=` and the cap-plane watch row 242// declares that marker. CHANGING THE MARKER WOULD LEAVE THE WATCH PERMANENTLY STALE WHILE THE STAMPER 243// LOOKED HEALTHY, which is the failure one row above it in the plane (surfsentinel) already warns about. 244// 245// PLAIN TRUNCATE-AND-WRITE, DELIBERATELY, NOT A TEMP+RENAME. The failure mode of a torn read here is a 246// SHORTER number, i.e. a SMALLER epoch, i.e. a LARGER age -- so a reader that catches a partial write 247// reads STALE. That fails in the safe direction for a dead-man's switch, and it leaves no `.nxnew` 248// litter behind a crash. Returns bytes written, or -1 -- and -1 is a NAMED exit for the caller, never 249// a silent no-op: a stamper that cannot write is exactly as blind as one that never ran. 250func hp_write_stamp(path: *u8, ts: i64) -> i64 { 251 let fd: i64 = sys_openat_wr(path, HP_MODE_0644) 252 if fd < 0 { return 0 - 1 } 253 let b: *u8 = sys_mmap(HP_STAMP_CAP) 254 let t: *u8 = sys_mmap(HP_STAMP_CAP) 255 let pfx: *u8 = "ts=" as *u8 256 var o: i64 = 0 257 var i: i64 = 0 258 while pfx[i] != (0 as u8) { b[o] = pfx[i]; o = o + 1; i = i + 1 } 259 var m: i64 = ts 260 var k: i64 = 0 261 if m == 0 { t[0] = HP_ASCII_ZERO as u8; k = 1 } 262 while m > 0 { t[k] = (HP_ASCII_ZERO + (m % HP_B10)) as u8; m = m / HP_B10; k = k + 1 } 263 var q: i64 = k - 1 264 while q >= 0 { b[o] = t[q]; o = o + 1; q = q - 1 } 265 b[o] = HP_ASCII_NL as u8 266 o = o + 1 267 let w: i64 = sys_write(fd, b, o) 268 sys_fsync(fd) 269 sys_close(fd) 270 sys_munmap(b, HP_STAMP_CAP) 271 sys_munmap(t, HP_STAMP_CAP) 272 if w != o { return 0 - 1 } 273 return o 274} 275 276// THE REASON TRAVELS WITH THE EXIT CODE. A caller reading only the number still gets a diagnosis, 277// and the log line names the conjunct that failed instead of leaving the reader to guess the alarming 278// third. `connect-failed` deliberately covers refused / unreachable / connect-budget-exceeded together: 279// nx_connect_bounded returns -1 for all three by design and the estate has no variant that reports 280// which, so splitting them here would be a distinction this organ cannot actually make. NAMED, not 281// pretended. 282func hp_reason(code: i64) -> *u8 { 283 if code == HP_EXIT_OK { return "none" as *u8 } 284 if code == HP_EXIT_USAGE { return "usage" as *u8 } 285 if code == HP_EXIT_CONNECT_FAILED { return "connect-failed" as *u8 } 286 if code == HP_EXIT_PATTERN_ABSENT { return "pattern-absent" as *u8 } 287 if code == HP_EXIT_STATUS_NOT_2XX { return "status-not-2xx" as *u8 } 288 if code == HP_EXIT_STAMP_WRITE_FAILED { return "stamp-write-failed" as *u8 } 289 if code == HP_EXIT_RECV_TIMEOUT { return "recv-timeout" as *u8 } 290 if code == HP_EXIT_SEND_FAILED { return "send-failed" as *u8 } 291 if code == HP_EXIT_BODY_TRUNCATED { return "body-truncated-pattern-unproven" as *u8 } 292 if code == HP_EXIT_PATH_TOO_LONG { return "path-too-long" as *u8 } 293 if code == HP_EXIT_SOCKET_FAILED { return "socket-failed" as *u8 } 294 if code == HP_EXIT_EMPTY_RESPONSE { return "empty-response" as *u8 } 295 if code == HP_EXIT_NOT_HTTP { return "not-http" as *u8 } 296 return "unknown" as *u8 297} 298 299// THE VERDICT LINE IS LAST AND IT IS ALSO ON STDERR WHEN IT FAILS. The cron row this replaces threw its 300// output away; a replacement whose failure is only visible to someone who kept stdout would inherit the 301// exact defect. stderr survives `>/dev/null` and lands in the mail/log of every cron implementation. 302// THERE IS DELIBERATELY NO QUIET FLAG. The defect being retired is a probe with no observable outcome; 303// a switch that restores that silence is the defect with a config key. Every run says what happened. 304func hp_emit(code: i64) -> i64 { 305 gw("NX-HTTP-PROBE verdict=" as *u8) 306 if code == HP_EXIT_OK { gw("OK" as *u8) } else { gw("FAIL" as *u8) } 307 gw(" reason=" as *u8); gw(hp_reason(code)) 308 gw(" exit=" as *u8); gn(code); gw("\n" as *u8) 309 if code != HP_EXIT_OK { 310 hp_werr("NX-HTTP-PROBE verdict=FAIL reason=" as *u8) 311 hp_werr(hp_reason(code)) 312 hp_werr(" exit=" as *u8); hp_nerr(code); hp_werr("\n" as *u8) 313 } 314 return code 315} 316 317// ---- THE ORGAN. One function, one job, callable from the CLI and from the gate. -------------------- 318// expect: empty string = no pattern assertion. stamp_path: empty string = no heartbeat. 319// verbose: 1 prints the per-leg progress lines before the verdict; 0 prints the verdict only. 320// ORDER IS THE CONTRACT: transport, then status, then pattern, THEN stamp. The heartbeat is the LAST 321// thing that happens and only on a fully clean run, so every named failure leaves the watch to go 322// STALE rather than quietly reading healthy. That ordering is the whole safety property and the gate 323// asserts the ABSENCE of the stamp on every failing path, not merely the exit code. 324func hp_run(port: i64, path: *u8, expect: *u8, budget_sec: i64, stamp_path: *u8, verbose: i64) -> i64 { 325 let plen: i64 = hp_strlen(path) 326 if plen > HP_PATH_MAX { return hp_emit(HP_EXIT_PATH_TOO_LONG) } 327 if budget_sec <= 0 { return hp_emit(HP_EXIT_USAGE) } 328 329 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0) 330 if fd < 0 { return hp_emit(HP_EXIT_SOCKET_FAILED) } 331 332 let addr: *u8 = sys_mmap(HP_ADDR_BYTES) 333 nx_http_client_sockaddr_ipv4(addr, HP_LOOPBACK_A, HP_LOOPBACK_B, HP_LOOPBACK_C, HP_LOOPBACK_D, port) 334 if nx_connect_bounded(fd, addr, HP_ADDR_BYTES, budget_sec * HP_MS_PER_SEC) != 0 { 335 sys_close(fd) 336 sys_munmap(addr, HP_ADDR_BYTES) 337 return hp_emit(HP_EXIT_CONNECT_FAILED) 338 } 339 sys_set_socket_timeout(fd, budget_sec) 340 341 let host: *u8 = "localhost" as *u8 342 let req: *u8 = sys_mmap(HP_REQ_CAP) 343 let rl: i64 = nx_http_client_build_request(path, plen, host, hp_strlen(host), req) 344 let wr: i64 = sys_write(fd, req, rl) 345 sys_munmap(req, HP_REQ_CAP) 346 if wr != rl { 347 sys_close(fd) 348 sys_munmap(addr, HP_ADDR_BYTES) 349 return hp_emit(HP_EXIT_SEND_FAILED) 350 } 351 352 let deadline: i64 = sys_now_realtime_sec() + budget_sec 353 let buf: *u8 = sys_mmap(HP_BODY_CAP) 354 let fl: *i64 = sys_mmap(HP_FLAG_SLOTS) as *i64 355 let n: i64 = hp_drain(fd, buf, HP_BODY_CAP, deadline, fl) 356 sys_close(fd) 357 sys_munmap(addr, HP_ADDR_BYTES) 358 359 var code: i64 = HP_EXIT_OK 360 var status: i64 = 0 - 1 361 362 if fl[HP_FL_DEADLINE] == 1 { code = HP_EXIT_RECV_TIMEOUT } 363 if code == HP_EXIT_OK { if n == 0 { code = HP_EXIT_EMPTY_RESPONSE } } 364 if code == HP_EXIT_OK { 365 status = hp_status_of(buf, n) 366 if status < 0 { code = HP_EXIT_NOT_HTTP } 367 } 368 if code == HP_EXIT_OK { 369 if status < HP_STATUS_2XX_LO { code = HP_EXIT_STATUS_NOT_2XX } 370 if status >= HP_STATUS_2XX_HI { code = HP_EXIT_STATUS_NOT_2XX } 371 } 372 373 if verbose == 1 { 374 gw("GET " as *u8); gw(path); gw(" -> status=" as *u8); gn(status) 375 gw(" bytes=" as *u8); gn(n) 376 gw(" reads=" as *u8); gn(fl[HP_FL_READS]) 377 gw(" clean_eof=" as *u8); gn(fl[HP_FL_CLEAN_EOF]) 378 gw(" truncated=" as *u8); gn(fl[HP_FL_CAP_HIT]) 379 gw(" deadline_hit=" as *u8); gn(fl[HP_FL_DEADLINE]) 380 gw("\n" as *u8) 381 } 382 383 if code == HP_EXIT_OK { 384 if hp_strlen(expect) > 0 { 385 let found: i64 = hp_contains(buf, n, expect) 386 if verbose == 1 { 387 gw(" expect '" as *u8); gw(expect); gw("': " as *u8) 388 if found == 1 { gw("FOUND\n" as *u8) } else { gw("MISSING\n" as *u8) } 389 } 390 if found == 0 { 391 // ABSENCE NEEDS EXHAUSTIVE COVERAGE. A body that hit the cap was not fully read, so 392 // "the pattern is not there" is not a conclusion this run is entitled to publish. 393 if fl[HP_FL_CAP_HIT] == 1 { code = HP_EXIT_BODY_TRUNCATED } else { code = HP_EXIT_PATTERN_ABSENT } 394 } 395 } 396 } 397 sys_munmap(buf, HP_BODY_CAP) 398 399 if code == HP_EXIT_OK { 400 if hp_strlen(stamp_path) > 0 { 401 let now: i64 = sys_now_realtime_sec() 402 let wrote: i64 = hp_write_stamp(stamp_path, now) 403 if wrote < 0 { code = HP_EXIT_STAMP_WRITE_FAILED } 404 else { 405 if verbose == 1 { 406 gw(" stamp " as *u8); gw(stamp_path); gw(": WROTE ts=" as *u8); gn(now) 407 gw(" bytes=" as *u8); gn(wrote); gw("\n" as *u8) 408 } 409 } 410 } 411 } 412 sys_munmap(fl as *u8, HP_FLAG_SLOTS) 413 return hp_emit(code) 414}