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>
diff --git a/gopls/internal/regtest/runner.go b/gopls/internal/regtest/runner.go
index 9f8a779..8c5efe0 100644
--- a/gopls/internal/regtest/runner.go
+++ b/gopls/internal/regtest/runner.go
@@ -125,6 +125,12 @@
})
}
+func SendPID() RunOption {
+ return optionSetter(func(opts *runConfig) {
+ opts.editor.SendPID = true
+ })
+}
+
// EditorConfig is a RunOption option that configured the regtest editor.
type EditorConfig fake.EditorConfig
diff --git a/gopls/internal/regtest/workspace_test.go b/gopls/internal/regtest/workspace_test.go
index 9eaff55..1887ab4 100644
--- a/gopls/internal/regtest/workspace_test.go
+++ b/gopls/internal/regtest/workspace_test.go
@@ -6,6 +6,9 @@
import (
"fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
"strings"
"testing"
@@ -563,3 +566,65 @@
)
})
}
+
+func TestWorkspaceDirAccess(t *testing.T) {
+ const multiModule = `
+-- moda/a/go.mod --
+module a.com
+
+-- moda/a/a.go --
+package main
+
+func main() {
+ fmt.Println("Hello")
+}
+-- modb/go.mod --
+module b.com
+-- modb/b/b.go --
+package main
+
+func main() {
+ fmt.Println("World")
+}
+`
+ withOptions(
+ WithModes(Experimental),
+ SendPID(),
+ ).run(t, multiModule, func(t *testing.T, env *Env) {
+ pid := os.Getpid()
+ // Don't factor this out of Server.addFolders. vscode-go expects this
+ // directory.
+ modPath := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.workspace", pid), "go.mod")
+ gotb, err := ioutil.ReadFile(modPath)
+ if err != nil {
+ t.Fatalf("reading expected workspace modfile: %v", err)
+ }
+ got := string(gotb)
+ for _, want := range []string{"a.com v0.0.0-goplsworkspace", "b.com v0.0.0-goplsworkspace"} {
+ if !strings.Contains(got, want) {
+ // want before got here, since the go.mod is multi-line
+ t.Fatalf("workspace go.mod missing %q. got:\n%s", want, got)
+ }
+ }
+ workdir := env.Sandbox.Workdir.RootURI().SpanURI().Filename()
+ env.WriteWorkspaceFile("gopls.mod", fmt.Sprintf(`
+ module gopls-workspace
+
+ require (
+ a.com v0.0.0-goplsworkspace
+ )
+
+ replace a.com => %s/moda/a
+ `, workdir))
+ env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1))
+ gotb, err = ioutil.ReadFile(modPath)
+ if err != nil {
+ t.Fatalf("reading expected workspace modfile: %v", err)
+ }
+ got = string(gotb)
+ want := "b.com v0.0.0-goplsworkspace"
+ if strings.Contains(got, want) {
+ t.Fatalf("workspace go.mod contains unexpected %q. got:\n%s", want, got)
+ }
+ })
+}
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 4afcb79..36bfc50 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -7,6 +7,7 @@
import (
"context"
"fmt"
+ "os"
"strconv"
"strings"
"sync"
@@ -143,10 +144,10 @@
return s.cache
}
-func (s *Session) NewView(ctx context.Context, name string, folder span.URI, options *source.Options) (source.View, source.Snapshot, func(), error) {
+func (s *Session) NewView(ctx context.Context, name string, folder, tempWorkspace span.URI, options *source.Options) (source.View, source.Snapshot, func(), error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
- view, snapshot, release, err := s.createView(ctx, name, folder, options, 0)
+ view, snapshot, release, err := s.createView(ctx, name, folder, tempWorkspace, options, 0)
if err != nil {
return nil, nil, func() {}, err
}
@@ -156,7 +157,7 @@
return view, snapshot, release, nil
}
-func (s *Session) createView(ctx context.Context, name string, folder span.URI, options *source.Options, snapshotID uint64) (*View, *snapshot, func(), error) {
+func (s *Session) createView(ctx context.Context, name string, folder, tempWorkspace span.URI, options *source.Options, snapshotID uint64) (*View, *snapshot, func(), error) {
index := atomic.AddInt64(&viewIndex, 1)
if s.cache.options != nil {
@@ -182,6 +183,8 @@
v := &View{
session: s,
+ initialWorkspaceLoad: make(chan struct{}),
+ initializationSema: make(chan struct{}, 1),
id: strconv.FormatInt(index, 10),
options: options,
baseCtx: baseCtx,
@@ -192,6 +195,7 @@
filesByURI: make(map[span.URI]*fileBase),
filesByBase: make(map[string][]*fileBase),
workspaceInformation: *ws,
+ tempWorkspace: tempWorkspace,
}
v.importsState = &importsState{
ctx: backgroundCtx,
@@ -202,26 +206,24 @@
},
}
v.snapshot = &snapshot{
- id: snapshotID,
- view: v,
- initialized: make(chan struct{}),
- initializationSema: make(chan struct{}, 1),
- initializeOnce: &sync.Once{},
- generation: s.cache.store.Generation(generationName(v, 0)),
- packages: make(map[packageKey]*packageHandle),
- ids: make(map[span.URI][]packageID),
- metadata: make(map[packageID]*metadata),
- files: make(map[span.URI]source.VersionedFileHandle),
- goFiles: make(map[parseKey]*parseGoHandle),
- importedBy: make(map[packageID][]packageID),
- actions: make(map[actionKey]*actionHandle),
- workspacePackages: make(map[packageID]packagePath),
- unloadableFiles: make(map[span.URI]struct{}),
- parseModHandles: make(map[span.URI]*parseModHandle),
- modTidyHandles: make(map[span.URI]*modTidyHandle),
- modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
- modWhyHandles: make(map[span.URI]*modWhyHandle),
- workspace: workspace,
+ id: snapshotID,
+ view: v,
+ initializeOnce: &sync.Once{},
+ generation: s.cache.store.Generation(generationName(v, 0)),
+ packages: make(map[packageKey]*packageHandle),
+ ids: make(map[span.URI][]packageID),
+ metadata: make(map[packageID]*metadata),
+ files: make(map[span.URI]source.VersionedFileHandle),
+ goFiles: make(map[parseKey]*parseGoHandle),
+ importedBy: make(map[packageID][]packageID),
+ actions: make(map[actionKey]*actionHandle),
+ workspacePackages: make(map[packageID]packagePath),
+ unloadableFiles: make(map[span.URI]struct{}),
+ parseModHandles: make(map[span.URI]*parseModHandle),
+ modTidyHandles: make(map[span.URI]*modTidyHandle),
+ modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
+ modWhyHandles: make(map[span.URI]*modWhyHandle),
+ workspace: workspace,
}
// Initialize the view without blocking.
@@ -231,6 +233,19 @@
release := snapshot.generation.Acquire(initCtx)
go func() {
snapshot.initialize(initCtx, true)
+ if v.tempWorkspace != "" {
+ var err error
+ if err = os.Mkdir(v.tempWorkspace.Filename(), 0700); err == nil {
+ var wsdir span.URI
+ wsdir, err = snapshot.getWorkspaceDir(initCtx)
+ if err == nil {
+ err = copyWorkspace(v.tempWorkspace, wsdir)
+ }
+ }
+ if err != nil {
+ event.Error(initCtx, "creating workspace dir", err)
+ }
+ }
release()
}()
return v, snapshot, snapshot.generation.Acquire(ctx), nil
@@ -349,7 +364,7 @@
view.snapshotMu.Lock()
snapshotID := view.snapshot.id
view.snapshotMu.Unlock()
- v, _, release, err := s.createView(ctx, view.name, view.folder, options, snapshotID)
+ v, _, release, err := s.createView(ctx, view.name, view.folder, view.tempWorkspace, options, snapshotID)
release()
if err != nil {
// we have dropped the old view, but could not create the new one
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 15b9855..045415f 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -47,31 +47,15 @@
builtin *builtinPackageHandle
// The snapshot's initialization state is controlled by the fields below.
- // These fields are propagated across snapshots to avoid multiple
- // concurrent initializations. They may be invalidated during cloning.
//
- // initialized is closed when the snapshot has been fully initialized. On
- // initialization, the snapshot's workspace packages are loaded. All of the
- // fields below are set as part of initialization. If we failed to load, we
- // only retry if the go.mod file changes, to avoid too many go/packages
- // calls.
- //
- // When the view is created, its snapshot's initializeOnce is non-nil,
- // initialized is open. Once initialization completes, initializedErr may
- // be set and initializeOnce becomes nil. If initializedErr is non-nil,
- // initialization may be retried (depending on how files are changed). To
- // indicate that initialization should be retried, initializeOnce will be
- // set. The next time a caller requests workspace packages, the
- // initialization will retry.
- initialized chan struct{}
-
- // initializationSema is used as a mutex to guard initializeOnce and
- // initializedErr, which will be updated after each attempt to initialize
- // the snapshot. We use a channel instead of a mutex to avoid blocking when
- // a context is canceled.
- initializationSema chan struct{}
- initializeOnce *sync.Once
- initializedErr error
+ // initializeOnce guards snapshot initialization. Each snapshot is
+ // initialized at most once: reinitialization is triggered on later snapshots
+ // by invalidating this field.
+ initializeOnce *sync.Once
+ // initializedErr holds the last error resulting from initialization. If
+ // initialization fails, we only retry when the the workspace modules change,
+ // to avoid too many go/packages calls.
+ initializedErr error
// mu guards all of the maps in the snapshot.
mu sync.Mutex
@@ -884,7 +868,7 @@
select {
case <-ctx.Done():
return
- case <-s.initialized:
+ case <-s.view.initialWorkspaceLoad:
}
// We typically prefer to run something as intensive as the IWL without
// blocking. I'm not sure if there is a way to do that here.
@@ -1020,7 +1004,12 @@
return fmt.Sprintf("v%v/%v", v.id, snapshotID)
}
-func (s *snapshot) clone(ctx context.Context, changes map[span.URI]*fileChange, forceReloadMetadata bool) *snapshot {
+func (s *snapshot) clone(ctx context.Context, changes map[span.URI]*fileChange, forceReloadMetadata bool) (*snapshot, bool) {
+ // Track some important types of changes.
+ var (
+ vendorChanged bool
+ modulesChanged bool
+ )
newWorkspace, workspaceChanged := s.workspace.invalidate(ctx, changes)
s.mu.Lock()
@@ -1028,28 +1017,26 @@
newGen := s.view.session.cache.store.Generation(generationName(s.view, s.id+1))
result := &snapshot{
- id: s.id + 1,
- generation: newGen,
- view: s.view,
- builtin: s.builtin,
- initialized: s.initialized,
- initializationSema: s.initializationSema,
- initializeOnce: s.initializeOnce,
- initializedErr: s.initializedErr,
- ids: make(map[span.URI][]packageID),
- importedBy: make(map[packageID][]packageID),
- metadata: make(map[packageID]*metadata),
- packages: make(map[packageKey]*packageHandle),
- actions: make(map[actionKey]*actionHandle),
- files: make(map[span.URI]source.VersionedFileHandle),
- goFiles: make(map[parseKey]*parseGoHandle),
- workspacePackages: make(map[packageID]packagePath),
- unloadableFiles: make(map[span.URI]struct{}),
- parseModHandles: make(map[span.URI]*parseModHandle),
- modTidyHandles: make(map[span.URI]*modTidyHandle),
- modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
- modWhyHandles: make(map[span.URI]*modWhyHandle),
- workspace: newWorkspace,
+ id: s.id + 1,
+ generation: newGen,
+ view: s.view,
+ builtin: s.builtin,
+ initializeOnce: s.initializeOnce,
+ initializedErr: s.initializedErr,
+ ids: make(map[span.URI][]packageID),
+ importedBy: make(map[packageID][]packageID),
+ metadata: make(map[packageID]*metadata),
+ packages: make(map[packageKey]*packageHandle),
+ actions: make(map[actionKey]*actionHandle),
+ files: make(map[span.URI]source.VersionedFileHandle),
+ goFiles: make(map[parseKey]*parseGoHandle),
+ workspacePackages: make(map[packageID]packagePath),
+ unloadableFiles: make(map[span.URI]struct{}),
+ parseModHandles: make(map[span.URI]*parseModHandle),
+ modTidyHandles: make(map[span.URI]*modTidyHandle),
+ modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
+ modWhyHandles: make(map[span.URI]*modWhyHandle),
+ workspace: newWorkspace,
}
if !workspaceChanged && s.workspaceDirHandle != nil {
@@ -1105,14 +1092,11 @@
result.modWhyHandles[k] = v
}
- var reinitialize reinitializeView
-
// directIDs keeps track of package IDs that have directly changed.
// It maps id->invalidateMetadata.
directIDs := map[packageID]bool{}
// Invalidate all package metadata if the workspace module has changed.
if workspaceChanged {
- reinitialize = definitelyReinit
for k := range s.metadata {
directIDs[k] = true
}
@@ -1122,7 +1106,7 @@
// Maybe reinitialize the view if we see a change in the vendor
// directory.
if inVendor(uri) {
- reinitialize = maybeReinit
+ vendorChanged = true
}
// The original FileHandle for this URI is cached on the snapshot.
@@ -1158,8 +1142,8 @@
// If the view's go.mod file's contents have changed, invalidate
// the metadata for every known package in the snapshot.
delete(result.parseModHandles, uri)
- if _, ok := result.workspace.activeModFiles()[uri]; ok && reinitialize < maybeReinit {
- reinitialize = maybeReinit
+ if _, ok := result.workspace.activeModFiles()[uri]; ok {
+ modulesChanged = true
}
}
// Handle the invalidated file; it may have new contents or not exist.
@@ -1285,20 +1269,14 @@
}
// The snapshot may need to be reinitialized.
- if reinitialize != doNotReinit {
- result.reinitialize(reinitialize == definitelyReinit)
+ if modulesChanged || workspaceChanged || vendorChanged {
+ if workspaceChanged || result.initializedErr != nil {
+ result.initializeOnce = &sync.Once{}
+ }
}
- return result
+ return result, workspaceChanged
}
-type reinitializeView int
-
-const (
- doNotReinit = reinitializeView(iota)
- maybeReinit
- definitelyReinit
-)
-
// guessPackagesForURI returns all packages related to uri. If we haven't seen this
// URI before, we guess based on files in the same directory. This is of course
// incorrect in build systems where packages are not organized by directory.
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 598908a..489504b 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -75,9 +75,23 @@
snapshotMu sync.Mutex
snapshot *snapshot
+ // initialWorkspaceLoad is closed when the first workspace initialization has
+ // completed. If we failed to load, we only retry if the go.mod file changes,
+ // to avoid too many go/packages calls.
+ initialWorkspaceLoad chan struct{}
+
+ // initializationSema is used limit concurrent initialization of snapshots in
+ // the view. We use a channel instead of a mutex to avoid blocking when a
+ // context is canceled.
+ initializationSema chan struct{}
+
// workspaceInformation tracks various details about this view's
// environment variables, go version, and use of modules.
workspaceInformation
+
+ // tempWorkspace is a temporary directory dedicated to holding the latest
+ // version of the workspace go.mod file. (TODO: also go.sum file)
+ tempWorkspace span.URI
}
type workspaceInformation struct {
@@ -397,6 +411,8 @@
v.session.removeView(ctx, v)
}
+// TODO(rFindley): probably some of this should also be one in View.Shutdown
+// above?
func (v *View) shutdown(ctx context.Context) {
// Cancel the initial workspace load if it is still running.
v.initCancelFirstAttempt()
@@ -410,6 +426,11 @@
v.snapshotMu.Lock()
go v.snapshot.generation.Destroy()
v.snapshotMu.Unlock()
+ if v.tempWorkspace != "" {
+ if err := os.RemoveAll(v.tempWorkspace.Filename()); err != nil {
+ event.Error(ctx, "removing temp workspace", err)
+ }
+ }
}
func (v *View) BackgroundContext() context.Context {
@@ -465,11 +486,11 @@
select {
case <-ctx.Done():
return
- case s.initializationSema <- struct{}{}:
+ case s.view.initializationSema <- struct{}{}:
}
defer func() {
- <-s.initializationSema
+ <-s.view.initializationSema
}()
if s.initializeOnce == nil {
@@ -479,7 +500,7 @@
defer func() {
s.initializeOnce = nil
if firstAttempt {
- close(s.initialized)
+ close(s.view.initialWorkspaceLoad)
}
}()
@@ -552,12 +573,45 @@
defer v.snapshotMu.Unlock()
oldSnapshot := v.snapshot
- v.snapshot = oldSnapshot.clone(ctx, changes, forceReloadMetadata)
+
+ var workspaceChanged bool
+ v.snapshot, workspaceChanged = oldSnapshot.clone(ctx, changes, forceReloadMetadata)
+ if workspaceChanged && v.tempWorkspace != "" {
+ snap := v.snapshot
+ go func() {
+ wsdir, err := snap.getWorkspaceDir(ctx)
+ if err != nil {
+ event.Error(ctx, "getting workspace dir", err)
+ }
+ if err := copyWorkspace(v.tempWorkspace, wsdir); err != nil {
+ event.Error(ctx, "copying workspace dir", err)
+ }
+ }()
+ }
go oldSnapshot.generation.Destroy()
return v.snapshot, v.snapshot.generation.Acquire(ctx)
}
+func copyWorkspace(dst span.URI, src span.URI) error {
+ srcMod := filepath.Join(src.Filename(), "go.mod")
+ srcf, err := os.Open(srcMod)
+ if err != nil {
+ return errors.Errorf("opening snapshot mod file: %w", err)
+ }
+ defer srcf.Close()
+ dstMod := filepath.Join(dst.Filename(), "go.mod")
+ dstf, err := os.Create(dstMod)
+ if err != nil {
+ return errors.Errorf("truncating view mod file: %w", err)
+ }
+ defer dstf.Close()
+ if _, err := io.Copy(dstf, srcf); err != nil {
+ return errors.Errorf("copying modfiles: %w", err)
+ }
+ return nil
+}
+
func (v *View) cancelBackground() {
v.mu.Lock()
defer v.mu.Unlock()
@@ -569,19 +623,6 @@
v.backgroundCtx, v.cancel = context.WithCancel(v.baseCtx)
}
-func (s *snapshot) reinitialize(force bool) {
- s.initializationSema <- struct{}{}
- defer func() {
- <-s.initializationSema
- }()
-
- if !force && s.initializedErr == nil {
- return
- }
- var once sync.Once
- s.initializeOnce = &once
-}
-
func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, options *source.Options) (*workspaceInformation, error) {
if err := checkPathCase(folder.Filename()); err != nil {
return nil, errors.Errorf("invalid workspace configuration: %w", err)
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index b367293..9ef80fe 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -8,6 +8,7 @@
"bufio"
"context"
"fmt"
+ "os"
"path/filepath"
"regexp"
"strings"
@@ -89,6 +90,10 @@
// AllExperiments sets the "allExperiments" configuration, which enables
// all of gopls's opt-in settings.
AllExperiments bool
+
+ // Whether to send the current process ID, for testing data that is joined to
+ // the PID. This can only be set by one test.
+ SendPID bool
}
// NewEditor Creates a new Editor.
@@ -232,6 +237,9 @@
params.Capabilities.Window.WorkDoneProgress = true
// TODO: set client capabilities
params.InitializationOptions = e.configuration()
+ if e.Config.SendPID {
+ params.ProcessID = float64(os.Getpid())
+ }
// This is a bit of a hack, since the fake editor doesn't actually support
// watching changed files that match a specific glob pattern. However, the
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index 1c2a8bd..cc38cb2 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -33,6 +33,7 @@
s.state = serverInitializing
s.stateMu.Unlock()
+ s.clientPID = int(params.ProcessID)
s.progress.supportsWorkDoneProgress = params.Capabilities.Window.WorkDoneProgress
options := s.session.Options()
@@ -196,6 +197,8 @@
}()
}
dirsToWatch := map[span.URI]struct{}{}
+ // Only one view gets to have a workspace.
+ assignedWorkspace := false
for _, folder := range folders {
uri := span.URIFromURI(folder.URI)
// Ignore non-file URIs.
@@ -203,7 +206,22 @@
continue
}
work := s.progress.start(ctx, "Setting up workspace", "Loading packages...", nil, nil)
- snapshot, release, err := s.addView(ctx, folder.Name, uri)
+ var workspaceURI span.URI = ""
+ if !assignedWorkspace && s.clientPID != 0 {
+ // For quick-and-dirty testing, set the temp workspace file to
+ // $TMPDIR/gopls-<client PID>.workspace.
+ //
+ // This has a couple limitations:
+ // + If there are multiple workspace roots, only the first one gets
+ // written to this dir (and the client has no way to know precisely
+ // which one).
+ // + If a single client PID spawns multiple gopls sessions, they will
+ // clobber eachother's temp workspace.
+ wsdir := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.workspace", s.clientPID))
+ workspaceURI = span.URIFromPath(wsdir)
+ assignedWorkspace = true
+ }
+ snapshot, release, err := s.addView(ctx, folder.Name, uri, workspaceURI)
if err != nil {
viewErrors[uri] = err
work.end(fmt.Sprintf("Error loading packages: %s", err))
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index e031d6d..aa73f6c 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -49,7 +49,7 @@
tests.DefaultOptions(options)
session.SetOptions(options)
options.SetEnvSlice(datum.Config.Env)
- view, snapshot, release, err := session.NewView(ctx, datum.Config.Dir, span.URIFromPath(datum.Config.Dir), options)
+ view, snapshot, release, err := session.NewView(ctx, datum.Config.Dir, span.URIFromPath(datum.Config.Dir), "", options)
if err != nil {
t.Fatal(err)
}
diff --git a/internal/lsp/lsprpc/lsprpc.go b/internal/lsp/lsprpc/lsprpc.go
index 448c0e5..dc3e29c 100644
--- a/internal/lsp/lsprpc/lsprpc.go
+++ b/internal/lsp/lsprpc/lsprpc.go
@@ -39,8 +39,8 @@
// streams as a new LSP session, using a shared cache.
type StreamServer struct {
cache *cache.Cache
- // logConnections controls whether or not to log new connections.
- logConnections bool
+ // daemon controls whether or not to log new connections.
+ daemon bool
// serverForTest may be set to a test fake for testing.
serverForTest protocol.Server
@@ -49,8 +49,8 @@
// NewStreamServer creates a StreamServer using the shared cache. If
// withTelemetry is true, each session is instrumented with telemetry that
// records RPC statistics.
-func NewStreamServer(cache *cache.Cache, logConnections bool) *StreamServer {
- return &StreamServer{cache: cache, logConnections: logConnections}
+func NewStreamServer(cache *cache.Cache, daemon bool) *StreamServer {
+ return &StreamServer{cache: cache, daemon: daemon}
}
// ServeStream implements the jsonrpc2.StreamServer interface, by handling
@@ -78,10 +78,10 @@
ctx = protocol.WithClient(ctx, client)
conn.Go(ctx,
protocol.Handlers(
- handshaker(session, executable, s.logConnections,
+ handshaker(session, executable, s.daemon,
protocol.ServerHandler(server,
jsonrpc2.MethodNotFound))))
- if s.logConnections {
+ if s.daemon {
log.Printf("Session %s: connected", session.ID())
defer log.Printf("Session %s: exited", session.ID())
}
@@ -226,6 +226,8 @@
hreq.DebugAddr = di.ListenedDebugAddress
}
if err := protocol.Call(ctx, serverConn, handshakeMethod, hreq, &hresp); err != nil {
+ // TODO(rfindley): at some point in the future we should return an error
+ // here. Handshakes have become functional in nature.
event.Error(ctx, "forwarder: gopls handshake failed", err)
}
if hresp.GoplsPath != f.goplsPath {
diff --git a/internal/lsp/mod/mod_test.go b/internal/lsp/mod/mod_test.go
index f7c3360..6ce8926 100644
--- a/internal/lsp/mod/mod_test.go
+++ b/internal/lsp/mod/mod_test.go
@@ -45,7 +45,7 @@
if err != nil {
t.Fatal(err)
}
- _, _, release, err := session.NewView(ctx, "diagnostics_test", span.URIFromPath(folder), options)
+ _, _, release, err := session.NewView(ctx, "diagnostics_test", span.URIFromPath(folder), "", options)
release()
if err != nil {
t.Fatal(err)
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 763f4d1..058ffbc 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -65,7 +65,8 @@
stateMu sync.Mutex
state serverState
- session source.Session
+ session source.Session
+ clientPID int
// notifications generated before serverInitialized
notifications []*protocol.ShowMessageParams
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index ada58b5..932351e 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -51,7 +51,7 @@
options := source.DefaultOptions().Clone()
tests.DefaultOptions(options)
options.SetEnvSlice(datum.Config.Env)
- view, _, release, err := session.NewView(ctx, "source_test", span.URIFromPath(datum.Config.Dir), options)
+ view, _, release, err := session.NewView(ctx, "source_test", span.URIFromPath(datum.Config.Dir), "", options)
release()
if err != nil {
t.Fatal(err)
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 390102e..fa307df 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -269,7 +269,7 @@
// A session may have many active views at any given time.
type Session interface {
// NewView creates a new View, returning it and its first snapshot.
- NewView(ctx context.Context, name string, folder span.URI, options *Options) (View, Snapshot, func(), error)
+ NewView(ctx context.Context, name string, folder, tempWorkspaceDir span.URI, options *Options) (View, Snapshot, func(), error)
// Cache returns the cache that created this session, for debugging only.
Cache() interface{}
diff --git a/internal/lsp/workspace.go b/internal/lsp/workspace.go
index c1ba89b..9b29668 100644
--- a/internal/lsp/workspace.go
+++ b/internal/lsp/workspace.go
@@ -26,7 +26,7 @@
return s.addFolders(ctx, event.Added)
}
-func (s *Server) addView(ctx context.Context, name string, uri span.URI) (source.Snapshot, func(), error) {
+func (s *Server) addView(ctx context.Context, name string, uri, tempWorkspace span.URI) (source.Snapshot, func(), error) {
s.stateMu.Lock()
state := s.state
s.stateMu.Unlock()
@@ -37,7 +37,7 @@
if err := s.fetchConfig(ctx, name, uri, options); err != nil {
return nil, func() {}, err
}
- _, snapshot, release, err := s.session.NewView(ctx, name, uri, options)
+ _, snapshot, release, err := s.session.NewView(ctx, name, uri, tempWorkspace, options)
return snapshot, release, err
}