blob: 5639bab0c479f7e2dd84e0805ffab86b51f1b603 [file] [log] [blame]
// 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 server defines gopls' implementation of the LSP server
// interface, [protocol.Server]. Call [New] to create an instance.
package server
import (
"context"
"crypto/rand"
"embed"
"encoding/base64"
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
paths "path"
"strconv"
"strings"
"sync"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/golang"
"golang.org/x/tools/gopls/internal/progress"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/internal/event"
)
// New creates an LSP server and binds it to handle incoming client
// messages on the supplied stream.
func New(session *cache.Session, client protocol.ClientCloser, options *settings.Options) protocol.Server {
const concurrentAnalyses = 1
// If this assignment fails to compile after a protocol
// upgrade, it means that one or more new methods need new
// stub declarations in unimplemented.go.
return &server{
diagnostics: make(map[protocol.DocumentURI]*fileDiagnostics),
watchedGlobPatterns: nil, // empty
changedFiles: make(map[protocol.DocumentURI]unit),
session: session,
client: client,
diagnosticsSema: make(chan unit, concurrentAnalyses),
progress: progress.NewTracker(client),
options: options,
viewsToDiagnose: make(map[*cache.View]uint64),
}
}
type serverState int
const (
serverCreated = serverState(iota)
serverInitializing // set once the server has received "initialize" request
serverInitialized // set once the server has received "initialized" request
serverShutDown
)
func (s serverState) String() string {
switch s {
case serverCreated:
return "created"
case serverInitializing:
return "initializing"
case serverInitialized:
return "initialized"
case serverShutDown:
return "shutDown"
}
return fmt.Sprintf("(unknown state: %d)", int(s))
}
// server implements the protocol.server interface.
type server struct {
client protocol.ClientCloser
stateMu sync.Mutex
state serverState
// notifications generated before serverInitialized
notifications []*protocol.ShowMessageParams
session *cache.Session
tempDir string
// changedFiles tracks files for which there has been a textDocument/didChange.
changedFilesMu sync.Mutex
changedFiles map[protocol.DocumentURI]unit
// folders is only valid between initialize and initialized, and holds the
// set of folders to build views for when we are ready.
// Each has a valid, non-empty 'file'-scheme URI.
pendingFolders []protocol.WorkspaceFolder
// watchedGlobPatterns is the set of glob patterns that we have requested
// the client watch on disk. It will be updated as the set of directories
// that the server should watch changes.
// The map field may be reassigned but the map is immutable.
watchedGlobPatternsMu sync.Mutex
watchedGlobPatterns map[protocol.RelativePattern]unit
watchRegistrationCount int
diagnosticsMu sync.Mutex
diagnostics map[protocol.DocumentURI]*fileDiagnostics
// diagnosticsSema limits the concurrency of diagnostics runs, which can be
// expensive.
diagnosticsSema chan unit
progress *progress.Tracker
// When the workspace fails to load, we show its status through a progress
// report with an error message.
criticalErrorStatusMu sync.Mutex
criticalErrorStatus *progress.WorkDone
// Track an ongoing CPU profile created with the StartProfile command and
// terminated with the StopProfile command.
ongoingProfileMu sync.Mutex
ongoingProfile *os.File // if non-nil, an ongoing profile is writing to this file
// Track most recently requested options.
optionsMu sync.Mutex
options *settings.Options
// Track the most recent completion results, for measuring completion efficacy
efficacyMu sync.Mutex
efficacyURI protocol.DocumentURI
efficacyVersion int32
efficacyItems []protocol.CompletionItem
efficacyPos protocol.Position
// Web server (for package documentation, etc) associated with this
// LSP server. Opened on demand, and closed during LSP Shutdown.
webOnce sync.Once
web *web
webErr error
// # Modification tracking and diagnostics
//
// For the purpose of tracking diagnostics, we need a monotonically
// increasing clock. Each time a change occurs on the server, this clock is
// incremented and the previous diagnostics pass is cancelled. When the
// changed is processed, the Session (via DidModifyFiles) determines which
// Views are affected by the change and these views are added to the
// viewsToDiagnose set. Then the server calls diagnoseChangedViews
// in a separate goroutine. Any Views that successfully complete their
// diagnostics are removed from the viewsToDiagnose set, provided they haven't
// been subsequently marked for re-diagnosis (as determined by the latest
// modificationID referenced by viewsToDiagnose).
//
// In this way, we enforce eventual completeness of the diagnostic set: any
// views requiring diagnosis are diagnosed, though possibly at a later point
// in time. Notably, the logic in Session.DidModifyFiles to determines if a
// view needs diagnosis considers whether any packages in the view were
// invalidated. Consider the following sequence of snapshots for a given view
// V:
//
// C1 C2
// S1 -> S2 -> S3
//
// In this case, suppose that S1 was fully type checked, and then two changes
// C1 and C2 occur in rapid succession, to a file in their package graph but
// perhaps not enclosed by V's root. In this case, the logic of
// DidModifyFiles will detect that V needs to be reloaded following C1. In
// order for our eventual consistency to be sound, we need to avoid the race
// where S2 is being diagnosed, C2 arrives, and S3 is not detected as needing
// diagnosis because the relevant package has not yet been computed in S2. To
// achieve this, we only remove V from viewsToDiagnose if the diagnosis of S2
// completes before C2 is processed, which we can confirm by checking
// S2.BackgroundContext().
modificationMu sync.Mutex
cancelPrevDiagnostics func()
viewsToDiagnose map[*cache.View]uint64 // View -> modification at which it last required diagnosis
lastModificationID uint64 // incrementing clock
}
func (s *server) WorkDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error {
ctx, done := event.Start(ctx, "lsp.Server.workDoneProgressCancel")
defer done()
return s.progress.Cancel(params.Token)
}
// web encapsulates the web server associated with an LSP server.
// It is used for package documentation and other queries
// where HTML makes more sense than a client editor UI.
//
// Example URL:
//
// http://127.0.0.1:PORT/gopls/SECRET/...
//
// where
// - PORT is the random port number;
// - "gopls" helps the reader guess which program is the server;
// - SECRET is the 64-bit token; and
// - ... is the material part of the endpoint.
//
// Valid endpoints:
//
// open?file=%s&line=%d&col=%d - open a file
// pkg/PKGPATH?view=%s - show doc for package in a given view
type web struct {
server *http.Server
addr url.URL // "http://127.0.0.1:PORT/gopls/SECRET"
mux *http.ServeMux
}
// getWeb returns the web server associated with this
// LSP server, creating it on first request.
func (s *server) getWeb() (*web, error) {
s.webOnce.Do(func() {
s.web, s.webErr = s.initWeb()
})
return s.web, s.webErr
}
// initWeb starts the local web server through which gopls
// serves package documentation and suchlike.
//
// Clients should use [getWeb].
func (s *server) initWeb() (*web, error) {
// Use 64 random bits as the base of the URL namespace.
// This ensures that URLs are unguessable to any local
// processes that connect to the server, preventing
// exfiltration of source code.
//
// (Note: depending on the LSP client, URLs that are passed to
// it via showDocument and that result in the opening of a
// browser tab may be transiently published through the argv
// array of the open(1) or xdg-open(1) command.)
token := make([]byte, 8)
if _, err := rand.Read(token); err != nil {
return nil, fmt.Errorf("generating secret token: %v", err)
}
// Pick any free port.
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
// -- There should be no early returns after this point. --
// The root mux is not authenticated.
rootMux := http.NewServeMux()
rootMux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
http.Error(w, "request URI lacks authentication segment", http.StatusUnauthorized)
})
rootMux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, "/assets/favicon.ico", http.StatusMovedPermanently)
})
secret := "/gopls/" + base64.RawURLEncoding.EncodeToString(token)
webMux := http.NewServeMux()
rootMux.Handle(secret+"/", http.StripPrefix(secret, webMux))
rootMux.Handle("/assets/", http.FileServer(http.FS(assets)))
webServer := &http.Server{Addr: listener.Addr().String(), Handler: rootMux}
go func() {
// This should run until LSP Shutdown, at which point
// it will return ErrServerClosed. Any other error
// means it failed to start.
if err := webServer.Serve(listener); err != nil {
if err != http.ErrServerClosed {
log.Print(err)
}
}
}()
web := &web{
server: webServer,
addr: url.URL{Scheme: "http", Host: webServer.Addr, Path: secret},
mux: webMux,
}
// The /open handler allows the browser to request that the
// LSP client editor open a file; see web.urlToOpen.
webMux.HandleFunc("/open", func(w http.ResponseWriter, req *http.Request) {
if err := req.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
uri := protocol.URIFromPath(req.Form.Get("file"))
line, _ := strconv.Atoi(req.Form.Get("line")) // 1-based
col, _ := strconv.Atoi(req.Form.Get("col")) // 1-based UTF-8
posn := protocol.Position{
Line: uint32(line - 1),
Character: uint32(col - 1), // TODO(adonovan): map to UTF-16
}
openClientEditor(req.Context(), s.client, protocol.Location{
URI: uri,
Range: protocol.Range{Start: posn, End: posn},
})
})
// The /pkg/PATH&view=... handler shows package documentation for PATH.
webMux.Handle("/pkg/", http.StripPrefix("/pkg/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
if err := req.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Get snapshot of specified view.
view, err := s.session.View(req.Form.Get("view"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
snapshot, release, err := view.Snapshot()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer release()
// Find package by path.
var found *metadata.Package
for _, mp := range snapshot.MetadataGraph().Packages {
if string(mp.PkgPath) == req.URL.Path && mp.ForTest == "" {
found = mp
break
}
}
if found == nil {
// TODO(adonovan): what should we do for external test packages?
http.Error(w, "package not found", http.StatusNotFound)
return
}
// Type-check the package and render its documentation.
pkgs, err := snapshot.TypeCheck(ctx, found.ID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
pkgURL := func(path golang.PackagePath, fragment string) protocol.URI {
return web.pkgURL(view, path, fragment)
}
content, err := golang.RenderPackageDoc(pkgs[0], web.openURL, pkgURL)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(content)
})))
return web, nil
}
// assets holds our static web server content.
//
//go:embed assets/*
var assets embed.FS
// openURL returns an /open URL that, when visited, causes the client
// editor to open the specified file/line/column (in 1-based UTF-8
// coordinates).
//
// (Rendering may generate hundreds of positions across files of many
// packages, so don't convert to LSP coordinates yet: wait until the
// URL is opened.)
func (w *web) openURL(filename string, line, col8 int) protocol.URI {
return w.url(
"open",
fmt.Sprintf("file=%s&line=%d&col=%d", url.QueryEscape(filename), line, col8),
"")
}
// pkgURL returns a /pkg URL for the documentation of the specified package.
// The optional fragment must be of the form "Println" or "Buffer.WriteString".
func (w *web) pkgURL(v *cache.View, path golang.PackagePath, fragment string) protocol.URI {
return w.url(
"pkg/"+string(path),
"view="+url.QueryEscape(v.ID()),
fragment)
}
// url returns a URL by joining a relative path, an (encoded) query,
// and an (unencoded) fragment onto the authenticated base URL of the
// web server.
func (w *web) url(path, query, fragment string) protocol.URI {
url2 := w.addr
url2.Path = paths.Join(url2.Path, strings.TrimPrefix(path, "/"))
url2.RawQuery = query
url2.Fragment = fragment
return protocol.URI(url2.String())
}