788 lines
17 KiB
Bash
Executable File
788 lines
17 KiB
Bash
Executable File
#!/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" <<EOF
|
|
[Desktop Entry]
|
|
Name=$APP_NAME
|
|
Comment=Linux Parental Control Manager
|
|
Exec=$PYTHON -m desktop.main
|
|
Path=$INSTALL_DIR
|
|
Icon=preferences-system-parental-controls
|
|
Terminal=false
|
|
Type=Application
|
|
Categories=Settings;
|
|
StartupNotify=true
|
|
EOF
|
|
|
|
chmod 644 "$DESKTOP_FILE"
|
|
|
|
success "Desktop launcher installed."
|
|
|
|
|
|
# ============================================================
|
|
# Validate desktop launcher
|
|
# ============================================================
|
|
|
|
if command -v desktop-file-validate >/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
|