返回 DeepSeek-Reasonix
tab_controller_boot.go
根目录 / desktop / tab_controller_boot.go
1 package main
2
3 import (
4 "context"
5 "errors"
6
7 "reasonix/internal/boot"
8 "reasonix/internal/control"
9 "reasonix/internal/session"
10 )
11
12 var errTabControllerExtensionsChanged = errors.New("desktop: controller extensions changed during build")
13
14 // buildTabControllerBoot is a thin wrapper around boot.Build so the large
15 // controller assembly path can stay under function-size / complexity budgets.
16 func (a *App) buildTabControllerBoot(ctx context.Context, opts boot.Options) (control.SessionAPI, error) {
17 if opts.SessionService == nil {
18 opts.SessionService = a.desktopSessionService(opts.SessionDir)
19 }
20 if opts.OnSessionRotation == nil {
21 opts.OnSessionRotation = a.prepareDesktopSessionRotation
22 }
23 return boot.Build(ctx, opts)
24 }
25
26 func desktopSessionRoot(sessionDir string) string {
27 return session.RootForLegacyDir(sessionDir)
28 }
29
30 func (a *App) desktopSessionService(sessionDir string) *session.Service {
31 if a == nil {
32 return nil
33 }
34 a.sessionServicesMu.Lock()
35 defer a.sessionServicesMu.Unlock()
36 root := a.desktopSessions.root
37 // Zero-value Apps in narrow tests retain an isolated legacy-derived root;
38 // NewApp always supplies the production v5 root.
39 if root == "" {
40 root = desktopSessionRoot(sessionDir)
41 a.desktopSessions.root = root
42 }
43 if root == "" {
44 return nil
45 }
46 if a.sessionServices == nil {
47 a.sessionServices = map[string]*session.Service{}
48 }
49 for _, service := range a.sessionServices {
50 // There is deliberately one local service even when a caller still
51 // carries a project-local legacy sessionDir during the cutover.
52 if service != nil {
53 return service
54 }
55 }
56 if service := a.sessionServices[root]; service != nil {
57 return service
58 }
59 service, err := session.NewService("local", session.NewFilesystemPersistence(root))
60 if err != nil {
61 return nil
62 }
63 a.sessionServices[root] = service
64 return service
65 }
66
67 // buildTabControllerBootFenced keeps optimistic builds concurrent with each
68 // other but excludes live MCP mutation. The generation check happens after the
69 // gate so a build that loaded stale configuration never launches extensions.
70 func (a *App) buildTabControllerBootFenced(ctx context.Context, generation uint64, opts boot.Options) (control.SessionAPI, error) {
71 a.extensionBuildMu.RLock()
72 defer a.extensionBuildMu.RUnlock()
73 if a.currentExtensionGeneration() != generation {
74 return nil, errTabControllerExtensionsChanged
75 }
76 return a.buildTabControllerBoot(ctx, opts)
77 }
78
79 // lockTabControllerPublication makes extension generation and project
80 // maintenance reservations part of the same publication admission. An MCP
81 // writer bumps the generation before releasing runtimeAdmissionMu, while a
82 // worktree mutation publishes its canonical reservation before releasing the
83 // write side, so neither stale registries nor a late project controller can be
84 // installed afterward.
85 func (a *App) lockTabControllerPublication(generation uint64, scope, workspaceRoot string) (func(), bool) {
86 a.runtimeAdmissionMu.RLock()
87 if a.currentExtensionGeneration() != generation {
88 a.runtimeAdmissionMu.RUnlock()
89 return nil, false
90 }
91 if scope != "project" {
92 return a.runtimeAdmissionMu.RUnlock, true
93 }
94 key := canonicalRuntimeRoot(workspaceRoot)
95 if key == "" || a.workspaceMergeReservedSnapshot(key) {
96 a.runtimeAdmissionMu.RUnlock()
97 return nil, false
98 }
99 return a.runtimeAdmissionMu.RUnlock, true
100 }
101
102 func (a *App) handleTabControllerBootError(
103 tab *WorkspaceTab,
104 registration *sharedHostMCPRegistration,
105 rootKey string,
106 buildGeneration uint64,
107 wailsCtx context.Context,
108 err error,
109 ) bool {
110 if err == nil {
111 return false
112 }
113 registration.rollback()
114 if errors.Is(err, errTabControllerExtensionsChanged) {
115 a.abandonSupersededBuild(tab, nil, rootKey, "")
116 a.scheduleDeferredStartupBuild(tab.ID)
117 return true
118 }
119 a.mu.Lock()
120 if a.tabBuildSupersededLocked(tab, buildGeneration) {
121 a.mu.Unlock()
122 a.abandonSupersededBuild(tab, nil, rootKey, "")
123 return true
124 }
125 leaseHeld, save := a.markTabStartupFailureLocked(tab, err, keepStartupRestore)
126 hostKey := takeTabSharedHostKey(tab)
127 tab.releaseSessionLease()
128 a.mu.Unlock()
129 a.writeTabsSaveRequest(save)
130 if hostKey != "" {
131 a.releaseSharedHost(hostKey)
132 }
133 if leaseHeld {
134 a.scheduleDeferredStartupBuild(tab.ID)
135 }
136 a.emitReady(wailsCtx, tab.ID)
137 return true
138 }
139
139 lines GO