2026-08-17 15:15:05 +00:00
"""
2026-09-03 04:21:03 +00:00
sshx - link plugin — creates a shell at sshx . io and returns the shareable link
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == ==
- Auto arch detection ( x86_64 / aarch64 / armv6 / armv7 )
- Downloads sshx binary from S3 without curl | sh
2026-09-03 04:47:45 +00:00
- Starts ` sshx ` on demand and parses https : / / sshx . io / s / . . . link from stdout
2026-08-17 15:15:05 +00:00
"""
import subprocess
feat: keep auto-start but via nohup & to avoid hermes kill, remove ntfy, passive dashboard
- __init__.py: re-add auto-start but via nohup (bash -c "nohup {bin} --quiet > /tmp/sshx_link.txt 2>&1 < /dev/null & echo $!"), pid to /tmp/sshx.pid, link to /tmp/sshx_link.txt, _is_alive checks pid file, _run_sshx uses nohup, _stop kills pid file
- dashboard/plugin_api.py: same nohup auto-start thread (3s delay), _is_alive checks pid file, /status reads link file, /start uses nohup, no ntfy
- dashboard keeps passive UI (only show current link, poll every 3s) - hermes no longer kills background shell
- bump v2.3.0
2026-09-03 04:54:14 +00:00
import threading
2026-08-17 15:15:05 +00:00
import logging
import shutil
import os
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
import platform
import stat
import urllib . request
2026-09-03 04:21:03 +00:00
import re
import tarfile
import tempfile
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
import time
2026-08-17 15:15:05 +00:00
logger = logging . getLogger ( __name__ )
2026-09-03 04:21:03 +00:00
_sshx_process = None
_sshx_link = None
SSHX_BIN = " /tmp/sshx "
SSHX_BIN_ALT = os . path . expanduser ( " ~/.local/bin/sshx " )
SSHX_TAR = " /tmp/sshx.tar.gz "
feat: keep auto-start but via nohup & to avoid hermes kill, remove ntfy, passive dashboard
- __init__.py: re-add auto-start but via nohup (bash -c "nohup {bin} --quiet > /tmp/sshx_link.txt 2>&1 < /dev/null & echo $!"), pid to /tmp/sshx.pid, link to /tmp/sshx_link.txt, _is_alive checks pid file, _run_sshx uses nohup, _stop kills pid file
- dashboard/plugin_api.py: same nohup auto-start thread (3s delay), _is_alive checks pid file, /status reads link file, /start uses nohup, no ntfy
- dashboard keeps passive UI (only show current link, poll every 3s) - hermes no longer kills background shell
- bump v2.3.0
2026-09-03 04:54:14 +00:00
SSHX_LINK_FILE = " /tmp/sshx_link.txt "
SSHX_PID_FILE = " /tmp/sshx.pid "
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
2026-09-03 04:21:03 +00:00
LINK_RE = re . compile ( r " https://sshx \ .io/s/[A-Za-z0-9 \ -_]+(?:#[^ \ s \" ' ]*)? " )
def _detect_sshx_arch ( ) :
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
machine = platform . machine ( ) . lower ( )
2026-09-03 04:21:03 +00:00
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 "
logger . info ( f " Detected arch= { arch } suffix= { suffix } -> sshx url= { url } " )
return url
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
2026-09-03 04:21:03 +00:00
def _find_sshx_bin ( ) - > str | None :
for p in [ SSHX_BIN , SSHX_BIN_ALT , shutil . which ( " sshx " ) ] :
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
if p and os . path . exists ( p ) and os . access ( p , os . X_OK ) :
return p
return None
2026-09-03 04:21:03 +00:00
def _ensure_sshx_installed ( ) - > str | None :
existing = _find_sshx_bin ( )
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
if existing :
return existing
2026-09-03 04:21:03 +00:00
url = _get_sshx_url ( )
dest = SSHX_BIN
os . makedirs ( os . path . dirname ( SSHX_BIN_ALT ) , exist_ok = True )
tmp_tar = SSHX_TAR
2026-08-17 15:15:05 +00:00
try :
2026-09-03 04:21:03 +00:00
logger . info ( f " Downloading sshx from { url } -> { tmp_tar } " )
urllib . request . urlretrieve ( url , tmp_tar )
logger . info ( f " Extracting { tmp_tar } " )
with tarfile . open ( tmp_tar , " r:gz " ) as tf :
member = None
for m in tf . getmembers ( ) :
base = os . path . basename ( m . name )
if base . startswith ( " ._ " ) :
continue
if base == " sshx " and m . isfile ( ) :
member = m
break
if not member :
for m in tf . getmembers ( ) :
base = os . path . basename ( m . name )
if base . startswith ( " ._ " ) :
continue
if m . isfile ( ) :
member = m
break
if not member :
member = tf . getmembers ( ) [ 0 ]
tmpdir = tempfile . mkdtemp ( )
try :
tf . extract ( member , path = tmpdir , filter = ' fully_trusted ' )
except TypeError :
tf . extract ( member , path = tmpdir )
extracted = os . path . join ( tmpdir , member . name )
if not os . path . exists ( extracted ) :
for root , _ , files in os . walk ( tmpdir ) :
if " sshx " in files :
extracted = os . path . join ( root , " sshx " )
break
shutil . copy2 ( extracted , dest )
os . chmod ( dest , os . stat ( dest ) . st_mode | stat . S_IEXEC )
try :
if dest != SSHX_BIN_ALT :
shutil . copy2 ( dest , SSHX_BIN_ALT )
os . chmod ( SSHX_BIN_ALT , os . stat ( SSHX_BIN_ALT ) . st_mode | stat . S_IEXEC )
except Exception :
pass
shutil . rmtree ( tmpdir , ignore_errors = True )
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
try :
2026-09-03 04:21:03 +00:00
os . remove ( tmp_tar )
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
except Exception :
pass
2026-09-03 04:47:45 +00:00
logger . info ( f " sshx installed to { dest } " )
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
return dest
except Exception as e :
2026-09-03 04:21:03 +00:00
logger . error ( f " sshx install failed from { url } : { e } " )
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
return None
2026-09-03 04:21:03 +00:00
def _is_sshx_alive ( ) - > bool :
global _sshx_process
feat: keep auto-start but via nohup & to avoid hermes kill, remove ntfy, passive dashboard
- __init__.py: re-add auto-start but via nohup (bash -c "nohup {bin} --quiet > /tmp/sshx_link.txt 2>&1 < /dev/null & echo $!"), pid to /tmp/sshx.pid, link to /tmp/sshx_link.txt, _is_alive checks pid file, _run_sshx uses nohup, _stop kills pid file
- dashboard/plugin_api.py: same nohup auto-start thread (3s delay), _is_alive checks pid file, /status reads link file, /start uses nohup, no ntfy
- dashboard keeps passive UI (only show current link, poll every 3s) - hermes no longer kills background shell
- bump v2.3.0
2026-09-03 04:54:14 +00:00
# check in-memory process first
if _sshx_process is not None and _sshx_process . poll ( ) is None :
return True
# check nohup pid file (survives hermes kill)
try :
if os . path . exists ( SSHX_PID_FILE ) :
with open ( SSHX_PID_FILE , " r " ) as f :
pid = int ( f . read ( ) . strip ( ) )
os . kill ( pid , 0 )
return True
except Exception :
pass
# check link file + pgrep fallback
try :
if os . path . exists ( SSHX_LINK_FILE ) :
# if file exists and recent (< 24h), assume alive - sshx itself handles disconnect
return True
except Exception :
pass
return False
def _read_link_file ( ) - > str | None :
try :
if os . path . exists ( SSHX_LINK_FILE ) :
with open ( SSHX_LINK_FILE , " r " ) as f :
txt = f . read ( )
m = LINK_RE . search ( txt )
if m :
return m . group ( 0 )
except Exception :
pass
return None
def _run_sshx_nohup ( ) - > str | None :
""" Run sshx via nohup & disown so hermes doesn ' t kill it. Returns link or None. """
global _sshx_link , _sshx_process
bin_path = _ensure_sshx_installed ( )
if not bin_path :
return None
# clean old files
for p in [ SSHX_LINK_FILE , SSHX_PID_FILE ] :
try :
os . remove ( p )
except Exception :
pass
logger . info ( f " Starting sshx via nohup { bin_path } --quiet > { SSHX_LINK_FILE } 2>&1 & " )
try :
# Use bash to launch detached nohup: captures pid to file
cmd = f " nohup { bin_path } --quiet > { SSHX_LINK_FILE } 2>&1 < /dev/null & echo $! "
result = subprocess . run ( [ " bash " , " -c " , cmd ] , capture_output = True , text = True , timeout = 5 )
pid_str = result . stdout . strip ( ) . split ( ) [ - 1 ] if result . stdout . strip ( ) else " "
try :
pid = int ( pid_str )
with open ( SSHX_PID_FILE , " w " ) as f :
f . write ( str ( pid ) )
logger . info ( f " sshx nohup pid= { pid } " )
except Exception as e :
logger . warning ( f " failed to get pid: { result . stdout } { result . stderr } { e } " )
# wait for link to appear in file (sshx writes quickly with --quiet)
for _ in range ( 20 ) :
time . sleep ( 1 )
link = _read_link_file ( )
if link :
_sshx_link = link
logger . info ( f " sshx nohup ready: { link } " )
return link
# if process died early, check
if pid_str and pid_str . isdigit ( ) :
try :
os . kill ( int ( pid_str ) , 0 )
except OSError :
# died, read any output
try :
txt = open ( SSHX_LINK_FILE ) . read ( ) if os . path . exists ( SSHX_LINK_FILE ) else " "
except Exception :
txt = " "
logger . warning ( f " sshx nohup died early, file tail: { txt [ - 400 : ] } " )
break
link = _read_link_file ( )
if link :
_sshx_link = link
return link
logger . warning ( f " sshx nohup timeout, file content: { open ( SSHX_LINK_FILE ) . read ( ) [ - 400 : ] if os . path . exists ( SSHX_LINK_FILE ) else ' no file ' } " )
return None
except Exception as e :
logger . error ( f " nohup start failed: { e } " , exc_info = True )
return None
2026-09-03 04:21:03 +00:00
2026-09-03 04:23:49 +00:00
_sshx_last_output = " "
_sshx_last_error = " "
def _capture_link ( proc , timeout = 20 ) - > str | None :
global _sshx_last_output
2026-09-03 04:21:03 +00:00
link = None
start = time . time ( )
import select
output = " "
while time . time ( ) - start < timeout :
if proc . poll ( ) is not None :
try :
rem = proc . stdout . read ( ) or " "
output + = rem
except Exception :
pass
break
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
try :
2026-09-03 04:21:03 +00:00
r , _ , _ = select . select ( [ proc . stdout ] , [ ] , [ ] , 0.5 )
if r :
line = proc . stdout . readline ( )
if not line :
time . sleep ( 0.1 )
continue
output + = line
logger . info ( f " sshx stdout: { line . strip ( ) } " )
m = LINK_RE . search ( line )
if m :
link = m . group ( 0 )
break
m2 = LINK_RE . search ( output )
if m2 :
link = m2 . group ( 0 )
break
except Exception as e :
logger . debug ( f " capture error: { e } " )
time . sleep ( 0.2 )
if not link :
m = LINK_RE . search ( output )
if m :
link = m . group ( 0 )
2026-09-03 04:23:49 +00:00
_sshx_last_output = output [ - 2000 : ]
2026-09-03 04:21:03 +00:00
if not link :
2026-09-03 04:23:49 +00:00
logger . warning ( f " sshx capture timeout, output tail: { output [ - 600 : ] } " )
2026-09-03 04:21:03 +00:00
return link
def _run_sshx ( tool_ctx ) :
feat: keep auto-start but via nohup & to avoid hermes kill, remove ntfy, passive dashboard
- __init__.py: re-add auto-start but via nohup (bash -c "nohup {bin} --quiet > /tmp/sshx_link.txt 2>&1 < /dev/null & echo $!"), pid to /tmp/sshx.pid, link to /tmp/sshx_link.txt, _is_alive checks pid file, _run_sshx uses nohup, _stop kills pid file
- dashboard/plugin_api.py: same nohup auto-start thread (3s delay), _is_alive checks pid file, /status reads link file, /start uses nohup, no ntfy
- dashboard keeps passive UI (only show current link, poll every 3s) - hermes no longer kills background shell
- bump v2.3.0
2026-09-03 04:54:14 +00:00
global _sshx_link
# if already have link file, return it
existing = _read_link_file ( )
if existing :
_sshx_link = existing
tool_ctx . yield_result ( { " status " : " already_running " , " link " : existing } )
return
2026-09-03 04:21:03 +00:00
if _is_sshx_alive ( ) and _sshx_link :
tool_ctx . yield_result ( { " status " : " already_running " , " link " : _sshx_link } )
2026-08-17 15:15:05 +00:00
return
feat: keep auto-start but via nohup & to avoid hermes kill, remove ntfy, passive dashboard
- __init__.py: re-add auto-start but via nohup (bash -c "nohup {bin} --quiet > /tmp/sshx_link.txt 2>&1 < /dev/null & echo $!"), pid to /tmp/sshx.pid, link to /tmp/sshx_link.txt, _is_alive checks pid file, _run_sshx uses nohup, _stop kills pid file
- dashboard/plugin_api.py: same nohup auto-start thread (3s delay), _is_alive checks pid file, /status reads link file, /start uses nohup, no ntfy
- dashboard keeps passive UI (only show current link, poll every 3s) - hermes no longer kills background shell
- bump v2.3.0
2026-09-03 04:54:14 +00:00
link = _run_sshx_nohup ( )
2026-09-03 04:21:03 +00:00
if link :
feat: keep auto-start but via nohup & to avoid hermes kill, remove ntfy, passive dashboard
- __init__.py: re-add auto-start but via nohup (bash -c "nohup {bin} --quiet > /tmp/sshx_link.txt 2>&1 < /dev/null & echo $!"), pid to /tmp/sshx.pid, link to /tmp/sshx_link.txt, _is_alive checks pid file, _run_sshx uses nohup, _stop kills pid file
- dashboard/plugin_api.py: same nohup auto-start thread (3s delay), _is_alive checks pid file, /status reads link file, /start uses nohup, no ntfy
- dashboard keeps passive UI (only show current link, poll every 3s) - hermes no longer kills background shell
- bump v2.3.0
2026-09-03 04:54:14 +00:00
tool_ctx . yield_result ( { " status " : " running " , " link " : link } )
2026-09-03 04:21:03 +00:00
else :
feat: keep auto-start but via nohup & to avoid hermes kill, remove ntfy, passive dashboard
- __init__.py: re-add auto-start but via nohup (bash -c "nohup {bin} --quiet > /tmp/sshx_link.txt 2>&1 < /dev/null & echo $!"), pid to /tmp/sshx.pid, link to /tmp/sshx_link.txt, _is_alive checks pid file, _run_sshx uses nohup, _stop kills pid file
- dashboard/plugin_api.py: same nohup auto-start thread (3s delay), _is_alive checks pid file, /status reads link file, /start uses nohup, no ntfy
- dashboard keeps passive UI (only show current link, poll every 3s) - hermes no longer kills background shell
- bump v2.3.0
2026-09-03 04:54:14 +00:00
tool_ctx . yield_result ( { " status " : " error " , " message " : _sshx_last_error or " Failed to start sshx via nohup, check /tmp/sshx_link.txt " , " output_tail " : _sshx_last_output [ - 400 : ] if _sshx_last_output else " " } )
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
2026-09-03 04:21:03 +00:00
def _stop_sshx ( tool_ctx = None ) :
global _sshx_process , _sshx_link
feat: keep auto-start but via nohup & to avoid hermes kill, remove ntfy, passive dashboard
- __init__.py: re-add auto-start but via nohup (bash -c "nohup {bin} --quiet > /tmp/sshx_link.txt 2>&1 < /dev/null & echo $!"), pid to /tmp/sshx.pid, link to /tmp/sshx_link.txt, _is_alive checks pid file, _run_sshx uses nohup, _stop kills pid file
- dashboard/plugin_api.py: same nohup auto-start thread (3s delay), _is_alive checks pid file, /status reads link file, /start uses nohup, no ntfy
- dashboard keeps passive UI (only show current link, poll every 3s) - hermes no longer kills background shell
- bump v2.3.0
2026-09-03 04:54:14 +00:00
# kill Popen if any
2026-09-03 04:21:03 +00:00
if _sshx_process and _sshx_process . poll ( ) is None :
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
try :
2026-09-03 04:21:03 +00:00
os . killpg ( os . getpgid ( _sshx_process . pid ) , 15 )
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
except Exception :
2026-09-03 04:21:03 +00:00
try :
_sshx_process . terminate ( )
except Exception :
pass
feat: keep auto-start but via nohup & to avoid hermes kill, remove ntfy, passive dashboard
- __init__.py: re-add auto-start but via nohup (bash -c "nohup {bin} --quiet > /tmp/sshx_link.txt 2>&1 < /dev/null & echo $!"), pid to /tmp/sshx.pid, link to /tmp/sshx_link.txt, _is_alive checks pid file, _run_sshx uses nohup, _stop kills pid file
- dashboard/plugin_api.py: same nohup auto-start thread (3s delay), _is_alive checks pid file, /status reads link file, /start uses nohup, no ntfy
- dashboard keeps passive UI (only show current link, poll every 3s) - hermes no longer kills background shell
- bump v2.3.0
2026-09-03 04:54:14 +00:00
time . sleep ( 0.3 )
2026-09-03 04:21:03 +00:00
if _sshx_process . poll ( ) is None :
try :
os . killpg ( os . getpgid ( _sshx_process . pid ) , 9 )
except Exception :
try :
_sshx_process . kill ( )
except Exception :
pass
feat: keep auto-start but via nohup & to avoid hermes kill, remove ntfy, passive dashboard
- __init__.py: re-add auto-start but via nohup (bash -c "nohup {bin} --quiet > /tmp/sshx_link.txt 2>&1 < /dev/null & echo $!"), pid to /tmp/sshx.pid, link to /tmp/sshx_link.txt, _is_alive checks pid file, _run_sshx uses nohup, _stop kills pid file
- dashboard/plugin_api.py: same nohup auto-start thread (3s delay), _is_alive checks pid file, /status reads link file, /start uses nohup, no ntfy
- dashboard keeps passive UI (only show current link, poll every 3s) - hermes no longer kills background shell
- bump v2.3.0
2026-09-03 04:54:14 +00:00
# kill nohup pid
try :
if os . path . exists ( SSHX_PID_FILE ) :
with open ( SSHX_PID_FILE ) as f :
pid = int ( f . read ( ) . strip ( ) )
try :
os . kill ( pid , 15 )
time . sleep ( 0.3 )
os . kill ( pid , 9 )
except OSError :
pass
os . remove ( SSHX_PID_FILE )
except Exception :
pass
try :
if os . path . exists ( SSHX_LINK_FILE ) :
os . remove ( SSHX_LINK_FILE )
except Exception :
pass
2026-09-03 04:21:03 +00:00
_sshx_process = None
_sshx_link = None
if tool_ctx :
tool_ctx . yield_result ( { " status " : " stopped " } )
2026-08-19 00:00:34 +00:00
2026-08-17 15:15:05 +00:00
def register ( ctx ) :
feat: keep auto-start but via nohup & to avoid hermes kill, remove ntfy, passive dashboard
- __init__.py: re-add auto-start but via nohup (bash -c "nohup {bin} --quiet > /tmp/sshx_link.txt 2>&1 < /dev/null & echo $!"), pid to /tmp/sshx.pid, link to /tmp/sshx_link.txt, _is_alive checks pid file, _run_sshx uses nohup, _stop kills pid file
- dashboard/plugin_api.py: same nohup auto-start thread (3s delay), _is_alive checks pid file, /status reads link file, /start uses nohup, no ntfy
- dashboard keeps passive UI (only show current link, poll every 3s) - hermes no longer kills background shell
- bump v2.3.0
2026-09-03 04:54:14 +00:00
# auto-start with nohup & so hermes doesn't kill shell
def _auto_start ( ) :
global _sshx_link
try :
time . sleep ( 2 )
# if already have link file, just load it
existing = _read_link_file ( )
if existing :
_sshx_link = existing
logger . info ( f " auto-start: existing link found { existing } " )
return
if _is_sshx_alive ( ) and _sshx_link :
return
logger . info ( " auto-start sshx via nohup... " )
link = _run_sshx_nohup ( )
if link :
logger . info ( f " auto-start sshx ready: { link } " )
else :
logger . warning ( " auto-start failed, no link " )
except Exception as e :
logger . error ( f " auto-start exception: { e } " , exc_info = True )
threading . Thread ( target = _auto_start , daemon = True ) . start ( )
2026-08-17 15:15:05 +00:00
ctx . register_tool (
name = " sshx_start " ,
toolset = " sshx-link " ,
schema = {
2026-09-03 04:47:45 +00:00
" description " : " Create a shell at sshx.io and return the shareable link. Auto arch detection. " ,
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
" parameters " : { " type " : " object " , " properties " : { } , " required " : [ ] } ,
2026-08-17 15:15:05 +00:00
} ,
2026-09-03 04:21:03 +00:00
handler = _run_sshx ,
2026-08-17 15:15:05 +00:00
)
2026-09-03 04:21:03 +00:00
ctx . register_tool (
name = " sshx_stop " ,
toolset = " sshx-link " ,
schema = {
" description " : " Stop the running sshx session " ,
" parameters " : { " type " : " object " , " properties " : { } , " required " : [ ] } ,
} ,
handler = _stop_sshx ,
)
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
ctx . register_tool (
name = " ttyd_start " ,
toolset = " sshx-link " ,
schema = {
2026-09-03 04:47:45 +00:00
" description " : " Alias of sshx_start " ,
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
" parameters " : { " type " : " object " , " properties " : { } , " required " : [ ] } ,
} ,
2026-09-03 04:21:03 +00:00
handler = _run_sshx ,
feat: replace sshx with ttyd - auto arch detection, no curl|sh, no dashboard restart
- __init__.py: replace curl|sh sshx install with urllib ttyd download, arch map x86_64/aarch64/arm/armhf/i686/mips*, cache to ~/.local/bin, no systemd restart, lazy warmup
- dashboard/plugin_api.py: full ttyd backend, auto-detect arch via platform.machine(), download from github releases, free port alloc, host header aware link, no _trigger_dashboard_restart, deprecated restart endpoint kept no-op
- dashboard/dist/index.js: ttyd UI with iframe embed, copy link, installing state, error handling, no restart notice
- plugin.yaml/manifest.json: bump 1.1.0, description ttyd, provides_tools ttyd_start
2026-08-28 12:47:44 +00:00
)
2026-09-03 04:47:45 +00:00
logger . info ( " sshx-link plugin registered (no auto-start) " )