hermes-sshx-link/dashboard/plugin_api.py

138 lines
3.8 KiB
Python

"""
sshx-link dashboard backend
===========================
FastAPI routes for starting sshx and returning the share link.
"""
import asyncio
import subprocess
import re
import shutil
import logging
from typing import Optional
from fastapi import APIRouter
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
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,
)
return shutil.which("sshx") is not None
except Exception as e:
logger.error("sshx install failed: %s", e)
return False
@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"},
)
# Kill old process if any
if _sshx_process and _sshx_process.poll() is None:
_sshx_process.terminate()
_sshx_process.wait(timeout=5)
# Start sshx serve
_sshx_process = subprocess.Popen(
["sshx", "--quiet"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
_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()
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():
"""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}
@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:
_sshx_process.wait(timeout=5)
except subprocess.TimeoutExpired:
_sshx_process.kill()
_sshx_process = None
_sshx_link = None
return {"status": "stopped"}
return {"status": "already_stopped"}