feat(rdp): robust RDP enable — step-machine + deep health check + self-heal + end-to-end verify + live progress & structured log
This commit is contained in:
+330
-75
@@ -306,85 +306,107 @@ def _rdp_health_restore():
|
||||
return ok
|
||||
|
||||
def _rdp_enable():
|
||||
import subprocess as _sp, winreg as _wr, time as _t
|
||||
"""Robust RDP enable — async step-machine. Writes live progress to
|
||||
rdp_progress.json + structured lines to rdp_enable.log, returns immediately
|
||||
with 'started'; dashboard polls /rdp action=progress for live steps."""
|
||||
import threading as _th, json as _j
|
||||
try:
|
||||
k = _wr.OpenKey(_wr.HKEY_LOCAL_MACHINE, r'SYSTEM\CurrentControlSet\Control\Terminal Server', 0, _wr.KEY_SET_VALUE)
|
||||
_wr.SetValueEx(k, 'fDenyTSConnections', 0, _wr.REG_DWORD, 0)
|
||||
_wr.CloseKey(k)
|
||||
except Exception as e:
|
||||
log(f'RDP enable registry error: {e}')
|
||||
try:
|
||||
_sp.run(['net', 'localgroup', 'Remote Desktop Users', 'hmo', '/add'], capture_output=True, timeout=5)
|
||||
if os.path.exists(_RDP_PROGRESS_FILE):
|
||||
with open(_RDP_PROGRESS_FILE, encoding='utf-8') as f:
|
||||
cur = _j.load(f)
|
||||
if cur.get('state') == 'running':
|
||||
return True, 'RDP enable 已在进行中(查看进度)'
|
||||
except Exception:
|
||||
pass
|
||||
_rdp_kill_tunnel()
|
||||
# 收养现存隧道:如果有 ssh 进程已在跑 -R 8080 转发(比如 bot 崩溃后留下的
|
||||
# detached ssh、或状态文件丢失后的野生隧道),直接写入 pid 文件收养,
|
||||
# 不再起新进程——否则新 ssh 会因 8080 被占报 "remote port forwarding failed",
|
||||
# 导致 dashboard 永远显示 disabled(2026-07-23 事故)。
|
||||
_existing = _rdp_find_tunnel_process()
|
||||
if _existing:
|
||||
_rdp_adopt_tunnel(_existing)
|
||||
log(f'RDP: tunnel already running (PID {_existing}), adopted — no new ssh needed')
|
||||
return True, f'RDP access enabled (adopted existing tunnel, PID {_existing})'
|
||||
# Health check: if RDP not responding, auto-restore
|
||||
_t.sleep(2)
|
||||
if not _rdp_health_check():
|
||||
log('RDP: initial health check FAILED — triggering auto-restore')
|
||||
_rdp_health_restore()
|
||||
else:
|
||||
log('RDP: health check passed')
|
||||
_rdp_write_progress('running', 'init', _RDP_STEP_MSG['init'], [])
|
||||
_rdp_log('init', 'start', 'RDP enable requested')
|
||||
_th.Thread(target=_rdp_enable_run, daemon=True).start()
|
||||
return True, 'RDP enable 已启动(后台执行中,可看实时进度)'
|
||||
|
||||
def _rdp_enable_run():
|
||||
"""The actual step-machine, runs in a daemon thread. Each step is recorded
|
||||
to rdp_progress.json (live) and rdp_enable.log (structured, for later debug)."""
|
||||
import subprocess as _sp, winreg as _wr, time as _t
|
||||
steps = []
|
||||
def mark(name, status, detail=''):
|
||||
steps.append({'name': name, 'status': status, 'detail': detail, 'ts': _rdp_now()})
|
||||
_rdp_write_progress('running', name, _RDP_STEP_MSG.get(name, name), steps, detail)
|
||||
_rdp_log(name, status, detail)
|
||||
|
||||
try:
|
||||
# ExitOnForwardFailure=yes: if remote port 8080 is already in use or
|
||||
# sshd rejects the -R forwarding, ssh exits immediately instead of
|
||||
# hanging with a dead tunnel (and xmpp_bot falsely reporting running).
|
||||
# DETACHED_PROCESS + CREATE_NEW_PROCESS_GROUP: ssh.exe survives parent
|
||||
# (xmpp_bot) restart/crash — tunnel stays up even if bot auto-recovers.
|
||||
# stderr → log file: next failure is diagnosable without reproducing.
|
||||
# env注入: DETACHED_PROCESS 下的子进程可能会丢失 USERPROFILE/HOMEDRIVE/HOMEPATH,
|
||||
# 导致 ssh 找不到 ~/.ssh/id_rsa → "Permission denied (publickey)".
|
||||
# 找私钥: 在 LocalSystem 权限下 ~ 是 C:\WINDOWS\system32\config\systemprofile,
|
||||
# 不是 C:\Users\hmo, 所以要按候选路径搜索 .ssh/id_rsa.
|
||||
_ssh_key = None
|
||||
for _user_home in [os.path.expanduser('~'), r'C:\Users\hmo']:
|
||||
_candidate = os.path.join(_user_home, '.ssh', 'id_rsa')
|
||||
if os.path.isfile(_candidate):
|
||||
_ssh_key = _candidate
|
||||
break
|
||||
if not _ssh_key:
|
||||
return False, 'SSH private key (~/.ssh/id_rsa) not found in any candidate home directory'
|
||||
cmd = ['ssh.exe',
|
||||
'-i', _ssh_key,
|
||||
'-o', 'StrictHostKeyChecking=no',
|
||||
'-o', 'ServerAliveInterval=30',
|
||||
'-o', 'ExitOnForwardFailure=yes',
|
||||
'-o', 'IdentitiesOnly=yes',
|
||||
'-N', '-R', '0.0.0.0:8080:localhost:3389',
|
||||
'root@47.115.32.206']
|
||||
si = _sp.STARTUPINFO()
|
||||
si.dwFlags |= _sp.STARTF_USESHOWWINDOW
|
||||
_ssh_stderr_path = os.path.join(_LOG_DIR, 'rdp_tunnel_ssh.log')
|
||||
_ssh_err_fh = open(_ssh_stderr_path, 'a', encoding='utf-8')
|
||||
_ssh_err_fh.write(f"\n{'='*60}\n{_t.strftime('%Y-%m-%d %H:%M:%S')} RDP tunnel start\n")
|
||||
_ssh_err_fh.flush()
|
||||
# 显式复制父进程环境,补全 ssh 需要的 HOME/USERPROFILE
|
||||
_ssh_env = os.environ.copy()
|
||||
_ssh_env['HOME'] = os.path.expanduser('~')
|
||||
_ssh_env['USERPROFILE'] = os.path.expanduser('~')
|
||||
p = _sp.Popen(cmd, startupinfo=si,
|
||||
stdout=_sp.DEVNULL, stderr=_ssh_err_fh,
|
||||
env=_ssh_env,
|
||||
creationflags=_sp.DETACHED_PROCESS | _sp.CREATE_NEW_PROCESS_GROUP)
|
||||
_t.sleep(3)
|
||||
if p.poll() is not None:
|
||||
_ssh_err_fh.close()
|
||||
return False, f'SSH tunnel exited immediately (code {p.poll()}). See {_ssh_stderr_path}'
|
||||
with open(_RDP_PID_FILE, 'w') as f:
|
||||
f.write(str(p.pid))
|
||||
log(f'RDP tunnel enabled (SSH reverse :8080, PID {p.pid}, detached)')
|
||||
return True, 'RDP access enabled'
|
||||
# Step 1: 开注册表
|
||||
try:
|
||||
k = _wr.OpenKey(_wr.HKEY_LOCAL_MACHINE, r'SYSTEM\CurrentControlSet\Control\Terminal Server', 0, _wr.KEY_SET_VALUE)
|
||||
_wr.SetValueEx(k, 'fDenyTSConnections', 0, _wr.REG_DWORD, 0)
|
||||
_wr.CloseKey(k)
|
||||
mark('registry', 'ok', 'fDenyTSConnections=0')
|
||||
except Exception as e:
|
||||
mark('registry', 'fail', str(e))
|
||||
|
||||
# Step 2: 加入 RDP 用户组
|
||||
try:
|
||||
_sp.run(['net', 'localgroup', 'Remote Desktop Users', 'hmo', '/add'], capture_output=True, timeout=8)
|
||||
mark('rdp_users', 'ok', 'hmo 已加入 Remote Desktop Users')
|
||||
except Exception as e:
|
||||
mark('rdp_users', 'warn', str(e))
|
||||
|
||||
# Step 3: 深度健康检查
|
||||
hc = _rdp_deep_health_check()
|
||||
mark('health_check', 'ok' if hc['healthy'] else 'warn',
|
||||
'x224=%s svc=%s listen=%s 卡死=%d nla=%s cert=%s' % (
|
||||
hc['x224'], hc['termservice'], hc['port_listen'], hc['stale_sessions'], hc['nla'], hc['cert_ok']))
|
||||
|
||||
# Step 4: 自愈(如需)
|
||||
if not hc['healthy']:
|
||||
_rdp_log('restore', 'start', 'issues: ' + '; '.join(hc['issues']))
|
||||
cleared = _rdp_kill_stale_rdp_sessions()
|
||||
if cleared:
|
||||
_rdp_log('restore', 'detail', '清理卡死会话: ' + ','.join(cleared))
|
||||
_rdp_health_restore()
|
||||
hc2 = _rdp_deep_health_check()
|
||||
# NLA 卡死处理:连接能到但建不了会话且 NLA 开 → 临时关 NLA 再重启(隧道内网场景可逆)
|
||||
if not hc2['healthy'] and hc2.get('nla') == 1:
|
||||
_rdp_log('restore', 'detail', '仍异常且NLA开启 → 临时关闭NLA并重启TermService')
|
||||
_rdp_set_nla(False)
|
||||
_t.sleep(1)
|
||||
_sp.run(['net', 'stop', 'TermService', '/y'], capture_output=True, timeout=30)
|
||||
_t.sleep(2)
|
||||
_sp.run(['net', 'start', 'TermService'], capture_output=True, timeout=30)
|
||||
_t.sleep(3)
|
||||
mark('restore', 'ok', '自愈完成: ' + ('; '.join(hc['issues']) or 'no-op'))
|
||||
else:
|
||||
mark('restore', 'skip', '健康,无需自愈')
|
||||
|
||||
# Step 5: 建立隧道
|
||||
_rdp_kill_tunnel()
|
||||
_t.sleep(4) # 等阿里云端 8080 转发随旧 ssh 断开而释放,避免新隧道 forwarding failed
|
||||
_existing = _rdp_find_tunnel_process()
|
||||
if _existing:
|
||||
_rdp_adopt_tunnel(_existing)
|
||||
mark('tunnel', 'ok', '收养已有隧道 PID %d' % _existing)
|
||||
else:
|
||||
ok, msg = _rdp_start_tunnel()
|
||||
mark('tunnel', 'ok' if ok else 'fail', msg)
|
||||
if not ok:
|
||||
_rdp_write_progress('failed', 'tunnel', '隧道建立失败', steps, msg)
|
||||
_rdp_log('done', 'failed', msg)
|
||||
return
|
||||
|
||||
# Step 6: 端到端验证
|
||||
_t.sleep(2)
|
||||
if _rdp_verify_endtoend():
|
||||
mark('verify', 'ok', '端到端握手通过 (47.115.32.206:8080)')
|
||||
_rdp_write_progress('done', 'done', '✓ RDP 已就绪,可连接 47.115.32.206:8080', steps, '')
|
||||
_rdp_log('done', 'ok', 'RDP fully enabled and verified')
|
||||
else:
|
||||
mark('verify', 'warn', '端到端验证未通过(隧道通但RDP握手失败,可能需重试)')
|
||||
_rdp_write_progress('done', 'done', '隧道已建立,端到端验证未通过,请稍后重试连接', steps, '')
|
||||
_rdp_log('done', 'warn', 'tunnel up but end-to-end verify failed')
|
||||
except Exception as e:
|
||||
return False, 'Failed: ' + str(e)
|
||||
mark('done', 'fail', str(e))
|
||||
_rdp_write_progress('failed', 'done', '启动失败: ' + str(e), steps, str(e))
|
||||
_rdp_log('done', 'failed', str(e))
|
||||
|
||||
|
||||
def _rdp_disable():
|
||||
import subprocess as _sp, winreg as _wr
|
||||
@@ -469,6 +491,235 @@ def _rdp_status():
|
||||
pass
|
||||
return {'ok': True, 'tunnel_running': tunnel_on, 'rdp_enabled': rdp_on, 'rdp_port': 3389, 'tunnel_host': 'root@47.115.32.206', 'tunnel_port': 8080}
|
||||
|
||||
# ============================================================
|
||||
# RDP Robustness — step-machine + progress + structured log
|
||||
# 让 enable 按钮"点了就能用":深度检查 + 自愈 + 端到端验证 + 实时进度
|
||||
# ============================================================
|
||||
_RDP_PROGRESS_FILE = os.path.join(os.path.dirname(__file__), 'gateway', 'scripts', 'rdp_progress.json')
|
||||
_RDP_ENABLE_LOG = os.path.join(_LOG_DIR, 'rdp_enable.log')
|
||||
|
||||
_RDP_STEP_MSG = {
|
||||
'init': '开始启动 RDP 远程桌面',
|
||||
'registry': '开启远程桌面注册表',
|
||||
'rdp_users': '加入远程桌面用户组',
|
||||
'health_check': '深度健康检查(TermService/端口/卡死连接/NLA/证书)',
|
||||
'restore': '检测到异常,正在自我修复',
|
||||
'tunnel': '建立 SSH 反向隧道',
|
||||
'verify': '端到端连通性验证',
|
||||
'done': '完成',
|
||||
}
|
||||
|
||||
def _rdp_now():
|
||||
import time as _t
|
||||
return _t.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
def _rdp_log(step, status, detail=''):
|
||||
"""Append a structured line to rdp_enable.log AND mirror to bot log."""
|
||||
line = ("[%s] [%s] %s %s" % (_rdp_now(), step.upper(), status.upper(), detail)).rstrip()
|
||||
try:
|
||||
with open(_RDP_ENABLE_LOG, 'a', encoding='utf-8') as f:
|
||||
f.write(line + '\n')
|
||||
except Exception:
|
||||
pass
|
||||
log('RDP ' + line)
|
||||
|
||||
def _rdp_write_progress(state, step, message, steps=None, detail=''):
|
||||
"""Write rdp_progress.json for the dashboard to poll live steps."""
|
||||
import json as _j
|
||||
prog = {'state': state, 'step': step, 'message': message,
|
||||
'detail': detail, 'steps': steps or [], 'updated': _rdp_now()}
|
||||
try:
|
||||
with open(_RDP_PROGRESS_FILE, 'w', encoding='utf-8') as f:
|
||||
_j.dump(prog, f, ensure_ascii=False, indent=1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _rdp_progress():
|
||||
"""Return current enable progress (for /rdp action=progress)."""
|
||||
import json as _j
|
||||
try:
|
||||
if os.path.exists(_RDP_PROGRESS_FILE):
|
||||
with open(_RDP_PROGRESS_FILE, encoding='utf-8') as f:
|
||||
d = _j.load(f)
|
||||
d['ok'] = True
|
||||
return d
|
||||
except Exception:
|
||||
pass
|
||||
return {'ok': True, 'state': 'idle', 'step': '', 'message': '无进行中的任务', 'steps': [], 'detail': ''}
|
||||
|
||||
def _rdp_read_enable_log(lines=80):
|
||||
"""Tail the structured enable log (for /rdp action=enable_log)."""
|
||||
try:
|
||||
if os.path.exists(_RDP_ENABLE_LOG):
|
||||
with open(_RDP_ENABLE_LOG, encoding='utf-8', errors='replace') as f:
|
||||
data = f.readlines()
|
||||
return {'ok': True, 'lines': [l.rstrip('\n') for l in data[-lines:]]}
|
||||
except Exception as e:
|
||||
return {'ok': False, 'error': str(e), 'lines': []}
|
||||
return {'ok': True, 'lines': []}
|
||||
|
||||
def _rdp_deep_health_check():
|
||||
"""Deep RDP health check — unlike _rdp_health_check (X.224/TCP only), this
|
||||
also checks TermService, 3389 listen, stale/close-wait sessions, NLA setting
|
||||
and cert validity, so it can detect "connects but stuck at configuring"
|
||||
(NLA stall / dead session) that the shallow check blindly passes."""
|
||||
import subprocess as _sp
|
||||
res = {'x224': _rdp_health_check(), 'termservice': False, 'port_listen': False,
|
||||
'stale_sessions': 0, 'nla': None, 'cert_ok': None, 'healthy': False, 'issues': []}
|
||||
try:
|
||||
r = _sp.run(['sc', 'query', 'TermService'], capture_output=True, text=True, timeout=8)
|
||||
res['termservice'] = 'RUNNING' in r.stdout
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
r = _sp.run(['powershell', '-NoProfile', '-Command',
|
||||
"if(Get-NetTCPConnection -LocalPort 3389 -State Listen -EA SilentlyContinue){'yes'}"],
|
||||
capture_output=True, text=True, timeout=12)
|
||||
res['port_listen'] = 'yes' in r.stdout
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
r = _sp.run(['powershell', '-NoProfile', '-Command',
|
||||
"@(Get-NetTCPConnection -LocalPort 3389 -EA SilentlyContinue | Where-Object {$_.State -in 'CloseWait','FinWait1','FinWait2','LastAck','TimeWait'}).Count"],
|
||||
capture_output=True, text=True, timeout=12)
|
||||
n = r.stdout.strip()
|
||||
res['stale_sessions'] = int(n) if n.isdigit() else 0
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
import winreg as _wr
|
||||
k = _wr.OpenKey(_wr.HKEY_LOCAL_MACHINE, r'SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp', 0, _wr.KEY_READ)
|
||||
v, _ = _wr.QueryValueEx(k, 'UserAuthentication')
|
||||
_wr.CloseKey(k)
|
||||
res['nla'] = int(v)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
r = _sp.run(['powershell', '-NoProfile', '-Command',
|
||||
"$c=Get-ChildItem 'Cert:\\LocalMachine\\Remote Desktop' -EA SilentlyContinue | Where-Object {$_.HasPrivateKey -and $_.NotAfter -gt (Get-Date)} | Select-Object -First 1; if($c){'ok'}else{'none'}"],
|
||||
capture_output=True, text=True, timeout=12)
|
||||
res['cert_ok'] = 'ok' in r.stdout
|
||||
except Exception:
|
||||
pass
|
||||
issues = []
|
||||
if not res['termservice']: issues.append('TermService未运行')
|
||||
if not res['port_listen']: issues.append('3389未监听')
|
||||
if not res['x224']: issues.append('X.224握手失败')
|
||||
if res['stale_sessions'] > 0: issues.append('卡死连接x%d' % res['stale_sessions'])
|
||||
if res['cert_ok'] is False: issues.append('RDP证书异常')
|
||||
res['issues'] = issues
|
||||
res['healthy'] = bool(res['x224'] and res['termservice'] and res['port_listen']
|
||||
and res['stale_sessions'] == 0 and res['cert_ok'] is not False)
|
||||
return res
|
||||
|
||||
def _rdp_kill_stale_rdp_sessions():
|
||||
"""Log off half-dead/disconnected RDP sessions and clear close-wait conns that
|
||||
block new session setup (a common cause of 'stuck at configuring')."""
|
||||
import subprocess as _sp
|
||||
cleared = []
|
||||
try:
|
||||
r = _sp.run(['qwinsta'], capture_output=True, text=True, timeout=8)
|
||||
for line in r.stdout.splitlines():
|
||||
if 'rdp-tcp#' in line and ('Disc' in line or '断开' in line or 'Down' in line):
|
||||
parts = line.split()
|
||||
for p in parts:
|
||||
if p.isdigit() and int(p) > 0:
|
||||
_sp.run(['logoff', p], capture_output=True, timeout=5)
|
||||
cleared.append('session#' + p)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
return cleared
|
||||
|
||||
def _rdp_set_nla(enabled):
|
||||
"""Set NLA (UserAuthentication) on RDP-Tcp. enabled=True→1, False→0."""
|
||||
import winreg as _wr
|
||||
try:
|
||||
k = _wr.OpenKey(_wr.HKEY_LOCAL_MACHINE, r'SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp', 0, _wr.KEY_SET_VALUE)
|
||||
_wr.SetValueEx(k, 'UserAuthentication', 0, _wr.REG_DWORD, 1 if enabled else 0)
|
||||
_wr.CloseKey(k)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _rdp_start_tunnel():
|
||||
"""Start the SSH reverse tunnel (extracted from old _rdp_enable body).
|
||||
Retries when the Aliyun-side 8080 forwarding is still held by a just-killed
|
||||
previous tunnel ('remote port forwarding failed for listen port 8080') —
|
||||
that port needs a few seconds to be released after the old ssh dies."""
|
||||
import subprocess as _sp, time as _t, os
|
||||
_retry_waits = [8, 12, 18, 25] # 递增等待:覆盖阿里云端 8080 转发随旧 ssh 断开而缓慢释放的时间
|
||||
for _attempt in range(5):
|
||||
try:
|
||||
_ssh_key = None
|
||||
for _user_home in [os.path.expanduser('~'), r'C:\Users\hmo']:
|
||||
_candidate = os.path.join(_user_home, '.ssh', 'id_rsa')
|
||||
if os.path.isfile(_candidate):
|
||||
_ssh_key = _candidate
|
||||
break
|
||||
if not _ssh_key:
|
||||
return False, 'SSH 私钥 ~/.ssh/id_rsa 未找到'
|
||||
cmd = ['ssh.exe', '-i', _ssh_key,
|
||||
'-o', 'StrictHostKeyChecking=no', '-o', 'ServerAliveInterval=30',
|
||||
'-o', 'ExitOnForwardFailure=yes', '-o', 'IdentitiesOnly=yes',
|
||||
'-N', '-R', '0.0.0.0:8080:localhost:3389', 'root@47.115.32.206']
|
||||
si = _sp.STARTUPINFO()
|
||||
si.dwFlags |= _sp.STARTF_USESHOWWINDOW
|
||||
_ssh_stderr_path = os.path.join(_LOG_DIR, 'rdp_tunnel_ssh.log')
|
||||
_ssh_err_fh = open(_ssh_stderr_path, 'a', encoding='utf-8')
|
||||
_ssh_err_fh.write('\n' + '=' * 60 + '\n' + _t.strftime('%Y-%m-%d %H:%M:%S') + ' RDP tunnel start (attempt %d)\n' % (_attempt + 1))
|
||||
_ssh_err_fh.flush()
|
||||
_ssh_env = os.environ.copy()
|
||||
_ssh_env['HOME'] = os.path.expanduser('~')
|
||||
_ssh_env['USERPROFILE'] = os.path.expanduser('~')
|
||||
p = _sp.Popen(cmd, startupinfo=si, stdout=_sp.DEVNULL, stderr=_ssh_err_fh,
|
||||
env=_ssh_env,
|
||||
creationflags=_sp.DETACHED_PROCESS | _sp.CREATE_NEW_PROCESS_GROUP)
|
||||
_t.sleep(3)
|
||||
if p.poll() is not None:
|
||||
_ssh_err_fh.close()
|
||||
# ssh 立即退出:判断是否 8080 被旧隧道残留占用(可重试)
|
||||
_fwd_fail = False
|
||||
try:
|
||||
with open(_ssh_stderr_path, encoding='utf-8', errors='replace') as _ef:
|
||||
_tail = _ef.read()[-600:]
|
||||
_fwd_fail = ('forwarding failed' in _tail) or ('8080' in _tail and 'listen' in _tail)
|
||||
except Exception:
|
||||
pass
|
||||
if _fwd_fail and _attempt < 4:
|
||||
_wait = _retry_waits[_attempt]
|
||||
_rdp_log('tunnel', 'warn', '8080 被旧隧道残留占用,等待 %ds 后重试 (%d/5)' % (_wait, _attempt + 1))
|
||||
_rdp_write_progress('running', 'tunnel', 'SSH 反向隧道', None, '8080 残留占用,等待 %ds 重试 (%d/5)' % (_wait, _attempt + 1))
|
||||
_t.sleep(_wait)
|
||||
continue
|
||||
return False, 'SSH 隧道立即退出(code %s),见 %s' % (p.poll(), _ssh_stderr_path)
|
||||
with open(_RDP_PID_FILE, 'w') as f:
|
||||
f.write(str(p.pid))
|
||||
return True, '隧道已建立 PID %d' % p.pid
|
||||
except Exception as e:
|
||||
if _attempt < 4:
|
||||
_t.sleep(_retry_waits[_attempt])
|
||||
continue
|
||||
return False, '隧道建立异常: ' + str(e)
|
||||
return False, 'SSH 隧道多次失败(8080 可能被旧隧道残留占用,请稍后重试)'
|
||||
|
||||
def _rdp_verify_endtoend():
|
||||
"""After tunnel is up, verify full chain via public endpoint 47.115.32.206:8080
|
||||
with an X.224 handshake (proves 8080→tunnel→3389→RDP all work)."""
|
||||
import socket as _sk
|
||||
try:
|
||||
_s = _sk.create_connection(('47.115.32.206', 8080), timeout=8)
|
||||
_s.sendall(bytes.fromhex('030000130ee00000000000010008000b000000'))
|
||||
_s.settimeout(8)
|
||||
_resp = _s.recv(1024)
|
||||
_s.close()
|
||||
if len(_resp) >= 6 and _resp[5] in (0xd0, 0x03):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
# ── OpenCode Go Usage Monitor helpers ──
|
||||
# Reads cached aggregation from gateway/temp/usage_stats.json.
|
||||
# Triggers asynchronous collection by spawning usage_collector.py in a daemon thread.
|
||||
@@ -1049,8 +1300,12 @@ class _BridgeHandler(http.server.BaseHTTPRequestHandler):
|
||||
self._reply(200, {'ok': ok, 'message': msg})
|
||||
elif action == 'status':
|
||||
self._reply(200, _rdp_status())
|
||||
elif action == 'progress':
|
||||
self._reply(200, _rdp_progress())
|
||||
elif action == 'enable_log':
|
||||
self._reply(200, _rdp_read_enable_log(body.get('lines', 80)))
|
||||
else:
|
||||
self._reply(400, {'ok': False, 'error': 'action must be start|stop|status'})
|
||||
self._reply(400, {'ok': False, 'error': 'action must be start|stop|status|progress|enable_log'})
|
||||
return
|
||||
# /usage endpoint — OpenCode Go usage monitor (read cache / trigger collection)
|
||||
# Pattern mirrors /rdp and /easytier. Two actions:
|
||||
|
||||
Reference in New Issue
Block a user