hermes-sshx-link/dashboard/plugin_api.py

132 lines
3.8 KiB
Python
Raw Normal View History

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
logger = logging.getLogger(__name__)
router = APIRouter()
_sshx_process = None
_sshx_link = None
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 _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():
global _sshx_process, _sshx_link
if _sshx_process and _sshx_process.poll() is None 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"})
if _sshx_process and _sshx_process.poll() is None:
_sshx_process.terminate()
try:
_sshx_process.wait(timeout=5)
except subprocess.TimeoutExpired:
_sshx_process.kill()
_sshx_process = subprocess.Popen(
["sshx", "--quiet"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
_sshx_link = None
loop = asyncio.get_event_loop()
link_future = loop.create_future()
_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"})
except Exception as e:
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.get("/status")
async def sshx_status():
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}
@router.post("/stop")
async def stop_sshx():
global _sshx_process, _sshx_link
if _sshx_process and _sshx_process.poll() is None:
_sshx_process.terminate()
try:
_sshx_process.wait(timeout=5)
except subprocess.TimeoutExpired:
_sshx_process.kill()
_sshx_process = None
_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)})