optimized installer
This commit is contained in:
+607
@@ -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()
|
||||
@@ -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__":
|
||||
|
||||
+710
-78
@@ -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
|
||||
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" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=$APP_NAME
|
||||
Comment=Linux Parental Control Manager
|
||||
Exec=$INSTALL_DIR/.venv/bin/python -m desktop.main
|
||||
Exec=$PYTHON -m desktop.main
|
||||
Path=$INSTALL_DIR
|
||||
Icon=preferences-system-parental-controls
|
||||
Terminal=false
|
||||
@@ -115,27 +530,231 @@ EOF
|
||||
|
||||
chmod 644 "$DESKTOP_FILE"
|
||||
|
||||
echo
|
||||
echo "Desktop launcher installed:"
|
||||
echo " $DESKTOP_FILE"
|
||||
success "Desktop launcher installed."
|
||||
|
||||
# ------------------------------------------
|
||||
# 6. Update desktop database
|
||||
# ------------------------------------------
|
||||
|
||||
echo
|
||||
echo "[6/6] Updating application database"
|
||||
# ============================================================
|
||||
# Validate desktop launcher
|
||||
# ============================================================
|
||||
|
||||
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."
|
||||
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
|
||||
|
||||
# ------------------------------------------
|
||||
# Verify installation
|
||||
# ------------------------------------------
|
||||
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 "=========================================="
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
PySide6==6.11.2
|
||||
PySide6_Addons==6.11.2
|
||||
PySide6_Essentials==6.11.2
|
||||
shiboken6==6.11.2
|
||||
|
||||
Reference in New Issue
Block a user