console-error-scanner 1.16.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- console_error_scanner/__init__.py +5 -0
- console_error_scanner/__main__.py +370 -0
- console_error_scanner/app.py +1870 -0
- console_error_scanner/app.tcss +91 -0
- console_error_scanner/i18n.py +74 -0
- console_error_scanner/locale/de.json +417 -0
- console_error_scanner/locale/en.json +417 -0
- console_error_scanner/models/__init__.py +1 -0
- console_error_scanner/models/history.py +220 -0
- console_error_scanner/models/robots.py +154 -0
- console_error_scanner/models/scan_result.py +265 -0
- console_error_scanner/models/settings.py +242 -0
- console_error_scanner/models/sitemap.py +347 -0
- console_error_scanner/models/whitelist.py +226 -0
- console_error_scanner/screens/__init__.py +1 -0
- console_error_scanner/screens/diet_advisor.py +128 -0
- console_error_scanner/screens/error_detail.py +164 -0
- console_error_scanner/screens/history.py +187 -0
- console_error_scanner/screens/scan_summary.py +234 -0
- console_error_scanner/screens/settings.py +292 -0
- console_error_scanner/screens/top_errors.py +304 -0
- console_error_scanner/screens/whitelist.py +154 -0
- console_error_scanner/services/__init__.py +1 -0
- console_error_scanner/services/image_clipboard.py +104 -0
- console_error_scanner/services/preview_service.py +291 -0
- console_error_scanner/services/rate_limit.py +56 -0
- console_error_scanner/services/reporter.py +561 -0
- console_error_scanner/services/scanner.py +965 -0
- console_error_scanner/services/site_score.py +151 -0
- console_error_scanner/widgets/__init__.py +1 -0
- console_error_scanner/widgets/bar_chart.py +60 -0
- console_error_scanner/widgets/preview_panel.py +356 -0
- console_error_scanner/widgets/results_table.py +594 -0
- console_error_scanner/widgets/stats_panel.py +387 -0
- console_error_scanner/widgets/summary_panel.py +176 -0
- console_error_scanner-1.16.3.dist-info/METADATA +509 -0
- console_error_scanner-1.16.3.dist-info/RECORD +41 -0
- console_error_scanner-1.16.3.dist-info/WHEEL +5 -0
- console_error_scanner-1.16.3.dist-info/entry_points.txt +2 -0
- console_error_scanner-1.16.3.dist-info/licenses/LICENSE +202 -0
- console_error_scanner-1.16.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Entry Point fuer Console Error Scanner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from typing import TextIO
|
|
9
|
+
|
|
10
|
+
# Frozen-EXE Erkennung (PyInstaller UND Nuitka):
|
|
11
|
+
# PLAYWRIGHT_BROWSERS_PATH muss gesetzt werden BEVOR playwright importiert wird,
|
|
12
|
+
# damit das gebundelte Chromium im "browsers"-Unterordner gefunden wird.
|
|
13
|
+
_is_frozen = getattr(sys, "frozen", False) or "__compiled__" in globals()
|
|
14
|
+
if _is_frozen:
|
|
15
|
+
_exe_dir = os.path.dirname(sys.executable)
|
|
16
|
+
_browsers_dir = os.path.join(_exe_dir, "browsers")
|
|
17
|
+
if os.path.isdir(_browsers_dir):
|
|
18
|
+
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = _browsers_dir
|
|
19
|
+
|
|
20
|
+
from textual_widgets import reset_terminal_title, set_terminal_title
|
|
21
|
+
|
|
22
|
+
# Log-Handle offen halten, solange der Prozess laeuft - faulthandler schreibt
|
|
23
|
+
# beim fatalen Signal direkt hinein. Ohne Referenz wuerde der GC es schliessen.
|
|
24
|
+
_fault_log: TextIO | None = None
|
|
25
|
+
|
|
26
|
+
from console_error_scanner import __version__
|
|
27
|
+
from console_error_scanner.i18n import SUPPORTED_LANGUAGES, load_locale
|
|
28
|
+
from console_error_scanner.models.settings import Settings
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _silence_proactor_teardown_noise() -> None:
|
|
32
|
+
"""Unterdrueckt das bekannte Windows-asyncio-Teardown-Rauschen.
|
|
33
|
+
|
|
34
|
+
Auf Windows finalisiert der ProactorEventLoop die Subprocess-/Pipe-
|
|
35
|
+
Transports des Playwright-Chromium teilweise erst per Garbage-Collector
|
|
36
|
+
NACH dem (sauberen) App-Ende. Deren ``__del__`` wirft dann
|
|
37
|
+
'unclosed transport' (ResourceWarning) bzw. 'I/O operation on closed pipe'
|
|
38
|
+
(ValueError) - reines kosmetisches Rauschen, das ueber ``sys.unraisablehook``
|
|
39
|
+
auf stderr landet. Wir schlucken GENAU diese Faelle; alles andere geht
|
|
40
|
+
unveraendert an den Original-Hook (keine echten Fehler werden versteckt).
|
|
41
|
+
"""
|
|
42
|
+
if sys.platform != "win32":
|
|
43
|
+
return
|
|
44
|
+
original = sys.unraisablehook
|
|
45
|
+
|
|
46
|
+
def _hook(unraisable: sys.UnraisableHookArgs) -> None:
|
|
47
|
+
text = f"{unraisable.err_msg or ''} {unraisable.exc_value or ''}"
|
|
48
|
+
if "closed pipe" in text or "unclosed transport" in text:
|
|
49
|
+
return
|
|
50
|
+
original(unraisable)
|
|
51
|
+
|
|
52
|
+
sys.unraisablehook = _hook
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main() -> None:
|
|
56
|
+
"""Haupteinstiegspunkt fuer die CLI."""
|
|
57
|
+
_enable_faulthandler()
|
|
58
|
+
_silence_proactor_teardown_noise()
|
|
59
|
+
|
|
60
|
+
settings = Settings.load()
|
|
61
|
+
saved_lang = settings.language
|
|
62
|
+
|
|
63
|
+
parser = argparse.ArgumentParser(
|
|
64
|
+
prog="console-error-scanner",
|
|
65
|
+
description=f"\n Console Error Scanner v{__version__}\n",
|
|
66
|
+
epilog=_usage_examples(),
|
|
67
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"sitemap_url",
|
|
72
|
+
nargs="?",
|
|
73
|
+
default="",
|
|
74
|
+
metavar="URL_OR_FILE",
|
|
75
|
+
help=(
|
|
76
|
+
"URL der Website, Sitemap-URL oder lokale sitemap.xml. "
|
|
77
|
+
"Bei Domain-URLs wird die Sitemap automatisch gesucht. "
|
|
78
|
+
"Ohne Argument fragt die App die URL beim ersten 's' ab."
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--concurrency",
|
|
83
|
+
"-c",
|
|
84
|
+
type=int,
|
|
85
|
+
default=None,
|
|
86
|
+
metavar="N",
|
|
87
|
+
help="Max parallele Browser-Tabs (Default aus Settings)",
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--timeout",
|
|
91
|
+
"-t",
|
|
92
|
+
type=int,
|
|
93
|
+
default=None,
|
|
94
|
+
metavar="SEC",
|
|
95
|
+
help="Timeout pro Seite in Sekunden (Default aus Settings)",
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--output-json",
|
|
99
|
+
default="",
|
|
100
|
+
metavar="PATH",
|
|
101
|
+
help="JSON-Report automatisch speichern",
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--output-html",
|
|
105
|
+
default="",
|
|
106
|
+
metavar="PATH",
|
|
107
|
+
help="HTML-Report automatisch speichern",
|
|
108
|
+
)
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--no-headless",
|
|
111
|
+
action="store_true",
|
|
112
|
+
default=False,
|
|
113
|
+
help="Browser sichtbar starten (Debugging)",
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--filter",
|
|
117
|
+
"-f",
|
|
118
|
+
default="",
|
|
119
|
+
metavar="TEXT",
|
|
120
|
+
help="Nur URLs scannen die TEXT enthalten",
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
"--console-level",
|
|
124
|
+
default="",
|
|
125
|
+
choices=["", "error", "warn", "all"],
|
|
126
|
+
metavar="LEVEL",
|
|
127
|
+
help="Console-Level: error | warn | all (Default aus Settings)",
|
|
128
|
+
)
|
|
129
|
+
parser.add_argument(
|
|
130
|
+
"--user-agent",
|
|
131
|
+
default="",
|
|
132
|
+
metavar="UA",
|
|
133
|
+
help="Custom User-Agent String (Default: realistischer Chrome)",
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--cookie",
|
|
137
|
+
action="append",
|
|
138
|
+
default=[],
|
|
139
|
+
metavar="NAME=VALUE",
|
|
140
|
+
help="Cookie setzen (mehrfach moeglich). Default aus Settings.",
|
|
141
|
+
)
|
|
142
|
+
parser.add_argument(
|
|
143
|
+
"--whitelist",
|
|
144
|
+
"-w",
|
|
145
|
+
default="",
|
|
146
|
+
metavar="PATH",
|
|
147
|
+
help="Pfad zur Whitelist-JSON (Default aus Settings)",
|
|
148
|
+
)
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"--no-consent",
|
|
151
|
+
action="store_true",
|
|
152
|
+
default=None,
|
|
153
|
+
help="Cookie-Consent NICHT akzeptieren (nur Banner verstecken)",
|
|
154
|
+
)
|
|
155
|
+
parser.add_argument(
|
|
156
|
+
"--no-scroll",
|
|
157
|
+
action="store_true",
|
|
158
|
+
default=None,
|
|
159
|
+
help="Seite nicht scrollen (kein Lazy-Loading-Trigger)",
|
|
160
|
+
)
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
"--ignore-robots",
|
|
163
|
+
action="store_true",
|
|
164
|
+
default=None,
|
|
165
|
+
help="robots.txt ignorieren (nur fuer eigene Seiten sinnvoll)",
|
|
166
|
+
)
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--lang",
|
|
169
|
+
default=saved_lang,
|
|
170
|
+
choices=SUPPORTED_LANGUAGES,
|
|
171
|
+
help=f"Language ({', '.join(SUPPORTED_LANGUAGES)})",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
args = parser.parse_args()
|
|
175
|
+
|
|
176
|
+
# Sprache laden (CLI > Settings > Default)
|
|
177
|
+
lang = args.lang
|
|
178
|
+
load_locale(lang)
|
|
179
|
+
|
|
180
|
+
# Sprache persistent speichern wenn per CLI geaendert
|
|
181
|
+
if lang != saved_lang:
|
|
182
|
+
settings.language = lang
|
|
183
|
+
settings.save()
|
|
184
|
+
|
|
185
|
+
# Cookies parsen: "NAME=VALUE" -> {"name": "NAME", "value": "VALUE"}
|
|
186
|
+
cookies = []
|
|
187
|
+
for cookie_str in args.cookie:
|
|
188
|
+
if "=" not in cookie_str:
|
|
189
|
+
from console_error_scanner.i18n import t
|
|
190
|
+
|
|
191
|
+
print(t("cli.invalid_cookie", cookie=cookie_str))
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
name, value = cookie_str.split("=", 1)
|
|
194
|
+
cookies.append({"name": name.strip(), "value": value.strip()})
|
|
195
|
+
|
|
196
|
+
# textual-image (TGP/Sixel) eager initialisieren, sobald das Terminal
|
|
197
|
+
# Grafik-faehig ist - egal ob Vorschau aktuell an oder aus ist. Sonst
|
|
198
|
+
# leaken die DA1-/Cell-Size-Query-Antworten beim spaeteren Aktivieren der
|
|
199
|
+
# Vorschau in den fokussierten Input (Filter-Suche).
|
|
200
|
+
_preinit_graphics_backend()
|
|
201
|
+
|
|
202
|
+
# App NACH load_locale importieren - t() ist sofort verfuegbar
|
|
203
|
+
from console_error_scanner.app import ConsoleErrorScannerApp
|
|
204
|
+
|
|
205
|
+
set_terminal_title(f"✗ console-error-scanner v{__version__}")
|
|
206
|
+
try:
|
|
207
|
+
app = ConsoleErrorScannerApp(
|
|
208
|
+
sitemap_url=args.sitemap_url,
|
|
209
|
+
concurrency=args.concurrency,
|
|
210
|
+
timeout=args.timeout,
|
|
211
|
+
output_json=args.output_json,
|
|
212
|
+
output_html=args.output_html,
|
|
213
|
+
headless=not args.no_headless,
|
|
214
|
+
url_filter=args.filter,
|
|
215
|
+
console_level=args.console_level,
|
|
216
|
+
user_agent=args.user_agent,
|
|
217
|
+
cookies=cookies,
|
|
218
|
+
whitelist_path=args.whitelist,
|
|
219
|
+
accept_consent=(not args.no_consent) if args.no_consent is not None else None,
|
|
220
|
+
trigger_lazy_load=(not args.no_scroll) if args.no_scroll is not None else None,
|
|
221
|
+
respect_robots=(not args.ignore_robots) if args.ignore_robots is not None else None,
|
|
222
|
+
)
|
|
223
|
+
app.run()
|
|
224
|
+
finally:
|
|
225
|
+
reset_terminal_title()
|
|
226
|
+
_reset_mouse_tracking()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _preinit_graphics_backend() -> None:
|
|
230
|
+
"""Eager-Import textual-image vor App-Start.
|
|
231
|
+
|
|
232
|
+
textual-image sendet beim Import Escape-Queries ans Terminal (DA1
|
|
233
|
+
Primary Device Attributes, Cell-Size-Query). Diese MUESSEN vor
|
|
234
|
+
`App.run()` laufen, damit die Antworten von der Library konsumiert
|
|
235
|
+
werden und nicht in Textual-Inputs landen (typischer Effekt: kryptische
|
|
236
|
+
`[?61;...c<35;...M`-Strings im Filter-Eingabefeld).
|
|
237
|
+
|
|
238
|
+
Wir laufen NUR, wenn das Terminal ueberhaupt ein Grafik-Protokoll
|
|
239
|
+
unterstuetzen koennte (TGP/Sixel) - sonst spart das den Import auf
|
|
240
|
+
"dummen" Terminals (SSH ohne Mux, dumb-tty etc.).
|
|
241
|
+
"""
|
|
242
|
+
if not _terminal_supports_graphics():
|
|
243
|
+
return
|
|
244
|
+
try:
|
|
245
|
+
import time
|
|
246
|
+
|
|
247
|
+
import textual_image.renderable # noqa: F401
|
|
248
|
+
import textual_image.widget # noqa: F401
|
|
249
|
+
from textual_image._terminal import get_cell_size
|
|
250
|
+
|
|
251
|
+
get_cell_size()
|
|
252
|
+
# Kurze Pause, damit das Terminal Zeit hat die Probe-Queries zu
|
|
253
|
+
# beantworten BEVOR Textual stdin uebernimmt - sonst hat man die
|
|
254
|
+
# Antworten zwar abgeschickt, sie kommen aber erst zurueck wenn
|
|
255
|
+
# der Cooked-Mode schon zu ist (Windows Terminal/mintty sind hier
|
|
256
|
+
# spuerbar langsamer als ein lokales xterm).
|
|
257
|
+
time.sleep(0.15)
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _terminal_supports_graphics() -> bool:
|
|
263
|
+
"""Schnelle Heuristik: kann das Terminal evtl. TGP oder Sixel?"""
|
|
264
|
+
term = os.environ.get("TERM", "").lower()
|
|
265
|
+
term_program = os.environ.get("TERM_PROGRAM", "").lower()
|
|
266
|
+
if os.environ.get("KITTY_WINDOW_ID"):
|
|
267
|
+
return True
|
|
268
|
+
if "kitty" in term or "ghostty" in term:
|
|
269
|
+
return True
|
|
270
|
+
if term_program in ("wezterm", "ghostty", "mintty", "iterm.app"):
|
|
271
|
+
return True
|
|
272
|
+
if os.environ.get("KONSOLE_VERSION"):
|
|
273
|
+
return True
|
|
274
|
+
if os.environ.get("WT_SESSION"):
|
|
275
|
+
return True
|
|
276
|
+
return term in ("foot", "xterm", "mlterm", "mintty") or "foot" in term
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _usage_examples() -> str:
|
|
280
|
+
"""Gibt die Nutzungsbeispiele zurueck."""
|
|
281
|
+
return """
|
|
282
|
+
Examples:
|
|
283
|
+
console-error-scanner https://example.com
|
|
284
|
+
console-error-scanner https://example.com/sitemap.xml
|
|
285
|
+
console-error-scanner sitemap.xml
|
|
286
|
+
console-error-scanner https://example.com --concurrency 12
|
|
287
|
+
console-error-scanner https://example.com --lang en
|
|
288
|
+
|
|
289
|
+
Keybindings (TUI):
|
|
290
|
+
c = Crawl m = Load sitemap r = Report h = History s = Settings
|
|
291
|
+
w = Whitelist e = Errors only t = Theme l = Log
|
|
292
|
+
d = Copy detail F10 = Top 10 / = Filter i = Info q = Quit
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _enable_faulthandler() -> None:
|
|
297
|
+
"""Faengt HARTE Abstuerze ab, die an Pythons Exception-Handling UND am
|
|
298
|
+
finally-Block vorbeilaufen: native Access Violation, Stack-Overflow, fataler
|
|
299
|
+
Interpreter-Fehler.
|
|
300
|
+
|
|
301
|
+
faulthandler schreibt den Traceback aller Threads in fault.log - separat vom
|
|
302
|
+
Terminal, das der Maus-Tracking-Muell sonst unlesbar macht. CrashGuard und
|
|
303
|
+
_persist_crash greifen nur bei normalen Python-Exceptions, dieser Handler bei
|
|
304
|
+
allem darunter. Die Startzeile ist die zweite Haelfte der Diagnose: steht
|
|
305
|
+
danach nichts weiter in der Datei, wurde der Prozess von aussen abgeraeumt.
|
|
306
|
+
"""
|
|
307
|
+
import atexit
|
|
308
|
+
import contextlib
|
|
309
|
+
import faulthandler
|
|
310
|
+
from datetime import datetime
|
|
311
|
+
|
|
312
|
+
from console_error_scanner.models.settings import SETTINGS_FILE
|
|
313
|
+
|
|
314
|
+
global _fault_log
|
|
315
|
+
with contextlib.suppress(Exception):
|
|
316
|
+
verzeichnis = SETTINGS_FILE.parent
|
|
317
|
+
verzeichnis.mkdir(parents=True, exist_ok=True)
|
|
318
|
+
# Bewusst offen lassen (Prozess-Lebensdauer) - faulthandler schreibt
|
|
319
|
+
# beim fatalen Signal direkt in dieses Handle.
|
|
320
|
+
_fault_log = open(verzeichnis / "fault.log", "a", encoding="utf-8") # noqa: SIM115
|
|
321
|
+
stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
322
|
+
_fault_log.write(f"\n===== Start {stamp} - v{__version__} =====\n")
|
|
323
|
+
_fault_log.flush()
|
|
324
|
+
faulthandler.enable(file=_fault_log, all_threads=True)
|
|
325
|
+
# Gegenstueck zur Startzeile - siehe _write_fault_end.
|
|
326
|
+
atexit.register(_write_fault_end)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _reset_mouse_tracking() -> None:
|
|
330
|
+
"""Schaltet alle Maus-Tracking-Modi des Terminals ab (idempotent).
|
|
331
|
+
|
|
332
|
+
Schreibt bewusst nach sys.__stdout__, NICHT sys.stdout: Textual kapert
|
|
333
|
+
sys.stdout zur Laufzeit, ein Reset dorthin landet im Nichts und das Terminal
|
|
334
|
+
bleibt im Maus-Tracking-Modus haengen. Der garantierte Reset passiert im
|
|
335
|
+
Shell-Wrapper run.ps1 (laeuft auch nach einem harten Crash) - das hier ist
|
|
336
|
+
der zusaetzliche In-Prozess-Pfad.
|
|
337
|
+
"""
|
|
338
|
+
stream = sys.__stdout__
|
|
339
|
+
if stream is None or not stream.isatty():
|
|
340
|
+
return
|
|
341
|
+
stream.write("[?1000l[?1002l[?1003l[?1006l[?1015l")
|
|
342
|
+
stream.flush()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _write_fault_end() -> None:
|
|
346
|
+
"""Schreibt die Endzeile der Sitzungsklammer (ueber atexit registriert).
|
|
347
|
+
|
|
348
|
+
Erst dieses Gegenstueck zur Startzeile macht die Datei aussagekraeftig:
|
|
349
|
+
|
|
350
|
+
Start + Ende -> sauber beendet
|
|
351
|
+
Start + Traceback -> Python-Fehler (der Handler hat ihn gesehen)
|
|
352
|
+
Start und sonst nichts -> Prozess hart abgeraeumt
|
|
353
|
+
|
|
354
|
+
Unter Windows hilft ein Signalhandler dabei nicht: ein Abbruch von aussen
|
|
355
|
+
laeuft dort ueber TerminateProcess und liefert dem Ziel kein abfangbares
|
|
356
|
+
Signal. Die FEHLENDE Endzeile ist der einzige Beleg.
|
|
357
|
+
"""
|
|
358
|
+
import contextlib
|
|
359
|
+
from datetime import datetime
|
|
360
|
+
|
|
361
|
+
if _fault_log is None:
|
|
362
|
+
return
|
|
363
|
+
with contextlib.suppress(Exception):
|
|
364
|
+
stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
365
|
+
_fault_log.write(f"===== Ende {stamp} =====\n")
|
|
366
|
+
_fault_log.flush()
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
if __name__ == "__main__":
|
|
370
|
+
main()
|