diff --git a/plugin-repos/march-madness/requirements.txt b/plugin-repos/march-madness/requirements.txt index 9f2bf55d..d1d90878 100644 --- a/plugin-repos/march-madness/requirements.txt +++ b/plugin-repos/march-madness/requirements.txt @@ -1,5 +1,5 @@ requests>=2.33.0 -urllib3>=2.2.2 +urllib3>=2.6.3 Pillow>=12.2.0 pytz>=2022.1 numpy>=1.24.0 diff --git a/src/display_manager.py b/src/display_manager.py index c296bc49..9519a4c7 100644 --- a/src/display_manager.py +++ b/src/display_manager.py @@ -1,4 +1,6 @@ +import json import os +import tempfile if os.getenv("EMULATOR", "false") == "true": from RGBMatrixEmulator import RGBMatrix, RGBMatrixOptions else: @@ -175,14 +177,28 @@ class DisplayManager: # Do not raise here; allow fallback mode so web preview and non-hardware environments work # Write hardware status file so the web UI can surface init failures + _hw_status = {"ok": self.matrix is not None, "error": _init_error_str} + _status_path = "/tmp/led_matrix_hw_status.json" # nosec B108 try: - import json as _json - _hw_status = {"ok": self.matrix is not None, "error": _init_error_str} - _status_path = "/tmp/led_matrix_hw_status.json" # nosec B108 - with open(_status_path, "w") as _f: - _json.dump(_hw_status, _f) + if os.path.islink(_status_path): + logger.warning("Skipping hardware status write: %s is a symlink", _status_path) + else: + _fd, _tmp_path = tempfile.mkstemp(dir="/tmp", prefix=".led_hw_") # nosec B108 + try: + with os.fdopen(_fd, "w") as _f: + json.dump(_hw_status, _f) + _f.flush() + os.fsync(_f.fileno()) + os.chmod(_tmp_path, 0o600) + os.replace(_tmp_path, _status_path) + except Exception: + try: + os.unlink(_tmp_path) + except OSError: + pass + raise except Exception: - pass + logger.error("Failed to write hardware status file", exc_info=True) @property def width(self): @@ -764,8 +780,8 @@ class DisplayManager: try: self.image = Image.new('RGB', (self.width, self.height)) self.draw = ImageDraw.Draw(self.image) - except Exception: # nosec B110 - best-effort canvas reset during cleanup; non-critical - pass + except (OSError, RuntimeError, ValueError, MemoryError): + logger.debug("Canvas reset during cleanup failed", exc_info=True) # Reset the singleton state when cleaning up DisplayManager._instance = None DisplayManager._initialized = False diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index b59e75be..58c4fe68 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -1545,8 +1545,12 @@ def get_hardware_status(): return jsonify({"status": "success", "data": hw_data}) except FileNotFoundError: return jsonify({"status": "success", "data": {"ok": None, "error": "Display service not yet started"}}) - except Exception as e: - return jsonify({"status": "error", "message": str(e)}), 500 + except (json.JSONDecodeError, PermissionError): + logger.error("Failed to read hardware status file", exc_info=True) + return jsonify({"status": "error", "message": "Unable to read hardware status"}), 500 + except Exception: + logger.error("Unexpected error reading hardware status", exc_info=True) + return jsonify({"status": "error", "message": "Unable to read hardware status"}), 500 @api_v3.route('/display/current', methods=['GET']) def get_display_current():