| // 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. |
| |
| // Replay logs. See README.md |
| package main |
| |
| import ( |
| "bufio" |
| "context" |
| "flag" |
| "fmt" |
| "log" |
| "os" |
| "os/exec" |
| "sort" |
| "strconv" |
| "strings" |
| |
| "golang.org/x/tools/gopls/integration/parse" |
| "golang.org/x/tools/internal/fakenet" |
| "golang.org/x/tools/internal/jsonrpc2" |
| p "golang.org/x/tools/internal/lsp/protocol" |
| ) |
| |
| var ( |
| command = flag.String("cmd", "", "location of server to send to, looks for gopls") |
| cmp = flag.Bool("cmp", false, "only compare log and /tmp/seen") |
| logrdr *bufio.Scanner |
| msgs []*parse.Logmsg |
| // requests and responses/errors, by id |
| clreq = make(map[string]*parse.Logmsg) |
| clresp = make(map[string]*parse.Logmsg) |
| svreq = make(map[string]*parse.Logmsg) |
| svresp = make(map[string]*parse.Logmsg) |
| ) |
| |
| func main() { |
| log.SetFlags(log.Lshortfile) |
| flag.Usage = func() { |
| fmt.Fprintln(flag.CommandLine.Output(), "replay [options] <logfile>") |
| flag.PrintDefaults() |
| } |
| flag.Parse() |
| if flag.NArg() != 1 { |
| flag.Usage() |
| os.Exit(2) |
| } |
| logf := flag.Arg(0) |
| |
| orig, err := parse.ToRlog(logf) |
| if err != nil { |
| log.Fatalf("error parsing logfile %q: %v", logf, err) |
| } |
| ctx := context.Background() |
| msgs = orig.Logs |
| log.Printf("old %d, hist:%s", len(msgs), orig.Histogram) |
| |
| if !*cmp { |
| log.Print("calling mimic") |
| mimic(ctx) |
| } |
| seen, err := parse.ToRlog("/tmp/seen") |
| if err != nil { |
| log.Fatal(err) |
| } |
| newMsgs := seen.Logs |
| log.Printf("new %d, hist:%s", len(newMsgs), seen.Histogram) |
| |
| ok := make(map[string]int) |
| f := func(x []*parse.Logmsg, label string, diags map[p.DocumentURI][]p.Diagnostic) { |
| counts := make(map[parse.MsgType]int) |
| for _, l := range x { |
| if l.Method == "window/logMessage" { |
| // don't care |
| //continue |
| } |
| if l.Method == "textDocument/publishDiagnostics" { |
| v, ok := l.Body.(*p.PublishDiagnosticsParams) |
| if !ok { |
| log.Fatalf("got %T expected PublishDiagnosticsParams", l.Body) |
| } |
| diags[v.URI] = v.Diagnostics |
| } |
| counts[l.Type]++ |
| // notifications only |
| if l.Type != parse.ToServer && l.Type != parse.ToClient { |
| continue |
| } |
| s := fmt.Sprintf("%s %s %s", strings.Replace(l.Hdr, "\r", "", -1), label, l.Type) |
| if i := strings.Index(s, "notification"); i != -1 { |
| s = s[i+12:] |
| } |
| if len(s) > 120 { |
| s = s[:120] |
| } |
| ok[s]++ |
| } |
| msg := "" |
| for i := parse.ClRequest; i <= parse.ReportErr; i++ { |
| msg += fmt.Sprintf("%s:%d ", i, counts[i]) |
| } |
| log.Printf("%s: %s", label, msg) |
| } |
| mdiags := make(map[p.DocumentURI][]p.Diagnostic) |
| f(msgs, "old", mdiags) |
| vdiags := make(map[p.DocumentURI][]p.Diagnostic) |
| f(newMsgs, "new", vdiags) |
| buf := []string{} |
| for k := range ok { |
| buf = append(buf, fmt.Sprintf("%s %d", k, ok[k])) |
| } |
| if len(buf) > 0 { |
| log.Printf("counts of notifications") |
| sort.Strings(buf) |
| for _, k := range buf { |
| log.Print(k) |
| } |
| } |
| buf = buf[0:0] |
| for k, v := range mdiags { |
| va := vdiags[k] |
| if len(v) != len(va) { |
| buf = append(buf, fmt.Sprintf("new has %d, old has %d for %s", |
| len(va), len(v), k)) |
| } |
| } |
| for ka := range vdiags { |
| if _, ok := mdiags[ka]; !ok { |
| buf = append(buf, fmt.Sprintf("new diagnostics, but no old ones, for %s", |
| ka)) |
| } |
| } |
| if len(buf) > 0 { |
| log.Print("diagnostics differ:") |
| for _, s := range buf { |
| log.Print(s) |
| } |
| } |
| } |
| |
| func send(ctx context.Context, l *parse.Logmsg, stream jsonrpc2.Stream, id *jsonrpc2.ID) { |
| if id == nil { |
| // need to use the number version of ID |
| n, err := strconv.Atoi(l.ID) |
| if err != nil { |
| n = 0 |
| } |
| nid := jsonrpc2.NewIntID(int64(n)) |
| id = &nid |
| } |
| var msg jsonrpc2.Message |
| var err error |
| switch l.Type { |
| case parse.ClRequest: |
| msg, err = jsonrpc2.NewCall(*id, l.Method, l.Body) |
| case parse.SvResponse: |
| msg, err = jsonrpc2.NewResponse(*id, l.Body, nil) |
| case parse.ToServer: |
| msg, err = jsonrpc2.NewNotification(l.Method, l.Body) |
| default: |
| log.Fatalf("sending %s", l.Type) |
| } |
| if err != nil { |
| log.Fatal(err) |
| } |
| stream.Write(ctx, msg) |
| } |
| |
| func respond(ctx context.Context, c *jsonrpc2.Call, stream jsonrpc2.Stream) { |
| // c is a server request |
| // pick out the id, and look for the response in msgs |
| id := c.ID() |
| idstr := fmt.Sprint(id) |
| for _, l := range msgs { |
| if l.ID == idstr && l.Type == parse.SvResponse { |
| // check that the methods match? |
| // need to send back the same ID we got. |
| send(ctx, l, stream, &id) |
| return |
| } |
| } |
| log.Fatalf("no response found %q %+v %+v", c.Method(), c.ID(), c) |
| } |
| |
| func findgopls() string { |
| totry := [][]string{{"GOBIN", "/gopls"}, {"GOPATH", "/bin/gopls"}, {"HOME", "/go/bin/gopls"}} |
| // looks in the places go install would install: |
| // GOBIN, else GOPATH/bin, else HOME/go/bin |
| ok := func(s string) bool { |
| fd, err := os.Open(s) |
| if err != nil { |
| return false |
| } |
| fi, err := fd.Stat() |
| if err != nil { |
| return false |
| } |
| return fi.Mode()&0111 != 0 |
| } |
| for _, t := range totry { |
| g := os.Getenv(t[0]) |
| if g != "" && ok(g+t[1]) { |
| gopls := g + t[1] |
| log.Printf("using gopls at %s", gopls) |
| return gopls |
| } |
| } |
| log.Fatal("could not find gopls") |
| return "" |
| } |
| |
| func mimic(ctx context.Context) { |
| log.Printf("mimic %d", len(msgs)) |
| if *command == "" { |
| *command = findgopls() |
| } |
| cmd := exec.Command(*command, "-logfile", "/tmp/seen", "-rpc.trace") |
| toServer, err := cmd.StdinPipe() |
| if err != nil { |
| log.Fatal(err) |
| } |
| fromServer, err := cmd.StdoutPipe() |
| if err != nil { |
| log.Fatal(err) |
| } |
| err = cmd.Start() |
| if err != nil { |
| log.Fatal(err) |
| } |
| conn := fakenet.NewConn("stdio", fromServer, toServer) |
| stream := jsonrpc2.NewHeaderStream(conn) |
| rchan := make(chan jsonrpc2.Message, 10) // do we need buffering? |
| rdr := func() { |
| for { |
| msg, _, err := stream.Read(ctx) |
| if err != nil { |
| rchan <- nil // close it instead? |
| return |
| } |
| rchan <- msg |
| } |
| } |
| go rdr() |
| // send as many as possible: all clrequests and toservers up to a clresponse |
| // and loop |
| seenids := make(map[string]bool) // id's that have been responded to: |
| big: |
| for _, l := range msgs { |
| switch l.Type { |
| case parse.ToServer: // just send these as we get to them |
| send(ctx, l, stream, nil) |
| case parse.ClRequest: |
| send(ctx, l, stream, nil) // for now, wait for a response, to make sure code is ok |
| fallthrough |
| case parse.ClResponse, parse.ReportErr: // don't go past these until they're received |
| if seenids[l.ID] { |
| break // onward, as it has been received already |
| } |
| done: |
| for { |
| msg := <-rchan |
| if msg == nil { |
| break big |
| } |
| // if it's svrequest, do something |
| // if it's clresponse or reporterr, add to seenids, and if it |
| // is l.id, break out of the loop, and continue the outer loop |
| |
| switch msg := msg.(type) { |
| case *jsonrpc2.Call: |
| if parse.FromServer(msg.Method()) { |
| respond(ctx, msg, stream) |
| continue done // still waiting |
| } |
| case *jsonrpc2.Response: |
| id := fmt.Sprint(msg.ID()) |
| seenids[id] = true |
| if id == l.ID { |
| break done |
| } |
| } |
| } |
| case parse.SvRequest: // not ours to send |
| continue |
| case parse.SvResponse: // sent by us, if the request arrives |
| continue |
| case parse.ToClient: // we don't send these |
| continue |
| } |
| } |
| } |