feat: auto-install sshx on plugin load + defer dashboard restart + safe JSON parsing
- __init__.py: auto-install sshx in background thread on register() - plugin_api.py: startup event auto-installs sshx, defers dashboard restart via BackgroundTasks - plugin_api.py: _ensure_sshx returns (success, needs_restart) tuple - plugin_api.py: /status returns 'installing' state while auto-install runs - index.js: safeFetchJSON wraps fetch+JSON.parse with error handling - index.js: shows 'Installing sshx...' with polling when status=installing - index.js: checks install status before calling /start
This commit is contained in:
parent
d25b984eac
commit
ffece76498
20
__init__.py
20
__init__.py
|
|
@ -88,8 +88,26 @@ def _run_sshx(tool_ctx):
|
|||
threading.Thread(target=_reader, daemon=True).start()
|
||||
|
||||
|
||||
def _auto_install_sshx():
|
||||
if shutil.which("sshx"):
|
||||
logger.info("sshx already installed at %s", shutil.which("sshx"))
|
||||
return
|
||||
logger.info("Auto-installing sshx...")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["sh", "-c", "curl -sSf https://sshx.io/get | sh"],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
if result.returncode == 0 and shutil.which("sshx"):
|
||||
logger.info("sshx installed successfully")
|
||||
else:
|
||||
logger.error("sshx install failed: %s", result.stderr)
|
||||
except Exception as e:
|
||||
logger.error("sshx auto-install error: %s", e)
|
||||
|
||||
|
||||
def register(ctx):
|
||||
"""Register the sshx_start tool."""
|
||||
threading.Thread(target=_auto_install_sshx, daemon=True).start()
|
||||
ctx.register_tool(
|
||||
name="sshx_start",
|
||||
toolset="sshx-link",
|
||||
|
|
|
|||
|
|
@ -8,8 +8,16 @@
|
|||
|
||||
const fetchJSON = SDK.fetchJSON;
|
||||
|
||||
async function safeFetchJSON(path, opts) {
|
||||
const res = await fetch(path, opts || {});
|
||||
const text = await res.text();
|
||||
try { return JSON.parse(text); } catch (_) {
|
||||
throw new Error("Server returned non-JSON (status " + res.status + ")");
|
||||
}
|
||||
}
|
||||
|
||||
async function apiPost(path) {
|
||||
const res = await fetchJSON(path, { method: "POST" });
|
||||
const res = await safeFetchJSON(path, { method: "POST" });
|
||||
return res;
|
||||
}
|
||||
|
||||
|
|
@ -22,7 +30,12 @@
|
|||
setStatus("loading");
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetchJSON("/api/plugins/sshx-link/start");
|
||||
var res = await safeFetchJSON("/api/plugins/sshx-link/status");
|
||||
if (res.status === "installing") {
|
||||
setStatus("installing");
|
||||
return;
|
||||
}
|
||||
res = await safeFetchJSON("/api/plugins/sshx-link/start");
|
||||
if (res.link) {
|
||||
setLink(res.link);
|
||||
setStatus("running");
|
||||
|
|
@ -38,7 +51,7 @@
|
|||
|
||||
const stopSSHX = useCallback(async function () {
|
||||
try {
|
||||
await apiPost("/api/plugins/sshx-link/stop");
|
||||
await safeFetchJSON("/api/plugins/sshx-link/stop", { method: "POST" });
|
||||
setLink(null);
|
||||
setStatus("idle");
|
||||
} catch (err) {
|
||||
|
|
@ -48,10 +61,12 @@
|
|||
|
||||
const checkStatus = useCallback(async function () {
|
||||
try {
|
||||
const res = await fetchJSON("/api/plugins/sshx-link/status");
|
||||
const res = await safeFetchJSON("/api/plugins/sshx-link/status");
|
||||
if (res.status === "running" && res.link) {
|
||||
setLink(res.link);
|
||||
setStatus("running");
|
||||
} else if (res.status === "installing") {
|
||||
setStatus("installing");
|
||||
}
|
||||
} catch (_) {}
|
||||
}, []);
|
||||
|
|
@ -60,6 +75,28 @@
|
|||
checkStatus();
|
||||
}, [checkStatus]);
|
||||
|
||||
useEffect(function () {
|
||||
if (status !== "installing") return;
|
||||
var timer = setInterval(function () {
|
||||
checkStatus();
|
||||
}, 2000);
|
||||
return function () { clearInterval(timer); };
|
||||
}, [status, checkStatus]);
|
||||
|
||||
if (status === "installing") {
|
||||
return React.createElement(Card, null,
|
||||
React.createElement(CardHeader, null,
|
||||
React.createElement(CardTitle, null, "SSHX Shared Terminal")
|
||||
),
|
||||
React.createElement(CardContent, null,
|
||||
React.createElement("p", {
|
||||
className: "text-sm text-yellow-500",
|
||||
style: { fontWeight: 600 }
|
||||
}, "Installing sshx...")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "running" && link) {
|
||||
return React.createElement(Card, null,
|
||||
React.createElement(CardHeader, null,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import shutil
|
|||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, BackgroundTasks
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -14,28 +14,58 @@ router = APIRouter()
|
|||
|
||||
_sshx_link = None
|
||||
_sshx_pid = None
|
||||
_sshx_installing = False
|
||||
_sshx_log = "/tmp/sshx_link.log"
|
||||
_sshx_pid_file = "/tmp/sshx_link.pid"
|
||||
|
||||
LINK_RE = re.compile(r"https://sshx\.io/s/[A-Za-z0-9_-]+(?:#[A-Za-z0-9_-]+)?")
|
||||
|
||||
import threading as _threading
|
||||
|
||||
def _ensure_sshx() -> bool:
|
||||
already = shutil.which("sshx") is not None
|
||||
if already:
|
||||
return True
|
||||
|
||||
def _auto_install():
|
||||
global _sshx_installing
|
||||
if shutil.which("sshx"):
|
||||
logger.info("sshx already installed at %s", shutil.which("sshx"))
|
||||
return
|
||||
_sshx_installing = True
|
||||
logger.info("Auto-installing sshx...")
|
||||
try:
|
||||
subprocess.run(
|
||||
["sh", "-c", "curl -sSf https://sshx.io/get | sh"],
|
||||
capture_output=True, timeout=60,
|
||||
)
|
||||
if shutil.which("sshx"):
|
||||
logger.info("sshx installed successfully")
|
||||
_trigger_dashboard_restart()
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
logger.error("sshx install failed")
|
||||
except Exception as e:
|
||||
logger.error("sshx auto-install error: %s", e)
|
||||
finally:
|
||||
_sshx_installing = False
|
||||
|
||||
|
||||
@router.on_event("startup")
|
||||
def _startup_auto_install():
|
||||
_threading.Thread(target=_auto_install, daemon=True).start()
|
||||
|
||||
|
||||
def _ensure_sshx() -> tuple[bool, bool]:
|
||||
"""Ensure sshx is installed. Returns (success, needs_restart)."""
|
||||
if shutil.which("sshx"):
|
||||
return True, False
|
||||
try:
|
||||
subprocess.run(
|
||||
["sh", "-c", "curl -sSf https://sshx.io/get | sh"],
|
||||
capture_output=True, timeout=60,
|
||||
)
|
||||
if shutil.which("sshx"):
|
||||
return True, True # newly installed, needs restart
|
||||
return False, False
|
||||
except Exception as e:
|
||||
logger.error("sshx install failed: %s", e)
|
||||
return False
|
||||
return False, False
|
||||
|
||||
|
||||
def _trigger_dashboard_restart():
|
||||
|
|
@ -118,18 +148,23 @@ def _start_sshx_background():
|
|||
|
||||
|
||||
@router.get("/start")
|
||||
async def start_sshx():
|
||||
async def start_sshx(background_tasks: BackgroundTasks):
|
||||
global _sshx_link, _sshx_pid
|
||||
|
||||
if _is_alive() and _sshx_link:
|
||||
return {"status": "running", "link": _sshx_link}
|
||||
|
||||
if not _ensure_sshx():
|
||||
ok, needs_restart = _ensure_sshx()
|
||||
if not ok:
|
||||
return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to install sshx"})
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(None, _start_sshx_background)
|
||||
|
||||
# Defer dashboard restart until AFTER the response is sent
|
||||
if needs_restart:
|
||||
background_tasks.add_task(_trigger_dashboard_restart)
|
||||
|
||||
if result:
|
||||
return {"status": "running", "link": _sshx_link}
|
||||
return JSONResponse(status_code=504, content={"status": "error", "message": "sshx did not return a link in time"})
|
||||
|
|
@ -137,9 +172,11 @@ async def start_sshx():
|
|||
|
||||
@router.get("/status")
|
||||
async def sshx_status():
|
||||
global _sshx_link
|
||||
global _sshx_link, _sshx_installing
|
||||
if _is_alive() and _sshx_link:
|
||||
return {"status": "running", "link": _sshx_link}
|
||||
if _sshx_installing:
|
||||
return {"status": "installing", "link": None}
|
||||
return {"status": "stopped", "link": None}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue