blob: 794af9564ae9a195331d507136306a2fc1a821e0 [file] [log] [blame]
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cache
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"golang.org/x/tools/gopls/internal/bug"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source/typerefs"
"golang.org/x/tools/gopls/internal/persistent"
"golang.org/x/tools/gopls/internal/vulncheck"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/memoize"
"golang.org/x/tools/internal/xcontext"
)
type Session struct {
// Unique identifier for this session.
id string
// Immutable attributes shared across views.
cache *Cache // shared cache
gocmdRunner *gocommand.Runner // limits go command concurrency
viewMu sync.Mutex
views []*View
viewMap map[protocol.DocumentURI]*View // file->best view
parseCache *parseCache
*overlayFS
}
// ID returns the unique identifier for this session on this server.
func (s *Session) ID() string { return s.id }
func (s *Session) String() string { return s.id }
// GoCommandRunner returns the gocommand Runner for this session.
func (s *Session) GoCommandRunner() *gocommand.Runner {
return s.gocmdRunner
}
// Shutdown the session and all views it has created.
func (s *Session) Shutdown(ctx context.Context) {
var views []*View
s.viewMu.Lock()
views = append(views, s.views...)
s.views = nil
s.viewMap = nil
s.viewMu.Unlock()
for _, view := range views {
view.shutdown()
}
s.parseCache.stop()
event.Log(ctx, "Shutdown session", KeyShutdownSession.Of(s))
}
// Cache returns the cache that created this session, for debugging only.
func (s *Session) Cache() *Cache {
return s.cache
}
// TODO(rfindley): is the logic surrounding this error actually necessary?
var ErrViewExists = errors.New("view already exists for session")
// NewView creates a new View, returning it and its first snapshot. If a
// non-empty tempWorkspace directory is provided, the View will record a copy
// of its gopls workspace module in that directory, so that client tooling
// can execute in the same main module. On success it also returns a release
// function that must be called when the Snapshot is no longer needed.
func (s *Session) NewView(ctx context.Context, folder *Folder) (*View, *Snapshot, func(), error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
// Querying the file system to check whether
// two folders denote the same existing directory.
if inode1, err := os.Stat(filepath.FromSlash(folder.Dir.Path())); err == nil {
for _, view := range s.views {
inode2, err := os.Stat(filepath.FromSlash(view.folder.Dir.Path()))
if err == nil && os.SameFile(inode1, inode2) {
return nil, nil, nil, ErrViewExists
}
}
}
def, err := getViewDefinition(ctx, s.gocmdRunner, s, folder)
if err != nil {
return nil, nil, nil, err
}
view, snapshot, release, err := s.createView(ctx, def, folder, 0)
if err != nil {
return nil, nil, nil, err
}
s.views = append(s.views, view)
// we always need to drop the view map
s.viewMap = make(map[protocol.DocumentURI]*View)
return view, snapshot, release, nil
}
// TODO(rfindley): clarify that createView can never be cancelled (with the
// possible exception of server shutdown).
// On success, the caller becomes responsible for calling the release function once.
func (s *Session) createView(ctx context.Context, def *viewDefinition, folder *Folder, seqID uint64) (*View, *Snapshot, func(), error) {
index := atomic.AddInt64(&viewIndex, 1)
// We want a true background context and not a detached context here
// the spans need to be unrelated and no tag values should pollute it.
baseCtx := event.Detach(xcontext.Detach(ctx))
backgroundCtx, cancel := context.WithCancel(baseCtx)
v := &View{
id: strconv.FormatInt(index, 10),
gocmdRunner: s.gocmdRunner,
folder: folder,
initialWorkspaceLoad: make(chan struct{}),
initializationSema: make(chan struct{}, 1),
baseCtx: baseCtx,
parseCache: s.parseCache,
fs: s.overlayFS,
viewDefinition: def,
}
v.importsState = &importsState{
ctx: backgroundCtx,
processEnv: &imports.ProcessEnv{
GocmdRunner: s.gocmdRunner,
SkipPathInScan: func(dir string) bool {
prefix := strings.TrimSuffix(string(v.folder.Dir), "/") + "/"
uri := strings.TrimSuffix(string(protocol.URIFromPath(dir)), "/")
if !strings.HasPrefix(uri+"/", prefix) {
return false
}
filterer := NewFilterer(folder.Options.DirectoryFilters)
rel := strings.TrimPrefix(uri, prefix)
disallow := filterer.Disallow(rel)
return disallow
},
},
}
v.snapshot = &Snapshot{
sequenceID: seqID,
globalID: nextSnapshotID(),
view: v,
backgroundCtx: backgroundCtx,
cancel: cancel,
store: s.cache.store,
packages: new(persistent.Map[PackageID, *packageHandle]),
meta: new(metadataGraph),
files: newFileMap(),
activePackages: new(persistent.Map[PackageID, *Package]),
symbolizeHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
shouldLoad: new(persistent.Map[PackageID, []PackagePath]),
unloadableFiles: new(persistent.Set[protocol.DocumentURI]),
parseModHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
parseWorkHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
modTidyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
modVulnHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
modWhyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
pkgIndex: typerefs.NewPackageIndex(),
moduleUpgrades: new(persistent.Map[protocol.DocumentURI, map[string]string]),
vulns: new(persistent.Map[protocol.DocumentURI, *vulncheck.Result]),
}
// Save one reference in the view.
v.releaseSnapshot = v.snapshot.Acquire()
// Record the environment of the newly created view in the log.
event.Log(ctx, viewEnv(v))
// Initialize the view without blocking.
initCtx, initCancel := context.WithCancel(xcontext.Detach(ctx))
v.initCancelFirstAttempt = initCancel
snapshot := v.snapshot
// Pass a second reference to the background goroutine.
bgRelease := snapshot.Acquire()
go func() {
defer bgRelease()
snapshot.initialize(initCtx, true)
}()
// Return a third reference to the caller.
return v, snapshot, snapshot.Acquire(), nil
}
// ViewByName returns a view with a matching name, if the session has one.
func (s *Session) ViewByName(name string) *View {
s.viewMu.Lock()
defer s.viewMu.Unlock()
for _, view := range s.views {
if view.Name() == name {
return view
}
}
return nil
}
// View returns the view with a matching id, if present.
func (s *Session) View(id string) (*View, error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
for _, view := range s.views {
if view.ID() == id {
return view, nil
}
}
return nil, fmt.Errorf("no view with ID %q", id)
}
// ViewOf returns a view corresponding to the given URI.
// If the file is not already associated with a view, pick one using some heuristics.
func (s *Session) ViewOf(uri protocol.DocumentURI) (*View, error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
return s.viewOfLocked(uri)
}
// Precondition: caller holds s.viewMu lock.
func (s *Session) viewOfLocked(uri protocol.DocumentURI) (*View, error) {
// Check if we already know this file.
if v, found := s.viewMap[uri]; found {
return v, nil
}
// Pick the best view for this file and memoize the result.
if len(s.views) == 0 {
return nil, fmt.Errorf("no views in session")
}
s.viewMap[uri] = bestViewForURI(uri, s.views)
return s.viewMap[uri], nil
}
func (s *Session) Views() []*View {
s.viewMu.Lock()
defer s.viewMu.Unlock()
result := make([]*View, len(s.views))
copy(result, s.views)
return result
}
// bestViewForURI returns the most closely matching view for the given URI
// out of the given set of views.
func bestViewForURI(uri protocol.DocumentURI, views []*View) *View {
// we need to find the best view for this file
var longest *View
for _, view := range views {
if longest != nil && len(longest.folder.Dir) > len(view.folder.Dir) {
continue
}
// TODO(rfindley): this should consider the workspace layout (i.e.
// go.work).
if view.contains(uri) {
longest = view
}
}
if longest != nil {
return longest
}
// Try our best to return a view that knows the file.
for _, view := range views {
if view.knownFile(uri) {
return view
}
}
// TODO: are there any more heuristics we can use?
return views[0]
}
// RemoveView removes the view v from the session
func (s *Session) RemoveView(view *View) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
i := s.dropView(view)
if i == -1 { // error reported elsewhere
return
}
// delete this view... we don't care about order but we do want to make
// sure we can garbage collect the view
s.views = removeElement(s.views, i)
}
// updateViewLocked recreates the view with the given options.
//
// If the resulting error is non-nil, the view may or may not have already been
// dropped from the session.
func (s *Session) updateViewLocked(ctx context.Context, view *View, def *viewDefinition, folder *Folder) (*View, error) {
// Preserve the snapshot ID if we are recreating the view.
view.snapshotMu.Lock()
if view.snapshot == nil {
view.snapshotMu.Unlock()
panic("updateView called after View was already shut down")
}
// TODO(rfindley): we should probably increment the sequence ID here.
seqID := view.snapshot.sequenceID // Preserve sequence IDs when updating a view in place.
view.snapshotMu.Unlock()
i := s.dropView(view)
if i == -1 {
return nil, fmt.Errorf("view %q not found", view.id)
}
var (
snapshot *Snapshot
release func()
err error
)
view, snapshot, release, err = s.createView(ctx, def, folder, seqID)
if err != nil {
// we have dropped the old view, but could not create the new one
// this should not happen and is very bad, but we still need to clean
// up the view array if it happens
s.views = removeElement(s.views, i)
return nil, err
}
defer release()
// The new snapshot has lost the history of the previous view. As a result,
// it may not see open files that aren't in its build configuration (as it
// would have done via didOpen notifications). This can lead to inconsistent
// behavior when configuration is changed mid-session.
//
// Ensure the new snapshot observes all open files.
for _, o := range view.fs.Overlays() {
_, _ = snapshot.ReadFile(ctx, o.URI())
}
// substitute the new view into the array where the old view was
s.views[i] = view
return view, nil
}
// removeElement removes the ith element from the slice replacing it with the last element.
// TODO(adonovan): generics, someday.
func removeElement(slice []*View, index int) []*View {
last := len(slice) - 1
slice[index] = slice[last]
slice[last] = nil // aid GC
return slice[:last]
}
// dropView removes v from the set of views for the receiver s and calls
// v.shutdown, returning the index of v in s.views (if found), or -1 if v was
// not found. s.viewMu must be held while calling this function.
func (s *Session) dropView(v *View) int {
// we always need to drop the view map
s.viewMap = make(map[protocol.DocumentURI]*View)
for i := range s.views {
if v == s.views[i] {
// we found the view, drop it and return the index it was found at
s.views[i] = nil
v.shutdown()
return i
}
}
// TODO(rfindley): it looks wrong that we don't shutdown v in this codepath.
// We should never get here.
bug.Reportf("tried to drop nonexistent view %q", v.id)
return -1
}
// ResetView resets the best view for the given URI.
func (s *Session) ResetView(ctx context.Context, uri protocol.DocumentURI) (*View, error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
v := bestViewForURI(uri, s.views)
return s.updateViewLocked(ctx, v, v.viewDefinition, v.folder)
}
// DidModifyFiles reports a file modification to the session. It returns
// the new snapshots after the modifications have been applied, paired with
// the affected file URIs for those snapshots.
// On success, it returns a release function that
// must be called when the snapshots are no longer needed.
//
// TODO(rfindley): what happens if this function fails? It must leave us in a
// broken state, which we should surface to the user, probably as a request to
// restart gopls.
func (s *Session) DidModifyFiles(ctx context.Context, changes []file.Modification) (map[*Snapshot][]protocol.DocumentURI, func(), error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
// Update overlays.
//
// TODO(rfindley): I think we do this while holding viewMu to prevent views
// from seeing the updated file content before they have processed
// invalidations, which could lead to a partial view of the changes (i.e.
// spurious diagnostics). However, any such view would immediately be
// invalidated here, so it is possible that we could update overlays before
// acquiring viewMu.
if err := s.updateOverlays(ctx, changes); err != nil {
return nil, nil, err
}
// Re-create views whose definition may have changed.
//
// checkViews controls whether to re-evaluate view definitions when
// collecting views below. Any addition or deletion of a go.mod or go.work
// file may have affected the definition of the view.
checkViews := false
for _, c := range changes {
// Any on-disk change to a go.work file causes a re-diagnosis.
//
// TODO(rfindley): go.work files need not be named "go.work" -- we need to
// check each view's source to handle the case of an explicit GOWORK value.
// Write a test that fails, and fix this.
if isGoWork(c.URI) && (c.Action == file.Save || c.OnDisk) {
checkViews = true
break
}
// Opening/Close/Create/Delete of go.mod files all trigger
// re-evaluation of Views. Changes do not as they can't affect the set of
// Views.
if isGoMod(c.URI) && c.Action != file.Change && c.Action != file.Save {
checkViews = true
break
}
}
if checkViews {
for _, view := range s.views {
// TODO(rfindley): can we avoid running the go command (go env)
// synchronously to change processing? Can we assume that the env did not
// change, and derive go.work using a combination of the configured
// GOWORK value and filesystem?
info, err := getViewDefinition(ctx, s.gocmdRunner, s, view.folder)
if err != nil {
// Catastrophic failure, equivalent to a failure of session
// initialization and therefore should almost never happen. One
// scenario where this failure mode could occur is if some file
// permissions have changed preventing us from reading go.mod
// files.
//
// TODO(rfindley): consider surfacing this error more loudly. We
// could report a bug, but it's not really a bug.
event.Error(ctx, "fetching workspace information", err)
} else if !viewDefinitionsEqual(view.viewDefinition, info) {
if _, err := s.updateViewLocked(ctx, view, info, view.folder); err != nil {
// More catastrophic failure. The view may or may not still exist.
// The best we can do is log and move on.
event.Error(ctx, "recreating view", err)
}
}
}
}
// Collect information about views affected by these changes.
views := make(map[*View]map[protocol.DocumentURI]file.Handle)
affectedViews := map[protocol.DocumentURI][]*View{}
for _, c := range changes {
// Build the list of affected views.
var changedViews []*View
for _, view := range s.views {
// Don't propagate changes that are outside of the view's scope
// or knowledge.
if !view.relevantChange(c) {
continue
}
changedViews = append(changedViews, view)
}
// If the change is not relevant to any view, but the change is
// happening in the editor, assign it the most closely matching view.
if len(changedViews) == 0 {
if c.OnDisk {
continue
}
bestView, err := s.viewOfLocked(c.URI)
if err != nil {
return nil, nil, err
}
changedViews = append(changedViews, bestView)
}
affectedViews[c.URI] = changedViews
// Apply the changes to all affected views.
fh := mustReadFile(ctx, s, c.URI)
for _, view := range changedViews {
// Make sure that the file is added to the view's seenFiles set.
view.markKnown(c.URI)
if _, ok := views[view]; !ok {
views[view] = make(map[protocol.DocumentURI]file.Handle)
}
views[view][c.URI] = fh
}
}
var releases []func()
viewToSnapshot := make(map[*View]*Snapshot)
for view, changed := range views {
snapshot, release := view.Invalidate(ctx, StateChange{Files: changed})
releases = append(releases, release)
viewToSnapshot[view] = snapshot
}
// The release function is called when the
// returned URIs no longer need to be valid.
release := func() {
for _, release := range releases {
release()
}
}
// We only want to diagnose each changed file once, in the view to which
// it "most" belongs. We do this by picking the best view for each URI,
// and then aggregating the set of snapshots and their URIs (to avoid
// diagnosing the same snapshot multiple times).
snapshotURIs := map[*Snapshot][]protocol.DocumentURI{}
for _, mod := range changes {
viewSlice, ok := affectedViews[mod.URI]
if !ok || len(viewSlice) == 0 {
continue
}
view := bestViewForURI(mod.URI, viewSlice)
snapshot, ok := viewToSnapshot[view]
if !ok {
panic(fmt.Sprintf("no snapshot for view %s", view.folder.Dir))
}
snapshotURIs[snapshot] = append(snapshotURIs[snapshot], mod.URI)
}
return snapshotURIs, release, nil
}
// ExpandModificationsToDirectories returns the set of changes with the
// directory changes removed and expanded to include all of the files in
// the directory.
func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []file.Modification) []file.Modification {
var snapshots []*Snapshot
s.viewMu.Lock()
for _, v := range s.views {
snapshot, release, err := v.Snapshot()
if err != nil {
continue // view is shut down; continue with others
}
defer release()
snapshots = append(snapshots, snapshot)
}
s.viewMu.Unlock()
// Expand the modification to any file we could care about, which we define
// to be any file observed by any of the snapshots.
//
// There may be other files in the directory, but if we haven't read them yet
// we don't need to invalidate them.
var result []file.Modification
for _, c := range changes {
expanded := make(map[protocol.DocumentURI]bool)
for _, snapshot := range snapshots {
for _, uri := range snapshot.filesInDir(c.URI) {
expanded[uri] = true
}
}
if len(expanded) == 0 {
result = append(result, c)
} else {
for uri := range expanded {
result = append(result, file.Modification{
URI: uri,
Action: c.Action,
LanguageID: "",
OnDisk: c.OnDisk,
// changes to directories cannot include text or versions
})
}
}
}
return result
}
// Precondition: caller holds s.viewMu lock.
// TODO(rfindley): move this to fs_overlay.go.
func (fs *overlayFS) updateOverlays(ctx context.Context, changes []file.Modification) error {
fs.mu.Lock()
defer fs.mu.Unlock()
for _, c := range changes {
o, ok := fs.overlays[c.URI]
// If the file is not opened in an overlay and the change is on disk,
// there's no need to update an overlay. If there is an overlay, we
// may need to update the overlay's saved value.
if !ok && c.OnDisk {
continue
}
// Determine the file kind on open, otherwise, assume it has been cached.
var kind file.Kind
switch c.Action {
case file.Open:
kind = file.KindForLang(c.LanguageID)
default:
if !ok {
return fmt.Errorf("updateOverlays: modifying unopened overlay %v", c.URI)
}
kind = o.kind
}
// Closing a file just deletes its overlay.
if c.Action == file.Close {
delete(fs.overlays, c.URI)
continue
}
// If the file is on disk, check if its content is the same as in the
// overlay. Saves and on-disk file changes don't come with the file's
// content.
text := c.Text
if text == nil && (c.Action == file.Save || c.OnDisk) {
if !ok {
return fmt.Errorf("no known content for overlay for %s", c.Action)
}
text = o.content
}
// On-disk changes don't come with versions.
version := c.Version
if c.OnDisk || c.Action == file.Save {
version = o.version
}
hash := file.HashOf(text)
var sameContentOnDisk bool
switch c.Action {
case file.Delete:
// Do nothing. sameContentOnDisk should be false.
case file.Save:
// Make sure the version and content (if present) is the same.
if false && o.version != version { // Client no longer sends the version
return fmt.Errorf("updateOverlays: saving %s at version %v, currently at %v", c.URI, c.Version, o.version)
}
if c.Text != nil && o.hash != hash {
return fmt.Errorf("updateOverlays: overlay %s changed on save", c.URI)
}
sameContentOnDisk = true
default:
fh := mustReadFile(ctx, fs.delegate, c.URI)
_, readErr := fh.Content()
sameContentOnDisk = (readErr == nil && fh.Identity().Hash == hash)
}
o = &Overlay{
uri: c.URI,
version: version,
content: text,
kind: kind,
hash: hash,
saved: sameContentOnDisk,
}
// NOTE: previous versions of this code checked here that the overlay had a
// view and file kind (but we don't know why).
fs.overlays[c.URI] = o
}
return nil
}
func mustReadFile(ctx context.Context, fs file.Source, uri protocol.DocumentURI) file.Handle {
ctx = xcontext.Detach(ctx)
fh, err := fs.ReadFile(ctx, uri)
if err != nil {
// ReadFile cannot fail with an uncancellable context.
bug.Reportf("reading file failed unexpectedly: %v", err)
return brokenFile{uri, err}
}
return fh
}
// A brokenFile represents an unexpected failure to read a file.
type brokenFile struct {
uri protocol.DocumentURI
err error
}
func (b brokenFile) URI() protocol.DocumentURI { return b.uri }
func (b brokenFile) Identity() file.Identity { return file.Identity{URI: b.uri} }
func (b brokenFile) SameContentsOnDisk() bool { return false }
func (b brokenFile) Version() int32 { return 0 }
func (b brokenFile) Content() ([]byte, error) { return nil, b.err }
// FileWatchingGlobPatterns returns a new set of glob patterns to
// watch every directory known by the view. For views within a module,
// this is the module root, any directory in the module root, and any
// replace targets.
func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]struct{} {
s.viewMu.Lock()
defer s.viewMu.Unlock()
patterns := map[string]struct{}{}
for _, view := range s.views {
snapshot, release, err := view.Snapshot()
if err != nil {
continue // view is shut down; continue with others
}
for k, v := range snapshot.fileWatchingGlobPatterns(ctx) {
patterns[k] = v
}
release()
}
return patterns
}