1// Copyright 2020 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package cache
6
7import (
8	"context"
9	"fmt"
10	"reflect"
11	"strings"
12	"sync"
13	"time"
14
15	"golang.org/x/tools/internal/event"
16	"golang.org/x/tools/internal/event/keys"
17	"golang.org/x/tools/internal/gocommand"
18	"golang.org/x/tools/internal/imports"
19	"golang.org/x/tools/internal/lsp/source"
20)
21
22type importsState struct {
23	ctx context.Context
24
25	mu                   sync.Mutex
26	processEnv           *imports.ProcessEnv
27	cleanupProcessEnv    func()
28	cacheRefreshDuration time.Duration
29	cacheRefreshTimer    *time.Timer
30	cachedModFileHash    string
31	cachedBuildFlags     []string
32}
33
34func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot, fn func(*imports.Options) error) error {
35	s.mu.Lock()
36	defer s.mu.Unlock()
37
38	// Find the hash of the active mod file, if any. Using the unsaved content
39	// is slightly wasteful, since we'll drop caches a little too often, but
40	// the mod file shouldn't be changing while people are autocompleting.
41	var modFileHash string
42	if snapshot.workspaceMode()&usesWorkspaceModule == 0 {
43		for m := range snapshot.workspace.getActiveModFiles() { // range to access the only element
44			modFH, err := snapshot.GetFile(ctx, m)
45			if err != nil {
46				return err
47			}
48			modFileHash = modFH.FileIdentity().Hash
49		}
50	} else {
51		modFile, err := snapshot.workspace.modFile(ctx, snapshot)
52		if err != nil {
53			return err
54		}
55		modBytes, err := modFile.Format()
56		if err != nil {
57			return err
58		}
59		modFileHash = hashContents(modBytes)
60	}
61
62	// view.goEnv is immutable -- changes make a new view. Options can change.
63	// We can't compare build flags directly because we may add -modfile.
64	snapshot.view.optionsMu.Lock()
65	localPrefix := snapshot.view.options.Local
66	currentBuildFlags := snapshot.view.options.BuildFlags
67	changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) ||
68		snapshot.view.options.VerboseOutput != (s.processEnv.Logf != nil) ||
69		modFileHash != s.cachedModFileHash
70	snapshot.view.optionsMu.Unlock()
71
72	// If anything relevant to imports has changed, clear caches and
73	// update the processEnv. Clearing caches blocks on any background
74	// scans.
75	if changed {
76		// As a special case, skip cleanup the first time -- we haven't fully
77		// initialized the environment yet and calling GetResolver will do
78		// unnecessary work and potentially mess up the go.mod file.
79		if s.cleanupProcessEnv != nil {
80			if resolver, err := s.processEnv.GetResolver(); err == nil {
81				if modResolver, ok := resolver.(*imports.ModuleResolver); ok {
82					modResolver.ClearForNewMod()
83				}
84			}
85			s.cleanupProcessEnv()
86		}
87		s.cachedModFileHash = modFileHash
88		s.cachedBuildFlags = currentBuildFlags
89		var err error
90		s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, snapshot)
91		if err != nil {
92			return err
93		}
94	}
95
96	// Run the user function.
97	opts := &imports.Options{
98		// Defaults.
99		AllErrors:   true,
100		Comments:    true,
101		Fragment:    true,
102		FormatOnly:  false,
103		TabIndent:   true,
104		TabWidth:    8,
105		Env:         s.processEnv,
106		LocalPrefix: localPrefix,
107	}
108
109	if err := fn(opts); err != nil {
110		return err
111	}
112
113	if s.cacheRefreshTimer == nil {
114		// Don't refresh more than twice per minute.
115		delay := 30 * time.Second
116		// Don't spend more than a couple percent of the time refreshing.
117		if adaptive := 50 * s.cacheRefreshDuration; adaptive > delay {
118			delay = adaptive
119		}
120		s.cacheRefreshTimer = time.AfterFunc(delay, s.refreshProcessEnv)
121	}
122
123	return nil
124}
125
126// populateProcessEnv sets the dynamically configurable fields for the view's
127// process environment. Assumes that the caller is holding the s.view.importsMu.
128func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapshot) (cleanup func(), err error) {
129	pe := s.processEnv
130
131	if snapshot.view.Options().VerboseOutput {
132		pe.Logf = func(format string, args ...interface{}) {
133			event.Log(ctx, fmt.Sprintf(format, args...))
134		}
135	} else {
136		pe.Logf = nil
137	}
138
139	// Take an extra reference to the snapshot so that its workspace directory
140	// (if any) isn't destroyed while we're using it.
141	release := snapshot.generation.Acquire(ctx)
142	_, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{
143		WorkingDir: snapshot.view.rootURI.Filename(),
144	})
145	if err != nil {
146		return nil, err
147	}
148	pe.WorkingDir = inv.WorkingDir
149	pe.BuildFlags = inv.BuildFlags
150	pe.WorkingDir = inv.WorkingDir
151	pe.ModFile = inv.ModFile
152	pe.ModFlag = inv.ModFlag
153	pe.Env = map[string]string{}
154	for _, kv := range inv.Env {
155		split := strings.SplitN(kv, "=", 2)
156		if len(split) != 2 {
157			continue
158		}
159		pe.Env[split[0]] = split[1]
160	}
161
162	return func() {
163		cleanupInvocation()
164		release()
165	}, nil
166}
167
168func (s *importsState) refreshProcessEnv() {
169	start := time.Now()
170
171	s.mu.Lock()
172	env := s.processEnv
173	if resolver, err := s.processEnv.GetResolver(); err == nil {
174		resolver.ClearForNewScan()
175	}
176	s.mu.Unlock()
177
178	event.Log(s.ctx, "background imports cache refresh starting")
179	if err := imports.PrimeCache(context.Background(), env); err == nil {
180		event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)))
181	} else {
182		event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err))
183	}
184	s.mu.Lock()
185	s.cacheRefreshDuration = time.Since(start)
186	s.cacheRefreshTimer = nil
187	s.mu.Unlock()
188}
189
190func (s *importsState) destroy() {
191	s.mu.Lock()
192	if s.cleanupProcessEnv != nil {
193		s.cleanupProcessEnv()
194	}
195	s.mu.Unlock()
196}
197