blob: 5f52128fba95f49eab37ab998552893aa00747cf [file] [log] [blame]
// 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)
}