+++ /dev/null
-// Copyright 2020 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 lsprpc implements a jsonrpc2.StreamServer that may be used to
-// serve the LSP on a jsonrpc2 channel.
-package lsprpc
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "log"
- "net"
- "os"
- "strconv"
- "sync/atomic"
- "time"
-
- "golang.org/x/tools/internal/event"
- "golang.org/x/tools/internal/gocommand"
- "golang.org/x/tools/internal/jsonrpc2"
- "golang.org/x/tools/internal/lsp"
- "golang.org/x/tools/internal/lsp/cache"
- "golang.org/x/tools/internal/lsp/debug"
- "golang.org/x/tools/internal/lsp/debug/tag"
- "golang.org/x/tools/internal/lsp/protocol"
- errors "golang.org/x/xerrors"
-)
-
-// AutoNetwork is the pseudo network type used to signal that gopls should use
-// automatic discovery to resolve a remote address.
-const AutoNetwork = "auto"
-
-// Unique identifiers for client/server.
-var serverIndex int64
-
-// The StreamServer type is a jsonrpc2.StreamServer that handles incoming
-// 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
-
- // serverForTest may be set to a test fake for testing.
- serverForTest protocol.Server
-}
-
-// 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}
-}
-
-// ServeStream implements the jsonrpc2.StreamServer interface, by handling
-// incoming streams using a new lsp server.
-func (s *StreamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) error {
- client := protocol.ClientDispatcher(conn)
- session := s.cache.NewSession(ctx)
- server := s.serverForTest
- if server == nil {
- server = lsp.NewServer(session, client)
- }
- // Clients may or may not send a shutdown message. Make sure the server is
- // shut down.
- // TODO(rFindley): this shutdown should perhaps be on a disconnected context.
- defer func() {
- if err := server.Shutdown(ctx); err != nil {
- event.Error(ctx, "error shutting down", err)
- }
- }()
- executable, err := os.Executable()
- if err != nil {
- log.Printf("error getting gopls path: %v", err)
- executable = ""
- }
- ctx = protocol.WithClient(ctx, client)
- conn.Go(ctx,
- protocol.Handlers(
- handshaker(session, executable, s.logConnections,
- protocol.ServerHandler(server,
- jsonrpc2.MethodNotFound))))
- if s.logConnections {
- log.Printf("Session %s: connected", session.ID())
- defer log.Printf("Session %s: exited", session.ID())
- }
- <-conn.Done()
- return conn.Err()
-}
-
-// A Forwarder is a jsonrpc2.StreamServer that handles an LSP stream by
-// forwarding it to a remote. This is used when the gopls process started by
-// the editor is in the `-remote` mode, which means it finds and connects to a
-// separate gopls daemon. In these cases, we still want the forwarder gopls to
-// be instrumented with telemetry, and want to be able to in some cases hijack
-// the jsonrpc2 connection with the daemon.
-type Forwarder struct {
- network, addr string
-
- // goplsPath is the path to the current executing gopls binary.
- goplsPath string
-
- // configuration for the auto-started gopls remote.
- remoteConfig remoteConfig
-}
-
-type remoteConfig struct {
- debug string
- listenTimeout time.Duration
- logfile string
-}
-
-// A RemoteOption configures the behavior of the auto-started remote.
-type RemoteOption interface {
- set(*remoteConfig)
-}
-
-// RemoteDebugAddress configures the address used by the auto-started Gopls daemon
-// for serving debug information.
-type RemoteDebugAddress string
-
-func (d RemoteDebugAddress) set(cfg *remoteConfig) {
- cfg.debug = string(d)
-}
-
-// RemoteListenTimeout configures the amount of time the auto-started gopls
-// daemon will wait with no client connections before shutting down.
-type RemoteListenTimeout time.Duration
-
-func (d RemoteListenTimeout) set(cfg *remoteConfig) {
- cfg.listenTimeout = time.Duration(d)
-}
-
-// RemoteLogfile configures the logfile location for the auto-started gopls
-// daemon.
-type RemoteLogfile string
-
-func (l RemoteLogfile) set(cfg *remoteConfig) {
- cfg.logfile = string(l)
-}
-
-func defaultRemoteConfig() remoteConfig {
- return remoteConfig{
- listenTimeout: 1 * time.Minute,
- }
-}
-
-// NewForwarder creates a new Forwarder, ready to forward connections to the
-// remote server specified by network and addr.
-func NewForwarder(network, addr string, opts ...RemoteOption) *Forwarder {
- gp, err := os.Executable()
- if err != nil {
- log.Printf("error getting gopls path for forwarder: %v", err)
- gp = ""
- }
-
- rcfg := defaultRemoteConfig()
- for _, opt := range opts {
- opt.set(&rcfg)
- }
-
- fwd := &Forwarder{
- network: network,
- addr: addr,
- goplsPath: gp,
- remoteConfig: rcfg,
- }
- return fwd
-}
-
-// QueryServerState queries the server state of the current server.
-func QueryServerState(ctx context.Context, network, address string) (*ServerState, error) {
- if network == AutoNetwork {
- gp, err := os.Executable()
- if err != nil {
- return nil, errors.Errorf("getting gopls path: %w", err)
- }
- network, address = autoNetworkAddress(gp, address)
- }
- netConn, err := net.DialTimeout(network, address, 5*time.Second)
- if err != nil {
- return nil, errors.Errorf("dialing remote: %w", err)
- }
- serverConn := jsonrpc2.NewConn(jsonrpc2.NewHeaderStream(netConn))
- serverConn.Go(ctx, jsonrpc2.MethodNotFound)
- var state ServerState
- if err := protocol.Call(ctx, serverConn, sessionsMethod, nil, &state); err != nil {
- return nil, errors.Errorf("querying server state: %w", err)
- }
- return &state, nil
-}
-
-// ServeStream dials the forwarder remote and binds the remote to serve the LSP
-// on the incoming stream.
-func (f *Forwarder) ServeStream(ctx context.Context, clientConn jsonrpc2.Conn) error {
- client := protocol.ClientDispatcher(clientConn)
-
- netConn, err := f.connectToRemote(ctx)
- if err != nil {
- return errors.Errorf("forwarder: connecting to remote: %w", err)
- }
- serverConn := jsonrpc2.NewConn(jsonrpc2.NewHeaderStream(netConn))
- server := protocol.ServerDispatcher(serverConn)
-
- // Forward between connections.
- serverConn.Go(ctx,
- protocol.Handlers(
- protocol.ClientHandler(client,
- jsonrpc2.MethodNotFound)))
- // Don't run the clientConn yet, so that we can complete the handshake before
- // processing any client messages.
-
- // Do a handshake with the server instance to exchange debug information.
- index := atomic.AddInt64(&serverIndex, 1)
- serverID := strconv.FormatInt(index, 10)
- var (
- hreq = handshakeRequest{
- ServerID: serverID,
- GoplsPath: f.goplsPath,
- }
- hresp handshakeResponse
- )
- if di := debug.GetInstance(ctx); di != nil {
- hreq.Logfile = di.Logfile
- hreq.DebugAddr = di.ListenedDebugAddress
- }
- if err := protocol.Call(ctx, serverConn, handshakeMethod, hreq, &hresp); err != nil {
- event.Error(ctx, "forwarder: gopls handshake failed", err)
- }
- if hresp.GoplsPath != f.goplsPath {
- event.Error(ctx, "", fmt.Errorf("forwarder: gopls path mismatch: forwarder is %q, remote is %q", f.goplsPath, hresp.GoplsPath))
- }
- event.Log(ctx, "New server",
- tag.NewServer.Of(serverID),
- tag.Logfile.Of(hresp.Logfile),
- tag.DebugAddress.Of(hresp.DebugAddr),
- tag.GoplsPath.Of(hresp.GoplsPath),
- tag.ClientID.Of(hresp.SessionID),
- )
- clientConn.Go(ctx,
- protocol.Handlers(
- forwarderHandler(
- protocol.ServerHandler(server,
- jsonrpc2.MethodNotFound))))
-
- select {
- case <-serverConn.Done():
- clientConn.Close()
- case <-clientConn.Done():
- serverConn.Close()
- }
-
- err = nil
- if serverConn.Err() != nil {
- err = errors.Errorf("remote disconnected: %v", err)
- } else if clientConn.Err() != nil {
- err = errors.Errorf("client disconnected: %v", err)
- }
- event.Log(ctx, fmt.Sprintf("forwarder: exited with error: %v", err))
- return err
-}
-
-func (f *Forwarder) connectToRemote(ctx context.Context) (net.Conn, error) {
- return connectToRemote(ctx, f.network, f.addr, f.goplsPath, f.remoteConfig)
-}
-
-func ConnectToRemote(ctx context.Context, network, addr string, opts ...RemoteOption) (net.Conn, error) {
- rcfg := defaultRemoteConfig()
- for _, opt := range opts {
- opt.set(&rcfg)
- }
- // This is not strictly necessary, as it won't be used if not connecting to
- // the 'auto' remote.
- goplsPath, err := os.Executable()
- if err != nil {
- return nil, fmt.Errorf("unable to resolve gopls path: %v", err)
- }
- return connectToRemote(ctx, network, addr, goplsPath, rcfg)
-}
-
-func connectToRemote(ctx context.Context, inNetwork, inAddr, goplsPath string, rcfg remoteConfig) (net.Conn, error) {
- var (
- netConn net.Conn
- err error
- network, address = inNetwork, inAddr
- )
- if inNetwork == AutoNetwork {
- // f.network is overloaded to support a concept of 'automatic' addresses,
- // which signals that the gopls remote address should be automatically
- // derived.
- // So we need to resolve a real network and address here.
- network, address = autoNetworkAddress(goplsPath, inAddr)
- }
- // Attempt to verify that we own the remote. This is imperfect, but if we can
- // determine that the remote is owned by a different user, we should fail.
- ok, err := verifyRemoteOwnership(network, address)
- if err != nil {
- // If the ownership check itself failed, we fail open but log an error to
- // the user.
- event.Error(ctx, "unable to check daemon socket owner, failing open", err)
- } else if !ok {
- // We succesfully checked that the socket is not owned by us, we fail
- // closed.
- return nil, fmt.Errorf("socket %q is owned by a different user", address)
- }
- const dialTimeout = 1 * time.Second
- // Try dialing our remote once, in case it is already running.
- netConn, err = net.DialTimeout(network, address, dialTimeout)
- if err == nil {
- return netConn, nil
- }
- // If our remote is on the 'auto' network, start it if it doesn't exist.
- if inNetwork == AutoNetwork {
- if goplsPath == "" {
- return nil, fmt.Errorf("cannot auto-start remote: gopls path is unknown")
- }
- if network == "unix" {
- // Sometimes the socketfile isn't properly cleaned up when gopls shuts
- // down. Since we have already tried and failed to dial this address, it
- // should *usually* be safe to remove the socket before binding to the
- // address.
- // TODO(rfindley): there is probably a race here if multiple gopls
- // instances are simultaneously starting up.
- if _, err := os.Stat(address); err == nil {
- if err := os.Remove(address); err != nil {
- return nil, errors.Errorf("removing remote socket file: %w", err)
- }
- }
- }
- args := []string{"serve",
- "-listen", fmt.Sprintf(`%s;%s`, network, address),
- "-listen.timeout", rcfg.listenTimeout.String(),
- }
- if rcfg.logfile != "" {
- args = append(args, "-logfile", rcfg.logfile)
- }
- if rcfg.debug != "" {
- args = append(args, "-debug", rcfg.debug)
- }
- if err := startRemote(goplsPath, args...); err != nil {
- return nil, errors.Errorf("startRemote(%q, %v): %w", goplsPath, args, err)
- }
- }
-
- const retries = 5
- // It can take some time for the newly started server to bind to our address,
- // so we retry for a bit.
- for retry := 0; retry < retries; retry++ {
- startDial := time.Now()
- netConn, err = net.DialTimeout(network, address, dialTimeout)
- if err == nil {
- return netConn, nil
- }
- event.Log(ctx, fmt.Sprintf("failed attempt #%d to connect to remote: %v\n", retry+2, err))
- // In case our failure was a fast-failure, ensure we wait at least
- // f.dialTimeout before trying again.
- if retry != retries-1 {
- time.Sleep(dialTimeout - time.Since(startDial))
- }
- }
- return nil, errors.Errorf("dialing remote: %w", err)
-}
-
-// forwarderHandler intercepts 'exit' messages to prevent the shared gopls
-// instance from exiting. In the future it may also intercept 'shutdown' to
-// provide more graceful shutdown of the client connection.
-func forwarderHandler(handler jsonrpc2.Handler) jsonrpc2.Handler {
- return func(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2.Request) error {
- // The gopls workspace environment defaults to the process environment in
- // which gopls daemon was started. To avoid discrepancies in Go environment
- // between the editor and daemon, inject any unset variables in `go env`
- // into the options sent by initialize.
- //
- // See also golang.org/issue/37830.
- if r.Method() == "initialize" {
- if newr, err := addGoEnvToInitializeRequest(ctx, r); err == nil {
- r = newr
- } else {
- log.Printf("unable to add local env to initialize request: %v", err)
- }
- }
- return handler(ctx, reply, r)
- }
-}
-
-// addGoEnvToInitializeRequest builds a new initialize request in which we set
-// any environment variables output by `go env` and not already present in the
-// request.
-//
-// It returns an error if r is not an initialize requst, or is otherwise
-// malformed.
-func addGoEnvToInitializeRequest(ctx context.Context, r jsonrpc2.Request) (jsonrpc2.Request, error) {
- var params protocol.ParamInitialize
- if err := json.Unmarshal(r.Params(), ¶ms); err != nil {
- return nil, err
- }
- var opts map[string]interface{}
- switch v := params.InitializationOptions.(type) {
- case nil:
- opts = make(map[string]interface{})
- case map[string]interface{}:
- opts = v
- default:
- return nil, fmt.Errorf("unexpected type for InitializationOptions: %T", v)
- }
- envOpt, ok := opts["env"]
- if !ok {
- envOpt = make(map[string]interface{})
- }
- env, ok := envOpt.(map[string]interface{})
- if !ok {
- return nil, fmt.Errorf(`env option is %T, expected a map`, envOpt)
- }
- goenv, err := getGoEnv(ctx, env)
- if err != nil {
- return nil, err
- }
- for govar, value := range goenv {
- env[govar] = value
- }
- opts["env"] = env
- params.InitializationOptions = opts
- call, ok := r.(*jsonrpc2.Call)
- if !ok {
- return nil, fmt.Errorf("%T is not a *jsonrpc2.Call", r)
- }
- return jsonrpc2.NewCall(call.ID(), "initialize", params)
-}
-
-func getGoEnv(ctx context.Context, env map[string]interface{}) (map[string]string, error) {
- var runEnv []string
- for k, v := range env {
- runEnv = append(runEnv, fmt.Sprintf("%s=%s", k, v))
- }
- runner := gocommand.Runner{}
- output, err := runner.Run(ctx, gocommand.Invocation{
- Verb: "env",
- Args: []string{"-json"},
- Env: runEnv,
- })
- if err != nil {
- return nil, err
- }
- envmap := make(map[string]string)
- if err := json.Unmarshal(output.Bytes(), &envmap); err != nil {
- return nil, err
- }
- return envmap, nil
-}
-
-// A handshakeRequest identifies a client to the LSP server.
-type handshakeRequest struct {
- // ServerID is the ID of the server on the client. This should usually be 0.
- ServerID string `json:"serverID"`
- // Logfile is the location of the clients log file.
- Logfile string `json:"logfile"`
- // DebugAddr is the client debug address.
- DebugAddr string `json:"debugAddr"`
- // GoplsPath is the path to the Gopls binary running the current client
- // process.
- GoplsPath string `json:"goplsPath"`
-}
-
-// A handshakeResponse is returned by the LSP server to tell the LSP client
-// information about its session.
-type handshakeResponse struct {
- // SessionID is the server session associated with the client.
- SessionID string `json:"sessionID"`
- // Logfile is the location of the server logs.
- Logfile string `json:"logfile"`
- // DebugAddr is the server debug address.
- DebugAddr string `json:"debugAddr"`
- // GoplsPath is the path to the Gopls binary running the current server
- // process.
- GoplsPath string `json:"goplsPath"`
-}
-
-// ClientSession identifies a current client LSP session on the server. Note
-// that it looks similar to handshakeResposne, but in fact 'Logfile' and
-// 'DebugAddr' now refer to the client.
-type ClientSession struct {
- SessionID string `json:"sessionID"`
- Logfile string `json:"logfile"`
- DebugAddr string `json:"debugAddr"`
-}
-
-// ServerState holds information about the gopls daemon process, including its
-// debug information and debug information of all of its current connected
-// clients.
-type ServerState struct {
- Logfile string `json:"logfile"`
- DebugAddr string `json:"debugAddr"`
- GoplsPath string `json:"goplsPath"`
- CurrentClientID string `json:"currentClientID"`
- Clients []ClientSession `json:"clients"`
-}
-
-const (
- handshakeMethod = "gopls/handshake"
- sessionsMethod = "gopls/sessions"
-)
-
-func handshaker(session *cache.Session, goplsPath string, logHandshakes bool, handler jsonrpc2.Handler) jsonrpc2.Handler {
- return func(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2.Request) error {
- switch r.Method() {
- case handshakeMethod:
- // We log.Printf in this handler, rather than event.Log when we want logs
- // to go to the daemon log rather than being reflected back to the
- // client.
- var req handshakeRequest
- if err := json.Unmarshal(r.Params(), &req); err != nil {
- if logHandshakes {
- log.Printf("Error processing handshake for session %s: %v", session.ID(), err)
- }
- sendError(ctx, reply, err)
- return nil
- }
- if logHandshakes {
- log.Printf("Session %s: got handshake. Logfile: %q, Debug addr: %q", session.ID(), req.Logfile, req.DebugAddr)
- }
- event.Log(ctx, "Handshake session update",
- cache.KeyUpdateSession.Of(session),
- tag.DebugAddress.Of(req.DebugAddr),
- tag.Logfile.Of(req.Logfile),
- tag.ServerID.Of(req.ServerID),
- tag.GoplsPath.Of(req.GoplsPath),
- )
- resp := handshakeResponse{
- SessionID: session.ID(),
- GoplsPath: goplsPath,
- }
- if di := debug.GetInstance(ctx); di != nil {
- resp.Logfile = di.Logfile
- resp.DebugAddr = di.ListenedDebugAddress
- }
-
- return reply(ctx, resp, nil)
- case sessionsMethod:
- resp := ServerState{
- GoplsPath: goplsPath,
- CurrentClientID: session.ID(),
- }
- if di := debug.GetInstance(ctx); di != nil {
- resp.Logfile = di.Logfile
- resp.DebugAddr = di.ListenedDebugAddress
- for _, c := range di.State.Clients() {
- resp.Clients = append(resp.Clients, ClientSession{
- SessionID: c.Session.ID(),
- Logfile: c.Logfile,
- DebugAddr: c.DebugAddress,
- })
- }
- }
- return reply(ctx, resp, nil)
- }
- return handler(ctx, reply, r)
- }
-}
-
-func sendError(ctx context.Context, reply jsonrpc2.Replier, err error) {
- err = errors.Errorf("%v: %w", err, jsonrpc2.ErrParse)
- if err := reply(ctx, nil, err); err != nil {
- event.Error(ctx, "", err)
- }
-}