From 5f7dc7d831afe206956b0d96d9d083e908cf31cf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 22:19:07 -0400 Subject: [PATCH] fix(flights): stop writing debug images on every map composite _get_map_background() saved two PNGs every time it composed a map, with nothing gating it -- no debug flag, no config check -- under a comment reading "Debug: Save composite image to see what's happening". Left-over debugging shipped as production behaviour. Measured on a live rig: debug_composite.png 5.36 MB debug_cropped.png 0.04 MB composites in 6h 5 That is roughly 108 MB a day written to an SD card for files nothing reads, on a device whose cards have already failed twice with unreadable-block-device symptoms. It also gets worse, not better, with the change that makes this plugin honour the ticker's narrower render request: composing at two widths instead of one roughly doubles the count. They also landed in the process's working directory, which is the install root, where a pre-commit hook has previously swept them into a commit. Now written only when debug logging is enabled, and into the plugin's own tile cache directory rather than wherever the process happens to be running. For comparison, the health-record churn fixed in the core repo was ~6,300 small writes a day; this is 108 MB of large ones from a single plugin. test_no_debug_image_writes.py walks the module's AST and asserts every save() sits under an isEnabledFor(DEBUG) guard and that no bare working-directory path remains. Mutation-checked: ungating the writes fails two checks, and restoring the bare CWD path fails another. All 10 flights suites pass. --- plugins.json | 4 +- plugins/ledmatrix-flights/manager.py | 34 +++++--- plugins/ledmatrix-flights/manifest.json | 8 +- .../test_no_debug_image_writes.py | 83 +++++++++++++++++++ 4 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 plugins/ledmatrix-flights/test_no_debug_image_writes.py diff --git a/plugins.json b/plugins.json index fe71fed6..cdf6b706 100644 --- a/plugins.json +++ b/plugins.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-08-20", + "last_updated": "2026-08-19", "plugins": [ { "id": "cricket-scoreboard", @@ -439,7 +439,7 @@ "last_updated": "2026-08-03", "verified": true, "screenshot": "", - "latest_version": "1.12.13" + "latest_version": "1.12.14" }, { "id": "march-madness", diff --git a/plugins/ledmatrix-flights/manager.py b/plugins/ledmatrix-flights/manager.py index 06385747..6a2eb31a 100644 --- a/plugins/ledmatrix-flights/manager.py +++ b/plugins/ledmatrix-flights/manager.py @@ -2406,17 +2406,29 @@ def _get_map_background(self, center_lat: float, center_lon: float, self.logger.debug(f"[Flight Tracker] Map displays {desired_miles_wide:.1f} miles wide x {desired_miles_high:.1f} miles high (no stretching)") self.logger.debug(f"[Flight Tracker] Native tile scale: {pixels_per_mile_at_zoom:.3f} pixels/mile, cropped {crop_width_needed}x{crop_height_needed} pixels, scaled to {self.display_width}x{self.display_height}") - # Debug: Save composite image to see what's happening - try: - debug_composite = Path("debug_composite.png") - composite.save(debug_composite) - self.logger.debug(f"[Flight Tracker] Saved composite to: {debug_composite}") - - debug_cropped = Path("debug_cropped.png") - cropped.save(debug_cropped) - self.logger.debug(f"[Flight Tracker] Saved cropped to: {debug_cropped}") - except Exception as e: - self.logger.debug(f"[Flight Tracker] Could not save debug images: {e}") + # Debug aid, off unless debug logging is on. This used to run + # unconditionally, writing both PNGs on every composite: measured on a + # live rig at 5.36 MB for the composite and 0.04 MB for the crop, five + # composites in six hours -- about 108 MB a day written to an SD card + # for files nothing reads. They also landed in the process's working + # directory, which is the install root, where a pre-commit hook has + # previously swept them into a commit. + if self.logger.isEnabledFor(logging.DEBUG): + debug_dir = getattr(self, "tile_cache_dir", None) + if debug_dir is None: + self.logger.debug( + "[Flight Tracker] No cache directory; skipping debug images") + else: + try: + composite_path = Path(debug_dir) / "debug_composite.png" + composite.save(composite_path) + cropped_path = Path(debug_dir) / "debug_cropped.png" + cropped.save(cropped_path) + self.logger.debug( + "[Flight Tracker] Saved debug images to: %s", debug_dir) + except OSError as e: + self.logger.debug( + f"[Flight Tracker] Could not save debug images: {e}") return cropped diff --git a/plugins/ledmatrix-flights/manifest.json b/plugins/ledmatrix-flights/manifest.json index a293588b..fbe8ad66 100644 --- a/plugins/ledmatrix-flights/manifest.json +++ b/plugins/ledmatrix-flights/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-flights", "name": "Flight Tracker", - "version": "1.12.13", + "version": "1.12.14", "description": "Real-time aircraft tracking with ADS-B/FlightRadar24/OpenSky/adsb.fi/adsb.lol data, map backgrounds, area mode, flight tracking, anchor airport, flight records, and optional airport weather (METAR/TAF/PIREP/SIGMET via the free NOAA Aviation Weather Center API)", "author": "ChuckBuilds", "entry_point": "manager.py", @@ -37,6 +37,12 @@ "min_ledmatrix_version": "2.0.0", "max_ledmatrix_version": "3.0.0", "versions": [ + { + "version": "1.12.14", + "released": "2026-08-19", + "ledmatrix_min_version": "2.0.0", + "notes": "Stop writing debug images on every map composite. Two PNGs were saved unconditionally -- no debug flag, no config check, under a comment reading \"Debug: Save composite image to see what's happening\". Measured on a live rig they are 5.36 MB and 0.04 MB, written five times in six hours: about 108 MB a day onto an SD card for files nothing reads. They also landed in the process working directory, which is the install root. They are now written only when debug logging is enabled, and into the plugin's own tile cache directory." + }, { "version": "1.12.13", "released": "2026-08-19", diff --git a/plugins/ledmatrix-flights/test_no_debug_image_writes.py b/plugins/ledmatrix-flights/test_no_debug_image_writes.py new file mode 100644 index 00000000..dff0249d --- /dev/null +++ b/plugins/ledmatrix-flights/test_no_debug_image_writes.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Composing the map must not write debug PNGs unless debug logging is on. + +_get_map_background() saved two images on every composite, ungated -- no debug +flag, no config check, and the comment above it read "Debug: Save composite +image to see what's happening". Left-over debugging shipped as production +behaviour. + +Measured on a live rig: debug_composite.png is 5.36 MB and debug_cropped.png +0.04 MB, with five composites in six hours. That is roughly 108 MB a day +written to an SD card for files nothing reads, on a device whose cards have +already failed twice. Honouring the ticker's narrower render request makes the +plugin compose at two widths rather than one, so the count roughly doubles. + +They also landed in the process's working directory -- the install root -- +where a pre-commit hook has previously swept them into a commit. + +Run: /bin/python plugins/ledmatrix-flights/test_no_debug_image_writes.py +""" + +import ast +import sys +from pathlib import Path + +PLUGIN = Path(__file__).resolve().parent +MANAGER = PLUGIN / "manager.py" +failures = [] + + +def check(label, ok): + print((" PASS " if ok else " FAIL ") + label) + if not ok: + failures.append(label) + + +def _save_calls(tree): + """Every `.save(...)` in the module, with its line number.""" + return [n for n in ast.walk(tree) + if isinstance(n, ast.Call) + and getattr(n.func, "attr", None) == "save"] + + +def _guarding_debug_check(tree, lineno): + """True when the statement at `lineno` sits under an isEnabledFor guard.""" + for node in ast.walk(tree): + if not isinstance(node, ast.If): + continue + test = ast.dump(node.test) + if "isEnabledFor" not in test: + continue + for child in ast.walk(node): + if getattr(child, "lineno", None) == lineno: + return True + return False + + +def main(): + tree = ast.parse(MANAGER.read_text(encoding="utf-8")) + + print("debug images are written only when debug logging is enabled") + saves = _save_calls(tree) + check(f"{len(saves)} save() call(s) found in manager.py", bool(saves)) + for call in saves: + guarded = _guarding_debug_check(tree, call.lineno) + check(f"the save at line {call.lineno} is behind a debug-level guard", + guarded) + + print("\nand not into the process working directory") + source = MANAGER.read_text(encoding="utf-8") + check('no bare Path("debug_composite.png")', + 'Path("debug_composite.png")' not in source) + check('no bare Path("debug_cropped.png")', + 'Path("debug_cropped.png")' not in source) + check("debug images go under the tile cache directory", + "tile_cache_dir" in source.split("debug_composite.png")[0][-600:]) + + print("\n%s" % ("FAILED: %d" % len(failures) if failures + else "All checks passed")) + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main())