mirror of
https://github.com/reactos/syzkaller.git
synced 2024-11-30 14:50:36 +00:00
fd3bba535d
We used to show number of fixed bugs at the top of the main page. However, now with the button nagivation, "fixed" is shown on every page. Fetching and processing all bugs on every page would be unwise. Cache these stats in memcache. It will be useful to show more stats in future.
1159 lines
35 KiB
Go
1159 lines
35 KiB
Go
// Copyright 2017 syzkaller project authors. All rights reserved.
|
|
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/syzkaller/dashboard/dashapi"
|
|
"github.com/google/syzkaller/pkg/email"
|
|
"github.com/google/syzkaller/pkg/html"
|
|
"golang.org/x/net/context"
|
|
db "google.golang.org/appengine/datastore"
|
|
"google.golang.org/appengine/log"
|
|
)
|
|
|
|
// Backend-independent reporting logic.
|
|
// Two main entry points:
|
|
// - reportingPoll is called by backends to get list of bugs that need to be reported.
|
|
// - incomingCommand is called by backends to update bug statuses.
|
|
|
|
const (
|
|
maxMailLogLen = 1 << 20
|
|
maxMailReportLen = 64 << 10
|
|
maxInlineError = 16 << 10
|
|
notifyResendPeriod = 14 * 24 * time.Hour
|
|
notifyAboutBadCommitPeriod = 90 * 24 * time.Hour
|
|
never = 100 * 365 * 24 * time.Hour
|
|
internalError = "internal error"
|
|
// This is embedded as first line of syzkaller reproducer files.
|
|
syzReproPrefix = "# See https://goo.gl/kgGztJ for information about syzkaller reproducers.\n"
|
|
)
|
|
|
|
// reportingPoll is called by backends to get list of bugs that need to be reported.
|
|
func reportingPollBugs(c context.Context, typ string) []*dashapi.BugReport {
|
|
state, err := loadReportingState(c)
|
|
if err != nil {
|
|
log.Errorf(c, "%v", err)
|
|
return nil
|
|
}
|
|
bugs, _, err := loadOpenBugs(c)
|
|
if err != nil {
|
|
log.Errorf(c, "%v", err)
|
|
return nil
|
|
}
|
|
log.Infof(c, "fetched %v bugs", len(bugs))
|
|
sort.Sort(bugReportSorter(bugs))
|
|
var reports []*dashapi.BugReport
|
|
for _, bug := range bugs {
|
|
rep, err := handleReportBug(c, typ, state, bug)
|
|
if err != nil {
|
|
log.Errorf(c, "%v: failed to report bug %v: %v", bug.Namespace, bug.Title, err)
|
|
continue
|
|
}
|
|
if rep == nil {
|
|
continue
|
|
}
|
|
reports = append(reports, rep)
|
|
}
|
|
return reports
|
|
}
|
|
|
|
func handleReportBug(c context.Context, typ string, state *ReportingState, bug *Bug) (
|
|
*dashapi.BugReport, error) {
|
|
reporting, bugReporting, crash, crashKey, _, _, _, err := needReport(c, typ, state, bug)
|
|
if err != nil || reporting == nil {
|
|
return nil, err
|
|
}
|
|
rep, err := createBugReport(c, bug, crash, crashKey, bugReporting, reporting)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Infof(c, "bug %q: reporting to %v", bug.Title, reporting.Name)
|
|
return rep, nil
|
|
}
|
|
|
|
func needReport(c context.Context, typ string, state *ReportingState, bug *Bug) (
|
|
reporting *Reporting, bugReporting *BugReporting, crash *Crash,
|
|
crashKey *db.Key, reportingIdx int, status, link string, err error) {
|
|
reporting, bugReporting, reportingIdx, status, err = currentReporting(c, bug)
|
|
if err != nil || reporting == nil {
|
|
return
|
|
}
|
|
if typ != "" && typ != reporting.Config.Type() {
|
|
status = "on a different reporting"
|
|
reporting, bugReporting = nil, nil
|
|
return
|
|
}
|
|
link = bugReporting.Link
|
|
if !bugReporting.Reported.IsZero() && bugReporting.ReproLevel >= bug.ReproLevel {
|
|
status = fmt.Sprintf("%v: reported%v on %v",
|
|
reporting.DisplayTitle, reproStr(bugReporting.ReproLevel),
|
|
html.FormatTime(bugReporting.Reported))
|
|
reporting, bugReporting = nil, nil
|
|
return
|
|
}
|
|
ent := state.getEntry(timeNow(c), bug.Namespace, reporting.Name)
|
|
cfg := config.Namespaces[bug.Namespace]
|
|
if timeSince(c, bug.FirstTime) < cfg.ReportingDelay {
|
|
status = fmt.Sprintf("%v: initial reporting delay", reporting.DisplayTitle)
|
|
reporting, bugReporting = nil, nil
|
|
return
|
|
}
|
|
if bug.ReproLevel < ReproLevelC && timeSince(c, bug.FirstTime) < cfg.WaitForRepro {
|
|
status = fmt.Sprintf("%v: waiting for C repro", reporting.DisplayTitle)
|
|
reporting, bugReporting = nil, nil
|
|
return
|
|
}
|
|
if !cfg.MailWithoutReport && !bug.HasReport {
|
|
status = fmt.Sprintf("%v: no report", reporting.DisplayTitle)
|
|
reporting, bugReporting = nil, nil
|
|
return
|
|
}
|
|
|
|
crash, crashKey, err = findCrashForBug(c, bug)
|
|
if err != nil {
|
|
status = fmt.Sprintf("%v: no crashes!", reporting.DisplayTitle)
|
|
reporting, bugReporting = nil, nil
|
|
return
|
|
}
|
|
|
|
// Limit number of reports sent per day,
|
|
// but don't limit sending repros to already reported bugs.
|
|
if bugReporting.Reported.IsZero() && ent.Sent >= reporting.DailyLimit {
|
|
status = fmt.Sprintf("%v: out of quota for today", reporting.DisplayTitle)
|
|
reporting, bugReporting = nil, nil
|
|
return
|
|
}
|
|
|
|
// Ready to be reported.
|
|
if bugReporting.Reported.IsZero() {
|
|
// This update won't be committed, but it will prevent us from
|
|
// reporting too many bugs in a single poll.
|
|
ent.Sent++
|
|
}
|
|
status = fmt.Sprintf("%v: ready to report", reporting.DisplayTitle)
|
|
if !bugReporting.Reported.IsZero() {
|
|
status += fmt.Sprintf(" (reported%v on %v)",
|
|
reproStr(bugReporting.ReproLevel), html.FormatTime(bugReporting.Reported))
|
|
}
|
|
return
|
|
}
|
|
|
|
func reportingPollNotifications(c context.Context, typ string) []*dashapi.BugNotification {
|
|
bugs, _, err := loadOpenBugs(c)
|
|
if err != nil {
|
|
log.Errorf(c, "%v", err)
|
|
return nil
|
|
}
|
|
log.Infof(c, "fetched %v bugs", len(bugs))
|
|
var notifs []*dashapi.BugNotification
|
|
for _, bug := range bugs {
|
|
if config.Namespaces[bug.Namespace].Decommissioned {
|
|
continue
|
|
}
|
|
notif, err := handleReportNotif(c, typ, bug)
|
|
if err != nil {
|
|
log.Errorf(c, "%v: failed to create bug notif %v: %v", bug.Namespace, bug.Title, err)
|
|
continue
|
|
}
|
|
if notif == nil {
|
|
continue
|
|
}
|
|
notifs = append(notifs, notif)
|
|
if len(notifs) >= 10 {
|
|
break // don't send too many at once just in case
|
|
}
|
|
}
|
|
return notifs
|
|
}
|
|
|
|
func handleReportNotif(c context.Context, typ string, bug *Bug) (*dashapi.BugNotification, error) {
|
|
reporting, bugReporting, _, _, err := currentReporting(c, bug)
|
|
if err != nil || reporting == nil {
|
|
return nil, nil
|
|
}
|
|
if typ != "" && typ != reporting.Config.Type() {
|
|
return nil, nil
|
|
}
|
|
if bug.Status != BugStatusOpen || bugReporting.Reported.IsZero() {
|
|
return nil, nil
|
|
}
|
|
|
|
if reporting.moderation &&
|
|
reporting.Embargo != 0 &&
|
|
len(bug.Commits) == 0 &&
|
|
bugReporting.OnHold.IsZero() &&
|
|
timeSince(c, bugReporting.Reported) > reporting.Embargo {
|
|
log.Infof(c, "%v: upstreaming (embargo): %v", bug.Namespace, bug.Title)
|
|
return createNotification(c, dashapi.BugNotifUpstream, true, "", bug, reporting, bugReporting)
|
|
}
|
|
if reporting.moderation &&
|
|
len(bug.Commits) == 0 &&
|
|
bugReporting.OnHold.IsZero() &&
|
|
reporting.Filter(bug) == FilterSkip {
|
|
log.Infof(c, "%v: upstreaming (skip): %v", bug.Namespace, bug.Title)
|
|
return createNotification(c, dashapi.BugNotifUpstream, true, "", bug, reporting, bugReporting)
|
|
}
|
|
if len(bug.Commits) == 0 &&
|
|
bug.wontBeFixBisected() &&
|
|
timeSince(c, bug.LastActivity) > notifyResendPeriod &&
|
|
timeSince(c, bug.LastTime) > bug.obsoletePeriod() {
|
|
log.Infof(c, "%v: obsoleting: %v", bug.Namespace, bug.Title)
|
|
return createNotification(c, dashapi.BugNotifObsoleted, false, "", bug, reporting, bugReporting)
|
|
}
|
|
if len(bug.Commits) > 0 &&
|
|
len(bug.PatchedOn) == 0 &&
|
|
timeSince(c, bug.LastActivity) > notifyResendPeriod &&
|
|
timeSince(c, bug.FixTime) > notifyAboutBadCommitPeriod {
|
|
log.Infof(c, "%v: bad fix commit: %v", bug.Namespace, bug.Title)
|
|
commits := strings.Join(bug.Commits, "\n")
|
|
return createNotification(c, dashapi.BugNotifBadCommit, true, commits, bug, reporting, bugReporting)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// TODO: this is what we would like to do, but we need to figure out
|
|
// KMSAN story: we don't do fix bisection on it (rebased),
|
|
// do we want to close all old KMSAN bugs with repros?
|
|
// For now we only enable this in tests.
|
|
var obsoleteWhatWontBeFixBisected = false
|
|
|
|
func (bug *Bug) wontBeFixBisected() bool {
|
|
if bug.ReproLevel == ReproLevelNone {
|
|
return true
|
|
}
|
|
if obsoleteWhatWontBeFixBisected {
|
|
cfg := config.Namespaces[bug.Namespace]
|
|
for _, mgr := range bug.HappenedOn {
|
|
if !cfg.Managers[mgr].FixBisectionDisabled {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (bug *Bug) obsoletePeriod() time.Duration {
|
|
period := never
|
|
if config.Obsoleting.MinPeriod == 0 {
|
|
return period
|
|
}
|
|
// Before we have at least 10 crashes, any estimation of frequency is too imprecise.
|
|
// In such case we conservatively assume it still happens.
|
|
if bug.NumCrashes >= 10 {
|
|
// This is linear extrapolation for when the next crash should happen.
|
|
period = bug.LastTime.Sub(bug.FirstTime) / time.Duration(bug.NumCrashes-1)
|
|
// Let's be conservative with obsoleting too early.
|
|
period *= 100
|
|
}
|
|
min, max := config.Obsoleting.MinPeriod, config.Obsoleting.MaxPeriod
|
|
if config.Obsoleting.NonFinalMinPeriod != 0 &&
|
|
bug.Reporting[len(bug.Reporting)-1].Reported.IsZero() {
|
|
min, max = config.Obsoleting.NonFinalMinPeriod, config.Obsoleting.NonFinalMaxPeriod
|
|
}
|
|
if len(bug.HappenedOn) == 1 {
|
|
mgr := config.Namespaces[bug.Namespace].Managers[bug.HappenedOn[0]]
|
|
if mgr.ObsoletingMinPeriod != 0 {
|
|
min, max = mgr.ObsoletingMinPeriod, mgr.ObsoletingMaxPeriod
|
|
}
|
|
}
|
|
if period < min {
|
|
period = min
|
|
}
|
|
if period > max {
|
|
period = max
|
|
}
|
|
return period
|
|
}
|
|
|
|
func createNotification(c context.Context, typ dashapi.BugNotif, public bool, text string, bug *Bug,
|
|
reporting *Reporting, bugReporting *BugReporting) (*dashapi.BugNotification, error) {
|
|
reportingConfig, err := json.Marshal(reporting.Config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
crash, _, err := findCrashForBug(c, bug)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("no crashes for bug")
|
|
}
|
|
build, err := loadBuild(c, bug.Namespace, crash.BuildID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
kernelRepo := kernelRepoInfo(build)
|
|
notif := &dashapi.BugNotification{
|
|
Type: typ,
|
|
Namespace: bug.Namespace,
|
|
Config: reportingConfig,
|
|
ID: bugReporting.ID,
|
|
ExtID: bugReporting.ExtID,
|
|
Title: bug.displayTitle(),
|
|
Text: text,
|
|
Public: public,
|
|
CC: kernelRepo.CC,
|
|
}
|
|
if public {
|
|
notif.Maintainers = append(crash.Maintainers, kernelRepo.Maintainers...)
|
|
}
|
|
if (public || reporting.moderation) && bugReporting.CC != "" {
|
|
notif.CC = append(notif.CC, strings.Split(bugReporting.CC, "|")...)
|
|
}
|
|
return notif, nil
|
|
}
|
|
|
|
func currentReporting(c context.Context, bug *Bug) (*Reporting, *BugReporting, int, string, error) {
|
|
for i := range bug.Reporting {
|
|
bugReporting := &bug.Reporting[i]
|
|
if !bugReporting.Closed.IsZero() {
|
|
continue
|
|
}
|
|
reporting := config.Namespaces[bug.Namespace].ReportingByName(bugReporting.Name)
|
|
if reporting == nil {
|
|
return nil, nil, 0, "", fmt.Errorf("%v: missing in config", bugReporting.Name)
|
|
}
|
|
if reporting.DailyLimit == 0 {
|
|
return nil, nil, 0, fmt.Sprintf("%v: reporting has daily limit 0", reporting.DisplayTitle), nil
|
|
}
|
|
switch reporting.Filter(bug) {
|
|
case FilterSkip:
|
|
if bugReporting.Reported.IsZero() {
|
|
continue
|
|
}
|
|
fallthrough
|
|
case FilterReport:
|
|
return reporting, bugReporting, i, "", nil
|
|
case FilterHold:
|
|
return nil, nil, 0, fmt.Sprintf("%v: reporting suspended", reporting.DisplayTitle), nil
|
|
}
|
|
}
|
|
return nil, nil, 0, "", fmt.Errorf("no reporting left")
|
|
}
|
|
|
|
func reproStr(level dashapi.ReproLevel) string {
|
|
switch level {
|
|
case ReproLevelSyz:
|
|
return " syz repro"
|
|
case ReproLevelC:
|
|
return " C repro"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func createBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *db.Key,
|
|
bugReporting *BugReporting, reporting *Reporting) (*dashapi.BugReport, error) {
|
|
reportingConfig, err := json.Marshal(reporting.Config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var job *Job
|
|
if bug.BisectCause == BisectYes {
|
|
// If we have bisection results, report the crash/repro used for bisection.
|
|
job1, crash1, _, crashKey1, err := loadBisectJob(c, bug, JobBisectCause)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
job = job1
|
|
if !job.isUnreliableBisect() && (crash1.ReproC != 0 || crash.ReproC == 0) {
|
|
// Don't override the crash in this case,
|
|
// otherwise we will always think that we haven't reported the C repro.
|
|
crash, crashKey = crash1, crashKey1
|
|
}
|
|
}
|
|
crashLog, _, err := getText(c, textCrashLog, crash.Log)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(crashLog) > maxMailLogLen {
|
|
crashLog = crashLog[len(crashLog)-maxMailLogLen:]
|
|
}
|
|
report, _, err := getText(c, textCrashReport, crash.Report)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(report) > maxMailReportLen {
|
|
report = report[:maxMailReportLen]
|
|
}
|
|
reproC, _, err := getText(c, textReproC, crash.ReproC)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reproSyz, _, err := getText(c, textReproSyz, crash.ReproSyz)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(reproSyz) != 0 {
|
|
buf := new(bytes.Buffer)
|
|
buf.WriteString(syzReproPrefix)
|
|
if len(crash.ReproOpts) != 0 {
|
|
fmt.Fprintf(buf, "#%s\n", crash.ReproOpts)
|
|
}
|
|
buf.Write(reproSyz)
|
|
reproSyz = buf.Bytes()
|
|
}
|
|
build, err := loadBuild(c, bug.Namespace, crash.BuildID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
typ := dashapi.ReportNew
|
|
if !bugReporting.Reported.IsZero() {
|
|
typ = dashapi.ReportRepro
|
|
}
|
|
|
|
kernelRepo := kernelRepoInfo(build)
|
|
rep := &dashapi.BugReport{
|
|
Type: typ,
|
|
Config: reportingConfig,
|
|
ExtID: bugReporting.ExtID,
|
|
First: bugReporting.Reported.IsZero(),
|
|
Moderation: reporting.moderation,
|
|
Log: crashLog,
|
|
LogLink: externalLink(c, textCrashLog, crash.Log),
|
|
Report: report,
|
|
ReportLink: externalLink(c, textCrashReport, crash.Report),
|
|
CC: kernelRepo.CC,
|
|
Maintainers: append(crash.Maintainers, kernelRepo.Maintainers...),
|
|
ReproC: reproC,
|
|
ReproCLink: externalLink(c, textReproC, crash.ReproC),
|
|
ReproSyz: reproSyz,
|
|
ReproSyzLink: externalLink(c, textReproSyz, crash.ReproSyz),
|
|
CrashID: crashKey.IntID(),
|
|
NumCrashes: bug.NumCrashes,
|
|
HappenedOn: managersToRepos(c, bug.Namespace, bug.HappenedOn),
|
|
}
|
|
if bugReporting.CC != "" {
|
|
rep.CC = append(rep.CC, strings.Split(bugReporting.CC, "|")...)
|
|
}
|
|
if build.Type == BuildFailed {
|
|
rep.Maintainers = append(rep.Maintainers, kernelRepo.BuildMaintainers...)
|
|
}
|
|
if bug.BisectCause == BisectYes && !job.isUnreliableBisect() {
|
|
rep.BisectCause = bisectFromJob(c, rep, job)
|
|
}
|
|
if err := fillBugReport(c, rep, bug, bugReporting, build); err != nil {
|
|
return nil, err
|
|
}
|
|
return rep, nil
|
|
}
|
|
|
|
// fillBugReport fills common report fields for bug and job reports.
|
|
func fillBugReport(c context.Context, rep *dashapi.BugReport, bug *Bug, bugReporting *BugReporting,
|
|
build *Build) error {
|
|
kernelConfig, _, err := getText(c, textKernelConfig, build.KernelConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
creditEmail, err := email.AddAddrContext(ownEmail(c), bugReporting.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rep.Namespace = bug.Namespace
|
|
rep.ID = bugReporting.ID
|
|
rep.Title = bug.displayTitle()
|
|
rep.Link = fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID)
|
|
rep.CreditEmail = creditEmail
|
|
rep.OS = build.OS
|
|
rep.Arch = build.Arch
|
|
rep.VMArch = build.VMArch
|
|
rep.UserSpaceArch = kernelArch(build.Arch)
|
|
rep.CompilerID = build.CompilerID
|
|
rep.KernelRepo = build.KernelRepo
|
|
rep.KernelRepoAlias = kernelRepoInfo(build).Alias
|
|
rep.KernelBranch = build.KernelBranch
|
|
rep.KernelCommit = build.KernelCommit
|
|
rep.KernelCommitTitle = build.KernelCommitTitle
|
|
rep.KernelCommitDate = build.KernelCommitDate
|
|
rep.KernelConfig = kernelConfig
|
|
rep.KernelConfigLink = externalLink(c, textKernelConfig, build.KernelConfig)
|
|
rep.NoRepro = build.Type == BuildFailed
|
|
for _, addr := range bug.UNCC {
|
|
rep.CC = email.RemoveFromEmailList(rep.CC, addr)
|
|
rep.Maintainers = email.RemoveFromEmailList(rep.Maintainers, addr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loadBisectJob(c context.Context, bug *Bug, jobType JobType) (*Job, *Crash, *db.Key, *db.Key, error) {
|
|
bugKey := bug.key(c)
|
|
var jobs []*Job
|
|
keys, err := db.NewQuery("Job").
|
|
Ancestor(bugKey).
|
|
Filter("Type=", jobType).
|
|
Filter("Finished>", time.Time{}).
|
|
Order("-Finished").
|
|
Limit(1).
|
|
GetAll(c, &jobs)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, fmt.Errorf("failed to query jobs: %v", err)
|
|
}
|
|
if len(jobs) == 0 {
|
|
jobStr := map[JobType]string{
|
|
JobBisectCause: "bisect cause",
|
|
JobBisectFix: "bisect fix",
|
|
}
|
|
return nil, nil, nil, nil, fmt.Errorf("can't find %s job for bug", jobStr[jobType])
|
|
}
|
|
job := jobs[0]
|
|
crash := new(Crash)
|
|
crashKey := db.NewKey(c, "Crash", "", job.CrashID, bugKey)
|
|
if err := db.Get(c, crashKey, crash); err != nil {
|
|
return nil, nil, nil, nil, fmt.Errorf("failed to get crash: %v", err)
|
|
}
|
|
return job, crash, keys[0], crashKey, nil
|
|
}
|
|
|
|
func managersToRepos(c context.Context, ns string, managers []string) []string {
|
|
var repos []string
|
|
dedup := make(map[string]bool)
|
|
for _, manager := range managers {
|
|
build, err := lastManagerBuild(c, ns, manager)
|
|
if err != nil {
|
|
log.Errorf(c, "failed to get manager %q build: %v", manager, err)
|
|
continue
|
|
}
|
|
repo := kernelRepoInfo(build).Alias
|
|
if dedup[repo] {
|
|
continue
|
|
}
|
|
dedup[repo] = true
|
|
repos = append(repos, repo)
|
|
}
|
|
sort.Strings(repos)
|
|
return repos
|
|
}
|
|
|
|
func loadAllBugs(c context.Context, filter func(*db.Query) *db.Query) ([]*Bug, []*db.Key, error) {
|
|
var bugs []*Bug
|
|
var keys []*db.Key
|
|
err := foreachBug(c, filter, func(bug *Bug, key *db.Key) error {
|
|
bugs = append(bugs, bug)
|
|
keys = append(keys, key)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return bugs, keys, nil
|
|
}
|
|
|
|
func loadNamespaceBugs(c context.Context, ns string) ([]*Bug, []*db.Key, error) {
|
|
return loadAllBugs(c, func(query *db.Query) *db.Query {
|
|
return query.Filter("Namespace=", ns)
|
|
})
|
|
}
|
|
|
|
func loadOpenBugs(c context.Context) ([]*Bug, []*db.Key, error) {
|
|
return loadAllBugs(c, func(query *db.Query) *db.Query {
|
|
return query.Filter("Status<", BugStatusFixed)
|
|
})
|
|
}
|
|
|
|
func foreachBug(c context.Context, filter func(*db.Query) *db.Query, fn func(bug *Bug, key *db.Key) error) error {
|
|
const batchSize = 1000
|
|
var cursor *db.Cursor
|
|
for {
|
|
query := db.NewQuery("Bug").Limit(batchSize)
|
|
if filter != nil {
|
|
query = filter(query)
|
|
}
|
|
if cursor != nil {
|
|
query = query.Start(*cursor)
|
|
}
|
|
iter := query.Run(c)
|
|
for i := 0; ; i++ {
|
|
bug := new(Bug)
|
|
key, err := iter.Next(bug)
|
|
if err == db.Done {
|
|
if i < batchSize {
|
|
return nil
|
|
}
|
|
break
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch bugs: %v", err)
|
|
}
|
|
if err := fn(bug, key); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
cur, err := iter.Cursor()
|
|
if err != nil {
|
|
return fmt.Errorf("cursor failed while fetching bugs: %v", err)
|
|
}
|
|
cursor = &cur
|
|
}
|
|
}
|
|
|
|
// reportingPollClosed is called by backends to get list of closed bugs.
|
|
func reportingPollClosed(c context.Context, ids []string) ([]string, error) {
|
|
idMap := make(map[string]bool, len(ids))
|
|
for _, id := range ids {
|
|
idMap[id] = true
|
|
}
|
|
var closed []string
|
|
err := foreachBug(c, nil, func(bug *Bug, _ *db.Key) error {
|
|
for i := range bug.Reporting {
|
|
bugReporting := &bug.Reporting[i]
|
|
if !idMap[bugReporting.ID] {
|
|
continue
|
|
}
|
|
var err error
|
|
bug, err = canonicalBug(c, bug)
|
|
if err != nil {
|
|
log.Errorf(c, "%v", err)
|
|
break
|
|
}
|
|
if bug.Status >= BugStatusFixed || !bugReporting.Closed.IsZero() {
|
|
closed = append(closed, bugReporting.ID)
|
|
}
|
|
break
|
|
}
|
|
return nil
|
|
})
|
|
return closed, err
|
|
}
|
|
|
|
// incomingCommand is entry point to bug status updates.
|
|
func incomingCommand(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) {
|
|
log.Infof(c, "got command: %+v", cmd)
|
|
ok, reason, err := incomingCommandImpl(c, cmd)
|
|
if err != nil {
|
|
log.Errorf(c, "%v (%v)", reason, err)
|
|
} else if !ok && reason != "" {
|
|
log.Errorf(c, "invalid update: %v", reason)
|
|
}
|
|
return ok, reason, err
|
|
}
|
|
|
|
func incomingCommandImpl(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) {
|
|
for i, com := range cmd.FixCommits {
|
|
if len(com) >= 2 && com[0] == '"' && com[len(com)-1] == '"' {
|
|
com = com[1 : len(com)-1]
|
|
cmd.FixCommits[i] = com
|
|
}
|
|
if len(com) < 3 {
|
|
return false, fmt.Sprintf("bad commit title: %q", com), nil
|
|
}
|
|
}
|
|
bug, bugKey, err := findBugByReportingID(c, cmd.ID)
|
|
if err != nil {
|
|
return false, internalError, err
|
|
}
|
|
var dupKey *db.Key
|
|
if cmd.Status == dashapi.BugStatusDup {
|
|
if looksLikeReportingHash(cmd.DupOf) {
|
|
_, dupKey, _ = findBugByReportingID(c, cmd.DupOf)
|
|
}
|
|
if dupKey == nil {
|
|
// Email reporting passes bug title in cmd.DupOf, try to find bug by title.
|
|
var dup *Bug
|
|
dup, dupKey, err = findDupByTitle(c, bug.Namespace, cmd.DupOf)
|
|
if err != nil {
|
|
return false, "can't find the dup bug", err
|
|
}
|
|
dupReporting := lastReportedReporting(dup)
|
|
if dupReporting == nil {
|
|
return false, "can't find the dup bug", fmt.Errorf("dup does not have reporting")
|
|
}
|
|
cmd.DupOf = dupReporting.ID
|
|
}
|
|
}
|
|
now := timeNow(c)
|
|
ok, reply := false, ""
|
|
tx := func(c context.Context) error {
|
|
var err error
|
|
ok, reply, err = incomingCommandTx(c, now, cmd, bugKey, dupKey)
|
|
return err
|
|
}
|
|
err = db.RunInTransaction(c, tx, &db.TransactionOptions{
|
|
XG: true,
|
|
// Default is 3 which fails sometimes.
|
|
// We don't want incoming bug updates to fail,
|
|
// because for e.g. email we won't have an external retry.
|
|
Attempts: 30,
|
|
})
|
|
if err != nil {
|
|
return false, internalError, err
|
|
}
|
|
return ok, reply, nil
|
|
}
|
|
|
|
func checkDupBug(c context.Context, cmd *dashapi.BugUpdate, bug *Bug, bugKey, dupKey *db.Key) (
|
|
*Bug, bool, string, error) {
|
|
dup := new(Bug)
|
|
if err := db.Get(c, dupKey, dup); err != nil {
|
|
return nil, false, internalError, fmt.Errorf("can't find the dup by key: %v", err)
|
|
}
|
|
bugReporting, _ := bugReportingByID(bug, cmd.ID)
|
|
dupReporting, _ := bugReportingByID(dup, cmd.DupOf)
|
|
if bugReporting == nil || dupReporting == nil {
|
|
return nil, false, internalError, fmt.Errorf("can't find bug reporting")
|
|
}
|
|
if bugKey.StringID() == dupKey.StringID() {
|
|
if bugReporting.Name == dupReporting.Name {
|
|
return nil, false, "Can't dup bug to itself.", nil
|
|
}
|
|
return nil, false, fmt.Sprintf("Can't dup bug to itself in different reporting (%v->%v).\n"+
|
|
"Please dup syzbot bugs only onto syzbot bugs for the same kernel/reporting.",
|
|
bugReporting.Name, dupReporting.Name), nil
|
|
}
|
|
if bug.Namespace != dup.Namespace {
|
|
return nil, false, fmt.Sprintf("Duplicate bug corresponds to a different kernel (%v->%v).\n"+
|
|
"Please dup syzbot bugs only onto syzbot bugs for the same kernel.",
|
|
bug.Namespace, dup.Namespace), nil
|
|
}
|
|
if !allowCrossReportingDup(c, bug, dup, bugReporting, dupReporting) {
|
|
return nil, false, fmt.Sprintf("Can't dup bug to a bug in different reporting (%v->%v)."+
|
|
"Please dup syzbot bugs only onto syzbot bugs for the same kernel/reporting.",
|
|
bugReporting.Name, dupReporting.Name), nil
|
|
}
|
|
dupCanon, err := canonicalBug(c, dup)
|
|
if err != nil {
|
|
return nil, false, internalError, fmt.Errorf("failed to get canonical bug for dup: %v", err)
|
|
}
|
|
if !dupReporting.Closed.IsZero() && dupCanon.Status == BugStatusOpen {
|
|
return nil, false, "Dup bug is already upstreamed.", nil
|
|
}
|
|
if dupCanon.keyHash() == bugKey.StringID() {
|
|
return nil, false, "Setting this dup would lead to a bug cycle, cycles are not allowed.", nil
|
|
}
|
|
return dup, true, "", nil
|
|
}
|
|
|
|
func allowCrossReportingDup(c context.Context, bug, dup *Bug,
|
|
bugReporting, dupReporting *BugReporting) bool {
|
|
bugIdx := getReportingIdx(c, bug, bugReporting)
|
|
dupIdx := getReportingIdx(c, dup, dupReporting)
|
|
if bugIdx < 0 || dupIdx < 0 {
|
|
return false
|
|
}
|
|
if bugIdx == dupIdx {
|
|
return true
|
|
}
|
|
// We generally allow duping only within the same reporting.
|
|
// But there is one exception: we also allow duping from last but one
|
|
// reporting to the last one (which is stable, final destination)
|
|
// provided that these two reportings have the same access level and type.
|
|
// The rest of the combinations can lead to surprising states and
|
|
// information hiding, so we don't allow them.
|
|
cfg := config.Namespaces[bug.Namespace]
|
|
bugConfig := &cfg.Reporting[bugIdx]
|
|
dupConfig := &cfg.Reporting[dupIdx]
|
|
lastIdx := len(cfg.Reporting) - 1
|
|
return bugIdx == lastIdx-1 && dupIdx == lastIdx &&
|
|
bugConfig.AccessLevel == dupConfig.AccessLevel &&
|
|
bugConfig.Config.Type() == dupConfig.Config.Type()
|
|
}
|
|
|
|
func getReportingIdx(c context.Context, bug *Bug, bugReporting *BugReporting) int {
|
|
for i := range bug.Reporting {
|
|
if bug.Reporting[i].Name == bugReporting.Name {
|
|
return i
|
|
}
|
|
}
|
|
log.Errorf(c, "failed to find bug reporting by name: %q/%q", bug.Title, bugReporting.Name)
|
|
return -1
|
|
}
|
|
|
|
func incomingCommandTx(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bugKey, dupKey *db.Key) (
|
|
bool, string, error) {
|
|
bug := new(Bug)
|
|
if err := db.Get(c, bugKey, bug); err != nil {
|
|
return false, internalError, fmt.Errorf("can't find the corresponding bug: %v", err)
|
|
}
|
|
var dup *Bug
|
|
if cmd.Status == dashapi.BugStatusDup {
|
|
dup1, ok, reason, err := checkDupBug(c, cmd, bug, bugKey, dupKey)
|
|
if !ok || err != nil {
|
|
return ok, reason, err
|
|
}
|
|
dup = dup1
|
|
}
|
|
state, err := loadReportingState(c)
|
|
if err != nil {
|
|
return false, internalError, err
|
|
}
|
|
ok, reason, err := incomingCommandUpdate(c, now, cmd, bugKey, bug, dup, state)
|
|
if !ok || err != nil {
|
|
return ok, reason, err
|
|
}
|
|
if _, err := db.Put(c, bugKey, bug); err != nil {
|
|
return false, internalError, fmt.Errorf("failed to put bug: %v", err)
|
|
}
|
|
if err := saveReportingState(c, state); err != nil {
|
|
return false, internalError, err
|
|
}
|
|
return true, "", nil
|
|
}
|
|
|
|
func incomingCommandUpdate(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bugKey *db.Key,
|
|
bug, dup *Bug, state *ReportingState) (bool, string, error) {
|
|
bugReporting, final := bugReportingByID(bug, cmd.ID)
|
|
if bugReporting == nil {
|
|
return false, internalError, fmt.Errorf("can't find bug reporting")
|
|
}
|
|
if ok, reply, err := checkBugStatus(c, cmd, bug, bugReporting); !ok {
|
|
return false, reply, err
|
|
}
|
|
stateEnt := state.getEntry(now, bug.Namespace, bugReporting.Name)
|
|
if ok, reply, err := incomingCommandCmd(c, now, cmd, bug, dup, bugReporting, final, stateEnt); !ok {
|
|
return false, reply, err
|
|
}
|
|
if len(cmd.FixCommits) != 0 && (bug.Status == BugStatusOpen || bug.Status == BugStatusDup) {
|
|
sort.Strings(cmd.FixCommits)
|
|
if !reflect.DeepEqual(bug.Commits, cmd.FixCommits) {
|
|
bug.updateCommits(cmd.FixCommits, now)
|
|
}
|
|
}
|
|
if cmd.CrashID != 0 {
|
|
// Rememeber that we've reported this crash.
|
|
if err := markCrashReported(c, cmd.CrashID, bugKey, now); err != nil {
|
|
return false, internalError, err
|
|
}
|
|
bugReporting.CrashID = cmd.CrashID
|
|
}
|
|
if bugReporting.ExtID == "" {
|
|
bugReporting.ExtID = cmd.ExtID
|
|
}
|
|
if bugReporting.Link == "" {
|
|
bugReporting.Link = cmd.Link
|
|
}
|
|
if len(cmd.CC) != 0 && cmd.Status != dashapi.BugStatusUnCC {
|
|
merged := email.MergeEmailLists(strings.Split(bugReporting.CC, "|"), cmd.CC)
|
|
bugReporting.CC = strings.Join(merged, "|")
|
|
}
|
|
if bugReporting.ReproLevel < cmd.ReproLevel {
|
|
bugReporting.ReproLevel = cmd.ReproLevel
|
|
}
|
|
if bug.Status != BugStatusDup {
|
|
bug.DupOf = ""
|
|
}
|
|
if cmd.Status != dashapi.BugStatusOpen || !cmd.OnHold {
|
|
bugReporting.OnHold = time.Time{}
|
|
}
|
|
bug.LastActivity = now
|
|
return true, "", nil
|
|
}
|
|
|
|
func incomingCommandCmd(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bug, dup *Bug,
|
|
bugReporting *BugReporting, final bool, stateEnt *ReportingStateEntry) (bool, string, error) {
|
|
switch cmd.Status {
|
|
case dashapi.BugStatusOpen:
|
|
bug.Status = BugStatusOpen
|
|
bug.Closed = time.Time{}
|
|
if bugReporting.Reported.IsZero() {
|
|
bugReporting.Reported = now
|
|
stateEnt.Sent++ // sending repro does not count against the quota
|
|
}
|
|
if bugReporting.OnHold.IsZero() && cmd.OnHold {
|
|
bugReporting.OnHold = now
|
|
}
|
|
// Close all previous reporting if they are not closed yet
|
|
// (can happen due to Status == ReportingDisabled).
|
|
for i := range bug.Reporting {
|
|
if bugReporting == &bug.Reporting[i] {
|
|
break
|
|
}
|
|
if bug.Reporting[i].Closed.IsZero() {
|
|
bug.Reporting[i].Closed = now
|
|
}
|
|
}
|
|
if bug.ReproLevel < cmd.ReproLevel {
|
|
return false, internalError,
|
|
fmt.Errorf("bug update with invalid repro level: %v/%v",
|
|
bug.ReproLevel, cmd.ReproLevel)
|
|
}
|
|
case dashapi.BugStatusUpstream:
|
|
if final {
|
|
return false, "Can't upstream, this is final destination.", nil
|
|
}
|
|
if len(bug.Commits) != 0 {
|
|
// We could handle this case, but how/when it will occur
|
|
// in real life is unclear now.
|
|
return false, "Can't upstream this bug, the bug has fixing commits.", nil
|
|
}
|
|
bug.Status = BugStatusOpen
|
|
bug.Closed = time.Time{}
|
|
bugReporting.Closed = now
|
|
bugReporting.Auto = cmd.Notification
|
|
case dashapi.BugStatusInvalid:
|
|
bug.Closed = now
|
|
bug.Status = BugStatusInvalid
|
|
bugReporting.Closed = now
|
|
bugReporting.Auto = cmd.Notification
|
|
case dashapi.BugStatusDup:
|
|
bug.Status = BugStatusDup
|
|
bug.Closed = now
|
|
bug.DupOf = dup.keyHash()
|
|
case dashapi.BugStatusUpdate:
|
|
// Just update Link, Commits, etc below.
|
|
case dashapi.BugStatusUnCC:
|
|
bug.UNCC = email.MergeEmailLists(bug.UNCC, cmd.CC)
|
|
default:
|
|
return false, internalError, fmt.Errorf("unknown bug status %v", cmd.Status)
|
|
}
|
|
return true, "", nil
|
|
}
|
|
|
|
func checkBugStatus(c context.Context, cmd *dashapi.BugUpdate, bug *Bug, bugReporting *BugReporting) (
|
|
bool, string, error) {
|
|
switch bug.Status {
|
|
case BugStatusOpen:
|
|
case BugStatusDup:
|
|
canon, err := canonicalBug(c, bug)
|
|
if err != nil {
|
|
return false, internalError, err
|
|
}
|
|
if canon.Status != BugStatusOpen {
|
|
// We used to reject updates to closed bugs,
|
|
// but this is confusing and non-actionable for users.
|
|
// So now we fail the update, but give empty reason,
|
|
// which means "don't notify user".
|
|
if cmd.Status == dashapi.BugStatusUpdate {
|
|
// This happens when people discuss old bugs.
|
|
log.Infof(c, "Dup bug is already closed")
|
|
} else {
|
|
log.Errorf(c, "Dup bug is already closed")
|
|
}
|
|
return false, "", nil
|
|
}
|
|
case BugStatusFixed, BugStatusInvalid:
|
|
if cmd.Status != dashapi.BugStatusUpdate {
|
|
log.Errorf(c, "This bug is already closed")
|
|
}
|
|
return false, "", nil
|
|
default:
|
|
return false, internalError, fmt.Errorf("unknown bug status %v", bug.Status)
|
|
}
|
|
if !bugReporting.Closed.IsZero() {
|
|
if cmd.Status != dashapi.BugStatusUpdate {
|
|
log.Errorf(c, "This bug reporting is already closed")
|
|
}
|
|
return false, "", nil
|
|
}
|
|
return true, "", nil
|
|
}
|
|
|
|
func findBugByReportingID(c context.Context, id string) (*Bug, *db.Key, error) {
|
|
var bugs []*Bug
|
|
keys, err := db.NewQuery("Bug").
|
|
Filter("Reporting.ID=", id).
|
|
Limit(2).
|
|
GetAll(c, &bugs)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to fetch bugs: %v", err)
|
|
}
|
|
if len(bugs) == 0 {
|
|
return nil, nil, fmt.Errorf("failed to find bug by reporting id %q", id)
|
|
}
|
|
if len(bugs) > 1 {
|
|
return nil, nil, fmt.Errorf("multiple bugs for reporting id %q", id)
|
|
}
|
|
return bugs[0], keys[0], nil
|
|
}
|
|
|
|
func findDupByTitle(c context.Context, ns, title string) (*Bug, *db.Key, error) {
|
|
title, seq, err := splitDisplayTitle(title)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
bugHash := bugKeyHash(ns, title, seq)
|
|
bugKey := db.NewKey(c, "Bug", bugHash, 0, nil)
|
|
bug := new(Bug)
|
|
if err := db.Get(c, bugKey, bug); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to get dup: %v", err)
|
|
}
|
|
return bug, bugKey, nil
|
|
}
|
|
|
|
func bugReportingByID(bug *Bug, id string) (*BugReporting, bool) {
|
|
for i := range bug.Reporting {
|
|
if bug.Reporting[i].ID == id {
|
|
return &bug.Reporting[i], i == len(bug.Reporting)-1
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func bugReportingByName(bug *Bug, name string) *BugReporting {
|
|
for i := range bug.Reporting {
|
|
if bug.Reporting[i].Name == name {
|
|
return &bug.Reporting[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func lastReportedReporting(bug *Bug) *BugReporting {
|
|
for i := len(bug.Reporting) - 1; i >= 0; i-- {
|
|
if !bug.Reporting[i].Reported.IsZero() {
|
|
return &bug.Reporting[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func queryCrashesForBug(c context.Context, bugKey *db.Key, limit int) (
|
|
[]*Crash, []*db.Key, error) {
|
|
var crashes []*Crash
|
|
keys, err := db.NewQuery("Crash").
|
|
Ancestor(bugKey).
|
|
Order("-ReportLen").
|
|
Order("-Time").
|
|
Limit(limit).
|
|
GetAll(c, &crashes)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to fetch crashes: %v", err)
|
|
}
|
|
return crashes, keys, nil
|
|
}
|
|
|
|
func queryJobsForBug(c context.Context, bugKey *db.Key, jobType JobType) (
|
|
[]*Job, []*db.Key, error) {
|
|
var jobs []*Job
|
|
keys, err := db.NewQuery("Job").
|
|
Ancestor(bugKey).
|
|
Filter("Type=", jobType).
|
|
Filter("Finished>", time.Time{}).
|
|
Order("-Finished").
|
|
GetAll(c, &jobs)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to fetch fix bisections: %v", err)
|
|
}
|
|
return jobs, keys, nil
|
|
}
|
|
|
|
func queryCrashForJob(c context.Context, job *Job, bugKey *db.Key) (*Crash, error) {
|
|
// If there was no crash corresponding to the Job, return.
|
|
if job.CrashTitle == "" {
|
|
return nil, nil
|
|
}
|
|
// First, fetch the crash who's repro was used to start the bisection
|
|
// job.
|
|
crash := new(Crash)
|
|
crashKey := db.NewKey(c, "Crash", "", job.CrashID, bugKey)
|
|
if err := db.Get(c, crashKey, crash); err != nil {
|
|
return nil, err
|
|
}
|
|
// Now, create a crash object with the crash details from the job.
|
|
ret := &Crash{
|
|
Manager: crash.Manager,
|
|
Time: job.Finished,
|
|
Log: job.Log,
|
|
Report: job.CrashReport,
|
|
ReproOpts: crash.ReproOpts,
|
|
ReproSyz: crash.ReproSyz,
|
|
ReproC: crash.ReproC,
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func findCrashForBug(c context.Context, bug *Bug) (*Crash, *db.Key, error) {
|
|
bugKey := bug.key(c)
|
|
crashes, keys, err := queryCrashesForBug(c, bugKey, 1)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if len(crashes) < 1 {
|
|
return nil, nil, fmt.Errorf("no crashes")
|
|
}
|
|
crash, key := crashes[0], keys[0]
|
|
if bug.ReproLevel == ReproLevelC {
|
|
if crash.ReproC == 0 {
|
|
log.Errorf(c, "bug '%v': has C repro, but crash without C repro", bug.Title)
|
|
}
|
|
} else if bug.ReproLevel == ReproLevelSyz {
|
|
if crash.ReproSyz == 0 {
|
|
log.Errorf(c, "bug '%v': has syz repro, but crash without syz repro", bug.Title)
|
|
}
|
|
} else if bug.HasReport {
|
|
if crash.Report == 0 {
|
|
log.Errorf(c, "bug '%v': has report, but crash without report", bug.Title)
|
|
}
|
|
}
|
|
return crash, key, nil
|
|
}
|
|
|
|
func loadReportingState(c context.Context) (*ReportingState, error) {
|
|
state := new(ReportingState)
|
|
key := db.NewKey(c, "ReportingState", "", 1, nil)
|
|
if err := db.Get(c, key, state); err != nil && err != db.ErrNoSuchEntity {
|
|
return nil, fmt.Errorf("failed to get reporting state: %v", err)
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
func saveReportingState(c context.Context, state *ReportingState) error {
|
|
key := db.NewKey(c, "ReportingState", "", 1, nil)
|
|
if _, err := db.Put(c, key, state); err != nil {
|
|
return fmt.Errorf("failed to put reporting state: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (state *ReportingState) getEntry(now time.Time, namespace, name string) *ReportingStateEntry {
|
|
if namespace == "" || name == "" {
|
|
panic(fmt.Sprintf("requesting reporting state for %v/%v", namespace, name))
|
|
}
|
|
// Convert time to date of the form 20170125.
|
|
date := timeDate(now)
|
|
for i := range state.Entries {
|
|
ent := &state.Entries[i]
|
|
if ent.Namespace == namespace && ent.Name == name {
|
|
if ent.Date != date {
|
|
ent.Date = date
|
|
ent.Sent = 0
|
|
}
|
|
return ent
|
|
}
|
|
}
|
|
state.Entries = append(state.Entries, ReportingStateEntry{
|
|
Namespace: namespace,
|
|
Name: name,
|
|
Date: date,
|
|
Sent: 0,
|
|
})
|
|
return &state.Entries[len(state.Entries)-1]
|
|
}
|
|
|
|
// bugReportSorter sorts bugs by priority we want to report them.
|
|
// E.g. we want to report bugs with reproducers before bugs without reproducers.
|
|
type bugReportSorter []*Bug
|
|
|
|
func (a bugReportSorter) Len() int { return len(a) }
|
|
func (a bugReportSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a bugReportSorter) Less(i, j int) bool {
|
|
if a[i].ReproLevel != a[j].ReproLevel {
|
|
return a[i].ReproLevel > a[j].ReproLevel
|
|
}
|
|
if a[i].HasReport != a[j].HasReport {
|
|
return a[i].HasReport
|
|
}
|
|
if a[i].NumCrashes != a[j].NumCrashes {
|
|
return a[i].NumCrashes > a[j].NumCrashes
|
|
}
|
|
return a[i].FirstTime.Before(a[j].FirstTime)
|
|
}
|
|
|
|
// kernelArch returns arch as kernel developers know it (rather than Go names).
|
|
// Currently Linux-specific.
|
|
func kernelArch(arch string) string {
|
|
switch arch {
|
|
case "386":
|
|
return "i386"
|
|
case "amd64":
|
|
return "" // this is kinda the default, so we don't notify about it
|
|
default:
|
|
return arch
|
|
}
|
|
}
|