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