fix(logs): include ledmatrix-web logs in viewer and log subprocess stderr on failure

Two bugs conspired to produce "check the logs" toasts with an empty log viewer:

1. The log viewer (both SSE stream and REST endpoint) only queried
   ledmatrix.service via journalctl. Web API errors are logged by the
   Flask process running as ledmatrix-web.service, so they never
   appeared in the viewer. Add -u ledmatrix-web.service to both calls;
   also add --output=short-iso so timestamps from the two services
   sort cleanly when interleaved. Use shutil.which-resolved absolute
   paths for sudo/journalctl (S607 compliance) in api_v3.py; fall back
   to known Pi paths if which returns None.

2. app.py: resolve journalctl and systemctl to absolute paths via
   shutil.which at module init (_JOURNALCTL, _SYSTEMCTL). Replace bare
   names in logs_generator() and the cached systemctl is-active check.
   Guard both sites: logs_generator yields a clear SSE error message
   and sleeps 60 s if journalctl is not found; the systemctl block is
   skipped entirely if systemctl is not found, leaving the cache at its
   last-known value.

3. When execute_system_action() ran a systemctl command that returned
   non-zero, only the return code was logged — result.stderr was
   silently discarded. Log it at ERROR level and include returncode and
   stderr in the JSON response so callers get actionable failure details.
   Same fix applied to the early-return start_display branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-05-25 14:40:30 -04:00
parent 0c7d03a476
commit 0e54eaf4dd
2 changed files with 43 additions and 14 deletions

View File

@@ -3,6 +3,7 @@ import json
import logging import logging
import os import os
import queue import queue
import shutil
import sys import sys
import subprocess import subprocess
import threading import threading
@@ -24,6 +25,9 @@ from src.plugin_system.state_manager import PluginStateManager
from src.plugin_system.operation_history import OperationHistory from src.plugin_system.operation_history import OperationHistory
from src.plugin_system.health_monitor import PluginHealthMonitor from src.plugin_system.health_monitor import PluginHealthMonitor
_JOURNALCTL = shutil.which('journalctl')
_SYSTEMCTL = shutil.which('systemctl')
# Create Flask app # Create Flask app
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.urandom(24) app.secret_key = os.urandom(24)
@@ -492,8 +496,9 @@ def system_status_generator():
# Check if display service is running (cached to avoid per-client subprocess forks) # Check if display service is running (cached to avoid per-client subprocess forks)
now = time.time() now = time.time()
if (now - _ledmatrix_service_cache['timestamp']) >= _LEDMATRIX_SERVICE_CACHE_TTL: if (now - _ledmatrix_service_cache['timestamp']) >= _LEDMATRIX_SERVICE_CACHE_TTL:
if _SYSTEMCTL:
try: try:
result = subprocess.run(['systemctl', 'is-active', 'ledmatrix'], result = subprocess.run([_SYSTEMCTL, 'is-active', 'ledmatrix'],
capture_output=True, text=True, timeout=2) capture_output=True, text=True, timeout=2)
_ledmatrix_service_cache['active'] = result.stdout.strip() == 'active' _ledmatrix_service_cache['active'] = result.stdout.strip() == 'active'
except (subprocess.SubprocessError, OSError): except (subprocess.SubprocessError, OSError):
@@ -589,8 +594,13 @@ def logs_generator():
# Get recent logs from journalctl (simplified version) # Get recent logs from journalctl (simplified version)
# Note: User should be in systemd-journal group to read logs without sudo # Note: User should be in systemd-journal group to read logs without sudo
try: try:
if not _JOURNALCTL:
yield {'timestamp': time.time(), 'logs': 'journalctl not found; cannot read logs'}
time.sleep(60)
continue
result = subprocess.run( result = subprocess.run(
['journalctl', '-u', 'ledmatrix.service', '-n', '50', '--no-pager'], [_JOURNALCTL, '-u', 'ledmatrix.service', '-u', 'ledmatrix-web.service',
'-n', '50', '--no-pager', '--output=short-iso'],
capture_output=True, text=True, timeout=5 capture_output=True, text=True, timeout=5
) )
@@ -606,7 +616,7 @@ def logs_generator():
# No logs available # No logs available
logs_data = { logs_data = {
'timestamp': time.time(), 'timestamp': time.time(),
'logs': 'No logs available from ledmatrix service' 'logs': 'No logs available from ledmatrix or ledmatrix-web service'
} }
yield logs_data yield logs_data
else: else:

View File

@@ -4,6 +4,7 @@ import os
import re import re
import stat import stat
import sys import sys
import shutil
import subprocess import subprocess
import tempfile import tempfile
import time import time
@@ -25,6 +26,9 @@ from src.web_interface.validators import (
) )
from src.error_aggregator import get_error_aggregator from src.error_aggregator import get_error_aggregator
_SUDO = shutil.which('sudo')
_JOURNALCTL = shutil.which('journalctl')
# Will be initialized when blueprint is registered # Will be initialized when blueprint is registered
config_manager = None config_manager = None
plugin_manager = None plugin_manager = None
@@ -1459,10 +1463,16 @@ def execute_system_action():
result = subprocess.run(['sudo', 'systemctl', 'start', 'ledmatrix'], result = subprocess.run(['sudo', 'systemctl', 'start', 'ledmatrix'],
capture_output=True, text=True) capture_output=True, text=True)
logger.info("start_display (%s) returned code %d", mode, result.returncode) logger.info("start_display (%s) returned code %d", mode, result.returncode)
return jsonify({ if result.returncode != 0 and result.stderr:
logger.error("start_display (%s) stderr: %s", mode, result.stderr.strip())
resp = {
'status': 'success' if result.returncode == 0 else 'error', 'status': 'success' if result.returncode == 0 else 'error',
'message': 'Display started' if result.returncode == 0 else 'Failed to start display', 'message': 'Display started' if result.returncode == 0 else 'Failed to start display',
}) }
if result.returncode != 0:
resp['returncode'] = result.returncode
resp['stderr'] = result.stderr.strip()
return jsonify(resp)
else: else:
result = subprocess.run(['sudo', 'systemctl', 'start', 'ledmatrix'], result = subprocess.run(['sudo', 'systemctl', 'start', 'ledmatrix'],
capture_output=True, text=True) capture_output=True, text=True)
@@ -1564,10 +1574,16 @@ def execute_system_action():
return jsonify({'status': 'error', 'message': 'Unknown action'}), 400 return jsonify({'status': 'error', 'message': 'Unknown action'}), 400
logger.info("system action '%s' returncode=%d", action, result.returncode) logger.info("system action '%s' returncode=%d", action, result.returncode)
return jsonify({ if result.returncode != 0 and result.stderr:
logger.error("system action '%s' stderr: %s", action, result.stderr.strip())
resp = {
'status': 'success' if result.returncode == 0 else 'error', 'status': 'success' if result.returncode == 0 else 'error',
'message': 'Action completed' if result.returncode == 0 else 'Action failed; check logs for details', 'message': 'Action completed' if result.returncode == 0 else 'Action failed; check logs for details',
}) }
if result.returncode != 0:
resp['returncode'] = result.returncode
resp['stderr'] = result.stderr.strip()
return jsonify(resp)
except Exception as e: except Exception as e:
logger.error("execute_system_action failed: %s", e, exc_info=True) logger.error("execute_system_action failed: %s", e, exc_info=True)
@@ -6425,9 +6441,12 @@ def list_plugin_assets():
def get_logs(): def get_logs():
"""Get system logs from journalctl""" """Get system logs from journalctl"""
try: try:
if not _SUDO or not _JOURNALCTL:
return jsonify({'status': 'error', 'message': 'sudo or journalctl not found on this system'}), 503
# Get recent logs from journalctl # Get recent logs from journalctl
result = subprocess.run( result = subprocess.run(
['sudo', 'journalctl', '-u', 'ledmatrix.service', '-n', '100', '--no-pager'], [_SUDO, _JOURNALCTL, '-u', 'ledmatrix.service', '-u', 'ledmatrix-web.service',
'-n', '100', '--no-pager', '--output=short-iso'],
capture_output=True, capture_output=True,
text=True, text=True,
timeout=5 timeout=5
@@ -6438,7 +6457,7 @@ def get_logs():
return jsonify({ return jsonify({
'status': 'success', 'status': 'success',
'data': { 'data': {
'logs': logs_text if logs_text else 'No logs available from ledmatrix service' 'logs': logs_text if logs_text else 'No logs available from ledmatrix or ledmatrix-web service'
} }
}) })
else: else: