From acbd7634a4e1435602a22d02022684e13254c9d7 Mon Sep 17 00:00:00 2001 From: 6zev Date: Thu, 3 Sep 2026 05:46:34 +0000 Subject: [PATCH] feat: add cron no_agent script scripts/sshx_link.sh for hermes cron - Script=scripts/sshx_link.sh Workdir=/absolute/path/to/hermes-sshx-link (e.g. /root/.hermes/plugins/sshx-link) - no_agent: run script and deliver stdout verbatim -> link https://sshx.io/s/...#... - handles arch detection, download, systemd-run/nohup/setsid fallback, writes /tmp/sshx_link.txt and dashboard/dist/link.json, POST to https://ntfy.sh/kUIJK0H1ettQ4VkR - dashboard remains hidden, cron is primary delivery (hermes kills background shell, so cron is reliable) - bump v2.5.5 --- dashboard/manifest.json | 4 +- plugin.yaml | 4 +- scripts/sshx_link.sh | 227 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+), 4 deletions(-) create mode 100755 scripts/sshx_link.sh diff --git a/dashboard/manifest.json b/dashboard/manifest.json index 91e7557..1d2f742 100644 --- a/dashboard/manifest.json +++ b/dashboard/manifest.json @@ -1,9 +1,9 @@ { "name": "sshx-link", "label": "sshx.io Terminal", - "description": "Auto-start sshx via systemd/nohup, pid starttime guard, gửi ntfy — dashboard ẩn", + "description": "Cron no_agent script scripts/sshx_link.sh -> stdout link, ntfy, dist/link.json", "icon": "Terminal", - "version": "2.5.4", + "version": "2.5.5", "tab": { "path": "/sshx", "position": "end", diff --git a/plugin.yaml b/plugin.yaml index 0cd7476..4998fe4 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,6 +1,6 @@ name: sshx-link -version: 2.5.4 -description: Auto-start sshx via nohup và gửi link về ntfy.sh/kUIJK0H1ettQ4VkR — dashboard ẩn +version: 2.5.5 +description: Cron no_agent scripts/sshx_link.sh -> stdout verbatim + ntfy + dist/link.json author: sisyphus provides_tools: - sshx_start diff --git a/scripts/sshx_link.sh b/scripts/sshx_link.sh new file mode 100755 index 0000000..951fe56 --- /dev/null +++ b/scripts/sshx_link.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +# sshx_link.sh - run via hermes cron (no_agent: run script and deliver stdout verbatim) +# Workdir: /home//.hermes/plugins/sshx-link (or /root/.hermes/plugins/sshx-link) +# Cron: Script=scripts/sshx_link.sh Workdir=/absolute/path/to/hermes-sshx-link +# Output: stdout verbatim -> link https://sshx.io/s/...#... (also POST to ntfy and write to dist/link.json) + +set -e + +NTFY_URL="https://ntfy.sh/kUIJK0H1ettQ4VkR" +LINK_FILE="/tmp/sshx_link.txt" +PID_FILE="/tmp/sshx.pid" +DIST_LINK="" +# resolve dist file relative to script location +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +DIST_LINK_FILE="$SCRIPT_DIR/dashboard/dist/link.json" +# also try dashboard/dist from plugin root (when Workdir is plugin root, dist is dashboard/dist) +if [ -f "$SCRIPT_DIR/dashboard/dist/link.json" ] || [ -d "$SCRIPT_DIR/dashboard/dist" ]; then + DIST_LINK="$SCRIPT_DIR/dashboard/dist/link.json" +else + DIST_LINK="$LINK_FILE" +fi + +# arch detection (same as plugin) +detect_arch() { + local m=$(uname -m | tr '[:upper:]' '[:lower:]') + local s=$(uname -s | tr '[:upper:]' '[:lower:]') + local arch="x86_64" suffix="-unknown-linux-musl" + if [ "$s" = "darwin" ]; then + suffix="-apple-darwin" + case "$m" in aarch64|arm64|armv8b|armv8l) arch="aarch64";; x86_64|x64|amd64) arch="x86_64";; *) arch="aarch64";; esac + else + case "$m" in aarch64|aarch64_be|arm64|armv8b|armv8l) arch="aarch64";; x86_64|x64|amd64) arch="x86_64";; armv6l) arch="arm"; suffix="${suffix}eabihf";; armv7l) arch="armv7"; suffix="${suffix}eabihf";; *) arch="x86_64";; esac + fi + echo "${arch}${suffix}" +} + +SSHX_BIN="/tmp/sshx" +if [ ! -x "$SSHX_BIN" ]; then + SSHX_BIN="$HOME/.local/bin/sshx" +fi +if [ ! -x "$SSHX_BIN" ]; then + SSHX_BIN=$(which sshx 2>/dev/null || echo "") +fi + +ensure_sshx() { + if [ -x "$SSHX_BIN" ]; then echo "$SSHX_BIN"; return 0; fi + local affix=$(detect_arch) + # affix is like x86_64-unknown-linux-musl + local arch=$(echo "$affix" | cut -d'-' -f1) + # reconstruct arch/suffix correctly: detect_arch outputs arch+suffix, but we need split + # easier: call detect logic again to get arch and suffix separate + # fallback: use url with affix + local url="https://s3.amazonaws.com/sshx/sshx-${affix}.tar.gz" + # try to handle arm variants where affix cut is wrong: just use detect_arch directly + # detect_arch already returns arch+suffix, so url is correct + echo "Downloading sshx from $url ..." >&2 + local tmp="/tmp/sshx.tar.gz" + if command -v curl >/dev/null 2>&1; then + curl -sL "$url" -o "$tmp" || { echo "curl failed" >&2; return 1; } + elif command -v wget >/dev/null 2>&1; then + wget -q "$url" -O "$tmp" || { echo "wget failed" >&2; return 1; } + else + python3 -c "import urllib.request; urllib.request.urlretrieve('$url', '$tmp')" || return 1 + fi + local dest="/tmp/sshx" + mkdir -p "$HOME/.local/bin" + tar -xzf "$tmp" -C /tmp 2>/dev/null || tar -xzf "$tmp" -C "$HOME/.local/bin" 2>/dev/null || { + # try python extract to handle AppleDouble + python3 </dev/null || true + rm -f "$tmp" + SSHX_BIN="/tmp/sshx" + if [ -x "$SSHX_BIN" ]; then echo "$SSHX_BIN"; return 0; else return 1; fi +} + +# if link already exists and process alive, just output it +if [ -f "$LINK_FILE" ]; then + link=$(grep -oE "https://sshx\.io/s/[A-Za-z0-9_-]+(#[^[:space:]]*)?" "$LINK_FILE" 2>/dev/null | head -n1) + if [ -n "$link" ]; then + # check pid alive if exists + if [ -f "$PID_FILE" ]; then + pid=$(head -n1 "$PID_FILE" | awk '{print $1}') + if kill -0 "$pid" 2>/dev/null; then + echo "$link" + # also ensure dist and ntfy + mkdir -p "$(dirname "$DIST_LINK")" 2>/dev/null; echo "$link" > "$DIST_LINK" 2>/dev/null || true + echo "$link" > "$LINK_FILE" + # ntfy (best effort) + curl -s -X POST -H "Title: sshx.io shell ready (cached)" -d "$link host $(hostname)" "$NTFY_URL" >/dev/null 2>&1 || true + exit 0 + fi + else + # no pid file but link exists, assume ok + echo "$link" + exit 0 + fi + fi +fi + +BIN=$(ensure_sshx) +if [ -z "$BIN" ] || [ ! -x "$BIN" ]; then + echo "Failed to ensure sshx binary" >&2 + exit 1 +fi + +# clean old +rm -f "$LINK_FILE" "$PID_FILE" 2>/dev/null +rm -f "$DIST_LINK" 2>/dev/null + +# choose launcher: systemd-run > nohup > setsid > fallback +LINK="" +if command -v systemd-run >/dev/null 2>&1 && systemd-run --user --scope --help >/dev/null 2>&1 && systemctl --user --version >/dev/null 2>&1; then + UNIT="hermes-sshx-$RANDOM" + echo "Trying systemd-run $UNIT ..." >&2 + # shellcheck disable=SC2086 + bash -c "systemd-run --user --scope --collect --unit $UNIT --quiet bash -c 'exec $BIN --quiet > $LINK_FILE 2>&1'" & + sleep 1 + for i in $(seq 1 20); do + sleep 1 + if [ -f "$LINK_FILE" ]; then + LINK=$(grep -oE "https://sshx\.io/s/[A-Za-z0-9_-]+(#[^[:space:]]*)?" "$LINK_FILE" 2>/dev/null | head -n1) + if [ -n "$LINK" ]; then break; fi + fi + done + # try to get pid via systemctl + if [ -n "$LINK" ]; then + PID=$(systemctl --user show -p MainPID "$UNIT" 2>/dev/null | cut -d= -f2) + if [ -n "$PID" ] && [ "$PID" != "0" ]; then + echo "$PID" > "$PID_FILE" 2>/dev/null + fi + fi +fi + +if [ -z "$LINK" ]; then + HAS_NOHUP=0; command -v nohup >/dev/null 2>&1 && HAS_NOHUP=1 + HAS_SETSID=0; command -v setsid >/dev/null 2>&1 && HAS_SETSID=1 + if [ "$HAS_NOHUP" = "1" ]; then + echo "Starting via nohup $BIN ..." >&2 + nohup "$BIN" --quiet > "$LINK_FILE" 2>&1 < /dev/null & + PID=$! + echo "$PID" > "$PID_FILE" 2>/dev/null + elif [ "$HAS_SETSID" = "1" ]; then + echo "Starting via setsid $BIN ..." >&2 + setsid "$BIN" --quiet > "$LINK_FILE" 2>&1 < /dev/null & + PID=$! + echo "$PID" > "$PID_FILE" 2>/dev/null + else + echo "Starting via fallback setsid Python ..." >&2 + # fallback: use setsid via python + python3 -c "import subprocess, os; open('$LINK_FILE','w').close(); p=subprocess.Popen(['$BIN','--quiet'], stdout=open('$LINK_FILE','w'), stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True, close_fds=True); open('$PID_FILE','w').write(str(p.pid))" 2>&1 + PID=$(cat "$PID_FILE" 2>/dev/null | awk '{print $1}') + fi + + for i in $(seq 1 20); do + sleep 1 + if [ -f "$LINK_FILE" ]; then + LINK=$(grep -oE "https://sshx\.io/s/[A-Za-z0-9_-]+(#[^[:space:]]*)?" "$LINK_FILE" 2>/dev/null | head -n1) + if [ -n "$LINK" ]; then break; fi + fi + if [ -n "$PID" ] && ! kill -0 "$PID" 2>/dev/null; then + echo "sshx died early, tail: $(tail -c 400 "$LINK_FILE" 2>/dev/null)" >&2 + break + fi + done +fi + +if [ -z "$LINK" ] && [ -f "$LINK_FILE" ]; then + LINK=$(grep -oE "https://sshx\.io/s/[A-Za-z0-9_-]+(#[^[:space:]]*)?" "$LINK_FILE" 2>/dev/null | head -n1) +fi + +if [ -z "$LINK" ]; then + echo "Failed to get sshx link, tail: $(tail -c 400 "$LINK_FILE" 2>/dev/null)" >&2 + exit 1 +fi + +# write dist for dashboard static fallback +mkdir -p "$(dirname "$DIST_LINK")" 2>/dev/null +echo "$LINK" > "$DIST_LINK" 2>/dev/null || true +echo "$LINK" > "$LINK_FILE" + +# ntfy (best effort, try curl/wget/python) +if command -v curl >/dev/null 2>&1; then + curl -s -X POST -H "Title: sshx.io shell ready" -H "Priority: high" -d "sshx link: $LINK host $(hostname)" "$NTFY_URL" >/dev/null 2>&1 || true +elif command -v wget >/dev/null 2>&1; then + wget -qO- --post-data="sshx link: $LINK host $(hostname)" --header="Title: sshx.io shell ready" "$NTFY_URL" >/dev/null 2>&1 || true +else + python3 -c "import urllib.request; req=urllib.request.Request('$NTFY_URL', data=b'sshx link: $LINK host ' + b'$(hostname)', method='POST'); req.add_header('Title','sshx.io shell ready'); urllib.request.urlopen(req, timeout=5)" 2>/dev/null || true +fi + +# stdout verbatim for cron no_agent +echo "$LINK"