golang.org/x/tools/gopls@v0.15.3/internal/lsprpc/lsprpc.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package lsprpc implements a jsonrpc2.StreamServer that may be used to
     6  // serve the LSP on a jsonrpc2 channel.
     7  package lsprpc
     8  
     9  import (
    10  	"context"
    11  	"encoding/json"
    12  	"fmt"
    13  	"log"
    14  	"net"
    15  	"os"
    16  	"strconv"
    17  	"strings"
    18  	"sync"
    19  	"sync/atomic"
    20  	"time"
    21  
    22  	"golang.org/x/tools/gopls/internal/cache"
    23  	"golang.org/x/tools/gopls/internal/debug"
    24  	"golang.org/x/tools/gopls/internal/protocol"
    25  	"golang.org/x/tools/gopls/internal/protocol/command"
    26  	"golang.org/x/tools/gopls/internal/server"
    27  	"golang.org/x/tools/gopls/internal/settings"
    28  	"golang.org/x/tools/internal/event"
    29  	"golang.org/x/tools/internal/event/tag"
    30  	"golang.org/x/tools/internal/jsonrpc2"
    31  )
    32  
    33  // Unique identifiers for client/server.
    34  var serverIndex int64
    35  
    36  // The streamServer type is a jsonrpc2.streamServer that handles incoming
    37  // streams as a new LSP session, using a shared cache.
    38  type streamServer struct {
    39  	cache *cache.Cache
    40  	// daemon controls whether or not to log new connections.
    41  	daemon bool
    42  
    43  	// optionsOverrides is passed to newly created sessions.
    44  	optionsOverrides func(*settings.Options)
    45  
    46  	// serverForTest may be set to a test fake for testing.
    47  	serverForTest protocol.Server
    48  }
    49  
    50  // NewStreamServer creates a StreamServer using the shared cache. If
    51  // withTelemetry is true, each session is instrumented with telemetry that
    52  // records RPC statistics.
    53  func NewStreamServer(cache *cache.Cache, daemon bool, optionsFunc func(*settings.Options)) jsonrpc2.StreamServer {
    54  	return &streamServer{cache: cache, daemon: daemon, optionsOverrides: optionsFunc}
    55  }
    56  
    57  // ServeStream implements the jsonrpc2.StreamServer interface, by handling
    58  // incoming streams using a new lsp server.
    59  func (s *streamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) error {
    60  	client := protocol.ClientDispatcher(conn)
    61  	session := cache.NewSession(ctx, s.cache)
    62  	svr := s.serverForTest
    63  	if svr == nil {
    64  		options := settings.DefaultOptions(s.optionsOverrides)
    65  		svr = server.New(session, client, options)
    66  		if instance := debug.GetInstance(ctx); instance != nil {
    67  			instance.AddService(svr, session)
    68  		}
    69  	}
    70  	// Clients may or may not send a shutdown message. Make sure the server is
    71  	// shut down.
    72  	// TODO(rFindley): this shutdown should perhaps be on a disconnected context.
    73  	defer func() {
    74  		if err := svr.Shutdown(ctx); err != nil {
    75  			event.Error(ctx, "error shutting down", err)
    76  		}
    77  	}()
    78  	executable, err := os.Executable()
    79  	if err != nil {
    80  		log.Printf("error getting gopls path: %v", err)
    81  		executable = ""
    82  	}
    83  	ctx = protocol.WithClient(ctx, client)
    84  	conn.Go(ctx,
    85  		protocol.Handlers(
    86  			handshaker(session, executable, s.daemon,
    87  				protocol.ServerHandler(svr,
    88  					jsonrpc2.MethodNotFound))))
    89  	if s.daemon {
    90  		log.Printf("Session %s: connected", session.ID())
    91  		defer log.Printf("Session %s: exited", session.ID())
    92  	}
    93  	<-conn.Done()
    94  	return conn.Err()
    95  }
    96  
    97  // A forwarder is a jsonrpc2.StreamServer that handles an LSP stream by
    98  // forwarding it to a remote. This is used when the gopls process started by
    99  // the editor is in the `-remote` mode, which means it finds and connects to a
   100  // separate gopls daemon. In these cases, we still want the forwarder gopls to
   101  // be instrumented with telemetry, and want to be able to in some cases hijack
   102  // the jsonrpc2 connection with the daemon.
   103  type forwarder struct {
   104  	dialer *autoDialer
   105  
   106  	mu sync.Mutex
   107  	// Hold on to the server connection so that we can redo the handshake if any
   108  	// information changes.
   109  	serverConn jsonrpc2.Conn
   110  	serverID   string
   111  }
   112  
   113  // NewForwarder creates a new forwarder (a [jsonrpc2.StreamServer]),
   114  // ready to forward connections to the
   115  // remote server specified by rawAddr. If provided and rawAddr indicates an
   116  // 'automatic' address (starting with 'auto;'), argFunc may be used to start a
   117  // remote server for the auto-discovered address.
   118  func NewForwarder(rawAddr string, argFunc func(network, address string) []string) (jsonrpc2.StreamServer, error) {
   119  	dialer, err := newAutoDialer(rawAddr, argFunc)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	fwd := &forwarder{
   124  		dialer: dialer,
   125  	}
   126  	return fwd, nil
   127  }
   128  
   129  // QueryServerState returns a JSON-encodable struct describing the state of the named server.
   130  func QueryServerState(ctx context.Context, addr string) (any, error) {
   131  	serverConn, err := dialRemote(ctx, addr)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	var state serverState
   136  	if err := protocol.Call(ctx, serverConn, sessionsMethod, nil, &state); err != nil {
   137  		return nil, fmt.Errorf("querying server state: %w", err)
   138  	}
   139  	return &state, nil
   140  }
   141  
   142  // dialRemote is used for making calls into the gopls daemon. addr should be a
   143  // URL, possibly on the synthetic 'auto' network (e.g. tcp://..., unix://...,
   144  // or auto://...).
   145  func dialRemote(ctx context.Context, addr string) (jsonrpc2.Conn, error) {
   146  	network, address := ParseAddr(addr)
   147  	if network == autoNetwork {
   148  		gp, err := os.Executable()
   149  		if err != nil {
   150  			return nil, fmt.Errorf("getting gopls path: %w", err)
   151  		}
   152  		network, address = autoNetworkAddress(gp, address)
   153  	}
   154  	netConn, err := net.DialTimeout(network, address, 5*time.Second)
   155  	if err != nil {
   156  		return nil, fmt.Errorf("dialing remote: %w", err)
   157  	}
   158  	serverConn := jsonrpc2.NewConn(jsonrpc2.NewHeaderStream(netConn))
   159  	serverConn.Go(ctx, jsonrpc2.MethodNotFound)
   160  	return serverConn, nil
   161  }
   162  
   163  // ExecuteCommand connects to the named server, sends it a
   164  // workspace/executeCommand request (with command 'id' and arguments
   165  // JSON encoded in 'request'), and populates the result variable.
   166  func ExecuteCommand(ctx context.Context, addr string, id string, request, result any) error {
   167  	serverConn, err := dialRemote(ctx, addr)
   168  	if err != nil {
   169  		return err
   170  	}
   171  	args, err := command.MarshalArgs(request)
   172  	if err != nil {
   173  		return err
   174  	}
   175  	params := protocol.ExecuteCommandParams{
   176  		Command:   id,
   177  		Arguments: args,
   178  	}
   179  	return protocol.Call(ctx, serverConn, "workspace/executeCommand", params, result)
   180  }
   181  
   182  // ServeStream dials the forwarder remote and binds the remote to serve the LSP
   183  // on the incoming stream.
   184  func (f *forwarder) ServeStream(ctx context.Context, clientConn jsonrpc2.Conn) error {
   185  	client := protocol.ClientDispatcher(clientConn)
   186  
   187  	netConn, err := f.dialer.dialNet(ctx)
   188  	if err != nil {
   189  		return fmt.Errorf("forwarder: connecting to remote: %w", err)
   190  	}
   191  	serverConn := jsonrpc2.NewConn(jsonrpc2.NewHeaderStream(netConn))
   192  	server := protocol.ServerDispatcher(serverConn)
   193  
   194  	// Forward between connections.
   195  	serverConn.Go(ctx,
   196  		protocol.Handlers(
   197  			protocol.ClientHandler(client,
   198  				jsonrpc2.MethodNotFound)))
   199  
   200  	// Don't run the clientConn yet, so that we can complete the handshake before
   201  	// processing any client messages.
   202  
   203  	// Do a handshake with the server instance to exchange debug information.
   204  	index := atomic.AddInt64(&serverIndex, 1)
   205  	f.mu.Lock()
   206  	f.serverConn = serverConn
   207  	f.serverID = strconv.FormatInt(index, 10)
   208  	f.mu.Unlock()
   209  	f.handshake(ctx)
   210  	clientConn.Go(ctx,
   211  		protocol.Handlers(
   212  			f.handler(
   213  				protocol.ServerHandler(server,
   214  					jsonrpc2.MethodNotFound))))
   215  
   216  	select {
   217  	case <-serverConn.Done():
   218  		clientConn.Close()
   219  	case <-clientConn.Done():
   220  		serverConn.Close()
   221  	}
   222  
   223  	err = nil
   224  	if serverConn.Err() != nil {
   225  		err = fmt.Errorf("remote disconnected: %v", serverConn.Err())
   226  	} else if clientConn.Err() != nil {
   227  		err = fmt.Errorf("client disconnected: %v", clientConn.Err())
   228  	}
   229  	event.Log(ctx, fmt.Sprintf("forwarder: exited with error: %v", err))
   230  	return err
   231  }
   232  
   233  // TODO(rfindley): remove this handshaking in favor of middleware.
   234  func (f *forwarder) handshake(ctx context.Context) {
   235  	// This call to os.Executable is redundant, and will be eliminated by the
   236  	// transition to the V2 API.
   237  	goplsPath, err := os.Executable()
   238  	if err != nil {
   239  		event.Error(ctx, "getting executable for handshake", err)
   240  		goplsPath = ""
   241  	}
   242  	var (
   243  		hreq = handshakeRequest{
   244  			ServerID:  f.serverID,
   245  			GoplsPath: goplsPath,
   246  		}
   247  		hresp handshakeResponse
   248  	)
   249  	if di := debug.GetInstance(ctx); di != nil {
   250  		hreq.Logfile = di.Logfile
   251  		hreq.DebugAddr = di.ListenedDebugAddress()
   252  	}
   253  	if err := protocol.Call(ctx, f.serverConn, handshakeMethod, hreq, &hresp); err != nil {
   254  		// TODO(rfindley): at some point in the future we should return an error
   255  		// here.  Handshakes have become functional in nature.
   256  		event.Error(ctx, "forwarder: gopls handshake failed", err)
   257  	}
   258  	if hresp.GoplsPath != goplsPath {
   259  		event.Error(ctx, "", fmt.Errorf("forwarder: gopls path mismatch: forwarder is %q, remote is %q", goplsPath, hresp.GoplsPath))
   260  	}
   261  	event.Log(ctx, "New server",
   262  		tag.NewServer.Of(f.serverID),
   263  		tag.Logfile.Of(hresp.Logfile),
   264  		tag.DebugAddress.Of(hresp.DebugAddr),
   265  		tag.GoplsPath.Of(hresp.GoplsPath),
   266  		tag.ClientID.Of(hresp.SessionID),
   267  	)
   268  }
   269  
   270  func ConnectToRemote(ctx context.Context, addr string) (net.Conn, error) {
   271  	dialer, err := newAutoDialer(addr, nil)
   272  	if err != nil {
   273  		return nil, err
   274  	}
   275  	return dialer.dialNet(ctx)
   276  }
   277  
   278  // handler intercepts messages to the daemon to enrich them with local
   279  // information.
   280  func (f *forwarder) handler(handler jsonrpc2.Handler) jsonrpc2.Handler {
   281  	return func(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2.Request) error {
   282  		// Intercept certain messages to add special handling.
   283  		switch r.Method() {
   284  		case "initialize":
   285  			if newr, err := addGoEnvToInitializeRequest(ctx, r); err == nil {
   286  				r = newr
   287  			} else {
   288  				log.Printf("unable to add local env to initialize request: %v", err)
   289  			}
   290  		case "workspace/executeCommand":
   291  			var params protocol.ExecuteCommandParams
   292  			if err := json.Unmarshal(r.Params(), &params); err == nil {
   293  				if params.Command == command.StartDebugging.ID() {
   294  					var args command.DebuggingArgs
   295  					if err := command.UnmarshalArgs(params.Arguments, &args); err == nil {
   296  						reply = f.replyWithDebugAddress(ctx, reply, args)
   297  					} else {
   298  						event.Error(ctx, "unmarshaling debugging args", err)
   299  					}
   300  				}
   301  			} else {
   302  				event.Error(ctx, "intercepting executeCommand request", err)
   303  			}
   304  		}
   305  		// The gopls workspace environment defaults to the process environment in
   306  		// which gopls daemon was started. To avoid discrepancies in Go environment
   307  		// between the editor and daemon, inject any unset variables in `go env`
   308  		// into the options sent by initialize.
   309  		//
   310  		// See also golang.org/issue/37830.
   311  		return handler(ctx, reply, r)
   312  	}
   313  }
   314  
   315  // addGoEnvToInitializeRequest builds a new initialize request in which we set
   316  // any environment variables output by `go env` and not already present in the
   317  // request.
   318  //
   319  // It returns an error if r is not an initialize request, or is otherwise
   320  // malformed.
   321  func addGoEnvToInitializeRequest(ctx context.Context, r jsonrpc2.Request) (jsonrpc2.Request, error) {
   322  	var params protocol.ParamInitialize
   323  	if err := json.Unmarshal(r.Params(), &params); err != nil {
   324  		return nil, err
   325  	}
   326  	var opts map[string]interface{}
   327  	switch v := params.InitializationOptions.(type) {
   328  	case nil:
   329  		opts = make(map[string]interface{})
   330  	case map[string]interface{}:
   331  		opts = v
   332  	default:
   333  		return nil, fmt.Errorf("unexpected type for InitializationOptions: %T", v)
   334  	}
   335  	envOpt, ok := opts["env"]
   336  	if !ok {
   337  		envOpt = make(map[string]interface{})
   338  	}
   339  	env, ok := envOpt.(map[string]interface{})
   340  	if !ok {
   341  		return nil, fmt.Errorf(`env option is %T, expected a map`, envOpt)
   342  	}
   343  	goenv, err := getGoEnv(ctx, env)
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  	// We don't want to propagate GOWORK unless explicitly set since that could mess with
   348  	// path inference during cmd/go invocations, see golang/go#51825.
   349  	_, goworkSet := os.LookupEnv("GOWORK")
   350  	for govar, value := range goenv {
   351  		if govar == "GOWORK" && !goworkSet {
   352  			continue
   353  		}
   354  		env[govar] = value
   355  	}
   356  	opts["env"] = env
   357  	params.InitializationOptions = opts
   358  	call, ok := r.(*jsonrpc2.Call)
   359  	if !ok {
   360  		return nil, fmt.Errorf("%T is not a *jsonrpc2.Call", r)
   361  	}
   362  	return jsonrpc2.NewCall(call.ID(), "initialize", params)
   363  }
   364  
   365  func (f *forwarder) replyWithDebugAddress(outerCtx context.Context, r jsonrpc2.Replier, args command.DebuggingArgs) jsonrpc2.Replier {
   366  	di := debug.GetInstance(outerCtx)
   367  	if di == nil {
   368  		event.Log(outerCtx, "no debug instance to start")
   369  		return r
   370  	}
   371  	return func(ctx context.Context, result interface{}, outerErr error) error {
   372  		if outerErr != nil {
   373  			return r(ctx, result, outerErr)
   374  		}
   375  		// Enrich the result with our own debugging information. Since we're an
   376  		// intermediary, the jsonrpc2 package has deserialized the result into
   377  		// maps, by default. Re-do the unmarshalling.
   378  		raw, err := json.Marshal(result)
   379  		if err != nil {
   380  			event.Error(outerCtx, "marshaling intermediate command result", err)
   381  			return r(ctx, result, err)
   382  		}
   383  		var modified command.DebuggingResult
   384  		if err := json.Unmarshal(raw, &modified); err != nil {
   385  			event.Error(outerCtx, "unmarshaling intermediate command result", err)
   386  			return r(ctx, result, err)
   387  		}
   388  		addr := args.Addr
   389  		if addr == "" {
   390  			addr = "localhost:0"
   391  		}
   392  		addr, err = di.Serve(outerCtx, addr)
   393  		if err != nil {
   394  			event.Error(outerCtx, "starting debug server", err)
   395  			return r(ctx, result, outerErr)
   396  		}
   397  		urls := []string{"http://" + addr}
   398  		modified.URLs = append(urls, modified.URLs...)
   399  		go f.handshake(ctx)
   400  		return r(ctx, modified, nil)
   401  	}
   402  }
   403  
   404  // A handshakeRequest identifies a client to the LSP server.
   405  type handshakeRequest struct {
   406  	// ServerID is the ID of the server on the client. This should usually be 0.
   407  	ServerID string `json:"serverID"`
   408  	// Logfile is the location of the clients log file.
   409  	Logfile string `json:"logfile"`
   410  	// DebugAddr is the client debug address.
   411  	DebugAddr string `json:"debugAddr"`
   412  	// GoplsPath is the path to the Gopls binary running the current client
   413  	// process.
   414  	GoplsPath string `json:"goplsPath"`
   415  }
   416  
   417  // A handshakeResponse is returned by the LSP server to tell the LSP client
   418  // information about its session.
   419  type handshakeResponse struct {
   420  	// SessionID is the server session associated with the client.
   421  	SessionID string `json:"sessionID"`
   422  	// Logfile is the location of the server logs.
   423  	Logfile string `json:"logfile"`
   424  	// DebugAddr is the server debug address.
   425  	DebugAddr string `json:"debugAddr"`
   426  	// GoplsPath is the path to the Gopls binary running the current server
   427  	// process.
   428  	GoplsPath string `json:"goplsPath"`
   429  }
   430  
   431  // clientSession identifies a current client LSP session on the server. Note
   432  // that it looks similar to handshakeResposne, but in fact 'Logfile' and
   433  // 'DebugAddr' now refer to the client.
   434  type clientSession struct {
   435  	SessionID string `json:"sessionID"`
   436  	Logfile   string `json:"logfile"`
   437  	DebugAddr string `json:"debugAddr"`
   438  }
   439  
   440  // serverState holds information about the gopls daemon process, including its
   441  // debug information and debug information of all of its current connected
   442  // clients.
   443  type serverState struct {
   444  	Logfile         string          `json:"logfile"`
   445  	DebugAddr       string          `json:"debugAddr"`
   446  	GoplsPath       string          `json:"goplsPath"`
   447  	CurrentClientID string          `json:"currentClientID"`
   448  	Clients         []clientSession `json:"clients"`
   449  }
   450  
   451  const (
   452  	handshakeMethod = "gopls/handshake"
   453  	sessionsMethod  = "gopls/sessions"
   454  )
   455  
   456  func handshaker(session *cache.Session, goplsPath string, logHandshakes bool, handler jsonrpc2.Handler) jsonrpc2.Handler {
   457  	return func(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2.Request) error {
   458  		switch r.Method() {
   459  		case handshakeMethod:
   460  			// We log.Printf in this handler, rather than event.Log when we want logs
   461  			// to go to the daemon log rather than being reflected back to the
   462  			// client.
   463  			var req handshakeRequest
   464  			if err := json.Unmarshal(r.Params(), &req); err != nil {
   465  				if logHandshakes {
   466  					log.Printf("Error processing handshake for session %s: %v", session.ID(), err)
   467  				}
   468  				sendError(ctx, reply, err)
   469  				return nil
   470  			}
   471  			if logHandshakes {
   472  				log.Printf("Session %s: got handshake. Logfile: %q, Debug addr: %q", session.ID(), req.Logfile, req.DebugAddr)
   473  			}
   474  			event.Log(ctx, "Handshake session update",
   475  				cache.KeyUpdateSession.Of(session),
   476  				tag.DebugAddress.Of(req.DebugAddr),
   477  				tag.Logfile.Of(req.Logfile),
   478  				tag.ServerID.Of(req.ServerID),
   479  				tag.GoplsPath.Of(req.GoplsPath),
   480  			)
   481  			resp := handshakeResponse{
   482  				SessionID: session.ID(),
   483  				GoplsPath: goplsPath,
   484  			}
   485  			if di := debug.GetInstance(ctx); di != nil {
   486  				resp.Logfile = di.Logfile
   487  				resp.DebugAddr = di.ListenedDebugAddress()
   488  			}
   489  			return reply(ctx, resp, nil)
   490  
   491  		case sessionsMethod:
   492  			resp := serverState{
   493  				GoplsPath:       goplsPath,
   494  				CurrentClientID: session.ID(),
   495  			}
   496  			if di := debug.GetInstance(ctx); di != nil {
   497  				resp.Logfile = di.Logfile
   498  				resp.DebugAddr = di.ListenedDebugAddress()
   499  				for _, c := range di.State.Clients() {
   500  					resp.Clients = append(resp.Clients, clientSession{
   501  						SessionID: c.Session.ID(),
   502  						Logfile:   c.Logfile,
   503  						DebugAddr: c.DebugAddress,
   504  					})
   505  				}
   506  			}
   507  			return reply(ctx, resp, nil)
   508  		}
   509  		return handler(ctx, reply, r)
   510  	}
   511  }
   512  
   513  func sendError(ctx context.Context, reply jsonrpc2.Replier, err error) {
   514  	err = fmt.Errorf("%v: %w", err, jsonrpc2.ErrParse)
   515  	if err := reply(ctx, nil, err); err != nil {
   516  		event.Error(ctx, "", err)
   517  	}
   518  }
   519  
   520  // ParseAddr parses the address of a gopls remote.
   521  // TODO(rFindley): further document this syntax, and allow URI-style remote
   522  // addresses such as "auto://...".
   523  func ParseAddr(listen string) (network string, address string) {
   524  	// Allow passing just -remote=auto, as a shorthand for using automatic remote
   525  	// resolution.
   526  	if listen == autoNetwork {
   527  		return autoNetwork, ""
   528  	}
   529  	if parts := strings.SplitN(listen, ";", 2); len(parts) == 2 {
   530  		return parts[0], parts[1]
   531  	}
   532  	return "tcp", listen
   533  }