From 981f477e31bd81ea79635990b9f045781cd316a5 Mon Sep 17 00:00:00 2001 From: Velsa Date: Sun, 22 Mar 2026 08:49:59 -0300 Subject: [PATCH] fix: reconfigure stdout to UTF-8 on Windows to prevent UnicodeEncodeError on startup (#4493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: reconfigure stdout UTF-8 on Windows to prevent UnicodeEncodeError from emoji * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: default frontend_path when None to fix blank page when venv is pre-activated * Restore Windows UTF-8 stdout fix dropped in earlier commit The cp1252 console encoding on Windows cannot render emoji characters used in startup messages (e.g. print("✅ Frontend loaded ...")). This causes UnicodeEncodeError and crashes the server before it starts. Place sys.stdout.reconfigure(encoding="utf-8", errors="replace") at the top of run_server(), unconditionally before any print() or structlog call, so all emoji output is covered -- including the frontend status messages and silent=True paths that the original placement missed. Guarded by sys.platform == "win32" and hasattr check, so it is a no-op on Linux/macOS and safe in non-standard stdout environments (Jupyter, piped IO). * fix: preserve run_server(None) as headless, fix CLI frontend kwarg Remove the frontend_path=None fallback in run_server() that changed None from "headless/API-only" to "mount bundled frontend", breaking backwards compatibility for embedders. The blank-page bug was actually caused by the CLI wrappers always passing frontend_path=frontend (even when frontend=None), which overrode run_server()'s default. Fix studio.py and ui.py to only pass frontend_path when the user explicitly sets --frontend. * fix: use timeout loop for shutdown event in ui command Match studio_default()'s shutdown loop that uses a 1-second timeout on Event.wait(). Without a timeout, the bare wait() blocks at the C level on Linux, preventing Python from delivering SIGINT (Ctrl+C). --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han --- studio/backend/run.py | 8 ++++++++ unsloth_cli/commands/studio.py | 10 ++++------ unsloth_cli/commands/ui.py | 15 ++++++++------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/studio/backend/run.py b/studio/backend/run.py index 5388d3148b..2d86f70578 100644 --- a/studio/backend/run.py +++ b/studio/backend/run.py @@ -175,6 +175,14 @@ def run_server( """ global _server, _shutdown_event + # On Windows the default console encoding (cp1252) cannot encode emoji. + # Reconfigure stdout to UTF-8 so startup messages do not crash the server. + if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"): + try: + sys.stdout.reconfigure(encoding = "utf-8", errors = "replace") + except Exception: + pass + import nest_asyncio nest_asyncio.apply() diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py index 8a2267a79e..c4008aa4a6 100644 --- a/unsloth_cli/commands/studio.py +++ b/unsloth_cli/commands/studio.py @@ -133,12 +133,10 @@ def studio_default( display_host = _resolve_external_ip() if host == "0.0.0.0" else host typer.echo(f"Starting Unsloth Studio on http://{display_host}:{port}") - run_server( - host = host, - port = port, - frontend_path = frontend, - silent = silent, - ) + run_kwargs = dict(host = host, port = port, silent = silent) + if frontend is not None: + run_kwargs["frontend_path"] = frontend + run_server(**run_kwargs) from studio.backend.run import _shutdown_event diff --git a/unsloth_cli/commands/ui.py b/unsloth_cli/commands/ui.py index 4457465e96..8f76636990 100644 --- a/unsloth_cli/commands/ui.py +++ b/unsloth_cli/commands/ui.py @@ -80,18 +80,19 @@ def ui( display_host = _resolve_external_ip() if host == "0.0.0.0" else host typer.echo(f"Starting Unsloth Studio on http://{display_host}:{port}") - run_server( - host = host, - port = port, - frontend_path = frontend, - silent = silent, - ) + run_kwargs = dict(host = host, port = port, silent = silent) + if frontend is not None: + run_kwargs["frontend_path"] = frontend + run_server(**run_kwargs) from studio.backend.run import _shutdown_event try: if _shutdown_event is not None: - _shutdown_event.wait() + # NOTE: Event.wait() without a timeout blocks at the C level + # on Linux, preventing Python from delivering SIGINT (Ctrl+C). + while not _shutdown_event.is_set(): + _shutdown_event.wait(timeout = 1) else: while True: time.sleep(1)