mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
* fix(web): unify operation history tracking for monorepo plugin operations The operation history UI was reading from the wrong data source (operation_queue instead of operation_history), install/update records lacked version details, toggle operations used a type name that didn't match UI filters, and the Clear History button was non-functional. - Switch GET /plugins/operation/history to read from OperationHistory audit log with return type hint and targeted exception handling - Add DELETE /plugins/operation/history endpoint; wire up Clear button - Add _get_plugin_version helper with specific exception handling (FileNotFoundError, PermissionError, json.JSONDecodeError) and structured logging with plugin_id/path context - Record plugin version, branch, and commit details on install/update - Record install failures in the direct (non-queue) code path - Replace "toggle" operation type with "enable"/"disable" - Add normalizeStatus() in JS to map completed→success, error→failed so status filter works regardless of server-side convention - Truncate commit SHAs to 7 chars in details display - Fix HTML filter options, operation type colors, duplicate JS init Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(plugins): prevent root-owned files from blocking plugin updates The root ledmatrix service creates __pycache__ and data cache files owned by root inside plugin directories. The web service (non-root) cannot delete these when updating or uninstalling plugins, causing operations to fail with "Permission denied". Defense in depth with three layers: - Prevent: PYTHONDONTWRITEBYTECODE=1 in systemd service + run.py - Fallback: sudoers rules for rm on plugin directories - Code: _safe_remove_directory() now uses sudo as last resort, and all bare shutil.rmtree() calls routed through it Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): harden sudo removal with path-validated helper script Address code review findings: - Replace raw rm/find sudoers wildcards with a vetted helper script (safe_plugin_rm.sh) that resolves symlinks and validates the target is a strict child of plugin-repos/ or plugins/ before deletion - Add allow-list validation in sudo_remove_directory() that checks resolved paths against allowed bases before invoking sudo - Check _safe_remove_directory() return value before shutil.move() in the manifest ID rename path - Move stat import to module level in store_manager.py - Use stat.S_IRWXU instead of 0o777 in chmod fallback stage - Add ignore_errors=True to temp dir cleanup in finally block - Use command -v instead of which in configure_web_sudo.sh Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): address code review round 2 — harden paths and error handling - safe_plugin_rm.sh: use realpath --canonicalize-missing for ALLOWED_BASES so the script doesn't fail under set -e when dirs don't exist yet - safe_plugin_rm.sh: add -- before path in rm -rf to prevent flag injection - permission_utils.py: use shutil.which('bash') instead of hardcoded /bin/bash to match whatever path the sudoers BASH_PATH resolves to - store_manager.py: check _safe_remove_directory() return before shutil.move() in _install_from_monorepo_zip to prevent moving into a non-removed target - store_manager.py: catch OSError instead of PermissionError in Stage 1 removal to handle both EACCES and EPERM error codes - store_manager.py: hoist sudo_remove_directory import to module level - configure_web_sudo.sh: harden safe_plugin_rm.sh to root-owned 755 so the web user cannot modify the vetted helper script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): validate command paths in sudoers config and use resolved paths - configure_web_sudo.sh: validate that required commands (systemctl, bash, python3) resolve to non-empty paths before generating sudoers entries; abort with clear error if any are missing; skip optional commands (reboot, poweroff, journalctl) with a warning instead of emitting malformed NOPASSWD lines; validate helper script exists on disk - permission_utils.py: pass the already-resolved path to the subprocess call and use it for the post-removal exists() check, eliminating a TOCTOU window between Python-side validation and shell-side execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
74 lines
3.1 KiB
Python
Executable File
74 lines
3.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import logging
|
|
import sys
|
|
import os
|
|
import argparse
|
|
|
|
# Prevent Python from creating __pycache__ directories in plugin dirs.
|
|
# The root service loads plugins via importlib, and root-owned __pycache__
|
|
# files block the web service (non-root) from updating/uninstalling plugins.
|
|
sys.dont_write_bytecode = True
|
|
|
|
# Add project directory to Python path (needed before importing src modules)
|
|
project_dir = os.path.dirname(os.path.abspath(__file__))
|
|
if project_dir not in sys.path:
|
|
sys.path.insert(0, project_dir)
|
|
|
|
# Parse command-line arguments BEFORE any imports
|
|
parser = argparse.ArgumentParser(description='LEDMatrix Display Controller')
|
|
parser.add_argument('-e', '--emulator', action='store_true',
|
|
help='Run in emulator mode (uses pygame/RGBMatrixEmulator instead of hardware)')
|
|
parser.add_argument('-d', '--debug', action='store_true',
|
|
help='Enable debug logging and verbose output')
|
|
args = parser.parse_args()
|
|
|
|
# Set emulator mode if requested (must be done BEFORE any imports that check EMULATOR env var)
|
|
if args.emulator:
|
|
os.environ["EMULATOR"] = "true"
|
|
print("=" * 60)
|
|
print("LEDMatrix Emulator Mode Enabled")
|
|
print("=" * 60)
|
|
print("Using pygame/RGBMatrixEmulator for display")
|
|
print("Press ESC to exit\n")
|
|
|
|
# Project directory already added above
|
|
|
|
# Debug output (only in debug mode or emulator mode)
|
|
debug_mode = args.debug or args.emulator or os.environ.get('LEDMATRIX_DEBUG', '').lower() == 'true'
|
|
if debug_mode:
|
|
print(f"DEBUG: Project directory: {project_dir}", flush=True)
|
|
print(f"DEBUG: Python path[0]: {sys.path[0]}", flush=True)
|
|
print(f"DEBUG: Current working directory: {os.getcwd()}", flush=True)
|
|
print(f"DEBUG: EMULATOR mode: {os.environ.get('EMULATOR', 'false')}", flush=True)
|
|
|
|
# Additional debugging for plugin system (only in debug mode)
|
|
if debug_mode:
|
|
try:
|
|
plugin_system_path = os.path.join(project_dir, 'src', 'plugin_system')
|
|
if plugin_system_path not in sys.path:
|
|
sys.path.insert(0, plugin_system_path)
|
|
print(f"DEBUG: Added plugin_system path to sys.path: {plugin_system_path}", flush=True)
|
|
|
|
# Try to import the plugin system directly to get better error info
|
|
print("DEBUG: Attempting to import src.plugin_system...", flush=True)
|
|
from src.plugin_system import PluginManager
|
|
print("DEBUG: Plugin system import successful", flush=True)
|
|
except ImportError as e:
|
|
print(f"DEBUG: Plugin system import failed: {e}", flush=True)
|
|
print(f"DEBUG: Import error details: {type(e).__name__}", flush=True)
|
|
except Exception as e:
|
|
print(f"DEBUG: Unexpected error during plugin system import: {e}", flush=True)
|
|
|
|
# Configure logging before importing any other modules
|
|
# Use centralized logging configuration
|
|
from src.logging_config import setup_logging
|
|
|
|
log_level = logging.DEBUG if debug_mode else logging.INFO
|
|
format_type = 'readable' # Use 'json' for structured logging in production
|
|
setup_logging(level=log_level, format_type=format_type, include_location=debug_mode)
|
|
|
|
# Now import the display controller
|
|
from src.display_controller import main
|
|
|
|
if __name__ == "__main__":
|
|
main() |