1// Copyright 2017 syzkaller project authors. All rights reserved. 2// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4package dash 5 6import ( 7 "bytes" 8 "fmt" 9 "net/http" 10 "sort" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/google/syzkaller/dashboard/dashapi" 16 "github.com/google/syzkaller/pkg/email" 17 "golang.org/x/net/context" 18 "google.golang.org/appengine/datastore" 19 "google.golang.org/appengine/log" 20) 21 22// This file contains web UI http handlers. 23 24func initHTTPHandlers() { 25 http.Handle("/", handlerWrapper(handleMain)) 26 http.Handle("/bug", handlerWrapper(handleBug)) 27 http.Handle("/text", handlerWrapper(handleText)) 28 http.Handle("/x/.config", handlerWrapper(handleTextX(textKernelConfig))) 29 http.Handle("/x/log.txt", handlerWrapper(handleTextX(textCrashLog))) 30 http.Handle("/x/repro.syz", handlerWrapper(handleTextX(textReproSyz))) 31 http.Handle("/x/repro.c", handlerWrapper(handleTextX(textReproC))) 32 http.Handle("/x/patch.diff", handlerWrapper(handleTextX(textPatch))) 33 http.Handle("/x/error.txt", handlerWrapper(handleTextX(textError))) 34} 35 36type uiMain struct { 37 Header *uiHeader 38 Now time.Time 39 Log []byte 40 Managers []*uiManager 41 Jobs []*uiJob 42 BugNamespaces []*uiBugNamespace 43} 44 45type uiManager struct { 46 Namespace string 47 Name string 48 Link string 49 CurrentBuild *uiBuild 50 FailedBuildBugLink string 51 LastActive time.Time 52 LastActiveBad bool 53 CurrentUpTime time.Duration 54 MaxCorpus int64 55 MaxCover int64 56 TotalFuzzingTime time.Duration 57 TotalCrashes int64 58 TotalExecs int64 59} 60 61type uiBuild struct { 62 Time time.Time 63 SyzkallerCommit string 64 KernelAlias string 65 KernelCommit string 66 KernelConfigLink string 67} 68 69type uiBugPage struct { 70 Header *uiHeader 71 Now time.Time 72 Bug *uiBug 73 DupOf *uiBugGroup 74 Dups *uiBugGroup 75 Similar *uiBugGroup 76 SampleReport []byte 77 Crashes []*uiCrash 78} 79 80type uiBugNamespace struct { 81 Name string 82 Caption string 83 CoverLink string 84 FixedLink string 85 FixedCount int 86 Groups []*uiBugGroup 87} 88 89type uiBugGroup struct { 90 Now time.Time 91 Caption string 92 Fragment string 93 Namespace string 94 ShowNamespace bool 95 ShowPatch bool 96 ShowPatched bool 97 ShowStatus bool 98 ShowIndex int 99 Bugs []*uiBug 100} 101 102type uiBug struct { 103 Namespace string 104 Title string 105 NumCrashes int64 106 NumCrashesBad bool 107 FirstTime time.Time 108 LastTime time.Time 109 ReportedTime time.Time 110 ClosedTime time.Time 111 ReproLevel dashapi.ReproLevel 112 ReportingIndex int 113 Status string 114 Link string 115 ExternalLink string 116 CreditEmail string 117 Commits string 118 PatchedOn []string 119 MissingOn []string 120 NumManagers int 121} 122 123type uiCrash struct { 124 Manager string 125 Time time.Time 126 Maintainers string 127 LogLink string 128 ReportLink string 129 ReproSyzLink string 130 ReproCLink string 131 *uiBuild 132} 133 134type uiJob struct { 135 Created time.Time 136 BugLink string 137 ExternalLink string 138 User string 139 Reporting string 140 Namespace string 141 Manager string 142 BugTitle string 143 BugID string 144 KernelAlias string 145 KernelCommit string 146 PatchLink string 147 Attempts int 148 Started time.Time 149 Finished time.Time 150 CrashTitle string 151 CrashLogLink string 152 CrashReportLink string 153 ErrorLink string 154 Reported bool 155} 156 157// handleMain serves main page. 158func handleMain(c context.Context, w http.ResponseWriter, r *http.Request) error { 159 var errorLog []byte 160 var managers []*uiManager 161 var jobs []*uiJob 162 if accessLevel(c, r) == AccessAdmin && r.FormValue("fixed") == "" { 163 var err error 164 errorLog, err = fetchErrorLogs(c) 165 if err != nil { 166 return err 167 } 168 managers, err = loadManagers(c) 169 if err != nil { 170 return err 171 } 172 jobs, err = loadRecentJobs(c) 173 if err != nil { 174 return err 175 } 176 } 177 bugNamespaces, err := fetchBugs(c, r) 178 if err != nil { 179 return err 180 } 181 data := &uiMain{ 182 Header: commonHeader(c, r), 183 Now: timeNow(c), 184 Log: errorLog, 185 Managers: managers, 186 Jobs: jobs, 187 BugNamespaces: bugNamespaces, 188 } 189 return serveTemplate(w, "main.html", data) 190} 191 192// handleBug serves page about a single bug (which is passed in id argument). 193func handleBug(c context.Context, w http.ResponseWriter, r *http.Request) error { 194 bug := new(Bug) 195 if id := r.FormValue("id"); id != "" { 196 bugKey := datastore.NewKey(c, "Bug", id, 0, nil) 197 if err := datastore.Get(c, bugKey, bug); err != nil { 198 return err 199 } 200 } else if extID := r.FormValue("extid"); extID != "" { 201 var err error 202 bug, _, err = findBugByReportingID(c, extID) 203 if err != nil { 204 return err 205 } 206 } else { 207 return ErrDontLog(fmt.Errorf("mandatory parameter id/extid is missing")) 208 } 209 accessLevel := accessLevel(c, r) 210 if err := checkAccessLevel(c, r, bug.sanitizeAccess(accessLevel)); err != nil { 211 return err 212 } 213 state, err := loadReportingState(c) 214 if err != nil { 215 return err 216 } 217 managers, err := managerList(c, bug.Namespace) 218 if err != nil { 219 return err 220 } 221 var dupOf *uiBugGroup 222 if bug.DupOf != "" { 223 dup := new(Bug) 224 if err := datastore.Get(c, datastore.NewKey(c, "Bug", bug.DupOf, 0, nil), dup); err != nil { 225 return err 226 } 227 if accessLevel >= dup.sanitizeAccess(accessLevel) { 228 dupOf = &uiBugGroup{ 229 Now: timeNow(c), 230 Caption: "Duplicate of", 231 Bugs: []*uiBug{createUIBug(c, dup, state, managers)}, 232 } 233 } 234 } 235 uiBug := createUIBug(c, bug, state, managers) 236 crashes, sampleReport, err := loadCrashesForBug(c, bug) 237 if err != nil { 238 return err 239 } 240 dups, err := loadDupsForBug(c, r, bug, state, managers) 241 if err != nil { 242 return err 243 } 244 similar, err := loadSimilarBugs(c, r, bug, state) 245 if err != nil { 246 return err 247 } 248 data := &uiBugPage{ 249 Header: commonHeader(c, r), 250 Now: timeNow(c), 251 Bug: uiBug, 252 DupOf: dupOf, 253 Dups: dups, 254 Similar: similar, 255 SampleReport: sampleReport, 256 Crashes: crashes, 257 } 258 return serveTemplate(w, "bug.html", data) 259} 260 261// handleText serves plain text blobs (crash logs, reports, reproducers, etc). 262func handleTextImpl(c context.Context, w http.ResponseWriter, r *http.Request, tag string) error { 263 var id int64 264 if x := r.FormValue("x"); x != "" { 265 xid, err := strconv.ParseUint(x, 16, 64) 266 if err != nil || xid == 0 { 267 return ErrDontLog(fmt.Errorf("failed to parse text id: %v", err)) 268 } 269 id = int64(xid) 270 } else { 271 // Old link support, don't remove. 272 xid, err := strconv.ParseInt(r.FormValue("id"), 10, 64) 273 if err != nil || xid == 0 { 274 return ErrDontLog(fmt.Errorf("failed to parse text id: %v", err)) 275 } 276 id = xid 277 } 278 crash, err := checkTextAccess(c, r, tag, id) 279 if err != nil { 280 return err 281 } 282 data, ns, err := getText(c, tag, id) 283 if err != nil { 284 return err 285 } 286 if err := checkAccessLevel(c, r, config.Namespaces[ns].AccessLevel); err != nil { 287 return err 288 } 289 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 290 // Unfortunately filename does not work in chrome on linux due to: 291 // https://bugs.chromium.org/p/chromium/issues/detail?id=608342 292 w.Header().Set("Content-Disposition", "inline; filename="+textFilename(tag)) 293 if tag == textReproSyz { 294 // Add link to documentation and repro opts for syzkaller reproducers. 295 w.Write([]byte(syzReproPrefix)) 296 if crash != nil { 297 fmt.Fprintf(w, "#%s\n", crash.ReproOpts) 298 } 299 } 300 w.Write(data) 301 return nil 302} 303 304func handleText(c context.Context, w http.ResponseWriter, r *http.Request) error { 305 return handleTextImpl(c, w, r, r.FormValue("tag")) 306} 307 308func handleTextX(tag string) contextHandler { 309 return func(c context.Context, w http.ResponseWriter, r *http.Request) error { 310 return handleTextImpl(c, w, r, tag) 311 } 312} 313 314func textFilename(tag string) string { 315 switch tag { 316 case textKernelConfig: 317 return ".config" 318 case textCrashLog: 319 return "log.txt" 320 case textCrashReport: 321 return "report.txt" 322 case textReproSyz: 323 return "repro.syz" 324 case textReproC: 325 return "repro.c" 326 case textPatch: 327 return "patch.diff" 328 case textError: 329 return "error.txt" 330 default: 331 return "text.txt" 332 } 333} 334 335func fetchBugs(c context.Context, r *http.Request) ([]*uiBugNamespace, error) { 336 state, err := loadReportingState(c) 337 if err != nil { 338 return nil, err 339 } 340 accessLevel := accessLevel(c, r) 341 onlyFixed := r.FormValue("fixed") 342 var res []*uiBugNamespace 343 for ns, cfg := range config.Namespaces { 344 if accessLevel < cfg.AccessLevel { 345 continue 346 } 347 if onlyFixed != "" && onlyFixed != ns { 348 continue 349 } 350 uiNamespace, err := fetchNamespaceBugs(c, accessLevel, ns, state, onlyFixed != "") 351 if err != nil { 352 return nil, err 353 } 354 res = append(res, uiNamespace) 355 } 356 sort.Sort(uiBugNamespaceSorter(res)) 357 return res, nil 358} 359 360func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string, 361 state *ReportingState, onlyFixed bool) (*uiBugNamespace, error) { 362 query := datastore.NewQuery("Bug").Filter("Namespace=", ns) 363 if onlyFixed { 364 query = query.Filter("Status=", BugStatusFixed) 365 } 366 var bugs []*Bug 367 _, err := query.GetAll(c, &bugs) 368 if err != nil { 369 return nil, err 370 } 371 managers, err := managerList(c, ns) 372 if err != nil { 373 return nil, err 374 } 375 fixedCount := 0 376 groups := make(map[int][]*uiBug) 377 bugMap := make(map[string]*uiBug) 378 var dups []*Bug 379 for _, bug := range bugs { 380 if bug.Status == BugStatusFixed { 381 fixedCount++ 382 } 383 if bug.Status == BugStatusInvalid || bug.Status == BugStatusFixed != onlyFixed { 384 continue 385 } 386 if accessLevel < bug.sanitizeAccess(accessLevel) { 387 continue 388 } 389 if bug.Status == BugStatusDup { 390 dups = append(dups, bug) 391 continue 392 } 393 uiBug := createUIBug(c, bug, state, managers) 394 bugMap[bugKeyHash(bug.Namespace, bug.Title, bug.Seq)] = uiBug 395 id := uiBug.ReportingIndex 396 if bug.Status == BugStatusFixed { 397 id = -1 398 } else if uiBug.Commits != "" { 399 id = -2 400 } 401 groups[id] = append(groups[id], uiBug) 402 } 403 for _, dup := range dups { 404 bug := bugMap[dup.DupOf] 405 if bug == nil { 406 continue // this can be an invalid bug which we filtered above 407 } 408 mergeUIBug(c, bug, dup) 409 } 410 var uiGroups []*uiBugGroup 411 for index, bugs := range groups { 412 sort.Sort(uiBugSorter(bugs)) 413 caption, fragment, showPatch, showPatched := "", "", false, false 414 switch index { 415 case -1: 416 caption, showPatch, showPatched = "fixed", true, false 417 case -2: 418 caption, showPatch, showPatched = "fix pending", false, true 419 fragment = ns + "-pending" 420 case len(config.Namespaces[ns].Reporting) - 1: 421 caption, showPatch, showPatched = "open", false, false 422 fragment = ns + "-open" 423 default: 424 reporting := &config.Namespaces[ns].Reporting[index] 425 caption, showPatch, showPatched = reporting.DisplayTitle, false, false 426 fragment = ns + "-" + reporting.Name 427 } 428 uiGroups = append(uiGroups, &uiBugGroup{ 429 Now: timeNow(c), 430 Caption: fmt.Sprintf("%v (%v)", caption, len(bugs)), 431 Fragment: fragment, 432 Namespace: ns, 433 ShowPatch: showPatch, 434 ShowPatched: showPatched, 435 ShowIndex: index, 436 Bugs: bugs, 437 }) 438 } 439 sort.Sort(uiBugGroupSorter(uiGroups)) 440 fixedLink := "" 441 if !onlyFixed { 442 fixedLink = fmt.Sprintf("?fixed=%v", ns) 443 } 444 cfg := config.Namespaces[ns] 445 uiNamespace := &uiBugNamespace{ 446 Name: ns, 447 Caption: cfg.DisplayTitle, 448 CoverLink: cfg.CoverLink, 449 FixedCount: fixedCount, 450 FixedLink: fixedLink, 451 Groups: uiGroups, 452 } 453 return uiNamespace, nil 454} 455 456func loadDupsForBug(c context.Context, r *http.Request, bug *Bug, state *ReportingState, managers []string) ( 457 *uiBugGroup, error) { 458 bugHash := bugKeyHash(bug.Namespace, bug.Title, bug.Seq) 459 var dups []*Bug 460 _, err := datastore.NewQuery("Bug"). 461 Filter("Status=", BugStatusDup). 462 Filter("DupOf=", bugHash). 463 GetAll(c, &dups) 464 if err != nil { 465 return nil, err 466 } 467 var results []*uiBug 468 accessLevel := accessLevel(c, r) 469 for _, dup := range dups { 470 if accessLevel < dup.sanitizeAccess(accessLevel) { 471 continue 472 } 473 results = append(results, createUIBug(c, dup, state, managers)) 474 } 475 group := &uiBugGroup{ 476 Now: timeNow(c), 477 Caption: "duplicates", 478 ShowPatched: true, 479 ShowStatus: true, 480 Bugs: results, 481 } 482 return group, nil 483} 484 485func loadSimilarBugs(c context.Context, r *http.Request, bug *Bug, state *ReportingState) (*uiBugGroup, error) { 486 var similar []*Bug 487 _, err := datastore.NewQuery("Bug"). 488 Filter("Title=", bug.Title). 489 GetAll(c, &similar) 490 if err != nil { 491 return nil, err 492 } 493 managers := make(map[string][]string) 494 var results []*uiBug 495 accessLevel := accessLevel(c, r) 496 for _, similar := range similar { 497 if accessLevel < similar.sanitizeAccess(accessLevel) { 498 continue 499 } 500 if similar.Namespace == bug.Namespace && similar.Seq == bug.Seq { 501 continue 502 } 503 if managers[similar.Namespace] == nil { 504 mgrs, err := managerList(c, similar.Namespace) 505 if err != nil { 506 return nil, err 507 } 508 managers[similar.Namespace] = mgrs 509 } 510 results = append(results, createUIBug(c, similar, state, managers[similar.Namespace])) 511 } 512 group := &uiBugGroup{ 513 Now: timeNow(c), 514 Caption: "similar bugs", 515 ShowNamespace: true, 516 ShowPatched: true, 517 ShowStatus: true, 518 Bugs: results, 519 } 520 return group, nil 521} 522 523func createUIBug(c context.Context, bug *Bug, state *ReportingState, managers []string) *uiBug { 524 reportingIdx, status, link := 0, "", "" 525 var reported time.Time 526 var err error 527 if bug.Status == BugStatusOpen { 528 _, _, _, _, reportingIdx, status, link, err = needReport(c, "", state, bug) 529 reported = bug.Reporting[reportingIdx].Reported 530 if err != nil { 531 status = err.Error() 532 } 533 if status == "" { 534 status = "???" 535 } 536 } else { 537 for i := range bug.Reporting { 538 bugReporting := &bug.Reporting[i] 539 if i == len(bug.Reporting)-1 || 540 bug.Status == BugStatusInvalid && !bugReporting.Closed.IsZero() && 541 bug.Reporting[i+1].Closed.IsZero() || 542 (bug.Status == BugStatusFixed || bug.Status == BugStatusDup) && 543 bugReporting.Closed.IsZero() { 544 reportingIdx = i 545 reported = bugReporting.Reported 546 link = bugReporting.Link 547 switch bug.Status { 548 case BugStatusInvalid: 549 status = "closed as invalid" 550 case BugStatusFixed: 551 status = "fixed" 552 case BugStatusDup: 553 status = "closed as dup" 554 default: 555 status = fmt.Sprintf("unknown (%v)", bug.Status) 556 } 557 status = fmt.Sprintf("%v on %v", status, formatTime(bug.Closed)) 558 break 559 } 560 } 561 } 562 creditEmail, err := email.AddAddrContext(ownEmail(c), bug.Reporting[reportingIdx].ID) 563 if err != nil { 564 log.Errorf(c, "failed to generate credit email: %v", err) 565 } 566 id := bugKeyHash(bug.Namespace, bug.Title, bug.Seq) 567 uiBug := &uiBug{ 568 Namespace: bug.Namespace, 569 Title: bug.displayTitle(), 570 NumCrashes: bug.NumCrashes, 571 FirstTime: bug.FirstTime, 572 LastTime: bug.LastTime, 573 ReportedTime: reported, 574 ClosedTime: bug.Closed, 575 ReproLevel: bug.ReproLevel, 576 ReportingIndex: reportingIdx, 577 Status: status, 578 Link: bugLink(id), 579 ExternalLink: link, 580 CreditEmail: creditEmail, 581 NumManagers: len(managers), 582 } 583 updateBugBadness(c, uiBug) 584 if len(bug.Commits) != 0 { 585 uiBug.Commits = bug.Commits[0] 586 if len(bug.Commits) > 1 { 587 uiBug.Commits = fmt.Sprintf("%q", bug.Commits) 588 } 589 for _, mgr := range managers { 590 found := false 591 for _, mgr1 := range bug.PatchedOn { 592 if mgr == mgr1 { 593 found = true 594 break 595 } 596 } 597 if found { 598 uiBug.PatchedOn = append(uiBug.PatchedOn, mgr) 599 } else { 600 uiBug.MissingOn = append(uiBug.MissingOn, mgr) 601 } 602 } 603 sort.Strings(uiBug.PatchedOn) 604 sort.Strings(uiBug.MissingOn) 605 } 606 return uiBug 607} 608 609func mergeUIBug(c context.Context, bug *uiBug, dup *Bug) { 610 bug.NumCrashes += dup.NumCrashes 611 if bug.LastTime.Before(dup.LastTime) { 612 bug.LastTime = dup.LastTime 613 } 614 if bug.ReproLevel < dup.ReproLevel { 615 bug.ReproLevel = dup.ReproLevel 616 } 617 updateBugBadness(c, bug) 618} 619 620func updateBugBadness(c context.Context, bug *uiBug) { 621 bug.NumCrashesBad = bug.NumCrashes >= 10000 && timeNow(c).Sub(bug.LastTime) < 24*time.Hour 622} 623 624func loadCrashesForBug(c context.Context, bug *Bug) ([]*uiCrash, []byte, error) { 625 bugHash := bugKeyHash(bug.Namespace, bug.Title, bug.Seq) 626 bugKey := datastore.NewKey(c, "Bug", bugHash, 0, nil) 627 // We can have more than maxCrashes crashes, if we have lots of reproducers. 628 crashes, _, err := queryCrashesForBug(c, bugKey, maxCrashes+200) 629 if err != nil || len(crashes) == 0 { 630 return nil, nil, err 631 } 632 builds := make(map[string]*Build) 633 var results []*uiCrash 634 for _, crash := range crashes { 635 build := builds[crash.BuildID] 636 if build == nil { 637 build, err = loadBuild(c, bug.Namespace, crash.BuildID) 638 if err != nil { 639 return nil, nil, err 640 } 641 builds[crash.BuildID] = build 642 } 643 ui := &uiCrash{ 644 Manager: crash.Manager, 645 Time: crash.Time, 646 Maintainers: fmt.Sprintf("%q", crash.Maintainers), 647 LogLink: textLink(textCrashLog, crash.Log), 648 ReportLink: textLink(textCrashReport, crash.Report), 649 ReproSyzLink: textLink(textReproSyz, crash.ReproSyz), 650 ReproCLink: textLink(textReproC, crash.ReproC), 651 uiBuild: makeUIBuild(build), 652 } 653 results = append(results, ui) 654 } 655 sampleReport, _, err := getText(c, textCrashReport, crashes[0].Report) 656 if err != nil { 657 return nil, nil, err 658 } 659 return results, sampleReport, nil 660} 661 662func makeUIBuild(build *Build) *uiBuild { 663 return &uiBuild{ 664 Time: build.Time, 665 SyzkallerCommit: build.SyzkallerCommit, 666 KernelAlias: kernelRepoInfo(build).Alias, 667 KernelCommit: build.KernelCommit, 668 KernelConfigLink: textLink(textKernelConfig, build.KernelConfig), 669 } 670} 671 672func loadManagers(c context.Context) ([]*uiManager, error) { 673 now := timeNow(c) 674 date := timeDate(now) 675 managers, managerKeys, err := loadAllManagers(c) 676 if err != nil { 677 return nil, err 678 } 679 var buildKeys []*datastore.Key 680 var statsKeys []*datastore.Key 681 for i, mgr := range managers { 682 if mgr.CurrentBuild != "" { 683 buildKeys = append(buildKeys, buildKey(c, mgr.Namespace, mgr.CurrentBuild)) 684 } 685 if timeDate(mgr.LastAlive) == date { 686 statsKeys = append(statsKeys, 687 datastore.NewKey(c, "ManagerStats", "", int64(date), managerKeys[i])) 688 } 689 } 690 builds := make([]*Build, len(buildKeys)) 691 if err := datastore.GetMulti(c, buildKeys, builds); err != nil { 692 return nil, err 693 } 694 uiBuilds := make(map[string]*uiBuild) 695 for _, build := range builds { 696 uiBuilds[build.Namespace+"|"+build.ID] = makeUIBuild(build) 697 } 698 stats := make([]*ManagerStats, len(statsKeys)) 699 if err := datastore.GetMulti(c, statsKeys, stats); err != nil { 700 return nil, err 701 } 702 var fullStats []*ManagerStats 703 for _, mgr := range managers { 704 if timeDate(mgr.LastAlive) != date { 705 fullStats = append(fullStats, &ManagerStats{}) 706 continue 707 } 708 fullStats = append(fullStats, stats[0]) 709 stats = stats[1:] 710 } 711 var results []*uiManager 712 for i, mgr := range managers { 713 stats := fullStats[i] 714 results = append(results, &uiManager{ 715 Namespace: mgr.Namespace, 716 Name: mgr.Name, 717 Link: mgr.Link, 718 CurrentBuild: uiBuilds[mgr.Namespace+"|"+mgr.CurrentBuild], 719 FailedBuildBugLink: bugLink(mgr.FailedBuildBug), 720 LastActive: mgr.LastAlive, 721 LastActiveBad: now.Sub(mgr.LastAlive) > 12*time.Hour, 722 CurrentUpTime: mgr.CurrentUpTime, 723 MaxCorpus: stats.MaxCorpus, 724 MaxCover: stats.MaxCover, 725 TotalFuzzingTime: stats.TotalFuzzingTime, 726 TotalCrashes: stats.TotalCrashes, 727 TotalExecs: stats.TotalExecs, 728 }) 729 } 730 sort.Sort(uiManagerSorter(results)) 731 return results, nil 732} 733 734func loadRecentJobs(c context.Context) ([]*uiJob, error) { 735 var jobs []*Job 736 keys, err := datastore.NewQuery("Job"). 737 Order("-Created"). 738 Limit(20). 739 GetAll(c, &jobs) 740 if err != nil { 741 return nil, err 742 } 743 var results []*uiJob 744 for i, job := range jobs { 745 ui := &uiJob{ 746 Created: job.Created, 747 BugLink: bugLink(keys[i].Parent().StringID()), 748 ExternalLink: job.Link, 749 User: job.User, 750 Reporting: job.Reporting, 751 Namespace: job.Namespace, 752 Manager: job.Manager, 753 BugTitle: job.BugTitle, 754 KernelAlias: kernelRepoInfoRaw(job.KernelRepo, job.KernelBranch).Alias, 755 PatchLink: textLink(textPatch, job.Patch), 756 Attempts: job.Attempts, 757 Started: job.Started, 758 Finished: job.Finished, 759 CrashTitle: job.CrashTitle, 760 CrashLogLink: textLink(textCrashLog, job.CrashLog), 761 CrashReportLink: textLink(textCrashReport, job.CrashReport), 762 ErrorLink: textLink(textError, job.Error), 763 } 764 results = append(results, ui) 765 } 766 return results, nil 767} 768 769func fetchErrorLogs(c context.Context) ([]byte, error) { 770 const ( 771 minLogLevel = 3 772 maxLines = 100 773 maxLineLen = 1000 774 reportPeriod = 7 * 24 * time.Hour 775 ) 776 q := &log.Query{ 777 StartTime: time.Now().Add(-reportPeriod), 778 AppLogs: true, 779 ApplyMinLevel: true, 780 MinLevel: minLogLevel, 781 } 782 result := q.Run(c) 783 var lines []string 784 for i := 0; i < maxLines; i++ { 785 rec, err := result.Next() 786 if rec == nil { 787 break 788 } 789 if err != nil { 790 entry := fmt.Sprintf("ERROR FETCHING LOGS: %v\n", err) 791 lines = append(lines, entry) 792 break 793 } 794 for _, al := range rec.AppLogs { 795 if al.Level < minLogLevel { 796 continue 797 } 798 text := strings.Replace(al.Message, "\n", " ", -1) 799 text = strings.Replace(text, "\r", "", -1) 800 if len(text) > maxLineLen { 801 text = text[:maxLineLen] 802 } 803 res := "" 804 if !strings.Contains(rec.Resource, "method=log_error") { 805 res = fmt.Sprintf(" (%v)", rec.Resource) 806 } 807 entry := fmt.Sprintf("%v: %v%v\n", al.Time.Format("Jan 02 15:04"), text, res) 808 lines = append(lines, entry) 809 } 810 } 811 buf := new(bytes.Buffer) 812 for i := len(lines) - 1; i >= 0; i-- { 813 buf.WriteString(lines[i]) 814 } 815 return buf.Bytes(), nil 816} 817 818func bugLink(id string) string { 819 if id == "" { 820 return "" 821 } 822 return "/bug?id=" + id 823} 824 825type uiManagerSorter []*uiManager 826 827func (a uiManagerSorter) Len() int { return len(a) } 828func (a uiManagerSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 829func (a uiManagerSorter) Less(i, j int) bool { 830 if a[i].Namespace != a[j].Namespace { 831 return a[i].Namespace < a[j].Namespace 832 } 833 return a[i].Name < a[j].Name 834} 835 836type uiBugSorter []*uiBug 837 838func (a uiBugSorter) Len() int { return len(a) } 839func (a uiBugSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 840func (a uiBugSorter) Less(i, j int) bool { 841 if a[i].Namespace != a[j].Namespace { 842 return a[i].Namespace < a[j].Namespace 843 } 844 if a[i].ClosedTime != a[j].ClosedTime { 845 return a[i].ClosedTime.After(a[j].ClosedTime) 846 } 847 return a[i].ReportedTime.After(a[j].ReportedTime) 848} 849 850type uiBugGroupSorter []*uiBugGroup 851 852func (a uiBugGroupSorter) Len() int { return len(a) } 853func (a uiBugGroupSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 854func (a uiBugGroupSorter) Less(i, j int) bool { return a[i].ShowIndex > a[j].ShowIndex } 855 856type uiBugNamespaceSorter []*uiBugNamespace 857 858func (a uiBugNamespaceSorter) Len() int { return len(a) } 859func (a uiBugNamespaceSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 860func (a uiBugNamespaceSorter) Less(i, j int) bool { return a[i].Caption < a[j].Caption } 861