Skip to content

Commit 330dc7d

Browse files
committed
internal/lsp/cache: assign a static temp workspace dir to the first view
Editors need a way to run commands in the same workspace that gopls sees. Longer term, we need a good solution for this that supports multiple workspace folders, but for now just write the first folder's workspace to a deterministic location: $TMPDIR/gopls-<client PID>.workspace. Using the client-provided PID allows this mechanism to work even for multi-session daemons. Along the way, simplify the snapshot reinitialization logic a bit. Fixes golang/go#42126 Change-Id: I5b9f454fcf1a1a8fa49a4b0a122e55e762d398b4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/264618 Run-TryBot: Robert Findley <rfindley@google.com> gopls-CI: kokoro <noreply+kokoro@google.com> Reviewed-by: Heschi Kreinick <heschi@google.com> TryBot-Result: Go Bot <gobot@golang.org> Trust: Robert Findley <rfindley@google.com>
1 parent b653051 commit 330dc7d

File tree

14 files changed

+254
-120
lines changed

14 files changed

+254
-120
lines changed

gopls/internal/regtest/runner.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ func WithModes(modes Mode) RunOption {
125125
})
126126
}
127127

128+
func SendPID() RunOption {
129+
return optionSetter(func(opts *runConfig) {
130+
opts.editor.SendPID = true
131+
})
132+
}
133+
128134
// EditorConfig is a RunOption option that configured the regtest editor.
129135
type EditorConfig fake.EditorConfig
130136

gopls/internal/regtest/workspace_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ package regtest
66

77
import (
88
"fmt"
9+
"io/ioutil"
10+
"os"
11+
"path/filepath"
912
"strings"
1013
"testing"
1114

@@ -563,3 +566,65 @@ func main() {
563566
)
564567
})
565568
}
569+
570+
func TestWorkspaceDirAccess(t *testing.T) {
571+
const multiModule = `
572+
-- moda/a/go.mod --
573+
module a.com
574+
575+
-- moda/a/a.go --
576+
package main
577+
578+
func main() {
579+
fmt.Println("Hello")
580+
}
581+
-- modb/go.mod --
582+
module b.com
583+
-- modb/b/b.go --
584+
package main
585+
586+
func main() {
587+
fmt.Println("World")
588+
}
589+
`
590+
withOptions(
591+
WithModes(Experimental),
592+
SendPID(),
593+
).run(t, multiModule, func(t *testing.T, env *Env) {
594+
pid := os.Getpid()
595+
// Don't factor this out of Server.addFolders. vscode-go expects this
596+
// directory.
597+
modPath := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.workspace", pid), "go.mod")
598+
gotb, err := ioutil.ReadFile(modPath)
599+
if err != nil {
600+
t.Fatalf("reading expected workspace modfile: %v", err)
601+
}
602+
got := string(gotb)
603+
for _, want := range []string{"a.com v0.0.0-goplsworkspace", "b.com v0.0.0-goplsworkspace"} {
604+
if !strings.Contains(got, want) {
605+
// want before got here, since the go.mod is multi-line
606+
t.Fatalf("workspace go.mod missing %q. got:\n%s", want, got)
607+
}
608+
}
609+
workdir := env.Sandbox.Workdir.RootURI().SpanURI().Filename()
610+
env.WriteWorkspaceFile("gopls.mod", fmt.Sprintf(`
611+
module gopls-workspace
612+
613+
require (
614+
a.com v0.0.0-goplsworkspace
615+
)
616+
617+
replace a.com => %s/moda/a
618+
`, workdir))
619+
env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1))
620+
gotb, err = ioutil.ReadFile(modPath)
621+
if err != nil {
622+
t.Fatalf("reading expected workspace modfile: %v", err)
623+
}
624+
got = string(gotb)
625+
want := "b.com v0.0.0-goplsworkspace"
626+
if strings.Contains(got, want) {
627+
t.Fatalf("workspace go.mod contains unexpected %q. got:\n%s", want, got)
628+
}
629+
})
630+
}

internal/lsp/cache/session.go

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package cache
77
import (
88
"context"
99
"fmt"
10+
"os"
1011
"strconv"
1112
"strings"
1213
"sync"
@@ -143,10 +144,10 @@ func (s *Session) Cache() interface{} {
143144
return s.cache
144145
}
145146

146-
func (s *Session) NewView(ctx context.Context, name string, folder span.URI, options *source.Options) (source.View, source.Snapshot, func(), error) {
147+
func (s *Session) NewView(ctx context.Context, name string, folder, tempWorkspace span.URI, options *source.Options) (source.View, source.Snapshot, func(), error) {
147148
s.viewMu.Lock()
148149
defer s.viewMu.Unlock()
149-
view, snapshot, release, err := s.createView(ctx, name, folder, options, 0)
150+
view, snapshot, release, err := s.createView(ctx, name, folder, tempWorkspace, options, 0)
150151
if err != nil {
151152
return nil, nil, func() {}, err
152153
}
@@ -156,7 +157,7 @@ func (s *Session) NewView(ctx context.Context, name string, folder span.URI, opt
156157
return view, snapshot, release, nil
157158
}
158159

159-
func (s *Session) createView(ctx context.Context, name string, folder span.URI, options *source.Options, snapshotID uint64) (*View, *snapshot, func(), error) {
160+
func (s *Session) createView(ctx context.Context, name string, folder, tempWorkspace span.URI, options *source.Options, snapshotID uint64) (*View, *snapshot, func(), error) {
160161
index := atomic.AddInt64(&viewIndex, 1)
161162

162163
if s.cache.options != nil {
@@ -182,6 +183,8 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
182183

183184
v := &View{
184185
session: s,
186+
initialWorkspaceLoad: make(chan struct{}),
187+
initializationSema: make(chan struct{}, 1),
185188
id: strconv.FormatInt(index, 10),
186189
options: options,
187190
baseCtx: baseCtx,
@@ -192,6 +195,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
192195
filesByURI: make(map[span.URI]*fileBase),
193196
filesByBase: make(map[string][]*fileBase),
194197
workspaceInformation: *ws,
198+
tempWorkspace: tempWorkspace,
195199
}
196200
v.importsState = &importsState{
197201
ctx: backgroundCtx,
@@ -202,26 +206,24 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
202206
},
203207
}
204208
v.snapshot = &snapshot{
205-
id: snapshotID,
206-
view: v,
207-
initialized: make(chan struct{}),
208-
initializationSema: make(chan struct{}, 1),
209-
initializeOnce: &sync.Once{},
210-
generation: s.cache.store.Generation(generationName(v, 0)),
211-
packages: make(map[packageKey]*packageHandle),
212-
ids: make(map[span.URI][]packageID),
213-
metadata: make(map[packageID]*metadata),
214-
files: make(map[span.URI]source.VersionedFileHandle),
215-
goFiles: make(map[parseKey]*parseGoHandle),
216-
importedBy: make(map[packageID][]packageID),
217-
actions: make(map[actionKey]*actionHandle),
218-
workspacePackages: make(map[packageID]packagePath),
219-
unloadableFiles: make(map[span.URI]struct{}),
220-
parseModHandles: make(map[span.URI]*parseModHandle),
221-
modTidyHandles: make(map[span.URI]*modTidyHandle),
222-
modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
223-
modWhyHandles: make(map[span.URI]*modWhyHandle),
224-
workspace: workspace,
209+
id: snapshotID,
210+
view: v,
211+
initializeOnce: &sync.Once{},
212+
generation: s.cache.store.Generation(generationName(v, 0)),
213+
packages: make(map[packageKey]*packageHandle),
214+
ids: make(map[span.URI][]packageID),
215+
metadata: make(map[packageID]*metadata),
216+
files: make(map[span.URI]source.VersionedFileHandle),
217+
goFiles: make(map[parseKey]*parseGoHandle),
218+
importedBy: make(map[packageID][]packageID),
219+
actions: make(map[actionKey]*actionHandle),
220+
workspacePackages: make(map[packageID]packagePath),
221+
unloadableFiles: make(map[span.URI]struct{}),
222+
parseModHandles: make(map[span.URI]*parseModHandle),
223+
modTidyHandles: make(map[span.URI]*modTidyHandle),
224+
modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
225+
modWhyHandles: make(map[span.URI]*modWhyHandle),
226+
workspace: workspace,
225227
}
226228

227229
// Initialize the view without blocking.
@@ -231,6 +233,19 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
231233
release := snapshot.generation.Acquire(initCtx)
232234
go func() {
233235
snapshot.initialize(initCtx, true)
236+
if v.tempWorkspace != "" {
237+
var err error
238+
if err = os.Mkdir(v.tempWorkspace.Filename(), 0700); err == nil {
239+
var wsdir span.URI
240+
wsdir, err = snapshot.getWorkspaceDir(initCtx)
241+
if err == nil {
242+
err = copyWorkspace(v.tempWorkspace, wsdir)
243+
}
244+
}
245+
if err != nil {
246+
event.Error(initCtx, "creating workspace dir", err)
247+
}
248+
}
234249
release()
235250
}()
236251
return v, snapshot, snapshot.generation.Acquire(ctx), nil
@@ -349,7 +364,7 @@ func (s *Session) updateView(ctx context.Context, view *View, options *source.Op
349364
view.snapshotMu.Lock()
350365
snapshotID := view.snapshot.id
351366
view.snapshotMu.Unlock()
352-
v, _, release, err := s.createView(ctx, view.name, view.folder, options, snapshotID)
367+
v, _, release, err := s.createView(ctx, view.name, view.folder, view.tempWorkspace, options, snapshotID)
353368
release()
354369
if err != nil {
355370
// we have dropped the old view, but could not create the new one

internal/lsp/cache/snapshot.go

Lines changed: 43 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -47,31 +47,15 @@ type snapshot struct {
4747
builtin *builtinPackageHandle
4848

4949
// The snapshot's initialization state is controlled by the fields below.
50-
// These fields are propagated across snapshots to avoid multiple
51-
// concurrent initializations. They may be invalidated during cloning.
5250
//
53-
// initialized is closed when the snapshot has been fully initialized. On
54-
// initialization, the snapshot's workspace packages are loaded. All of the
55-
// fields below are set as part of initialization. If we failed to load, we
56-
// only retry if the go.mod file changes, to avoid too many go/packages
57-
// calls.
58-
//
59-
// When the view is created, its snapshot's initializeOnce is non-nil,
60-
// initialized is open. Once initialization completes, initializedErr may
61-
// be set and initializeOnce becomes nil. If initializedErr is non-nil,
62-
// initialization may be retried (depending on how files are changed). To
63-
// indicate that initialization should be retried, initializeOnce will be
64-
// set. The next time a caller requests workspace packages, the
65-
// initialization will retry.
66-
initialized chan struct{}
67-
68-
// initializationSema is used as a mutex to guard initializeOnce and
69-
// initializedErr, which will be updated after each attempt to initialize
70-
// the snapshot. We use a channel instead of a mutex to avoid blocking when
71-
// a context is canceled.
72-
initializationSema chan struct{}
73-
initializeOnce *sync.Once
74-
initializedErr error
51+
// initializeOnce guards snapshot initialization. Each snapshot is
52+
// initialized at most once: reinitialization is triggered on later snapshots
53+
// by invalidating this field.
54+
initializeOnce *sync.Once
55+
// initializedErr holds the last error resulting from initialization. If
56+
// initialization fails, we only retry when the the workspace modules change,
57+
// to avoid too many go/packages calls.
58+
initializedErr error
7559

7660
// mu guards all of the maps in the snapshot.
7761
mu sync.Mutex
@@ -884,7 +868,7 @@ func (s *snapshot) AwaitInitialized(ctx context.Context) {
884868
select {
885869
case <-ctx.Done():
886870
return
887-
case <-s.initialized:
871+
case <-s.view.initialWorkspaceLoad:
888872
}
889873
// We typically prefer to run something as intensive as the IWL without
890874
// blocking. I'm not sure if there is a way to do that here.
@@ -1020,36 +1004,39 @@ func generationName(v *View, snapshotID uint64) string {
10201004
return fmt.Sprintf("v%v/%v", v.id, snapshotID)
10211005
}
10221006

1023-
func (s *snapshot) clone(ctx context.Context, changes map[span.URI]*fileChange, forceReloadMetadata bool) *snapshot {
1007+
func (s *snapshot) clone(ctx context.Context, changes map[span.URI]*fileChange, forceReloadMetadata bool) (*snapshot, bool) {
1008+
// Track some important types of changes.
1009+
var (
1010+
vendorChanged bool
1011+
modulesChanged bool
1012+
)
10241013
newWorkspace, workspaceChanged := s.workspace.invalidate(ctx, changes)
10251014

10261015
s.mu.Lock()
10271016
defer s.mu.Unlock()
10281017

10291018
newGen := s.view.session.cache.store.Generation(generationName(s.view, s.id+1))
10301019
result := &snapshot{
1031-
id: s.id + 1,
1032-
generation: newGen,
1033-
view: s.view,
1034-
builtin: s.builtin,
1035-
initialized: s.initialized,
1036-
initializationSema: s.initializationSema,
1037-
initializeOnce: s.initializeOnce,
1038-
initializedErr: s.initializedErr,
1039-
ids: make(map[span.URI][]packageID),
1040-
importedBy: make(map[packageID][]packageID),
1041-
metadata: make(map[packageID]*metadata),
1042-
packages: make(map[packageKey]*packageHandle),
1043-
actions: make(map[actionKey]*actionHandle),
1044-
files: make(map[span.URI]source.VersionedFileHandle),
1045-
goFiles: make(map[parseKey]*parseGoHandle),
1046-
workspacePackages: make(map[packageID]packagePath),
1047-
unloadableFiles: make(map[span.URI]struct{}),
1048-
parseModHandles: make(map[span.URI]*parseModHandle),
1049-
modTidyHandles: make(map[span.URI]*modTidyHandle),
1050-
modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
1051-
modWhyHandles: make(map[span.URI]*modWhyHandle),
1052-
workspace: newWorkspace,
1020+
id: s.id + 1,
1021+
generation: newGen,
1022+
view: s.view,
1023+
builtin: s.builtin,
1024+
initializeOnce: s.initializeOnce,
1025+
initializedErr: s.initializedErr,
1026+
ids: make(map[span.URI][]packageID),
1027+
importedBy: make(map[packageID][]packageID),
1028+
metadata: make(map[packageID]*metadata),
1029+
packages: make(map[packageKey]*packageHandle),
1030+
actions: make(map[actionKey]*actionHandle),
1031+
files: make(map[span.URI]source.VersionedFileHandle),
1032+
goFiles: make(map[parseKey]*parseGoHandle),
1033+
workspacePackages: make(map[packageID]packagePath),
1034+
unloadableFiles: make(map[span.URI]struct{}),
1035+
parseModHandles: make(map[span.URI]*parseModHandle),
1036+
modTidyHandles: make(map[span.URI]*modTidyHandle),
1037+
modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
1038+
modWhyHandles: make(map[span.URI]*modWhyHandle),
1039+
workspace: newWorkspace,
10531040
}
10541041

10551042
if !workspaceChanged && s.workspaceDirHandle != nil {
@@ -1105,14 +1092,11 @@ func (s *snapshot) clone(ctx context.Context, changes map[span.URI]*fileChange,
11051092
result.modWhyHandles[k] = v
11061093
}
11071094

1108-
var reinitialize reinitializeView
1109-
11101095
// directIDs keeps track of package IDs that have directly changed.
11111096
// It maps id->invalidateMetadata.
11121097
directIDs := map[packageID]bool{}
11131098
// Invalidate all package metadata if the workspace module has changed.
11141099
if workspaceChanged {
1115-
reinitialize = definitelyReinit
11161100
for k := range s.metadata {
11171101
directIDs[k] = true
11181102
}
@@ -1122,7 +1106,7 @@ func (s *snapshot) clone(ctx context.Context, changes map[span.URI]*fileChange,
11221106
// Maybe reinitialize the view if we see a change in the vendor
11231107
// directory.
11241108
if inVendor(uri) {
1125-
reinitialize = maybeReinit
1109+
vendorChanged = true
11261110
}
11271111

11281112
// The original FileHandle for this URI is cached on the snapshot.
@@ -1158,8 +1142,8 @@ func (s *snapshot) clone(ctx context.Context, changes map[span.URI]*fileChange,
11581142
// If the view's go.mod file's contents have changed, invalidate
11591143
// the metadata for every known package in the snapshot.
11601144
delete(result.parseModHandles, uri)
1161-
if _, ok := result.workspace.activeModFiles()[uri]; ok && reinitialize < maybeReinit {
1162-
reinitialize = maybeReinit
1145+
if _, ok := result.workspace.activeModFiles()[uri]; ok {
1146+
modulesChanged = true
11631147
}
11641148
}
11651149
// Handle the invalidated file; it may have new contents or not exist.
@@ -1285,20 +1269,14 @@ copyIDs:
12851269
}
12861270

12871271
// The snapshot may need to be reinitialized.
1288-
if reinitialize != doNotReinit {
1289-
result.reinitialize(reinitialize == definitelyReinit)
1272+
if modulesChanged || workspaceChanged || vendorChanged {
1273+
if workspaceChanged || result.initializedErr != nil {
1274+
result.initializeOnce = &sync.Once{}
1275+
}
12901276
}
1291-
return result
1277+
return result, workspaceChanged
12921278
}
12931279

1294-
type reinitializeView int
1295-
1296-
const (
1297-
doNotReinit = reinitializeView(iota)
1298-
maybeReinit
1299-
definitelyReinit
1300-
)
1301-
13021280
// guessPackagesForURI returns all packages related to uri. If we haven't seen this
13031281
// URI before, we guess based on files in the same directory. This is of course
13041282
// incorrect in build systems where packages are not organized by directory.

0 commit comments

Comments
 (0)