2026-08-17 15:15:05 +00:00
import asyncio
import subprocess
2026-08-17 22:33:19 +00:00
import os
2026-08-17 15:15:05 +00:00
import logging
2026-08-17 22:33:19 +00:00
import time
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 shutil
2026-09-03 04:21:03 +00:00
import select
2026-08-17 22:03:34 +00:00
from typing import Optional
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
from fastapi import APIRouter , Request
2026-08-17 15:15:05 +00:00
from fastapi . responses import JSONResponse
logger = logging . getLogger ( __name__ )
router = APIRouter ( )
2026-09-03 04:21:03 +00:00
_sshx_link = None
_sshx_process : Optional [ subprocess . Popen ] = None
_sshx_installing = False
_sshx_bin = " /tmp/sshx "
_sshx_bin_alt = os . path . expanduser ( " ~/.local/bin/sshx " )
_sshx_tar = " /tmp/sshx.tar.gz "
2026-09-03 04:23:49 +00:00
_sshx_last_output = " "
_sshx_last_error = " "
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 " sshx arch= { arch } suffix= { suffix } 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 ( ) - > Optional [ str ] :
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-08-17 15:15:05 +00:00
2026-09-03 04:21:03 +00:00
def _ensure_sshx ( ) - > Optional [ str ] :
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:23:49 +00:00
global _sshx_installing , _sshx_last_error
2026-09-03 04:21:03 +00:00
_sshx_installing = True
2026-08-17 22:41:52 +00:00
try :
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
logger . info ( f " Downloading sshx { url } -> { tmp_tar } " )
2026-09-03 04:23:49 +00:00
# quick network check
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 } " )
2026-09-03 04:21:03 +00:00
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
2026-09-03 04:23:49 +00:00
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
2026-09-03 04:21:03 +00:00
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 )
2026-08-17 22:30:21 +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 :
2026-08-17 22:33:19 +00:00
pass
2026-09-03 04:23:49 +00:00
# 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 ) } " )
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:23:49 +00:00
_sshx_last_error = f " install exception: { e } "
logger . error ( _sshx_last_error , exc_info = 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
return None
finally :
2026-09-03 04:21:03 +00:00
_sshx_installing = False
2026-08-17 22:33:19 +00:00
def _is_alive ( ) - > bool :
2026-09-03 04:21:03 +00:00
global _sshx_process
if not _sshx_process :
2026-08-17 22:33:19 +00:00
return False
2026-09-03 04:21:03 +00:00
return _sshx_process . poll ( ) is None
2026-08-17 22:33:19 +00:00
2026-09-03 04:21:03 +00:00
def _kill_sshx ( ) :
global _sshx_process , _sshx_link
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 )
except Exception :
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
_sshx_process . terminate ( )
except Exception :
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
pass
2026-09-03 04:21:03 +00:00
time . sleep ( 0.5 )
if _sshx_process . poll ( ) is None :
try :
os . killpg ( os . getpgid ( _sshx_process . pid ) , 9 )
except Exception :
try :
_sshx_process . kill ( )
except Exception :
pass
_sshx_process = None
_sshx_link = None
2026-08-17 22:30:21 +00:00
2026-08-17 15:15:05 +00:00
2026-09-03 04:23:49 +00:00
def _capture_link_sync ( proc , timeout = 20 ) - > Optional [ str ] :
global _sshx_last_output
2026-09-03 04:21:03 +00:00
link = None
start = time . time ( )
output = " "
while time . time ( ) - start < timeout :
if proc . poll ( ) is not 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
rem = proc . stdout . read ( ) or " "
output + = rem
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
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 : ] # keep tail
2026-09-03 04:21:03 +00:00
if not link :
2026-09-03 04:23:49 +00:00
logger . warning ( f " sshx link not found after { timeout } s, output tail: { output [ - 600 : ] } " )
2026-09-03 04:21:03 +00:00
return link
2026-08-17 15:15:05 +00:00
2026-09-03 04:21:03 +00:00
def _start_sshx_background ( ) - > bool :
2026-09-03 04:23:49 +00:00
global _sshx_process , _sshx_link , _sshx_last_error , _sshx_last_output
2026-09-03 04:21:03 +00:00
_kill_sshx ( )
bin_path = _ensure_sshx ( )
if not bin_path :
2026-09-03 04:23:49 +00:00
if not _sshx_last_error :
_sshx_last_error = " binary not found and install failed (no download) "
2026-09-03 04:21:03 +00:00
return False
2026-09-03 04:23:49 +00:00
# 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 " )
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 :
proc = subprocess . Popen (
2026-09-03 04:21:03 +00:00
[ bin_path , " --quiet " ] ,
stdout = subprocess . PIPE ,
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
stderr = subprocess . STDOUT ,
2026-09-03 04:21:03 +00:00
text = True ,
bufsize = 1 ,
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
stdin = subprocess . DEVNULL ,
start_new_session = True ,
)
2026-09-03 04:21:03 +00:00
_sshx_process = proc
2026-09-03 04:23:49 +00:00
link = _capture_link_sync ( proc , timeout = 20 )
2026-09-03 04:21:03 +00:00
if link :
_sshx_link = link
2026-09-03 04:23:49 +00:00
_sshx_last_error = " "
2026-09-03 04:21:03 +00:00
logger . info ( f " sshx running link= { _sshx_link } pid= { proc . pid } " )
return True
2026-09-03 04:23:49 +00:00
# link not found — keep process if alive for debug, but capture output
2026-09-03 04:21:03 +00:00
if proc . poll ( ) is None :
2026-09-03 04:23:49 +00:00
_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 )
2026-09-03 04:21:03 +00:00
return False
2026-09-03 04:23:49 +00:00
# died
code = proc . poll ( )
_sshx_last_error = f " sshx exited quickly code= { code } output: { _sshx_last_output [ : 800 ] } "
logger . error ( _sshx_last_error )
2026-09-03 04:21:03 +00:00
_sshx_process = None
return False
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 as e :
2026-09-03 04:23:49 +00:00
_sshx_last_error = f " Failed to start sshx: { e } "
logger . error ( _sshx_last_error , exc_info = 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
return False
2026-08-17 15:15:05 +00:00
2026-09-03 04:21:03 +00:00
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
@router.get ( " /start " )
2026-09-03 04:21:03 +00:00
async def start_sshx ( request : Request ) :
2026-09-03 04:23:49 +00:00
global _sshx_link , _sshx_process , _sshx_last_error , _sshx_last_output
2026-09-03 04:21:03 +00:00
if _is_alive ( ) and _sshx_link :
return { " status " : " running " , " link " : _sshx_link , " pid " : _sshx_process . pid if _sshx_process else 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
2026-09-03 04:21:03 +00:00
bin_path = _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 not bin_path :
loop = asyncio . get_event_loop ( )
2026-09-03 04:21:03 +00:00
bin_path = await loop . run_in_executor ( None , _ensure_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 not bin_path :
2026-09-03 04:23:49 +00:00
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 } } )
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-08-17 15:15:05 +00:00
loop = asyncio . get_event_loop ( )
2026-09-03 04:21:03 +00:00
ok = await loop . run_in_executor ( None , _start_sshx_background )
if ok and _sshx_link :
return { " status " : " running " , " link " : _sshx_link , " pid " : _sshx_process . pid if _sshx_process else None }
if _is_alive ( ) :
2026-09-03 04:23:49 +00:00
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_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 ( ) ) } } )
2026-08-19 00:00:34 +00:00
2026-08-17 15:15:05 +00:00
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
@router.post ( " /start " )
2026-09-03 04:21:03 +00:00
async def start_sshx_post ( request : Request ) :
return await start_sshx ( request )
2026-08-17 15:15:05 +00:00
@router.get ( " /status " )
2026-09-03 04:21:03 +00:00
async def sshx_status ( ) :
2026-09-03 04:23:49 +00:00
global _sshx_link , _sshx_installing , _sshx_process , _sshx_last_error , _sshx_last_output
2026-09-03 04:21:03 +00:00
if _is_alive ( ) and _sshx_link :
return { " status " : " running " , " link " : _sshx_link , " pid " : _sshx_process . pid }
if _sshx_installing :
2026-09-03 04:23:49 +00:00
return { " status " : " installing " , " link " : None , " message " : " downloading sshx... " }
2026-09-03 04:21:03 +00:00
if _is_alive ( ) and not _sshx_link :
2026-09-03 04:23:49 +00:00
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 : ] }
2026-08-17 15:15:05 +00:00
return { " status " : " stopped " , " link " : None }
2026-09-03 04:21:03 +00:00
2026-08-17 15:15:05 +00:00
@router.post ( " /stop " )
2026-09-03 04:21:03 +00:00
async def stop_sshx ( ) :
_kill_sshx ( )
2026-08-17 22:33:19 +00:00
return { " status " : " stopped " }
2026-08-17 22:30:21 +00:00
2026-09-03 04:21:03 +00:00
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
@router.get ( " /stop " )
2026-09-03 04:21:03 +00:00
async def stop_sshx_get ( ) :
_kill_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
return { " status " : " stopped " }
2026-08-17 22:30:21 +00:00
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:23:49 +00:00
@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 ( ) ,
}
2026-09-03 04:21:03 +00:00
# compat: old ttyd endpoint still works but now maps to 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
@router.get ( " /restart-dashboard " )
2026-09-03 04:21:03 +00:00
async def restart_dashboard ( ) :
return { " status " : " deprecated " , " message " : " No restart needed. This endpoint is no-op. " }
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
@router.post ( " /restart-dashboard " )
async def restart_dashboard_post ( ) :
return { " status " : " deprecated " , " message " : " No restart needed. This endpoint is no-op. " }