github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libgit/autogit_manager.go (about) 1 // Copyright 2017 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package libgit 6 7 import ( 8 "context" 9 "os" 10 "sync" 11 12 lru "github.com/hashicorp/golang-lru" 13 "github.com/keybase/client/go/kbfs/data" 14 "github.com/keybase/client/go/kbfs/libfs" 15 "github.com/keybase/client/go/kbfs/libkbfs" 16 "github.com/keybase/client/go/kbfs/tlf" 17 "github.com/keybase/client/go/kbfs/tlfhandle" 18 "github.com/keybase/client/go/logger" 19 "github.com/keybase/client/go/protocol/keybase1" 20 "github.com/pkg/errors" 21 "gopkg.in/src-d/go-git.v4/plumbing" 22 ) 23 24 const ( 25 // Debug tag ID for an individual autogit operation 26 ctxAutogitOpID = "AGID" 27 ) 28 29 type ctxAutogitTagKey int 30 31 const ( 32 ctxAutogitIDKey ctxAutogitTagKey = iota 33 ) 34 35 type browserCacheKey struct { 36 fs *libfs.FS 37 repoName string 38 branch plumbing.ReferenceName 39 subdir string 40 } 41 42 type browserCacheValue struct { 43 repoFS *libfs.FS 44 browser *Browser 45 } 46 47 // AutogitManager can clone and pull source git repos into a 48 // destination folder, potentially across different TLFs. New 49 // requests for an operation in a destination repo are blocked by any 50 // ongoing requests for the same folder, and multiple outstanding 51 // requests for the same destination folder get rolled up into one. 52 type AutogitManager struct { 53 config libkbfs.Config 54 log logger.Logger 55 deferLog logger.Logger 56 57 registryLock sync.RWMutex 58 registeredFBs map[data.FolderBranch]bool 59 repoNodesForWatchedIDs map[libkbfs.NodeID]*repoDirNode 60 watchedNodes []libkbfs.Node // preventing GC on the watched nodes 61 deleteCancels map[string]context.CancelFunc 62 shutdown bool 63 64 sharedInBrowserCache sharedInBrowserCache 65 66 browserLock sync.Mutex 67 browserCache *lru.Cache 68 69 doRemoveSelfCheckouts sync.Once 70 } 71 72 // NewAutogitManager constructs a new AutogitManager instance. 73 func NewAutogitManager( 74 config libkbfs.Config, browserCacheSize int) *AutogitManager { 75 log := config.MakeLogger("") 76 browserCache, err := lru.New(browserCacheSize) 77 if err != nil { 78 panic(err.Error()) 79 } 80 sharedCache, err := newLRUSharedInBrowserCache() 81 if err != nil { 82 panic(err.Error()) 83 } 84 85 return &AutogitManager{ 86 config: config, 87 log: log, 88 deferLog: log.CloneWithAddedDepth(1), 89 registeredFBs: make(map[data.FolderBranch]bool), 90 repoNodesForWatchedIDs: make(map[libkbfs.NodeID]*repoDirNode), 91 deleteCancels: make(map[string]context.CancelFunc), 92 browserCache: browserCache, 93 sharedInBrowserCache: sharedCache, 94 } 95 } 96 97 // Shutdown shuts down this manager. 98 func (am *AutogitManager) Shutdown() { 99 am.registryLock.Lock() 100 defer am.registryLock.Unlock() 101 am.shutdown = true 102 for _, cancel := range am.deleteCancels { 103 cancel() 104 } 105 } 106 107 func (am *AutogitManager) removeOldCheckoutsForHandle( 108 ctx context.Context, h *tlfhandle.Handle, branch data.BranchName) { 109 // Make an "unwrapped" FS, so we don't end up recursively entering 110 // the virtual autogit nodes again. 111 fs, err := libfs.NewUnwrappedFS( 112 ctx, am.config, h, branch, "", "", keybase1.MDPriorityNormal) 113 if err != nil { 114 am.log.CDebugf(ctx, "Error making unwrapped FS for TLF %s: %+v", 115 h.GetCanonicalPath(), err) 116 return 117 } 118 119 fi, err := fs.Stat(AutogitRoot) 120 if os.IsNotExist(errors.Cause(err)) { 121 // No autogit repos to remove. 122 return 123 } else if err != nil { 124 am.log.CDebugf(ctx, 125 "Error checking autogit in unwrapped FS for TLF %s: %+v", 126 h.GetCanonicalPath(), err) 127 return 128 } 129 130 ctx, ok := func() (context.Context, bool) { 131 am.registryLock.Lock() 132 defer am.registryLock.Unlock() 133 if am.shutdown { 134 return nil, false 135 } 136 p := h.GetCanonicalPath() 137 if _, ok := am.deleteCancels[p]; ok { 138 return nil, false 139 } 140 141 ctx, cancel := context.WithCancel(ctx) 142 am.deleteCancels[p] = cancel 143 return ctx, true 144 }() 145 if !ok { 146 return 147 } 148 149 am.log.CDebugf(ctx, "Recursively deleting old autogit data in TLF %s", 150 h.GetCanonicalPath()) 151 defer func() { 152 am.log.CDebugf(ctx, "Recursive delete of autogit done: %+v", err) 153 am.registryLock.Lock() 154 defer am.registryLock.Unlock() 155 delete(am.deleteCancels, h.GetCanonicalPath()) 156 }() 157 err = libfs.RecursiveDelete(ctx, fs, fi) 158 } 159 160 func (am *AutogitManager) removeOldCheckouts(node libkbfs.Node) { 161 ctx := libkbfs.CtxWithRandomIDReplayable( 162 context.Background(), ctxAutogitIDKey, ctxAutogitOpID, am.log) 163 164 h, err := am.config.KBFSOps().GetTLFHandle(ctx, node) 165 if err != nil { 166 am.log.CDebugf(ctx, "Error getting handle: %+v", err) 167 return 168 } 169 170 am.removeOldCheckoutsForHandle(ctx, h, node.GetFolderBranch().Branch) 171 } 172 173 func (am *AutogitManager) removeSelfCheckouts() { 174 ctx := libkbfs.CtxWithRandomIDReplayable( 175 context.Background(), ctxAutogitIDKey, ctxAutogitOpID, am.log) 176 177 session, err := am.config.KBPKI().GetCurrentSession(ctx) 178 if err != nil { 179 am.log.CDebugf(ctx, 180 "Unable to get session; ignoring self-autogit delete: +%v", err) 181 return 182 } 183 184 h, err := libkbfs.GetHandleFromFolderNameAndType( 185 ctx, am.config.KBPKI(), am.config.MDOps(), 186 am.config, string(session.Name), tlf.Private) 187 if err != nil { 188 am.log.CDebugf(ctx, 189 "Unable to get private handle; ignoring self-autogit delete: +%v", 190 err) 191 return 192 } 193 194 am.removeOldCheckoutsForHandle(ctx, h, data.MasterBranch) 195 } 196 197 func (am *AutogitManager) registerRepoNode( 198 nodeToWatch libkbfs.Node, rdn *repoDirNode) { 199 am.registryLock.Lock() 200 defer am.registryLock.Unlock() 201 am.repoNodesForWatchedIDs[nodeToWatch.GetID()] = rdn 202 fb := nodeToWatch.GetFolderBranch() 203 if am.registeredFBs[fb] { 204 return 205 } 206 207 go am.removeOldCheckouts(rdn) 208 am.doRemoveSelfCheckouts.Do(func() { go am.removeSelfCheckouts() }) 209 am.watchedNodes = append(am.watchedNodes, nodeToWatch) 210 err := am.config.Notifier().RegisterForChanges( 211 []data.FolderBranch{fb}, am) 212 if err != nil { 213 am.log.CWarningf( 214 context.TODO(), "Error registering %s: +%v", fb.Tlf, err) 215 return 216 } 217 am.registeredFBs[fb] = true 218 } 219 220 // LocalChange implements the libkbfs.Observer interface for AutogitManager. 221 func (am *AutogitManager) LocalChange( 222 ctx context.Context, node libkbfs.Node, wr libkbfs.WriteRange) { 223 // Do nothing. 224 } 225 226 func (am *AutogitManager) getNodesToInvalidate( 227 affectedNodeIDs []libkbfs.NodeID) ( 228 nodes []libkbfs.Node, repoNodeIDs []libkbfs.NodeID) { 229 am.registryLock.RLock() 230 defer am.registryLock.RUnlock() 231 for _, nodeID := range affectedNodeIDs { 232 node, ok := am.repoNodesForWatchedIDs[nodeID] 233 if ok { 234 nodes = append(nodes, node) 235 repoNodeIDs = append(repoNodeIDs, nodeID) 236 } 237 } 238 return nodes, repoNodeIDs 239 } 240 241 func (am *AutogitManager) clearInvalidatedBrowsers( 242 repoNodeIDs []libkbfs.NodeID) { 243 am.browserLock.Lock() 244 defer am.browserLock.Unlock() 245 246 keys := am.browserCache.Keys() 247 for _, k := range keys { 248 // Clear all cached browsers associated with 249 tmp, ok := am.browserCache.Get(k) 250 if !ok { 251 continue 252 } 253 v, ok := tmp.(browserCacheValue) 254 if !ok { 255 continue 256 } 257 rootNodeID := v.repoFS.RootNode().GetID() 258 // Note that in almost all cases, `repoNodeIDs` should only 259 // have one entry (since only one repo is updated in a single 260 // metadata update), so iterating here should be cheaper than 261 // making a map. 262 for _, nodeID := range repoNodeIDs { 263 if rootNodeID == nodeID { 264 am.log.CDebugf( 265 context.TODO(), "Invalidating browser for %s", 266 v.repoFS.Root()) 267 am.browserCache.Remove(k) 268 break 269 } 270 } 271 } 272 } 273 274 // BatchChanges implements the libkbfs.Observer interface for AutogitManager. 275 func (am *AutogitManager) BatchChanges( 276 ctx context.Context, _ []libkbfs.NodeChange, 277 affectedNodeIDs []libkbfs.NodeID) { 278 nodes, repoNodeIDs := am.getNodesToInvalidate(affectedNodeIDs) 279 go am.clearInvalidatedBrowsers(repoNodeIDs) 280 for _, node := range nodes { 281 node := node 282 go func() { 283 ctx := libkbfs.CtxWithRandomIDReplayable( 284 context.Background(), ctxAutogitIDKey, ctxAutogitOpID, am.log) 285 err := am.config.KBFSOps().InvalidateNodeAndChildren(ctx, node) 286 if err != nil { 287 am.log.CDebugf(ctx, "Error invalidating children: %+v", err) 288 } 289 }() 290 } 291 } 292 293 // TlfHandleChange implements the libkbfs.Observer interface for 294 // AutogitManager. 295 func (am *AutogitManager) TlfHandleChange( 296 ctx context.Context, newHandle *tlfhandle.Handle) { 297 // Do nothing. 298 } 299 300 func (am *AutogitManager) getBrowserForRepoLocked( 301 ctx context.Context, gitFS *libfs.FS, repoName string, 302 branch plumbing.ReferenceName, subdir string) (*libfs.FS, *Browser, error) { 303 repoName = normalizeRepoName(repoName) 304 key := browserCacheKey{gitFS, repoName, branch, subdir} 305 tmp, ok := am.browserCache.Get(key) 306 if ok { 307 b, ok := tmp.(browserCacheValue) 308 if !ok { 309 return nil, nil, errors.Errorf("Bad browser in cache: %T", tmp) 310 } 311 return b.repoFS, b.browser, nil 312 } 313 314 // It's kind of dumb to hold the browser lock through all of this, 315 // but it doesn't seem worthwhile to build the whole 316 // channel/notification system that would be needed to manage 317 // multiple concurrent requests for the same repo node. 318 319 am.log.CDebugf(ctx, "Making browser for repo=%s, branch=%s, subdir=%s", 320 repoName, branch, subdir) 321 322 // Recurse to get the root browser, and then chroot to the subdir. 323 if subdir != "" { 324 repoFS, rootB, err := am.getBrowserForRepoLocked( 325 ctx, gitFS, repoName, branch, "") 326 if err != nil { 327 return nil, nil, err 328 } 329 330 b, err := rootB.Chroot(subdir) 331 if err != nil { 332 return nil, nil, err 333 } 334 browser, ok := b.(*Browser) 335 if !ok { 336 return nil, nil, errors.Errorf("Bad browser type: %T", b) 337 } 338 am.browserCache.Add(key, browserCacheValue{repoFS, browser}) 339 return repoFS, browser, nil 340 } 341 342 billyFS, err := gitFS.Chroot(repoName) 343 if err != nil { 344 return nil, nil, err 345 } 346 repoFS := billyFS.(*libfs.FS) 347 browser, err := NewBrowser( 348 repoFS, am.config.Clock(), branch, am.sharedInBrowserCache) 349 if err != nil { 350 return nil, nil, err 351 } 352 am.browserCache.Add(key, browserCacheValue{repoFS, browser}) 353 return repoFS, browser, nil 354 } 355 356 // GetBrowserForRepo returns the root FS for the specified repo and a 357 // `Browser` for the branch and subdir. 358 func (am *AutogitManager) GetBrowserForRepo( 359 ctx context.Context, gitFS *libfs.FS, repoName string, 360 branch plumbing.ReferenceName, subdir string) (*libfs.FS, *Browser, error) { 361 am.browserLock.Lock() 362 defer am.browserLock.Unlock() 363 return am.getBrowserForRepoLocked(ctx, gitFS, repoName, branch, subdir) 364 } 365 366 // StartAutogit launches autogit, and returns a function that should 367 // be called on shutdown. 368 func StartAutogit(config libkbfs.Config, browserCacheSize int) func() { 369 am := NewAutogitManager(config, browserCacheSize) 370 rw := rootWrapper{am} 371 config.AddRootNodeWrapper(rw.wrap) 372 return am.Shutdown 373 }