""" 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 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) -> str | None: """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", "serve"], 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"}