feat: add autostart lifecycle controls

This commit is contained in:
thePR0M3TH3AN committed 2026-03-19 16:15:51 -04:00
1 parent 2dd0cf4ed8
commit 2de7ea073c
7 files changed
+152 -4

No files matched your search

+19 -1
View File
@@ -274,6 +274,13 @@ def print_report(report_type: str, json_output: bool = False):
console.print(f"Tier: {report['tier']}")
if report["hosting_model"]:
console.print(f"Hosting model: {report['hosting_model']}")
summary = report.get("summary", {})
if summary:
console.print(f"Overall: {'OK' if summary.get('ok') else 'WARN'}")
for section in report.get("sections", {}).values():
if not section.get("checks"):
continue
console.print(f"{section['title']}: {'OK' if section.get('ok') else 'WARN'}")
for check in report["checks"]:
style = "green" if check["ok"] else "red"
state = "OK" if check["ok"] else "FAIL"
@@ -1701,6 +1708,15 @@ def install_autostart():
"Run 'voxvera start-all' manually or add it to your init system.")
def uninstall_autostart():
"""Remove the platform-appropriate autostart mechanism."""
adapter = get_platform_adapter(cli_module=sys.modules[__name__])
try:
adapter.uninstall_autostart()
except NotImplementedError:
print(f"Autostart uninstall not supported on {adapter.label}.")
def _find_voxvera_bin() -> str:
"""Return the absolute path to the voxvera binary/script."""
voxvera_bin = shutil.which("voxvera")
@@ -1989,7 +2005,7 @@ def main(argv=None):
sub.add_parser("start-all", help="Start all configured sites (non-interactive)")
sub.add_parser("stop-all", help="Stop all running sites (non-interactive)")
p_autostart = sub.add_parser("autostart", help="Install or inspect platform autostart support")
p_autostart.add_argument("action", nargs="?", choices=["install", "status"], default="install")
p_autostart.add_argument("action", nargs="?", choices=["install", "status", "uninstall"], default="install")
p_autostart.add_argument("--json", action="store_true", help="Emit machine-readable JSON for status")
sub.add_parser("rebuild-all", help="Rebuild all existing sites from the current template (preserves keys and config)")
sub.add_parser("_internal_onionshare", help=argparse.SUPPRESS)
@@ -2073,6 +2089,8 @@ def main(argv=None):
elif args.command == "autostart":
if args.action == "status":
print_autostart_status(json_output=args.json)
elif args.action == "uninstall":
uninstall_autostart()
else:
install_autostart()
elif args.command == "quickstart":
+26
View File
@@ -77,6 +77,9 @@ class PlatformAdapter:
def install_autostart(self) -> None:
raise NotImplementedError("Autostart is not implemented for this platform.")
def uninstall_autostart(self) -> None:
raise NotImplementedError("Autostart uninstall is not implemented for this platform.")
def platform_status(self) -> dict:
entry = self.support_entry()
matrix = self.load_support_matrix()
@@ -92,6 +95,23 @@ class PlatformAdapter:
"next_gaps": entry.get("next_gaps", []),
}
def _doctor_section(self, title: str, check_names: list[str], checks: list[dict]) -> dict:
section_checks = [check for check in checks if check["name"] in check_names]
return {
"title": title,
"ok": all(check.get("ok") for check in section_checks) if section_checks else True,
"checks": section_checks,
}
def build_doctor_sections(self, checks: list[dict]) -> dict:
return {
"platform_contract": self._doctor_section("Platform contract", ["platform"], checks),
"dependencies": self._doctor_section("Dependencies", ["voxvera_cli", "onionshare_cli"], checks),
"network": self._doctor_section("Network", [], checks),
"autostart": self._doctor_section("Autostart", [], checks),
"content_state": self._doctor_section("Content state", [], checks),
}
def doctor_report(self) -> dict:
checks = []
voxvera_path = shutil.which("voxvera")
@@ -120,6 +140,7 @@ class PlatformAdapter:
}
)
sections = self.build_doctor_sections(checks)
return {
"platform_id": self.platform_id,
"label": platform_status["label"],
@@ -130,6 +151,11 @@ class PlatformAdapter:
"required_capabilities": platform_status["required_capabilities"],
"current_notes": platform_status["current_notes"],
"next_gaps": platform_status["next_gaps"],
"summary": {
"ok": all(check.get("ok") for check in checks),
"failing_checks": [check["name"] for check in checks if not check.get("ok")],
},
"sections": sections,
"checks": checks,
}
+28
View File
@@ -138,6 +138,23 @@ Persistent=true
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"]
@@ -191,4 +208,15 @@ Persistent=true
"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
+5
View File
@@ -88,3 +88,8 @@ class MacOSPlatformAdapter(PlatformAdapter):
print(f"LaunchAgent installed: {self.plist_path}")
print("Sites will start automatically on login.")
print(f" Disable with: launchctl unload {self.plist_path}")
def uninstall_autostart(self) -> None:
self._run_capture(["launchctl", "unload", str(self.plist_path)])
self.plist_path.unlink(missing_ok=True)
print(f"Removed LaunchAgent: {self.plist_path}")
+7
View File
@@ -52,3 +52,10 @@ class WindowsPlatformAdapter(PlatformAdapter):
print(f' Remove with: schtasks /Delete /TN "{self.task_name}" /F')
return
print("Failed to create scheduled task. Try running as Administrator.")
def uninstall_autostart(self) -> None:
ok, _output = self._run_capture(["schtasks", "/Delete", "/TN", self.task_name, "/F"])
if ok:
print(f"Windows scheduled task '{self.task_name}' removed.")
return
print(f"Windows scheduled task '{self.task_name}' could not be removed.")