// 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) }