blob: 88bcfce1ed6a415789b74317feb80491b9aa84a6 [file] [log] [blame]
// 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 dash
import (
"bytes"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/email"
"github.com/google/syzkaller/pkg/html"
"github.com/google/syzkaller/pkg/vcs"
"golang.org/x/net/context"
db "google.golang.org/appengine/datastore"
"google.golang.org/appengine/log"
)
// This file contains web UI http handlers.
func initHTTPHandlers() {
http.Handle("/", handlerWrapper(handleMain))
http.Handle("/bug", handlerWrapper(handleBug))
http.Handle("/text", handlerWrapper(handleText))
http.Handle("/admin", handlerWrapper(handleAdmin))
http.Handle("/x/.config", handlerWrapper(handleTextX(textKernelConfig)))
http.Handle("/x/log.txt", handlerWrapper(handleTextX(textCrashLog)))
http.Handle("/x/report.txt", handlerWrapper(handleTextX(textCrashReport)))
http.Handle("/x/repro.syz", handlerWrapper(handleTextX(textReproSyz)))
http.Handle("/x/repro.c", handlerWrapper(handleTextX(textReproC)))
http.Handle("/x/patch.diff", handlerWrapper(handleTextX(textPatch)))
http.Handle("/x/bisect.txt", handlerWrapper(handleTextX(textLog)))
http.Handle("/x/error.txt", handlerWrapper(handleTextX(textError)))
for ns := range config.Namespaces {
http.Handle("/"+ns, handlerWrapper(handleMain))
http.Handle("/"+ns+"/fixed", handlerWrapper(handleFixed))
}
}
type uiMainPage struct {
Header *uiHeader
Now time.Time
FixedLink string
FixedCount int
Managers []*uiManager
Groups []*uiBugGroup
}
type uiFixedPage struct {
Header *uiHeader
Now time.Time
Bugs *uiBugGroup
}
type uiAdminPage struct {
Header *uiHeader
Log []byte
Managers []*uiManager
Jobs []*uiJob
}
type uiManager struct {
Now time.Time
Namespace string
Name string
Link string
CoverLink string
CurrentBuild *uiBuild
FailedBuildBugLink string
FailedSyzBuildBugLink string
LastActive time.Time
LastActiveBad bool
CurrentUpTime time.Duration
MaxCorpus int64
MaxCover int64
TotalFuzzingTime time.Duration
TotalCrashes int64
TotalExecs int64
}
type uiBuild struct {
Time time.Time
SyzkallerCommit string
SyzkallerCommitLink string
SyzkallerCommitDate time.Time
KernelAlias string
KernelCommit string
KernelCommitLink string
KernelCommitTitle string
KernelCommitDate time.Time
KernelConfigLink string
}
type uiCommit struct {
Hash string
Title string
Link string
Author string
CC []string
Date time.Time
}
type uiBugPage struct {
Header *uiHeader
Now time.Time
Bug *uiBug
BisectCause *uiJob
DupOf *uiBugGroup
Dups *uiBugGroup
Similar *uiBugGroup
SampleReport []byte
HasMaintainers bool
Crashes []*uiCrash
}
type uiBugGroup struct {
Now time.Time
Caption string
Fragment string
Namespace string
ShowNamespace bool
ShowPatch bool
ShowPatched bool
ShowStatus bool
ShowIndex int
Bugs []*uiBug
}
type uiBug struct {
Namespace string
Title string
NumCrashes int64
NumCrashesBad bool
BisectCause bool
FirstTime time.Time
LastTime time.Time
ReportedTime time.Time
ClosedTime time.Time
ReproLevel dashapi.ReproLevel
ReportingIndex int
Status string
Link string
ExternalLink string
CreditEmail string
Commits []*uiCommit
PatchedOn []string
MissingOn []string
NumManagers int
}
type uiCrash struct {
Manager string
Time time.Time
Maintainers string
LogLink string
ReportLink string
ReproSyzLink string
ReproCLink string
*uiBuild
}
type uiJob struct {
Type JobType
Created time.Time
BugLink string
ExternalLink string
User string
Reporting string
Namespace string
Manager string
BugTitle string
BugID string
KernelAlias string
KernelCommit string
PatchLink string
Attempts int
Started time.Time
Finished time.Time
Duration time.Duration
CrashTitle string
CrashLogLink string
CrashReportLink string
LogLink string
ErrorLink string
Commit *uiCommit // for conclusive bisection
Commits []*uiCommit // for inconclusive bisection
Crash *uiCrash
Reported bool
}
// handleMain serves main page.
func handleMain(c context.Context, w http.ResponseWriter, r *http.Request) error {
if ns := r.FormValue("fixed"); ns != "" {
http.Redirect(w, r, fmt.Sprintf("/%v/fixed", ns), http.StatusMovedPermanently)
return nil
}
hdr, err := commonHeader(c, r, w, "")
if err != nil {
return err
}
accessLevel := accessLevel(c, r)
managers, err := loadManagers(c, accessLevel, hdr.Namespace)
if err != nil {
return err
}
groups, fixedCount, err := fetchNamespaceBugs(c, accessLevel, hdr.Namespace)
if err != nil {
return err
}
data := &uiMainPage{
Header: hdr,
Now: timeNow(c),
FixedCount: fixedCount,
FixedLink: fmt.Sprintf("/%v/fixed", hdr.Namespace),
Groups: groups,
Managers: managers,
}
return serveTemplate(w, "main.html", data)
}
func handleFixed(c context.Context, w http.ResponseWriter, r *http.Request) error {
accessLevel := accessLevel(c, r)
hdr, err := commonHeader(c, r, w, "")
if err != nil {
return err
}
hdr.Subpage = "/fixed"
bugs, err := fetchFixedBugs(c, accessLevel, hdr.Namespace)
if err != nil {
return err
}
data := &uiFixedPage{
Header: hdr,
Now: timeNow(c),
Bugs: bugs,
}
return serveTemplate(w, "fixed.html", data)
}
func handleAdmin(c context.Context, w http.ResponseWriter, r *http.Request) error {
accessLevel := accessLevel(c, r)
if accessLevel != AccessAdmin {
return ErrAccess
}
hdr, err := commonHeader(c, r, w, "")
if err != nil {
return err
}
managers, err := loadManagers(c, accessLevel, "")
if err != nil {
return err
}
errorLog, err := fetchErrorLogs(c)
if err != nil {
return err
}
jobs, err := loadRecentJobs(c)
if err != nil {
return err
}
data := &uiAdminPage{
Header: hdr,
Log: errorLog,
Managers: managers,
Jobs: jobs,
}
return serveTemplate(w, "admin.html", data)
}
// handleBug serves page about a single bug (which is passed in id argument).
func handleBug(c context.Context, w http.ResponseWriter, r *http.Request) error {
bug := new(Bug)
if id := r.FormValue("id"); id != "" {
bugKey := db.NewKey(c, "Bug", id, 0, nil)
if err := db.Get(c, bugKey, bug); err != nil {
return err
}
} else if extID := r.FormValue("extid"); extID != "" {
var err error
bug, _, err = findBugByReportingID(c, extID)
if err != nil {
return err
}
} else {
return ErrDontLog(fmt.Errorf("mandatory parameter id/extid is missing"))
}
accessLevel := accessLevel(c, r)
if err := checkAccessLevel(c, r, bug.sanitizeAccess(accessLevel)); err != nil {
return err
}
hdr, err := commonHeader(c, r, w, bug.Namespace)
if err != nil {
return err
}
state, err := loadReportingState(c)
if err != nil {
return err
}
managers, err := managerList(c, bug.Namespace)
if err != nil {
return err
}
var dupOf *uiBugGroup
if bug.DupOf != "" {
dup := new(Bug)
if err := db.Get(c, db.NewKey(c, "Bug", bug.DupOf, 0, nil), dup); err != nil {
return err
}
if accessLevel >= dup.sanitizeAccess(accessLevel) {
dupOf = &uiBugGroup{
Now: timeNow(c),
Caption: "Duplicate of",
Bugs: []*uiBug{createUIBug(c, dup, state, managers)},
}
}
}
uiBug := createUIBug(c, bug, state, managers)
crashes, sampleReport, err := loadCrashesForBug(c, bug)
if err != nil {
return err
}
dups, err := loadDupsForBug(c, r, bug, state, managers)
if err != nil {
return err
}
similar, err := loadSimilarBugs(c, r, bug, state)
if err != nil {
return err
}
var bisectCause *uiJob
if bug.BisectCause > BisectPending {
job, _, jobKey, _, err := loadBisectJob(c, bug)
if err != nil {
return err
}
crash := new(Crash)
crashKey := db.NewKey(c, "Crash", "", job.CrashID, bug.key(c))
if err := db.Get(c, crashKey, crash); err != nil {
return fmt.Errorf("failed to get crash: %v", err)
}
build, err := loadBuild(c, bug.Namespace, crash.BuildID)
if err != nil {
return err
}
bisectCause = makeUIJob(job, jobKey, crash, build)
}
hasMaintainers := false
for _, crash := range crashes {
if len(crash.Maintainers) != 0 {
hasMaintainers = true
break
}
}
data := &uiBugPage{
Header: hdr,
Now: timeNow(c),
Bug: uiBug,
BisectCause: bisectCause,
DupOf: dupOf,
Dups: dups,
Similar: similar,
SampleReport: sampleReport,
HasMaintainers: hasMaintainers,
Crashes: crashes,
}
return serveTemplate(w, "bug.html", data)
}
// handleText serves plain text blobs (crash logs, reports, reproducers, etc).
func handleTextImpl(c context.Context, w http.ResponseWriter, r *http.Request, tag string) error {
var id int64
if x := r.FormValue("x"); x != "" {
xid, err := strconv.ParseUint(x, 16, 64)
if err != nil || xid == 0 {
return ErrDontLog(fmt.Errorf("failed to parse text id: %v", err))
}
id = int64(xid)
} else {
// Old link support, don't remove.
xid, err := strconv.ParseInt(r.FormValue("id"), 10, 64)
if err != nil || xid == 0 {
return ErrDontLog(fmt.Errorf("failed to parse text id: %v", err))
}
id = xid
}
crash, err := checkTextAccess(c, r, tag, id)
if err != nil {
return err
}
data, ns, err := getText(c, tag, id)
if err != nil {
return err
}
if err := checkAccessLevel(c, r, config.Namespaces[ns].AccessLevel); err != nil {
return err
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
// Unfortunately filename does not work in chrome on linux due to:
// https://bugs.chromium.org/p/chromium/issues/detail?id=608342
w.Header().Set("Content-Disposition", "inline; filename="+textFilename(tag))
if tag == textReproSyz {
// Add link to documentation and repro opts for syzkaller reproducers.
w.Write([]byte(syzReproPrefix))
if crash != nil {
fmt.Fprintf(w, "#%s\n", crash.ReproOpts)
}
}
w.Write(data)
return nil
}
func handleText(c context.Context, w http.ResponseWriter, r *http.Request) error {
return handleTextImpl(c, w, r, r.FormValue("tag"))
}
func handleTextX(tag string) contextHandler {
return func(c context.Context, w http.ResponseWriter, r *http.Request) error {
return handleTextImpl(c, w, r, tag)
}
}
func textFilename(tag string) string {
switch tag {
case textKernelConfig:
return ".config"
case textCrashLog:
return "log.txt"
case textCrashReport:
return "report.txt"
case textReproSyz:
return "repro.syz"
case textReproC:
return "repro.c"
case textPatch:
return "patch.diff"
case textLog:
return "bisect.txt"
case textError:
return "error.txt"
default:
return "text.txt"
}
}
func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string) ([]*uiBugGroup, int, error) {
var bugs []*Bug
_, err := db.NewQuery("Bug").
Filter("Namespace=", ns).
GetAll(c, &bugs)
if err != nil {
return nil, 0, err
}
state, err := loadReportingState(c)
if err != nil {
return nil, 0, err
}
managers, err := managerList(c, ns)
if err != nil {
return nil, 0, err
}
fixedCount := 0
groups := make(map[int][]*uiBug)
bugMap := make(map[string]*uiBug)
var dups []*Bug
for _, bug := range bugs {
if bug.Status == BugStatusFixed {
fixedCount++
continue
}
if bug.Status == BugStatusInvalid {
continue
}
if accessLevel < bug.sanitizeAccess(accessLevel) {
continue
}
if bug.Status == BugStatusDup {
dups = append(dups, bug)
continue
}
uiBug := createUIBug(c, bug, state, managers)
bugMap[bug.keyHash()] = uiBug
id := uiBug.ReportingIndex
if len(uiBug.Commits) != 0 {
id = -1
}
groups[id] = append(groups[id], uiBug)
}
for _, dup := range dups {
bug := bugMap[dup.DupOf]
if bug == nil {
continue // this can be an invalid bug which we filtered above
}
mergeUIBug(c, bug, dup)
}
cfg := config.Namespaces[ns]
var uiGroups []*uiBugGroup
for index, bugs := range groups {
sort.Slice(bugs, func(i, j int) bool {
if bugs[i].Namespace != bugs[j].Namespace {
return bugs[i].Namespace < bugs[j].Namespace
}
if bugs[i].ClosedTime != bugs[j].ClosedTime {
return bugs[i].ClosedTime.After(bugs[j].ClosedTime)
}
return bugs[i].ReportedTime.After(bugs[j].ReportedTime)
})
caption, fragment, showPatched := "", "", false
switch index {
case -1:
caption, showPatched = "fix pending", true
fragment = "pending"
case len(cfg.Reporting) - 1:
caption, showPatched = "open", false
fragment = "open"
default:
reporting := &cfg.Reporting[index]
caption, showPatched = reporting.DisplayTitle, false
fragment = reporting.Name
}
uiGroups = append(uiGroups, &uiBugGroup{
Now: timeNow(c),
Caption: caption,
Fragment: fragment,
Namespace: ns,
ShowPatched: showPatched,
ShowIndex: index,
Bugs: bugs,
})
}
sort.Slice(uiGroups, func(i, j int) bool {
return uiGroups[i].ShowIndex > uiGroups[j].ShowIndex
})
return uiGroups, fixedCount, nil
}
func fetchFixedBugs(c context.Context, accessLevel AccessLevel, ns string) (*uiBugGroup, error) {
var bugs []*Bug
_, err := db.NewQuery("Bug").
Filter("Namespace=", ns).
Filter("Status=", BugStatusFixed).
GetAll(c, &bugs)
if err != nil {
return nil, err
}
state, err := loadReportingState(c)
if err != nil {
return nil, err
}
managers, err := managerList(c, ns)
if err != nil {
return nil, err
}
res := &uiBugGroup{
Now: timeNow(c),
Caption: "fixed",
ShowPatch: true,
Namespace: ns,
}
for _, bug := range bugs {
if accessLevel < bug.sanitizeAccess(accessLevel) {
continue
}
res.Bugs = append(res.Bugs, createUIBug(c, bug, state, managers))
}
sort.Slice(res.Bugs, func(i, j int) bool {
return res.Bugs[i].ClosedTime.After(res.Bugs[j].ClosedTime)
})
return res, nil
}
func loadDupsForBug(c context.Context, r *http.Request, bug *Bug, state *ReportingState, managers []string) (
*uiBugGroup, error) {
bugHash := bug.keyHash()
var dups []*Bug
_, err := db.NewQuery("Bug").
Filter("Status=", BugStatusDup).
Filter("DupOf=", bugHash).
GetAll(c, &dups)
if err != nil {
return nil, err
}
var results []*uiBug
accessLevel := accessLevel(c, r)
for _, dup := range dups {
if accessLevel < dup.sanitizeAccess(accessLevel) {
continue
}
results = append(results, createUIBug(c, dup, state, managers))
}
group := &uiBugGroup{
Now: timeNow(c),
Caption: "duplicates",
ShowPatched: true,
ShowStatus: true,
Bugs: results,
}
return group, nil
}
func loadSimilarBugs(c context.Context, r *http.Request, bug *Bug, state *ReportingState) (*uiBugGroup, error) {
var similar []*Bug
_, err := db.NewQuery("Bug").
Filter("Title=", bug.Title).
GetAll(c, &similar)
if err != nil {
return nil, err
}
managers := make(map[string][]string)
var results []*uiBug
accessLevel := accessLevel(c, r)
domain := config.Namespaces[bug.Namespace].SimilarityDomain
for _, similar := range similar {
if accessLevel < similar.sanitizeAccess(accessLevel) {
continue
}
if similar.Namespace == bug.Namespace && similar.Seq == bug.Seq {
continue
}
if config.Namespaces[similar.Namespace].SimilarityDomain != domain {
continue
}
if managers[similar.Namespace] == nil {
mgrs, err := managerList(c, similar.Namespace)
if err != nil {
return nil, err
}
managers[similar.Namespace] = mgrs
}
results = append(results, createUIBug(c, similar, state, managers[similar.Namespace]))
}
group := &uiBugGroup{
Now: timeNow(c),
Caption: "similar bugs",
ShowNamespace: true,
ShowPatched: true,
ShowStatus: true,
Bugs: results,
}
return group, nil
}
func createUIBug(c context.Context, bug *Bug, state *ReportingState, managers []string) *uiBug {
reportingIdx, status, link := 0, "", ""
var reported time.Time
var err error
if bug.Status == BugStatusOpen {
_, _, _, _, reportingIdx, status, link, err = needReport(c, "", state, bug)
reported = bug.Reporting[reportingIdx].Reported
if err != nil {
status = err.Error()
}
if status == "" {
status = "???"
}
} else {
for i := range bug.Reporting {
bugReporting := &bug.Reporting[i]
if i == len(bug.Reporting)-1 ||
bug.Status == BugStatusInvalid && !bugReporting.Closed.IsZero() &&
bug.Reporting[i+1].Closed.IsZero() ||
(bug.Status == BugStatusFixed || bug.Status == BugStatusDup) &&
bugReporting.Closed.IsZero() {
reportingIdx = i
reported = bugReporting.Reported
link = bugReporting.Link
switch bug.Status {
case BugStatusInvalid:
status = "closed as invalid"
if bugReporting.Auto {
status = "auto-" + status
}
case BugStatusFixed:
status = "fixed"
case BugStatusDup:
status = "closed as dup"
default:
status = fmt.Sprintf("unknown (%v)", bug.Status)
}
status = fmt.Sprintf("%v on %v", status, html.FormatTime(bug.Closed))
break
}
}
}
creditEmail, err := email.AddAddrContext(ownEmail(c), bug.Reporting[reportingIdx].ID)
if err != nil {
log.Errorf(c, "failed to generate credit email: %v", err)
}
id := bug.keyHash()
uiBug := &uiBug{
Namespace: bug.Namespace,
Title: bug.displayTitle(),
BisectCause: bug.BisectCause > BisectPending,
NumCrashes: bug.NumCrashes,
FirstTime: bug.FirstTime,
LastTime: bug.LastTime,
ReportedTime: reported,
ClosedTime: bug.Closed,
ReproLevel: bug.ReproLevel,
ReportingIndex: reportingIdx,
Status: status,
Link: bugLink(id),
ExternalLink: link,
CreditEmail: creditEmail,
NumManagers: len(managers),
}
updateBugBadness(c, uiBug)
if len(bug.Commits) != 0 {
for i, com := range bug.Commits {
cfg := config.Namespaces[bug.Namespace]
info := bug.getCommitInfo(i)
uiBug.Commits = append(uiBug.Commits, &uiCommit{
Hash: info.Hash,
Title: com,
Link: vcs.CommitLink(cfg.Repos[0].URL, info.Hash),
})
}
for _, mgr := range managers {
found := false
for _, mgr1 := range bug.PatchedOn {
if mgr == mgr1 {
found = true
break
}
}
if found {
uiBug.PatchedOn = append(uiBug.PatchedOn, mgr)
} else {
uiBug.MissingOn = append(uiBug.MissingOn, mgr)
}
}
sort.Strings(uiBug.PatchedOn)
sort.Strings(uiBug.MissingOn)
}
return uiBug
}
func mergeUIBug(c context.Context, bug *uiBug, dup *Bug) {
bug.NumCrashes += dup.NumCrashes
bug.BisectCause = bug.BisectCause || dup.BisectCause > BisectPending
if bug.LastTime.Before(dup.LastTime) {
bug.LastTime = dup.LastTime
}
if bug.ReproLevel < dup.ReproLevel {
bug.ReproLevel = dup.ReproLevel
}
updateBugBadness(c, bug)
}
func updateBugBadness(c context.Context, bug *uiBug) {
bug.NumCrashesBad = bug.NumCrashes >= 10000 && timeNow(c).Sub(bug.LastTime) < 24*time.Hour
}
func loadCrashesForBug(c context.Context, bug *Bug) ([]*uiCrash, []byte, error) {
bugKey := bug.key(c)
// We can have more than maxCrashes crashes, if we have lots of reproducers.
crashes, _, err := queryCrashesForBug(c, bugKey, 2*maxCrashes+200)
if err != nil || len(crashes) == 0 {
return nil, nil, err
}
builds := make(map[string]*Build)
var results []*uiCrash
for _, crash := range crashes {
build := builds[crash.BuildID]
if build == nil {
build, err = loadBuild(c, bug.Namespace, crash.BuildID)
if err != nil {
return nil, nil, err
}
builds[crash.BuildID] = build
}
results = append(results, makeUICrash(crash, build))
}
sampleReport, _, err := getText(c, textCrashReport, crashes[0].Report)
if err != nil {
return nil, nil, err
}
return results, sampleReport, nil
}
func makeUICrash(crash *Crash, build *Build) *uiCrash {
ui := &uiCrash{
Manager: crash.Manager,
Time: crash.Time,
Maintainers: strings.Join(crash.Maintainers, ", "),
LogLink: textLink(textCrashLog, crash.Log),
ReportLink: textLink(textCrashReport, crash.Report),
ReproSyzLink: textLink(textReproSyz, crash.ReproSyz),
ReproCLink: textLink(textReproC, crash.ReproC),
}
if build != nil {
ui.uiBuild = makeUIBuild(build)
}
return ui
}
func makeUIBuild(build *Build) *uiBuild {
return &uiBuild{
Time: build.Time,
SyzkallerCommit: build.SyzkallerCommit,
SyzkallerCommitLink: vcs.LogLink(vcs.SyzkallerRepo, build.SyzkallerCommit),
SyzkallerCommitDate: build.SyzkallerCommitDate,
KernelAlias: kernelRepoInfo(build).Alias,
KernelCommit: build.KernelCommit,
KernelCommitLink: vcs.LogLink(build.KernelRepo, build.KernelCommit),
KernelCommitTitle: build.KernelCommitTitle,
KernelCommitDate: build.KernelCommitDate,
KernelConfigLink: textLink(textKernelConfig, build.KernelConfig),
}
}
func loadManagers(c context.Context, accessLevel AccessLevel, ns string) ([]*uiManager, error) {
now := timeNow(c)
date := timeDate(now)
managers, managerKeys, err := loadAllManagers(c, ns)
if err != nil {
return nil, err
}
for i := 0; i < len(managers); i++ {
if accessLevel >= config.Namespaces[managers[i].Namespace].AccessLevel {
continue
}
last := len(managers) - 1
managers[i] = managers[last]
managers = managers[:last]
managerKeys[i] = managerKeys[last]
managerKeys = managerKeys[:last]
i--
}
var buildKeys []*db.Key
var statsKeys []*db.Key
for i, mgr := range managers {
if mgr.CurrentBuild != "" {
buildKeys = append(buildKeys, buildKey(c, mgr.Namespace, mgr.CurrentBuild))
}
if timeDate(mgr.LastAlive) == date {
statsKeys = append(statsKeys,
db.NewKey(c, "ManagerStats", "", int64(date), managerKeys[i]))
}
}
builds := make([]*Build, len(buildKeys))
if err := db.GetMulti(c, buildKeys, builds); err != nil {
return nil, err
}
uiBuilds := make(map[string]*uiBuild)
for _, build := range builds {
uiBuilds[build.Namespace+"|"+build.ID] = makeUIBuild(build)
}
stats := make([]*ManagerStats, len(statsKeys))
if err := db.GetMulti(c, statsKeys, stats); err != nil {
return nil, err
}
var fullStats []*ManagerStats
for _, mgr := range managers {
if timeDate(mgr.LastAlive) != date {
fullStats = append(fullStats, &ManagerStats{})
continue
}
fullStats = append(fullStats, stats[0])
stats = stats[1:]
}
var results []*uiManager
for i, mgr := range managers {
stats := fullStats[i]
link := mgr.Link
if accessLevel < AccessUser {
link = ""
}
results = append(results, &uiManager{
Now: timeNow(c),
Namespace: mgr.Namespace,
Name: mgr.Name,
Link: link,
CoverLink: config.CoverPath + mgr.Name + ".html",
CurrentBuild: uiBuilds[mgr.Namespace+"|"+mgr.CurrentBuild],
FailedBuildBugLink: bugLink(mgr.FailedBuildBug),
FailedSyzBuildBugLink: bugLink(mgr.FailedSyzBuildBug),
LastActive: mgr.LastAlive,
LastActiveBad: now.Sub(mgr.LastAlive) > 6*time.Hour,
CurrentUpTime: mgr.CurrentUpTime,
MaxCorpus: stats.MaxCorpus,
MaxCover: stats.MaxCover,
TotalFuzzingTime: stats.TotalFuzzingTime,
TotalCrashes: stats.TotalCrashes,
TotalExecs: stats.TotalExecs,
})
}
sort.Slice(results, func(i, j int) bool {
if results[i].Namespace != results[j].Namespace {
return results[i].Namespace < results[j].Namespace
}
return results[i].Name < results[j].Name
})
return results, nil
}
func loadRecentJobs(c context.Context) ([]*uiJob, error) {
var jobs []*Job
keys, err := db.NewQuery("Job").
Order("-Created").
Limit(80).
GetAll(c, &jobs)
if err != nil {
return nil, err
}
var results []*uiJob
for i, job := range jobs {
results = append(results, makeUIJob(job, keys[i], nil, nil))
}
return results, nil
}
func makeUIJob(job *Job, jobKey *db.Key, crash *Crash, build *Build) *uiJob {
ui := &uiJob{
Type: job.Type,
Created: job.Created,
BugLink: bugLink(jobKey.Parent().StringID()),
ExternalLink: job.Link,
User: job.User,
Reporting: job.Reporting,
Namespace: job.Namespace,
Manager: job.Manager,
BugTitle: job.BugTitle,
KernelAlias: kernelRepoInfoRaw(job.Namespace, job.KernelRepo, job.KernelBranch).Alias,
PatchLink: textLink(textPatch, job.Patch),
Attempts: job.Attempts,
Started: job.Started,
Finished: job.Finished,
CrashTitle: job.CrashTitle,
CrashLogLink: textLink(textCrashLog, job.CrashLog),
CrashReportLink: textLink(textCrashReport, job.CrashReport),
LogLink: textLink(textLog, job.Log),
ErrorLink: textLink(textError, job.Error),
}
if !job.Finished.IsZero() {
ui.Duration = job.Finished.Sub(job.Started)
}
for _, com := range job.Commits {
ui.Commits = append(ui.Commits, &uiCommit{
Hash: com.Hash,
Title: com.Title,
Author: fmt.Sprintf("%v <%v>", com.AuthorName, com.Author),
CC: strings.Split(com.CC, "|"),
Date: com.Date,
})
}
if len(ui.Commits) == 1 {
ui.Commit = ui.Commits[0]
ui.Commits = nil
}
if crash != nil {
ui.Crash = makeUICrash(crash, build)
}
return ui
}
func fetchErrorLogs(c context.Context) ([]byte, error) {
const (
minLogLevel = 3
maxLines = 100
maxLineLen = 1000
reportPeriod = 7 * 24 * time.Hour
)
q := &log.Query{
StartTime: time.Now().Add(-reportPeriod),
AppLogs: true,
ApplyMinLevel: true,
MinLevel: minLogLevel,
}
result := q.Run(c)
var lines []string
for i := 0; i < maxLines; i++ {
rec, err := result.Next()
if rec == nil {
break
}
if err != nil {
entry := fmt.Sprintf("ERROR FETCHING LOGS: %v\n", err)
lines = append(lines, entry)
break
}
for _, al := range rec.AppLogs {
if al.Level < minLogLevel {
continue
}
text := strings.Replace(al.Message, "\n", " ", -1)
text = strings.Replace(text, "\r", "", -1)
if len(text) > maxLineLen {
text = text[:maxLineLen]
}
res := ""
if !strings.Contains(rec.Resource, "method=log_error") {
res = fmt.Sprintf(" (%v)", rec.Resource)
}
entry := fmt.Sprintf("%v: %v%v\n", al.Time.Format("Jan 02 15:04"), text, res)
lines = append(lines, entry)
}
}
buf := new(bytes.Buffer)
for i := len(lines) - 1; i >= 0; i-- {
buf.WriteString(lines[i])
}
return buf.Bytes(), nil
}
func bugLink(id string) string {
if id == "" {
return ""
}
return "/bug?id=" + id
}