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
 }