fix: debug No link returned on 13.140.132.105 - improve error reporting + timeout
- increase capture timeout 15->20s, keep output tail for diagnostics - plugin_api: add _sshx_last_output/_last_error, /debug endpoint with arch/url/binary/platform - /start now returns debug + output_tail on 500/504/202 so frontend shows real reason instead of 'No link returned' - /status returns error+tail, installing/starting now shows message - frontend: handle starting/installing/error with Debug view, show output_tail+debug JSON, refresh every 2.5s, Debug button - __init__.py: same capture+error improvements for CLI tool - bump v2.0.1
This commit is contained in:
parent
51b27ed109
commit
39981e6f18
25
__init__.py
25
__init__.py
|
|
@ -156,12 +156,14 @@ def _is_sshx_alive() -> bool:
|
||||||
return _sshx_process.poll() is None
|
return _sshx_process.poll() is None
|
||||||
|
|
||||||
|
|
||||||
def _capture_link(proc, timeout=15) -> str | None:
|
_sshx_last_output = ""
|
||||||
|
_sshx_last_error = ""
|
||||||
|
|
||||||
|
def _capture_link(proc, timeout=20) -> str | None:
|
||||||
"""Read stdout until link found or timeout. Returns link or None."""
|
"""Read stdout until link found or timeout. Returns link or None."""
|
||||||
|
global _sshx_last_output
|
||||||
link = None
|
link = None
|
||||||
start = time.time()
|
start = time.time()
|
||||||
# use non-blocking read via thread? simpler: read line with timeout via poll
|
|
||||||
# We'll read via proc.stdout readline with timeout loop
|
|
||||||
import select
|
import select
|
||||||
|
|
||||||
output = ""
|
output = ""
|
||||||
|
|
@ -201,9 +203,9 @@ def _capture_link(proc, timeout=15) -> str | None:
|
||||||
m = LINK_RE.search(output)
|
m = LINK_RE.search(output)
|
||||||
if m:
|
if m:
|
||||||
link = m.group(0)
|
link = m.group(0)
|
||||||
# also try reading logfile if we used file
|
_sshx_last_output = output[-2000:]
|
||||||
if not link:
|
if not link:
|
||||||
logger.warning(f"sshx capture timeout, output so far: {output[:500]}")
|
logger.warning(f"sshx capture timeout, output tail: {output[-600:]}")
|
||||||
return link
|
return link
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -238,23 +240,24 @@ def _run_sshx(tool_ctx):
|
||||||
tool_ctx.yield_result({"status": "error", "message": f"Failed to start sshx: {e}"})
|
tool_ctx.yield_result({"status": "error", "message": f"Failed to start sshx: {e}"})
|
||||||
return
|
return
|
||||||
|
|
||||||
link = _capture_link(_sshx_process, timeout=15)
|
link = _capture_link(_sshx_process, timeout=20)
|
||||||
if link:
|
if link:
|
||||||
_sshx_link = link
|
_sshx_link = link
|
||||||
logger.info(f"sshx running at {_sshx_link} pid={_sshx_process.pid}")
|
logger.info(f"sshx running at {_sshx_link} pid={_sshx_process.pid}")
|
||||||
tool_ctx.yield_result({"status": "running", "link": _sshx_link, "pid": _sshx_process.pid})
|
tool_ctx.yield_result({"status": "running", "link": _sshx_link, "pid": _sshx_process.pid})
|
||||||
else:
|
else:
|
||||||
# process may still be alive but link not captured — check if died
|
global _sshx_last_error
|
||||||
if _sshx_process.poll() is not None:
|
if _sshx_process.poll() is not None:
|
||||||
try:
|
try:
|
||||||
out = _sshx_process.stdout.read() or ""
|
out = _sshx_process.stdout.read() or ""
|
||||||
except Exception:
|
except Exception:
|
||||||
out = ""
|
out = _sshx_last_output
|
||||||
tool_ctx.yield_result({"status": "error", "message": f"sshx exited quickly (code={_sshx_process.poll()}): {out[:300]}"})
|
_sshx_last_error = f"sshx exited quickly (code={_sshx_process.poll()}): {(_sshx_last_output or out)[:600]}"
|
||||||
|
tool_ctx.yield_result({"status": "error", "message": _sshx_last_error, "output_tail": _sshx_last_output[-400:]})
|
||||||
_sshx_process = None
|
_sshx_process = None
|
||||||
else:
|
else:
|
||||||
# still running but no link parsed — return with log hint
|
_sshx_last_error = f"sshx started (pid={_sshx_process.pid}) but link not parsed in 20s. tail: {_sshx_last_output[:400]}. Check outbound to https://sshx.io"
|
||||||
tool_ctx.yield_result({"status": "running", "link": None, "message": "sshx started but link not parsed yet, check logs", "pid": _sshx_process.pid})
|
tool_ctx.yield_result({"status": "error", "message": _sshx_last_error, "output_tail": _sshx_last_output[-400:], "pid": _sshx_process.pid})
|
||||||
|
|
||||||
|
|
||||||
def _stop_sshx(tool_ctx=None):
|
def _stop_sshx(tool_ctx=None):
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
const res = await fetch(path, opts || {});
|
const res = await fetch(path, opts || {});
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
try { return JSON.parse(text); } catch (_) {
|
try { return JSON.parse(text); } catch (_) {
|
||||||
throw new Error("Server returned non-JSON (status " + res.status + "): " + text.slice(0,300));
|
throw new Error("Server returned non-JSON (status " + res.status + "): " + text.slice(0,800));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,38 +18,53 @@
|
||||||
const [link, setLink] = useState(null);
|
const [link, setLink] = useState(null);
|
||||||
const [status, setStatus] = useState("idle");
|
const [status, setStatus] = useState("idle");
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [debug, setDebug] = useState(null);
|
||||||
|
|
||||||
|
const fetchDebug = useCallback(async function(){
|
||||||
|
try { const d = await safeFetchJSON("/api/plugins/sshx-link/debug"); setDebug(d); } catch(_){}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const start = useCallback(async function () {
|
const start = useCallback(async function () {
|
||||||
setStatus("loading");
|
setStatus("loading");
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setDebug(null);
|
||||||
try {
|
try {
|
||||||
var res = await safeFetchJSON("/api/plugins/sshx-link/status");
|
var st = await safeFetchJSON("/api/plugins/sshx-link/status");
|
||||||
if (res.status === "installing") {
|
if (st.status === "installing" || st.status === "starting") {
|
||||||
setStatus("installing");
|
setStatus("installing");
|
||||||
|
setError(st.message || "Đang xử lý...");
|
||||||
|
fetchDebug();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (res.status === "running" && res.link) {
|
if (st.status === "running" && st.link) {
|
||||||
setLink(res.link);
|
setLink(st.link);
|
||||||
setStatus("running");
|
setStatus("running");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res = await safeFetchJSON("/api/plugins/sshx-link/start");
|
if (st.status === "error" && st.message) {
|
||||||
|
// show previous error but try to restart anyway
|
||||||
|
}
|
||||||
|
var res = await safeFetchJSON("/api/plugins/sshx-link/start");
|
||||||
|
// res may be 202 starting
|
||||||
if (res.link) {
|
if (res.link) {
|
||||||
setLink(res.link);
|
setLink(res.link);
|
||||||
setStatus("running");
|
setStatus("running");
|
||||||
} else if (res.status === "starting") {
|
} else if (res.status === "starting") {
|
||||||
setStatus("installing");
|
setStatus("installing");
|
||||||
setError("sshx đang khởi động, thử lại sau 2s...");
|
setError(res.message || "sshx đang khởi động, chờ 2s rồi thử lại... " + (res.output_tail || ""));
|
||||||
setTimeout(function(){ safeFetchJSON("/api/plugins/sshx-link/status").then(function(r){ if(r.link){ setLink(r.link); setStatus("running"); setError(null);} }); }, 2000);
|
fetchDebug();
|
||||||
} else {
|
} else {
|
||||||
setError(res.message || "No link returned");
|
setError((res.message || JSON.stringify(res)) + (res.output_tail ? "\nTail: " + res.output_tail : "") + (res.debug ? "\nDebug: " + JSON.stringify(res.debug) : ""));
|
||||||
setStatus("error");
|
setStatus("error");
|
||||||
|
fetchDebug();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// err may contain JSON body in message
|
||||||
setError(err.message || "Failed to start sshx");
|
setError(err.message || "Failed to start sshx");
|
||||||
setStatus("error");
|
setStatus("error");
|
||||||
|
fetchDebug();
|
||||||
}
|
}
|
||||||
}, []);
|
}, [fetchDebug]);
|
||||||
|
|
||||||
const stop = useCallback(async function () {
|
const stop = useCallback(async function () {
|
||||||
try {
|
try {
|
||||||
|
|
@ -57,6 +72,7 @@
|
||||||
setLink(null);
|
setLink(null);
|
||||||
setStatus("idle");
|
setStatus("idle");
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setDebug(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
}
|
}
|
||||||
|
|
@ -68,8 +84,14 @@
|
||||||
if (res.status === "running" && res.link) {
|
if (res.status === "running" && res.link) {
|
||||||
setLink(res.link);
|
setLink(res.link);
|
||||||
setStatus("running");
|
setStatus("running");
|
||||||
|
setError(null);
|
||||||
} else if (res.status === "installing" || res.status === "starting") {
|
} else if (res.status === "installing" || res.status === "starting") {
|
||||||
setStatus("installing");
|
setStatus("installing");
|
||||||
|
if (res.message) setError(res.message);
|
||||||
|
} else if (res.status === "error") {
|
||||||
|
setStatus("error");
|
||||||
|
setError(res.message + (res.output_tail ? "\nTail: " + res.output_tail : ""));
|
||||||
|
setDebug(res);
|
||||||
} else if (res.status === "stopped") {
|
} else if (res.status === "stopped") {
|
||||||
if (status === "running") {
|
if (status === "running") {
|
||||||
setLink(null);
|
setLink(null);
|
||||||
|
|
@ -84,25 +106,27 @@
|
||||||
}, [checkStatus]);
|
}, [checkStatus]);
|
||||||
|
|
||||||
useEffect(function () {
|
useEffect(function () {
|
||||||
if (status !== "installing") return;
|
if (status !== "installing" && status !== "loading") return;
|
||||||
var timer = setInterval(function () {
|
var timer = setInterval(function () {
|
||||||
checkStatus();
|
checkStatus();
|
||||||
}, 2000);
|
}, 2500);
|
||||||
return function () { clearInterval(timer); };
|
return function () { clearInterval(timer); };
|
||||||
}, [status, checkStatus]);
|
}, [status, checkStatus]);
|
||||||
|
|
||||||
if (status === "installing") {
|
if (status === "installing" || status === "loading") {
|
||||||
return React.createElement(Card, null,
|
return React.createElement(Card, null,
|
||||||
React.createElement(CardHeader, null,
|
React.createElement(CardHeader, null,
|
||||||
React.createElement(CardTitle, null, "sshx.io Shared Terminal")
|
React.createElement(CardTitle, null, "sshx.io Shared Terminal"),
|
||||||
|
React.createElement("p", { className: "text-xs text-yellow-500", style: { fontWeight: 600 } }, status==="loading" ? "Đang tạo shell ở sshx.io..." : "Đang cài / khởi động sshx...")
|
||||||
),
|
),
|
||||||
React.createElement(CardContent, null,
|
React.createElement(CardContent, null,
|
||||||
React.createElement("p", {
|
React.createElement("p", { className: "text-sm", style: { whiteSpace:"pre-wrap", fontWeight:600, color:"#eab308"}}, error || "Đang tải binary từ s3.amazonaws.com/sshx..."),
|
||||||
className: "text-sm text-yellow-500",
|
React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.5rem" } }, "Tự động thử lại mỗi 2.5s. Nếu kẹt quá 20s, kiểm tra outbound tới s3.amazonaws.com và sshx.io trên server 13.140.132.105."),
|
||||||
style: { fontWeight: 600 }
|
debug && React.createElement("pre", { style:{marginTop:"0.75rem", fontSize:"0.68rem", background:"#111", padding:"0.5rem", borderRadius:"6px", overflow:"auto", maxHeight:"200px", whiteSpace:"pre-wrap"}}, JSON.stringify(debug, null, 2)),
|
||||||
}, "Đang cài / khởi động sshx (auto arch detect)..."),
|
React.createElement("div", { style:{display:"flex", gap:"0.5rem", marginTop:"0.75rem"}},
|
||||||
React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.5rem" } }, "Đang tải binary từ s3.amazonaws.com/sshx, không cần restart dashboard. Tự động thử lại sau 2s."),
|
React.createElement(Button, { onClick: checkStatus, variant:"outline", size:"sm"}, "Refresh"),
|
||||||
error && React.createElement("p", { className: "text-xs text-yellow-700", style: { marginTop: "0.5rem"}}, error)
|
React.createElement(Button, { onClick: stop, variant:"destructive", size:"sm"}, "Stop")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +186,7 @@
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.5rem" } },
|
React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.5rem" } },
|
||||||
"Terminal nhúng ở trên (infinite canvas, cộng tác realtime). Nếu iframe bị chặn, bấm Open để mở tab mới. Link có dạng https://sshx.io/s/<id>#<key> — chia sẻ cho user là xong."
|
"Terminal nhúng ở trên. Nếu iframe bị chặn, bấm Open. Link dạng https://sshx.io/s/<id>#<key> — chia sẻ cho user là xong."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -177,16 +201,19 @@
|
||||||
React.createElement("p", {
|
React.createElement("p", {
|
||||||
className: "text-sm text-muted-foreground",
|
className: "text-sm text-muted-foreground",
|
||||||
style: { marginBottom: "1rem" }
|
style: { marginBottom: "1rem" }
|
||||||
}, "Bấm Start để tạo shell cộng tác trên sshx.io. Plugin sẽ tự tải binary sshx (S3), chạy sshx và parse link https://sshx.io/s/... trả về cho user."),
|
}, "Bấm Start để tạo shell cộng tác trên sshx.io. Plugin sẽ tự tải binary sshx (S3), chạy sshx --quiet và parse link https://sshx.io/s/..."),
|
||||||
error && React.createElement("p", {
|
error && React.createElement("pre", {
|
||||||
className: "text-sm text-red-500",
|
style: { marginBottom: "1rem", whiteSpace: "pre-wrap", fontSize:"0.85rem", color:"#ef4444", background:"#1a0a0a", padding:"0.5rem", borderRadius:"6px", overflow:"auto", maxHeight:"300px"}
|
||||||
style: { marginBottom: "1rem", whiteSpace: "pre-wrap" }
|
|
||||||
}, "Error: " + error),
|
}, "Error: " + error),
|
||||||
React.createElement(Button, {
|
debug && React.createElement("pre", { style:{marginBottom:"1rem", fontSize:"0.68rem", background:"#111", padding:"0.5rem", borderRadius:"6px", overflow:"auto", maxHeight:"200px", whiteSpace:"pre-wrap"}}, "Debug: " + JSON.stringify(debug, null, 2)),
|
||||||
onClick: start,
|
React.createElement("div", { style:{display:"flex", gap:"0.5rem", flexWrap:"wrap"}},
|
||||||
disabled: status === "loading"
|
React.createElement(Button, {
|
||||||
}, status === "loading" ? "Đang tạo shell ở sshx.io..." : "Start sshx → Lấy link"),
|
onClick: start,
|
||||||
React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.75rem"}}, "Tương đương lệnh: curl -sSf https://sshx.io/get | sh -s run — nhưng tự động hoàn toàn trong dashboard.")
|
disabled: status === "loading"
|
||||||
|
}, status === "loading" ? "Đang tạo shell..." : "Start sshx → Lấy link"),
|
||||||
|
React.createElement(Button, { onClick: fetchDebug, variant:"outline", size:"sm"}, "Debug")
|
||||||
|
),
|
||||||
|
React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.75rem"}}, "Nếu lỗi 'No link returned' hoặc timeout, bấm Debug để xem arch/url/binary, kiểm tra server có ra được s3.amazonaws.com và sshx.io không (firewall/security group).")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"label": "sshx.io Terminal",
|
"label": "sshx.io Terminal",
|
||||||
"description": "Tạo shell ở sshx.io và trả link chia sẻ — auto arch, không cần restart",
|
"description": "Tạo shell ở sshx.io và trả link chia sẻ — auto arch, không cần restart",
|
||||||
"icon": "Terminal",
|
"icon": "Terminal",
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"tab": {
|
"tab": {
|
||||||
"path": "/sshx",
|
"path": "/sshx",
|
||||||
"position": "end"
|
"position": "end"
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ _sshx_installing = False
|
||||||
_sshx_bin = "/tmp/sshx"
|
_sshx_bin = "/tmp/sshx"
|
||||||
_sshx_bin_alt = os.path.expanduser("~/.local/bin/sshx")
|
_sshx_bin_alt = os.path.expanduser("~/.local/bin/sshx")
|
||||||
_sshx_tar = "/tmp/sshx.tar.gz"
|
_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\"']*)?")
|
LINK_RE = re.compile(r"https://sshx\.io/s/[A-Za-z0-9\-_]+(?:#[^\s\"']*)?")
|
||||||
|
|
||||||
|
|
@ -74,7 +76,7 @@ def _ensure_sshx() -> Optional[str]:
|
||||||
existing = _find_sshx_bin()
|
existing = _find_sshx_bin()
|
||||||
if existing:
|
if existing:
|
||||||
return existing
|
return existing
|
||||||
global _sshx_installing
|
global _sshx_installing, _sshx_last_error
|
||||||
_sshx_installing = True
|
_sshx_installing = True
|
||||||
try:
|
try:
|
||||||
url = _get_sshx_url()
|
url = _get_sshx_url()
|
||||||
|
|
@ -82,8 +84,14 @@ def _ensure_sshx() -> Optional[str]:
|
||||||
os.makedirs(os.path.dirname(_sshx_bin_alt), exist_ok=True)
|
os.makedirs(os.path.dirname(_sshx_bin_alt), exist_ok=True)
|
||||||
tmp_tar = _sshx_tar
|
tmp_tar = _sshx_tar
|
||||||
logger.info(f"Downloading sshx {url} -> {tmp_tar}")
|
logger.info(f"Downloading sshx {url} -> {tmp_tar}")
|
||||||
urllib.request.urlretrieve(url, tmp_tar)
|
# quick network check
|
||||||
logger.info(f"Extracting {tmp_tar}")
|
try:
|
||||||
|
urllib.request.urlretrieve(url, tmp_tar)
|
||||||
|
except Exception as e:
|
||||||
|
_sshx_last_error = f"download failed {url}: {e}"
|
||||||
|
logger.error(_sshx_last_error)
|
||||||
|
return None
|
||||||
|
logger.info(f"Extracting {tmp_tar} size={os.path.getsize(tmp_tar) if os.path.exists(tmp_tar) else 0}")
|
||||||
with tarfile.open(tmp_tar, "r:gz") as tf:
|
with tarfile.open(tmp_tar, "r:gz") as tf:
|
||||||
member = None
|
member = None
|
||||||
for m in tf.getmembers():
|
for m in tf.getmembers():
|
||||||
|
|
@ -114,6 +122,10 @@ def _ensure_sshx() -> Optional[str]:
|
||||||
if "sshx" in files:
|
if "sshx" in files:
|
||||||
extracted = os.path.join(root, "sshx")
|
extracted = os.path.join(root, "sshx")
|
||||||
break
|
break
|
||||||
|
if not extracted or not os.path.exists(extracted):
|
||||||
|
_sshx_last_error = f"extract failed: member={member.name} tmpdir={tmpdir}"
|
||||||
|
logger.error(_sshx_last_error)
|
||||||
|
return None
|
||||||
shutil.copy2(extracted, dest)
|
shutil.copy2(extracted, dest)
|
||||||
os.chmod(dest, os.stat(dest).st_mode | stat.S_IEXEC)
|
os.chmod(dest, os.stat(dest).st_mode | stat.S_IEXEC)
|
||||||
try:
|
try:
|
||||||
|
|
@ -127,10 +139,15 @@ def _ensure_sshx() -> Optional[str]:
|
||||||
os.remove(tmp_tar)
|
os.remove(tmp_tar)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
logger.info(f"sshx installed to {dest}")
|
# verify
|
||||||
|
if not os.path.exists(dest) or not os.access(dest, os.X_OK):
|
||||||
|
_sshx_last_error = f"binary not executable after install: {dest}"
|
||||||
|
return None
|
||||||
|
logger.info(f"sshx installed to {dest} size={os.path.getsize(dest)}")
|
||||||
return dest
|
return dest
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"sshx install failed: {e}")
|
_sshx_last_error = f"install exception: {e}"
|
||||||
|
logger.error(_sshx_last_error, exc_info=True)
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
_sshx_installing = False
|
_sshx_installing = False
|
||||||
|
|
@ -166,7 +183,8 @@ def _kill_sshx():
|
||||||
_sshx_link = None
|
_sshx_link = None
|
||||||
|
|
||||||
|
|
||||||
def _capture_link_sync(proc, timeout=15) -> Optional[str]:
|
def _capture_link_sync(proc, timeout=20) -> Optional[str]:
|
||||||
|
global _sshx_last_output
|
||||||
link = None
|
link = None
|
||||||
start = time.time()
|
start = time.time()
|
||||||
output = ""
|
output = ""
|
||||||
|
|
@ -202,18 +220,27 @@ def _capture_link_sync(proc, timeout=15) -> Optional[str]:
|
||||||
m = LINK_RE.search(output)
|
m = LINK_RE.search(output)
|
||||||
if m:
|
if m:
|
||||||
link = m.group(0)
|
link = m.group(0)
|
||||||
|
_sshx_last_output = output[-2000:] # keep tail
|
||||||
if not link:
|
if not link:
|
||||||
logger.warning(f"sshx link not found after {timeout}s, output: {output[:600]}")
|
logger.warning(f"sshx link not found after {timeout}s, output tail: {output[-600:]}")
|
||||||
return link
|
return link
|
||||||
|
|
||||||
|
|
||||||
def _start_sshx_background() -> bool:
|
def _start_sshx_background() -> bool:
|
||||||
global _sshx_process, _sshx_link
|
global _sshx_process, _sshx_link, _sshx_last_error, _sshx_last_output
|
||||||
_kill_sshx()
|
_kill_sshx()
|
||||||
bin_path = _ensure_sshx()
|
bin_path = _ensure_sshx()
|
||||||
if not bin_path:
|
if not bin_path:
|
||||||
|
if not _sshx_last_error:
|
||||||
|
_sshx_last_error = "binary not found and install failed (no download)"
|
||||||
return False
|
return False
|
||||||
logger.info(f"Starting sshx {bin_path}")
|
# sanity check binary
|
||||||
|
try:
|
||||||
|
# quick version check
|
||||||
|
subprocess.run([bin_path, "--help"], timeout=2, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"sshx help check failed: {e}")
|
||||||
|
logger.info(f"Starting sshx {bin_path} --quiet")
|
||||||
try:
|
try:
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
[bin_path, "--quiet"],
|
[bin_path, "--quiet"],
|
||||||
|
|
@ -225,26 +252,32 @@ def _start_sshx_background() -> bool:
|
||||||
start_new_session=True,
|
start_new_session=True,
|
||||||
)
|
)
|
||||||
_sshx_process = proc
|
_sshx_process = proc
|
||||||
link = _capture_link_sync(proc, timeout=15)
|
link = _capture_link_sync(proc, timeout=20)
|
||||||
if link:
|
if link:
|
||||||
_sshx_link = link
|
_sshx_link = link
|
||||||
|
_sshx_last_error = ""
|
||||||
logger.info(f"sshx running link={_sshx_link} pid={proc.pid}")
|
logger.info(f"sshx running link={_sshx_link} pid={proc.pid}")
|
||||||
return True
|
return True
|
||||||
# if link not found but process still alive, keep it and return false to let caller handle
|
# link not found — keep process if alive for debug, but capture output
|
||||||
if proc.poll() is None:
|
if proc.poll() is None:
|
||||||
logger.warning("sshx started but link not parsed yet")
|
_sshx_last_error = f"sshx started (pid={proc.pid}) but link not parsed in 20s. output tail: {_sshx_last_output[:500]}. Check outbound to https://sshx.io, or firewall blocking. Binary={bin_path}"
|
||||||
|
logger.warning(_sshx_last_error)
|
||||||
return False
|
return False
|
||||||
logger.error(f"sshx exited quickly code={proc.poll()}")
|
# died
|
||||||
|
code = proc.poll()
|
||||||
|
_sshx_last_error = f"sshx exited quickly code={code} output: {_sshx_last_output[:800]}"
|
||||||
|
logger.error(_sshx_last_error)
|
||||||
_sshx_process = None
|
_sshx_process = None
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to start sshx: {e}")
|
_sshx_last_error = f"Failed to start sshx: {e}"
|
||||||
|
logger.error(_sshx_last_error, exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@router.get("/start")
|
@router.get("/start")
|
||||||
async def start_sshx(request: Request):
|
async def start_sshx(request: Request):
|
||||||
global _sshx_link, _sshx_process
|
global _sshx_link, _sshx_process, _sshx_last_error, _sshx_last_output
|
||||||
if _is_alive() and _sshx_link:
|
if _is_alive() and _sshx_link:
|
||||||
return {"status": "running", "link": _sshx_link, "pid": _sshx_process.pid if _sshx_process else None}
|
return {"status": "running", "link": _sshx_link, "pid": _sshx_process.pid if _sshx_process else None}
|
||||||
|
|
||||||
|
|
@ -253,17 +286,16 @@ async def start_sshx(request: Request):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
bin_path = await loop.run_in_executor(None, _ensure_sshx)
|
bin_path = await loop.run_in_executor(None, _ensure_sshx)
|
||||||
if not bin_path:
|
if not bin_path:
|
||||||
return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to install sshx (arch auto-detect). Check network to s3.amazonaws.com"})
|
return JSONResponse(status_code=500, content={"status": "error", "message": _sshx_last_error or "Failed to install sshx (arch auto-detect). Check network to s3.amazonaws.com", "debug": {"arch": _detect_sshx_arch(), "url": _get_sshx_url(), "last_output": _sshx_last_output}})
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
ok = await loop.run_in_executor(None, _start_sshx_background)
|
ok = await loop.run_in_executor(None, _start_sshx_background)
|
||||||
|
|
||||||
if ok and _sshx_link:
|
if ok and _sshx_link:
|
||||||
return {"status": "running", "link": _sshx_link, "pid": _sshx_process.pid if _sshx_process else None}
|
return {"status": "running", "link": _sshx_link, "pid": _sshx_process.pid if _sshx_process else None}
|
||||||
# if process alive but link not yet, try one more check
|
|
||||||
if _is_alive():
|
if _is_alive():
|
||||||
return JSONResponse(status_code=202, content={"status": "starting", "message": "sshx started but link not ready yet, retry /status", "pid": _sshx_process.pid if _sshx_process else None})
|
return JSONResponse(status_code=202, content={"status": "starting", "message": _sshx_last_error or "sshx started but link not ready yet, retry /status", "pid": _sshx_process.pid if _sshx_process else None, "output_tail": _sshx_last_output[-500:]})
|
||||||
return JSONResponse(status_code=504, content={"status": "error", "message": "sshx did not start in time, check logs"})
|
return JSONResponse(status_code=504, content={"status": "error", "message": _sshx_last_error or "sshx did not start in time, check logs and outbound to sshx.io/s3.amazonaws.com", "output_tail": _sshx_last_output[-800:], "debug": {"bin": _find_sshx_bin() or "not found", "arch": str(_detect_sshx_arch())}})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/start")
|
@router.post("/start")
|
||||||
|
|
@ -273,13 +305,15 @@ async def start_sshx_post(request: Request):
|
||||||
|
|
||||||
@router.get("/status")
|
@router.get("/status")
|
||||||
async def sshx_status():
|
async def sshx_status():
|
||||||
global _sshx_link, _sshx_installing, _sshx_process
|
global _sshx_link, _sshx_installing, _sshx_process, _sshx_last_error, _sshx_last_output
|
||||||
if _is_alive() and _sshx_link:
|
if _is_alive() and _sshx_link:
|
||||||
return {"status": "running", "link": _sshx_link, "pid": _sshx_process.pid}
|
return {"status": "running", "link": _sshx_link, "pid": _sshx_process.pid}
|
||||||
if _sshx_installing:
|
if _sshx_installing:
|
||||||
return {"status": "installing", "link": None}
|
return {"status": "installing", "link": None, "message": "downloading sshx..."}
|
||||||
if _is_alive() and not _sshx_link:
|
if _is_alive() and not _sshx_link:
|
||||||
return {"status": "starting", "link": None, "pid": _sshx_process.pid}
|
return {"status": "starting", "link": None, "pid": _sshx_process.pid, "message": _sshx_last_error, "output_tail": _sshx_last_output[-400:]}
|
||||||
|
if _sshx_last_error:
|
||||||
|
return {"status": "error", "link": None, "message": _sshx_last_error, "output_tail": _sshx_last_output[-400:]}
|
||||||
return {"status": "stopped", "link": None}
|
return {"status": "stopped", "link": None}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -295,6 +329,24 @@ async def stop_sshx_get():
|
||||||
return {"status": "stopped"}
|
return {"status": "stopped"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/debug")
|
||||||
|
async def debug_info():
|
||||||
|
return {
|
||||||
|
"bin": _find_sshx_bin(),
|
||||||
|
"bin_exists": os.path.exists(_sshx_bin) if _sshx_bin else False,
|
||||||
|
"bin_alt_exists": os.path.exists(_sshx_bin_alt) if _sshx_bin_alt else False,
|
||||||
|
"arch": _detect_sshx_arch(),
|
||||||
|
"url": _get_sshx_url(),
|
||||||
|
"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[-800:] if _sshx_last_output else "",
|
||||||
|
"platform": platform.platform(),
|
||||||
|
"machine": platform.machine(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# compat: old ttyd endpoint still works but now maps to sshx
|
# compat: old ttyd endpoint still works but now maps to sshx
|
||||||
@router.get("/restart-dashboard")
|
@router.get("/restart-dashboard")
|
||||||
async def restart_dashboard():
|
async def restart_dashboard():
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
name: sshx-link
|
name: sshx-link
|
||||||
version: 2.0.0
|
version: 2.0.1
|
||||||
description: Tạo shell ở sshx.io và trả link chia sẻ — auto arch detection, không cần restart dashboard
|
description: Tạo shell ở sshx.io và trả link chia sẻ — auto arch detection, không cần restart dashboard
|
||||||
author: sisyphus
|
author: sisyphus
|
||||||
provides_tools:
|
provides_tools:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue