From 298124c30fcef0794a56167b65c54e4f07ceb827 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 17 Sep 2026 13:29:45 +0500 Subject: [PATCH] optimized installer --- app/ipc.py | 607 ++++++++++++++++++++++++++++++++++++ app/main.py | 5 + install.sh | 788 ++++++++++++++++++++++++++++++++++++++++++----- requirements.txt | 3 - 4 files changed, 1322 insertions(+), 81 deletions(-) create mode 100644 app/ipc.py diff --git a/app/ipc.py b/app/ipc.py new file mode 100644 index 0000000..2a25bdb --- /dev/null +++ b/app/ipc.py @@ -0,0 +1,607 @@ +import json +import os +import socket +import struct +import subprocess +import threading +from pathlib import Path + +from .database import get_db + + +SOCKET_PATH = Path("/run/parental-control.sock") + +BUFFER_SIZE = 65536 + +POLKIT_ACTION = "org.sarlink.parentalcontrol.modify" + + +def get_peer_credentials(connection): + credentials = connection.getsockopt( + socket.SOL_SOCKET, + socket.SO_PEERCRED, + struct.calcsize("3i"), + ) + + return struct.unpack("3i", credentials) + + +def authorize_write(pid: int) -> bool: + try: + os.kill(pid, 0) + except OSError: + return False + + try: + result = subprocess.run( + [ + "pkcheck", + "--action-id", + POLKIT_ACTION, + "--process", + str(pid), + "--allow-user-interaction", + ], + capture_output=True, + text=True, + check=False, + ) + + return result.returncode == 0 + + except FileNotFoundError: + return False + + +class IPCOperations: + + @staticmethod + def get_users(): + with get_db() as db: + rows = db.execute( + """ + SELECT * + FROM users + ORDER BY username COLLATE NOCASE + """ + ).fetchall() + + return [dict(row) for row in rows] + + @staticmethod + def get_user(user_id): + with get_db() as db: + row = db.execute( + """ + SELECT * + FROM users + WHERE id = ? + """, + (user_id,), + ).fetchone() + + return dict(row) if row else None + + @staticmethod + def get_remaining_time(user_id): + with get_db() as db: + allowance_row = db.execute( + """ + SELECT COALESCE( + allowance_seconds, + 0 + ) AS total + FROM daily_allowances + WHERE user_id = ? + AND weekday = CAST( + strftime('%w', 'now', 'localtime') + AS INTEGER + ) + """, + (user_id,), + ).fetchone() + + allowance = ( + allowance_row["total"] + if allowance_row + else 0 + ) + + usage_row = db.execute( + """ + SELECT COALESCE( + used_seconds, + 0 + ) AS total + FROM usage + WHERE user_id = ? + AND date = date('now', 'localtime') + """, + (user_id,), + ).fetchone() + + used = ( + usage_row["total"] + if usage_row + else 0 + ) + + remaining = max( + allowance - used, + 0, + ) + + return remaining, allowance + + @staticmethod + def get_allowances(user_id): + with get_db() as db: + rows = db.execute( + """ + SELECT weekday, allowance_seconds + FROM daily_allowances + WHERE user_id = ? + """, + (user_id,), + ).fetchall() + + return { + row["weekday"]: row["allowance_seconds"] + for row in rows + } + + @staticmethod + def get_access_windows(user_id): + with get_db() as db: + rows = db.execute( + """ + SELECT weekday, start_minute, end_minute + FROM access_windows + WHERE user_id = ? + ORDER BY weekday, start_minute + """, + (user_id,), + ).fetchall() + + result = {} + + for row in rows: + result.setdefault( + row["weekday"], + [], + ).append( + ( + row["start_minute"], + row["end_minute"], + ) + ) + + return result + + @staticmethod + def create_user( + username, + enabled, + allowances, + access_windows, + ): + with get_db() as db: + cursor = db.execute( + """ + INSERT INTO users ( + username, + enabled + ) + VALUES (?, ?) + """, + ( + username, + 1 if enabled else 0, + ), + ) + + user_id = cursor.lastrowid + + for weekday, seconds in allowances.items(): + db.execute( + """ + INSERT INTO daily_allowances ( + user_id, + weekday, + allowance_seconds + ) + VALUES (?, ?, ?) + """, + ( + user_id, + weekday, + seconds, + ), + ) + + for weekday, windows in access_windows.items(): + for start_minute, end_minute in windows: + db.execute( + """ + INSERT INTO access_windows ( + user_id, + weekday, + start_minute, + end_minute + ) + VALUES (?, ?, ?, ?) + """, + ( + user_id, + weekday, + start_minute, + end_minute, + ), + ) + + return user_id + + @staticmethod + def update_user( + user_id, + enabled, + allowances, + access_windows, + ): + with get_db() as db: + db.execute( + """ + UPDATE users + SET enabled = ? + WHERE id = ? + """, + ( + 1 if enabled else 0, + user_id, + ), + ) + + for weekday, seconds in allowances.items(): + db.execute( + """ + INSERT INTO daily_allowances ( + user_id, + weekday, + allowance_seconds + ) + VALUES (?, ?, ?) + ON CONFLICT(user_id, weekday) + DO UPDATE SET + allowance_seconds = + excluded.allowance_seconds + """, + ( + user_id, + weekday, + seconds, + ), + ) + + db.execute( + """ + DELETE FROM access_windows + WHERE user_id = ? + """, + (user_id,), + ) + + for weekday, windows in access_windows.items(): + for start_minute, end_minute in windows: + db.execute( + """ + INSERT INTO access_windows ( + user_id, + weekday, + start_minute, + end_minute + ) + VALUES (?, ?, ?, ?) + """, + ( + user_id, + weekday, + start_minute, + end_minute, + ), + ) + + @staticmethod + def delete_user(user_id): + with get_db() as db: + db.execute( + """ + DELETE FROM users + WHERE id = ? + """, + (user_id,), + ) + + @staticmethod + def set_enabled(user_id, enabled): + with get_db() as db: + db.execute( + """ + UPDATE users + SET enabled = ? + WHERE id = ? + """, + ( + 1 if enabled else 0, + user_id, + ), + ) + + @staticmethod + def add_temporary_time( + user_id, + seconds, + ): + with get_db() as db: + db.execute( + """ + INSERT INTO temporary_grants ( + user_id, + seconds, + remaining_seconds + ) + VALUES (?, ?, ?) + """, + ( + user_id, + seconds, + seconds, + ), + ) + + +READ_METHODS = { + "get_users", + "get_user", + "get_remaining_time", + "get_allowances", + "get_access_windows", +} + + +WRITE_METHODS = { + "create_user", + "update_user", + "delete_user", + "set_enabled", + "add_temporary_time", +} + + +def handle_request(request, pid): + method = request.get("method") + arguments = request.get("args", {}) + + if method in READ_METHODS: + operation = getattr( + IPCOperations, + method, + None, + ) + + if operation is None: + raise RuntimeError( + f"Unknown method: {method}" + ) + + return operation(**arguments) + + if method in WRITE_METHODS: + if not authorize_write(pid): + raise PermissionError( + "Administrative authorization required" + ) + + operation = getattr( + IPCOperations, + method, + None, + ) + + if operation is None: + raise RuntimeError( + f"Unknown method: {method}" + ) + + return operation(**arguments) + + raise RuntimeError( + f"Unknown IPC method: {method}" + ) + + +def handle_client(connection): + try: + pid, uid, gid = get_peer_credentials( + connection + ) + + data = bytearray() + + while True: + chunk = connection.recv( + BUFFER_SIZE + ) + + if not chunk: + break + + data.extend(chunk) + + if b"\n" in chunk: + break + + if not data: + return + + line = bytes(data).split( + b"\n", + 1, + )[0] + + request = json.loads( + line.decode("utf-8") + ) + + try: + result = handle_request( + request, + pid, + ) + + response = { + "ok": True, + "result": result, + } + + except PermissionError as exc: + response = { + "ok": False, + "error": str(exc), + "error_type": "authorization", + } + + except Exception as exc: + response = { + "ok": False, + "error": str(exc), + "error_type": "server", + } + + payload = ( + json.dumps( + response, + separators=(",", ":"), + ).encode("utf-8") + + b"\n" + ) + + connection.sendall(payload) + + except Exception: + try: + connection.sendall( + b'{"ok":false,"error":"Invalid request",' + b'"error_type":"server"}\n' + ) + except Exception: + pass + + finally: + connection.close() + + +class IPCServer: + + def __init__( + self, + socket_path=SOCKET_PATH, + ): + self.socket_path = Path( + socket_path + ) + + self.server_socket = None + self._thread = None + self._stop_event = threading.Event() + + def start(self): + if self._thread is not None: + return + + self.socket_path.parent.mkdir( + parents=True, + exist_ok=True, + ) + + try: + self.socket_path.unlink() + except FileNotFoundError: + pass + + self.server_socket = socket.socket( + socket.AF_UNIX, + socket.SOCK_STREAM, + ) + + self.server_socket.bind( + str(self.socket_path) + ) + + os.chmod( + self.socket_path, + 0o666, + ) + + self.server_socket.listen(16) + + self._stop_event.clear() + + self._thread = threading.Thread( + target=self._run, + name="parental-control-ipc", + daemon=True, + ) + + self._thread.start() + + def stop(self): + self._stop_event.set() + + if self.server_socket is not None: + try: + self.server_socket.close() + except Exception: + pass + + if self._thread is not None: + self._thread.join(timeout=2) + + self._thread = None + self.server_socket = None + + try: + self.socket_path.unlink() + except FileNotFoundError: + pass + + def _run(self): + while not self._stop_event.is_set(): + try: + connection, _ = ( + self.server_socket.accept() + ) + + except OSError: + if self._stop_event.is_set(): + break + + continue + + thread = threading.Thread( + target=handle_client, + args=(connection,), + daemon=True, + ) + + thread.start() + + +ipc_server = IPCServer() + + +def start_ipc_server(): + ipc_server.start() + + +def stop_ipc_server(): + ipc_server.stop() diff --git a/app/main.py b/app/main.py index 9bf1e58..03bc2ff 100644 --- a/app/main.py +++ b/app/main.py @@ -2,19 +2,24 @@ import time from .database import initialize_database from .scheduler import start_scheduler, stop_scheduler +from .ipc import start_ipc_server, stop_ipc_server def main(): initialize_database() + start_ipc_server() start_scheduler() try: while True: time.sleep(60) + except KeyboardInterrupt: pass + finally: stop_scheduler() + stop_ipc_server() if __name__ == "__main__": diff --git a/install.sh b/install.sh index 2ab8fd5..cfcacb3 100755 --- a/install.sh +++ b/install.sh @@ -1,110 +1,525 @@ #!/bin/bash -set -e +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 "Please run with sudo:" - echo " sudo ./install.sh" + echo + echo "This installer requires administrator privileges." + echo + echo "Run:" + echo + echo " sudo ./install.sh" + echo exit 1 fi -INSTALL_DIR="$(cd "$(dirname "$0")" && pwd)" -APP_NAME="Parental Control" -DESKTOP_FILE="/usr/share/applications/parental-control.desktop" -SERVICE_FILE="/etc/systemd/system/parental-control.service" +# ============================================================ +# 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 "Installing from:" +echo "Application:" +echo " $APP_NAME" +echo +echo "Installation directory:" echo " $INSTALL_DIR" echo - -# ------------------------------------------ -# 1. System dependencies -# ------------------------------------------ - -echo "[1/6] Installing system dependencies" - -pacman -S --needed --noconfirm \ - python \ - python-pip - +echo "Installation user:" +echo " $INSTALL_USER" echo -echo "System dependencies installed." -# ------------------------------------------ -# 2. Python virtual environment -# ------------------------------------------ -echo -echo "[2/6] Creating Python virtual environment" +# ============================================================ +# Check project files +# ============================================================ -if [ ! -d "$INSTALL_DIR/.venv" ]; then - python -m venv "$INSTALL_DIR/.venv" -else - echo "Virtual environment already exists." +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 -echo -echo "Virtual environment ready." +# shellcheck disable=SC1091 +source /etc/os-release -# ------------------------------------------ -# 3. Python dependencies -# ------------------------------------------ +OS_ID="${ID:-unknown}" +OS_NAME="${NAME:-Unknown Linux}" +OS_VERSION="${VERSION_ID:-unknown}" -echo -echo "[3/6] Installing Python packages" +echo " Distribution: $OS_NAME" +echo " Version: $OS_VERSION" -"$INSTALL_DIR/.venv/bin/python" -m pip install \ + +# ============================================================ +# 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 -"$INSTALL_DIR/.venv/bin/python" -m pip install \ +"$PYTHON" -m pip install \ + --disable-pip-version-check \ -r "$INSTALL_DIR/requirements.txt" -echo -echo "Python packages installed." +success "Python dependencies installed." -# ------------------------------------------ -# 4. Install systemd service -# ------------------------------------------ -echo -echo "[4/6] Installing systemd service" +# ============================================================ +# 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_FILE" + > "$SERVICE_TEMP" -chmod 644 "$SERVICE_FILE" +# Ensure the service does not write Python bytecode into the +# Git repository. -systemctl daemon-reload -systemctl enable parental-control.service -systemctl restart parental-control.service +if ! grep -q '^Environment=PYTHONDONTWRITEBYTECODE=' "$SERVICE_TEMP"; then + sed -i \ + '/^\[Service\]/a Environment=PYTHONDONTWRITEBYTECODE=1' \ + "$SERVICE_TEMP" +fi -echo -echo "Systemd service installed and started." +install \ + -o root \ + -g root \ + -m 644 \ + "$SERVICE_TEMP" \ + "$SERVICE_FILE" -# ------------------------------------------ -# 5. Install desktop application -# ------------------------------------------ +rm -f "$SERVICE_TEMP" -echo -echo "[5/6] Installing application launcher" +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." -if command -v update-desktop-database >/dev/null 2>&1; then - update-desktop-database /usr/share/applications else - echo "update-desktop-database is not installed." - echo "Skipping desktop database update." + warning "desktop-file-validate is not available." fi -# ------------------------------------------ -# Verify installation -# ------------------------------------------ + +# ============================================================ +# 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 "==========================================" @@ -145,11 +764,24 @@ echo echo "Application:" echo " $APP_NAME" echo -echo "Launch it from your application menu." -echo "On your system:" +echo "Launch:" +echo " Application menu" +echo " Rofi" +echo +echo "Rofi:" echo " Super + D" echo echo "Service:" -echo " systemctl status parental-control" +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 diff --git a/requirements.txt b/requirements.txt index 318d753..27c14d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1 @@ PySide6==6.11.2 -PySide6_Addons==6.11.2 -PySide6_Essentials==6.11.2 -shiboken6==6.11.2