Files
kiddo/backend/actions.py

172 lines
4.9 KiB
Python

import logging
import shutil
import subprocess
import time
from typing import List, Optional
from backend.settings import get_settings
logger = logging.getLogger(__name__)
class ActionError(Exception):
pass
def _run(cmd: List[str], env: Optional[dict] = None) -> subprocess.CompletedProcess:
"""Run a command respecting dry-run mode."""
settings = get_settings()
if settings.dry_run:
logger.info("[dry-run] %s", " ".join(cmd))
return subprocess.CompletedProcess(cmd, 0, stdout="[dry-run]", stderr="")
return subprocess.run(cmd, check=True, capture_output=True, text=True, env=env)
def _user_exists(user: str) -> bool:
try:
return _run(["id", user]).returncode == 0
except subprocess.CalledProcessError:
return False
def _get_uid(user: str) -> Optional[str]:
try:
result = _run(["id", "-u", user])
return result.stdout.strip()
except subprocess.CalledProcessError:
return None
def _is_logged_in(user: str) -> bool:
try:
result = _run(["who"])
for line in result.stdout.splitlines():
if line.strip() == "[dry-run]":
continue
if line.startswith(f"{user} "):
return True
except subprocess.CalledProcessError:
logger.warning("Could not determine login status for %s", user)
return False
def _play_sound_if_available() -> Optional[str]:
settings = get_settings()
if not settings.sound_player:
return "sound player not configured"
if not shutil.which(settings.sound_player):
return f"{settings.sound_player} not found"
try:
_run([settings.sound_player, settings.sound_file])
except subprocess.CalledProcessError as exc:
logger.warning("Sound failed: %s", exc)
return "sound failed"
return None
def _notify_user(user: str, message: str) -> Optional[str]:
settings = get_settings()
notify = shutil.which(settings.notify_send_path)
if not notify:
return "notify-send not available"
uid = _get_uid(user)
if not uid:
return "could not resolve uid for notification"
env = {
"DISPLAY": ":0",
"DBUS_SESSION_BUS_ADDRESS": f"unix:path=/run/user/{uid}/bus",
}
cmd = [
"sudo",
"-u",
user,
notify,
"Safe Kiddo",
message,
"-t",
str(settings.notify_timeout * 1000),
]
try:
_run(cmd, env=env)
except subprocess.CalledProcessError as exc:
logger.warning("Notification failed: %s", exc)
return "notification failed"
return None
def disable_user(
user: str,
countdown: Optional[int] = None,
sound: Optional[bool] = None,
message: Optional[str] = None,
) -> List[str]:
settings = get_settings()
if not _user_exists(user):
raise ActionError(f"user '{user}' not found")
countdown_seconds = settings.default_countdown if countdown is None else countdown
play_sound = settings.default_sound if sound is None else sound
steps: List[str] = []
_run(["sudo", "usermod", "-L", user])
steps.append("account locked")
logged_in = _is_logged_in(user)
if logged_in:
steps.append("user is logged in")
info_message = message or f"Shutdown in {countdown_seconds} seconds. Save your work."
notify_result = _notify_user(user, info_message)
if notify_result:
steps.append(notify_result)
if play_sound:
sound_result = _play_sound_if_available()
if sound_result:
steps.append(sound_result)
if countdown_seconds > 0 and not settings.dry_run:
for remaining in range(countdown_seconds, 0, -1):
time.sleep(1)
if remaining % 10 == 0 or remaining <= 5:
_notify_user(user, f"Shutdown in {remaining} seconds.")
if play_sound:
_play_sound_if_available()
try:
_run(["sudo", "pkill", "-KILL", "-u", user])
steps.append("sessions terminated")
except subprocess.CalledProcessError as exc:
if exc.returncode == 1:
steps.append("no sessions to terminate")
else:
raise
if logged_in:
_run(["sudo", "shutdown", "now"])
steps.append("system shutdown triggered")
else:
steps.append("no active session; shutdown skipped")
return steps
def enable_user(user: str) -> List[str]:
if not _user_exists(user):
raise ActionError(f"user '{user}' not found")
_run(["sudo", "usermod", "-U", user])
return ["account unlocked"]
def list_logged_in_users() -> List[str]:
try:
result = _run(["who"])
users: List[str] = []
for line in result.stdout.splitlines():
if line.strip() in ("", "[dry-run]"):
continue
users.append(line.split()[0])
return users
except subprocess.CalledProcessError:
return []