diff --git a/dashboard/plugin_api.py b/dashboard/plugin_api.py index 59eaece..9964f50 100644 --- a/dashboard/plugin_api.py +++ b/dashboard/plugin_api.py @@ -1,14 +1,9 @@ -""" -sshx-link dashboard backend -=========================== -FastAPI routes for starting sshx and returning the share link. -""" - import asyncio import subprocess import re import shutil import logging +import threading from typing import Optional from fastapi import APIRouter from fastapi.responses import JSONResponse @@ -16,26 +11,19 @@ from fastapi.responses import JSONResponse logger = logging.getLogger(__name__) router = APIRouter() -# Keep track of running sshx process _sshx_process = None _sshx_link = None - -def _find_link(text: str) -> Optional[str]: - """Extract sshx link from output.""" - match = re.search(r"https://sshx\.io/s/[A-Za-z0-9_-]+", text) - return match.group(0) if match else None +LINK_RE = re.compile(r"https://sshx\.io/s/[A-Za-z0-9_-]+") def _ensure_sshx() -> bool: - """Install sshx if not present.""" if shutil.which("sshx"): return True try: subprocess.run( ["sh", "-c", "curl -sSf https://sshx.io/get | sh"], - capture_output=True, - timeout=60, + capture_output=True, timeout=60, ) return shutil.which("sshx") is not None except Exception as e: @@ -43,28 +31,49 @@ def _ensure_sshx() -> bool: return False +def _start_reader(proc, future): + global _sshx_link + + def _read(): + try: + found = False + for line in proc.stdout: + line = line.strip() + if not found: + m = LINK_RE.search(line) + if m: + _sshx_link = m.group(0) + found = True + if not future.done(): + future.set_result(_sshx_link) + if not found and not future.done(): + future.set_exception(RuntimeError("sshx exited without link")) + except Exception as e: + if not future.done(): + future.set_exception(e) + + t = threading.Thread(target=_read, daemon=True) + t.start() + return t + + @router.get("/start") async def start_sshx(): - """Start sshx serve and return the share link.""" global _sshx_process, _sshx_link - # Already running if _sshx_process and _sshx_process.poll() is None and _sshx_link: return {"status": "running", "link": _sshx_link} - # Install if needed if not _ensure_sshx(): - return JSONResponse( - status_code=500, - content={"status": "error", "message": "Failed to install sshx"}, - ) + return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to install sshx"}) - # Kill old process if any if _sshx_process and _sshx_process.poll() is None: _sshx_process.terminate() - _sshx_process.wait(timeout=5) + try: + _sshx_process.wait(timeout=5) + except subprocess.TimeoutExpired: + _sshx_process.kill() - # Start sshx serve _sshx_process = subprocess.Popen( ["sshx", "--quiet"], stdout=subprocess.PIPE, @@ -74,47 +83,22 @@ async def start_sshx(): ) _sshx_link = None - - # Read output in background thread, await link loop = asyncio.get_event_loop() link_future = loop.create_future() - - def _reader(): - global _sshx_link - for line in _sshx_process.stdout: - line = line.strip() - link = _find_link(line) - if link: - _sshx_link = link - if not link_future.done(): - link_future.set_result(link) - return - if not link_future.done(): - link_future.set_exception(RuntimeError("sshx exited without link")) - - import threading - threading.Thread(target=_reader, daemon=True).start() + _start_reader(_sshx_process, link_future) try: link = await asyncio.wait_for(link_future, timeout=30) return {"status": "running", "link": link} except asyncio.TimeoutError: - return JSONResponse( - status_code=504, - content={"status": "error", "message": "sshx did not return a link in time"}, - ) + return JSONResponse(status_code=504, content={"status": "error", "message": "sshx did not return a link in time"}) except Exception as e: - return JSONResponse( - status_code=500, - content={"status": "error", "message": str(e)}, - ) + return JSONResponse(status_code=500, content={"status": "error", "message": str(e)}) @router.get("/status") async def sshx_status(): - """Check if sshx is running and return the link.""" global _sshx_process, _sshx_link - if _sshx_process and _sshx_process.poll() is None and _sshx_link: return {"status": "running", "link": _sshx_link} return {"status": "stopped", "link": None} @@ -122,9 +106,7 @@ async def sshx_status(): @router.post("/stop") async def stop_sshx(): - """Stop the running sshx process.""" global _sshx_process, _sshx_link - if _sshx_process and _sshx_process.poll() is None: _sshx_process.terminate() try: @@ -135,3 +117,15 @@ async def stop_sshx(): _sshx_link = None return {"status": "stopped"} return {"status": "already_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, + ) + return {"status": "restarting"} + except Exception as e: + return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})