""" 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"}