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