mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 10:36:35 +00:00
A General-tab toggle (off by default) checks for and installs LEDMatrix and plugin updates once a week, overnight in the configured timezone. - Pre-update checks skip (and report) instead of forcing: local edits or commits, merge/live rebase, no upstream, low disk, missing health check, or a version that was already rolled back. An abandoned rebase (HEAD back on a branch) is cleared, since it would otherwise block every pull. - The pull reuses the Update Code path (now perform_core_update(), which reports dependency install failures as data). - ledmatrix-update-verify.service, started via a .path unit from a request file, restarts the services from its own cgroup, requires them to come up and stay up, and otherwise resets to the previous commit and reinstalls the previous requirements. It runs a copy of the checker taken before the pull. - No SSH needed: switching the toggle on restarts the display service, which (as root) installs the two units from the repo templates for the web user. first_time_install.sh installs them too and takes --enable-auto-update / LEDMATRIX_AUTO_UPDATE (passed through by one-shot-install.sh). - Plugins update after the code passes its check; failures, blocks and rollbacks raise an Overview banner and show under the toggle. Tested end to end on a Pi: web-UI setup, a good update, a broken web service and a broken display (both rolled back), a blocked local edit, and an abandoned rebase found on the device. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
142 lines
5.1 KiB
Python
142 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
LED Matrix Web Interface V3 Startup Script
|
|
Modern web interface with real-time display preview and plugin management.
|
|
"""
|
|
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
def get_local_ips():
|
|
"""Get list of local IP addresses the service will be accessible on."""
|
|
ips = []
|
|
|
|
# Check if AP mode is active
|
|
try:
|
|
result = subprocess.run(
|
|
["systemctl", "is-active", "hostapd"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=2
|
|
)
|
|
if result.returncode == 0 and result.stdout.strip() == "active":
|
|
ips.append("192.168.4.1 (AP Mode)")
|
|
except Exception: # nosec B110 - AP mode IP detection is non-critical startup info; systemctl may not exist
|
|
pass
|
|
|
|
# Get IPs from hostname -I
|
|
try:
|
|
result = subprocess.run(
|
|
["hostname", "-I"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=2
|
|
)
|
|
if result.returncode == 0:
|
|
for ip in result.stdout.strip().split():
|
|
ip = ip.strip()
|
|
if ip and not ip.startswith("127.") and ip != "192.168.4.1":
|
|
ips.append(ip)
|
|
except Exception: # nosec B110 - hostname -I output parsing; non-critical startup info
|
|
pass
|
|
|
|
# Fallback: try socket method
|
|
if not ips:
|
|
try:
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
try:
|
|
s.connect(('8.8.8.8', 80))
|
|
ip = s.getsockname()[0]
|
|
if ip and not ip.startswith("127."):
|
|
ips.append(ip)
|
|
finally:
|
|
s.close()
|
|
except Exception:
|
|
pass
|
|
|
|
return ips if ips else ["localhost"]
|
|
|
|
def main():
|
|
"""Main startup function."""
|
|
# Change to project root directory
|
|
project_root = Path(__file__).parent.parent
|
|
os.chdir(project_root)
|
|
|
|
# Add to Python path
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
# Configure logging to suppress non-critical socket errors
|
|
# These occur when clients disconnect and are harmless
|
|
werkzeug_logger = logging.getLogger('werkzeug')
|
|
original_log_exception = werkzeug_logger.error
|
|
|
|
def log_exception_filtered(message, *args, **kwargs):
|
|
"""Filter out non-critical socket errors from werkzeug logs."""
|
|
if isinstance(message, str):
|
|
# Suppress "No route to host" and similar connection errors
|
|
if 'No route to host' in message or 'errno 113' in message:
|
|
# Log at debug level instead of error
|
|
werkzeug_logger.debug(message, *args, **kwargs)
|
|
return
|
|
# Suppress broken pipe errors (client disconnected)
|
|
if 'Broken pipe' in message or 'errno 32' in message:
|
|
werkzeug_logger.debug(message, *args, **kwargs)
|
|
return
|
|
# For exceptions, check if it's a socket error
|
|
if 'exc_info' in kwargs and kwargs['exc_info']:
|
|
exc_type, exc_value, exc_tb = kwargs['exc_info']
|
|
if isinstance(exc_value, OSError):
|
|
# Suppress common non-critical socket errors
|
|
if exc_value.errno in (113, 32, 104): # No route to host, Broken pipe, Connection reset
|
|
werkzeug_logger.debug(message, *args, **kwargs)
|
|
return
|
|
# Log everything else normally
|
|
original_log_exception(message, *args, **kwargs)
|
|
|
|
werkzeug_logger.error = log_exception_filtered
|
|
|
|
# Import and run the Flask app
|
|
from web_interface.app import app, start_auto_update_scheduler
|
|
start_auto_update_scheduler()
|
|
|
|
print("Starting LED Matrix Web Interface V3...")
|
|
print("Web server binding to: 0.0.0.0:5000")
|
|
|
|
# Get and display accessible IP addresses
|
|
ips = get_local_ips()
|
|
if ips:
|
|
print("Access the interface at:")
|
|
for ip in ips:
|
|
if "AP Mode" in ip:
|
|
print(" - http://192.168.4.1:5000 (AP Mode - connect to LEDMatrix-Setup WiFi)")
|
|
else:
|
|
print(f" - http://{ip}:5000")
|
|
else:
|
|
print(" - http://localhost:5000 (local only)")
|
|
print(" - http://<your-pi-ip>:5000 (replace with your Pi's IP address)")
|
|
|
|
# Run the web server with error handling for client disconnections
|
|
try:
|
|
# threaded=True is Flask's default since 1.0, but set it explicitly
|
|
# so it's self-documenting: the two /api/v3/stream/* SSE endpoints
|
|
# hold long-lived connections and would starve other requests under
|
|
# a single-threaded server.
|
|
app.run(host='0.0.0.0', port=5000, debug=False, threaded=True)
|
|
except (OSError, BrokenPipeError) as e:
|
|
# Suppress non-critical socket errors (client disconnections)
|
|
if isinstance(e, OSError) and e.errno in (113, 32, 104): # No route to host, Broken pipe, Connection reset
|
|
werkzeug_logger.debug(f"Client disconnected: {e}", exc_info=True)
|
|
# Re-raise only if it's not a client disconnection error
|
|
if e.errno not in (113, 32, 104):
|
|
raise
|
|
else:
|
|
raise
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|