hermes-sshx-link/dashboard/plugin_api.py

148 lines
4.6 KiB
Python

import subprocess
import os
import logging
import time
import platform
import stat
import urllib.request
import re
import tarfile
import tempfile
import shutil
import select
from typing import Optional
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
logger = logging.getLogger(__name__)
router = APIRouter()
_sshx_link = None
_sshx_process: Optional[subprocess.Popen] = None
_sshx_installing = False
_sshx_bin = "/tmp/sshx"
_sshx_bin_alt = os.path.expanduser("~/.local/bin/sshx")
_sshx_tar = "/tmp/sshx.tar.gz"
_sshx_last_output = ""
_sshx_last_error = ""
LINK_RE = re.compile(r"https://sshx\.io/s/[A-Za-z0-9\-_]+(?:#[^\s\"']*)?")
def _detect_sshx_arch():
machine = platform.machine().lower()
system = platform.system().lower()
if system == "darwin":
suffix = "-apple-darwin"
if machine in ("aarch64", "arm64", "armv8l", "armv8b"):
arch = "aarch64"
elif machine in ("x86_64", "x64", "amd64"):
arch = "x86_64"
else:
arch = "aarch64" if "arm" in machine else "x86_64"
return arch, suffix
suffix = "-unknown-linux-musl"
if machine in ("aarch64", "aarch64_be", "arm64", "armv8b", "armv8l"):
arch = "aarch64"
elif machine in ("x86_64", "x64", "amd64"):
arch = "x86_64"
elif machine == "armv6l":
arch = "arm"
suffix += "eabihf"
elif machine == "armv7l":
arch = "armv7"
suffix += "eabihf"
else:
arch = "x86_64"
return arch, suffix
def _get_sshx_url() -> str:
arch, suffix = _detect_sshx_arch()
url = f"https://s3.amazonaws.com/sshx/sshx-{arch}{suffix}.tar.gz"
return url
def _find_sshx_bin() -> Optional[str]:
for p in [_sshx_bin, _sshx_bin_alt, shutil.which("sshx")]:
if p and os.path.exists(p) and os.access(p, os.X_OK):
return p
return None
def _is_alive() -> bool:
global _sshx_process
if not _sshx_process:
return False
return _sshx_process.poll() is None
@router.get("/status")
async def sshx_status():
global _sshx_link, _sshx_installing, _sshx_process
# passive mode: only report current link, no auto-start
# also try to detect externally running sshx (not managed by plugin) via ps
if _is_alive() and _sshx_link:
return {"status": "running", "link": _sshx_link, "pid": _sshx_process.pid}
if _sshx_installing:
return {"status": "installing", "link": None}
if _is_alive() and not _sshx_link:
return {"status": "starting", "link": None, "pid": _sshx_process.pid}
# no auto background process - just report stopped, frontend will only show link if exists
# also check if link was persisted to /tmp/sshx_link.txt by external runner
try:
if os.path.exists("/tmp/sshx_link.txt"):
with open("/tmp/sshx_link.txt", "r") as f:
txt = f.read().strip()
m = LINK_RE.search(txt)
if m:
return {"status": "running", "link": m.group(0), "pid": None, "source": "file"}
except Exception:
pass
return {"status": "stopped", "link": None}
@router.get("/debug")
async def debug_info():
return {
"bin": _find_sshx_bin(),
"is_alive": _is_alive(),
"pid": _sshx_process.pid if _sshx_process and _is_alive() else None,
"link": _sshx_link,
"last_error": _sshx_last_error,
"last_output_tail": _sshx_last_output[-600:] if _sshx_last_output else "",
"platform": platform.platform(),
}
# keep /start for manual use but do NOT auto-call it; hermes kills background shell so keep it minimal
@router.get("/start")
async def start_sshx(request: Request):
return JSONResponse(status_code=410, content={"status": "disabled", "message": "Auto background sshx disabled — hermes kills shell. Run sshx manually on host (e.g. nohup sshx --quiet > /tmp/sshx_link.txt 2>&1 &) and dashboard will display the link from /tmp/sshx_link.txt"})
@router.post("/start")
async def start_sshx_post(request: Request):
return await start_sshx(request)
@router.post("/stop")
async def stop_sshx():
global _sshx_process, _sshx_link
_sshx_process = None
_sshx_link = None
return {"status": "stopped"}
@router.get("/stop")
async def stop_sshx_get():
global _sshx_process, _sshx_link
_sshx_process = None
_sshx_link = None
return {"status": "stopped"}
@router.get("/restart-dashboard")
async def restart_dashboard():
return {"status": "deprecated", "message": "No restart needed."}
@router.post("/restart-dashboard")
async def restart_dashboard_post():
return {"status": "deprecated", "message": "No restart needed."}