fix(win): stop the Fortran-runtime console-close abort and cp1252 UnicodeEncodeError crash classes (#1153, #1155)
Two Windows-only backend crash classes, one boundary (process spawn/stdio): forrtl: error (200) (#1153 and the crash markers in #1155/#1152): MKL's Intel Fortran runtime installs a console CTRL handler that aborts the whole backend (exit 2 / 0xC000013A) when a console CLOSE/LOGOFF event reaches it. The backend was spawned with no console isolation, so OS console events could reach it mid-session. Now: - the desktop shell spawns the backend with CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP (no console → no console events, stdio is piped anyway) and sets FOR_DISABLE_CONSOLE_CTRL_HANDLER=1; - backend/main.py setdefaults the same var before torch/numpy can load MKL, covering scripts/run.sh and bare uvicorn launches too. 'charmap' codec can't encode (#1155): kittentts print()s the user's text on every generate; on Windows the child's stdout is cp1252, so Vietnamese text raised UnicodeEncodeError and surfaced as a bogus '400 Bad Request'. The process-wide SafeFileWrapper only swallowed OSError (its EPIPE job). Now: - stdio is reconfigured to UTF-8 (errors=backslashreplace) at startup; - SafeFileWrapper also swallows UnicodeError — logs are best-effort, synthesis is not; - the shell sets PYTHONUTF8=1 for the child (Windows→parity with macOS/Linux; process env wins for power users); - the crash-log append opens with encoding=utf-8 so tracebacks carrying user text can't re-trip the same codec. Regression tests: tests/test_windows_stdio_guards.py (cp1252 stream write must not raise; main must set the Fortran guard + UTF-8 stdio). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9ff394a48b
commit
a601db8448
+22
-1
@@ -29,6 +29,16 @@ if sys.platform == "win32":
|
||||
os.environ.setdefault("TORCHDYNAMO_DISABLE", "1")
|
||||
os.environ.setdefault("TORCHINDUCTOR_DISABLE", "1")
|
||||
|
||||
# The Intel Fortran runtime bundled with MKL (under numpy/scipy) installs a
|
||||
# console CTRL handler that aborts the whole process with `forrtl: error
|
||||
# (200): program aborting due to window-CLOSE event` when a Windows console
|
||||
# CLOSE/LOGOFF/SHUTDOWN event reaches it — seen in the wild as backend crashes
|
||||
# with exit code 2 / 0xC000013A mid-session (#1153 class). The RTL reads this
|
||||
# at DLL init, so it must be set before torch/numpy import MKL; setdefault so
|
||||
# an explicit user value wins. A no-op everywhere the Fortran RTL isn't
|
||||
# handling console events (macOS/Linux), hence unconditional (and testable).
|
||||
os.environ.setdefault("FOR_DISABLE_CONSOLE_CTRL_HANDLER", "1")
|
||||
|
||||
# The backend's stdout/stderr are pipes owned by the desktop shell that
|
||||
# spawned it. If that shell exits while the backend survives (crash,
|
||||
# relaunch, orphan), the pipes close — and the next write raises
|
||||
@@ -40,6 +50,17 @@ if sys.platform == "win32":
|
||||
# already uses for its own fp.)
|
||||
from utils.hf_progress import SafeFileWrapper as _SafeStdio # noqa: E402
|
||||
|
||||
# Force UTF-8 stdio before wrapping (#1155): on Windows the spawned backend's
|
||||
# stdout defaults to cp1252, and any library that prints user text (kittentts
|
||||
# prints the full synth text on every generate) raised UnicodeEncodeError on
|
||||
# Vietnamese/CJK/…, killing the request with a bogus 400. backslashreplace
|
||||
# keeps even a non-UTF-8-able sink from ever raising.
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="backslashreplace")
|
||||
except Exception: # noqa: BLE001 — pythonw/frozen builds may lack reconfigure
|
||||
pass
|
||||
|
||||
if not getattr(sys.stdout, "_is_safe_wrapper", False):
|
||||
sys.stdout = _SafeStdio(sys.stdout)
|
||||
if not getattr(sys.stderr, "_is_safe_wrapper", False):
|
||||
@@ -787,7 +808,7 @@ async def global_exception_handler(request: Request, exc: Exception):
|
||||
return Response(status_code=499)
|
||||
try:
|
||||
# Serialize writes so concurrent unhandled exceptions don't interleave frames.
|
||||
with _crash_log_lock, open(CRASH_LOG_PATH, "a") as f:
|
||||
with _crash_log_lock, open(CRASH_LOG_PATH, "a", encoding="utf-8", errors="backslashreplace") as f:
|
||||
f.write(f"\n--- {time.strftime('%Y-%m-%dT%H:%M:%S')} ---\n")
|
||||
f.write(f"Request: {request.url}\n")
|
||||
f.write(traceback.format_exc())
|
||||
|
||||
@@ -121,7 +121,11 @@ class SafeFileWrapper:
|
||||
def write(self, s):
|
||||
try:
|
||||
self.fp.write(s)
|
||||
except OSError:
|
||||
except (OSError, UnicodeError):
|
||||
# OSError: EPIPE from a dead parent shell (the wrapper's original
|
||||
# job). UnicodeError (#1155): a library print of user text hitting
|
||||
# a non-UTF-8 stream — cp1252 stdout on Windows — must not abort
|
||||
# the operation that printed. Logs are best-effort; work is not.
|
||||
pass
|
||||
def flush(self):
|
||||
try:
|
||||
|
||||
@@ -348,6 +348,21 @@ pub fn spawn_backend<R: tauri::Runtime>(app: &tauri::AppHandle<R>, progress: Opt
|
||||
env.push(("TORCHDYNAMO_DISABLE".into(), "1".into()));
|
||||
env.push(("HF_HUB_DISABLE_SYMLINKS_WARNING".into(), "1".into()));
|
||||
env.push(("HF_HUB_DISABLE_SYMLINKS".into(), "1".into()));
|
||||
// #1153 class: the Intel Fortran runtime in MKL (numpy/scipy) aborts
|
||||
// the whole backend with `forrtl: error (200)` when a console
|
||||
// CLOSE/LOGOFF event reaches the child. Belt (this env var disables
|
||||
// that handler) and suspenders (CREATE_NO_WINDOW below means no
|
||||
// console gets the event at all). Process env wins for power users.
|
||||
if std::env::var("FOR_DISABLE_CONSOLE_CTRL_HANDLER").is_err() {
|
||||
env.push(("FOR_DISABLE_CONSOLE_CTRL_HANDLER".into(), "1".into()));
|
||||
}
|
||||
// #1155: without UTF-8 mode the child's stdio + default file
|
||||
// encoding is cp1252, and a library print of Vietnamese/CJK user
|
||||
// text raised UnicodeEncodeError mid-synthesis. macOS/Linux are
|
||||
// UTF-8 already — this brings Windows to parity.
|
||||
if std::env::var("PYTHONUTF8").is_err() {
|
||||
env.push(("PYTHONUTF8".into(), "1".into()));
|
||||
}
|
||||
}
|
||||
// HF endpoint precedence: process env (power user) > setup-screen custom
|
||||
// mirror > region preset.
|
||||
@@ -393,6 +408,18 @@ pub fn spawn_backend<R: tauri::Runtime>(app: &tauri::AppHandle<R>, progress: Opt
|
||||
for (k, v) in &env {
|
||||
cmd.env(k, v);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
// CREATE_NO_WINDOW (0x08000000) | CREATE_NEW_PROCESS_GROUP (0x00000200).
|
||||
// The backend used to inherit the app's console context, so OS console
|
||||
// CLOSE/LOGOFF events could reach it and MKL's Fortran runtime aborted
|
||||
// the process (`forrtl: error (200)`, exit 2 / 0xC000013A — #1153
|
||||
// class). No console + own process group = no console events, ever.
|
||||
// stdout/stderr are piped above, so nothing is lost. Same flag the
|
||||
// nvidia-smi probe already uses (setup.rs).
|
||||
cmd.creation_flags(0x0800_0000 | 0x0000_0200);
|
||||
}
|
||||
let mut child = match cmd
|
||||
.args([
|
||||
"-m",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""#1155 / #1153 — Windows stdio + console guards.
|
||||
|
||||
Two crash classes from the wild, one boundary:
|
||||
|
||||
* #1155: kittentts `print()`s the user's text; on Windows the backend's
|
||||
stdout defaults to cp1252, so Vietnamese/CJK/etc. raised
|
||||
UnicodeEncodeError — surfaced to the user as a bogus
|
||||
`400 Bad Request: 'charmap' codec can't encode character…`. The
|
||||
process-wide SafeFileWrapper only swallowed OSError (its EPIPE job), so
|
||||
the encode error sailed through. Guard both layers: stdio is reconfigured
|
||||
to UTF-8 at startup, and the wrapper also swallows UnicodeError (logs are
|
||||
best-effort; synthesis is not).
|
||||
|
||||
* #1153-class: the Intel Fortran runtime (MKL, under numpy/scipy) installs
|
||||
a console CTRL handler that aborts the whole backend with
|
||||
`forrtl: error (200): program aborting due to window-CLOSE event` when a
|
||||
console CLOSE event reaches the process. FOR_DISABLE_CONSOLE_CTRL_HANDLER=1
|
||||
disables that handler; main.py must set it before MKL can load. (The
|
||||
desktop shell also sets it — plus CREATE_NO_WINDOW — at spawn; this is
|
||||
the guard for `scripts/run.sh` / `python -m uvicorn` launches.)
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("OMNIVOICE_MODEL", "test")
|
||||
os.environ.setdefault("OMNIVOICE_DISABLE_FILE_LOG", "1")
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
|
||||
|
||||
VIETNAMESE = "Generating audio for text: xin chào, bạn khỏe không ả" # noqa: RUF001
|
||||
|
||||
|
||||
def test_safe_file_wrapper_swallows_unicode_encode_errors():
|
||||
from utils.hf_progress import SafeFileWrapper
|
||||
|
||||
cp1252 = io.TextIOWrapper(io.BytesIO(), encoding="cp1252")
|
||||
wrapped = SafeFileWrapper(cp1252)
|
||||
# The exact #1155 shape: user text with U+1EA3 hitting a charmap stream.
|
||||
wrapped.write(VIETNAMESE) # must not raise
|
||||
wrapped.flush()
|
||||
|
||||
|
||||
def test_main_sets_fortran_console_guard_and_utf8_stdio():
|
||||
import main # noqa: F401 (import-time side effects are the contract)
|
||||
|
||||
# forrtl error (200) guard — read by the Intel Fortran RTL at DLL init,
|
||||
# so it must be in the environment before torch/numpy import MKL.
|
||||
assert os.environ.get("FOR_DISABLE_CONSOLE_CTRL_HANDLER") == "1"
|
||||
|
||||
# The stream under the EPIPE wrapper must be UTF-8 so no library print
|
||||
# of user text can ever hit a charmap codec (kittentts does exactly
|
||||
# that on every generate call).
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
fp = getattr(stream, "fp", stream)
|
||||
enc = (getattr(fp, "encoding", None) or "utf-8").lower().replace("-", "")
|
||||
assert enc == "utf8", f"stdio still on {enc}"
|
||||
Reference in New Issue
Block a user