DeferTask/DeferImmediateTask return bool (nothrow OOM); wait_que and
sql_que free the BQUE and refund quota/waitcost when enqueue fails.
db_write_object reports stream status via ferror; db_write returns -1
on I/O failure after header/object/end/flush checks, and dump/dbconvert
callers refuse to publish a truncated flatfile.
Residual Phase 3 player/staff prose that still used T() on live paths:
connect/reconnect/disconnect room and monitor lines, [Suspect] flags,
GAME: restart/shutdown/signal/log-full, dual-path channel boot, and
@email hostname resolution. pot + xx complete + ko msgmerge append;
tools/merge_nls_markings.py for Windows agents without xgettext
(canonical pot remains make -C mux/po pot on Linux).
Replace ~900 typographic \xE2\x80\x.. escapes (curly quotes, en dashes)
and a few \xE2\x80\230 octal workarounds with real UTF-8 in message
strings under mux/modules and mux/src. Leave stringutil and convert
charset mapping tables as explicit byte sequences.
Two residuals found while cross-reviewing the #1112 fix. Both are the
same root cause and were invisible while every capture came back empty.
1. The "clear the register" path did not clear. funceval2.cpp passed
RegAssign(..., 0, nullptr) for an unset group or a register list longer
than the capture count, but RegAssign early-returns on a null ptr
(eval.cpp), so the register kept its PREVIOUS value. Harmless when
nothing ever filled; now that captures work, `setq(2,secret)` followed
by a regmatch whose group 2 does not participate reads `secret` back
out of %q2 and looks like a capture. Pass T("") — non-null, length 0 —
which is exactly what the named-register branch alongside already does.
2. engine.cpp regexp_match() still had `PCRE2_SIZE outlen = 0;`. This is
the %0..%9 path for REGEXP-flagged $-commands and ^-listens (callers in
engine.cpp atr_match1, command.cpp, look.cpp) — not the regmatch()
function the PR fixed. Same in/out capacity semantics, same
PCRE2_ERROR_NOMEMORY, and the rc<0 branch then frees the arg to
nullptr, so every regexp $-command capture was empty. Arguably wider
blast radius than regmatch() itself.
Verified with a new REGEXP $-command case in the wild-capture scenario
(smoke cannot reach $-commands — muxscript does not wire match_mine).
With engine.cpp reverted the new case reports CAP5<><> (both captures
empty) while the four wildcard cases still pass, isolating the defect;
with the fix, CAP5<alice><bob>.
smoke 1324/1324; scenario 5/5 wild-capture + 6/6 site-threshold.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restart (#1039–#1043): zero process-local DESC fields after load,
restore peer via getpeername, strip non-survivable TLS/WS flags,
drop TLS and WebSocket before exec, tear down adopt failures with
shutdownsock, abort partial restart loads cleanly.
DB (#1044–#1047): cache_flush_writes returns success only after
Commit; leave dirty queue on failure; atr_head union SQLite with
pending cache/queue; set mudstate.bStandAlone in DbConvert; flush
attr queue before import Commit.
Queue (#1048–#1052): setup_que refunds on OOM; sql_que frees BQUE
on Query fail and inits waittime; do_wait rolls back semaphore if
wait_que fails; include nest depth limit 50; CPU halt_que uses
Owner+object for machines.
JIT (#1053–#1057): full 64-bit rv_load_i64; setq/r single-char
RegisterSet; decline long CARGS; accumulate inline u() watermarks;
bounded guest_strnlen on ECALL paths.
Conf/convert (#1058–#1061): @enable/@disable sync g_dc; live
driver knobs for output/retry/max_players/timeouts/quota; safe
heap encode for t5x/t6h/r7h attributes.
The alarm clock (the runaway-softcode/PCRE CPU limiter) was migrated from
a SIGPROF interval timer to a std::thread + condition_variable, but the
post-fork children still called alarm_clock.clear(), which locks
mux_alarm::mutex_. That mutex is held by the alarm thread on every command
via set()/clear().
fork() duplicates only the calling thread, so the alarm thread does not
exist in the child and the alarm can never fire there. If fork() lands in
the alarm thread's brief mutex window, the child inherits mutex_ locked
with no owner and self-deadlocks in clear() before exec. A later stubslave
reboot/shutdown then blocks the parent forever in waitpid(pid, NULL, 0),
parking the entire network loop: idle, no CPU, not accepting connections,
reconnect gets nothing, and only a full restart clears it. The rare race
plus the need for a subsequent slave reboot explains why it took days.
Fixes:
- Child side (root cause): replace alarm_clock.clear() with the lock-free,
async-signal-safe alarm_clock.alarmed.store(false) in the three fork
children (stubslave boot, database dump, helper process).
- Defense in depth: add reap_child_bounded() (WNOHANG poll, then SIGKILL)
in place of the two blocking waitpid() calls in the stubslave paths, so
no wedged child can ever park the main thread again. This also cleans up
the orphaned stubslaves that accumulated across restarts.
The legitimate main-thread alarm_clock.clear() calls in net.cpp and
cque.cpp (which arm/disarm the CPU limiter around command execution) are
unchanged.
Pending field validation on the farm test deployment; do not push yet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use the new LBuf_Adopt() macro to take RAII ownership of caller-owned
pool buffers returned by atr_get, atr_pget, and atr_get_LEN. This
eliminates ~82 explicit free_lbuf calls and automatically covers
early-return paths that previously required careful manual placement
of the free.
Heaviest conversions: player.cpp (13), comsys.cpp (14), command.cpp (9).
Complex interleaved patterns (did_it charge/runout swaps, PureName
reassignment, process_cmdent loops) left manual for now.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fourth wave: ast (11), funceval (10), funceval2 (7), comsys (3),
speech (10), set (6), engine (7), command (10). Covers the NOEVAL
handlers (cand/cor/if/switch/iter), function evaluators (ifelse,
letq, objeval, sortby, munge, while, sandbox), notify_check message
buffers, and command dispatch paths. Buffers stored in fargs[]
arrays, returned through output pointers, or used in ping-pong swap
patterns are left manual.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add LBuf class to alloc.h: an RAII wrapper around alloc_lbuf/free_lbuf
that moves LBUF_SIZE buffers from the stack to the heap pool. Convert
all 108 non-static UTF8 xxx[LBUF_SIZE] stack arrays across 25 source
files. Static BSS buffers (24) are unchanged.
This eliminates LBUF_SIZE from recursive stack frames, making it safe
to increase LBUF_SIZE without risking stack overflow in the evaluation
pipeline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The modular architecture (netmux.exe, libmux.dll, engine.dll, etc.)
with static CRT (/MT) gave each module its own CRT heap. FILE* handles
from mux_fopen (in libmux) crashed when used by stdio functions in
other modules — fclose in write_pidfile, fgets in cf_include, etc.
Switch all 11 vcxproj files to /MD (shared CRT DLL) so all modules
share one CRT instance. Ship msvcp140.dll, vcruntime140.dll, and
vcruntime140_1.dll in the binary distribution.
Also add mux_fclose to libmux as good hygiene (pairs with mux_fopen),
and replace all cross-module fclose calls. This change is safe on Unix
where everything links into one process.
Add Startmux.bat as a replacement for Startmux.wsf since Windows
Script Host is no longer associated by default on modern Windows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
strip_fancy_quotes was applied to msgPlain before both pattern
matching and wildcard capture, so %0/%1/etc. received ASCII
quotes instead of the original typographic ones.
Now: match against a normalized copy (ASCII quotes) to determine
if the pattern hits, then re-capture from the original text so
%0 retains Unicode smart quotes for display. Applied to both
the @listen path in notify_check and the ^-listen path in
atr_match1.
Updated TC001 expected hash to reflect fancy quotes in %0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite notify_check() from mux_string to raw PUA-encoded UTF-8 lbufs.
The engine now passes PUA strings through; the network layer converts
PUA to ANSI/XTERM/HTML per client. html_escape() replaces encode_Html(),
co_strip_color() replaces export_TextPlain() for @listen matching.
Remove mux_string overloads: raw_notify, raw_notify_html,
send_text_to_player (session.cpp, conn_bridge.cpp, net.cpp).
Convert handle_ears, look_for_exits (predicates.cpp), notify_comsys
(comsys.cpp), and small uses in mail.cpp/match.cpp.
Add extern "C" guards to color_ops.h for C++ inclusion.
593/593 smoke tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>