1
0
mirror of https://github.com/golang/go synced 2024-10-01 07:28:35 -06:00
go/internal/lsp/cache/view.go
Ian Cottrell 0e55654012 internal/lsp: add shutdown handling
We correcly cancel all background tasks and drop all active views when
the server is asked to shut down now.
This was mostly to support the command line being able to exit cleanly

Change-Id: Iff9f5ab51572aad5e3245dc01aa87b00dcd47963
Reviewed-on: https://go-review.googlesource.com/c/tools/+/174940
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-05-15 23:16:32 +00:00

388 lines
9.5 KiB
Go

// Copyright 2018 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"
"fmt"
"go/ast"
"go/parser"
"go/token"
"go/types"
"os"
"sync"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/xlog"
"golang.org/x/tools/internal/span"
)
type view struct {
// mu protects all mutable state of the view.
mu sync.Mutex
// baseCtx is the context handed to NewView. This is the parent of all
// background contexts created for this view.
baseCtx context.Context
// backgroundCtx is the current context used by background tasks initiated
// by the view.
backgroundCtx context.Context
// cancel is called when all action being performed by the current view
// should be stopped.
cancel context.CancelFunc
// the logger to use to communicate back with the client
log xlog.Logger
// Name is the user visible name of this view.
name string
// Folder is the root of this view.
folder span.URI
// Config is the configuration used for the view's interaction with the
// go/packages API. It is shared across all views.
config packages.Config
// keep track of files by uri and by basename, a single file may be mapped
// to multiple uris, and the same basename may map to multiple files
filesByURI map[span.URI]*file
filesByBase map[string][]*file
// contentChanges saves the content changes for a given state of the view.
// When type information is requested by the view, all of the dirty changes
// are applied, potentially invalidating some data in the caches. The
// closures in the dirty slice assume that their caller is holding the
// view's mutex.
contentChanges map[span.URI]func()
// mcache caches metadata for the packages of the opened files in a view.
mcache *metadataCache
// pcache caches type information for the packages of the opened files in a view.
pcache *packageCache
// builtinPkg is the AST package used to resolve builtin types.
builtinPkg *ast.Package
}
type metadataCache struct {
mu sync.Mutex
packages map[string]*metadata
}
type metadata struct {
id, pkgPath, name string
files []string
typesSizes types.Sizes
parents, children map[string]bool
}
type packageCache struct {
mu sync.Mutex
packages map[string]*entry
}
type entry struct {
pkg *pkg
err error
ready chan struct{} // closed to broadcast ready condition
}
func NewView(ctx context.Context, log xlog.Logger, name string, folder span.URI, config *packages.Config) source.View {
backgroundCtx, cancel := context.WithCancel(ctx)
v := &view{
baseCtx: ctx,
backgroundCtx: backgroundCtx,
builtinPkg: builtinPkg(*config),
cancel: cancel,
log: log,
config: *config,
name: name,
folder: folder,
filesByURI: make(map[span.URI]*file),
filesByBase: make(map[string][]*file),
contentChanges: make(map[span.URI]func()),
mcache: &metadataCache{
packages: make(map[string]*metadata),
},
pcache: &packageCache{
packages: make(map[string]*entry),
},
}
return v
}
// Name returns the user visible name of this view.
func (v *view) Name() string {
return v.name
}
// Folder returns the root of this view.
func (v *view) Folder() span.URI {
return v.folder
}
// Config returns the configuration used for the view's interaction with the
// go/packages API. It is shared across all views.
func (v *view) Config() packages.Config {
return v.config
}
func (v *view) SetEnv(env []string) {
v.config.Env = env
}
func (v *view) Shutdown(context.Context) {
v.mu.Lock()
defer v.mu.Unlock()
if v.cancel != nil {
v.cancel()
v.cancel = nil
}
}
func (v *view) BackgroundContext() context.Context {
v.mu.Lock()
defer v.mu.Unlock()
return v.backgroundCtx
}
func (v *view) BuiltinPackage() *ast.Package {
return v.builtinPkg
}
func builtinPkg(cfg packages.Config) *ast.Package {
var bpkg *ast.Package
cfg.Mode = packages.LoadFiles
pkgs, _ := packages.Load(&cfg, "builtin")
if len(pkgs) != 1 {
bpkg, _ = ast.NewPackage(cfg.Fset, nil, nil, nil)
return bpkg
}
pkg := pkgs[0]
files := make(map[string]*ast.File)
for _, filename := range pkg.GoFiles {
file, err := parser.ParseFile(cfg.Fset, filename, nil, parser.ParseComments)
if err != nil {
bpkg, _ = ast.NewPackage(cfg.Fset, nil, nil, nil)
return bpkg
}
files[filename] = file
}
bpkg, _ = ast.NewPackage(cfg.Fset, files, nil, nil)
return bpkg
}
func (v *view) FileSet() *token.FileSet {
return v.config.Fset
}
// SetContent sets the overlay contents for a file.
func (v *view) SetContent(ctx context.Context, uri span.URI, content []byte) error {
v.mu.Lock()
defer v.mu.Unlock()
// Cancel all still-running previous requests, since they would be
// operating on stale data.
v.cancel()
v.backgroundCtx, v.cancel = context.WithCancel(v.baseCtx)
v.contentChanges[uri] = func() {
v.applyContentChange(uri, content)
}
return nil
}
// applyContentChanges applies all of the changed content stored in the view.
// It is assumed that the caller has locked both the view's and the mcache's
// mutexes.
func (v *view) applyContentChanges(ctx context.Context) error {
if ctx.Err() != nil {
return ctx.Err()
}
v.pcache.mu.Lock()
defer v.pcache.mu.Unlock()
for uri, change := range v.contentChanges {
change()
delete(v.contentChanges, uri)
}
return nil
}
// setContent applies a content update for a given file. It assumes that the
// caller is holding the view's mutex.
func (v *view) applyContentChange(uri span.URI, content []byte) {
f, err := v.getFile(uri)
if err != nil {
return
}
f.content = content
// TODO(rstambler): Should we recompute these here?
f.ast = nil
f.token = nil
// Remove the package and all of its reverse dependencies from the cache.
if f.pkg != nil {
v.remove(f.pkg.pkgPath, map[string]struct{}{})
}
switch {
case f.active && content == nil:
// The file was active, so we need to forget its content.
f.active = false
delete(f.view.config.Overlay, f.filename)
f.content = nil
case content != nil:
// This is an active overlay, so we update the map.
f.active = true
f.view.config.Overlay[f.filename] = f.content
}
}
// remove invalidates a package and its reverse dependencies in the view's
// package cache. It is assumed that the caller has locked both the mutexes
// of both the mcache and the pcache.
func (v *view) remove(pkgPath string, seen map[string]struct{}) {
if _, ok := seen[pkgPath]; ok {
return
}
m, ok := v.mcache.packages[pkgPath]
if !ok {
return
}
seen[pkgPath] = struct{}{}
for parentPkgPath := range m.parents {
v.remove(parentPkgPath, seen)
}
// All of the files in the package may also be holding a pointer to the
// invalidated package.
for _, filename := range m.files {
if f, _ := v.findFile(span.FileURI(filename)); f != nil {
f.pkg = nil
}
}
delete(v.pcache.packages, pkgPath)
}
// FindFile returns the file if the given URI is already a part of the view.
func (v *view) FindFile(ctx context.Context, uri span.URI) *file {
v.mu.Lock()
defer v.mu.Unlock()
f, err := v.findFile(uri)
if err != nil {
return nil
}
return f
}
// GetFile returns a File for the given URI. It will always succeed because it
// adds the file to the managed set if needed.
func (v *view) GetFile(ctx context.Context, uri span.URI) (source.File, error) {
v.mu.Lock()
defer v.mu.Unlock()
if ctx.Err() != nil {
return nil, ctx.Err()
}
return v.getFile(uri)
}
// getFile is the unlocked internal implementation of GetFile.
func (v *view) getFile(uri span.URI) (*file, error) {
filename, err := uri.Filename()
if err != nil {
return nil, err
}
if v.isIgnored(filename) {
return nil, fmt.Errorf("%s is ignored", filename)
}
if f, err := v.findFile(uri); err != nil {
return nil, err
} else if f != nil {
return f, nil
}
f := &file{
view: v,
filename: filename,
}
v.mapFile(uri, f)
return f, nil
}
// isIgnored checks if the given filename is a file we ignore.
// As of right now, we only ignore files in the "builtin" package.
func (v *view) isIgnored(filename string) bool {
bpkg := v.BuiltinPackage()
if bpkg != nil {
for builtinFilename := range bpkg.Files {
if filename == builtinFilename {
return true
}
}
}
return false
}
// findFile checks the cache for any file matching the given uri.
//
// An error is only returned for an irreparable failure, for example, if the
// filename in question does not exist.
func (v *view) findFile(uri span.URI) (*file, error) {
if f := v.filesByURI[uri]; f != nil {
// a perfect match
return f, nil
}
// no exact match stored, time to do some real work
// check for any files with the same basename
fname, err := uri.Filename()
if err != nil {
return nil, err
}
basename := basename(fname)
if candidates := v.filesByBase[basename]; candidates != nil {
pathStat, err := os.Stat(fname)
if os.IsNotExist(err) {
return nil, err
} else if err != nil {
return nil, nil // the file may exist, return without an error
}
for _, c := range candidates {
if cStat, err := os.Stat(c.filename); err == nil {
if os.SameFile(pathStat, cStat) {
// same file, map it
v.mapFile(uri, c)
return c, nil
}
}
}
}
// no file with a matching name was found, it wasn't in our cache
return nil, nil
}
func (v *view) mapFile(uri span.URI, f *file) {
v.filesByURI[uri] = f
f.uris = append(f.uris, uri)
if f.basename == "" {
f.basename = basename(f.filename)
v.filesByBase[f.basename] = append(v.filesByBase[f.basename], f)
}
}
func (v *view) Logger() xlog.Logger {
return v.log
}