@@ -97,39 +97,71 @@ func (s *Server) startPushPipeListener() {
9797 s .logger .Debug ("CMD_SERVICE_READY sent to new push client" , "clientID" , clientID )
9898 }
9999
100- // 异步读取 token 握手(8 字节),不阻塞主循环。
101- // 内层 goroutine 做实际 ReadFile(可能永久阻塞于旧版客户端),
102- // 外层 goroutine 持有 500ms 超时并在超时后退出(内层 goroutine 在 handle 关闭时自然退出)。
100+ // 单 goroutine 完成两件事:
101+ // 1) 阻塞读 8 字节 token 握手
102+ // 2) 握手后继续阻塞 ReadFile,专门用于检测对端关闭(死链监听)
103+ // 协议规定客户端发完 token 后不再发任何消息,所以 ReadFile 在握手后会
104+ // 永久 park 在内核等待,不消耗 CPU;客户端 close pipe 时 OS 立即唤醒
105+ // 并返回错误,我们走 defer 路径清理 handle —— 不再等到下一次广播写失败
106+ // 才"惰性发现"死链。
107+ //
108+ // 同 token 重连:旧 handle 必然失效,必须按 token 主动清理。
109+ // 不能按 PID 清理:同 PID 可能有多个合法实例(如 explorer.exe 的多个
110+ // CLangBar 宿主),它们各自持有不同 token,按 PID 误清会破坏正常推送。
103111 go func (h windows.Handle , pid uint32 , cid int ) {
104- tokenCh := make ( chan uint64 , 1 )
105- go func () {
106- var buf [ 8 ] byte
107- var n uint32
108- if err := windows . ReadFile ( h , buf [:], & n , nil ); err == nil && n == 8 {
109- tokenCh <- binary . LittleEndian . Uint64 ( buf [:] )
112+ defer func () {
113+ s . pushMu . Lock ()
114+ removed := s . cleanupPushHandle ( h )
115+ s . pushMu . Unlock ()
116+ if removed {
117+ windows . CloseHandle ( h )
110118 }
111119 }()
112- select {
113- case token := <- tokenCh :
114- if token == 0 {
115- return
120+
121+ // Phase 1: token 握手
122+ var buf [8 ]byte
123+ var n uint32
124+ if err := windows .ReadFile (h , buf [:], & n , nil ); err != nil || n == 0 {
125+ s .logger .Info ("Push pipe disconnected before token handshake" ,
126+ "clientID" , cid , "processID" , pid , "error" , err )
127+ return
128+ }
129+
130+ var registeredToken uint64
131+ if n >= 8 {
132+ token := binary .LittleEndian .Uint64 (buf [:])
133+ if token != 0 {
134+ s .pushMu .Lock ()
135+ if oldH , ok := s .tokenToPushHandle [token ]; ok && oldH != h {
136+ if s .cleanupPushHandle (oldH ) {
137+ windows .CloseHandle (oldH )
138+ s .logger .Info ("Push pipe: stale handle replaced by token reconnect" ,
139+ "clientID" , cid , "processID" , pid , "token" , token )
140+ }
141+ }
142+ if _ , exists := s .pushClients [h ]; exists {
143+ s .tokenToPushHandle [token ] = h
144+ s .pushHandleToToken [h ] = token
145+ registeredToken = token
146+ }
147+ s .pushMu .Unlock ()
148+ s .logger .Debug ("Push pipe: token registered" ,
149+ "clientID" , cid , "processID" , pid , "token" , token )
116150 }
117- s .pushMu .Lock ()
118- if _ , exists := s .pushClients [h ]; exists {
119- s .tokenToPushHandle [token ] = h
120- s .pushHandleToToken [h ] = token
151+ }
152+
153+ // Phase 2: 死链监听 —— ReadFile 在客户端 close pipe 时立刻返回错误
154+ var probe [16 ]byte
155+ for {
156+ err := windows .ReadFile (h , probe [:], & n , nil )
157+ if err != nil || n == 0 {
158+ s .logger .Info ("Push pipe client disconnected" ,
159+ "clientID" , cid , "processID" , pid , "token" , registeredToken , "error" , err )
160+ return
121161 }
122- s .pushMu .Unlock ()
123- s .logger .Debug ("Push pipe: token registered" , "clientID" , cid , "processID" , pid , "token" , token )
124- case <- time .After (500 * time .Millisecond ):
125- s .logger .Debug ("Push pipe: token handshake timed out (old client?)" , "clientID" , cid , "processID" , pid )
162+ // 协议不允许 token 之后再有数据,但万一发生只丢弃并继续监听。
126163 }
127164 }(handle , pushProcessID , clientID )
128-
129- // Note: We don't actively monitor disconnection here.
130- // Client disconnection is detected when write fails in PushCommitTextToActiveClient
131- // or PushStateToAllClients. This avoids false positives from GetNamedPipeHandleState
132- // which can return "Access is denied" on valid pipes.
133165 }
134166}
135167
0 commit comments