Skip to content

Commit 731f0fb

Browse files
michaelblaessclaude
andcommitted
feat: Sitzungsklammer in fault.log schliessen, Fehlerdiagnose dokumentieren
Die Startzeile allein sagt nur, dass der Prozess lief. Erst mit einer Endzeile laesst sich der entscheidende Fall auseinanderhalten: Start + Ende -> sauber beendet Start + Traceback -> Python-Fehler, das Programm hat ihn gesehen Start und sonst nichts -> Prozess hart abgeraeumt, KEIN Python-Fehler Im Terminal sehen die letzten beiden Faelle gleich aus - Steuerzeichen-Muell und sonst nichts. Ein Signalhandler hilft unter Windows nicht weiter: ein Abbruch von aussen laeuft dort ueber TerminateProcess und liefert dem Ziel kein abfangbares Signal. Die FEHLENDE Endzeile ist der einzige Beleg. Die Endzeile haengt an atexit und ist damit an denselben Pfad gebunden wie ein regulaeres Programmende. Belegt in beide Richtungen: bei sauberem Ende stehen beide Zeilen da, bei hart abgeraeumtem Prozess nur die Startzeile. Nebenbei den Typ von _fault_log praezisiert - object kennt weder write noch flush, mypy hat das zu Recht bemaengelt. Doku ---- Alle READMEs (deutsch und englisch) haben jetzt einen Abschnitt "Wenn etwas schiefgeht": welche der beiden Dateien wofuer zustaendig ist, wie sich die drei Faelle unterscheiden lassen und warum man unter Windows besser ueber run.ps1 startet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent fd78a82 commit 731f0fb

4 files changed

Lines changed: 111 additions & 1 deletion

File tree

README.de.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,37 @@ ein Fehler beim Auslesen) führt zu Englisch.
300300

301301
---
302302

303+
## Wenn etwas schiefgeht
304+
305+
Stuerzt das Programm ab, landet der Bericht auf Platte statt nur im Terminal -
306+
zwei Dateien neben den Einstellungen, beide im Speicherort-Tab der
307+
Einstellungen verlinkt, sobald es sie gibt:
308+
309+
| Datei | Wofuer |
310+
| --- | --- |
311+
| `last-crash.txt` | Python-Fehler samt Traceback. Wird geschrieben, **bevor** der Fehlerdialog laeuft - faellt dieser beim Neuaufbau selbst mit, waere der Bericht sonst verloren. |
312+
| `fault.log` | Alles darunter: native Speicherzugriffsfehler, Stack-Overflow, fataler Interpreter-Fehler. Solche Abstuerze laufen an Pythons Fehlerbehandlung vorbei. |
313+
314+
Beide Dateien werden **angehaengt**, nicht ersetzt - ein zweiter Absturz
315+
verdeckt den ersten nicht.
316+
317+
`fault.log` bekommt ausserdem je Programmlauf eine Start- und eine Endzeile.
318+
Damit ist ablesbar, was passiert ist:
319+
320+
| Was in der Datei steht | Was es bedeutet |
321+
| --- | --- |
322+
| Start, Traceback, Ende | Python-Fehler, das Programm hat ihn gesehen |
323+
| Start, Ende | sauber beendet |
324+
| Start, dann nichts | Prozess hart abgeraeumt - **kein** Python-Fehler |
325+
326+
Ein hart abgeraeumter Prozess und ein Absturz sehen im Terminal gleich aus.
327+
Erst die fehlende Endzeile trennt beide Faelle.
328+
329+
Unter Windows startest Du das Programm am besten ueber `run.ps1`: das Skript
330+
setzt das Terminal auch dann wieder zurueck, wenn das Programm hart abstuerzt
331+
und selbst nichts mehr tun kann. Sonst bleibt die Maus-Erfassung aktiv, und
332+
jede Mausbewegung kippt Steuerzeichen in die Eingabezeile.
333+
303334
## Entwickler
304335

305336
### Setup

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,37 @@ environment - German only for a demonstrably German-speaking environment, everyt
300300

301301
---
302302

303+
## When something goes wrong
304+
305+
If the program crashes, the report is written to disk instead of only to the
306+
terminal - two files next to the settings, both linked from the storage tab in
307+
the settings once they exist:
308+
309+
| File | What for |
310+
| --- | --- |
311+
| `last-crash.txt` | Python errors including the traceback. Written **before** the error dialog runs - if that dialog dies during its own re-layout, the report would otherwise be lost. |
312+
| `fault.log` | Everything below that: native access violations, stack overflow, fatal interpreter errors. Such crashes bypass Python's error handling entirely. |
313+
314+
Both files are **appended to**, not replaced - a second crash does not hide the
315+
first one.
316+
317+
`fault.log` also gets a start and an end line per run, which makes the file
318+
readable at a glance:
319+
320+
| What the file contains | What it means |
321+
| --- | --- |
322+
| start, traceback, end | Python error, the program saw it |
323+
| start, end | exited cleanly |
324+
| start, then nothing | process was killed - **not** a Python error |
325+
326+
A killed process and a crash look identical in the terminal. Only the missing
327+
end line tells them apart.
328+
329+
On Windows, prefer starting the program via `run.ps1`: the script restores the
330+
terminal even when the program crashes hard and can no longer do so itself.
331+
Otherwise mouse tracking stays on and every mouse move spills control
332+
characters into your prompt.
333+
303334
## Developers
304335

305336
### Setup

src/console_error_scanner/__main__.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import argparse
66
import os
77
import sys
8+
from typing import TextIO
89

910
# Frozen-EXE Erkennung (PyInstaller UND Nuitka):
1011
# PLAYWRIGHT_BROWSERS_PATH muss gesetzt werden BEVOR playwright importiert wird,
@@ -20,7 +21,7 @@
2021

2122
# Log-Handle offen halten, solange der Prozess laeuft - faulthandler schreibt
2223
# beim fatalen Signal direkt hinein. Ohne Referenz wuerde der GC es schliessen.
23-
_fault_log: object | None = None
24+
_fault_log: TextIO | None = None
2425

2526
from console_error_scanner import __version__
2627
from console_error_scanner.i18n import SUPPORTED_LANGUAGES, load_locale
@@ -307,6 +308,7 @@ def _enable_faulthandler() -> None:
307308
allem darunter. Die Startzeile ist die zweite Haelfte der Diagnose: steht
308309
danach nichts weiter in der Datei, wurde der Prozess von aussen abgeraeumt.
309310
"""
311+
import atexit
310312
import contextlib
311313
import faulthandler
312314
from datetime import datetime
@@ -324,6 +326,8 @@ def _enable_faulthandler() -> None:
324326
_fault_log.write(f"\n===== Start {stamp} - v{__version__} =====\n")
325327
_fault_log.flush()
326328
faulthandler.enable(file=_fault_log, all_threads=True)
329+
# Gegenstueck zur Startzeile - siehe _write_fault_end.
330+
atexit.register(_write_fault_end)
327331

328332

329333
def _reset_mouse_tracking() -> None:
@@ -340,3 +344,27 @@ def _reset_mouse_tracking() -> None:
340344
return
341345
stream.write("[?1000l[?1002l[?1003l[?1006l[?1015l")
342346
stream.flush()
347+
348+
349+
def _write_fault_end() -> None:
350+
"""Schreibt die Endzeile der Sitzungsklammer (ueber atexit registriert).
351+
352+
Erst dieses Gegenstueck zur Startzeile macht die Datei aussagekraeftig:
353+
354+
Start + Ende -> sauber beendet
355+
Start + Traceback -> Python-Fehler (der Handler hat ihn gesehen)
356+
Start und sonst nichts -> Prozess hart abgeraeumt
357+
358+
Unter Windows hilft ein Signalhandler dabei nicht: ein Abbruch von aussen
359+
laeuft dort ueber TerminateProcess und liefert dem Ziel kein abfangbares
360+
Signal. Die FEHLENDE Endzeile ist der einzige Beleg.
361+
"""
362+
import contextlib
363+
from datetime import datetime
364+
365+
if _fault_log is None:
366+
return
367+
with contextlib.suppress(Exception):
368+
stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
369+
_fault_log.write(f"===== Ende {stamp} =====\n")
370+
_fault_log.flush()

tests/test_crash_log.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,23 @@ def test_fault_log_start_line() -> None:
6161
assert erste_zeile.startswith("===== Start ")
6262
assert __version__ in erste_zeile
6363
assert faulthandler.is_enabled()
64+
65+
66+
def test_end_line_closes_the_session() -> None:
67+
"""Das Gegenstueck zur Startzeile - ohne sie ist die Datei nicht deutbar.
68+
69+
Steht nur eine Startzeile da, wurde der Prozess hart abgeraeumt; mit
70+
Endzeile ist er sauber gelaufen. Genau diese Unterscheidung ist der Zweck.
71+
"""
72+
from console_error_scanner.__main__ import _enable_faulthandler, _write_fault_end
73+
74+
_enable_faulthandler()
75+
_write_fault_end() # ruft sonst atexit auf
76+
zeilen = (
77+
(Path(settings_module.SETTINGS_FILE).parent / "fault.log")
78+
.read_text(encoding="utf-8")
79+
.strip()
80+
.split("\n")
81+
)
82+
assert any(z.startswith("===== Start ") for z in zeilen)
83+
assert any(z.startswith("===== Ende ") for z in zeilen)

0 commit comments

Comments
 (0)