import asyncio import subprocess import re import os import shutil import logging import time import threading from typing import Optional from fastapi import APIRouter from fastapi.responses import JSONResponse logger = logging.getLogger(__name__) router = APIRouter() _sshx_link = None _sshx_pid = None _sshx_log = "/tmp/sshx_link.log" LINK_RE = re.compile(r"https://sshx\.io/s/[A-Za-z0-9_-]+") def _ensure_sshx() -> bool: if shutil.which("sshx"): return True try: subprocess.run( ["sh", "-c", "curl -sSf https://sshx.io/get | sh"], capture_output=True, timeout=60, ) return shutil.which("sshx") is not None except Exception as e: logger.error("sshx install failed: %s", e) return False def _kill_sshx(): global _sshx_pid, _sshx_link if _sshx_pid: try: os.kill(_sshx_pid, 15) except ProcessLookupError: pass _sshx_pid = None _sshx_link = None if os.path.exists(_sshx_log): os.remove(_sshx_log) def _is_alive() -> bool: global _sshx_pid if not _sshx_pid: return False try: os.kill(_sshx_pid, 0) return True except ProcessLookupError: return False def _start_sshx_background(): global _sshx_pid, _sshx_link _kill_sshx() _sshx_link = None subprocess.Popen( ["nohup", "sshx", "--quiet"], stdout=open(_sshx_log, "w"), stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, start_new_session=True, ) for _ in range(30): time.sleep(1) if os.path.exists(_sshx_log) and os.path.getsize(_sshx_log) > 0: with open(_sshx_log) as f: content = f.read().strip() m = LINK_RE.search(content) if m: _sshx_link = m.group(0) try: with open("/tmp/sshx_pid") as pf: _sshx_pid = int(pf.read().strip()) except Exception: pass return True return False @router.get("/start") async def start_sshx(): global _sshx_link, _sshx_pid if _is_alive() and _sshx_link: return {"status": "running", "link": _sshx_link} if not _ensure_sshx(): return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to install sshx"}) loop = asyncio.get_event_loop() result = await loop.run_in_executor(None, _start_sshx_background) if result: return {"status": "running", "link": _sshx_link} return JSONResponse(status_code=504, content={"status": "error", "message": "sshx did not return a link in time"}) @router.get("/status") async def sshx_status(): global _sshx_link if _is_alive() and _sshx_link: return {"status": "running", "link": _sshx_link} return {"status": "stopped", "link": None} @router.post("/stop") async def stop_sshx(): _kill_sshx() return {"status": "stopped"} @router.post("/restart-dashboard") async def restart_dashboard(): try: subprocess.Popen( ["nohup", "bash", "-c", "sleep 1 && sudo systemctl restart hermes-dashboard"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, ) return {"status": "restarting"} except Exception as e: return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})