diff --git a/manage.sh b/manage.sh index 0449f9d..7d0e5e6 100755 --- a/manage.sh +++ b/manage.sh @@ -233,27 +233,33 @@ install_repeater() { SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Installation progress - ( - echo "0"; echo "# Creating service user..." + echo "" + echo "═══════════════════════════════════════════════════════════════" + echo " Installing pyMC Repeater" + echo "═══════════════════════════════════════════════════════════════" + echo "" + + echo ">>> Creating service user..." if ! id "$SERVICE_USER" &>/dev/null; then useradd --system --home /var/lib/pymc_repeater --shell /sbin/nologin "$SERVICE_USER" fi - echo "10"; echo "# Adding user to hardware groups..." + echo ">>> Adding user to hardware groups..." for grp in plugdev dialout gpio i2c spi; do getent group "$grp" >/dev/null 2>&1 && usermod -a -G "$grp" "$SERVICE_USER" 2>/dev/null || true done - echo "20"; echo "# Creating directories..." + echo ">>> Creating directories..." mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater - echo "25"; echo "# Installing system dependencies..." + echo ">>> Installing system dependencies..." apt-get update -qq - apt-get install -y libffi-dev libusb-1.0-0 jq pip python3-rrdtool wget swig build-essential python3-dev - pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true + DEBIAN_FRONTEND=noninteractive apt-get install -y libffi-dev libusb-1.0-0 policykit-1 jq pip python3-rrdtool wget swig build-essential python3-dev + pip install --break-system-packages setuptools_scm 2>&1 || true # Install mikefarah yq v4 if not already installed if ! command -v yq &> /dev/null || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then + echo ">>> Installing yq..." YQ_VERSION="v4.40.5" YQ_BINARY="yq_linux_arm64" if [[ "$(uname -m)" == "x86_64" ]]; then @@ -261,32 +267,31 @@ install_repeater() { elif [[ "$(uname -m)" == "armv7"* ]]; then YQ_BINARY="yq_linux_arm" fi - wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" && chmod +x /usr/local/bin/yq + wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" 2>/dev/null && chmod +x /usr/local/bin/yq fi - echo "28"; echo "# Generating version file..." + echo ">>> Generating version file..." cd "$SCRIPT_DIR" # Generate version file using setuptools_scm before copying if [ -d .git ]; then - git fetch --tags 2>/dev/null || true + git fetch --tags >/dev/null 2>&1 || true # Write the version file that will be copied - GENERATED_VERSION=$(python3 -m setuptools_scm 2>&1 || echo "unknown (setuptools_scm not available)") - python3 -c "from setuptools_scm import get_version; get_version(write_to='repeater/_version.py')" 2>&1 || echo " Warning: Could not generate _version.py file" - echo " Generated version: $GENERATED_VERSION" + python3 -m setuptools_scm >/dev/null 2>&1 || true + python3 -c "from setuptools_scm import get_version; get_version(write_to='repeater/_version.py')" >/dev/null 2>&1 || true fi # Clean up stale bytecode in source directory before copying find "$SCRIPT_DIR/repeater" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true find "$SCRIPT_DIR/repeater" -type f -name '*.pyc' -delete 2>/dev/null || true - echo "29"; echo "# Cleaning old installation files..." + echo ">>> Cleaning old installation files..." # Remove old repeater directory to ensure clean install rm -rf "$INSTALL_DIR/repeater" 2>/dev/null || true # Clean up old Python bytecode find "$INSTALL_DIR" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true find "$INSTALL_DIR" -type f -name '*.pyc' -delete 2>/dev/null || true - echo "30"; echo "# Installing files..." + echo ">>> Installing files..." cp -r "$SCRIPT_DIR/repeater" "$INSTALL_DIR/" cp "$SCRIPT_DIR/pyproject.toml" "$INSTALL_DIR/" cp "$SCRIPT_DIR/README.md" "$INSTALL_DIR/" @@ -295,24 +300,24 @@ install_repeater() { cp "$SCRIPT_DIR/radio-settings.json" /var/lib/pymc_repeater/ 2>/dev/null || true cp "$SCRIPT_DIR/radio-presets.json" /var/lib/pymc_repeater/ 2>/dev/null || true - echo "45"; echo "# Installing configuration..." + echo ">>> Installing configuration..." cp "$SCRIPT_DIR/config.yaml.example" "$CONFIG_DIR/config.yaml.example" if [ ! -f "$CONFIG_DIR/config.yaml" ]; then cp "$SCRIPT_DIR/config.yaml.example" "$CONFIG_DIR/config.yaml" fi - echo "55"; echo "# Installing systemd service..." + echo ">>> Installing systemd service..." cp "$SCRIPT_DIR/pymc-repeater.service" /etc/systemd/system/ systemctl daemon-reload - echo "60"; echo "# Installing udev rules for CH341..." + echo ">>> Installing udev rules for CH341..." if [ -f "$SCRIPT_DIR/../pyMC_core/99-ch341.rules" ]; then cp "$SCRIPT_DIR/../pyMC_core/99-ch341.rules" /etc/udev/rules.d/99-ch341.rules udevadm control --reload-rules 2>/dev/null || true udevadm trigger 2>/dev/null || true fi - echo "65"; echo "# Setting permissions..." + echo ">>> Setting permissions..." chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater chmod 750 "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater # Ensure the service user can create subdirectories in their home directory @@ -322,6 +327,7 @@ install_repeater() { chown -R "$SERVICE_USER:$SERVICE_USER" /var/lib/pymc_repeater/.config # Configure polkit for passwordless service restart + echo ">>> Configuring polkit for service management..." mkdir -p /etc/polkit-1/rules.d cat > /etc/polkit-1/rules.d/10-pymc-repeater.rules <<'EOF' polkit.addRule(function(action, subject) { @@ -334,11 +340,18 @@ polkit.addRule(function(action, subject) { EOF chmod 0644 /etc/polkit-1/rules.d/10-pymc-repeater.rules - echo "75"; echo "# Starting service..." + # Also configure sudoers as fallback for service restart + echo ">>> Configuring sudoers for service management..." + cat > /etc/sudoers.d/pymc-repeater <<'EOF' +# Allow repeater user to manage the pymc-repeater service without password +repeater ALL=(root) NOPASSWD: /usr/bin/systemctl restart pymc-repeater, /usr/bin/systemctl stop pymc-repeater, /usr/bin/systemctl start pymc-repeater, /usr/bin/systemctl status pymc-repeater +EOF + chmod 0440 /etc/sudoers.d/pymc-repeater + + echo ">>> Enabling service..." systemctl enable "$SERVICE_NAME" - echo "90"; echo "# Installation files complete..." - ) | $DIALOG --backtitle "pyMC Repeater Management" --title "Installing" --gauge "Setting up pyMC Repeater..." 8 70 0 + echo ">>> Installation files complete." # Install Python package outside of progress gauge for better error handling clear @@ -529,7 +542,7 @@ upgrade_repeater() { echo "[3/9] Updating system dependencies..." apt-get update -qq - apt-get install -y libffi-dev libusb-1.0-0 jq pip python3-rrdtool wget swig build-essential python3-dev + apt-get install -y libffi-dev libusb-1.0-0 policykit-1 jq pip python3-rrdtool wget swig build-essential python3-dev pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true # Install mikefarah yq v4 if not already installed @@ -620,6 +633,12 @@ polkit.addRule(function(action, subject) { }); EOF chmod 0644 /etc/polkit-1/rules.d/10-pymc-repeater.rules + # Also configure sudoers as fallback for service restart + cat > /etc/sudoers.d/pymc-repeater <<'EOF' +# Allow repeater user to manage the pymc-repeater service without password +repeater ALL=(root) NOPASSWD: /usr/bin/systemctl restart pymc-repeater, /usr/bin/systemctl stop pymc-repeater, /usr/bin/systemctl start pymc-repeater, /usr/bin/systemctl status pymc-repeater +EOF + chmod 0440 /etc/sudoers.d/pymc-repeater echo " ✓ Permissions updated" echo "[7/9] Reloading systemd..." @@ -742,33 +761,41 @@ uninstall_repeater() { fi if ask_yes_no "Confirm Uninstall" "This will completely remove pyMC Repeater including:\n\n- Service and files\n- Configuration (backup will be created)\n- Logs and data\n\nThis action cannot be undone!\n\nContinue?"; then - ( - echo "0"; echo "# Stopping and disabling service..." + echo "" + echo "═══════════════════════════════════════════════════════════════" + echo " Uninstalling pyMC Repeater" + echo "═══════════════════════════════════════════════════════════════" + echo "" + + echo ">>> Stopping and disabling service..." systemctl stop "$SERVICE_NAME" 2>/dev/null || true systemctl disable "$SERVICE_NAME" 2>/dev/null || true - echo "20"; echo "# Backing up configuration..." + echo ">>> Backing up configuration..." if [ -d "$CONFIG_DIR" ]; then cp -r "$CONFIG_DIR" "/tmp/pymc_repeater_config_backup_$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true fi - echo "40"; echo "# Removing service files..." + echo ">>> Removing service files..." rm -f /etc/systemd/system/pymc-repeater.service systemctl daemon-reload - echo "60"; echo "# Removing installation..." + echo ">>> Removing installation..." rm -rf "$INSTALL_DIR" rm -rf "$CONFIG_DIR" rm -rf "$LOG_DIR" rm -rf /var/lib/pymc_repeater - echo "80"; echo "# Removing service user..." + echo ">>> Removing service user..." if id "$SERVICE_USER" &>/dev/null; then userdel "$SERVICE_USER" 2>/dev/null || true fi - echo "100"; echo "# Uninstall complete!" - ) | $DIALOG --backtitle "pyMC Repeater Management" --title "Uninstalling" --gauge "Removing pyMC Repeater..." 8 70 0 + echo ">>> Removing polkit and sudoers rules..." + rm -f /etc/polkit-1/rules.d/10-pymc-repeater.rules + rm -f /etc/sudoers.d/pymc-repeater + + echo ">>> Uninstall complete!" show_info "Uninstall Complete" "\npyMC Repeater has been completely removed.\n\nConfiguration backup saved to /tmp/\n\nThank you for using pyMC Repeater!" fi diff --git a/pymc-repeater.service b/pymc-repeater.service index 3cbd51d..80b0456 100644 --- a/pymc-repeater.service +++ b/pymc-repeater.service @@ -28,8 +28,7 @@ StandardOutput=journal StandardError=journal SyslogIdentifier=pymc-repeater -# Security (relaxed for proper operation) -NoNewPrivileges=true +# Security (relaxed for service self-restart via sudo) ReadWritePaths=/var/log/pymc_repeater /var/lib/pymc_repeater /etc/pymc_repeater SupplementaryGroups=plugdev dialout diff --git a/repeater/service_utils.py b/repeater/service_utils.py index 78ba7e8..99def20 100644 --- a/repeater/service_utils.py +++ b/repeater/service_utils.py @@ -13,12 +13,13 @@ def restart_service() -> Tuple[bool, str]: """ Restart the pymc-repeater service via systemctl. - Uses polkit for authentication (requires proper polkit rules configured). - NoNewPrivileges systemd flag prevents sudo from working. + Tries polkit-based restart first (plain systemctl), then falls back + to sudo-based restart (requires sudoers.d rule installed by manage.sh). Returns: Tuple[bool, str]: (success, message) """ + # Try polkit-based restart first (works on bare metal / VMs with polkit running) try: result = subprocess.run( ['systemctl', 'restart', 'pymc-repeater'], @@ -28,19 +29,49 @@ def restart_service() -> Tuple[bool, str]: ) if result.returncode == 0: - logger.info("Service restart command executed successfully") + logger.info("Service restart via polkit succeeded") return True, "Service restart initiated" + + stderr = result.stderr or "" + if "Access denied" in stderr or "authorization" in stderr.lower(): + logger.info("Polkit denied restart, trying sudo fallback...") else: - error_msg = result.stderr or "Unknown error" - logger.error(f"Service restart failed: {error_msg}") - return False, f"Restart failed: {error_msg}" + # Some other error, still try sudo + logger.warning(f"systemctl restart failed ({result.returncode}): {stderr.strip()}") except subprocess.TimeoutExpired: + # Timeout likely means it's restarting - that's success logger.warning("Service restart command timed out (service may be restarting)") return True, "Service restart initiated (timeout - likely restarting)" except FileNotFoundError: logger.error("systemctl not found") return False, "systemctl not available" except Exception as e: - logger.error(f"Error executing restart command: {e}") + logger.warning(f"Polkit restart attempt failed: {e}") + + # Fallback: use sudo (requires /etc/sudoers.d/pymc-repeater rule) + try: + result = subprocess.run( + ['sudo', '--non-interactive', 'systemctl', 'restart', 'pymc-repeater'], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0: + logger.info("Service restart via sudo succeeded") + return True, "Service restart initiated" + else: + error_msg = result.stderr or "Unknown error" + logger.error(f"Service restart via sudo failed: {error_msg}") + return False, f"Restart failed: {error_msg}" + + except subprocess.TimeoutExpired: + logger.warning("Sudo restart timed out (service likely restarting)") + return True, "Service restart initiated (timeout - likely restarting)" + except FileNotFoundError: + logger.error("sudo not found - cannot restart service") + return False, "Neither polkit nor sudo available for service restart" + except Exception as e: + logger.error(f"Error executing sudo restart: {e}") return False, f"Restart command failed: {str(e)}" diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index 2a2a661..fda34a3 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -491,8 +491,8 @@ class APIEndpoints: import time time.sleep(2) # Give time for response to be sent try: - # Use systemctl without sudo - polkit rules allow the repeater user to restart the service - subprocess.run(['systemctl', 'restart', 'pymc-repeater'], check=False) + from repeater.service_utils import restart_service + restart_service() except Exception as e: logger.error(f"Failed to restart service: {e}")