fix: keep sshx output reader alive after link, add restart-dashboard endpoint
This commit is contained in:
parent
dbaa655a9e
commit
4111590b18
|
|
@ -1,14 +1,9 @@
|
||||||
"""
|
|
||||||
sshx-link dashboard backend
|
|
||||||
===========================
|
|
||||||
FastAPI routes for starting sshx and returning the share link.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
@ -16,26 +11,19 @@ from fastapi.responses import JSONResponse
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
# Keep track of running sshx process
|
|
||||||
_sshx_process = None
|
_sshx_process = None
|
||||||
_sshx_link = None
|
_sshx_link = None
|
||||||
|
|
||||||
|
LINK_RE = re.compile(r"https://sshx\.io/s/[A-Za-z0-9_-]+")
|
||||||
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:
|
def _ensure_sshx() -> bool:
|
||||||
"""Install sshx if not present."""
|
|
||||||
if shutil.which("sshx"):
|
if shutil.which("sshx"):
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["sh", "-c", "curl -sSf https://sshx.io/get | sh"],
|
["sh", "-c", "curl -sSf https://sshx.io/get | sh"],
|
||||||
capture_output=True,
|
capture_output=True, timeout=60,
|
||||||
timeout=60,
|
|
||||||
)
|
)
|
||||||
return shutil.which("sshx") is not None
|
return shutil.which("sshx") is not None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -43,28 +31,49 @@ def _ensure_sshx() -> bool:
|
||||||
return False
|
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")
|
@router.get("/start")
|
||||||
async def start_sshx():
|
async def start_sshx():
|
||||||
"""Start sshx serve and return the share link."""
|
|
||||||
global _sshx_process, _sshx_link
|
global _sshx_process, _sshx_link
|
||||||
|
|
||||||
# Already running
|
|
||||||
if _sshx_process and _sshx_process.poll() is None and _sshx_link:
|
if _sshx_process and _sshx_process.poll() is None and _sshx_link:
|
||||||
return {"status": "running", "link": _sshx_link}
|
return {"status": "running", "link": _sshx_link}
|
||||||
|
|
||||||
# Install if needed
|
|
||||||
if not _ensure_sshx():
|
if not _ensure_sshx():
|
||||||
return JSONResponse(
|
return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to install sshx"})
|
||||||
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:
|
if _sshx_process and _sshx_process.poll() is None:
|
||||||
_sshx_process.terminate()
|
_sshx_process.terminate()
|
||||||
|
try:
|
||||||
_sshx_process.wait(timeout=5)
|
_sshx_process.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
_sshx_process.kill()
|
||||||
|
|
||||||
# Start sshx serve
|
|
||||||
_sshx_process = subprocess.Popen(
|
_sshx_process = subprocess.Popen(
|
||||||
["sshx", "--quiet"],
|
["sshx", "--quiet"],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
|
|
@ -74,47 +83,22 @@ async def start_sshx():
|
||||||
)
|
)
|
||||||
|
|
||||||
_sshx_link = None
|
_sshx_link = None
|
||||||
|
|
||||||
# Read output in background thread, await link
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
link_future = loop.create_future()
|
link_future = loop.create_future()
|
||||||
|
_start_reader(_sshx_process, link_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:
|
try:
|
||||||
link = await asyncio.wait_for(link_future, timeout=30)
|
link = await asyncio.wait_for(link_future, timeout=30)
|
||||||
return {"status": "running", "link": link}
|
return {"status": "running", "link": link}
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
return JSONResponse(
|
return JSONResponse(status_code=504, content={"status": "error", "message": "sshx did not return a link in time"})
|
||||||
status_code=504,
|
|
||||||
content={"status": "error", "message": "sshx did not return a link in time"},
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JSONResponse(
|
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
|
||||||
status_code=500,
|
|
||||||
content={"status": "error", "message": str(e)},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/status")
|
@router.get("/status")
|
||||||
async def sshx_status():
|
async def sshx_status():
|
||||||
"""Check if sshx is running and return the link."""
|
|
||||||
global _sshx_process, _sshx_link
|
global _sshx_process, _sshx_link
|
||||||
|
|
||||||
if _sshx_process and _sshx_process.poll() is None and _sshx_link:
|
if _sshx_process and _sshx_process.poll() is None and _sshx_link:
|
||||||
return {"status": "running", "link": _sshx_link}
|
return {"status": "running", "link": _sshx_link}
|
||||||
return {"status": "stopped", "link": None}
|
return {"status": "stopped", "link": None}
|
||||||
|
|
@ -122,9 +106,7 @@ async def sshx_status():
|
||||||
|
|
||||||
@router.post("/stop")
|
@router.post("/stop")
|
||||||
async def stop_sshx():
|
async def stop_sshx():
|
||||||
"""Stop the running sshx process."""
|
|
||||||
global _sshx_process, _sshx_link
|
global _sshx_process, _sshx_link
|
||||||
|
|
||||||
if _sshx_process and _sshx_process.poll() is None:
|
if _sshx_process and _sshx_process.poll() is None:
|
||||||
_sshx_process.terminate()
|
_sshx_process.terminate()
|
||||||
try:
|
try:
|
||||||
|
|
@ -135,3 +117,15 @@ async def stop_sshx():
|
||||||
_sshx_link = None
|
_sshx_link = None
|
||||||
return {"status": "stopped"}
|
return {"status": "stopped"}
|
||||||
return {"status": "already_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)})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue