| package cache |
| |
| import ( |
| "context" |
| "fmt" |
| "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 string |
| cachedBuildFlags []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 string |
| if snapshot.workspaceMode()&usesWorkspaceModule == 0 { |
| for m := range snapshot.workspace.activeModFiles() { // 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 = hashContents(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 |
| changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) || |
| snapshot.view.options.VerboseOutput != (s.processEnv.Logf != nil) || |
| modFileHash != s.cachedModFileHash |
| 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 { |
| resolver.(*imports.ModuleResolver).ClearForNewMod() |
| } |
| s.cleanupProcessEnv() |
| } |
| s.cachedModFileHash = modFileHash |
| s.cachedBuildFlags = currentBuildFlags |
| 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 |
| } |
| |
| // Take an extra reference to the snapshot so that its workspace directory |
| // (if any) isn't destroyed while we're using it. |
| release := snapshot.generation.Acquire(ctx) |
| _, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{ |
| WorkingDir: snapshot.view.rootURI.Filename(), |
| }) |
| if err != nil { |
| return nil, err |
| } |
| pe.WorkingDir = inv.WorkingDir |
| pe.BuildFlags = inv.BuildFlags |
| pe.WorkingDir = inv.WorkingDir |
| pe.ModFile = inv.ModFile |
| pe.ModFlag = inv.ModFlag |
| 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] |
| } |
| |
| return func() { |
| cleanupInvocation() |
| release() |
| }, 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() |
| } |