| package cache |
| |
| import ( |
| "context" |
| "fmt" |
| "reflect" |
| "sync" |
| "time" |
| |
| "golang.org/x/tools/internal/event" |
| "golang.org/x/tools/internal/event/keys" |
| "golang.org/x/tools/internal/imports" |
| "golang.org/x/tools/internal/lsp/source" |
| "golang.org/x/tools/internal/span" |
| ) |
| |
| type importsState struct { |
| ctx context.Context |
| |
| mu sync.Mutex |
| processEnv *imports.ProcessEnv |
| cleanupProcessEnv func() |
| cacheRefreshDuration time.Duration |
| cacheRefreshTimer *time.Timer |
| cachedModFileIdentifier 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() |
| |
| // Use temporary go.mod files, but always go to disk for the contents. |
| // Rebuilding the cache is expensive, and we don't want to do it for |
| // transient changes. |
| var modFH, sumFH source.FileHandle |
| var modFileIdentifier string |
| var err error |
| // TODO(heschik): Change the goimports logic to use a persistent workspace |
| // module for workspace module mode. |
| // |
| // Get the go.mod file that corresponds to this view's root URI. This is |
| // broken because it assumes that the view's root is a module, but this is |
| // not more broken than the previous state--it is a temporary hack that |
| // should be removed ASAP. |
| var match *moduleRoot |
| for _, m := range snapshot.modules { |
| if m.rootURI == snapshot.view.rootURI { |
| match = m |
| } |
| } |
| if match != nil { |
| modFH, err = snapshot.GetFile(ctx, match.modURI) |
| if err != nil { |
| return err |
| } |
| modFileIdentifier = modFH.FileIdentity().Hash |
| if match.sumURI != "" { |
| sumFH, err = snapshot.GetFile(ctx, match.sumURI) |
| if err != nil { |
| return err |
| } |
| } |
| } |
| // v.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) || |
| modFileIdentifier != s.cachedModFileIdentifier |
| 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.cachedModFileIdentifier = modFileIdentifier |
| s.cachedBuildFlags = currentBuildFlags |
| s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, snapshot, modFH, sumFH) |
| 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, modFH, sumFH source.FileHandle) (cleanup func(), err error) { |
| cleanup = func() {} |
| pe := s.processEnv |
| |
| snapshot.view.optionsMu.Lock() |
| pe.BuildFlags = append([]string(nil), snapshot.view.options.BuildFlags...) |
| if snapshot.view.options.VerboseOutput { |
| pe.Logf = func(format string, args ...interface{}) { |
| event.Log(ctx, fmt.Sprintf(format, args...)) |
| } |
| } else { |
| pe.Logf = nil |
| } |
| snapshot.view.optionsMu.Unlock() |
| |
| pe.Env = map[string]string{} |
| for k, v := range snapshot.view.goEnv { |
| pe.Env[k] = v |
| } |
| pe.Env["GO111MODULE"] = snapshot.view.go111module |
| |
| var modURI span.URI |
| var modContent []byte |
| if modFH != nil { |
| modURI = modFH.URI() |
| modContent, err = modFH.Read() |
| if err != nil { |
| return nil, err |
| } |
| } |
| modmod, err := snapshot.needsModEqualsMod(ctx, modURI, modContent) |
| if err != nil { |
| return cleanup, err |
| } |
| if modmod { |
| pe.ModFlag = "mod" |
| } |
| |
| // Add -modfile to the build flags, if we are using it. |
| if snapshot.workspaceMode()&tempModfile != 0 && modFH != nil { |
| var tmpURI span.URI |
| tmpURI, cleanup, err = tempModFile(modFH, sumFH) |
| if err != nil { |
| return nil, err |
| } |
| pe.ModFile = tmpURI.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() |
| } |