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 source.FileHandle var gosum []byte var modFileIdentifier string var err error // TODO(rfindley): 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 matchURI span.URI for modURI := range snapshot.workspace.activeModFiles() { if dirURI(modURI) == snapshot.view.rootURI { matchURI = modURI } } // TODO(rFindley): should it be an error if matchURI is empty? if matchURI != "" { modFH, err = snapshot.GetFile(ctx, matchURI) if err != nil { return err } modFileIdentifier = modFH.FileIdentity().Hash gosum = snapshot.goSum(ctx, matchURI) } // 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, gosum) 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 source.FileHandle, gosum []byte) (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, gosum) 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() }