sitemap-tracker 2.13.1__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.
- sitemap_tracker/__init__.py +5 -0
- sitemap_tracker/__main__.py +389 -0
- sitemap_tracker/app.py +1614 -0
- sitemap_tracker/app.tcss +72 -0
- sitemap_tracker/i18n.py +100 -0
- sitemap_tracker/locale/de.json +404 -0
- sitemap_tracker/locale/en.json +404 -0
- sitemap_tracker/models/__init__.py +0 -0
- sitemap_tracker/models/crawl_result.py +188 -0
- sitemap_tracker/models/history.py +267 -0
- sitemap_tracker/models/robots.py +175 -0
- sitemap_tracker/models/settings.py +203 -0
- sitemap_tracker/models/sitemap_reader.py +284 -0
- sitemap_tracker/models/sitemap_writer.py +249 -0
- sitemap_tracker/models/url_utils.py +135 -0
- sitemap_tracker/screens/__init__.py +0 -0
- sitemap_tracker/screens/file_check.py +349 -0
- sitemap_tracker/screens/history.py +193 -0
- sitemap_tracker/screens/proxy_warning.py +107 -0
- sitemap_tracker/screens/scan_summary.py +194 -0
- sitemap_tracker/screens/settings.py +236 -0
- sitemap_tracker/screens/source_view.py +284 -0
- sitemap_tracker/services/__init__.py +0 -0
- sitemap_tracker/services/crawler.py +908 -0
- sitemap_tracker/services/file_checker.py +271 -0
- sitemap_tracker/services/page_analysis.py +225 -0
- sitemap_tracker/services/preview_service.py +318 -0
- sitemap_tracker/services/proxy_detect.py +119 -0
- sitemap_tracker/services/rate_limit.py +56 -0
- sitemap_tracker/services/reporter.py +192 -0
- sitemap_tracker/services/site_score.py +99 -0
- sitemap_tracker/services/source_fetcher.py +180 -0
- sitemap_tracker/services/zwischenablage.py +66 -0
- sitemap_tracker/widgets/__init__.py +0 -0
- sitemap_tracker/widgets/crawl_header.py +148 -0
- sitemap_tracker/widgets/page_tree.py +357 -0
- sitemap_tracker/widgets/preview_panel.py +335 -0
- sitemap_tracker/widgets/stats_panel.py +322 -0
- sitemap_tracker/widgets/url_table.py +797 -0
- sitemap_tracker-2.13.1.dist-info/METADATA +328 -0
- sitemap_tracker-2.13.1.dist-info/RECORD +45 -0
- sitemap_tracker-2.13.1.dist-info/WHEEL +5 -0
- sitemap_tracker-2.13.1.dist-info/entry_points.txt +2 -0
- sitemap_tracker-2.13.1.dist-info/licenses/LICENSE +191 -0
- sitemap_tracker-2.13.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""Entry Point fuer Sitemap Tracker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import contextlib
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from typing import TYPE_CHECKING, TextIO
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from sys import UnraisableHookArgs
|
|
13
|
+
|
|
14
|
+
# Frozen-EXE Erkennung (PyInstaller UND Nuitka):
|
|
15
|
+
# PLAYWRIGHT_BROWSERS_PATH muss gesetzt werden BEVOR playwright importiert wird,
|
|
16
|
+
# damit das gebundelte Chromium im "browsers"-Unterordner gefunden wird.
|
|
17
|
+
# PyInstaller setzt sys.frozen, Nuitka setzt stattdessen __compiled__.
|
|
18
|
+
_is_frozen = getattr(sys, "frozen", False) or "__compiled__" in globals()
|
|
19
|
+
if _is_frozen:
|
|
20
|
+
_exe_dir = os.path.dirname(sys.executable)
|
|
21
|
+
_browsers_dir = os.path.join(_exe_dir, "browsers")
|
|
22
|
+
if os.path.isdir(_browsers_dir):
|
|
23
|
+
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = _browsers_dir
|
|
24
|
+
|
|
25
|
+
from textual_widgets import reset_terminal_title, set_terminal_title
|
|
26
|
+
|
|
27
|
+
# Log-Handle offen halten, solange der Prozess laeuft - faulthandler schreibt
|
|
28
|
+
# beim fatalen Signal direkt hinein. Ohne Referenz wuerde der GC es schliessen.
|
|
29
|
+
_fault_log: TextIO | None = None
|
|
30
|
+
|
|
31
|
+
from sitemap_tracker import __version__
|
|
32
|
+
from sitemap_tracker.i18n import load_locale, t
|
|
33
|
+
from sitemap_tracker.models.settings import Settings
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _migrate_user_data() -> None:
|
|
37
|
+
"""Kopiert ``~/.sitemap-generator/`` einmalig nach ``~/.sitemap-tracker/``.
|
|
38
|
+
|
|
39
|
+
Das Tool hiess bis v1.x ``sitemap-generator`` und legte Settings sowie
|
|
40
|
+
History unter ``~/.sitemap-generator/`` ab. Beim ersten Start unter dem
|
|
41
|
+
neuen Namen ``sitemap-tracker`` werden Bestandskunden-Daten kopiert,
|
|
42
|
+
damit History und Einstellungen nicht verloren gehen.
|
|
43
|
+
|
|
44
|
+
Markiert das alte Verzeichnis mit ``.migrated_to_tracker``, damit die
|
|
45
|
+
Kopie nicht erneut ueberschreibt — der User kann das alte Verzeichnis
|
|
46
|
+
danach gefahrlos selbst loeschen.
|
|
47
|
+
"""
|
|
48
|
+
import shutil
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
|
|
51
|
+
old_dir = Path.home() / ".sitemap-generator"
|
|
52
|
+
new_dir = Path.home() / ".sitemap-tracker"
|
|
53
|
+
marker = old_dir / ".migrated_to_tracker"
|
|
54
|
+
if not old_dir.is_dir() or new_dir.exists() or marker.exists():
|
|
55
|
+
return
|
|
56
|
+
try:
|
|
57
|
+
shutil.copytree(old_dir, new_dir)
|
|
58
|
+
marker.write_text("Daten kopiert nach ~/.sitemap-tracker/\n", encoding="utf-8")
|
|
59
|
+
except Exception:
|
|
60
|
+
# Im Zweifel still abbrechen — fehlende Migration ist kein Fehlerfall,
|
|
61
|
+
# der User startet einfach mit Default-Settings.
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _silence_windows_teardown_noise() -> None:
|
|
66
|
+
"""Blendet die bekannten asyncio/playwright-Teardown-Meldungen aus.
|
|
67
|
+
|
|
68
|
+
Drei Quellen, drei Filter:
|
|
69
|
+
|
|
70
|
+
1. ``ResourceWarning: unclosed transport`` aus den ``__del__``-Methoden
|
|
71
|
+
der Proactor-Pipes (Windows asyncio + Playwright-Subprozess) — via
|
|
72
|
+
``warnings.filterwarnings``.
|
|
73
|
+
2. ``Exception ignored in: <function _ProactorBasePipeTransport.__del__>``
|
|
74
|
+
mit ``ValueError: I/O operation on closed pipe`` — via
|
|
75
|
+
``sys.unraisablehook``.
|
|
76
|
+
3. ``Future exception was never retrieved`` (z.B.
|
|
77
|
+
``net::ERR_ABORTED; maybe frame was detached?``) aus internen
|
|
78
|
+
Playwright-Tasks, die nach dem Browser-Close noch eine Antwort
|
|
79
|
+
erwarten — via ``loop.set_exception_handler``. Tritt vor allem im
|
|
80
|
+
Nuitka-Compilat auf, wo das Timing leicht anders ist.
|
|
81
|
+
"""
|
|
82
|
+
import asyncio
|
|
83
|
+
import warnings
|
|
84
|
+
|
|
85
|
+
warnings.filterwarnings("ignore", category=ResourceWarning, message=".*unclosed transport.*")
|
|
86
|
+
warnings.filterwarnings("ignore", category=ResourceWarning, message=".*subprocess.*")
|
|
87
|
+
|
|
88
|
+
prev = sys.unraisablehook
|
|
89
|
+
|
|
90
|
+
def _hook(unraisable: UnraisableHookArgs) -> None:
|
|
91
|
+
exc = unraisable.exc_value
|
|
92
|
+
if isinstance(exc, ValueError) and "closed pipe" in str(exc):
|
|
93
|
+
return
|
|
94
|
+
if isinstance(exc, ResourceWarning):
|
|
95
|
+
return
|
|
96
|
+
prev(unraisable)
|
|
97
|
+
|
|
98
|
+
sys.unraisablehook = _hook
|
|
99
|
+
|
|
100
|
+
def _loop_exc_handler(loop: asyncio.AbstractEventLoop, context: dict) -> None:
|
|
101
|
+
msg = str(context.get("message", ""))
|
|
102
|
+
exc = context.get("exception")
|
|
103
|
+
exc_msg = str(exc) if exc is not None else ""
|
|
104
|
+
if "never retrieved" in msg or "net::ERR_ABORTED" in exc_msg or "frame was detached" in exc_msg:
|
|
105
|
+
return # bekannte playwright/cleanup-Reste — ignorieren
|
|
106
|
+
loop.default_exception_handler(context)
|
|
107
|
+
|
|
108
|
+
# Asyncio nutzt den Event-Loop-Exception-Handler fuer
|
|
109
|
+
# "Future exception was never retrieved". Wir installieren ihn pro
|
|
110
|
+
# Loop, sobald ein Loop existiert — beim Programmstart ist meist
|
|
111
|
+
# noch keiner aktiv, deshalb haengen wir uns in den Policy-Loop-
|
|
112
|
+
# Factory-Mechanismus.
|
|
113
|
+
base_policy = asyncio.get_event_loop_policy()
|
|
114
|
+
|
|
115
|
+
class _PatchedPolicy(type(base_policy)): # type: ignore[misc]
|
|
116
|
+
def new_event_loop(self) -> asyncio.AbstractEventLoop:
|
|
117
|
+
loop = super().new_event_loop()
|
|
118
|
+
loop.set_exception_handler(_loop_exc_handler)
|
|
119
|
+
return loop
|
|
120
|
+
|
|
121
|
+
with contextlib.suppress(Exception):
|
|
122
|
+
asyncio.set_event_loop_policy(_PatchedPolicy())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _preinit_graphics() -> None:
|
|
126
|
+
"""Initialisiert textual-image vor dem App-Start.
|
|
127
|
+
|
|
128
|
+
textual-image sendet beim Import Terminal-Queries (DA1, Cell-Size).
|
|
129
|
+
Diese muessen laufen, bevor Textual stdin uebernimmt - sonst landen die
|
|
130
|
+
Antworten als Muell-Input in Textual-Widgets.
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
import textual_image.renderable # noqa: F401
|
|
134
|
+
import textual_image.widget # noqa: F401
|
|
135
|
+
from textual_image._terminal import get_cell_size
|
|
136
|
+
|
|
137
|
+
get_cell_size()
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main() -> None:
|
|
143
|
+
"""Haupteinstiegspunkt fuer die CLI."""
|
|
144
|
+
_enable_faulthandler()
|
|
145
|
+
# Einmalige Migration der User-Daten vom alten Tool-Namen
|
|
146
|
+
# (sitemap-generator) — muss VOR ``Settings.load()`` laufen.
|
|
147
|
+
_migrate_user_data()
|
|
148
|
+
|
|
149
|
+
# Sprache aus Settings laden (fuer CLI-Hilfe)
|
|
150
|
+
settings = Settings.load()
|
|
151
|
+
|
|
152
|
+
# Vorab --lang aus sys.argv parsen (vor argparse),
|
|
153
|
+
# damit Banner und Hilfetexte uebersetzt sind
|
|
154
|
+
lang = settings.language
|
|
155
|
+
for i, arg in enumerate(sys.argv[1:]):
|
|
156
|
+
if arg == "--lang" and i + 1 < len(sys.argv[1:]):
|
|
157
|
+
lang = sys.argv[i + 2]
|
|
158
|
+
break
|
|
159
|
+
if arg.startswith("--lang="):
|
|
160
|
+
lang = arg.split("=", 1)[1]
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
load_locale(lang)
|
|
164
|
+
|
|
165
|
+
# Sprache in Settings persistieren
|
|
166
|
+
if lang != settings.language:
|
|
167
|
+
settings.language = lang
|
|
168
|
+
settings.save()
|
|
169
|
+
|
|
170
|
+
parser = argparse.ArgumentParser(
|
|
171
|
+
prog="sitemap-tracker",
|
|
172
|
+
description=t("cli.banner", version=__version__),
|
|
173
|
+
epilog=t("cli.examples"),
|
|
174
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
parser.add_argument(
|
|
178
|
+
"url",
|
|
179
|
+
nargs="?",
|
|
180
|
+
default="",
|
|
181
|
+
metavar="URL",
|
|
182
|
+
help=t("cli.url_help"),
|
|
183
|
+
)
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"--output",
|
|
186
|
+
"-o",
|
|
187
|
+
default="",
|
|
188
|
+
metavar="PATH",
|
|
189
|
+
help=t("cli.output_help"),
|
|
190
|
+
)
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
"--max-depth",
|
|
193
|
+
"-d",
|
|
194
|
+
type=int,
|
|
195
|
+
default=None,
|
|
196
|
+
metavar="N",
|
|
197
|
+
help=t("cli.max_depth_help"),
|
|
198
|
+
)
|
|
199
|
+
parser.add_argument(
|
|
200
|
+
"--concurrency",
|
|
201
|
+
"-c",
|
|
202
|
+
type=int,
|
|
203
|
+
default=None,
|
|
204
|
+
metavar="N",
|
|
205
|
+
help=t("cli.concurrency_help"),
|
|
206
|
+
)
|
|
207
|
+
parser.add_argument(
|
|
208
|
+
"--timeout",
|
|
209
|
+
"-t",
|
|
210
|
+
type=int,
|
|
211
|
+
default=None,
|
|
212
|
+
metavar="SEC",
|
|
213
|
+
help=t("cli.timeout_help"),
|
|
214
|
+
)
|
|
215
|
+
parser.add_argument(
|
|
216
|
+
"--render",
|
|
217
|
+
action="store_true",
|
|
218
|
+
default=False,
|
|
219
|
+
help=t("cli.render_help"),
|
|
220
|
+
)
|
|
221
|
+
parser.add_argument(
|
|
222
|
+
"--no-headless",
|
|
223
|
+
action="store_true",
|
|
224
|
+
default=False,
|
|
225
|
+
help=t("cli.no_headless_help"),
|
|
226
|
+
)
|
|
227
|
+
parser.add_argument(
|
|
228
|
+
"--rate-limit",
|
|
229
|
+
type=int,
|
|
230
|
+
default=None,
|
|
231
|
+
metavar="N",
|
|
232
|
+
help=t("cli.rate_limit_help"),
|
|
233
|
+
)
|
|
234
|
+
parser.add_argument(
|
|
235
|
+
"--ignore-robots",
|
|
236
|
+
action="store_true",
|
|
237
|
+
default=False,
|
|
238
|
+
help=t("cli.ignore_robots_help"),
|
|
239
|
+
)
|
|
240
|
+
parser.add_argument(
|
|
241
|
+
"--user-agent",
|
|
242
|
+
default="",
|
|
243
|
+
metavar="UA",
|
|
244
|
+
help=t("cli.user_agent_help"),
|
|
245
|
+
)
|
|
246
|
+
parser.add_argument(
|
|
247
|
+
"--cookie",
|
|
248
|
+
action="append",
|
|
249
|
+
default=[],
|
|
250
|
+
metavar="NAME=VALUE",
|
|
251
|
+
help=t("cli.cookie_help"),
|
|
252
|
+
)
|
|
253
|
+
parser.add_argument(
|
|
254
|
+
"--lang",
|
|
255
|
+
default=lang,
|
|
256
|
+
choices=["de", "en"],
|
|
257
|
+
help="Sprache / Language (de, en)",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
args = parser.parse_args()
|
|
261
|
+
|
|
262
|
+
# Cookies parsen: "NAME=VALUE" -> {"name": "NAME", "value": "VALUE"}
|
|
263
|
+
cookies = []
|
|
264
|
+
for cookie_str in args.cookie:
|
|
265
|
+
if "=" not in cookie_str:
|
|
266
|
+
print(t("cli.invalid_cookie", cookie=cookie_str))
|
|
267
|
+
sys.exit(1)
|
|
268
|
+
name, value = cookie_str.split("=", 1)
|
|
269
|
+
cookies.append({"name": name.strip(), "value": value.strip()})
|
|
270
|
+
|
|
271
|
+
# Pruefen ob das Argument eine lokale XML-Datei ist
|
|
272
|
+
sitemap_file = ""
|
|
273
|
+
start_url = args.url
|
|
274
|
+
if start_url and os.path.isfile(start_url) and start_url.lower().endswith(".xml"):
|
|
275
|
+
sitemap_file = os.path.abspath(start_url)
|
|
276
|
+
start_url = "" # Wird aus der XML-Datei ermittelt
|
|
277
|
+
|
|
278
|
+
# Teardown-Rauschen aus playwright/asyncio auf Windows ausblenden.
|
|
279
|
+
_silence_windows_teardown_noise()
|
|
280
|
+
|
|
281
|
+
# textual-image vor App-Start initialisieren — IMMER, nicht nur bei
|
|
282
|
+
# aktivierter Vorschau. Das `PreviewPanel`-Widget wird im DOM auch dann
|
|
283
|
+
# erzeugt, wenn die Vorschau aus ist (nur unsichtbar), und sein
|
|
284
|
+
# __init__ kann textual-image lazy nachladen, sobald der User die
|
|
285
|
+
# Vorschau spaeter einschaltet. Wenn der Import erst nach Textual-
|
|
286
|
+
# Stdin-Hijack passiert, blutet die DA1/Cell-Size-Antwort des Terminals
|
|
287
|
+
# ins Eingabefeld. Einmal vorab importieren, fertig.
|
|
288
|
+
_preinit_graphics()
|
|
289
|
+
|
|
290
|
+
from sitemap_tracker.app import SitemapTrackerApp
|
|
291
|
+
|
|
292
|
+
# Terminal-Tab-Titel setzen - Textual macht das nicht selbst.
|
|
293
|
+
set_terminal_title(f"sitemap-tracker v{__version__}")
|
|
294
|
+
try:
|
|
295
|
+
app = SitemapTrackerApp(
|
|
296
|
+
start_url=start_url,
|
|
297
|
+
sitemap_file=sitemap_file,
|
|
298
|
+
output_path=args.output,
|
|
299
|
+
max_depth=args.max_depth,
|
|
300
|
+
concurrency=args.concurrency,
|
|
301
|
+
rate_per_minute=args.rate_limit,
|
|
302
|
+
timeout=args.timeout,
|
|
303
|
+
render=args.render,
|
|
304
|
+
headless=not args.no_headless,
|
|
305
|
+
respect_robots=not args.ignore_robots,
|
|
306
|
+
user_agent=args.user_agent,
|
|
307
|
+
cookies=cookies,
|
|
308
|
+
)
|
|
309
|
+
app.run()
|
|
310
|
+
finally:
|
|
311
|
+
reset_terminal_title()
|
|
312
|
+
_reset_mouse_tracking()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _enable_faulthandler() -> None:
|
|
316
|
+
"""Faengt HARTE Abstuerze ab, die an Pythons Exception-Handling UND am
|
|
317
|
+
finally-Block vorbeilaufen: native Access Violation, Stack-Overflow, fataler
|
|
318
|
+
Interpreter-Fehler.
|
|
319
|
+
|
|
320
|
+
faulthandler schreibt den Traceback aller Threads in fault.log - separat vom
|
|
321
|
+
Terminal, das der Maus-Tracking-Muell sonst unlesbar macht. CrashGuard und
|
|
322
|
+
_persist_crash greifen nur bei normalen Python-Exceptions, dieser Handler bei
|
|
323
|
+
allem darunter. Die Startzeile ist die zweite Haelfte der Diagnose: steht
|
|
324
|
+
danach nichts weiter in der Datei, wurde der Prozess von aussen abgeraeumt.
|
|
325
|
+
"""
|
|
326
|
+
import atexit
|
|
327
|
+
import contextlib
|
|
328
|
+
import faulthandler
|
|
329
|
+
from datetime import datetime
|
|
330
|
+
|
|
331
|
+
from .models.settings import SETTINGS_FILE
|
|
332
|
+
|
|
333
|
+
global _fault_log
|
|
334
|
+
with contextlib.suppress(Exception):
|
|
335
|
+
verzeichnis = SETTINGS_FILE.parent
|
|
336
|
+
verzeichnis.mkdir(parents=True, exist_ok=True)
|
|
337
|
+
# Bewusst offen lassen (Prozess-Lebensdauer) - faulthandler schreibt
|
|
338
|
+
# beim fatalen Signal direkt in dieses Handle.
|
|
339
|
+
_fault_log = open(verzeichnis / "fault.log", "a", encoding="utf-8") # noqa: SIM115
|
|
340
|
+
stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
341
|
+
_fault_log.write(f"\n===== Start {stamp} - v{__version__} =====\n")
|
|
342
|
+
_fault_log.flush()
|
|
343
|
+
faulthandler.enable(file=_fault_log, all_threads=True)
|
|
344
|
+
# Gegenstueck zur Startzeile - siehe _write_fault_end.
|
|
345
|
+
atexit.register(_write_fault_end)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _reset_mouse_tracking() -> None:
|
|
349
|
+
"""Schaltet alle Maus-Tracking-Modi des Terminals ab (idempotent).
|
|
350
|
+
|
|
351
|
+
Schreibt bewusst nach sys.__stdout__, NICHT sys.stdout: Textual kapert
|
|
352
|
+
sys.stdout zur Laufzeit, ein Reset dorthin landet im Nichts und das Terminal
|
|
353
|
+
bleibt im Maus-Tracking-Modus haengen. Der garantierte Reset passiert im
|
|
354
|
+
Shell-Wrapper run.ps1 (laeuft auch nach einem harten Crash) - das hier ist
|
|
355
|
+
der zusaetzliche In-Prozess-Pfad.
|
|
356
|
+
"""
|
|
357
|
+
stream = sys.__stdout__
|
|
358
|
+
if stream is None or not stream.isatty():
|
|
359
|
+
return
|
|
360
|
+
stream.write("\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l")
|
|
361
|
+
stream.flush()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _write_fault_end() -> None:
|
|
365
|
+
"""Schreibt die Endzeile der Sitzungsklammer (ueber atexit registriert).
|
|
366
|
+
|
|
367
|
+
Erst dieses Gegenstueck zur Startzeile macht die Datei aussagekraeftig:
|
|
368
|
+
|
|
369
|
+
Start + Ende -> sauber beendet
|
|
370
|
+
Start + Traceback -> Python-Fehler (der Handler hat ihn gesehen)
|
|
371
|
+
Start und sonst nichts -> Prozess hart abgeraeumt
|
|
372
|
+
|
|
373
|
+
Unter Windows hilft ein Signalhandler dabei nicht: ein Abbruch von aussen
|
|
374
|
+
laeuft dort ueber TerminateProcess und liefert dem Ziel kein abfangbares
|
|
375
|
+
Signal. Die FEHLENDE Endzeile ist der einzige Beleg.
|
|
376
|
+
"""
|
|
377
|
+
import contextlib
|
|
378
|
+
from datetime import datetime
|
|
379
|
+
|
|
380
|
+
if _fault_log is None:
|
|
381
|
+
return
|
|
382
|
+
with contextlib.suppress(Exception):
|
|
383
|
+
stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
384
|
+
_fault_log.write(f"===== Ende {stamp} =====\n")
|
|
385
|
+
_fault_log.flush()
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
if __name__ == "__main__":
|
|
389
|
+
main()
|