mirror of
https://github.com/golang/go
synced 2024-11-18 13:34:41 -07:00
81ef4df1d3
These are changes to treat errors more like responses. They are important for the forthcoming log viewer. Change-Id: Ief8de6ecea716673d4aee417de205842ceab4fc8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/213124 Run-TryBot: Rebecca Stambler <rstambler@golang.org> Reviewed-by: Robert Findley <rfindley@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org>
231 lines
6.0 KiB
Go
231 lines
6.0 KiB
Go
// 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 parse provides functions to parse LSP logs.
|
|
// Fully processed logs are returned by ToRLog().
|
|
package parse
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// MsgType is the type of message.
|
|
type MsgType int
|
|
|
|
const (
|
|
// ClRequest from client to server has method and id
|
|
ClRequest MsgType = iota
|
|
// ClResponse from server to client
|
|
ClResponse
|
|
// SvRequest from server to client, has method and id
|
|
SvRequest
|
|
// SvResponse from client to server
|
|
SvResponse
|
|
// ToServer notification has method, but no id
|
|
ToServer
|
|
// ToClient notification
|
|
ToClient
|
|
// ReportErr is an error message
|
|
ReportErr // errors have method and id
|
|
)
|
|
|
|
// Logmsg is the type of a parsed log entry.
|
|
type Logmsg struct {
|
|
Type MsgType
|
|
Method string
|
|
ID string // for requests/responses. Client and server request ids overlap
|
|
Elapsed string // for responses
|
|
Hdr string // header. do we need to keep all these strings?
|
|
Rest string // the unparsed result, with newlines or not
|
|
Body interface{} // the parsed result
|
|
}
|
|
|
|
// ReadLogs from a file. Most users should use ToRlog().
|
|
func ReadLogs(fname string) ([]*Logmsg, error) {
|
|
byid := make(map[string]int)
|
|
msgs := []*Logmsg{}
|
|
fd, err := os.Open(fname)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer fd.Close()
|
|
logrdr := bufio.NewScanner(fd)
|
|
logrdr.Buffer(nil, 1<<25) // a large buffer, for safety
|
|
logrdr.Split(scanLogs)
|
|
for i := 0; logrdr.Scan(); i++ {
|
|
flds := strings.SplitN(logrdr.Text(), "\n", 2)
|
|
if len(flds) == 1 {
|
|
flds = append(flds, "") // for Errors
|
|
}
|
|
msg, err := parselog(flds[0], flds[1])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse %q: %v", logrdr.Text(), err)
|
|
}
|
|
switch msg.Type {
|
|
case ClRequest, SvRequest:
|
|
v, err := msg.unmarshal(Requests(msg.Method))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v for %s, %T", err, msg.Method, Requests(msg.Method))
|
|
}
|
|
msg.Body = v
|
|
case ClResponse, SvResponse:
|
|
v, err := msg.doresponse()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v %s", err, msg.Method)
|
|
}
|
|
msg.Body = v
|
|
case ToServer, ToClient:
|
|
v, err := msg.unmarshal(Notifs(msg.Method))
|
|
if err != nil && Notifs(msg.Method) != nil {
|
|
return nil, fmt.Errorf("%s/%T: %v", msg.Method, Notifs(msg.Method), err)
|
|
}
|
|
msg.Body = v
|
|
case ReportErr:
|
|
msg.Body = msg.Rest // save cause
|
|
}
|
|
byid[msg.ID]++
|
|
msgs = append(msgs, msg)
|
|
}
|
|
if err = logrdr.Err(); err != nil {
|
|
return msgs, err
|
|
}
|
|
return msgs, nil
|
|
}
|
|
|
|
// parse a single log message, given first line, and the rest
|
|
func parselog(first, rest string) (*Logmsg, error) {
|
|
if strings.HasPrefix(rest, "Params: ") {
|
|
rest = rest[8:]
|
|
} else if strings.HasPrefix(rest, "Result: ") {
|
|
rest = rest[8:]
|
|
}
|
|
msg := &Logmsg{Hdr: first, Rest: rest}
|
|
fixid := func(s string) string {
|
|
// emacs does (n)., gopls does (n)'.
|
|
s = strings.Trim(s, "()'.{)")
|
|
return s
|
|
}
|
|
flds := strings.Fields(first)
|
|
chk := func(s string, n int) bool { return strings.Contains(first, s) && len(flds) == n }
|
|
// gopls and emacs differ in how they report elapsed time
|
|
switch {
|
|
case chk("Sending request", 9):
|
|
msg.Type = ClRequest
|
|
msg.Method = flds[6][1:]
|
|
msg.ID = fixid(flds[8][:len(flds[8])-2])
|
|
case chk("Received response", 11):
|
|
msg.Type = ClResponse
|
|
msg.Method = flds[6][1:]
|
|
msg.ID = fixid(flds[8])
|
|
msg.Elapsed = flds[10]
|
|
case chk("Received request", 9):
|
|
msg.Type = SvRequest
|
|
msg.Method = flds[6][1:]
|
|
msg.ID = fixid(flds[8])
|
|
case chk("Sending response", 11), // gopls
|
|
chk("Sending response", 13): // emacs
|
|
msg.Type = SvResponse
|
|
msg.Method = flds[6][1:]
|
|
msg.ID = fixid(flds[8][:len(flds[8])-1])
|
|
msg.Elapsed = flds[10]
|
|
case chk("Sending notification", 7):
|
|
msg.Type = ToServer
|
|
msg.Method = strings.Trim(flds[6], ".'")
|
|
if len(flds) == 9 {
|
|
log.Printf("len=%d method=%s %q", len(flds), msg.Method, first)
|
|
}
|
|
case chk("Received notification", 7):
|
|
msg.Type = ToClient
|
|
msg.Method = flds[6][1 : len(flds[6])-2]
|
|
case strings.HasPrefix(first, "[Error - "):
|
|
msg.Type = ReportErr
|
|
both := flds[5]
|
|
idx := strings.Index(both, "#") // relies on ID.Number
|
|
msg.Method = both[:idx]
|
|
msg.ID = fixid(both[idx+1:])
|
|
msg.Rest = strings.Join(flds[6:], " ")
|
|
msg.Rest = `"` + msg.Rest + `"`
|
|
default:
|
|
return nil, fmt.Errorf("surprise, first=%q with %d flds", first, len(flds))
|
|
}
|
|
return msg, nil
|
|
}
|
|
|
|
// unmarshal into a proposed type
|
|
func (l *Logmsg) unmarshal(p interface{}) (interface{}, error) {
|
|
r := []byte(l.Rest)
|
|
if err := json.Unmarshal(r, p); err != nil {
|
|
// need general alternatives, but for now
|
|
// if p is *[]foo and rest is {}, return an empty p (or *p?)
|
|
// or, cheat:
|
|
if l.Rest == "{}" {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (l *Logmsg) doresponse() (interface{}, error) {
|
|
for _, x := range Responses(l.Method) {
|
|
v, err := l.unmarshal(x)
|
|
if err == nil {
|
|
return v, nil
|
|
}
|
|
if x == nil {
|
|
return new(interface{}), nil
|
|
}
|
|
}
|
|
// failure!
|
|
rr := Responses(l.Method)
|
|
for _, x := range rr {
|
|
log.Printf("tried %T", x)
|
|
}
|
|
log.Fatalf("(%d) doresponse failed for %s %q", len(rr), l.Method, l.Rest)
|
|
return nil, nil
|
|
}
|
|
|
|
// be a little forgiving in separating log records
|
|
var recSep = regexp.MustCompile("\n\n\n|\r\n\r\n\r\n")
|
|
|
|
// return offset of start of next record, contents of record, error
|
|
func scanLogs(b []byte, atEOF bool) (int, []byte, error) { //bufio.SplitFunc
|
|
got := recSep.FindIndex(b)
|
|
if got == nil {
|
|
if atEOF && len(b) > 0 {
|
|
return 0, nil, errors.New("malformed log: all logs should end with a separator")
|
|
}
|
|
return 0, nil, nil
|
|
}
|
|
return got[1], b[:got[0]], nil
|
|
}
|
|
|
|
// String returns a user-useful versin of a Direction
|
|
func (d MsgType) String() string {
|
|
switch d {
|
|
case ClRequest:
|
|
return "clrequest"
|
|
case ClResponse:
|
|
return "clresponse"
|
|
case SvRequest:
|
|
return "svrequest"
|
|
case SvResponse:
|
|
return "svresponse"
|
|
case ToServer:
|
|
return "toserver"
|
|
case ToClient:
|
|
return "toclient"
|
|
case ReportErr:
|
|
return "reporterr"
|
|
}
|
|
return fmt.Sprintf("dirname: %d unknown", d)
|
|
}
|