blob: ae899578cbd9180d89a2413fd8cf298e2979ce91 [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"
"fmt"
"io"
"log"
"os"
"regexp"
"strings"
)
// Direction is the type of message,
type Direction int
const (
// Clrequest from client to server has method and id
Clrequest Direction = 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 {
Dir Direction
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 TRlog().
func ReadLogs(fname string) ([]*Logmsg, error) {
byid := make(map[string]int)
msgs := []*Logmsg{}
fd, err := os.Open(fname)
if err != nil {
return nil, err
}
logrdr := bufio.NewScanner(fd)
logrdr.Buffer(nil, 1<<25) // a large buffer, for safety
logrdr.Split(logRec)
for i := 0; logrdr.Scan(); i++ {
flds := strings.SplitN(logrdr.Text(), "\n", 2)
if len(flds) == 1 {
flds = append(flds, "") // for Errors
}
msg := parselog(flds[0], flds[1])
if msg == nil {
log.Fatalf("failed to parse %q", logrdr.Text())
continue
}
switch msg.Dir {
case Clrequest, Svrequest:
v, err := msg.unmarshal(Requests(msg.Method))
if err != nil {
log.Fatalf("%v for %s, %T", err, msg.Method, Requests(msg.Method))
}
msg.Body = v
case Clresponse, Svresponse:
v, err := msg.doresponse()
if err != nil {
log.Fatalf("%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 {
log.Fatalf("%s/%T: %v", msg.Method, Notifs(msg.Method), err)
}
msg.Body = v
case Reporterr:
msg.Body = msg.ID // 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 {
if strings.HasPrefix(rest, "Params: ") {
rest = rest[8:]
} else if strings.HasPrefix(rest, "Result: ") {
rest = rest[8:]
}
ans := &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):
ans.Dir = Clrequest
ans.Method = flds[6][1:]
ans.ID = fixid(flds[8][:len(flds[8])-2])
case chk("Received response", 11):
ans.Dir = Clresponse
ans.Method = flds[6][1:]
ans.ID = fixid(flds[8])
ans.Elapsed = flds[10]
case chk("Received request", 9):
ans.Dir = Svrequest
ans.Method = flds[6][1:]
ans.ID = fixid(flds[8])
case chk("Sending response", 11), // gopls
chk("Sending response", 13): // emacs
ans.Dir = Svresponse
ans.Method = flds[6][1:]
ans.ID = fixid(flds[8][:len(flds[8])-1])
ans.Elapsed = flds[10]
case chk("Sending notification", 7):
ans.Dir = Toserver
ans.Method = strings.Trim(flds[6], ".'")
if len(flds) == 9 {
log.Printf("len=%d method=%s %q", len(flds), ans.Method, first)
}
case chk("Received notification", 7):
ans.Dir = Toclient
ans.Method = flds[6][1 : len(flds[6])-2]
case strings.HasPrefix(first, "[Error - "):
ans.Dir = Reporterr
both := flds[5]
idx := strings.Index(both, "#") // relies on ID.Number
ans.Method = both[:idx]
ans.ID = fixid(both[idx+1:])
ans.Rest = strings.Join(flds[6:], " ")
default:
log.Fatalf("surprise, first=%q with %d flds", first, len(flds))
return nil
}
return ans
}
// 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 logRec(b []byte, atEOF bool) (int, []byte, error) { //bufio.SplitFunc
got := recSep.FindIndex(b)
if got == nil {
if !atEOF {
return 0, nil, nil // need more
}
return 0, nil, io.EOF
}
return got[1], b[:got[0]], nil
}
// String returns a user-useful versin of a Direction
func (d Direction) 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)
}