feat: M3 mosh 挂起→恢复<3s(唤醒脉冲 + 会话状态机三相)
- SessionMachine 加 mosh 三相(moshActive/moshParked/moshResuming)+看门狗兜底, mosh 接管即拆 SSH、后台冻结不重建、前台唤醒脉冲等 SSP 续(+5 单测) - bridge.go: Node.WakeUp()(tsnet InjectEvent+MagicSock.Rebind/ReSTUN) + MoshRelay.Rebind()(同端口重开 loopback、泵按 socket 生成容错) - iosclient.cc: g_mosh_last_heard 探针(导出 mosh_last_heard_ms) + SIGCONT 全屏重绘 - MoshSession: nudge()(SIGCONT) + lastHeardMs();SSHTerminalModel 前台脉冲三连 + healthy 轮询 + beginBackgroundTask;URL scheme 供无头前台唤醒 - 验证(vohive-vm over tsnet):15s 冻结→前台 wake pulse→1.3s recovered,<3s Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,8 @@ sleep N; xcrun simctl io $UDID screenshot /tmp/x.png # 然后用 Read 工具
|
|||||||
- **mosh over tsnet**:额外加 `-txTsnetKey <authkey>`(-txHost 填 tailnet IP;relay=loopback↔tsnet UDP)
|
- **mosh over tsnet**:额外加 `-txTsnetKey <authkey>`(-txHost 填 tailnet IP;relay=loopback↔tsnet UDP)
|
||||||
- 自定义 mosh-server 命令:`-txMoshServerCmd "…"`(默认 `mosh-server new -s -c 256 -l LANG=en_US.UTF-8 -l LC_ALL=en_US.UTF-8`)
|
- 自定义 mosh-server 命令:`-txMoshServerCmd "…"`(默认 `mosh-server new -s -c 256 -l LANG=en_US.UTF-8 -l LC_ALL=en_US.UTF-8`)
|
||||||
|
|
||||||
|
**M3 挂起→恢复无头验证**:退后台 `simctl openurl $UDID https://apple.com`;冻结 `kill -STOP $(pgrep -x TerminalX)`;解冻 `kill -CONT`;前台 `simctl openurl $UDID "terminalx://resume"`(URL scheme 已注册)。日志 `log stream | grep -E 'MOSHDBG|TXM3'` 看 `wake pulse`→`TXM3 recovered` 时间差判 <3s。**坑:模拟器看门狗会杀掉被 STOP 冻结过久(>~30s)的 app;且模拟器不复现 socket defunct/WG 过期,深挂起需真机锁屏补测。**
|
||||||
|
|
||||||
## 仓库结构
|
## 仓库结构
|
||||||
- `packages/TXCore` — 零依赖纯逻辑:tmux 解析器/layout/`SessionMachine`(会话状态机+退避重连)/`TmuxControlSequence`(DCS 检测)。
|
- `packages/TXCore` — 零依赖纯逻辑:tmux 解析器/layout/`SessionMachine`(会话状态机+退避重连)/`TmuxControlSequence`(DCS 检测)。
|
||||||
- `packages/TXTransport` — `SSHSession`(libssh2, 支持外部 fd)/`MoshSession`(mosh_main 桥)/`Transport` 协议;C shim: `CSSH`+`CSSHCore`(libssh2)、`CMosh`+`MoshCore`(mosh)。
|
- `packages/TXTransport` — `SSHSession`(libssh2, 支持外部 fd)/`MoshSession`(mosh_main 桥)/`Transport` 协议;C shim: `CSSH`+`CSSHCore`(libssh2)、`CMosh`+`MoshCore`(mosh)。
|
||||||
|
|||||||
19
apps/TerminalX/TerminalX-Info.plist
Normal file
19
apps/TerminalX/TerminalX-Info.plist
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<!-- 部分 Info.plist 基底:仅声明 URL scheme(供无头验证前台唤醒 terminalx://resume)。
|
||||||
|
其余键由 GENERATE_INFOPLIST_FILE=YES 自动生成并合并到本基底之上。 -->
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>ai.athom.terminalx</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>terminalx</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -3,6 +3,9 @@ import GhosttyTerminal
|
|||||||
import TXCore
|
import TXCore
|
||||||
import TXTransport
|
import TXTransport
|
||||||
import TsnetBridge
|
import TsnetBridge
|
||||||
|
#if canImport(UIKit)
|
||||||
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
enum TsnetError: Error { case newNodeFailed, notUp }
|
enum TsnetError: Error { case newNodeFailed, notUp }
|
||||||
|
|
||||||
@@ -55,6 +58,12 @@ final class TsnetManager: @unchecked Sendable {
|
|||||||
guard let n else { throw TsnetError.notUp }
|
guard let n else { throw TsnetError.notUp }
|
||||||
return try n.startMoshRelay(host, moshPort: moshPort, timeoutMs: timeoutMs)
|
return try n.startMoshRelay(host, moshPort: moshPort, timeoutMs: timeoutMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// M3:前台恢复时唤醒 tsnet 网络层(注入链路事件 + Rebind/重新 STUN)。可能阻塞,后台线程调用。
|
||||||
|
func wakeUp() {
|
||||||
|
lock.lock(); let n = node; lock.unlock()
|
||||||
|
n?.wakeUp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 线程安全地持有 transport 引用,供 libghostty 的输入/resize 回调(非主线程)安全取用。
|
/// 线程安全地持有 transport 引用,供 libghostty 的输入/resize 回调(非主线程)安全取用。
|
||||||
@@ -133,8 +142,15 @@ final class SSHTerminalModel: ObservableObject {
|
|||||||
private let moshHolder: MoshHolder
|
private let moshHolder: MoshHolder
|
||||||
private var moshSession: MoshSession?
|
private var moshSession: MoshSession?
|
||||||
private var moshRelay: TsnetbridgeMoshRelay?
|
private var moshRelay: TsnetbridgeMoshRelay?
|
||||||
private let moshScanner = MoshConnectScanner()
|
private var moshScanner = MoshConnectScanner()
|
||||||
private var moshBootstrapSent = false
|
private var moshBootstrapSent = false
|
||||||
|
// M3 前台恢复:基线时刻 + healthy 轮询 + 恢复看门狗 + 后台任务。
|
||||||
|
private var moshResumeBaseMs: UInt64 = 0
|
||||||
|
private var moshHealthyPoll: Task<Void, Never>?
|
||||||
|
private var resumeWatchdog: Task<Void, Never>?
|
||||||
|
#if canImport(UIKit)
|
||||||
|
private var bgTask: UIBackgroundTaskIdentifier = .invalid
|
||||||
|
#endif
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let holder = TransportHolder()
|
let holder = TransportHolder()
|
||||||
@@ -168,8 +184,14 @@ final class SSHTerminalModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func close() { run(machine.reduce(.closeRequested)) }
|
func close() { run(machine.reduce(.closeRequested)) }
|
||||||
func enterBackground() { run(machine.reduce(.enteredBackground)) }
|
func enterBackground() {
|
||||||
func enterForeground() { run(machine.reduce(.enteredForeground)) }
|
if moshMode { beginBgTask() } // 争取 ~30s 后台执行,mosh 每 3s ack 保活 NAT/WG/DERP
|
||||||
|
run(machine.reduce(.enteredBackground))
|
||||||
|
}
|
||||||
|
func enterForeground() {
|
||||||
|
endBgTask()
|
||||||
|
run(machine.reduce(.enteredForeground))
|
||||||
|
}
|
||||||
|
|
||||||
/// TerminalScreen 在 surface 首次布局后调用:放行缓冲输出(+ 无头自动命令)。
|
/// TerminalScreen 在 surface 首次布局后调用:放行缓冲输出(+ 无头自动命令)。
|
||||||
func markSurfaceReady() {
|
func markSurfaceReady() {
|
||||||
@@ -213,6 +235,10 @@ final class SSHTerminalModel: ObservableObject {
|
|||||||
case .cancelReconnectTimer: reconnectTask?.cancel(); reconnectTask = nil
|
case .cancelReconnectTimer: reconnectTask?.cancel(); reconnectTask = nil
|
||||||
case .teardownTransport: holder.transport?.stop()
|
case .teardownTransport: holder.transport?.stop()
|
||||||
case .notify(let message): banner = message
|
case .notify(let message): banner = message
|
||||||
|
case .nudgeResume: performWakePulse(); startHealthyPollIfNeeded()
|
||||||
|
case .scheduleResumeWatchdog(let ms): scheduleResumeWatchdog(ms)
|
||||||
|
case .cancelResumeWatchdog: cancelResumeWatchdog()
|
||||||
|
case .teardownMosh: teardownMosh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
syncUI()
|
syncUI()
|
||||||
@@ -343,14 +369,93 @@ final class SSHTerminalModel: ObservableObject {
|
|||||||
self.gate.deliver(Data(bytes))
|
self.gate.deliver(Data(bytes))
|
||||||
}
|
}
|
||||||
mosh.onClosed = { [weak self] rc in
|
mosh.onClosed = { [weak self] rc in
|
||||||
Task { @MainActor in self?.banner = "mosh 已结束(rc=\(rc))" }
|
Task { @MainActor in
|
||||||
|
guard let self else { return }
|
||||||
|
self.run(self.machine.reduce(.moshExited(rc: Int(rc))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
moshRelay = relay
|
moshRelay = relay
|
||||||
moshSession = mosh
|
moshSession = mosh
|
||||||
moshHolder.session = mosh // 输入/resize 从此路由到 mosh
|
moshHolder.session = mosh // 输入/resize 从此路由到 mosh
|
||||||
mosh.start()
|
mosh.start()
|
||||||
NSLog("MOSHDBG activateMosh ip=\(ip) port=\(port) started")
|
NSLog("MOSHDBG activateMosh ip=\(ip) port=\(port) started")
|
||||||
banner = "mosh 已连接"
|
// mosh 接管成功 → 状态机拆掉底层 idle SSH(moshActive 之后忽略其 transportClosed)。
|
||||||
|
run(machine.reduce(.moshEstablished))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - M3 前台快速恢复
|
||||||
|
|
||||||
|
/// 唤醒脉冲:tsnet WakeUp(后台线程,可能阻塞)+ relay 同端口 Rebind + mosh SIGCONT。
|
||||||
|
private func performWakePulse() {
|
||||||
|
guard let mosh = moshSession else { return }
|
||||||
|
if moshResumeBaseMs == 0 { moshResumeBaseMs = mosh.lastHeardMs() }
|
||||||
|
if tsnetAuthKey != nil {
|
||||||
|
let mgr = tsnetMgr
|
||||||
|
Task.detached { mgr.wakeUp() }
|
||||||
|
}
|
||||||
|
do { try moshRelay?.rebind() }
|
||||||
|
catch { NSLog("MOSHDBG relay rebind failed: \(error.localizedDescription)") }
|
||||||
|
mosh.nudge()
|
||||||
|
NSLog("MOSHDBG wake pulse base=\(moshResumeBaseMs)")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 轮询 mosh 探针:收到基线之后的新服务器状态即判定 SSP 已续(healthy)。
|
||||||
|
private func startHealthyPollIfNeeded() {
|
||||||
|
guard moshHealthyPoll == nil, let mosh = moshSession else { return }
|
||||||
|
let base = moshResumeBaseMs
|
||||||
|
moshHealthyPoll = Task { [weak self] in
|
||||||
|
for _ in 0 ..< 100 { // ~10s 安全上限(看门狗更早兜底)
|
||||||
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||||
|
if Task.isCancelled { return }
|
||||||
|
if mosh.lastHeardMs() > base {
|
||||||
|
guard let self else { return }
|
||||||
|
NSLog("TXM3 recovered base=\(base) now=\(mosh.lastHeardMs())")
|
||||||
|
self.moshHealthyPoll = nil
|
||||||
|
self.run(self.machine.reduce(.moshHealthy))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleResumeWatchdog(_ ms: Int) {
|
||||||
|
resumeWatchdog?.cancel()
|
||||||
|
resumeWatchdog = Task { [weak self] in
|
||||||
|
try? await Task.sleep(nanoseconds: UInt64(ms) * 1_000_000)
|
||||||
|
guard !Task.isCancelled, let self else { return }
|
||||||
|
self.run(self.machine.reduce(.resumeWatchdogFired))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelResumeWatchdog() {
|
||||||
|
resumeWatchdog?.cancel(); resumeWatchdog = nil
|
||||||
|
moshHealthyPoll?.cancel(); moshHealthyPoll = nil
|
||||||
|
moshResumeBaseMs = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 拆除 mosh 会话(兜底全量重建 / 关闭时)。重置引导状态,允许重新引导 mosh-server。
|
||||||
|
private func teardownMosh() {
|
||||||
|
cancelResumeWatchdog()
|
||||||
|
moshHolder.session = nil
|
||||||
|
moshSession?.close(); moshSession = nil
|
||||||
|
try? moshRelay?.close(); moshRelay = nil
|
||||||
|
moshBootstrapSent = false
|
||||||
|
moshScanner = MoshConnectScanner()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func beginBgTask() {
|
||||||
|
#if canImport(UIKit)
|
||||||
|
guard bgTask == .invalid else { return }
|
||||||
|
bgTask = UIApplication.shared.beginBackgroundTask(withName: "mosh-keepalive") { [weak self] in
|
||||||
|
self?.endBgTask()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func endBgTask() {
|
||||||
|
#if canImport(UIKit)
|
||||||
|
if bgTask != .invalid { UIApplication.shared.endBackgroundTask(bgTask); bgTask = .invalid }
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleTransportState(_ st: TransportState) {
|
private func handleTransportState(_ st: TransportState) {
|
||||||
@@ -379,7 +484,8 @@ final class SSHTerminalModel: ObservableObject {
|
|||||||
|
|
||||||
private func syncUI() {
|
private func syncUI() {
|
||||||
switch machine.phase {
|
switch machine.phase {
|
||||||
case .connected, .reconnecting, .waitingToReconnect, .backgroundParked:
|
case .connected, .reconnecting, .waitingToReconnect, .backgroundParked,
|
||||||
|
.moshActive, .moshParked, .moshResuming:
|
||||||
showsTerminal = true
|
showsTerminal = true
|
||||||
case .idle, .connecting, .authenticating, .closed, .failed:
|
case .idle, .connecting, .authenticating, .closed, .failed:
|
||||||
showsTerminal = false
|
showsTerminal = false
|
||||||
@@ -398,6 +504,9 @@ final class SSHTerminalModel: ObservableObject {
|
|||||||
case .reconnecting(let n): "重连中(第 \(n) 次)"
|
case .reconnecting(let n): "重连中(第 \(n) 次)"
|
||||||
case .failed(let r): "失败:\(r)"
|
case .failed(let r): "失败:\(r)"
|
||||||
case .closed: "已关闭"
|
case .closed: "已关闭"
|
||||||
|
case .moshActive: "mosh 已连接"
|
||||||
|
case .moshParked: "已挂起(后台)"
|
||||||
|
case .moshResuming(let n): "mosh 恢复中(第 \(n) 次)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ targets:
|
|||||||
CURRENT_PROJECT_VERSION: "1"
|
CURRENT_PROJECT_VERSION: "1"
|
||||||
SWIFT_VERSION: "6.0"
|
SWIFT_VERSION: "6.0"
|
||||||
GENERATE_INFOPLIST_FILE: YES
|
GENERATE_INFOPLIST_FILE: YES
|
||||||
|
# 部分 Info.plist 基底(仅 URL scheme,供无头验证前台唤醒);生成键合并其上。
|
||||||
|
INFOPLIST_FILE: TerminalX-Info.plist
|
||||||
TARGETED_DEVICE_FAMILY: "1,2"
|
TARGETED_DEVICE_FAMILY: "1,2"
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation: YES
|
INFOPLIST_KEY_UILaunchScreen_Generation: YES
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: YES
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: YES
|
||||||
|
|||||||
@@ -21,20 +21,6 @@
|
|||||||
<key>SupportedPlatformVariant</key>
|
<key>SupportedPlatformVariant</key>
|
||||||
<string>simulator</string>
|
<string>simulator</string>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
|
||||||
<key>BinaryPath</key>
|
|
||||||
<string>libMoshCore.a</string>
|
|
||||||
<key>LibraryIdentifier</key>
|
|
||||||
<string>ios-arm64</string>
|
|
||||||
<key>LibraryPath</key>
|
|
||||||
<string>libMoshCore.a</string>
|
|
||||||
<key>SupportedArchitectures</key>
|
|
||||||
<array>
|
|
||||||
<string>arm64</string>
|
|
||||||
</array>
|
|
||||||
<key>SupportedPlatform</key>
|
|
||||||
<string>ios</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
<dict>
|
||||||
<key>BinaryPath</key>
|
<key>BinaryPath</key>
|
||||||
<string>libMoshCore.a</string>
|
<string>libMoshCore.a</string>
|
||||||
@@ -49,6 +35,20 @@
|
|||||||
<key>SupportedPlatform</key>
|
<key>SupportedPlatform</key>
|
||||||
<string>macos</string>
|
<string>macos</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>BinaryPath</key>
|
||||||
|
<string>libMoshCore.a</string>
|
||||||
|
<key>LibraryIdentifier</key>
|
||||||
|
<string>ios-arm64</string>
|
||||||
|
<key>LibraryPath</key>
|
||||||
|
<string>libMoshCore.a</string>
|
||||||
|
<key>SupportedArchitectures</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>SupportedPlatform</key>
|
||||||
|
<string>ios</string>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XFWK</string>
|
<string>XFWK</string>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,6 +4,20 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>AvailableLibraries</key>
|
<key>AvailableLibraries</key>
|
||||||
<array>
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>BinaryPath</key>
|
||||||
|
<string>TsnetBridge.framework/TsnetBridge</string>
|
||||||
|
<key>LibraryIdentifier</key>
|
||||||
|
<string>ios-arm64</string>
|
||||||
|
<key>LibraryPath</key>
|
||||||
|
<string>TsnetBridge.framework</string>
|
||||||
|
<key>SupportedArchitectures</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>SupportedPlatform</key>
|
||||||
|
<string>ios</string>
|
||||||
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>BinaryPath</key>
|
<key>BinaryPath</key>
|
||||||
<string>TsnetBridge.framework/TsnetBridge</string>
|
<string>TsnetBridge.framework/TsnetBridge</string>
|
||||||
@@ -21,20 +35,6 @@
|
|||||||
<key>SupportedPlatformVariant</key>
|
<key>SupportedPlatformVariant</key>
|
||||||
<string>simulator</string>
|
<string>simulator</string>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
|
||||||
<key>BinaryPath</key>
|
|
||||||
<string>TsnetBridge.framework/TsnetBridge</string>
|
|
||||||
<key>LibraryIdentifier</key>
|
|
||||||
<string>ios-arm64</string>
|
|
||||||
<key>LibraryPath</key>
|
|
||||||
<string>TsnetBridge.framework</string>
|
|
||||||
<key>SupportedArchitectures</key>
|
|
||||||
<array>
|
|
||||||
<string>arm64</string>
|
|
||||||
</array>
|
|
||||||
<key>SupportedPlatform</key>
|
|
||||||
<string>ios</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XFWK</string>
|
<string>XFWK</string>
|
||||||
|
|||||||
@@ -27,13 +27,18 @@ tailnet 内 host:moshPort,回包再转回"最近一次上行的 client 源地
|
|||||||
- (nonnull instancetype)initWithRef:(_Nonnull id)ref;
|
- (nonnull instancetype)initWithRef:(_Nonnull id)ref;
|
||||||
- (nonnull instancetype)init;
|
- (nonnull instancetype)init;
|
||||||
/**
|
/**
|
||||||
* Close 关闭中继(两条泵 goroutine 随即因 conn 关闭而退出)。
|
* Close 关闭中继(两条泵随 conn 关闭而退出)。
|
||||||
*/
|
*/
|
||||||
- (BOOL)close:(NSError* _Nullable* _Nullable)error;
|
- (BOOL)close:(NSError* _Nullable* _Nullable)error;
|
||||||
/**
|
/**
|
||||||
* LocalPort 返回 loopback relay 监听的端口(mosh_main 的 port 参数)。
|
* LocalPort 返回固定的 loopback relay 端口(mosh_main 的 port 参数,跨 Rebind 不变)。
|
||||||
*/
|
*/
|
||||||
- (long)localPort;
|
- (long)localPort;
|
||||||
|
/**
|
||||||
|
* Rebind 在同端口重开 loopback socket(iOS 前台恢复用:挂起后旧 socket 可能被系统回收为 defunct)。
|
||||||
|
关旧 socket → 旧上行泵退出 → 新泵接管;下行泵绑定稳定 tsConn 无需重启。端口被占等返回 error(由上层升级为全量重建)。
|
||||||
|
*/
|
||||||
|
- (BOOL)rebind:(NSError* _Nullable* _Nullable)error;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +82,11 @@ Swift 侧 close(fd) 会触发泵结束并关闭 tsConn。返回 -1 + error 表
|
|||||||
gomobile 友好签名:基本类型入参、error 出参。
|
gomobile 友好签名:基本类型入参、error 出参。
|
||||||
*/
|
*/
|
||||||
- (BOOL)upWithAuthKey:(NSString* _Nullable)authKey timeoutMs:(long)timeoutMs error:(NSError* _Nullable* _Nullable)error;
|
- (BOOL)upWithAuthKey:(NSString* _Nullable)authKey timeoutMs:(long)timeoutMs error:(NSError* _Nullable* _Nullable)error;
|
||||||
|
/**
|
||||||
|
* WakeUp 在 iOS 前台恢复时主动唤醒 tsnet 网络层:注入链路变化事件 + Rebind/重新 STUN,
|
||||||
|
把"等首次写失败才发现 DERP/路径已死"变成"立刻重建",配合 mosh SSP 实现秒级恢复。
|
||||||
|
*/
|
||||||
|
- (void)wakeUp;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>100.0</string>
|
<string>100.0</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.0.1784861278</string>
|
<string>0.0.1784882284</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.0.1784861278</string>
|
<string>0.0.1784882284</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>FMWK</string>
|
<string>FMWK</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
Binary file not shown.
@@ -27,13 +27,18 @@ tailnet 内 host:moshPort,回包再转回"最近一次上行的 client 源地
|
|||||||
- (nonnull instancetype)initWithRef:(_Nonnull id)ref;
|
- (nonnull instancetype)initWithRef:(_Nonnull id)ref;
|
||||||
- (nonnull instancetype)init;
|
- (nonnull instancetype)init;
|
||||||
/**
|
/**
|
||||||
* Close 关闭中继(两条泵 goroutine 随即因 conn 关闭而退出)。
|
* Close 关闭中继(两条泵随 conn 关闭而退出)。
|
||||||
*/
|
*/
|
||||||
- (BOOL)close:(NSError* _Nullable* _Nullable)error;
|
- (BOOL)close:(NSError* _Nullable* _Nullable)error;
|
||||||
/**
|
/**
|
||||||
* LocalPort 返回 loopback relay 监听的端口(mosh_main 的 port 参数)。
|
* LocalPort 返回固定的 loopback relay 端口(mosh_main 的 port 参数,跨 Rebind 不变)。
|
||||||
*/
|
*/
|
||||||
- (long)localPort;
|
- (long)localPort;
|
||||||
|
/**
|
||||||
|
* Rebind 在同端口重开 loopback socket(iOS 前台恢复用:挂起后旧 socket 可能被系统回收为 defunct)。
|
||||||
|
关旧 socket → 旧上行泵退出 → 新泵接管;下行泵绑定稳定 tsConn 无需重启。端口被占等返回 error(由上层升级为全量重建)。
|
||||||
|
*/
|
||||||
|
- (BOOL)rebind:(NSError* _Nullable* _Nullable)error;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +82,11 @@ Swift 侧 close(fd) 会触发泵结束并关闭 tsConn。返回 -1 + error 表
|
|||||||
gomobile 友好签名:基本类型入参、error 出参。
|
gomobile 友好签名:基本类型入参、error 出参。
|
||||||
*/
|
*/
|
||||||
- (BOOL)upWithAuthKey:(NSString* _Nullable)authKey timeoutMs:(long)timeoutMs error:(NSError* _Nullable* _Nullable)error;
|
- (BOOL)upWithAuthKey:(NSString* _Nullable)authKey timeoutMs:(long)timeoutMs error:(NSError* _Nullable* _Nullable)error;
|
||||||
|
/**
|
||||||
|
* WakeUp 在 iOS 前台恢复时主动唤醒 tsnet 网络层:注入链路变化事件 + Rebind/重新 STUN,
|
||||||
|
把"等首次写失败才发现 DERP/路径已死"变成"立刻重建",配合 mosh SSP 实现秒级恢复。
|
||||||
|
*/
|
||||||
|
- (void)wakeUp;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>100.0</string>
|
<string>100.0</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.0.1784861278</string>
|
<string>0.0.1784882284</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.0.1784861278</string>
|
<string>0.0.1784882284</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>FMWK</string>
|
<string>FMWK</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
Binary file not shown.
@@ -6,7 +6,7 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 0. 一句话现状
|
## 0. 一句话现状
|
||||||
一个**原生 iPad SSH 终端**已可用并真机验证:libghostty 渲染 + 键盘输入 + SSH 直连 + **SSH-over-tsnet(Tailscale 用户态)** + **tmux -CC 原生 tab** + **mosh(LAN 直连已端到端验证)** + 断线自动重连。四大核心需求(libghostty/tailscale/tmux/**mosh 均已验证**)。仅在 iOS 模拟器验证(未上真机/未签名/未提交 git)。
|
一个**原生 iPad SSH 终端**已可用并真机验证:libghostty 渲染 + 键盘输入 + SSH 直连 + **SSH-over-tsnet(Tailscale 用户态)** + **tmux -CC 原生 tab** + **mosh(LAN + over-tsnet 均已端到端验证)** + 断线自动重连 + **mosh 挂起→恢复<3s(M3)**。四大核心需求(libghostty/tailscale/tmux/**mosh 均已验证**)。仅在 iOS 模拟器验证(未上真机/未签名)。
|
||||||
> **mosh 状态(2026-07-24 全部验证通过)**:mosh(C++,blinksh/ios)+protobuf-lite 交叉编译成 `MoshCore.xcframework`(CommonCrypto 后端,含 arm64 模拟器 slice);tsnet 桥加 UDP relay(`StartMoshRelay`);`MoshSession`(pipe↔FILE*/pthread/SIGWINCH)+`MoshConnectScanner`(TXCore,39 单测)+`SSHTerminalModel` 编排(SSH 引导 mosh-server→解析 MOSH CONNECT→接管)。
|
> **mosh 状态(2026-07-24 全部验证通过)**:mosh(C++,blinksh/ios)+protobuf-lite 交叉编译成 `MoshCore.xcframework`(CommonCrypto 后端,含 arm64 模拟器 slice);tsnet 桥加 UDP relay(`StartMoshRelay`);`MoshSession`(pipe↔FILE*/pthread/SIGWINCH)+`MoshConnectScanner`(TXCore,39 单测)+`SSHTerminalModel` 编排(SSH 引导 mosh-server→解析 MOSH CONNECT→接管)。
|
||||||
> - **LAN 直连端到端验证**(192.168.9.199):mosh-server 存活 104s≫60s 无客户端超时+子 shell+心跳=真连。
|
> - **LAN 直连端到端验证**(192.168.9.199):mosh-server 存活 104s≫60s 无客户端超时+子 shell+心跳=真连。
|
||||||
> - **mosh over tsnet 端到端验证**(vohive-vm 100.69.201.101):app 日志 `parsed MOSH CONNECT port=60002`→`relay up localPort=63542`→`activateMosh started`;屏幕渲染远端 shell 且无 mosh 断连 overlay=relay UDP 双向流通。**R1(tsnet UDP 数据报语义) 证伪**。
|
> - **mosh over tsnet 端到端验证**(vohive-vm 100.69.201.101):app 日志 `parsed MOSH CONNECT port=60002`→`relay up localPort=63542`→`activateMosh started`;屏幕渲染远端 shell 且无 mosh 断连 overlay=relay UDP 双向流通。**R1(tsnet UDP 数据报语义) 证伪**。
|
||||||
@@ -41,20 +41,21 @@
|
|||||||
| dial 重试打磨 | ✅ | `TsnetManager.dialFD` 6 次退避重试,解决 tsnet 首连路径预热超时 |
|
| dial 重试打磨 | ✅ | `TsnetManager.dialFD` 6 次退避重试,解决 tsnet 首连路径预热超时 |
|
||||||
| **R5** Go+gvisor+C+++Swift 同进程 | ✅ retire | 全程真机无崩溃 |
|
| **R5** Go+gvisor+C+++Swift 同进程 | ✅ retire | 全程真机无崩溃 |
|
||||||
| **M2** mosh | ✅ 完成(LAN + tsnet 均验证) | libmoshios(C++)+protobuf-lite→`MoshCore.xcframework`;UDP relay over tsnet;**LAN 直连 + mosh-over-tsnet 均端到端验证通过**(R1 证伪) |
|
| **M2** mosh | ✅ 完成(LAN + tsnet 均验证) | libmoshios(C++)+protobuf-lite→`MoshCore.xcframework`;UDP relay over tsnet;**LAN 直连 + mosh-over-tsnet 均端到端验证通过**(R1 证伪) |
|
||||||
| **M3** 生命周期深化 | ◻️ 部分 | 前后台 FSM 已做;mosh 快速恢复 + tmux 兜底 待做 |
|
| **M3** 生命周期深化 | ✅ mosh 快恢复(真机深挂起待补) | scenePhase FSM + **mosh 挂起→恢复<3s 已验证**(模拟器:15/30s 冻结→前台,wake pulse→1.3s 收到服务器包,"已恢复");冷启动重连(SSP 序号+key 持久化)划入 M4/M5 |
|
||||||
| **M4** 安全 | ◻️ 未做 | **当前 SSH 接受任意 host key**(见 SSHSession TODO);known_hosts 固定 + Secure Enclave 密钥 待做 |
|
| **M4** 安全 | ◻️ 未做 | **当前 SSH 接受任意 host key**(见 SSHSession TODO);known_hosts 固定 + Secure Enclave 密钥 待做 |
|
||||||
| **M5** 合规提审 | ⏸️ 暂缓 | 用户指示暂缓 |
|
| **M5** 合规提审 | ⏸️ 暂缓 | 用户指示暂缓 |
|
||||||
|
|
||||||
## 5. 代码地图与数据流
|
## 5. 代码地图与数据流
|
||||||
- `packages/TXCore/Sources/TXCore/`
|
- `packages/TXCore/Sources/TXCore/`
|
||||||
- `Tmux/TmuxControlParser.swift` `TmuxEvent.swift` `TmuxIDs.swift` `TmuxOutputDecoder.swift` `TmuxLayout.swift` `TmuxControlSequence.swift`(DCS 检测)—— 纯解析,35 单测在 `Tests/`。
|
- `Tmux/TmuxControlParser.swift` `TmuxEvent.swift` `TmuxIDs.swift` `TmuxOutputDecoder.swift` `TmuxLayout.swift` `TmuxControlSequence.swift`(DCS 检测)—— 纯解析,35 单测在 `Tests/`。
|
||||||
- `Session/SessionMachine.swift` —— `reduce(event)->[Effect]` 会话状态机(指数退避重连/后台冻结/前台恢复)。
|
- `Session/SessionMachine.swift` —— `reduce(event)->[Effect]` 会话状态机(指数退避重连/后台冻结/前台恢复)。**M3 加 mosh 三相** `moshActive/moshParked/moshResuming` + 事件 `moshEstablished/moshHealthy/moshExited/resumeWatchdogFired` + 效果 `nudgeResume/scheduleResumeWatchdog/teardownMosh`:mosh 接管即拆 SSH、后台冻结不重建、前台打唤醒脉冲等 SSP 续、两次脉冲无效兜底全量重建。
|
||||||
- `packages/TXTransport/Sources/`
|
- `packages/TXTransport/Sources/`
|
||||||
- `TXTransport/Transport.swift`(协议+SSHConfig+SSHError)、`SSHSession.swift`(libssh2;`preconnectedFD` 支持外部 fd;事件循环 poll+非阻塞)。
|
- `TXTransport/Transport.swift`(协议+SSHConfig+SSHError)、`SSHSession.swift`(libssh2;`preconnectedFD` 支持外部 fd;事件循环 poll+非阻塞)。
|
||||||
- `TXTransport/MoshSession.swift`(M2):驱动 `mosh_main`;pipe↔FILE* 桥(in/out)、Foundation.Thread 跑阻塞主循环+`pthread_self()` 供 `pthread_kill(SIGWINCH)` resize、忽略 SIGPIPE、setenv UTF-8 locale。
|
- `TXTransport/MoshSession.swift`(M2):驱动 `mosh_main`;pipe↔FILE* 桥(in/out)、Foundation.Thread 跑阻塞主循环+`pthread_self()` 供 `pthread_kill(SIGWINCH)` resize、忽略 SIGPIPE、setenv UTF-8 locale。
|
||||||
- `CSSH/`(C target:libssh2 头 + modulemap,供 `import CSSH`)、`CSSHCore`(binaryTarget,library-only)。
|
- `CSSH/`(C target:libssh2 头 + modulemap,供 `import CSSH`)、`CSSHCore`(binaryTarget,library-only)。
|
||||||
- `CMosh/`(C target:`moshiosbridge.h`(mosh_main)+modulemap+占位 shim.c,供 `import CMosh`)、`MoshCore`(binaryTarget,library-only);TXTransport 链 `libc++`/`libz`(mosh 是 C++ 且 compressor 用 zlib;CommonCrypto 属 libSystem 自动解析)。
|
- `CMosh/`(C target:`moshiosbridge.h`(mosh_main)+modulemap+占位 shim.c,供 `import CMosh`)、`MoshCore`(binaryTarget,library-only);TXTransport 链 `libc++`/`libz`(mosh 是 C++ 且 compressor 用 zlib;CommonCrypto 属 libSystem 自动解析)。
|
||||||
- `TXCore/Mosh/MoshConnect.swift`:`MoshConnectScanner` 流式扫描 `MOSH CONNECT <port> <key>`(注意 Swift 视 `\r\n` 为单 Character,行尾判定须用 `isNewline`)。
|
- `TXCore/Mosh/MoshConnect.swift`:`MoshConnectScanner` 流式扫描 `MOSH CONNECT <port> <key>`(注意 Swift 视 `\r\n` 为单 Character,行尾判定须用 `isNewline`)。
|
||||||
|
- **M3 快恢复链**:`bridge.go` 加 `Node.WakeUp()`(tsnet `InjectEvent`+`MagicSock.Rebind/ReSTUN`) 与 `MoshRelay.Rebind()`(同端口重开 loopback、泵按 socket 生成容错);`iosclient.cc` 加 `g_mosh_last_heard`(收到服务器包即更新,导出 `mosh_last_heard_ms()`) 并恢复 SIGCONT 全屏重绘;`MoshSession.nudge()`(SIGCONT) + `lastHeardMs()`;`SSHTerminalModel` 前台执行"唤醒脉冲三连"(WakeUp+Rebind+nudge)+healthy 轮询(探针增长→`moshHealthy`)+恢复看门狗+`beginBackgroundTask`。
|
||||||
- `apps/TerminalX/iOS/`
|
- `apps/TerminalX/iOS/`
|
||||||
- `SSHTerminalModel.swift` —— 核心编排:`OutputGate`(surface-ready 缓冲)、`TransportHolder`、`TsnetManager`(tsnet 节点 up/dial+重试)、`TsnetError`、`TmuxRouter`(raw/gateway 字节分流)、状态机驱动 startSession(直连/tsnet 分支)、`autoConnectIfConfigured`(启动参数)。
|
- `SSHTerminalModel.swift` —— 核心编排:`OutputGate`(surface-ready 缓冲)、`TransportHolder`、`TsnetManager`(tsnet 节点 up/dial+重试)、`TsnetError`、`TmuxRouter`(raw/gateway 字节分流)、状态机驱动 startSession(直连/tsnet 分支)、`autoConnectIfConfigured`(启动参数)。
|
||||||
- `ContentView.swift` —— 分支:`tmuxController != nil`→`TmuxTabbedView`(tab 条+活动窗口);否则 `RawTerminalView`;`TsnetProbeView`(纯自检);`ConnectionForm`。
|
- `ContentView.swift` —— 分支:`tmuxController != nil`→`TmuxTabbedView`(tab 条+活动窗口);否则 `RawTerminalView`;`TsnetProbeView`(纯自检);`ConnectionForm`。
|
||||||
@@ -74,8 +75,9 @@
|
|||||||
|
|
||||||
## 7. 下一步建议(按价值)
|
## 7. 下一步建议(按价值)
|
||||||
1. **M2 mosh 打磨**(功能已通,打磨项):`activateMosh` 写死 80x24 初值(靠首次 resize 纠正);SSH 会话在 mosh 接管后保持 idle 未关(占一条连接);`MoshSession.close()` 后 mosh 线程可能滞留(阻塞主循环,见 TODO);moshiosbridge.cc 顶部 `fwrite("Hello from the Bridge!")` 调试行可清理;`SSHTerminalModel` 里 `NSLog("MOSHDBG …")` 诊断日志可按需保留/删除;mosh-server 引导后远端会累积 detached 会话(`mosh-server new` 每次新建),可考虑复用或清理。**挂起→恢复<3s 门(M3)待专门测**。
|
1. **M2 mosh 打磨**(功能已通,打磨项):`activateMosh` 写死 80x24 初值(靠首次 resize 纠正);SSH 会话在 mosh 接管后保持 idle 未关(占一条连接);`MoshSession.close()` 后 mosh 线程可能滞留(阻塞主循环,见 TODO);moshiosbridge.cc 顶部 `fwrite("Hello from the Bridge!")` 调试行可清理;`SSHTerminalModel` 里 `NSLog("MOSHDBG …")` 诊断日志可按需保留/删除;mosh-server 引导后远端会累积 detached 会话(`mosh-server new` 每次新建),可考虑复用或清理。**挂起→恢复<3s 门(M3)待专门测**。
|
||||||
3. **tmux 多 pane 分屏**:`TmuxLayout` 已能解析布局树 → 把 window 内多 pane 渲染成 SwiftUI 分屏(每 pane 一 surface)。
|
3. **M3 真机深挂起补测**:模拟器不复现 socket defunct、且看门狗会杀掉被 `kill -STOP` 冻结过久的 app(>~30s 概率被杀),故 WG 密钥过期(>180s)/DERP 死链/socket defunct 路径需**真机锁屏数分钟**用例补测(`MoshRelay.Rebind`/`hop_port` 的正确性靠代码 + 真机)。
|
||||||
4. **M4 安全**:known_hosts 固定 + SE 密钥。
|
4. **tmux 多 pane 分屏**:`TmuxLayout` 已能解析布局树 → 把 window 内多 pane 渲染成 SwiftUI 分屏(每 pane 一 surface)。
|
||||||
|
5. **M4 安全 + 冷启动 mosh 重连**:known_hosts 固定 + SE 密钥;mosh 进程死后重连原 detached 会话需持久化 MOSH_KEY+port 且改 mosh 序列化 SSP 序号/终端状态(Blink 式,大活)。
|
||||||
5. **产品化**:主机列表持久化(Keychain)、多标签(非 tmux)、软键盘运维工具栏(GhosttyKit 内建,接线即可)。
|
5. **产品化**:主机列表持久化(Keychain)、多标签(非 tmux)、软键盘运维工具栏(GhosttyKit 内建,接线即可)。
|
||||||
|
|
||||||
## 8. 测试资源(用户提供,**凭据勿写入仓库/勿硬编码**,每次向用户索取)
|
## 8. 测试资源(用户提供,**凭据勿写入仓库/勿硬编码**,每次向用户索取)
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ public struct SessionMachine: Sendable, Equatable {
|
|||||||
case failed(reason: String)
|
case failed(reason: String)
|
||||||
/// 用户主动关闭。
|
/// 用户主动关闭。
|
||||||
case closed
|
case closed
|
||||||
|
// —— M3 mosh 会话相(mosh 接管后,SSH 已拆,生命周期由 mosh SSP 承载)——
|
||||||
|
/// mosh 已接管并活跃(底层 SSH 已拆除;transportClosed 不再影响)。
|
||||||
|
case moshActive
|
||||||
|
/// 进入后台,mosh 冻结(SSP 会在前台自动续传,不重建)。
|
||||||
|
case moshParked
|
||||||
|
/// 前台恢复中:已发唤醒脉冲,等 mosh SSP resync(记录第几次尝试,超时兜底)。
|
||||||
|
case moshResuming(attempt: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Event: Sendable, Equatable {
|
public enum Event: Sendable, Equatable {
|
||||||
@@ -35,6 +42,11 @@ public struct SessionMachine: Sendable, Equatable {
|
|||||||
case enteredForeground
|
case enteredForeground
|
||||||
case closeRequested
|
case closeRequested
|
||||||
case retryRequested // 用户手动重试(从 failed 复位)
|
case retryRequested // 用户手动重试(从 failed 复位)
|
||||||
|
// —— M3 mosh ——
|
||||||
|
case moshEstablished // mosh 接管会话成功(此后拆 SSH)
|
||||||
|
case moshHealthy // 前台恢复后收到服务器合法包(SSP 已续上)
|
||||||
|
case moshExited(rc: Int) // mosh 主循环退出(会话结束)
|
||||||
|
case resumeWatchdogFired // 恢复看门狗到点(本次唤醒脉冲未见 healthy)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Effect: Sendable, Equatable {
|
public enum Effect: Sendable, Equatable {
|
||||||
@@ -43,6 +55,11 @@ public struct SessionMachine: Sendable, Equatable {
|
|||||||
case cancelReconnectTimer
|
case cancelReconnectTimer
|
||||||
case teardownTransport
|
case teardownTransport
|
||||||
case notify(String) // UI 提示
|
case notify(String) // UI 提示
|
||||||
|
// —— M3 mosh ——
|
||||||
|
case nudgeResume // 打唤醒脉冲:tsnet WakeUp + relay Rebind + mosh SIGCONT
|
||||||
|
case scheduleResumeWatchdog(ms: Int) // 排程恢复看门狗
|
||||||
|
case cancelResumeWatchdog
|
||||||
|
case teardownMosh // 拆除 mosh 会话(relay + 线程)
|
||||||
}
|
}
|
||||||
|
|
||||||
public private(set) var phase: Phase = .idle
|
public private(set) var phase: Phase = .idle
|
||||||
@@ -52,11 +69,18 @@ public struct SessionMachine: Sendable, Equatable {
|
|||||||
/// 退避基数与上限(毫秒)。
|
/// 退避基数与上限(毫秒)。
|
||||||
private let baseDelayMS: Int
|
private let baseDelayMS: Int
|
||||||
private let maxDelayMS: Int
|
private let maxDelayMS: Int
|
||||||
|
/// M3:单次唤醒脉冲后等 mosh SSP 续上的看门狗窗口;超时则再脉冲一次。
|
||||||
|
public let resumeWatchdogMS: Int
|
||||||
|
/// M3:最多打几次唤醒脉冲,仍未 healthy 则兜底全量重建。
|
||||||
|
public let maxResumePulses: Int
|
||||||
|
|
||||||
public init(maxReconnectAttempts: Int = 6, baseDelayMS: Int = 500, maxDelayMS: Int = 16000) {
|
public init(maxReconnectAttempts: Int = 6, baseDelayMS: Int = 500, maxDelayMS: Int = 16000,
|
||||||
|
resumeWatchdogMS: Int = 3000, maxResumePulses: Int = 2) {
|
||||||
self.maxReconnectAttempts = maxReconnectAttempts
|
self.maxReconnectAttempts = maxReconnectAttempts
|
||||||
self.baseDelayMS = baseDelayMS
|
self.baseDelayMS = baseDelayMS
|
||||||
self.maxDelayMS = maxDelayMS
|
self.maxDelayMS = maxDelayMS
|
||||||
|
self.resumeWatchdogMS = resumeWatchdogMS
|
||||||
|
self.maxResumePulses = maxResumePulses
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 指数退避:base * 2^(attempt-1),封顶 maxDelayMS。
|
/// 指数退避:base * 2^(attempt-1),封顶 maxDelayMS。
|
||||||
@@ -135,10 +159,60 @@ public struct SessionMachine: Sendable, Equatable {
|
|||||||
phase = .backgroundParked
|
phase = .backgroundParked
|
||||||
return [.cancelReconnectTimer]
|
return [.cancelReconnectTimer]
|
||||||
|
|
||||||
|
// —— M3 mosh 会话相 ——
|
||||||
|
// mosh 接管成功(引导完成):拆掉底层 SSH,进入 mosh 自持会话。
|
||||||
|
case (.connected, .moshEstablished), (.reconnecting, .moshEstablished):
|
||||||
|
phase = .moshActive
|
||||||
|
return [.teardownTransport, .notify("mosh 已接管")]
|
||||||
|
|
||||||
|
// mosh 活跃期间底层 SSH 残连断开:忽略(mosh 不依赖 SSH)。
|
||||||
|
case (.moshActive, .transportClosed):
|
||||||
|
return []
|
||||||
|
|
||||||
|
// mosh 活跃 → 后台:冻结,不重建。
|
||||||
|
case (.moshActive, .enteredBackground):
|
||||||
|
phase = .moshParked
|
||||||
|
return []
|
||||||
|
|
||||||
|
// mosh 后台 → 前台:打唤醒脉冲 + 排看门狗,等 SSP 续上。
|
||||||
|
case (.moshParked, .enteredForeground):
|
||||||
|
phase = .moshResuming(attempt: 1)
|
||||||
|
return [.nudgeResume, .scheduleResumeWatchdog(ms: resumeWatchdogMS), .notify("恢复中…")]
|
||||||
|
|
||||||
|
// mosh 后台期间的杂项(底层断/重复进后台):保持 parked。
|
||||||
|
case (.moshParked, .transportClosed), (.moshParked, .enteredBackground):
|
||||||
|
return []
|
||||||
|
|
||||||
|
// 恢复中收到服务器合法包(SSP 已续):回到活跃。
|
||||||
|
case (.moshResuming, .moshHealthy):
|
||||||
|
phase = .moshActive
|
||||||
|
return [.cancelResumeWatchdog, .notify("已恢复")]
|
||||||
|
|
||||||
|
// 恢复看门狗到点:未 healthy → 再脉冲;达上限 → 兜底全量重建。
|
||||||
|
case (.moshResuming(let attempt), .resumeWatchdogFired):
|
||||||
|
if attempt < maxResumePulses {
|
||||||
|
phase = .moshResuming(attempt: attempt + 1)
|
||||||
|
return [.nudgeResume, .scheduleResumeWatchdog(ms: resumeWatchdogMS)]
|
||||||
|
}
|
||||||
|
phase = .reconnecting(attempt: 1)
|
||||||
|
return [.teardownMosh, .startConnect, .notify("mosh 恢复失败,重建会话…")]
|
||||||
|
|
||||||
|
// 恢复中又进后台:取消看门狗,回 parked。
|
||||||
|
case (.moshResuming, .enteredBackground):
|
||||||
|
phase = .moshParked
|
||||||
|
return [.cancelResumeWatchdog]
|
||||||
|
|
||||||
|
// mosh 主循环退出(会话结束):终态失败,可手动重试。
|
||||||
|
case (.moshActive, .moshExited(let rc)),
|
||||||
|
(.moshResuming, .moshExited(let rc)),
|
||||||
|
(.moshParked, .moshExited(let rc)):
|
||||||
|
phase = .failed(reason: "mosh 已结束(rc=\(rc))")
|
||||||
|
return [.cancelResumeWatchdog, .teardownMosh, .notify("mosh 会话结束")]
|
||||||
|
|
||||||
// —— 关闭 ——
|
// —— 关闭 ——
|
||||||
case (_, .closeRequested):
|
case (_, .closeRequested):
|
||||||
phase = .closed
|
phase = .closed
|
||||||
return [.cancelReconnectTimer, .teardownTransport]
|
return [.cancelReconnectTimer, .cancelResumeWatchdog, .teardownTransport, .teardownMosh]
|
||||||
|
|
||||||
// 其它组合不改变状态
|
// 其它组合不改变状态
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -125,4 +125,78 @@ struct SessionMachineTests {
|
|||||||
#expect(m2.reduce(.retryRequested) == [.startConnect])
|
#expect(m2.reduce(.retryRequested) == [.startConnect])
|
||||||
#expect(m2.phase == .connecting)
|
#expect(m2.phase == .connecting)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - M3 mosh 生命周期
|
||||||
|
|
||||||
|
private func connectedThenMosh() -> SessionMachine {
|
||||||
|
var m = SessionMachine()
|
||||||
|
_ = m.reduce(.connectRequested)
|
||||||
|
_ = m.reduce(.established)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("mosh 接管拆 SSH,之后忽略 transportClosed")
|
||||||
|
func moshTakeover() {
|
||||||
|
var m = connectedThenMosh()
|
||||||
|
let e = m.reduce(.moshEstablished)
|
||||||
|
#expect(m.phase == .moshActive)
|
||||||
|
#expect(e.contains(.teardownTransport))
|
||||||
|
// 底层 SSH 残连断开,不影响 mosh
|
||||||
|
#expect(m.reduce(.transportClosed(reason: "ssh eof")) == [])
|
||||||
|
#expect(m.phase == .moshActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("mosh 后台→前台:唤醒脉冲 + 看门狗,收到 healthy 即恢复")
|
||||||
|
func moshSuspendResumeHealthy() {
|
||||||
|
var m = connectedThenMosh()
|
||||||
|
_ = m.reduce(.moshEstablished)
|
||||||
|
#expect(m.reduce(.enteredBackground) == [])
|
||||||
|
#expect(m.phase == .moshParked)
|
||||||
|
let e = m.reduce(.enteredForeground)
|
||||||
|
#expect(m.phase == .moshResuming(attempt: 1))
|
||||||
|
#expect(e.contains(.nudgeResume))
|
||||||
|
#expect(e.contains(.scheduleResumeWatchdog(ms: 3000)))
|
||||||
|
// SSP 续上
|
||||||
|
let e2 = m.reduce(.moshHealthy)
|
||||||
|
#expect(m.phase == .moshActive)
|
||||||
|
#expect(e2.contains(.cancelResumeWatchdog))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("恢复看门狗:一次超时再脉冲,两次仍失败则兜底全量重建")
|
||||||
|
func moshResumeWatchdogFallback() {
|
||||||
|
var m = SessionMachine(resumeWatchdogMS: 3000, maxResumePulses: 2)
|
||||||
|
_ = m.reduce(.connectRequested)
|
||||||
|
_ = m.reduce(.established)
|
||||||
|
_ = m.reduce(.moshEstablished)
|
||||||
|
_ = m.reduce(.enteredBackground)
|
||||||
|
_ = m.reduce(.enteredForeground) // moshResuming(1)
|
||||||
|
// 第一次看门狗到点 → 再脉冲,进 moshResuming(2)
|
||||||
|
let e1 = m.reduce(.resumeWatchdogFired)
|
||||||
|
#expect(m.phase == .moshResuming(attempt: 2))
|
||||||
|
#expect(e1.contains(.nudgeResume))
|
||||||
|
// 第二次看门狗到点 → 兜底全量重建
|
||||||
|
let e2 = m.reduce(.resumeWatchdogFired)
|
||||||
|
#expect(m.phase == .reconnecting(attempt: 1))
|
||||||
|
#expect(e2.contains(.teardownMosh))
|
||||||
|
#expect(e2.contains(.startConnect))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("mosh 主循环退出转 failed 并拆除")
|
||||||
|
func moshExited() {
|
||||||
|
var m = connectedThenMosh()
|
||||||
|
_ = m.reduce(.moshEstablished)
|
||||||
|
let e = m.reduce(.moshExited(rc: 1))
|
||||||
|
if case .failed = m.phase {} else { Issue.record("期望 failed,实际 \(m.phase)") }
|
||||||
|
#expect(e.contains(.teardownMosh))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("mosh 恢复中进后台取消看门狗")
|
||||||
|
func moshResumingToBackground() {
|
||||||
|
var m = connectedThenMosh()
|
||||||
|
_ = m.reduce(.moshEstablished)
|
||||||
|
_ = m.reduce(.enteredBackground)
|
||||||
|
_ = m.reduce(.enteredForeground) // moshResuming(1)
|
||||||
|
#expect(m.reduce(.enteredBackground) == [.cancelResumeWatchdog])
|
||||||
|
#expect(m.phase == .moshParked)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,10 @@ extern "C"
|
|||||||
int mosh_main(FILE *f_in, FILE *f_out, struct winsize *window_size,
|
int mosh_main(FILE *f_in, FILE *f_out, struct winsize *window_size,
|
||||||
const char *ip, const char *port, const char *key, const char *predict_mode);
|
const char *ip, const char *port, const char *key, const char *predict_mode);
|
||||||
|
|
||||||
|
/* M3:最近一次收到新服务器状态的 mosh 单调时刻(ms);0=从未。前台恢复判定用。 */
|
||||||
|
#if __cplusplus
|
||||||
|
extern "C"
|
||||||
|
#endif
|
||||||
|
unsigned long long mosh_last_heard_ms(void);
|
||||||
|
|
||||||
#endif /* CMOSH_MOSHIOSBRIDGE_H */
|
#endif /* CMOSH_MOSHIOSBRIDGE_H */
|
||||||
|
|||||||
@@ -86,6 +86,16 @@ public final class MoshSession: @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// M3:唤醒 mosh 主循环(前台恢复)——发 SIGCONT,触发 select 立即 tick、发包、全屏重绘。
|
||||||
|
public func nudge() {
|
||||||
|
lock.lock(); let tid = moshThread; lock.unlock()
|
||||||
|
if let tid { pthread_kill(tid, SIGCONT) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// M3:mosh 最近收到新(已认证)服务器状态的时刻(mosh 单调钟 ms);0=从未。
|
||||||
|
/// 前台恢复时取基线、轮询其增长即判定 SSP 已续(healthy)。
|
||||||
|
public func lastHeardMs() -> UInt64 { UInt64(mosh_last_heard_ms()) }
|
||||||
|
|
||||||
/// 更新终端尺寸并通知 mosh(SIGWINCH → process_resize)。
|
/// 更新终端尺寸并通知 mosh(SIGWINCH → process_resize)。
|
||||||
public func resize(cols: Int, rows: Int) {
|
public func resize(cols: Int, rows: Int) {
|
||||||
winPtr.pointee.ws_col = UInt16(cols)
|
winPtr.pointee.ws_col = UInt16(cols)
|
||||||
|
|||||||
17
vendor/mosh/src/frontend/iosclient.cc
vendored
17
vendor/mosh/src/frontend/iosclient.cc
vendored
@@ -62,6 +62,16 @@
|
|||||||
|
|
||||||
#include "networktransport.cc"
|
#include "networktransport.cc"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* M3 探针:记录最近一次收到服务器数据报的时刻(mosh 单调 timestamp() ms)。
|
||||||
|
* process_network_input 仅在网络 socket 可读时调用,loopback 上只有服务器经 relay 转来的包,
|
||||||
|
* 故"进入该函数"≈"听到服务器"(含纯 ack,空闲屏幕也能检出恢复)。
|
||||||
|
* Swift 侧前台恢复时记基线值、轮询其增长 → 判定 SSP 已续("healthy")。跨线程用 atomic。 */
|
||||||
|
std::atomic<uint64_t> g_mosh_last_heard_ms{ 0 };
|
||||||
|
extern "C" unsigned long long mosh_last_heard_ms( void ) { return (unsigned long long)g_mosh_last_heard_ms.load(); }
|
||||||
|
|
||||||
void iOSClient::resume( void )
|
void iOSClient::resume( void )
|
||||||
{
|
{
|
||||||
/* Restore termios state */
|
/* Restore termios state */
|
||||||
@@ -73,8 +83,8 @@ void iOSClient::resume( void )
|
|||||||
// /* Put terminal in application-cursor-key mode */
|
// /* Put terminal in application-cursor-key mode */
|
||||||
// swrite( out_fd, display.open().c_str() );
|
// swrite( out_fd, display.open().c_str() );
|
||||||
|
|
||||||
// /* Flag that outer terminal state is unknown */
|
/* M3:SIGCONT(前台唤醒)→ 标记外层终端状态未知,下一帧全屏重绘。 */
|
||||||
// repaint_requested = true;
|
repaint_requested = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void iOSClient::init( void )
|
void iOSClient::init( void )
|
||||||
@@ -285,6 +295,9 @@ void iOSClient::process_network_input( void )
|
|||||||
{
|
{
|
||||||
network->recv();
|
network->recv();
|
||||||
|
|
||||||
|
/* M3 探针:收到服务器数据报 → 记录时刻供前台恢复判定(见上方说明)。 */
|
||||||
|
g_mosh_last_heard_ms.store( timestamp() );
|
||||||
|
|
||||||
/* Now give hints to the overlays */
|
/* Now give hints to the overlays */
|
||||||
overlays.get_notification_engine().server_heard( network->get_latest_remote_state().timestamp );
|
overlays.get_notification_engine().server_heard( network->get_latest_remote_state().timestamp );
|
||||||
overlays.get_notification_engine().server_acked( network->get_sent_state_acked_timestamp() );
|
overlays.get_notification_engine().server_acked( network->get_sent_state_acked_timestamp() );
|
||||||
|
|||||||
6
vendor/mosh/src/frontend/moshiosbridge.h
vendored
6
vendor/mosh/src/frontend/moshiosbridge.h
vendored
@@ -10,4 +10,10 @@ extern "C"
|
|||||||
int mosh_main(FILE *f_in, FILE *f_out, struct winsize *window_size,
|
int mosh_main(FILE *f_in, FILE *f_out, struct winsize *window_size,
|
||||||
const char *ip, const char *port, const char *key, const char *predict_mode);
|
const char *ip, const char *port, const char *key, const char *predict_mode);
|
||||||
|
|
||||||
|
/* M3:最近一次收到新服务器状态的 mosh 单调时刻(ms);0=从未。前台恢复判定用。 */
|
||||||
|
#if __cplusplus
|
||||||
|
extern "C"
|
||||||
|
#endif
|
||||||
|
unsigned long long mosh_last_heard_ms(void);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
144
vendor/tsnet-bridge/tsnetbridge/bridge.go
vendored
144
vendor/tsnet-bridge/tsnetbridge/bridge.go
vendored
@@ -10,6 +10,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -109,9 +110,12 @@ func (n *Node) DialTCPFD(hostOrIP string, port int, timeoutMs int) (int64, error
|
|||||||
// tailnet 内 host:moshPort,回包再转回"最近一次上行的 client 源地址"(吸收 mosh 的 roaming/端口跳变)。
|
// tailnet 内 host:moshPort,回包再转回"最近一次上行的 client 源地址"(吸收 mosh 的 roaming/端口跳变)。
|
||||||
// 与 DialTCPFD 的差异:mosh 内部自建 UDP socket 并 connect(),无法注入外部 fd,故用 loopback relay。
|
// 与 DialTCPFD 的差异:mosh 内部自建 UDP socket 并 connect(),无法注入外部 fd,故用 loopback relay。
|
||||||
type MoshRelay struct {
|
type MoshRelay struct {
|
||||||
local *net.UDPConn // 真实内核 loopback UDP socket(mosh-client 的对端)
|
mu sync.Mutex
|
||||||
tsConn net.Conn // tsnet netstack UDP 虚拟连接(到 mosh-server)
|
local *net.UDPConn // 当前 loopback socket;Rebind 在同端口替换(关旧 socket 令旧上行泵退出)
|
||||||
lastCli atomic.Pointer[net.UDPAddr]
|
localPort int // 固定本地端口(mosh 始终发往此端口,Rebind 保持不变)
|
||||||
|
tsConn net.Conn // tsnet netstack UDP 虚拟连接(纯内存,进程冻结无损,无需重建)
|
||||||
|
lastCli atomic.Pointer[net.UDPAddr]
|
||||||
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartMoshRelay 建立中继并启动双向泵,返回句柄。Swift 用 LocalPort() 作为 mosh_main 的 port
|
// StartMoshRelay 建立中继并启动双向泵,返回句柄。Swift 用 LocalPort() 作为 mosh_main 的 port
|
||||||
@@ -131,56 +135,108 @@ func (n *Node) StartMoshRelay(host string, moshPort int, timeoutMs int) (*MoshRe
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &MoshRelay{local: local, tsConn: tsConn}
|
r := &MoshRelay{
|
||||||
|
local: local,
|
||||||
// 上行:client(loopback) → tsnet(server)。记录最近 client 源地址供下行回送。
|
localPort: local.LocalAddr().(*net.UDPAddr).Port,
|
||||||
go func() {
|
tsConn: tsConn,
|
||||||
buf := make([]byte, 65536)
|
}
|
||||||
for {
|
go r.pumpUp(local) // 上行泵(随具体 socket 生成/退出,Rebind 会换新泵)
|
||||||
nr, cliAddr, err := local.ReadFromUDP(buf)
|
go r.pumpDown() // 下行泵(绑定稳定 tsConn,跨 Rebind 存活)
|
||||||
if nr > 0 {
|
|
||||||
r.lastCli.Store(cliAddr)
|
|
||||||
if _, werr := tsConn.Write(buf[:nr]); werr != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// 下行:tsnet(server) → client(loopback 最近源地址)。逐报文保边界。
|
|
||||||
go func() {
|
|
||||||
buf := make([]byte, 65536)
|
|
||||||
for {
|
|
||||||
nr, err := tsConn.Read(buf)
|
|
||||||
if nr > 0 {
|
|
||||||
if cli := r.lastCli.Load(); cli != nil {
|
|
||||||
if _, werr := local.WriteToUDP(buf[:nr], cli); werr != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalPort 返回 loopback relay 监听的端口(mosh_main 的 port 参数)。
|
// pumpUp:client(loopback) → tsnet(server)。绑定某个具体 socket;该 socket 关闭即退出
|
||||||
func (r *MoshRelay) LocalPort() int {
|
// (Rebind/Close 触发)。记录最近 client 源地址供下行回送(吸收 mosh 的 hop_port 换端口)。
|
||||||
return r.local.LocalAddr().(*net.UDPAddr).Port
|
func (r *MoshRelay) pumpUp(local *net.UDPConn) {
|
||||||
|
buf := make([]byte, 65536)
|
||||||
|
for {
|
||||||
|
nr, cli, err := local.ReadFromUDP(buf)
|
||||||
|
if nr > 0 {
|
||||||
|
r.lastCli.Store(cli)
|
||||||
|
if _, werr := r.tsConn.Write(buf[:nr]); werr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close 关闭中继(两条泵 goroutine 随即因 conn 关闭而退出)。
|
// pumpDown:tsnet(server) → client(最近 loopback 源地址)。绑定稳定 tsConn,跨 Rebind 存活;
|
||||||
|
// 每次写入"当前"socket(Rebind 后自动跟上)。逐报文保边界。
|
||||||
|
func (r *MoshRelay) pumpDown() {
|
||||||
|
buf := make([]byte, 65536)
|
||||||
|
for {
|
||||||
|
nr, err := r.tsConn.Read(buf)
|
||||||
|
if nr > 0 {
|
||||||
|
if cli := r.lastCli.Load(); cli != nil {
|
||||||
|
r.mu.Lock()
|
||||||
|
l := r.local
|
||||||
|
r.mu.Unlock()
|
||||||
|
if l != nil {
|
||||||
|
l.WriteToUDP(buf[:nr], cli)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalPort 返回固定的 loopback relay 端口(mosh_main 的 port 参数,跨 Rebind 不变)。
|
||||||
|
func (r *MoshRelay) LocalPort() int {
|
||||||
|
return r.localPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebind 在同端口重开 loopback socket(iOS 前台恢复用:挂起后旧 socket 可能被系统回收为 defunct)。
|
||||||
|
// 关旧 socket → 旧上行泵退出 → 新泵接管;下行泵绑定稳定 tsConn 无需重启。端口被占等返回 error(由上层升级为全量重建)。
|
||||||
|
func (r *MoshRelay) Rebind() error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if r.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if r.local != nil {
|
||||||
|
r.local.Close() // 释放端口 + 令旧 pumpUp 退出(UDP 无 TIME_WAIT,同端口可立即重绑)
|
||||||
|
}
|
||||||
|
nl, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: r.localPort})
|
||||||
|
if err != nil {
|
||||||
|
r.local = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.local = nl
|
||||||
|
go r.pumpUp(nl)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭中继(两条泵随 conn 关闭而退出)。
|
||||||
func (r *MoshRelay) Close() error {
|
func (r *MoshRelay) Close() error {
|
||||||
r.local.Close()
|
r.mu.Lock()
|
||||||
|
r.closed = true
|
||||||
|
l := r.local
|
||||||
|
r.local = nil
|
||||||
|
r.mu.Unlock()
|
||||||
|
if l != nil {
|
||||||
|
l.Close()
|
||||||
|
}
|
||||||
return r.tsConn.Close()
|
return r.tsConn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WakeUp 在 iOS 前台恢复时主动唤醒 tsnet 网络层:注入链路变化事件 + Rebind/重新 STUN,
|
||||||
|
// 把"等首次写失败才发现 DERP/路径已死"变成"立刻重建",配合 mosh SSP 实现秒级恢复。
|
||||||
|
func (n *Node) WakeUp() {
|
||||||
|
sys := n.srv.Sys()
|
||||||
|
if mon, ok := sys.NetMon.GetOK(); ok {
|
||||||
|
mon.InjectEvent()
|
||||||
|
}
|
||||||
|
if ms, ok := sys.MagicSock.GetOK(); ok {
|
||||||
|
ms.Rebind()
|
||||||
|
ms.ReSTUN("foreground")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PeersJSON 返回 tailnet 内其它节点列表(name/ip/online/os)的 JSON,用于选择连接目标。
|
// PeersJSON 返回 tailnet 内其它节点列表(name/ip/online/os)的 JSON,用于选择连接目标。
|
||||||
func (n *Node) PeersJSON() string {
|
func (n *Node) PeersJSON() string {
|
||||||
lc, err := n.srv.LocalClient()
|
lc, err := n.srv.LocalClient()
|
||||||
|
|||||||
Reference in New Issue
Block a user