#!/usr/bin/env bash set -Eeuo pipefail # ============================================================ # Linux Parental Control Installer # ============================================================ APP_NAME="Parental Control" SERVICE_NAME="parental-control.service" DESKTOP_ID="parental-control.desktop" INSTALL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" VENV_DIR="$INSTALL_DIR/.venv" PYTHON="$VENV_DIR/bin/python" SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME" DESKTOP_FILE="/usr/share/applications/$DESKTOP_ID" POLKIT_ACTION="org.parentalcontrol.modify" POLKIT_SOURCE="$INSTALL_DIR/$POLKIT_ACTION.policy" POLKIT_DEST="/etc/polkit-1/actions/$POLKIT_ACTION.policy" SOCKET_PATH="/run/parental-control.sock" INSTALL_USER="" INSTALL_GROUP="" PACKAGE_MANAGER="" POLKIT_AGENT="" # ============================================================ # Output helpers # ============================================================ info() { echo echo "==> $1" } success() { echo " $1" } warning() { echo " WARNING: $1" } error() { echo echo "ERROR: $1" echo exit 1 } # ============================================================ # Error handling # ============================================================ on_error() { echo echo "Installation failed." echo "Line: $1" echo "Command: $2" echo } trap 'on_error "$LINENO" "$BASH_COMMAND"' ERR # ============================================================ # Root check # ============================================================ if [ "$EUID" -ne 0 ]; then error "Run this installer with sudo: sudo ./install.sh" fi # ============================================================ # Find original user # ============================================================ if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then INSTALL_USER="$SUDO_USER" else INSTALL_USER="$(stat -c '%U' "$INSTALL_DIR")" fi if ! id "$INSTALL_USER" >/dev/null 2>&1; then error "Could not determine the normal installation user." fi INSTALL_GROUP="$(id -gn "$INSTALL_USER")" INSTALL_USER_HOME="$(getent passwd "$INSTALL_USER" | cut -d: -f6)" if [ -z "$INSTALL_USER_HOME" ] || [ ! -d "$INSTALL_USER_HOME" ]; then error "Could not determine the home directory of $INSTALL_USER." fi # ============================================================ # Header # ============================================================ clear 2>/dev/null || true echo echo "==========================================" echo " Linux Parental Control Installer" echo "==========================================" echo echo "Application:" echo " $APP_NAME" echo echo "Installation directory:" echo " $INSTALL_DIR" echo echo "Installation user:" echo " $INSTALL_USER" echo # ============================================================ # Check project files # ============================================================ info "Checking project files" REQUIRED_FILES=( "requirements.txt" "parental-control.service" "$POLKIT_ACTION.policy" "app/__init__.py" "app/main.py" "app/config.py" "app/enforcement.py" "app/scheduler.py" "app/users.py" "app/ipc.py" "desktop/__init__.py" "desktop/main.py" "desktop/client.py" "config/users.yaml" "config/state.json" ) for file in "${REQUIRED_FILES[@]}"; do if [ ! -f "$INSTALL_DIR/$file" ]; then error "Required file not found: $file" fi done success "All required project files found." # ============================================================ # Detect operating system # ============================================================ info "Detecting operating system" if [ ! -f /etc/os-release ]; then error "/etc/os-release was not found." fi # shellcheck disable=SC1091 source /etc/os-release OS_ID="${ID:-unknown}" OS_NAME="${NAME:-Unknown Linux}" OS_VERSION="${VERSION_ID:-unknown}" echo " Distribution: $OS_NAME" echo " Version: $OS_VERSION" # ============================================================ # Check systemd # ============================================================ info "Checking systemd" if ! command -v systemctl >/dev/null 2>&1; then error "systemd is required for this version of the application. This installer does not support non-systemd distributions." fi if [ ! -d /run/systemd/system ]; then error "systemd is installed but is not currently running." fi if ! command -v loginctl >/dev/null 2>&1; then error "loginctl was not found. The application requires systemd-logind." fi success "systemd and loginctl detected." # ============================================================ # Detect package manager # ============================================================ info "Detecting package manager" if command -v pacman >/dev/null 2>&1; then PACKAGE_MANAGER="pacman" elif command -v apt-get >/dev/null 2>&1; then PACKAGE_MANAGER="apt" elif command -v dnf >/dev/null 2>&1; then PACKAGE_MANAGER="dnf" elif command -v zypper >/dev/null 2>&1; then PACKAGE_MANAGER="zypper" else error "No supported package manager was detected. Supported package managers: pacman apt dnf zypper You can install the required dependencies manually, then run the installer again." fi success "Package manager: $PACKAGE_MANAGER" # ============================================================ # Install system dependencies # ============================================================ info "Installing system dependencies" case "$PACKAGE_MANAGER" in pacman) pacman -S --needed --noconfirm \ python \ python-pip \ polkit \ polkit-gnome \ desktop-file-utils \ systemd ;; apt) export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y \ python3 \ python3-pip \ python3-venv \ polkitd \ policykit-1 \ policykit-1-gnome \ desktop-file-utils \ systemd ;; dnf) dnf install -y \ python3 \ python3-pip \ python3-virtualenv \ polkit \ polkit-gnome \ desktop-file-utils \ systemd ;; zypper) zypper --non-interactive install \ python3 \ python3-pip \ python3-virtualenv \ polkit \ polkit-gnome \ desktop-file-utils \ systemd ;; esac success "System dependencies installed." # ============================================================ # Find Python # ============================================================ info "Checking Python" SYSTEM_PYTHON="" for candidate in python3 python; do if command -v "$candidate" >/dev/null 2>&1; then SYSTEM_PYTHON="$(command -v "$candidate")" break fi done if [ -z "$SYSTEM_PYTHON" ]; then error "Python 3 could not be found." fi PYTHON_MAJOR="$( "$SYSTEM_PYTHON" -c \ 'import sys; print(sys.version_info.major)' )" PYTHON_MINOR="$( "$SYSTEM_PYTHON" -c \ 'import sys; print(sys.version_info.minor)' )" PYTHON_VERSION="$( "$SYSTEM_PYTHON" -c \ 'import sys; print(".".join(map(str, sys.version_info[:3])))' )" if [ "$PYTHON_MAJOR" -ne 3 ]; then error "Python 3 is required. Found Python $PYTHON_VERSION." fi if [ "$PYTHON_MINOR" -lt 9 ]; then error "Python $PYTHON_VERSION is too old. Python 3.9 or newer is required." fi echo " Python: $SYSTEM_PYTHON" echo " Version: $PYTHON_VERSION" success "Python version is supported." # ============================================================ # Create virtual environment # ============================================================ info "Preparing Python virtual environment" if [ -x "$PYTHON" ]; then success "Existing virtual environment found." else if [ -d "$VENV_DIR" ]; then warning "Existing virtual environment is incomplete." warning "Removing it." rm -rf "$VENV_DIR" fi echo " Creating virtual environment..." if ! "$SYSTEM_PYTHON" -m venv "$VENV_DIR"; then error "Could not create the Python virtual environment. Install your distribution's Python venv package." fi success "Virtual environment created." fi # ============================================================ # Install Python dependencies # ============================================================ info "Installing Python dependencies" "$PYTHON" -m pip install \ --disable-pip-version-check \ --upgrade pip "$PYTHON" -m pip install \ --disable-pip-version-check \ -r "$INSTALL_DIR/requirements.txt" success "Python dependencies installed." # ============================================================ # Validate Python source # ============================================================ info "Checking Python source files" "$PYTHON" -m compileall \ -q \ "$INSTALL_DIR/app" \ "$INSTALL_DIR/desktop" success "Python source validation passed." # ============================================================ # Validate configuration # ============================================================ info "Checking application configuration" if [ ! -f "$INSTALL_DIR/config/users.yaml" ]; then error "users.yaml was not found." fi if [ ! -f "$INSTALL_DIR/config/state.json" ]; then error "state.json was not found." fi success "Configuration files found." # ============================================================ # Install systemd service # ============================================================ info "Installing systemd service" SERVICE_TEMP="$(mktemp)" sed \ "s|%INSTALL_DIR%|$INSTALL_DIR|g" \ "$INSTALL_DIR/parental-control.service" \ > "$SERVICE_TEMP" if ! grep -q '^Environment=PYTHONDONTWRITEBYTECODE=' "$SERVICE_TEMP"; then sed -i \ '/^\[Service\]/a Environment=PYTHONDONTWRITEBYTECODE=1' \ "$SERVICE_TEMP" fi install \ -o root \ -g root \ -m 644 \ "$SERVICE_TEMP" \ "$SERVICE_FILE" rm -f "$SERVICE_TEMP" success "Systemd service installed." # ============================================================ # Install polkit policy # ============================================================ info "Installing polkit policy" if [ ! -f "$POLKIT_SOURCE" ]; then error "Polkit policy file was not found: $POLKIT_SOURCE" fi mkdir -p /etc/polkit-1/actions install \ -o root \ -g root \ -m 644 \ "$POLKIT_SOURCE" \ "$POLKIT_DEST" success "Polkit policy installed." # ============================================================ # Detect polkit authentication agent # ============================================================ info "Detecting polkit authentication agent" POLKIT_AGENT="" POLKIT_AGENT_CANDIDATES=( "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1" "/usr/libexec/polkit-gnome-authentication-agent-1" "/usr/lib/polkit-1-gnome/polkit-gnome-authentication-agent-1" ) for candidate in "${POLKIT_AGENT_CANDIDATES[@]}"; do if [ -x "$candidate" ]; then POLKIT_AGENT="$candidate" break fi done if [ -n "$POLKIT_AGENT" ]; then success "Authentication agent found:" success "$POLKIT_AGENT" else warning "A polkit authentication agent was not found." warning "GUI administrative actions may require manual configuration." fi # ============================================================ # Configure Sway polkit authentication agent # ============================================================ configure_sway_polkit() { local user_home="$1" local sway_config="$user_home/.config/sway/config" local marker="parental-control-polkit-agent" if [ -z "$POLKIT_AGENT" ]; then return 0 fi if [ ! -f "$sway_config" ]; then warning "Sway configuration not found:" warning "$sway_config" return 0 fi if grep -Fq "$marker" "$sway_config"; then success "Sway polkit authentication agent is already configured." return 0 fi cp -a \ "$sway_config" \ "$sway_config.parental-control-backup" cat >> "$sway_config" < "$DESKTOP_FILE" </dev/null 2>&1; then info "Validating desktop launcher" if ! desktop-file-validate "$DESKTOP_FILE"; then error "Desktop launcher validation failed." fi success "Desktop launcher is valid." else warning "desktop-file-validate is not available." fi # ============================================================ # Update desktop database # ============================================================ if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database \ /usr/share/applications \ >/dev/null 2>&1 || true fi # ============================================================ # Restore source ownership # ============================================================ info "Fixing source ownership" chown -R \ "$INSTALL_USER:$INSTALL_GROUP" \ "$INSTALL_DIR" success "Source tree owned by $INSTALL_USER." # ============================================================ # Reload systemd # ============================================================ info "Reloading systemd" systemctl daemon-reload success "Systemd reloaded." # ============================================================ # Remove stale IPC socket # ============================================================ info "Cleaning stale IPC socket" if [ -e "$SOCKET_PATH" ]; then if [ -S "$SOCKET_PATH" ]; then rm -f "$SOCKET_PATH" success "Removed stale IPC socket." else warning "$SOCKET_PATH exists but is not a socket." warning "Leaving it untouched." fi fi # ============================================================ # Enable service # ============================================================ info "Enabling parental control service" systemctl enable "$SERVICE_NAME" success "Service enabled." # ============================================================ # Restart service # ============================================================ info "Starting parental control service" systemctl restart "$SERVICE_NAME" # ============================================================ # Wait for service # ============================================================ info "Waiting for service startup" SERVICE_READY=0 for _ in $(seq 1 15); do if systemctl is-active --quiet "$SERVICE_NAME"; then SERVICE_READY=1 break fi sleep 1 done if [ "$SERVICE_READY" -ne 1 ]; then echo echo "==========================================" echo " Service startup failed" echo "==========================================" echo systemctl status \ "$SERVICE_NAME" \ --no-pager || true echo echo "Recent service logs:" echo journalctl \ -u "$SERVICE_NAME" \ -n 50 \ --no-pager || true exit 1 fi success "Service is running." # ============================================================ # Verify IPC socket # ============================================================ info "Checking IPC socket" if [ ! -S "$SOCKET_PATH" ]; then warning "IPC socket was not created:" warning "$SOCKET_PATH" else success "IPC socket is active." fi # ============================================================ # Final verification # ============================================================ info "Running final checks" CHECK_FAILED=0 if systemctl is-active --quiet "$SERVICE_NAME"; then echo " Service: OK" else echo " Service: FAILED" CHECK_FAILED=1 fi if [ -x "$PYTHON" ]; then echo " Python: OK" else echo " Python: FAILED" CHECK_FAILED=1 fi if [ -f "$DESKTOP_FILE" ]; then echo " Desktop: OK" else echo " Desktop: FAILED" CHECK_FAILED=1 fi if [ -S "$SOCKET_PATH" ]; then echo " IPC: OK" else echo " IPC: WARNING" fi if [ -f "$POLKIT_DEST" ]; then echo " Polkit: OK" else echo " Polkit: FAILED" CHECK_FAILED=1 fi if [ -f "$INSTALL_DIR/config/users.yaml" ]; then echo " Users: OK" else echo " Users: FAILED" CHECK_FAILED=1 fi if [ -f "$INSTALL_DIR/config/state.json" ]; then echo " State: OK" else echo " State: FAILED" CHECK_FAILED=1 fi if [ "$CHECK_FAILED" -ne 0 ]; then error "One or more required installation checks failed." fi # ============================================================ # Finished # ============================================================ echo echo "==========================================" echo " Installation complete" echo "==========================================" echo echo "Application:" echo " $APP_NAME" echo echo "Launch:" echo " Application menu" echo " Rofi" echo echo "Service:" echo " systemctl status $SERVICE_NAME" echo echo "Logs:" echo " journalctl -u $SERVICE_NAME" echo echo "IPC:" echo " $SOCKET_PATH" echo echo "Configuration:" echo " $INSTALL_DIR/config/users.yaml" echo " $INSTALL_DIR/config/state.json" echo echo "Installation:" echo " $INSTALL_DIR" echo echo "Polkit agent:" if [ -n "$POLKIT_AGENT" ]; then echo " $POLKIT_AGENT" else echo " Not detected" fi echo echo "==========================================" echo