diff --git a/cmd/stcrashreceiver/sentry.go b/cmd/stcrashreceiver/sentry.go index fd679a14e..b76759dbf 100644 --- a/cmd/stcrashreceiver/sentry.go +++ b/cmd/stcrashreceiver/sentry.go @@ -19,6 +19,12 @@ import ( const reportServer = "https://crash.syncthing.net/report/" +var loader = newGithubSourceCodeLoader() + +func init() { + raven.SetSourceCodeLoader(loader) +} + func sendReport(dsn, path string, report []byte) error { pkt, err := parseReport(path, report) if err != nil { @@ -80,30 +86,38 @@ func parseReport(path string, report []byte) (*raven.Packet, error) { return nil, err } + // Lock the source code loader to the version we are processing here. + if version.commit != "" { + // We have a commit hash, so we know exactly which source to use + loader.LockWithVersion(version.commit) + } else if strings.HasPrefix(version.tag, "v") { + // Lets hope the tag is close enough + loader.LockWithVersion(version.tag) + } else { + // Last resort + loader.LockWithVersion("master") + } + defer loader.Unlock() + var trace raven.Stacktrace for _, gr := range ctx.Goroutines { if gr.First { trace.Frames = make([]*raven.StacktraceFrame, len(gr.Stack.Calls)) for i, sc := range gr.Stack.Calls { - trace.Frames[len(trace.Frames)-1-i] = &raven.StacktraceFrame{ - Function: sc.Func.Name(), - Module: sc.Func.PkgName(), - Filename: sc.SrcPath, - Lineno: sc.Line, - } + trace.Frames[len(trace.Frames)-1-i] = raven.NewStacktraceFrame(0, sc.Func.Name(), sc.SrcPath, sc.Line, 3, nil) } break } } pkt := &raven.Packet{ - Message: string(subjectLine), - Platform: "go", - Release: version.tag, + Message: string(subjectLine), + Platform: "go", + Release: version.tag, + Environment: version.environment(), Tags: raven.Tags{ raven.Tag{Key: "version", Value: version.version}, raven.Tag{Key: "tag", Value: version.tag}, - raven.Tag{Key: "commit", Value: version.commit}, raven.Tag{Key: "codename", Value: version.codename}, raven.Tag{Key: "runtime", Value: version.runtime}, raven.Tag{Key: "goos", Value: version.goos}, @@ -115,6 +129,9 @@ func parseReport(path string, report []byte) (*raven.Packet, error) { }, Interfaces: []raven.Interface{&trace}, } + if version.commit != "" { + pkt.Tags = append(pkt.Tags, raven.Tag{Key: "commit", Value: version.commit}) + } return pkt, nil } @@ -133,6 +150,19 @@ type version struct { builder string // "jb@kvin.kastelo.net" } +func (v version) environment() string { + if v.commit != "" { + return "Development" + } + if strings.Contains(v.tag, "-rc.") { + return "Candidate" + } + if strings.Contains(v.tag, "-") { + return "Beta" + } + return "Stable" +} + func parseVersion(line string) (version, error) { m := longVersionRE.FindStringSubmatch(line) if len(m) == 0 { diff --git a/cmd/stcrashreceiver/sourcecodeloader.go b/cmd/stcrashreceiver/sourcecodeloader.go new file mode 100644 index 000000000..ee3e91ffb --- /dev/null +++ b/cmd/stcrashreceiver/sourcecodeloader.go @@ -0,0 +1,114 @@ +// Copyright (C) 2019 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "strings" + "sync" + "time" +) + +const ( + urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/" + httpTimeout = 10 * time.Second +) + +type githubSourceCodeLoader struct { + mut sync.Mutex + version string + cache map[string]map[string][][]byte // version -> file -> lines + client *http.Client +} + +func newGithubSourceCodeLoader() *githubSourceCodeLoader { + return &githubSourceCodeLoader{ + cache: make(map[string]map[string][][]byte), + client: &http.Client{Timeout: httpTimeout}, + } +} + +func (l *githubSourceCodeLoader) LockWithVersion(version string) { + l.mut.Lock() + l.version = version + if _, ok := l.cache[version]; !ok { + l.cache[version] = make(map[string][][]byte) + } +} + +func (l *githubSourceCodeLoader) Unlock() { + l.mut.Unlock() +} + +func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) { + filename = filepath.ToSlash(filename) + lines, ok := l.cache[l.version][filename] + if !ok { + // Cache whatever we managed to find (or nil if nothing, so we don't try again) + defer func() { + l.cache[l.version][filename] = lines + }() + + knownPrefixes := []string{"/lib/", "/cmd/"} + var idx int + for _, pref := range knownPrefixes { + idx = strings.Index(filename, pref) + if idx >= 0 { + break + } + } + if idx == -1 { + return nil, 0 + } + + url := urlPrefix + l.version + filename[idx:] + resp, err := l.client.Get(url) + + if err != nil || resp.StatusCode != http.StatusOK { + fmt.Println("Loading source:", err.Error()) + return nil, 0 + } + data, err := ioutil.ReadAll(resp.Body) + _ = resp.Body.Close() + if err != nil { + fmt.Println("Loading source:", err.Error()) + return nil, 0 + } + lines = bytes.Split(data, []byte{'\n'}) + } + + return getLineFromLines(lines, line, context) +} + +func getLineFromLines(lines [][]byte, line, context int) ([][]byte, int) { + if lines == nil { + // cached error from ReadFile: return no lines + return nil, 0 + } + + line-- // stack trace lines are 1-indexed + start := line - context + var idx int + if start < 0 { + start = 0 + idx = line + } else { + idx = context + } + end := line + context + 1 + if line >= len(lines) { + return nil, 0 + } + if end > len(lines) { + end = len(lines) + } + return lines[start:end], idx +}