blob: 2ffe26978e8af847765669f3671948d9e71b9f90 [file]
// 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 cmd
import (
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"strings"
"time"
"golang.org/x/sync/errgroup"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/debug"
"golang.org/x/tools/gopls/internal/lsprpc"
"golang.org/x/tools/gopls/internal/mcp"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/fakenet"
"golang.org/x/tools/internal/jsonrpc2"
)
// serve defines the flags and working state of the gopls serve command.
type serve struct {
Logfile string `flag:"logfile" help:"filename to log to. if value is \"auto\", then logging to a default output file is enabled"`
Mode string `flag:"mode" help:"no effect"`
Address string `flag:"listen" help:"address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used."`
IdleTimeout time.Duration `flag:"listen.timeout" help:"when used with -listen, shut down the server when there are no connected clients for this duration"`
Trace bool `flag:"rpc.trace" help:"print the full rpc trace in lsp inspector format"`
Debug string `flag:"debug" help:"serve debug information on the supplied address"`
// MCP Server related configurations.
MCPAddress string `flag:"mcp.listen" help:"experimental: address on which to listen for model context protocol connections. If port is localhost:0, pick a random port in localhost instead."`
app *application
}
func (s *serve) Name() string { return "serve" }
func (s *serve) Parent() string { return s.app.Name() }
func (s *serve) Usage() string { return "[server-flags]" }
func (s *serve) ShortHelp() string {
return "run a server for Go code using the Language Server Protocol"
}
func (s *serve) DetailedHelp(f *flag.FlagSet) {
fmt.Fprint(f.Output(), ` gopls [flags] [server-flags]
The server communicates using JSONRPC2 on stdin and stdout, and is intended to be run directly as
a child of an editor process.
server-flags:
`)
printFlagDefaults(f)
}
// Run configures a server based on the flags, and then runs it.
// It blocks until the server shuts down.
func (s *serve) Run(ctx context.Context, args ...string) error {
if len(args) > 0 {
return commandLineErrorf("server does not take arguments, got %v", args)
}
di := debug.GetInstance(ctx)
isDaemon := s.Address != ""
if di != nil {
closeLog, err := di.SetLogFile(s.Logfile, isDaemon)
if err != nil {
return err
}
defer closeLog()
di.ServerAddress = s.Address
di.Serve(ctx, s.Debug)
}
var (
ss jsonrpc2.StreamServer
sessions mcp.Sessions // if non-nil, handle MCP sessions
)
if s.app.Remote != "" {
var err error
ss, err = lsprpc.NewForwarder(s.app.Remote, s.app.remoteArgs)
if err != nil {
return fmt.Errorf("creating forwarder: %w", err)
}
} else {
lsprpcServer := lsprpc.NewStreamServer(cache.New(nil), isDaemon, s.app.options)
ss = lsprpcServer
if s.MCPAddress != "" {
sessions = lsprpcServer
}
}
group, ctx := errgroup.WithContext(ctx)
// Indicate success by a special error so that successful termination
// of one server causes cancellation of the other.
success := errors.New("success")
// Start MCP server.
if sessions != nil {
countAttachedMCP.Inc()
group.Go(func() (err error) {
defer func() {
if err == nil {
err = success
}
}()
return mcp.Serve(ctx, s.MCPAddress, sessions, isDaemon, nil)
})
}
// Start LSP server.
group.Go(func() (err error) {
defer func() {
if err == nil {
err = success
}
}()
if s.Address != "" {
// -listen=address
network, addr := lsprpc.ParseAddr(s.Address)
if strings.HasPrefix(addr, ":") {
return fmt.Errorf("-listen=%s implicitly binds all network interfaces; please use an explicit host such as 0.0.0.0 (all interfaces) or localhost (safer)", addr)
}
log.Printf("Gopls LSP daemon: listening on %s network, address %s...", network, addr)
defer log.Printf("Gopls LSP daemon: exiting")
return jsonrpc2.ListenAndServe(ctx, network, addr, ss, s.IdleTimeout)
} else {
// communicate over stdin/stdout
stream := jsonrpc2.NewHeaderStream(fakenet.NewConn("stdio", os.Stdin, os.Stdout))
if s.Trace && di != nil {
stream = protocol.LoggingStream(stream, di.LogWriter)
}
conn := jsonrpc2.NewConn(stream)
if err := ss.ServeStream(ctx, conn); errors.Is(err, io.EOF) {
return nil
} else {
return err
}
}
})
// Wait for all servers to terminate, returning only the first error
// encountered. Subsequent errors are typically due to context cancellation
// and are disregarded.
if err := group.Wait(); err != nil && !errors.Is(err, success) {
return err
}
return nil
}