返回 DeepSeek-Reasonix
remote_tab_publication.go
根目录 / desktop / remote_tab_publication.go
1 package main
2
3 import (
4 "context"
5 "fmt"
6 "log"
7 "net/http"
8 )
9
10 // Route events, terminal state, explicit close and generation replacement
11 // share one publication order. Never wait for this fence while holding
12 // remoteTabMu; callers recheck the captured tab after acquiring both locks.
13 func (a *App) lockRemoteTabPublication(tabID string) *remoteTab {
14 a.remoteTabMu.Lock()
15 tab := a.remoteTabs[tabID]
16 a.remoteTabMu.Unlock()
17 if tab != nil {
18 tab.routeEventMu.Lock()
19 }
20 return tab
21 }
22
23 func (a *App) retireRemoteTabGeneration(tabID string, gen uint64) {
24 tab := a.lockRemoteTabPublication(tabID)
25 if tab == nil {
26 return
27 }
28 defer tab.routeEventMu.Unlock()
29 a.remoteTabMu.Lock()
30 if a.remoteTabs[tabID] != tab || tab.gen != gen {
31 a.remoteTabMu.Unlock()
32 return
33 }
34 cancel := tab.cancel
35 tab.gen++
36 tab.attachedGen = 0
37 tab.cancel = nil
38 tab.client = nil
39 tab.base = ""
40 tab.token = ""
41 closeRemoteTabProvisionalRouteLocked(tab)
42 a.remoteTabMu.Unlock()
43 if cancel != nil {
44 cancel()
45 }
46 }
47
48 // reconnectRemoteTabGeneration retires a dead pump and atomically parks its
49 // tab in reconnecting. The bool reports whether this pump should start the
50 // retry loop; a pump opened by an existing retry loop leaves retries to its
51 // caller so two loops cannot race each other.
52 func (a *App) reconnectRemoteTabGeneration(tabID string, gen uint64) bool {
53 tab := a.lockRemoteTabPublication(tabID)
54 if tab == nil {
55 return false
56 }
57 defer tab.routeEventMu.Unlock()
58 a.remoteTabMu.Lock()
59 if a.remoteTabs[tabID] != tab || tab.gen != gen {
60 a.remoteTabMu.Unlock()
61 return false
62 }
63 startRetry := tab.state != "reconnecting"
64 cancel := tab.cancel
65 tab.gen++
66 tab.attachedGen = 0
67 tab.cancel = nil
68 tab.client = nil
69 tab.base = ""
70 tab.token = ""
71 tab.state = "reconnecting"
72 tab.err = ""
73 closeRemoteTabProvisionalRouteLocked(tab)
74 a.remoteTabMu.Unlock()
75 if cancel != nil {
76 cancel()
77 }
78 a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: "reconnecting"})
79 return startRetry
80 }
81
82 // startRemoteTabReattach retires a dead pump generation and hands the tab to
83 // the reattach retry loop. It is the single recovery path for every stream
84 // failure — mid-stream EOF, a refused replacement connection, or a non-200
85 // /events response — so a healing tunnel always gets retried instead of
86 // parking a healthy tab in a terminal state. Callers hold no tab locks.
87 func (a *App) startRemoteTabReattach(tabID string, gen uint64) {
88 if startRetry := a.reconnectRemoteTabGeneration(tabID, gen); startRetry {
89 log.Printf("[remote] remoteTabPump: DIED tab=%s gen=%d — reattaching", tabID, gen)
90 a.goRemoteTabSafe("remoteTabReattach", func() { a.reattachRemoteTab(tabID) })
91 }
92 }
93
94 func (a *App) emitRemoteTabStateForGeneration(tabID string, gen uint64, state, errMsg string) bool {
95 tab := a.lockRemoteTabPublication(tabID)
96 if tab == nil {
97 return false
98 }
99 defer tab.routeEventMu.Unlock()
100 a.remoteTabMu.Lock()
101 if a.remoteTabs[tabID] != tab || tab.gen != gen {
102 a.remoteTabMu.Unlock()
103 return false
104 }
105 tab.state = state
106 tab.err = errMsg
107 a.remoteTabMu.Unlock()
108 a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: state, Error: errMsg})
109 return true
110 }
111
112 func (a *App) transitionRemoteTabState(tabID string, gen uint64, from, state, errMsg string) bool {
113 tab := a.lockRemoteTabPublication(tabID)
114 if tab == nil {
115 return false
116 }
117 defer tab.routeEventMu.Unlock()
118 return a.transitionRemoteTabStateLocked(tab, gen, from, state, errMsg)
119 }
120
121 func (a *App) transitionRemoteTabStateLocked(tab *remoteTab, gen uint64, from, state, errMsg string) bool {
122 tabID := tab.id
123 a.remoteTabMu.Lock()
124 if a.remoteTabs[tabID] != tab || tab.gen != gen || tab.state != from {
125 a.remoteTabMu.Unlock()
126 return false
127 }
128 tab.state = state
129 tab.err = errMsg
130 a.remoteTabMu.Unlock()
131 a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: state, Error: errMsg})
132 return true
133 }
134
135 func (a *App) emitRemoteTabState(tabID, state, errMsg string) {
136 tab := a.lockRemoteTabPublication(tabID)
137 if tab == nil {
138 return
139 }
140 defer tab.routeEventMu.Unlock()
141 a.emitRemoteTabStateLocked(tab, state, errMsg)
142 }
143
144 func (a *App) emitRemoteTabStateLocked(tab *remoteTab, state, errMsg string) {
145 tabID := tab.id
146 a.remoteTabMu.Lock()
147 if a.remoteTabs[tabID] != tab {
148 a.remoteTabMu.Unlock()
149 return
150 }
151 tab.state = state
152 tab.err = errMsg
153 a.remoteTabMu.Unlock()
154 a.emitRemoteEvent(fmt.Sprintf("remote-tab:%s:state", tabID), RemoteTabStateView{State: state, Error: errMsg})
155 }
156
157 func (a *App) installRemoteTabAttachPump(ctx context.Context, tabID string, tab *remoteTab, client *http.Client, base, token, targetPath string, installRoute bool) (context.Context, uint64, uint64, error) {
158 tab.routeEventMu.Lock()
159 a.remoteTabMu.Lock()
160 if a.remoteTabs[tabID] != tab {
161 a.remoteTabMu.Unlock()
162 tab.routeEventMu.Unlock()
163 return nil, 0, 0, fmt.Errorf("remote tab %q closed during bootstrap", tabID)
164 }
165 // Retire any pump installed by a concurrent reconnect so exactly one
166 // generation owns the event stream.
167 tab.gen++
168 if tab.cancel != nil {
169 tab.cancel()
170 }
171 tab.client = client
172 tab.base = base
173 tab.token = token
174 if installRoute {
175 commitRemoteTabAttachRoute(tab, targetPath, false)
176 }
177 attachPathRevision := tab.routing.pathRevision
178 gen := tab.gen
179 pumpCtx, cancelPump := context.WithCancel(ctx)
180 tab.cancel = cancelPump
181 a.remoteTabMu.Unlock()
182 // Only an attach that committed a session route announces itself here.
183 if installRoute && targetPath != "" {
184 a.publishRemoteTabAttachIdentityLocked(tabID, tab, gen)
185 }
186 tab.routeEventMu.Unlock()
187
188 return pumpCtx, gen, attachPathRevision, nil
189 }
190
191 // publishRemoteTabAttachIdentityLocked announces the tab once its route,
192 // client, and capabilities are live but before the foreground rotation
193 // starts, so history-first hydration can read the persisted window during
194 // /resume. Only an attach that committed a session route owns such a window;
195 // a routeless attach (a fresh session) must stay silent, because the first
196 // remote-tab:updated of a bootstrap is what the sidebar re-pulls its brand-new
197 // project group on, and a connecting meta there carries no session to list.
198 // The caller holds the tab's publication fence, so this meta cannot overtake
199 // or be overtaken by another publication for the same tab.
200 func (a *App) publishRemoteTabAttachIdentityLocked(tabID string, tab *remoteTab, gen uint64) {
201 a.remoteTabMu.Lock()
202 current := a.remoteTabs[tabID]
203 if current != tab || current.gen != gen {
204 a.remoteTabMu.Unlock()
205 return
206 }
207 meta := remoteTabMetaLocked(current)
208 a.remoteTabMu.Unlock()
209 a.emitRemoteEvent("remote-tab:updated", meta)
210 }
211
211 lines GO