| // Copyright 2020 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package cache |
| |
| import ( |
| "context" |
| "fmt" |
| "os" |
| "reflect" |
| "strings" |
| "sync" |
| "time" |
| |
| "golang.org/x/tools/internal/event" |
| "golang.org/x/tools/internal/event/keys" |
| "golang.org/x/tools/internal/gocommand" |
| "golang.org/x/tools/internal/imports" |
| "golang.org/x/tools/internal/lsp/source" |
| ) |
| |
| type importsState struct { |
| ctx context.Context |
| |
| mu sync.Mutex |
| processEnv *imports.ProcessEnv |
| cleanupProcessEnv func() |
| cacheRefreshDuration time.Duration |
| cacheRefreshTimer *time.Timer |
| cachedModFileHash source.Hash |
| cachedBuildFlags []string |
| cachedDirectoryFilters []string |
| } |
| |
| func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot, fn func(*imports.Options) error) error { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| // Find the hash of the active mod file, if any. Using the unsaved content |
| // is slightly wasteful, since we'll drop caches a little too often, but |
| // the mod file shouldn't be changing while people are autocompleting. |
| var modFileHash source.Hash |
| // If we are using 'legacyWorkspace' mode, we can just read the modfile from |
| // the snapshot. Otherwise, we need to get the synthetic workspace mod file. |
| // |
| // TODO(rfindley): we should be able to just always use the synthetic |
| // workspace module, or alternatively use the go.work file. |
| if snapshot.workspace.moduleSource == legacyWorkspace { |
| for m := range snapshot.workspace.getActiveModFiles() { // range to access the only element |
| modFH, err := snapshot.GetFile(ctx, m) |
| if err != nil { |
| return err |
| } |
| modFileHash = modFH.FileIdentity().Hash |
| } |
| } else { |
| modFile, err := snapshot.workspace.modFile(ctx, snapshot) |
| if err != nil { |
| return err |
| } |
| modBytes, err := modFile.Format() |
| if err != nil { |
| return err |
| } |
| modFileHash = source.HashOf(modBytes) |
| } |
| |
| // view.goEnv is immutable -- changes make a new view. Options can change. |
| // We can't compare build flags directly because we may add -modfile. |
| snapshot.view.optionsMu.Lock() |
| localPrefix := snapshot.view.options.Local |
| currentBuildFlags := snapshot.view.options.BuildFlags |
| currentDirectoryFilters := snapshot.view.options.DirectoryFilters |
| changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) || |
| snapshot.view.options.VerboseOutput != (s.processEnv.Logf != nil) || |
| modFileHash != s.cachedModFileHash || |
| !reflect.DeepEqual(snapshot.view.options.DirectoryFilters, s.cachedDirectoryFilters) |
| snapshot.view.optionsMu.Unlock() |
| |
| // If anything relevant to imports has changed, clear caches and |
| // update the processEnv. Clearing caches blocks on any background |
| // scans. |
| if changed { |
| // As a special case, skip cleanup the first time -- we haven't fully |
| // initialized the environment yet and calling GetResolver will do |
| // unnecessary work and potentially mess up the go.mod file. |
| if s.cleanupProcessEnv != nil { |
| if resolver, err := s.processEnv.GetResolver(); err == nil { |
| if modResolver, ok := resolver.(*imports.ModuleResolver); ok { |
| modResolver.ClearForNewMod() |
| } |
| } |
| s.cleanupProcessEnv() |
| } |
| s.cachedModFileHash = modFileHash |
| s.cachedBuildFlags = currentBuildFlags |
| s.cachedDirectoryFilters = currentDirectoryFilters |
| var err error |
| s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, snapshot) |
| if err != nil { |
| return err |
| } |
| } |
| |
| // Run the user function. |
| opts := &imports.Options{ |
| // Defaults. |
| AllErrors: true, |
| Comments: true, |
| Fragment: true, |
| FormatOnly: false, |
| TabIndent: true, |
| TabWidth: 8, |
| Env: s.processEnv, |
| LocalPrefix: localPrefix, |
| } |
| |
| if err := fn(opts); err != nil { |
| return err |
| } |
| |
| if s.cacheRefreshTimer == nil { |
| // Don't refresh more than twice per minute. |
| delay := 30 * time.Second |
| // Don't spend more than a couple percent of the time refreshing. |
| if adaptive := 50 * s.cacheRefreshDuration; adaptive > delay { |
| delay = adaptive |
| } |
| s.cacheRefreshTimer = time.AfterFunc(delay, s.refreshProcessEnv) |
| } |
| |
| return nil |
| } |
| |
| // populateProcessEnv sets the dynamically configurable fields for the view's |
| // process environment. Assumes that the caller is holding the s.view.importsMu. |
| func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapshot) (cleanup func(), err error) { |
| pe := s.processEnv |
| |
| if snapshot.view.Options().VerboseOutput { |
| pe.Logf = func(format string, args ...interface{}) { |
| event.Log(ctx, fmt.Sprintf(format, args...)) |
| } |
| } else { |
| pe.Logf = nil |
| } |
| |
| // Extract invocation details from the snapshot to use with goimports. |
| // |
| // TODO(rfindley): refactor to extract the necessary invocation logic into |
| // separate functions. Using goCommandInvocation is unnecessarily indirect, |
| // and has led to memory leaks in the past, when the snapshot was |
| // unintentionally held past its lifetime. |
| _, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{ |
| WorkingDir: snapshot.view.rootURI.Filename(), |
| }) |
| if err != nil { |
| return nil, err |
| } |
| |
| pe.BuildFlags = inv.BuildFlags |
| pe.ModFlag = "readonly" // processEnv operations should not mutate the modfile |
| pe.Env = map[string]string{} |
| for _, kv := range inv.Env { |
| split := strings.SplitN(kv, "=", 2) |
| if len(split) != 2 { |
| continue |
| } |
| pe.Env[split[0]] = split[1] |
| } |
| // We don't actually use the invocation, so clean it up now. |
| cleanupInvocation() |
| |
| // If the snapshot uses a synthetic workspace directory, create a copy for |
| // the lifecycle of the importsState. |
| // |
| // Notably, we cannot use the snapshot invocation working directory, as that |
| // is tied to the lifecycle of the snapshot. |
| // |
| // Otherwise return a no-op cleanup function. |
| cleanup = func() {} |
| if snapshot.usesWorkspaceDir() { |
| tmpDir, err := makeWorkspaceDir(ctx, snapshot.workspace, snapshot) |
| if err != nil { |
| return nil, err |
| } |
| pe.WorkingDir = tmpDir |
| cleanup = func() { |
| os.RemoveAll(tmpDir) // ignore error |
| } |
| } else { |
| pe.WorkingDir = snapshot.view.rootURI.Filename() |
| } |
| |
| return cleanup, nil |
| } |
| |
| func (s *importsState) refreshProcessEnv() { |
| start := time.Now() |
| |
| s.mu.Lock() |
| env := s.processEnv |
| if resolver, err := s.processEnv.GetResolver(); err == nil { |
| resolver.ClearForNewScan() |
| } |
| s.mu.Unlock() |
| |
| event.Log(s.ctx, "background imports cache refresh starting") |
| if err := imports.PrimeCache(context.Background(), env); err == nil { |
| event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start))) |
| } else { |
| event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err)) |
| } |
| s.mu.Lock() |
| s.cacheRefreshDuration = time.Since(start) |
| s.cacheRefreshTimer = nil |
| s.mu.Unlock() |
| } |
| |
| func (s *importsState) destroy() { |
| s.mu.Lock() |
| if s.cleanupProcessEnv != nil { |
| s.cleanupProcessEnv() |
| } |
| s.mu.Unlock() |
| } |