Skip to content

Commit 0a5cd10

Browse files
committed
internal/lsp: handle unknown revision in go.mod file
This change ensures that, when the initial workspace load fails, we re-run it if the go.mod file changes. Previously, if a user opened a workspace with a corrupt go.mod file, we never recovered. To reinitialize the workspace on-demand, we use the initializeOnce field as an indicator of whether or not we should reinitialize. Every call to awaitInitialized (which is called by all functions that need the IWL), passes through the initialization code. If a retry isn't necessary, this is a no-op, but if it is, we will call the initialization logic. Only the first attempt uses a detached context; subsequent attempts can be canceled by their contexts. To indicate that we should reinitialize, we call maybeReinitialize. Right now, we only call this when the go.mod file changes. In the future, we may need it in other cases. Fixes golang/go#38232 Change-Id: I77eefebb0ac38fbd0fe2c7da09c864eba45b075f Reviewed-on: https://go-review.googlesource.com/c/tools/+/242159 Run-TryBot: Rebecca Stambler <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Heschi Kreinick <[email protected]>
1 parent acdb8c1 commit 0a5cd10

File tree

5 files changed

+220
-63
lines changed

5 files changed

+220
-63
lines changed

internal/lsp/cache/session.go

+31-17
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,19 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
128128
backgroundCtx, cancel := context.WithCancel(baseCtx)
129129

130130
v := &View{
131-
session: s,
132-
initialized: make(chan struct{}),
133-
id: strconv.FormatInt(index, 10),
134-
options: options,
135-
baseCtx: baseCtx,
136-
backgroundCtx: backgroundCtx,
137-
cancel: cancel,
138-
name: name,
139-
folder: folder,
140-
filesByURI: make(map[span.URI]*fileBase),
141-
filesByBase: make(map[string][]*fileBase),
131+
session: s,
132+
initialized: make(chan struct{}),
133+
initializationSema: make(chan struct{}, 1),
134+
initializeOnce: &sync.Once{},
135+
id: strconv.FormatInt(index, 10),
136+
options: options,
137+
baseCtx: baseCtx,
138+
backgroundCtx: backgroundCtx,
139+
cancel: cancel,
140+
name: name,
141+
folder: folder,
142+
filesByURI: make(map[span.URI]*fileBase),
143+
filesByBase: make(map[string][]*fileBase),
142144
snapshot: &snapshot{
143145
id: snapshotID,
144146
packages: make(map[packageKey]*packageHandle),
@@ -171,8 +173,8 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
171173

172174
// Initialize the view without blocking.
173175
initCtx, initCancel := context.WithCancel(xcontext.Detach(ctx))
174-
v.initCancel = initCancel
175-
go v.initialize(initCtx, v.snapshot)
176+
v.initCancelFirstAttempt = initCancel
177+
go v.initialize(initCtx, v.snapshot, true)
176178
return v, v.snapshot, nil
177179
}
178180

@@ -324,7 +326,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif
324326
if err != nil {
325327
return nil, err
326328
}
327-
forceReloadMetadata := false
329+
var forceReloadMetadata bool
328330
for _, c := range changes {
329331
if c.Action == source.InvalidateMetadata {
330332
forceReloadMetadata = true
@@ -344,15 +346,27 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif
344346
if _, ok := views[view]; !ok {
345347
views[view] = make(map[span.URI]source.FileHandle)
346348
}
347-
if o, ok := overlays[c.URI]; ok {
348-
views[view][c.URI] = o
349+
var (
350+
fh source.FileHandle
351+
ok bool
352+
err error
353+
)
354+
if fh, ok = overlays[c.URI]; ok {
355+
views[view][c.URI] = fh
349356
} else {
350-
fh, err := s.cache.getFile(ctx, c.URI)
357+
fh, err = s.cache.getFile(ctx, c.URI)
351358
if err != nil {
352359
return nil, err
353360
}
354361
views[view][c.URI] = fh
355362
}
363+
// If the file change is to a go.mod file, and initialization for
364+
// the view has previously failed, we should attempt to retry.
365+
// TODO(rstambler): We can use unsaved contents with -modfile, so
366+
// maybe we should do that and retry on any change?
367+
if fh.Kind() == source.Mod && (c.OnDisk || c.Action == source.Save) {
368+
view.maybeReinitialize()
369+
}
356370
}
357371
}
358372
var snapshots []source.Snapshot

internal/lsp/cache/view.go

+68-19
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,30 @@ type View struct {
8686
snapshotMu sync.Mutex
8787
snapshot *snapshot
8888

89-
// initialized is closed when the view has been fully initialized.
90-
// On initialization, the view's workspace packages are loaded.
91-
// All of the fields below are set as part of initialization.
92-
// If we failed to load, we don't re-try to avoid too many go/packages calls.
93-
initializeOnce sync.Once
94-
initialized chan struct{}
95-
initCancel context.CancelFunc
96-
97-
// initializedErr needs no mutex, since any access to it happens after it
98-
// has been set.
99-
initializedErr error
89+
// initialized is closed when the view has been fully initialized. On
90+
// initialization, the view's workspace packages are loaded. All of the
91+
// fields below are set as part of initialization. If we failed to load, we
92+
// only retry if the go.mod file changes, to avoid too many go/packages
93+
// calls.
94+
//
95+
// When the view is created, initializeOnce is non-nil, initialized is
96+
// open, and initCancelFirstAttempt can be used to terminate
97+
// initialization. Once initialization completes, initializedErr may be set
98+
// and initializeOnce becomes nil. If initializedErr is non-nil,
99+
// initialization may be retried (depending on how files are changed). To
100+
// indicate that initialization should be retried, initializeOnce will be
101+
// set. The next time a caller requests workspace packages, the
102+
// initialization will retry.
103+
initialized chan struct{}
104+
initCancelFirstAttempt context.CancelFunc
105+
106+
// initializationSema is used as a mutex to guard initializeOnce and
107+
// initializedErr, which will be updated after each attempt to initialize
108+
// the view. We use a channel instead of a mutex to avoid blocking when a
109+
// context is canceled.
110+
initializationSema chan struct{}
111+
initializeOnce *sync.Once
112+
initializedErr error
100113

101114
// builtin pins the AST and package for builtin.go in memory.
102115
builtin *builtinPackageHandle
@@ -639,7 +652,7 @@ func (v *View) Shutdown(ctx context.Context) {
639652

640653
func (v *View) shutdown(ctx context.Context) {
641654
// Cancel the initial workspace load if it is still running.
642-
v.initCancel()
655+
v.initCancelFirstAttempt()
643656

644657
v.mu.Lock()
645658
defer v.mu.Unlock()
@@ -702,25 +715,48 @@ func (v *View) getSnapshot() *snapshot {
702715
return v.snapshot
703716
}
704717

705-
func (v *View) initialize(ctx context.Context, s *snapshot) {
706-
v.initializeOnce.Do(func() {
707-
defer close(v.initialized)
718+
func (v *View) initialize(ctx context.Context, s *snapshot, firstAttempt bool) {
719+
select {
720+
case <-ctx.Done():
721+
return
722+
case v.initializationSema <- struct{}{}:
723+
}
708724

709-
if err := s.load(ctx, viewLoadScope("LOAD_VIEW"), packagePath("builtin")); err != nil {
710-
if ctx.Err() != nil {
711-
return
725+
defer func() {
726+
<-v.initializationSema
727+
}()
728+
729+
if v.initializeOnce == nil {
730+
return
731+
}
732+
v.initializeOnce.Do(func() {
733+
defer func() {
734+
v.initializeOnce = nil
735+
if firstAttempt {
736+
close(v.initialized)
712737
}
713-
v.initializedErr = err
738+
}()
739+
740+
err := s.load(ctx, viewLoadScope("LOAD_VIEW"), packagePath("builtin"))
741+
if ctx.Err() != nil {
742+
return
743+
}
744+
if err != nil {
714745
event.Error(ctx, "initial workspace load failed", err)
715746
}
747+
v.initializedErr = err
716748
})
717749
}
718750

719751
func (v *View) awaitInitialized(ctx context.Context) {
720752
select {
721753
case <-ctx.Done():
754+
return
722755
case <-v.initialized:
723756
}
757+
// We typically prefer to run something as intensive as the IWL without
758+
// blocking. I'm not sure if there is a way to do that here.
759+
v.initialize(ctx, v.getSnapshot(), false)
724760
}
725761

726762
// invalidateContent invalidates the content of a Go file,
@@ -756,6 +792,19 @@ func (v *View) cancelBackground() {
756792
v.backgroundCtx, v.cancel = context.WithCancel(v.baseCtx)
757793
}
758794

795+
func (v *View) maybeReinitialize() {
796+
v.initializationSema <- struct{}{}
797+
defer func() {
798+
<-v.initializationSema
799+
}()
800+
801+
if v.initializedErr == nil {
802+
return
803+
}
804+
var once sync.Once
805+
v.initializeOnce = &once
806+
}
807+
759808
func (v *View) setBuildInformation(ctx context.Context, folder span.URI, env []string, modfileFlagEnabled bool) error {
760809
if err := checkPathCase(folder.Filename()); err != nil {
761810
return fmt.Errorf("invalid workspace configuration: %w", err)

internal/lsp/diagnostics.go

+41-24
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,16 @@ func (s *Server) diagnoseDetached(snapshot source.Snapshot) {
2828
ctx = xcontext.Detach(ctx)
2929
reports, shows := s.diagnose(ctx, snapshot, false)
3030
if shows != nil {
31-
// If a view has been created or the configuration changed, warn the user
31+
// If a view has been created or the configuration changed, warn the user.
3232
s.client.ShowMessage(ctx, shows)
3333
}
3434
s.publishReports(ctx, snapshot, reports)
3535
}
3636

3737
func (s *Server) diagnoseSnapshot(snapshot source.Snapshot) {
3838
ctx := snapshot.View().BackgroundContext()
39-
// Ignore possible workspace configuration warnings in the normal flow
39+
40+
// Ignore possible workspace configuration warnings in the normal flow.
4041
reports, _ := s.diagnose(ctx, snapshot, false)
4142
s.publishReports(ctx, snapshot, reports)
4243
}
@@ -67,7 +68,6 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysA
6768
if ctx.Err() != nil {
6869
return nil, nil
6970
}
70-
modURI := snapshot.View().ModFile()
7171
for id, diags := range reports {
7272
if id.URI == "" {
7373
event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename()))
@@ -82,27 +82,8 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysA
8282

8383
// Diagnose all of the packages in the workspace.
8484
wsPackages, err := snapshot.WorkspacePackages(ctx)
85-
if err == source.InconsistentVendoring {
86-
item, err := s.client.ShowMessageRequest(ctx, &protocol.ShowMessageRequestParams{
87-
Type: protocol.Error,
88-
Message: `Inconsistent vendoring detected. Please re-run "go mod vendor".
89-
See https://github.com/golang/go/issues/39164 for more detail on this issue.`,
90-
Actions: []protocol.MessageActionItem{
91-
{Title: "go mod vendor"},
92-
},
93-
})
94-
if item == nil || err != nil {
95-
return nil, nil
96-
}
97-
if err := s.directGoModCommand(ctx, protocol.URIFromSpanURI(modURI), "mod", []string{"vendor"}...); err != nil {
98-
return nil, &protocol.ShowMessageParams{
99-
Type: protocol.Error,
100-
Message: fmt.Sprintf(`"go mod vendor" failed with %v`, err),
101-
}
102-
}
103-
return nil, nil
104-
} else if err != nil {
105-
event.Error(ctx, "failed to load workspace packages, skipping diagnostics", err, tag.Snapshot.Of(snapshot.ID()), tag.Directory.Of(snapshot.View().Folder()))
85+
if err != nil {
86+
s.handleFatalErrors(ctx, snapshot, err)
10687
return nil, nil
10788
}
10889
var shows *protocol.ShowMessageParams
@@ -247,3 +228,39 @@ func toProtocolDiagnostics(diagnostics []*source.Diagnostic) []protocol.Diagnost
247228
}
248229
return reports
249230
}
231+
232+
func (s *Server) handleFatalErrors(ctx context.Context, snapshot source.Snapshot, err error) {
233+
switch err {
234+
case source.InconsistentVendoring:
235+
item, err := s.client.ShowMessageRequest(ctx, &protocol.ShowMessageRequestParams{
236+
Type: protocol.Error,
237+
Message: `Inconsistent vendoring detected. Please re-run "go mod vendor".
238+
See https://github.com/golang/go/issues/39164 for more detail on this issue.`,
239+
Actions: []protocol.MessageActionItem{
240+
{Title: "go mod vendor"},
241+
},
242+
})
243+
if item == nil || err != nil {
244+
event.Error(ctx, "go mod vendor ShowMessageRequest failed", err, tag.Directory.Of(snapshot.View().Folder()))
245+
return
246+
}
247+
modURI := snapshot.View().ModFile()
248+
if err := s.directGoModCommand(ctx, protocol.URIFromSpanURI(modURI), "mod", []string{"vendor"}...); err != nil {
249+
if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
250+
Type: protocol.Error,
251+
Message: fmt.Sprintf(`"go mod vendor" failed with %v`, err),
252+
}); err != nil {
253+
event.Error(ctx, "ShowMessage failed", err)
254+
}
255+
}
256+
default:
257+
msg := "failed to load workspace packages, skipping diagnostics"
258+
event.Error(ctx, msg, err, tag.Snapshot.Of(snapshot.ID()), tag.Directory.Of(snapshot.View().Folder()))
259+
if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
260+
Type: protocol.Error,
261+
Message: fmt.Sprintf("%s: %v", msg, err),
262+
}); err != nil {
263+
event.Error(ctx, "ShowMessage failed", err, tag.Directory.Of(snapshot.View().Folder()))
264+
}
265+
}
266+
}

internal/lsp/regtest/env.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -364,11 +364,13 @@ func EmptyShowMessage(title string) SimpleExpectation {
364364
}
365365
}
366366

367-
// SomeShowMessage asserts that the editor has received a ShowMessage.
367+
// SomeShowMessage asserts that the editor has received a ShowMessage with the given title.
368368
func SomeShowMessage(title string) SimpleExpectation {
369369
check := func(s State) (Verdict, interface{}) {
370-
if len(s.showMessage) > 0 {
371-
return Met, title
370+
for _, m := range s.showMessage {
371+
if strings.Contains(m.Message, title) {
372+
return Met, m
373+
}
372374
}
373375
return Unmet, nil
374376
}

0 commit comments

Comments
 (0)