From 5a1a095e166bd06f2cb9a945b21c0df82a73531a Mon Sep 17 00:00:00 2001 From: Chuck Date: Thu, 11 Jun 2026 12:05:47 -0400 Subject: [PATCH] fix(install): bound subprocess output and dedupe apt update in dependency installer Address coderabbitai review on PR #369: - _run() now streams combined stdout/stderr to a temp file and returns only the last ERROR_TAIL_LINES lines, instead of buffering full output in memory (Codacy also flagged the previous capture_output call as a subprocess-without-static-string security issue; the new call is annotated as safe since cmd is built from hardcoded args). - `apt update` now runs once in main() instead of once per package needing an apt fallback. --- scripts/install_dependencies_apt.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/scripts/install_dependencies_apt.py b/scripts/install_dependencies_apt.py index 41730844..21d07985 100644 --- a/scripts/install_dependencies_apt.py +++ b/scripts/install_dependencies_apt.py @@ -6,6 +6,7 @@ then falls back to pip with --break-system-packages import subprocess import sys +import tempfile import warnings from pathlib import Path @@ -16,13 +17,18 @@ ERROR_TAIL_LINES = 15 def _run(cmd): - """Run a command, capturing combined stdout/stderr. + """Run a command, streaming combined stdout/stderr to a temp file. Returns (success, output) instead of raising, so callers can report - *why* a command failed rather than just that it failed. + *why* a command failed rather than just that it failed. `output` is + bounded to the last ERROR_TAIL_LINES lines so failures from very + chatty commands (e.g. pip build logs) don't get buffered in memory. """ - result = subprocess.run(cmd, capture_output=True, text=True) - return result.returncode == 0, (result.stdout or "") + (result.stderr or "") + with tempfile.TemporaryFile(mode='w+b') as f: + result = subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT) # nosec B603 B607 - hardcoded apt/pip args, not user input + f.seek(0) + lines = f.read().decode('utf-8', errors='replace').splitlines() + return result.returncode == 0, '\n'.join(lines[-ERROR_TAIL_LINES:]) def install_via_apt(package_name): @@ -47,8 +53,6 @@ def install_via_apt(package_name): apt_package = apt_package_map.get(package_name, f'python3-{package_name}') print(f"Trying to install {apt_package} via apt...") - _run(['sudo', 'apt', 'update']) # best-effort refresh; ignore failures here - success, output = _run(['sudo', 'apt', 'install', '-y', apt_package]) if success: print(f"Successfully installed {apt_package} via apt") @@ -113,6 +117,9 @@ def main(): """Main installation function.""" print("Installing dependencies for LED Matrix Web Interface V2...") + print("Refreshing apt package index...") + _run(['sudo', 'apt', 'update']) # best-effort; individual installs surface their own errors + # List of required packages required_packages = [ 'flask',