A crash you can read, on a window that waits

Double-clicking the executable on Windows gives the process its own console, and
Windows destroys that console the moment the process ends. So a traceback was
drawn and erased in the same instant: the report was "it displays a trace but the
window closes almost instantly", which is not a rendering problem, it is the
window being owned by the thing that died.

Two changes, because one of them is not enough.

The window now waits. `GetConsoleProcessList` says how many processes share this
console; when the answer is one, nobody else is holding the window open and it
will close with us. On that path only, and only after a failure, MeshTerm asks for
a keypress before it goes. A run started from a shell leaves its output on the
screen anyway, and a run that worked should never make anyone press a key to be
allowed to leave.

And the interactive session logs what killed it. It was catching the missing
Windows console and re-raising everything else untouched, which is precisely
backwards for the case that needs a record most: the session whose console is
about to vanish is the one whose traceback nobody will get to read. It goes into
the file first, and the terminal is told where the file is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-09-07 14:49:54 -04:00
1 parent bdc16fc448
commit 3379ea2d44
2 files changed
+72 -9

No files matched your search

+5
View File
@@ -41,6 +41,11 @@ allowed to change behaviour, not just add to it.
the terminal says where to find it; expected errors go in as warnings, because a log
that only holds crashes cannot answer "what happened just before". The bug report form
asks for the file.
- The window stays open after a crash when MeshTerm owns it. Double-clicking the
executable on Windows gives the process its own console, which is destroyed the instant
the process ends — so an error was drawn and erased together and all anyone saw was a
flash. It now waits for a keypress, but only on that path: a run from a shell leaves its
output on screen already, and a successful run should not make anyone press a key.
- A readable answer when there is no Windows console. Git Bash, MSYS and Cygwin are not
Windows consoles, and prompt_toolkit's own error says so in the language of its
internals, arriving as a traceback that reads like a broken app rather than the
+67 -9
View File
@@ -10,6 +10,7 @@ from __future__ import annotations
import asyncio
import logging
import sys
from pathlib import Path
import typer
@@ -189,13 +190,20 @@ def main_callback(
console.print(f"[err]✗[/err] {exc}")
raise typer.Exit(code=1) from None
except Exception as exc:
if type(exc).__name__ != "NoConsoleScreenBufferError":
raise
# prompt_toolkit needs a real Win32 console and reports its absence in the
# language of its own internals. A traceback reads as "this app is broken"
# when the answer is "open a different terminal", so answer that instead.
_report_no_windows_console(console)
raise typer.Exit(code=1) from None
if type(exc).__name__ == "NoConsoleScreenBufferError":
# prompt_toolkit needs a real Win32 console and reports its absence in the
# language of its own internals. A traceback reads as "this app is broken"
# when the answer is "open a different terminal", so answer that instead.
get_logger().warning("no Windows console available: %s", exc)
_report_no_windows_console(console)
raise typer.Exit(code=1) from None
# Logged before it is re-raised, and this is the case the log matters most for:
# a session launched by double-clicking the executable owns its console window,
# so when it dies the window closes with it and takes the traceback along. The
# file is the only thing left to read afterwards.
get_logger().exception("the interactive session raised an unhandled error")
_report_log_location(console, settings.config_dir)
raise
def _report_log_location(console: Console, config_dir: Path) -> None:
@@ -348,9 +356,59 @@ def _register_all() -> None:
_register_all()
def _owns_its_console() -> bool:
"""Whether this process is the only one on its console — i.e. it was double-clicked.
Windows gives a console to a process launched from Explorer and destroys it the moment
that process ends, so an error message is displayed and erased in the same instant.
Launched from a shell instead, the console belongs to the shell and survives.
``GetConsoleProcessList`` tells the two apart: it reports how many processes are
attached to this console. One is us, alone, holding a window that dies with us.
Returns:
``True`` only on Windows, and only when nothing else shares the console.
"""
if sys.platform != "win32":
return False
try:
import ctypes
buffer = (ctypes.c_uint * 4)()
count = ctypes.windll.kernel32.GetConsoleProcessList(buffer, 4) # type: ignore[attr-defined]
except Exception: # pragma: no cover - no console at all, or a stubbed kernel32
return False
return count == 1
def main() -> None:
"""Console-script entry point."""
app()
"""Console-script entry point.
Holds the window open after a failure when MeshTerm owns it, because otherwise the
error is drawn and destroyed together and the person who needs to read it sees a
flash. Only on that path: a successful run should not make anyone press a key, and a
run from a shell leaves its output on the screen anyway.
"""
try:
app()
except SystemExit as exit_request:
if exit_request.code and _owns_its_console():
_wait_before_the_window_closes()
raise
except BaseException:
if _owns_its_console():
_wait_before_the_window_closes()
raise
def _wait_before_the_window_closes() -> None:
"""Ask for a keypress so a message stays readable, and never fail doing it."""
try:
print()
print("-- Press Enter to close this window --")
input()
except Exception: # pragma: no cover - stdin closed, redirected, or gone
pass
if __name__ == "__main__":