mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 09:40:40 +00:00
223 lines
7.8 KiB
Python
223 lines
7.8 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from .base import PlatformAdapter
|
|
|
|
|
|
class LinuxPlatformAdapter(PlatformAdapter):
|
|
platform_id = "linux_cli_systemd"
|
|
tier = "supported"
|
|
label = "Linux CLI with systemd --user"
|
|
|
|
@property
|
|
def user_systemd_dir(self) -> Path:
|
|
return self.home_dir / ".config" / "systemd" / "user"
|
|
|
|
@property
|
|
def service_path(self) -> Path:
|
|
return self.user_systemd_dir / "voxvera-start.service"
|
|
|
|
@property
|
|
def timer_path(self) -> Path:
|
|
return self.user_systemd_dir / "voxvera-start.timer"
|
|
|
|
def autostart_artifacts(self) -> list[str]:
|
|
return [str(self.service_path), str(self.timer_path)]
|
|
|
|
def render_systemd_service(self, voxvera_bin: str) -> str:
|
|
return f"""\
|
|
[Unit]
|
|
Description=VoxVera hidden-service recovery runner
|
|
After=network-online.target tor.service
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart={voxvera_bin} start-all
|
|
"""
|
|
|
|
def render_systemd_timer(self) -> str:
|
|
return """\
|
|
[Unit]
|
|
Description=Retry VoxVera hidden-service startup
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
|
|
[Timer]
|
|
OnBootSec=2min
|
|
OnStartupSec=2min
|
|
OnUnitActiveSec=5min
|
|
Unit=voxvera-start.service
|
|
Persistent=true
|
|
"""
|
|
|
|
def autostart_status(self) -> dict:
|
|
checks = []
|
|
artifacts = self.autostart_artifacts()
|
|
for path in [self.service_path, self.timer_path]:
|
|
checks.append(
|
|
{
|
|
"name": path.name,
|
|
"ok": path.exists(),
|
|
"detail": str(path) if path.exists() else f"missing: {path}",
|
|
}
|
|
)
|
|
|
|
if shutil.which("systemctl"):
|
|
ok, output = self._run_capture(["systemctl", "--user", "is-enabled", "voxvera-start.timer"])
|
|
checks.append(
|
|
{
|
|
"name": "systemd_timer_enabled",
|
|
"ok": ok and output == "enabled",
|
|
"detail": output,
|
|
}
|
|
)
|
|
ok, output = self._run_capture(["systemctl", "--user", "is-active", "voxvera-start.timer"])
|
|
checks.append(
|
|
{
|
|
"name": "systemd_timer_active",
|
|
"ok": ok and output == "active",
|
|
"detail": output,
|
|
}
|
|
)
|
|
else:
|
|
checks.append(
|
|
{
|
|
"name": "systemctl",
|
|
"ok": False,
|
|
"detail": "systemctl not found",
|
|
}
|
|
)
|
|
|
|
supported = all(check["ok"] for check in checks if check["name"] in {"voxvera-start.service", "voxvera-start.timer"})
|
|
return {
|
|
"platform_id": self.platform_id,
|
|
"label": self.label,
|
|
"tier": self.tier,
|
|
"supported": supported,
|
|
"message": "Linux recovery timer is the supported persistent-host path.",
|
|
"artifacts": artifacts,
|
|
"checks": checks,
|
|
}
|
|
|
|
def install_autostart(self) -> None:
|
|
if self.cli is None:
|
|
raise RuntimeError("CLI module is required to install Linux autostart.")
|
|
self.user_systemd_dir.mkdir(parents=True, exist_ok=True)
|
|
voxvera_bin = self.cli._find_voxvera_bin()
|
|
|
|
self.service_path.write_text(self.render_systemd_service(voxvera_bin), encoding="utf-8")
|
|
self.timer_path.write_text(self.render_systemd_timer(), encoding="utf-8")
|
|
|
|
legacy_service = self.user_systemd_dir / "voxvera.service"
|
|
if legacy_service.exists():
|
|
legacy_service.unlink()
|
|
|
|
commands = [
|
|
["systemctl", "--user", "daemon-reload"],
|
|
["systemctl", "--user", "disable", "--now", "voxvera.service"],
|
|
["systemctl", "--user", "enable", "--now", "voxvera-start.timer"],
|
|
["systemctl", "--user", "start", "voxvera-start.service"],
|
|
["loginctl", "enable-linger", os.environ.get("USER", "")],
|
|
]
|
|
for command in commands:
|
|
try:
|
|
subprocess.run(command, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
except Exception:
|
|
pass
|
|
|
|
print(f"Systemd user service installed: {self.service_path}")
|
|
print(f"Systemd user timer installed: {self.timer_path}")
|
|
print("Sites will be retried automatically after boot and every few minutes.")
|
|
print(" Manage with: systemctl --user [start|stop|status] voxvera-start.service")
|
|
print(" Timer status: systemctl --user status voxvera-start.timer")
|
|
|
|
def uninstall_autostart(self) -> None:
|
|
commands = [
|
|
["systemctl", "--user", "disable", "--now", "voxvera-start.timer"],
|
|
["systemctl", "--user", "stop", "voxvera-start.service"],
|
|
["systemctl", "--user", "daemon-reload"],
|
|
]
|
|
for command in commands:
|
|
try:
|
|
subprocess.run(command, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
except Exception:
|
|
pass
|
|
|
|
self.service_path.unlink(missing_ok=True)
|
|
self.timer_path.unlink(missing_ok=True)
|
|
print(f"Removed systemd user service: {self.service_path}")
|
|
print(f"Removed systemd user timer: {self.timer_path}")
|
|
|
|
def doctor_report(self) -> dict:
|
|
report = super().doctor_report()
|
|
checks = report["checks"]
|
|
|
|
tor_found = bool(shutil.which("tor"))
|
|
checks.append(
|
|
{
|
|
"name": "tor_binary",
|
|
"ok": tor_found,
|
|
"detail": shutil.which("tor") or "tor not found on PATH",
|
|
}
|
|
)
|
|
|
|
socks_port = os.getenv("TOR_SOCKS_PORT")
|
|
if not socks_port:
|
|
for candidate in ("9050", "9150"):
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(0.25)
|
|
result = sock.connect_ex(("127.0.0.1", int(candidate)))
|
|
sock.close()
|
|
if result == 0:
|
|
socks_port = candidate
|
|
break
|
|
except Exception:
|
|
pass
|
|
checks.append(
|
|
{
|
|
"name": "tor_socks_reachable",
|
|
"ok": bool(socks_port),
|
|
"detail": f"reachable on {socks_port}" if socks_port else "no Tor SOCKS port detected",
|
|
}
|
|
)
|
|
|
|
status = self.autostart_status()
|
|
timer_check = next((check for check in status["checks"] if check["name"] == "systemd_timer_enabled"), None)
|
|
if timer_check is not None:
|
|
checks.append(
|
|
{
|
|
"name": "autostart_timer_enabled",
|
|
"ok": timer_check["ok"],
|
|
"detail": timer_check["detail"],
|
|
}
|
|
)
|
|
|
|
host_root = self.data_dir / "host"
|
|
checks.append(
|
|
{
|
|
"name": "host_root",
|
|
"ok": host_root.exists(),
|
|
"detail": str(host_root) if host_root.exists() else f"missing: {host_root}",
|
|
}
|
|
)
|
|
report["summary"] = {
|
|
"ok": all(check.get("ok") for check in checks),
|
|
"failing_checks": [check["name"] for check in checks if not check.get("ok")],
|
|
}
|
|
report["sections"] = {
|
|
"platform_contract": self._doctor_section("Platform contract", ["platform"], checks),
|
|
"dependencies": self._doctor_section("Dependencies", ["voxvera_cli", "onionshare_cli", "tor_binary"], checks),
|
|
"network": self._doctor_section("Network", ["tor_socks_reachable"], checks),
|
|
"autostart": self._doctor_section("Autostart", ["autostart_timer_enabled"], checks),
|
|
"content_state": self._doctor_section("Content state", ["host_root"], checks),
|
|
}
|
|
return report
|