mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
* chore: stop tests and rigs writing to shared paths Two shared-state problems, both of which show up as a permanently dirty checkout or an unreproducible test failure. test_display_dirty_tracking.py builds a real DisplayManager, whose _snapshot_path defaults to the fixed /tmp/led_matrix_preview.png that the web UI reads. Every pytest process on the machine shares that one file, so two concurrent runs -- CI shards, a second worktree, an agent running the suite alongside -- overwrite each other's snapshot and the mtime assertions stop meaning anything. The module fixture now points it at a session-unique temp path; the individual tests that care still override it further. To be clear about what this does and does not fix: this is a real shared-path hazard, but it is NOT the cause of the intermittent 15-test failure in that module. That turned out to be the emulator's fixed TCP port, fixed in the follow-up commit. This change stands on its own merits. web_interface/app.py writes data/plugin_operations.json, data/plugin_state.json and data/operation_history.json as the web interface runs, into a directory that ships tracked (data/.gitkeep) and was otherwise unignored. So every rig that ever opened the web UI -- and every test run that constructs the app -- left three untracked files behind and a permanently dirty `git status`. Only data/.gitkeep is tracked under data/, so the negation keeps it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: stop the emulator binding a fixed port, so concurrent runs can't collide This is the cause of the intermittent full-suite failures we have been chasing: runs of identical code landing anywhere between 100 and 130 failures, while every implicated test passed in isolation. Six test modules set EMULATOR=true and build a real DisplayManager. The repo's emulator_config.json selects the "browser" adapter, which binds TCP port 8888 to serve the dev preview. That port is a machine-wide singleton, so a second pytest process -- a CI shard, another worktree, an agent running the suite alongside -- loses the bind. RGBMatrix construction then raises, DisplayManager catches it and falls back to `self.matrix = None`, and every test that subsequently touches the matrix dies with AttributeError: 'NoneType' object has no attribute 'SwapOnVSync' which names neither a port nor a socket, and points at the wrong file entirely. Because test_display_dirty_tracking's fixture is module-scoped, all 15 of its matrix-touching tests fail together or not at all -- the 15-test swing that made the totals look random. Demonstrated rather than assumed. Holding 0.0.0.0:8888 from a separate process and running test_display_dirty_tracking.py: without this change 15 failed, 6 passed with this change 21 passed The "raw" adapter renders in memory and binds nothing. Only display_adapter is overridden, in a throwaway config written per pytest process; the repo's emulator_config.json is untouched and `run.py -e` still opens the browser preview on 8888. Nothing in the suite referenced the adapter, and the tests wrap SwapOnVSync on the matrix object itself, so they are indifferent to what sits underneath. allow_adapter_fallback is forced off -- falling back would land us on the browser adapter and its fixed port, which is the whole problem. CONFIG_PATH is a bare relative filename resolved against the CWD, so it is set to an absolute path: the previous behaviour depended on where pytest was invoked from, and silently wrote a default config into whatever directory that was. Verified no regressions: full suite on this branch and with origin/main's versions of the touched files, same machine, back to back -- 115 failed / 4347 passed on both sides, zero failures unique to either. That 115 is the pre-existing Windows-environment baseline (POSIX file modes, fcntl, shell scripts, Linux-only binaries); CI on Linux remains authoritative. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: mark the shell entry points executable Eleven scripts shipped as 100644, so `./scripts/install/configure_web_sudo.sh` fails with "Permission denied" and only works if you know to prefix `bash`. That one matters most: the web UI's own error hint, added in #560, tells users to run exactly that path when a system action fails for want of passwordless sudo, and following that instruction verbatim did not work. All eleven carry a shebang and are invoked directly, never sourced. The two sourced libraries -- lib_lowmem.sh and lib_systemd_render.sh -- are deliberately left non-executable, which is what distinguishes a library from an entry point. Mode bits only, no content: 11 files changed, 0 insertions, 0 deletions. Applied with `git update-index --chmod=+x` because this checkout is on Windows, where core.fileMode is off and the working-tree bit is not tracked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
218 lines
8.5 KiB
Bash
Executable File
218 lines
8.5 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# LED Matrix Web Interface Sudo Configuration Script
|
|
# This script configures passwordless sudo access for the web interface user
|
|
|
|
set -e
|
|
|
|
echo "Configuring passwordless sudo access for LED Matrix Web Interface..."
|
|
|
|
# Get the current user (should be the user running the web interface)
|
|
WEB_USER=$(whoami)
|
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$PROJECT_DIR/../.." && pwd)"
|
|
|
|
echo "Detected web interface user: $WEB_USER"
|
|
echo "Project directory: $PROJECT_DIR"
|
|
echo "Project root: $PROJECT_ROOT"
|
|
|
|
# Check if running as root
|
|
if [ "$EUID" -eq 0 ]; then
|
|
echo "Error: This script should not be run as root."
|
|
echo "Run it as the user that will be running the web interface."
|
|
exit 1
|
|
fi
|
|
|
|
# Get the full paths to commands and validate each one
|
|
MISSING_CMDS=()
|
|
|
|
PYTHON_PATH=$(command -v python3) || true
|
|
SYSTEMCTL_PATH=$(command -v systemctl) || true
|
|
REBOOT_PATH=$(command -v reboot) || true
|
|
POWEROFF_PATH=$(command -v poweroff) || true
|
|
BASH_PATH=$(command -v bash) || true
|
|
JOURNALCTL_PATH=$(command -v journalctl) || true
|
|
SAFE_RM_PATH="$PROJECT_ROOT/scripts/fix_perms/safe_plugin_rm.sh"
|
|
SAFE_PIP_INSTALL_PATH="$PROJECT_ROOT/scripts/fix_perms/safe_pip_install.sh"
|
|
|
|
# Validate required commands (systemctl, bash, python3 are essential)
|
|
for CMD_NAME in SYSTEMCTL_PATH BASH_PATH PYTHON_PATH; do
|
|
CMD_VAL="${!CMD_NAME}"
|
|
if [ -z "$CMD_VAL" ]; then
|
|
MISSING_CMDS+=("$CMD_NAME")
|
|
fi
|
|
done
|
|
|
|
if [ ${#MISSING_CMDS[@]} -gt 0 ]; then
|
|
echo "Error: Required commands not found: ${MISSING_CMDS[*]}" >&2
|
|
echo "Cannot generate valid sudoers configuration without these." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Validate helper scripts exist
|
|
if [ ! -f "$SAFE_RM_PATH" ]; then
|
|
echo "Error: Safe plugin removal helper not found: $SAFE_RM_PATH" >&2
|
|
exit 1
|
|
fi
|
|
if [ ! -f "$SAFE_PIP_INSTALL_PATH" ]; then
|
|
echo "Error: Safe pip install helper not found: $SAFE_PIP_INSTALL_PATH" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Command paths:"
|
|
echo " Python: $PYTHON_PATH"
|
|
echo " Systemctl: $SYSTEMCTL_PATH"
|
|
echo " Reboot: ${REBOOT_PATH:-(not found, skipping)}"
|
|
echo " Poweroff: ${POWEROFF_PATH:-(not found, skipping)}"
|
|
echo " Bash: $BASH_PATH"
|
|
echo " Journalctl: ${JOURNALCTL_PATH:-(not found, skipping)}"
|
|
echo " Safe plugin rm: $SAFE_RM_PATH"
|
|
echo " Safe pip install: $SAFE_PIP_INSTALL_PATH"
|
|
|
|
# Create a temporary sudoers file
|
|
TEMP_SUDOERS="/tmp/ledmatrix_web_sudoers_$$"
|
|
|
|
{
|
|
echo "# LED Matrix Web Interface passwordless sudo configuration"
|
|
echo "# This allows the web interface user to run specific commands without a password"
|
|
echo ""
|
|
echo "# Allow $WEB_USER to run specific commands without a password for the LED Matrix web interface"
|
|
|
|
# Optional: reboot/poweroff (non-critical — skip if not found)
|
|
if [ -n "$REBOOT_PATH" ]; then
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $REBOOT_PATH"
|
|
fi
|
|
if [ -n "$POWEROFF_PATH" ]; then
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $POWEROFF_PATH"
|
|
fi
|
|
|
|
# Required: systemctl
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH start ledmatrix.service"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH stop ledmatrix.service"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH restart ledmatrix.service"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH enable ledmatrix.service"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH disable ledmatrix.service"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH status ledmatrix.service"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH is-active ledmatrix"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH is-active ledmatrix.service"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH start ledmatrix-web.service"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH stop ledmatrix-web.service"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH restart ledmatrix-web.service"
|
|
|
|
# Optional: journalctl (non-critical — skip if not found)
|
|
#
|
|
# NOEXEC, matching first_time_install.sh. These rules end in a wildcard and
|
|
# journalctl starts a pager, so without it the caller can reach a shell:
|
|
# less runs "!command" as the user the pager belongs to, which here is
|
|
# root. NOEXEC stops the granted command executing anything of its own.
|
|
if [ -n "$JOURNALCTL_PATH" ]; then
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD:NOEXEC: $JOURNALCTL_PATH -u ledmatrix.service *"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD:NOEXEC: $JOURNALCTL_PATH -u ledmatrix *"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD:NOEXEC: $JOURNALCTL_PATH -t ledmatrix *"
|
|
fi
|
|
|
|
# Required: python3, bash
|
|
# NOTE: display_controller.py/start_display.sh/stop_display.sh live at the
|
|
# project root, not under scripts/install/ (where this script lives) —
|
|
# must use PROJECT_ROOT here, not PROJECT_DIR.
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $PYTHON_PATH $PROJECT_ROOT/display_controller.py"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $BASH_PATH $PROJECT_ROOT/start_display.sh"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $BASH_PATH $PROJECT_ROOT/stop_display.sh"
|
|
echo ""
|
|
echo "# Allow web user to remove plugin directories via vetted helper script"
|
|
echo "# The helper validates that the target path resolves inside plugin-repos/ or plugins/"
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $BASH_PATH $SAFE_RM_PATH *"
|
|
echo ""
|
|
echo "# Allow web user to install a plugin's requirements.txt as root via vetted"
|
|
echo "# helper script, so packages are visible to root-run ledmatrix.service"
|
|
echo "# (not just the web interface's own user). The helper validates the target"
|
|
echo "# is requirements.txt at the project root or under plugin-repos/ or plugins/."
|
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $BASH_PATH $SAFE_PIP_INSTALL_PATH *"
|
|
} > "$TEMP_SUDOERS"
|
|
|
|
echo ""
|
|
echo "Generated sudoers configuration:"
|
|
echo "--------------------------------"
|
|
cat "$TEMP_SUDOERS"
|
|
echo "--------------------------------"
|
|
|
|
echo ""
|
|
echo "This configuration will allow the web interface to:"
|
|
echo "- Start/stop/restart the ledmatrix service"
|
|
echo "- Enable/disable the ledmatrix service"
|
|
echo "- Check service status"
|
|
echo "- View system logs via journalctl"
|
|
echo "- Run display_controller.py directly"
|
|
echo "- Execute start_display.sh and stop_display.sh"
|
|
echo "- Reboot and shutdown the system"
|
|
echo "- Remove plugin directories (for update/uninstall when root-owned files block deletion)"
|
|
echo "- Install plugin/base requirements.txt as root (so ledmatrix.service can see them)"
|
|
echo ""
|
|
|
|
# Ask for confirmation
|
|
read -p "Do you want to apply this configuration? (y/N): " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
echo "Configuration cancelled."
|
|
rm -f "$TEMP_SUDOERS"
|
|
exit 0
|
|
fi
|
|
|
|
# Apply the configuration using visudo
|
|
echo "Applying sudoers configuration..."
|
|
# Harden the helper script: root-owned, not writable by web user
|
|
echo "Hardening safe_plugin_rm.sh ownership..."
|
|
if ! sudo chown root:root "$SAFE_RM_PATH"; then
|
|
echo "Warning: Could not set ownership on $SAFE_RM_PATH"
|
|
fi
|
|
if ! sudo chmod 755 "$SAFE_RM_PATH"; then
|
|
echo "Warning: Could not set permissions on $SAFE_RM_PATH"
|
|
fi
|
|
echo "Hardening safe_pip_install.sh ownership..."
|
|
if ! sudo chown root:root "$SAFE_PIP_INSTALL_PATH"; then
|
|
echo "Warning: Could not set ownership on $SAFE_PIP_INSTALL_PATH"
|
|
fi
|
|
if ! sudo chmod 755 "$SAFE_PIP_INSTALL_PATH"; then
|
|
echo "Warning: Could not set permissions on $SAFE_PIP_INSTALL_PATH"
|
|
fi
|
|
|
|
if sudo cp "$TEMP_SUDOERS" /etc/sudoers.d/ledmatrix_web; then
|
|
echo "Configuration applied successfully!"
|
|
echo ""
|
|
echo "Testing sudo access..."
|
|
|
|
# Test a few commands
|
|
if sudo -n systemctl status ledmatrix.service > /dev/null 2>&1; then
|
|
echo "✓ systemctl status ledmatrix.service - OK"
|
|
else
|
|
echo "✗ systemctl status ledmatrix.service - Failed"
|
|
fi
|
|
|
|
if sudo -n test -f "$PROJECT_ROOT/start_display.sh"; then
|
|
echo "✓ File access test - OK"
|
|
else
|
|
echo "✗ File access test - Failed"
|
|
fi
|
|
|
|
echo ""
|
|
echo "Configuration complete! The web interface should now be able to:"
|
|
echo "- Execute system commands without password prompts"
|
|
echo "- Start and stop the LED matrix display"
|
|
echo "- Restart the system if needed"
|
|
echo ""
|
|
echo "You may need to restart the web interface service for changes to take effect:"
|
|
echo " sudo systemctl restart ledmatrix-web.service"
|
|
|
|
else
|
|
echo "Error: Failed to apply sudoers configuration."
|
|
echo "You may need to run this script with sudo privileges."
|
|
rm -f "$TEMP_SUDOERS"
|
|
exit 1
|
|
fi
|
|
|
|
# Clean up
|
|
rm -f "$TEMP_SUDOERS"
|
|
|
|
echo ""
|
|
echo "Configuration script completed successfully!"
|