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:
19
vendor/mosh/src/frontend/iosclient.cc
vendored
19
vendor/mosh/src/frontend/iosclient.cc
vendored
@@ -62,6 +62,16 @@
|
||||
|
||||
#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 )
|
||||
{
|
||||
/* Restore termios state */
|
||||
@@ -73,8 +83,8 @@ void iOSClient::resume( void )
|
||||
// /* Put terminal in application-cursor-key mode */
|
||||
// swrite( out_fd, display.open().c_str() );
|
||||
|
||||
// /* Flag that outer terminal state is unknown */
|
||||
// repaint_requested = true;
|
||||
/* M3:SIGCONT(前台唤醒)→ 标记外层终端状态未知,下一帧全屏重绘。 */
|
||||
repaint_requested = true;
|
||||
}
|
||||
|
||||
void iOSClient::init( void )
|
||||
@@ -284,7 +294,10 @@ void iOSClient::output_new_frame( void )
|
||||
void iOSClient::process_network_input( void )
|
||||
{
|
||||
network->recv();
|
||||
|
||||
|
||||
/* M3 探针:收到服务器数据报 → 记录时刻供前台恢复判定(见上方说明)。 */
|
||||
g_mosh_last_heard_ms.store( timestamp() );
|
||||
|
||||
/* Now give hints to the overlays */
|
||||
overlays.get_notification_engine().server_heard( network->get_latest_remote_state().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,
|
||||
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
|
||||
|
||||
144
vendor/tsnet-bridge/tsnetbridge/bridge.go
vendored
144
vendor/tsnet-bridge/tsnetbridge/bridge.go
vendored
@@ -10,6 +10,7 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -109,9 +110,12 @@ func (n *Node) DialTCPFD(hostOrIP string, port int, timeoutMs int) (int64, error
|
||||
// tailnet 内 host:moshPort,回包再转回"最近一次上行的 client 源地址"(吸收 mosh 的 roaming/端口跳变)。
|
||||
// 与 DialTCPFD 的差异:mosh 内部自建 UDP socket 并 connect(),无法注入外部 fd,故用 loopback relay。
|
||||
type MoshRelay struct {
|
||||
local *net.UDPConn // 真实内核 loopback UDP socket(mosh-client 的对端)
|
||||
tsConn net.Conn // tsnet netstack UDP 虚拟连接(到 mosh-server)
|
||||
lastCli atomic.Pointer[net.UDPAddr]
|
||||
mu sync.Mutex
|
||||
local *net.UDPConn // 当前 loopback socket;Rebind 在同端口替换(关旧 socket 令旧上行泵退出)
|
||||
localPort int // 固定本地端口(mosh 始终发往此端口,Rebind 保持不变)
|
||||
tsConn net.Conn // tsnet netstack UDP 虚拟连接(纯内存,进程冻结无损,无需重建)
|
||||
lastCli atomic.Pointer[net.UDPAddr]
|
||||
closed bool
|
||||
}
|
||||
|
||||
// StartMoshRelay 建立中继并启动双向泵,返回句柄。Swift 用 LocalPort() 作为 mosh_main 的 port
|
||||
@@ -131,56 +135,108 @@ func (n *Node) StartMoshRelay(host string, moshPort int, timeoutMs int) (*MoshRe
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := &MoshRelay{local: local, tsConn: tsConn}
|
||||
|
||||
// 上行:client(loopback) → tsnet(server)。记录最近 client 源地址供下行回送。
|
||||
go func() {
|
||||
buf := make([]byte, 65536)
|
||||
for {
|
||||
nr, cliAddr, err := local.ReadFromUDP(buf)
|
||||
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
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
r := &MoshRelay{
|
||||
local: local,
|
||||
localPort: local.LocalAddr().(*net.UDPAddr).Port,
|
||||
tsConn: tsConn,
|
||||
}
|
||||
go r.pumpUp(local) // 上行泵(随具体 socket 生成/退出,Rebind 会换新泵)
|
||||
go r.pumpDown() // 下行泵(绑定稳定 tsConn,跨 Rebind 存活)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// LocalPort 返回 loopback relay 监听的端口(mosh_main 的 port 参数)。
|
||||
func (r *MoshRelay) LocalPort() int {
|
||||
return r.local.LocalAddr().(*net.UDPAddr).Port
|
||||
// pumpUp:client(loopback) → tsnet(server)。绑定某个具体 socket;该 socket 关闭即退出
|
||||
// (Rebind/Close 触发)。记录最近 client 源地址供下行回送(吸收 mosh 的 hop_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 {
|
||||
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()
|
||||
}
|
||||
|
||||
// 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,用于选择连接目标。
|
||||
func (n *Node) PeersJSON() string {
|
||||
lc, err := n.srv.LocalClient()
|
||||
|
||||
Reference in New Issue
Block a user