| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | ) |
| 6 | |
| 7 | // remoteTabPendingOpenSelection retains the newest session-selection intent |
| 8 | // while an existing shell is still attaching. The local identity must not |
| 9 | // advance until Serve can accept the matching /resume or /new request. |
| 10 | type remoteTabPendingOpenSelection struct { |
| 11 | name string |
| 12 | sessionID string |
| 13 | path string |
| 14 | title string |
| 15 | newSession bool |
| 16 | reuseBlank bool |
| 17 | revision uint64 |
| 18 | deferred bool |
| 19 | identityCommitted bool |
| 20 | previous *remoteTabOpenSelection |
| 21 | } |
| 22 | |
| 23 | func newRemoteTabPendingOpenSelection(opts RemoteTabOpenOptions) *remoteTabPendingOpenSelection { |
| 24 | return &remoteTabPendingOpenSelection{ |
| 25 | name: strings.TrimSpace(opts.SessionName), sessionID: strings.TrimSpace(opts.SessionID), path: strings.TrimSpace(opts.SessionPath), |
| 26 | title: strings.TrimSpace(opts.SessionTitle), newSession: opts.NewSession, |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | // consumeQueuedRemoteTabOpenSelectionLocked retires the ready-tab handoff once |
| 31 | // its resume goroutine passes the revision fence. selectionMu keeps newer |
| 32 | // registrations out until this request and any rollback finish. Caller holds |
| 33 | // remoteTabMu. |
| 34 | func consumeQueuedRemoteTabOpenSelectionLocked(tab *remoteTab, revision uint64) { |
| 35 | if revision == 0 || tab.pendingSelection == nil || tab.pendingSelection.deferred || tab.pendingSelection.revision != revision { |
| 36 | return |
| 37 | } |
| 38 | tab.pendingSelection = nil |
| 39 | } |
| 40 | |
| 41 | func requeueRemoteTabOpenSelectionLocked(tab *remoteTab, selection *remoteTabPendingOpenSelection) { |
| 42 | pending := tab.pendingSelection |
| 43 | if pending != nil && (pending.revision == 0 || pending.revision > selection.revision) { |
| 44 | return |
| 45 | } |
| 46 | tab.pendingSelection = selection |
| 47 | } |
| 48 | |
| 49 | // applyPendingRemoteTabOpenSelection commits only the newest deferred intent |
| 50 | // once the shell reaches ready, then uses the normal transition path. |
| 51 | func (a *App) applyPendingRemoteTabOpenSelection(tabID string) { |
| 52 | a.tabSelectionMu.Lock() |
| 53 | defer a.tabSelectionMu.Unlock() |
| 54 | a.remoteTabMu.Lock() |
| 55 | tab := a.remoteTabs[tabID] |
| 56 | a.remoteTabMu.Unlock() |
| 57 | if tab == nil { |
| 58 | return |
| 59 | } |
| 60 | tab.routeEventMu.Lock() |
| 61 | a.remoteTabMu.Lock() |
| 62 | current := a.remoteTabs[tabID] |
| 63 | if current != tab || current.state != "ready" || current.pendingSelection == nil || !current.pendingSelection.deferred { |
| 64 | a.remoteTabMu.Unlock() |
| 65 | tab.routeEventMu.Unlock() |
| 66 | return |
| 67 | } |
| 68 | selection := current.pendingSelection |
| 69 | current.pendingSelection = nil |
| 70 | if selection.revision == 0 { |
| 71 | current.selectionRevision++ |
| 72 | selection.revision = current.selectionRevision |
| 73 | } else if current.selectionRevision != selection.revision { |
| 74 | a.remoteTabMu.Unlock() |
| 75 | tab.routeEventMu.Unlock() |
| 76 | return |
| 77 | } |
| 78 | if selection.newSession { |
| 79 | // Recheck the authoritative state at application time. The tab may have |
| 80 | // earned content while the shell was reconnecting, so reuseBlank from |
| 81 | // registration is only a snapshot and cannot discard this newer intent. |
| 82 | reuseCurrentBlank := current.session.reset |
| 83 | a.remoteTabMu.Unlock() |
| 84 | tab.routeEventMu.Unlock() |
| 85 | if reuseCurrentBlank { |
| 86 | return |
| 87 | } |
| 88 | if err := a.resetRemoteTabSession(tabID); err != nil { |
| 89 | a.emitRemoteTabState(tabID, "ready", err.Error()) |
| 90 | } |
| 91 | return |
| 92 | } |
| 93 | previous := selection.previous |
| 94 | identityChanged := !selection.identityCommitted |
| 95 | if previous == nil { |
| 96 | previous = &remoteTabOpenSelection{ |
| 97 | session: current.session, topicTitle: current.topicTitle, |
| 98 | currentPath: current.routing.currentPath, |
| 99 | pending: cloneRemotePendingEvents(current.pendingEvents), runtime: current.runtime, |
| 100 | revision: selection.revision, |
| 101 | } |
| 102 | } |
| 103 | if identityChanged { |
| 104 | current.session.newSession = false |
| 105 | current.session.name = selection.name |
| 106 | current.session.sessionID = selection.sessionID |
| 107 | current.session.path = selection.path |
| 108 | if selection.title != "" { |
| 109 | current.topicTitle = selection.title |
| 110 | } |
| 111 | if route := remoteSessionIdentityRoute(selection.path, selection.sessionID); route != "" { |
| 112 | // Same provisional gate as a ready-tab registration: the route is |
| 113 | // committed before Serve confirms it through the async /resume. |
| 114 | switching := current.routing.currentPath != route |
| 115 | commitRemoteTabAttachRoute(current, route, false) |
| 116 | if switching { |
| 117 | current.routing.rehydratingPath = route |
| 118 | } |
| 119 | } |
| 120 | } |
| 121 | selection.deferred = false |
| 122 | selection.identityCommitted = true |
| 123 | selection.previous = previous |
| 124 | current.pendingSelection = selection |
| 125 | current.err = "" |
| 126 | meta := remoteTabMetaLocked(current) |
| 127 | a.remoteTabMu.Unlock() |
| 128 | tab.routeEventMu.Unlock() |
| 129 | |
| 130 | if identityChanged { |
| 131 | a.emitRemoteEvent("remote-tab:updated", meta) |
| 132 | a.saveTabsFromRemote() |
| 133 | } |
| 134 | a.resumeRemoteTabOpenAsync(tabID, selection.name, selection.path, selection.title, previous) |
| 135 | } |
| 136 | |
| 137 | func (a *App) resumeRemoteTabOpenAsync(tabID, name, sessionPath, sessionTitle string, selection *remoteTabOpenSelection) { |
| 138 | revision := uint64(0) |
| 139 | if selection != nil { |
| 140 | if selection.revision == 0 { |
| 141 | a.remoteTabMu.Lock() |
| 142 | if current := a.remoteTabs[tabID]; current != nil { |
| 143 | selection.revision = current.selectionRevision |
| 144 | } |
| 145 | a.remoteTabMu.Unlock() |
| 146 | } |
| 147 | revision = selection.revision |
| 148 | } |
| 149 | a.goRemoteTabSafe("remoteTabResume", func() { |
| 150 | a.remoteTabMu.Lock() |
| 151 | tab := a.remoteTabs[tabID] |
| 152 | a.remoteTabMu.Unlock() |
| 153 | if tab == nil { |
| 154 | return |
| 155 | } |
| 156 | func() { |
| 157 | tab.selectionMu.Lock() |
| 158 | defer tab.selectionMu.Unlock() |
| 159 | a.resumeRemoteTabSessionPathForOpenSelection(tabID, name, sessionPath, sessionTitle, revision, selection) |
| 160 | }() |
| 161 | a.applyPendingRemoteTabOpenSelection(tabID) |
| 162 | }) |
| 163 | } |
| 164 | |
| 165 | func restoreRemoteTabOpenSelectionLocked(current *remoteTab, previous *remoteTabOpenSelection) { |
| 166 | current.session = previous.session |
| 167 | current.topicTitle = previous.topicTitle |
| 168 | current.routing.currentPath = previous.currentPath |
| 169 | current.routing.pathRevision++ |
| 170 | current.routing.revision++ |
| 171 | current.routing.rehydratingPath = "" |
| 172 | current.routing.rehydratingFrames = nil |
| 173 | current.pendingEvents = cloneRemotePendingEvents(previous.pending) |
| 174 | restoredRuntime := previous.runtime |
| 175 | restoredRuntime.revision = max(current.runtime.revision, previous.runtime.revision) + 1 |
| 176 | current.runtime = restoredRuntime |
| 177 | } |
| 178 |