| // Copyright 2019 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 protocol |
| |
| import ( |
| "context" |
| "fmt" |
| "io" |
| "strings" |
| "sync" |
| "time" |
| |
| "golang.org/x/tools/internal/jsonrpc2" |
| ) |
| |
| type loggingStream struct { |
| stream jsonrpc2.Stream |
| logMu sync.Mutex |
| log io.Writer |
| } |
| |
| // LoggingStream returns a stream that does LSP protocol logging too |
| func LoggingStream(str jsonrpc2.Stream, w io.Writer) jsonrpc2.Stream { |
| return &loggingStream{stream: str, log: w} |
| } |
| |
| func (s *loggingStream) Read(ctx context.Context) (jsonrpc2.Message, int64, error) { |
| msg, count, err := s.stream.Read(ctx) |
| if err == nil { |
| s.logCommon(msg, true) |
| } |
| return msg, count, err |
| } |
| |
| func (s *loggingStream) Write(ctx context.Context, msg jsonrpc2.Message) (int64, error) { |
| s.logCommon(msg, false) |
| count, err := s.stream.Write(ctx, msg) |
| return count, err |
| } |
| |
| func (s *loggingStream) Close() error { |
| return s.stream.Close() |
| } |
| |
| type req struct { |
| method string |
| start time.Time |
| } |
| |
| type mapped struct { |
| mu sync.Mutex |
| clientCalls map[string]req |
| serverCalls map[string]req |
| } |
| |
| var maps = &mapped{ |
| sync.Mutex{}, |
| make(map[string]req), |
| make(map[string]req), |
| } |
| |
| // these 4 methods are each used exactly once, but it seemed |
| // better to have the encapsulation rather than ad hoc mutex |
| // code in 4 places |
| func (m *mapped) client(id string) req { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| v := m.clientCalls[id] |
| delete(m.clientCalls, id) |
| return v |
| } |
| |
| func (m *mapped) server(id string) req { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| v := m.serverCalls[id] |
| delete(m.serverCalls, id) |
| return v |
| } |
| |
| func (m *mapped) setClient(id string, r req) { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| m.clientCalls[id] = r |
| } |
| |
| func (m *mapped) setServer(id string, r req) { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| m.serverCalls[id] = r |
| } |
| |
| const eor = "\r\n\r\n\r\n" |
| |
| func (s *loggingStream) logCommon(msg jsonrpc2.Message, isRead bool) { |
| s.logMu.Lock() |
| defer s.logMu.Unlock() |
| direction, pastTense := "Received", "Received" |
| get, set := maps.client, maps.setServer |
| if isRead { |
| direction, pastTense = "Sending", "Sent" |
| get, set = maps.server, maps.setClient |
| } |
| if msg == nil || s.log == nil { |
| return |
| } |
| tm := time.Now() |
| tmfmt := tm.Format("15:04:05.000 PM") |
| |
| buf := strings.Builder{} |
| fmt.Fprintf(&buf, "[Trace - %s] ", tmfmt) // common beginning |
| switch msg := msg.(type) { |
| case *jsonrpc2.Call: |
| id := fmt.Sprint(msg.ID()) |
| fmt.Fprintf(&buf, "%s request '%s - (%s)'.\n", direction, msg.Method(), id) |
| fmt.Fprintf(&buf, "Params: %s%s", msg.Params(), eor) |
| set(id, req{method: msg.Method(), start: tm}) |
| case *jsonrpc2.Notification: |
| fmt.Fprintf(&buf, "%s notification '%s'.\n", direction, msg.Method()) |
| fmt.Fprintf(&buf, "Params: %s%s", msg.Params(), eor) |
| case *jsonrpc2.Response: |
| id := fmt.Sprint(msg.ID()) |
| if err := msg.Err(); err != nil { |
| fmt.Fprintf(s.log, "[Error - %s] %s #%s %s%s", pastTense, tmfmt, id, err, eor) |
| return |
| } |
| cc := get(id) |
| elapsed := tm.Sub(cc.start) |
| fmt.Fprintf(&buf, "%s response '%s - (%s)' in %dms.\n", |
| direction, cc.method, id, elapsed/time.Millisecond) |
| fmt.Fprintf(&buf, "Result: %s%s", msg.Result(), eor) |
| } |
| s.log.Write([]byte(buf.String())) |
| } |