package relay import ( "context" "net" "sync" "time" "github.com/hashicorp/yamux" "visiona-backend/internal/session" ) // LocalHandle 是 remote-proxy 端的 session.Handle 實作, // 直接包住一個 yamux.Session(真實持有 tunnel 連線的地方)。 // // 為修 B2 Review M1(Heartbeat vs CleanupExpired race), // LocalHandle 以 mu 保護 summary 的 LastHeartbeat 讀寫; // Summary() 回傳 snapshot(副本)、RecordHeartbeat 在 lock 下寫入。 type LocalHandle struct { yamuxSession *yamux.Session mu sync.Mutex summary session.Summary } // NewLocalHandle 建立一個 LocalHandle。 // // token / remoteAddr 由 relay server 在 handleTunnelConnect 時傳入。 // ConnectedAt 與 LastHeartbeat 初始為 time.Now().UTC()。 func NewLocalHandle(yamuxSession *yamux.Session, token, remoteAddr string) *LocalHandle { now := time.Now().UTC() return &LocalHandle{ yamuxSession: yamuxSession, summary: session.Summary{ Token: token, ConnectedAt: now, LastHeartbeat: now, RemoteAddr: remoteAddr, }, } } // OpenStream 在 yamux session 上開一條新 stream。 // 若 session 已關閉回 ErrSessionClosed。 func (h *LocalHandle) OpenStream(ctx context.Context) (net.Conn, error) { if h.yamuxSession.IsClosed() { return nil, session.ErrSessionClosed } // yamux.Session.Open() 不接受 context;若 ctx 已取消應盡量早退。 if err := ctx.Err(); err != nil { return nil, err } stream, err := h.yamuxSession.Open() if err != nil { return nil, err } return stream, nil } // Close 關閉底層 yamux session(會同時關閉底下的 WebSocket)。 func (h *LocalHandle) Close() error { return h.yamuxSession.Close() } // IsClosed 回報 yamux session 是否已關閉。 func (h *LocalHandle) IsClosed() bool { return h.yamuxSession.IsClosed() } // Summary 回傳 session 的資訊快照。 // // 回傳的是 summary 的複本,防止 caller 觀察到中間態, // 也避免 Store.List 與 RecordHeartbeat 對同一 pointer 的並發寫入。 func (h *LocalHandle) Summary() *session.Summary { h.mu.Lock() defer h.mu.Unlock() cp := h.summary return &cp } // RecordHeartbeat 更新 LastHeartbeat;由 Store.Heartbeat 呼叫。 func (h *LocalHandle) RecordHeartbeat(t time.Time) { h.mu.Lock() defer h.mu.Unlock() h.summary.LastHeartbeat = t } // 編譯時檢查:LocalHandle 必須實作 session.Handle。 var _ session.Handle = (*LocalHandle)(nil)