From fc1d57b08d7be19e6befb8d9a9405221faa3d5c9 Mon Sep 17 00:00:00 2001 From: Rebecca Stambler Date: Thu, 10 Jan 2019 20:42:25 -0500 Subject: [PATCH] cmd/golsp: add a debugging tool to connect with golsp on a port This change allows golsp to be run on a port, with an intermediary command passing the data through. This allows for improved logging. Also, add necessary changes to VSCode integration to allow changing the name of the command for golsp. Change-Id: I20dca1a50296636e57e022342ee70f0610ad1531 Reviewed-on: https://go-review.googlesource.com/c/157497 Run-TryBot: Rebecca Stambler Reviewed-by: Ian Cottrell --- cmd/golsp/forward/main.go | 59 +++++++++ cmd/golsp/integration/vscode/package.json | 6 + cmd/golsp/integration/vscode/src/extension.ts | 3 +- internal/lsp/cmd/cmd.go | 2 +- internal/lsp/cmd/server.go | 113 +++++++++--------- internal/lsp/server.go | 24 ++++ 6 files changed, 150 insertions(+), 57 deletions(-) create mode 100644 cmd/golsp/forward/main.go diff --git a/cmd/golsp/forward/main.go b/cmd/golsp/forward/main.go new file mode 100644 index 0000000000..1139fe9d98 --- /dev/null +++ b/cmd/golsp/forward/main.go @@ -0,0 +1,59 @@ +// The forward command writes and reads to a golsp server on a network socket. +package main + +import ( + "context" + "flag" + "fmt" + "io" + "log" + "net" + "os" + + "golang.org/x/tools/internal/lsp/cmd" + "golang.org/x/tools/internal/tool" +) + +func main() { + tool.Main(context.Background(), &app{&cmd.Server{}}, os.Args[1:]) +} + +type app struct { + *cmd.Server +} + +func (*app) Name() string { return "forward" } +func (*app) Usage() string { return "[-port=]" } +func (*app) ShortHelp() string { return "An intermediary between an editor and GoLSP." } +func (*app) DetailedHelp(*flag.FlagSet) {} + +func (a *app) Run(ctx context.Context, args ...string) error { + if a.Server.Port == 0 { + a.ShortHelp() + os.Exit(0) + } + conn, err := net.Dial("tcp", fmt.Sprintf(":%v", a.Server.Port)) + if err != nil { + log.Print(err) + os.Exit(0) + } + + go func(conn net.Conn) { + _, err := io.Copy(conn, os.Stdin) + if err != nil { + log.Print(err) + os.Exit(0) + } + }(conn) + + go func(conn net.Conn) { + _, err := io.Copy(os.Stdout, conn) + if err != nil { + log.Print(err) + os.Exit(0) + } + }(conn) + + for { + } +} diff --git a/cmd/golsp/integration/vscode/package.json b/cmd/golsp/integration/vscode/package.json index e4d1ffc589..1665d5f4ab 100644 --- a/cmd/golsp/integration/vscode/package.json +++ b/cmd/golsp/integration/vscode/package.json @@ -45,6 +45,12 @@ "default": [], "description": "Flags to pass to golsp", "scope": "resource" + }, + "golsp.command": { + "type": "string", + "default": "golsp", + "description": "Name of the GoLSP binary", + "scope": "resource" } } } diff --git a/cmd/golsp/integration/vscode/src/extension.ts b/cmd/golsp/integration/vscode/src/extension.ts index 0767b6891c..5725be6bf6 100644 --- a/cmd/golsp/integration/vscode/src/extension.ts +++ b/cmd/golsp/integration/vscode/src/extension.ts @@ -12,9 +12,10 @@ import path = require('path'); export function activate(ctx: vscode.ExtensionContext): void { let document = vscode.window.activeTextEditor.document; let config = vscode.workspace.getConfiguration('golsp', document.uri); + let golspCommand: string = config['command']; let golspFlags: string[] = config['flags']; let serverOptions: - lsp.ServerOptions = {command: getBinPath('golsp'), args: golspFlags}; + lsp.ServerOptions = {command: getBinPath(golspCommand), args: golspFlags}; let clientOptions: lsp.LanguageClientOptions = { initializationOptions: {}, documentSelector: ['go'], diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go index f5041d906c..2320838079 100644 --- a/internal/lsp/cmd/cmd.go +++ b/internal/lsp/cmd/cmd.go @@ -23,7 +23,7 @@ type Application struct { // we also include the server directly for now, so the flags work even without // the verb. We should remove this when we stop allowing the server verb by // default - Server server + Server Server } // Name implements tool.Application returning the binary name. diff --git a/internal/lsp/cmd/server.go b/internal/lsp/cmd/server.go index ed3b6e0b5b..f6ab95a299 100644 --- a/internal/lsp/cmd/server.go +++ b/internal/lsp/cmd/server.go @@ -21,19 +21,20 @@ import ( "golang.org/x/tools/internal/tool" ) -// server is a struct that exposes the configurable parts of the LSP server as +// Server is a struct that exposes the configurable parts of the LSP server as // flags, in the right form for tool.Main to consume. -type server struct { +type Server 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"` + Port int `flag:"port" help:"port on which to run golsp for debugging purposes"` } -func (s *server) Name() string { return "server" } -func (s *server) Usage() string { return "" } -func (s *server) ShortHelp() string { +func (s *Server) Name() string { return "server" } +func (s *Server) Usage() string { return "" } +func (s *Server) ShortHelp() string { return "run a server for Go code using the Language Server Protocol" } -func (s *server) DetailedHelp(f *flag.FlagSet) { +func (s *Server) DetailedHelp(f *flag.FlagSet) { fmt.Fprint(f.Output(), ` The server communicates using JSONRPC2 on stdin and stdout, and is intended to be run directly as a child of an editor process. @@ -42,7 +43,7 @@ a child of an editor process. // Run configures a server based on the flags, and then runs it. // It blocks until the server shuts down. -func (s *server) Run(ctx context.Context, args ...string) error { +func (s *Server) Run(ctx context.Context, args ...string) error { if len(args) > 0 { return tool.CommandLineErrorf("server does not take arguments, got %v", args) } @@ -60,52 +61,54 @@ func (s *server) Run(ctx context.Context, args ...string) error { log.SetOutput(io.MultiWriter(os.Stderr, f)) out = f } - return lsp.RunServer( - ctx, - jsonrpc2.NewHeaderStream(os.Stdin, os.Stdout), - func(direction jsonrpc2.Direction, id *jsonrpc2.ID, elapsed time.Duration, method string, payload *json.RawMessage, err *jsonrpc2.Error) { - const eol = "\r\n\r\n\r\n" - if err != nil { - fmt.Fprintf(out, "[Error - %v] %s %s%s %v%s", time.Now().Format("3:04:05 PM"), - direction, method, id, err, eol) - return - } - outx := new(strings.Builder) - fmt.Fprintf(outx, "[Trace - %v] ", time.Now().Format("3:04:05 PM")) - switch direction { - case jsonrpc2.Send: - fmt.Fprint(outx, "Received ") - case jsonrpc2.Receive: - fmt.Fprint(outx, "Sending ") - } - switch { - case id == nil: - fmt.Fprint(outx, "notification ") - case elapsed >= 0: - fmt.Fprint(outx, "response ") - default: - fmt.Fprint(outx, "request ") - } - fmt.Fprintf(outx, "'%s", method) - switch { - case id == nil: - // do nothing - case id.Name != "": - fmt.Fprintf(outx, " - (%s)", id.Name) - default: - fmt.Fprintf(outx, " - (%d)", id.Number) - } - fmt.Fprint(outx, "'") - if elapsed >= 0 { - msec := int(elapsed.Round(time.Millisecond) / time.Millisecond) - fmt.Fprintf(outx, " in %dms", msec) - } - params := string(*payload) - if params == "null" { - params = "{}" - } - fmt.Fprintf(outx, ".\r\nParams: %s%s", params, eol) - fmt.Fprintf(out, "%s", outx.String()) - }, - ) + logger := func(direction jsonrpc2.Direction, id *jsonrpc2.ID, elapsed time.Duration, method string, payload *json.RawMessage, err *jsonrpc2.Error) { + const eol = "\r\n\r\n\r\n" + if err != nil { + fmt.Fprintf(out, "[Error - %v] %s %s%s %v%s", time.Now().Format("3:04:05 PM"), + direction, method, id, err, eol) + return + } + outx := new(strings.Builder) + fmt.Fprintf(outx, "[Trace - %v] ", time.Now().Format("3:04:05 PM")) + switch direction { + case jsonrpc2.Send: + fmt.Fprint(outx, "Received ") + case jsonrpc2.Receive: + fmt.Fprint(outx, "Sending ") + } + switch { + case id == nil: + fmt.Fprint(outx, "notification ") + case elapsed >= 0: + fmt.Fprint(outx, "response ") + default: + fmt.Fprint(outx, "request ") + } + fmt.Fprintf(outx, "'%s", method) + switch { + case id == nil: + // do nothing + case id.Name != "": + fmt.Fprintf(outx, " - (%s)", id.Name) + default: + fmt.Fprintf(outx, " - (%d)", id.Number) + } + fmt.Fprint(outx, "'") + if elapsed >= 0 { + msec := int(elapsed.Round(time.Millisecond) / time.Millisecond) + fmt.Fprintf(outx, " in %dms", msec) + } + params := string(*payload) + if params == "null" { + params = "{}" + } + fmt.Fprintf(outx, ".\r\nParams: %s%s", params, eol) + fmt.Fprintf(out, "%s", outx.String()) + } + // For debugging purposes only. + if s.Port != 0 { + return lsp.RunServerOnPort(ctx, s.Port, logger) + } + stream := jsonrpc2.NewHeaderStream(os.Stdin, os.Stdout) + return lsp.RunServer(ctx, stream, logger) } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index cdd07f2cb3..4460816402 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -6,7 +6,9 @@ package lsp import ( "context" + "fmt" "go/token" + "net" "os" "sync" @@ -26,6 +28,28 @@ func RunServer(ctx context.Context, stream jsonrpc2.Stream, opts ...interface{}) return conn.Wait(ctx) } +// RunServerOnPort starts an LSP server on the given port and does not exit. +// This function exists for debugging purposes. +func RunServerOnPort(ctx context.Context, port int, opts ...interface{}) error { + s := &server{} + ln, err := net.Listen("tcp", fmt.Sprintf(":%v", port)) + if err != nil { + return err + } + for { + conn, err := ln.Accept() + if err != nil { + return err + } + stream := jsonrpc2.NewHeaderStream(conn, conn) + go func() { + conn, client := protocol.RunServer(ctx, stream, s, opts...) + s.client = client + conn.Wait(ctx) + }() + } +} + type server struct { client protocol.Client