#!/bin/bash set -euo 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.sarlink.parentalcontrol.modify" POLKIT_SOURCE="$INSTALL_DIR/$POLKIT_ACTION.policy" POLKIT_DEST="/etc/polkit-1/actions/$POLKIT_ACTION.policy" SOCKET_PATH="/run/parental-control.sock" # ============================================================ # Output helpers # ============================================================ info() { echo echo "==> $1" } success() { echo " $1" } warning() { echo " WARNING: $1" } error() { echo echo "ERROR: $1" echo exit 1 } # ============================================================ # Root check # ============================================================ if [ "$EUID" -ne 0 ]; then echo echo "This installer requires administrator privileges." echo echo "Run:" echo echo " sudo ./install.sh" echo exit 1 fi # ============================================================ # Find original user # # When running through sudo, SUDO_USER is the person who # launched the installer. We use this to keep the source tree # owned by that user rather than root. # ============================================================ 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")" # ============================================================ # 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" "app/__init__.py" "app/main.py" "app/database.py" "app/enforcement.py" "app/scheduler.py" "app/users.py" "app/ipc.py" "desktop/__init__.py" "desktop/main.py" "desktop/client.py" ) 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." # ============================================================ # Check Linux # ============================================================ 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, but systemctl was not found." fi if [ ! -d /run/systemd/system ]; then error "systemd is installed but does not appear to be running." fi success "systemd detected." # ============================================================ # Detect package manager # ============================================================ info "Detecting package manager" 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" elif command -v apk >/dev/null 2>&1; then PACKAGE_MANAGER="apk" elif command -v xbps-install >/dev/null 2>&1; then PACKAGE_MANAGER="xbps" else error "No supported package manager was detected. Supported package managers: pacman apt dnf zypper apk xbps-install" fi success "Package manager: $PACKAGE_MANAGER" # ============================================================ # Install system packages # ============================================================ info "Installing system dependencies" case "$PACKAGE_MANAGER" in pacman) pacman -S --needed --noconfirm \ python \ python-pip \ polkit \ desktop-file-utils ;; apt) export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y \ python3 \ python3-pip \ python3-venv \ polkitd \ policykit-1 \ desktop-file-utils ;; dnf) dnf install -y \ python3 \ python3-pip \ polkit \ desktop-file-utils ;; zypper) zypper --non-interactive install \ python3 \ python3-pip \ polkit \ desktop-file-utils ;; apk) apk add \ python3 \ py3-pip \ py3-virtualenv \ polkit \ desktop-file-utils ;; xbps) xbps-install -Sy \ python3 \ python3-pip \ polkit \ desktop-file-utils ;; 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 echo " Python: $SYSTEM_PYTHON" echo " Version: $PYTHON_VERSION" # ============================================================ # Check Python version for PySide6 # ============================================================ if [ "$PYTHON_MINOR" -lt 9 ]; then error "Python $PYTHON_VERSION is too old for this application. Python 3.9 or newer is required." fi success "Python version is supported." # ============================================================ # Create/recreate virtual environment # ============================================================ info "Preparing Python virtual environment" if [ -x "$PYTHON" ]; then success "Existing virtual environment is valid." else if [ -d "$VENV_DIR" ]; then echo " Existing virtual environment is broken." echo " 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. Your distribution may require an additional 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 application # ============================================================ info "Checking Python source files" "$PYTHON" -m compileall \ -q \ "$INSTALL_DIR/app" \ "$INSTALL_DIR/desktop" success "Python source validation passed." # ============================================================ # Remove Python cache files created by installer # ============================================================ find "$INSTALL_DIR/app" \ "$INSTALL_DIR/desktop" \ -type d \ -name "__pycache__" \ -prune \ -exec rm -rf {} + \ 2>/dev/null || true # ============================================================ # Restore source ownership # ============================================================ info "Fixing source ownership" chown -R \ "$INSTALL_USER:$INSTALL_GROUP" \ "$INSTALL_DIR" success "Source tree owned by $INSTALL_USER." # ============================================================ # Prepare data directory # ============================================================ info "Preparing application data" mkdir -p "$INSTALL_DIR/data" # Database is owned by the root service. chown root:root "$INSTALL_DIR/data" chmod 700 "$INSTALL_DIR/data" if [ -f "$INSTALL_DIR/data/parental-control.db" ]; then chown root:root \ "$INSTALL_DIR/data/parental-control.db" chmod 600 \ "$INSTALL_DIR/data/parental-control.db" fi success "Data directory secured." # ============================================================ # Install systemd service # ============================================================ info "Installing systemd service" SERVICE_TEMP="$(mktemp)" sed \ "s|%INSTALL_DIR%|$INSTALL_DIR|g" \ "$INSTALL_DIR/parental-control.service" \ > "$SERVICE_TEMP" # Ensure the service does not write Python bytecode into the # Git repository. 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 "Checking polkit policy" if [ -f "$POLKIT_SOURCE" ]; then mkdir -p /etc/polkit-1/actions install \ -o root \ -g root \ -m 644 \ "$POLKIT_SOURCE" \ "$POLKIT_DEST" success "Polkit policy installed." else warning "Polkit policy file is not present yet." warning "Expected: $POLKIT_SOURCE" warning "Skipping polkit policy installation." fi # ============================================================ # Install desktop launcher # ============================================================ info "Installing desktop launcher" cat > "$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 # ============================================================ # 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 # ============================================================ 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 # ============================================================ # Verify database # ============================================================ info "Checking database" if [ -f "$INSTALL_DIR/data/parental-control.db" ]; then DB_OWNER="$(stat -c '%U:%G' "$INSTALL_DIR/data/parental-control.db")" DB_MODE="$(stat -c '%a' "$INSTALL_DIR/data/parental-control.db")" echo " Owner: $DB_OWNER" echo " Permissions: $DB_MODE" else warning "Database does not exist yet." warning "The service should create it automatically." 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: NOT INSTALLED" 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 "Rofi:" echo " Super + D" echo echo "Service:" echo " systemctl status $SERVICE_NAME" echo echo "Logs:" echo " journalctl -u $SERVICE_NAME" echo echo "IPC:" echo " $SOCKET_PATH" echo echo "Installation:" echo " $INSTALL_DIR" echo echo "==========================================" echo