github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/main.go (about) 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 4 package main 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "fmt" 11 "html/template" 12 "net/http" 13 "net/url" 14 "os" 15 "regexp" 16 "sort" 17 "strconv" 18 "strings" 19 "time" 20 21 "cloud.google.com/go/logging" 22 "cloud.google.com/go/logging/logadmin" 23 "github.com/google/syzkaller/dashboard/dashapi" 24 "github.com/google/syzkaller/pkg/debugtracer" 25 "github.com/google/syzkaller/pkg/email" 26 "github.com/google/syzkaller/pkg/hash" 27 "github.com/google/syzkaller/pkg/html" 28 "github.com/google/syzkaller/pkg/subsystem" 29 "github.com/google/syzkaller/pkg/vcs" 30 "golang.org/x/sync/errgroup" 31 "google.golang.org/api/iterator" 32 "google.golang.org/appengine/v2" 33 db "google.golang.org/appengine/v2/datastore" 34 "google.golang.org/appengine/v2/log" 35 "google.golang.org/appengine/v2/memcache" 36 "google.golang.org/appengine/v2/user" 37 proto "google.golang.org/genproto/googleapis/appengine/logging/v1" 38 ltype "google.golang.org/genproto/googleapis/logging/type" 39 ) 40 41 // This file contains web UI http handlers. 42 43 func initHTTPHandlers() { 44 http.Handle("/", handlerWrapper(handleMain)) 45 http.Handle("/bug", handlerWrapper(handleBug)) 46 http.Handle("/text", handlerWrapper(handleText)) 47 http.Handle("/admin", handlerWrapper(handleAdmin)) 48 http.Handle("/x/.config", handlerWrapper(handleTextX(textKernelConfig))) 49 http.Handle("/x/log.txt", handlerWrapper(handleTextX(textCrashLog))) 50 http.Handle("/x/report.txt", handlerWrapper(handleTextX(textCrashReport))) 51 http.Handle("/x/repro.syz", handlerWrapper(handleTextX(textReproSyz))) 52 http.Handle("/x/repro.c", handlerWrapper(handleTextX(textReproC))) 53 http.Handle("/x/repro.log", handlerWrapper(handleTextX(textReproLog))) 54 http.Handle("/x/patch.diff", handlerWrapper(handleTextX(textPatch))) 55 http.Handle("/x/bisect.txt", handlerWrapper(handleTextX(textLog))) 56 http.Handle("/x/error.txt", handlerWrapper(handleTextX(textError))) 57 http.Handle("/x/minfo.txt", handlerWrapper(handleTextX(textMachineInfo))) 58 for ns := range getConfig(context.Background()).Namespaces { 59 http.Handle("/"+ns, handlerWrapper(handleMain)) 60 http.Handle("/"+ns+"/fixed", handlerWrapper(handleFixed)) // nolint: goconst // remove it with goconst 1.7.0+ 61 http.Handle("/"+ns+"/invalid", handlerWrapper(handleInvalid)) 62 http.Handle("/"+ns+"/graph/bugs", handlerWrapper(handleKernelHealthGraph)) 63 http.Handle("/"+ns+"/graph/lifetimes", handlerWrapper(handleGraphLifetimes)) 64 http.Handle("/"+ns+"/graph/fuzzing", handlerWrapper(handleGraphFuzzing)) 65 http.Handle("/"+ns+"/graph/crashes", handlerWrapper(handleGraphCrashes)) 66 http.Handle("/"+ns+"/repos", handlerWrapper(handleRepos)) 67 http.Handle("/"+ns+"/bug-summaries", handlerWrapper(handleBugSummaries)) 68 http.Handle("/"+ns+"/subsystems", handlerWrapper(handleSubsystemsList)) 69 http.Handle("/"+ns+"/backports", handlerWrapper(handleBackports)) 70 http.Handle("/"+ns+"/s/", handlerWrapper(handleSubsystemPage)) 71 http.Handle("/"+ns+"/manager/", handlerWrapper(handleManagerPage)) 72 } 73 http.HandleFunc("/cron/cache_update", cacheUpdate) 74 http.HandleFunc("/cron/minute_cache_update", handleMinuteCacheUpdate) 75 http.HandleFunc("/cron/deprecate_assets", handleDeprecateAssets) 76 http.HandleFunc("/cron/refresh_subsystems", handleRefreshSubsystems) 77 http.HandleFunc("/cron/subsystem_reports", handleSubsystemReports) 78 } 79 80 type uiMainPage struct { 81 Header *uiHeader 82 Now time.Time 83 Decommissioned bool 84 Managers *uiManagerList 85 BugFilter *uiBugFilter 86 Groups []*uiBugGroup 87 } 88 89 type uiBugFilter struct { 90 Filter *userBugFilter 91 DropURL func(string, string) string 92 } 93 94 func makeUIBugFilter(c context.Context, filter *userBugFilter) *uiBugFilter { 95 url := getCurrentURL(c) 96 return &uiBugFilter{ 97 Filter: filter, 98 DropURL: func(name, value string) string { 99 return html.DropParam(url, name, value) 100 }, 101 } 102 } 103 104 type uiManagerList struct { 105 RepoLink string 106 List []*uiManager 107 } 108 109 func makeManagerList(managers []*uiManager, ns string) *uiManagerList { 110 return &uiManagerList{ 111 RepoLink: fmt.Sprintf("/%s/repos", ns), 112 List: managers, 113 } 114 } 115 116 type uiTerminalPage struct { 117 Header *uiHeader 118 Now time.Time 119 Bugs *uiBugGroup 120 Stats *uiBugStats 121 BugFilter *uiBugFilter 122 } 123 124 type uiBugStats struct { 125 Total int 126 AutoObsoleted int 127 ReproObsoleted int 128 UserObsoleted int 129 } 130 131 func (stats *uiBugStats) Record(bug *Bug, bugReporting *BugReporting) { 132 stats.Total++ 133 switch bug.Status { 134 case BugStatusInvalid: 135 if bugReporting.Auto { 136 stats.AutoObsoleted++ 137 } else { 138 stats.UserObsoleted++ 139 } 140 if bug.StatusReason == dashapi.InvalidatedByRevokedRepro { 141 stats.ReproObsoleted++ 142 } 143 } 144 } 145 146 type uiReposPage struct { 147 Header *uiHeader 148 Repos []*uiRepo 149 } 150 151 type uiRepo struct { 152 URL string 153 Branch string 154 Alias string 155 } 156 157 func (r uiRepo) String() string { 158 return r.URL + " " + r.Branch 159 } 160 161 func (r uiRepo) Equals(other *uiRepo) bool { 162 return r.String() == other.String() 163 } 164 165 type uiSubsystemPage struct { 166 Header *uiHeader 167 Info *uiSubsystem 168 Children []*uiSubsystem 169 Parents []*uiSubsystem 170 Groups []*uiBugGroup 171 } 172 173 type uiSubsystemsPage struct { 174 Header *uiHeader 175 List []*uiSubsystem 176 Unclassified *uiSubsystem 177 SomeHidden bool 178 ShowAllURL string 179 } 180 181 type uiSubsystem struct { 182 Name string 183 Lists string 184 Maintainers string 185 Open uiSubsystemStats 186 Fixed uiSubsystemStats 187 } 188 189 type uiSubsystemStats struct { 190 Count int 191 Link string 192 } 193 194 type uiAdminPage struct { 195 Header *uiHeader 196 Log []byte 197 Managers *uiManagerList 198 RecentJobs *uiJobList 199 PendingJobs *uiJobList 200 RunningJobs *uiJobList 201 TypeJobs *uiJobList 202 FixBisectionsLink string 203 CauseBisectionsLink string 204 JobOverviewLink string 205 MemcacheStats *memcache.Statistics 206 Stopped bool 207 StopLink string 208 MoreStopClicks int 209 } 210 211 type uiManagerPage struct { 212 Header *uiHeader 213 Manager *uiManager 214 Builds []*uiBuild 215 } 216 217 type uiManager struct { 218 Now time.Time 219 Namespace string 220 Name string 221 Link string // link to the syz-manager 222 PageLink string // link to the manager page 223 CoverLink string 224 CurrentBuild *uiBuild 225 FailedBuildBugLink string 226 FailedSyzBuildBugLink string 227 LastActive time.Time 228 CurrentUpTime time.Duration 229 MaxCorpus int64 230 MaxCover int64 231 TotalFuzzingTime time.Duration 232 TotalCrashes int64 233 TotalExecs int64 234 TotalExecsBad bool // highlight TotalExecs in red 235 } 236 237 type uiBuild struct { 238 Time time.Time 239 SyzkallerCommit string 240 SyzkallerCommitLink string 241 SyzkallerCommitDate time.Time 242 KernelRepo string 243 KernelBranch string 244 KernelAlias string 245 KernelCommit string 246 KernelCommitLink string 247 KernelCommitTitle string 248 KernelCommitDate time.Time 249 KernelConfigLink string 250 Assets []*uiAsset 251 } 252 253 type uiBugDiscussion struct { 254 Subject string 255 Link string 256 Total int 257 External int 258 Last time.Time 259 } 260 261 type uiReproAttempt struct { 262 Time time.Time 263 Manager string 264 LogLink string 265 } 266 267 type uiBugPage struct { 268 Header *uiHeader 269 Now time.Time 270 Bug *uiBug 271 BisectCause *uiJob 272 BisectFix *uiJob 273 FixCandidate *uiJob 274 Sections []*uiCollapsible 275 SampleReport template.HTML 276 Crashes *uiCrashTable 277 TestPatchJobs *uiJobList 278 LabelGroups []*uiBugLabelGroup 279 DebugSubsystems string 280 } 281 282 type uiBugLabelGroup struct { 283 Name string 284 Labels []*uiBugLabel 285 } 286 287 const ( 288 sectionBugList = "bug_list" 289 sectionJobList = "job_list" 290 sectionDiscussionList = "discussion_list" 291 sectionTestResults = "test_results" 292 sectionReproAttempts = "repro_attempts" 293 ) 294 295 type uiCollapsible struct { 296 Title string 297 Show bool // By default it's collapsed. 298 Type string // Template system understands it. 299 Value interface{} 300 } 301 302 func makeCollapsibleBugJobs(title string, jobs []*uiJob) *uiCollapsible { 303 return &uiCollapsible{ 304 Title: fmt.Sprintf("%s (%d)", title, len(jobs)), 305 Type: sectionJobList, 306 Value: &uiJobList{ 307 PerBug: true, 308 Jobs: jobs, 309 }, 310 } 311 } 312 313 type uiBugGroup struct { 314 Now time.Time 315 Caption string 316 Fragment string 317 Namespace string 318 ShowNamespace bool 319 ShowPatch bool 320 ShowPatched bool 321 ShowStatus bool 322 ShowIndex int 323 Bugs []*uiBug 324 DispLastAct bool 325 DispDiscuss bool 326 } 327 328 type uiJobList struct { 329 Title string 330 PerBug bool 331 Jobs []*uiJob 332 } 333 334 type uiCommit struct { 335 Hash string 336 Repo string 337 Branch string 338 Title string 339 Link string 340 Author string 341 CC []string 342 Date time.Time 343 } 344 345 type uiBug struct { 346 Namespace string 347 Title string 348 NumCrashes int64 349 NumCrashesBad bool 350 BisectCause BisectStatus 351 BisectFix BisectStatus 352 FirstTime time.Time 353 LastTime time.Time 354 ReportedTime time.Time 355 ClosedTime time.Time 356 ReproLevel dashapi.ReproLevel 357 ReportingIndex int 358 Status string 359 Link string 360 ExternalLink string 361 CreditEmail string 362 Commits []*uiCommit 363 PatchedOn []string 364 MissingOn []string 365 NumManagers int 366 LastActivity time.Time 367 Labels []*uiBugLabel 368 Discussions DiscussionSummary 369 ID string 370 } 371 372 type uiBugLabel struct { 373 Name string 374 Link string 375 } 376 377 type uiCrash struct { 378 Title string 379 Manager string 380 Time time.Time 381 Maintainers string 382 LogLink string 383 LogHasStrace bool 384 ReportLink string 385 ReproSyzLink string 386 ReproCLink string 387 ReproIsRevoked bool 388 MachineInfoLink string 389 Assets []*uiAsset 390 *uiBuild 391 } 392 393 type uiAsset struct { 394 Title string 395 DownloadURL string 396 } 397 398 type uiCrashTable struct { 399 Crashes []*uiCrash 400 Caption string 401 } 402 403 type uiJob struct { 404 *dashapi.JobInfo 405 Crash *uiCrash 406 InvalidateJobLink string 407 RestartJobLink string 408 FixCandidate bool 409 } 410 411 type uiBackportGroup struct { 412 From *uiRepo 413 To *uiRepo 414 Namespaces []string 415 List []*uiBackport 416 } 417 418 type uiBackport struct { 419 Commit *uiCommit 420 Bugs map[string][]*uiBug // namespace -> list of related bugs in it 421 } 422 423 type uiBackportsPage struct { 424 Header *uiHeader 425 Groups []*uiBackportGroup 426 DisplayNamespace func(string) string 427 } 428 429 type userBugFilter struct { 430 Manager string // show bugs that happened on the manager 431 OnlyManager string // show bugs that happened ONLY on the manager 432 Labels []string 433 NoSubsystem bool 434 } 435 436 func MakeBugFilter(r *http.Request) (*userBugFilter, error) { 437 if err := r.ParseForm(); err != nil { 438 return nil, err 439 } 440 return &userBugFilter{ 441 NoSubsystem: r.FormValue("no_subsystem") != "", 442 Manager: r.FormValue("manager"), 443 OnlyManager: r.FormValue("only_manager"), 444 Labels: r.Form["label"], 445 }, nil 446 } 447 448 func (filter *userBugFilter) MatchManagerName(name string) bool { 449 target := filter.ManagerName() 450 return target == "" || target == name 451 } 452 453 func (filter *userBugFilter) ManagerName() string { 454 if filter != nil && filter.OnlyManager != "" { 455 return filter.OnlyManager 456 } 457 if filter != nil && filter.Manager != "" { 458 return filter.Manager 459 } 460 return "" 461 } 462 463 func (filter *userBugFilter) MatchBug(bug *Bug) bool { 464 if filter == nil { 465 return true 466 } 467 if filter.OnlyManager != "" && (len(bug.HappenedOn) != 1 || bug.HappenedOn[0] != filter.OnlyManager) { 468 return false 469 } 470 if filter.Manager != "" && !stringInList(bug.HappenedOn, filter.Manager) { 471 return false 472 } 473 if filter.NoSubsystem && len(bug.LabelValues(SubsystemLabel)) > 0 { 474 return false 475 } 476 for _, rawLabel := range filter.Labels { 477 label, value := splitLabel(rawLabel) 478 if !bug.HasLabel(label, value) { 479 return false 480 } 481 } 482 return true 483 } 484 485 func (filter *userBugFilter) Hash() string { 486 return hash.String([]byte(fmt.Sprintf("%#v", filter))) 487 } 488 489 func splitLabel(rawLabel string) (BugLabelType, string) { 490 label, value, _ := strings.Cut(rawLabel, ":") 491 return BugLabelType(label), value 492 } 493 494 func (filter *userBugFilter) Any() bool { 495 if filter == nil { 496 return false 497 } 498 return len(filter.Labels) > 0 || filter.OnlyManager != "" || filter.Manager != "" || filter.NoSubsystem 499 } 500 501 // handleMain serves main page. 502 func handleMain(c context.Context, w http.ResponseWriter, r *http.Request) error { 503 hdr, err := commonHeader(c, r, w, "") 504 if err != nil { 505 return err 506 } 507 accessLevel := accessLevel(c, r) 508 filter, err := MakeBugFilter(r) 509 if err != nil { 510 return fmt.Errorf("%w: failed to parse URL parameters", ErrClientBadRequest) 511 } 512 managers, err := CachedUIManagers(c, accessLevel, hdr.Namespace, filter) 513 if err != nil { 514 return err 515 } 516 groups, err := fetchNamespaceBugs(c, accessLevel, hdr.Namespace, filter) 517 if err != nil { 518 return err 519 } 520 for _, group := range groups { 521 if getNsConfig(c, hdr.Namespace).DisplayDiscussions { 522 group.DispDiscuss = true 523 } else { 524 group.DispLastAct = true 525 } 526 } 527 data := &uiMainPage{ 528 Header: hdr, 529 Decommissioned: getNsConfig(c, hdr.Namespace).Decommissioned, 530 Now: timeNow(c), 531 Groups: groups, 532 Managers: makeManagerList(managers, hdr.Namespace), 533 BugFilter: makeUIBugFilter(c, filter), 534 } 535 536 if r.FormValue("json") == "1" { 537 w.Header().Set("Content-Type", "application/json") 538 return writeJSONVersionOf(w, data) 539 } 540 541 return serveTemplate(w, "main.html", data) 542 } 543 544 func handleFixed(c context.Context, w http.ResponseWriter, r *http.Request) error { 545 return handleTerminalBugList(c, w, r, &TerminalBug{ 546 Status: BugStatusFixed, 547 Subpage: "/fixed", // nolint: goconst // TODO: remove it once goconst 1.7.0+ landed 548 ShowPatch: true, 549 ShowPatched: true, 550 }) 551 } 552 553 func handleInvalid(c context.Context, w http.ResponseWriter, r *http.Request) error { 554 return handleTerminalBugList(c, w, r, &TerminalBug{ 555 Status: BugStatusInvalid, 556 Subpage: "/invalid", 557 ShowPatch: false, 558 ShowStats: true, 559 }) 560 } 561 562 func handleManagerPage(c context.Context, w http.ResponseWriter, r *http.Request) error { 563 hdr, err := commonHeader(c, r, w, "") 564 if err != nil { 565 return err 566 } 567 managers, err := CachedUIManagers(c, accessLevel(c, r), hdr.Namespace, nil) 568 if err != nil { 569 return err 570 } 571 var manager *uiManager 572 if pos := strings.Index(r.URL.Path, "/manager/"); pos != -1 { 573 manager = findManager(managers, r.URL.Path[pos+len("/manager/"):]) 574 } 575 if manager == nil { 576 return fmt.Errorf("%w: manager is unknown", ErrClientBadRequest) 577 } 578 builds, err := loadBuilds(c, hdr.Namespace, manager.Name, BuildNormal) 579 if err != nil { 580 return fmt.Errorf("failed to query builds: %w", err) 581 } 582 managerPage := &uiManagerPage{Manager: manager, Header: hdr} 583 for _, build := range builds { 584 managerPage.Builds = append(managerPage.Builds, makeUIBuild(c, build, false)) 585 } 586 return serveTemplate(w, "manager.html", managerPage) 587 } 588 589 func findManager(managers []*uiManager, name string) *uiManager { 590 for _, mgr := range managers { 591 if mgr.Name == name { 592 return mgr 593 } 594 } 595 return nil 596 } 597 598 func handleSubsystemPage(c context.Context, w http.ResponseWriter, r *http.Request) error { 599 hdr, err := commonHeader(c, r, w, "") 600 if err != nil { 601 return err 602 } 603 service := getNsConfig(c, hdr.Namespace).Subsystems.Service 604 if service == nil { 605 return fmt.Errorf("%w: the namespace does not have subsystems", ErrClientBadRequest) 606 } 607 var subsystem *subsystem.Subsystem 608 if pos := strings.Index(r.URL.Path, "/s/"); pos != -1 { 609 name := r.URL.Path[pos+3:] 610 if newName := getNsConfig(c, hdr.Namespace).Subsystems.Redirect[name]; newName != "" { 611 http.Redirect(w, r, r.URL.Path[:pos+3]+newName, http.StatusMovedPermanently) 612 return nil 613 } 614 subsystem = service.ByName(name) 615 } 616 if subsystem == nil { 617 return fmt.Errorf("%w: the subsystem is not found in the path %v", ErrClientBadRequest, r.URL.Path) 618 } 619 groups, err := fetchNamespaceBugs(c, accessLevel(c, r), 620 hdr.Namespace, &userBugFilter{ 621 Labels: []string{ 622 BugLabel{ 623 Label: SubsystemLabel, 624 Value: subsystem.Name, 625 }.String(), 626 }, 627 }) 628 if err != nil { 629 return err 630 } 631 for _, group := range groups { 632 group.DispDiscuss = getNsConfig(c, hdr.Namespace).DisplayDiscussions 633 } 634 cached, err := CacheGet(c, r, hdr.Namespace) 635 if err != nil { 636 return err 637 } 638 children := []*uiSubsystem{} 639 for _, item := range service.Children(subsystem) { 640 uiChild := createUISubsystem(hdr.Namespace, item, cached) 641 if uiChild.Open.Count+uiChild.Fixed.Count == 0 { 642 continue 643 } 644 children = append(children, uiChild) 645 } 646 parents := []*uiSubsystem{} 647 for _, item := range subsystem.Parents { 648 parents = append(parents, createUISubsystem(hdr.Namespace, item, cached)) 649 } 650 sort.Slice(children, func(i, j int) bool { return children[i].Name < children[j].Name }) 651 return serveTemplate(w, "subsystem_page.html", &uiSubsystemPage{ 652 Header: hdr, 653 Info: createUISubsystem(hdr.Namespace, subsystem, cached), 654 Children: children, 655 Parents: parents, 656 Groups: groups, 657 }) 658 } 659 660 func handleBackports(c context.Context, w http.ResponseWriter, r *http.Request) error { 661 hdr, err := commonHeader(c, r, w, "") 662 if err != nil { 663 return err 664 } 665 backports, err := loadAllBackports(c) 666 if err != nil { 667 return err 668 } 669 var groups []*uiBackportGroup 670 accessLevel := accessLevel(c, r) 671 for _, backport := range backports { 672 outgoing := stringInList(backport.FromNs, hdr.Namespace) 673 ui := &uiBackport{ 674 Commit: backport.Commit, 675 Bugs: map[string][]*uiBug{}, 676 } 677 incoming := false 678 for _, bug := range backport.Bugs { 679 if accessLevel < bug.sanitizeAccess(c, accessLevel) { 680 continue 681 } 682 if !outgoing && bug.Namespace != hdr.Namespace { 683 // If it's an incoming backport, don't include other namespaces. 684 continue 685 } 686 if bug.Namespace == hdr.Namespace { 687 incoming = true 688 } 689 ui.Bugs[bug.Namespace] = append(ui.Bugs[bug.Namespace], 690 createUIBug(c, bug, nil, nil)) 691 } 692 if len(ui.Bugs) == 0 { 693 continue 694 } 695 696 // Display either backports to/from repos of the namespace 697 // or the backports that affect bugs from the current namespace. 698 if !outgoing && !incoming { 699 continue 700 } 701 var group *uiBackportGroup 702 for _, existing := range groups { 703 if backport.From.Equals(existing.From) && 704 backport.To.Equals(existing.To) { 705 group = existing 706 break 707 } 708 } 709 if group == nil { 710 group = &uiBackportGroup{ 711 From: backport.From, 712 To: backport.To, 713 } 714 groups = append(groups, group) 715 } 716 group.List = append(group.List, ui) 717 } 718 for _, group := range groups { 719 var nsList []string 720 for _, backport := range group.List { 721 for ns := range backport.Bugs { 722 nsList = append(nsList, ns) 723 } 724 } 725 nsList = unique(nsList) 726 sort.Strings(nsList) 727 group.Namespaces = nsList 728 } 729 sort.Slice(groups, func(i, j int) bool { 730 return groups[i].From.String()+groups[i].To.String() < 731 groups[j].From.String()+groups[j].To.String() 732 }) 733 return serveTemplate(w, "backports.html", &uiBackportsPage{ 734 Header: hdr, 735 Groups: groups, 736 DisplayNamespace: func(ns string) string { 737 return getNsConfig(c, ns).DisplayTitle 738 }, 739 }) 740 } 741 742 type rawBackport struct { 743 Commit *uiCommit 744 From *uiRepo 745 FromNs []string // namespaces that correspond to From 746 To *uiRepo 747 Bugs []*Bug 748 } 749 750 func loadAllBackports(c context.Context) ([]*rawBackport, error) { 751 bugs, jobs, _, err := relevantBackportJobs(c) 752 if err != nil { 753 return nil, err 754 } 755 var ret []*rawBackport 756 perCommit := map[string]*rawBackport{} 757 for i, job := range jobs { 758 jobCommit := job.Commits[0] 759 to := &uiRepo{URL: job.MergeBaseRepo, Branch: job.MergeBaseBranch} 760 from := &uiRepo{URL: job.KernelRepo, Branch: job.KernelBranch} 761 commit := &uiCommit{ 762 Hash: jobCommit.Hash, 763 Title: jobCommit.Title, 764 Link: vcs.CommitLink(from.URL, jobCommit.Hash), 765 Repo: from.URL, 766 Branch: from.Branch, 767 } 768 769 hash := from.String() + to.String() + commit.Hash 770 backport := perCommit[hash] 771 if backport == nil { 772 backport = &rawBackport{ 773 From: from, 774 FromNs: namespacesForRepo(c, from.URL, from.Branch), 775 To: to, 776 Commit: commit} 777 ret = append(ret, backport) 778 perCommit[hash] = backport 779 } 780 backport.Bugs = append(backport.Bugs, bugs[i]) 781 } 782 return ret, nil 783 } 784 785 func namespacesForRepo(c context.Context, url, branch string) []string { 786 var ret []string 787 for ns, cfg := range getConfig(c).Namespaces { 788 has := false 789 for _, repo := range cfg.Repos { 790 if repo.NoPoll { 791 continue 792 } 793 if repo.URL == url && repo.Branch == branch { 794 has = true 795 break 796 } 797 } 798 if has { 799 ret = append(ret, ns) 800 } 801 } 802 return ret 803 } 804 805 func handleRepos(c context.Context, w http.ResponseWriter, r *http.Request) error { 806 hdr, err := commonHeader(c, r, w, "") 807 if err != nil { 808 return err 809 } 810 repos, err := loadRepos(c, hdr.Namespace) 811 if err != nil { 812 return err 813 } 814 return serveTemplate(w, "repos.html", &uiReposPage{ 815 Header: hdr, 816 Repos: repos, 817 }) 818 } 819 820 type TerminalBug struct { 821 Status int 822 Subpage string 823 ShowPatch bool 824 ShowPatched bool 825 ShowStats bool 826 Filter *userBugFilter 827 } 828 829 func handleTerminalBugList(c context.Context, w http.ResponseWriter, r *http.Request, typ *TerminalBug) error { 830 accessLevel := accessLevel(c, r) 831 hdr, err := commonHeader(c, r, w, "") 832 if err != nil { 833 return err 834 } 835 hdr.Subpage = typ.Subpage 836 typ.Filter, err = MakeBugFilter(r) 837 if err != nil { 838 return fmt.Errorf("%w: failed to parse URL parameters", ErrClientBadRequest) 839 } 840 extraBugs := []*Bug{} 841 if typ.Status == BugStatusFixed { 842 // Mix in bugs that have pending fixes. 843 extraBugs, err = fetchFixPendingBugs(c, hdr.Namespace, typ.Filter.ManagerName()) 844 if err != nil { 845 return err 846 } 847 } 848 bugs, stats, err := fetchTerminalBugs(c, accessLevel, hdr.Namespace, typ, extraBugs) 849 if err != nil { 850 return err 851 } 852 if !typ.ShowStats { 853 stats = nil 854 } 855 data := &uiTerminalPage{ 856 Header: hdr, 857 Now: timeNow(c), 858 Bugs: bugs, 859 Stats: stats, 860 BugFilter: makeUIBugFilter(c, typ.Filter), 861 } 862 863 if r.FormValue("json") == "1" { 864 w.Header().Set("Content-Type", "application/json") 865 return writeJSONVersionOf(w, data) 866 } 867 868 return serveTemplate(w, "terminal.html", data) 869 } 870 871 func handleAdmin(c context.Context, w http.ResponseWriter, r *http.Request) error { 872 accessLevel := accessLevel(c, r) 873 if accessLevel != AccessAdmin { 874 return ErrAccess 875 } 876 switch action := r.FormValue("action"); action { 877 case "": 878 case "memcache_flush": 879 if err := memcache.Flush(c); err != nil { 880 return fmt.Errorf("failed to flush memcache: %w", err) 881 } 882 case "invalidate_bisection": 883 return handleInvalidateBisection(c, w, r) 884 case "emergency_stop": 885 if err := recordEmergencyStop(c); err != nil { 886 return fmt.Errorf("failed to record an emergency stop: %w", err) 887 } 888 default: 889 return fmt.Errorf("%w: unknown action %q", ErrClientBadRequest, action) 890 } 891 hdr, err := commonHeader(c, r, w, "") 892 if err != nil { 893 return err 894 } 895 var ( 896 memcacheStats *memcache.Statistics 897 managers []*uiManager 898 errorLog []byte 899 recentJobs []*uiJob 900 pendingJobs []*uiJob 901 runningJobs []*uiJob 902 typeJobs []*uiJob 903 ) 904 g, _ := errgroup.WithContext(context.Background()) 905 g.Go(func() error { 906 var err error 907 memcacheStats, err = memcache.Stats(c) 908 return err 909 }) 910 g.Go(func() error { 911 var err error 912 managers, err = loadManagers(c, accessLevel, "", nil) 913 return err 914 }) 915 g.Go(func() error { 916 var err error 917 errorLog, err = fetchErrorLogs(c) 918 return err 919 }) 920 if r.FormValue("job_type") != "" { 921 value, err := strconv.Atoi(r.FormValue("job_type")) 922 if err != nil { 923 return fmt.Errorf("%w: %w", ErrClientBadRequest, err) 924 } 925 g.Go(func() error { 926 var err error 927 typeJobs, err = loadJobsOfType(c, JobType(value)) 928 return err 929 }) 930 } else { 931 g.Go(func() error { 932 var err error 933 recentJobs, err = loadRecentJobs(c) 934 return err 935 }) 936 g.Go(func() error { 937 var err error 938 pendingJobs, err = loadPendingJobs(c) 939 return err 940 }) 941 g.Go(func() error { 942 var err error 943 runningJobs, err = loadRunningJobs(c) 944 return err 945 }) 946 } 947 alreadyStopped := false 948 g.Go(func() error { 949 var err error 950 alreadyStopped, err = emergentlyStopped(c) 951 return err 952 }) 953 err = g.Wait() 954 if err != nil { 955 return err 956 } 957 data := &uiAdminPage{ 958 Header: hdr, 959 Log: errorLog, 960 Managers: makeManagerList(managers, hdr.Namespace), 961 MemcacheStats: memcacheStats, 962 Stopped: alreadyStopped, 963 MoreStopClicks: 2, 964 StopLink: html.AmendURL("/admin", "stop_clicked", "1"), 965 } 966 if r.FormValue("stop_clicked") != "" { 967 data.MoreStopClicks = 1 968 data.StopLink = html.AmendURL("/admin", "action", "emergency_stop") 969 } 970 if r.FormValue("job_type") != "" { 971 data.TypeJobs = &uiJobList{Title: "Last jobs:", Jobs: typeJobs} 972 data.JobOverviewLink = "/admin" 973 } else { 974 data.RecentJobs = &uiJobList{Title: "Recent jobs:", Jobs: recentJobs} 975 data.RunningJobs = &uiJobList{Title: "Running jobs:", Jobs: runningJobs} 976 data.PendingJobs = &uiJobList{Title: "Pending jobs:", Jobs: pendingJobs} 977 data.FixBisectionsLink = html.AmendURL("/admin", "job_type", fmt.Sprintf("%d", JobBisectFix)) 978 data.CauseBisectionsLink = html.AmendURL("/admin", "job_type", fmt.Sprintf("%d", JobBisectCause)) 979 } 980 return serveTemplate(w, "admin.html", data) 981 } 982 983 // handleBug serves page about a single bug (which is passed in id argument). 984 // nolint: funlen, gocyclo 985 func handleBug(c context.Context, w http.ResponseWriter, r *http.Request) error { 986 bug, err := findBugByID(c, r) 987 if err != nil { 988 return fmt.Errorf("%w: %w", ErrClientNotFound, err) 989 } 990 accessLevel := accessLevel(c, r) 991 if err := checkAccessLevel(c, r, bug.sanitizeAccess(c, accessLevel)); err != nil { 992 return err 993 } 994 if r.FormValue("debug_subsystems") != "" && accessLevel == AccessAdmin { 995 return debugBugSubsystems(c, w, bug) 996 } 997 hdr, err := commonHeader(c, r, w, bug.Namespace) 998 if err != nil { 999 return err 1000 } 1001 state, err := loadReportingState(c) 1002 if err != nil { 1003 return err 1004 } 1005 managers, err := CachedManagerList(c, bug.Namespace) 1006 if err != nil { 1007 return err 1008 } 1009 sections := []*uiCollapsible{} 1010 if bug.DupOf != "" { 1011 dup := new(Bug) 1012 if err := db.Get(c, db.NewKey(c, "Bug", bug.DupOf, 0, nil), dup); err != nil { 1013 return err 1014 } 1015 if accessLevel >= dup.sanitizeAccess(c, accessLevel) { 1016 sections = append(sections, &uiCollapsible{ 1017 Title: "Duplicate of", 1018 Show: true, 1019 Type: sectionBugList, 1020 Value: &uiBugGroup{ 1021 Now: timeNow(c), 1022 Bugs: []*uiBug{createUIBug(c, dup, state, managers)}, 1023 }, 1024 }) 1025 } 1026 } 1027 uiBug := createUIBug(c, bug, state, managers) 1028 crashes, sampleReport, err := loadCrashesForBug(c, bug) 1029 if err != nil { 1030 return err 1031 } 1032 crashesTable := &uiCrashTable{ 1033 Crashes: crashes, 1034 Caption: fmt.Sprintf("Crashes (%d)", bug.NumCrashes), 1035 } 1036 dups, err := loadDupsForBug(c, r, bug, state, managers) 1037 if err != nil { 1038 return err 1039 } 1040 if len(dups.Bugs) > 0 { 1041 sections = append(sections, &uiCollapsible{ 1042 Title: fmt.Sprintf("Duplicate bugs (%d)", len(dups.Bugs)), 1043 Type: sectionBugList, 1044 Value: dups, 1045 }) 1046 } 1047 discussions, err := getBugDiscussionsUI(c, bug) 1048 if err != nil { 1049 return err 1050 } 1051 if len(discussions) > 0 { 1052 sections = append(sections, &uiCollapsible{ 1053 Title: fmt.Sprintf("Discussions (%d)", len(discussions)), 1054 Show: true, 1055 Type: sectionDiscussionList, 1056 Value: discussions, 1057 }) 1058 } 1059 treeTestJobs, err := treeTestJobs(c, bug) 1060 if err != nil { 1061 return err 1062 } 1063 if len(treeTestJobs) > 0 { 1064 sections = append(sections, &uiCollapsible{ 1065 Title: fmt.Sprintf("Bug presence (%d)", len(treeTestJobs)), 1066 Show: true, 1067 Type: sectionTestResults, 1068 Value: treeTestJobs, 1069 }) 1070 } 1071 similar, err := loadSimilarBugsUI(c, r, bug, state) 1072 if err != nil { 1073 return err 1074 } 1075 if len(similar.Bugs) > 0 { 1076 sections = append(sections, &uiCollapsible{ 1077 Title: fmt.Sprintf("Similar bugs (%d)", len(similar.Bugs)), 1078 Show: getNsConfig(c, hdr.Namespace).AccessLevel != AccessPublic, 1079 Type: sectionBugList, 1080 Value: similar, 1081 }) 1082 } 1083 causeBisections, err := queryBugJobs(c, bug, JobBisectCause) 1084 if err != nil { 1085 return fmt.Errorf("failed to load cause bisections: %w", err) 1086 } 1087 var bisectCause *uiJob 1088 if bug.BisectCause > BisectPending { 1089 bisectCause, err = causeBisections.uiBestBisection(c) 1090 if err != nil { 1091 return err 1092 } 1093 } 1094 fixBisections, err := queryBugJobs(c, bug, JobBisectFix) 1095 if err != nil { 1096 return fmt.Errorf("failed to load cause bisections: %w", err) 1097 } 1098 var bisectFix *uiJob 1099 if bug.BisectFix > BisectPending { 1100 bisectFix, err = fixBisections.uiBestBisection(c) 1101 if err != nil { 1102 return err 1103 } 1104 } 1105 var fixCandidate *uiJob 1106 if bug.FixCandidateJob != "" { 1107 fixCandidate, err = fixBisections.uiBestFixCandidate(c) 1108 if err != nil { 1109 return err 1110 } 1111 } 1112 testPatchJobs, err := loadTestPatchJobs(c, bug) 1113 if err != nil { 1114 return err 1115 } 1116 if len(testPatchJobs) > 0 { 1117 sections = append(sections, &uiCollapsible{ 1118 Title: fmt.Sprintf("Last patch testing requests (%d)", len(testPatchJobs)), 1119 Type: sectionJobList, 1120 Value: &uiJobList{ 1121 PerBug: true, 1122 Jobs: testPatchJobs, 1123 }, 1124 }) 1125 } 1126 if accessLevel == AccessAdmin && len(bug.ReproAttempts) > 0 { 1127 reproAttempts := getReproAttempts(bug) 1128 sections = append(sections, &uiCollapsible{ 1129 Title: fmt.Sprintf("Failed repro attempts (%d)", len(reproAttempts)), 1130 Type: sectionReproAttempts, 1131 Value: reproAttempts, 1132 }) 1133 } 1134 data := &uiBugPage{ 1135 Header: hdr, 1136 Now: timeNow(c), 1137 Bug: uiBug, 1138 BisectCause: bisectCause, 1139 BisectFix: bisectFix, 1140 FixCandidate: fixCandidate, 1141 Sections: sections, 1142 SampleReport: sampleReport, 1143 Crashes: crashesTable, 1144 LabelGroups: getLabelGroups(c, bug), 1145 } 1146 if accessLevel == AccessAdmin && !bug.hasUserSubsystems() { 1147 data.DebugSubsystems = html.AmendURL(data.Bug.Link, "debug_subsystems", "1") 1148 } 1149 // bug.BisectFix is set to BisectNot in three cases : 1150 // - no fix bisections have been performed on the bug 1151 // - fix bisection was performed but resulted in a crash on HEAD 1152 // - there have been infrastructure problems during the job execution 1153 if len(fixBisections.all()) > 1 || len(fixBisections.all()) > 0 && bisectFix == nil { 1154 uiList, err := fixBisections.uiAll(c) 1155 if err != nil { 1156 return err 1157 } 1158 if len(uiList) != 0 { 1159 data.Sections = append(data.Sections, makeCollapsibleBugJobs( 1160 "Fix bisection attempts", uiList)) 1161 } 1162 } 1163 // Similarly, a cause bisection can be repeated if there were infrastructure problems. 1164 if len(causeBisections.all()) > 1 || len(causeBisections.all()) > 0 && bisectCause == nil { 1165 uiList, err := causeBisections.uiAll(c) 1166 if err != nil { 1167 return err 1168 } 1169 if len(uiList) != 0 { 1170 data.Sections = append(data.Sections, makeCollapsibleBugJobs( 1171 "Cause bisection attempts", uiList)) 1172 } 1173 } 1174 if r.FormValue("json") == "1" { 1175 w.Header().Set("Content-Type", "application/json") 1176 return writeJSONVersionOf(w, data) 1177 } 1178 1179 return serveTemplate(w, "bug.html", data) 1180 } 1181 1182 func getReproAttempts(bug *Bug) []*uiReproAttempt { 1183 var ret []*uiReproAttempt 1184 for _, item := range bug.ReproAttempts { 1185 ret = append(ret, &uiReproAttempt{ 1186 Time: item.Time, 1187 Manager: item.Manager, 1188 LogLink: textLink(textReproLog, item.Log), 1189 }) 1190 } 1191 return ret 1192 } 1193 1194 type labelGroupInfo struct { 1195 Label BugLabelType 1196 Name string 1197 } 1198 1199 var labelGroupOrder = []labelGroupInfo{ 1200 { 1201 Label: OriginLabel, 1202 Name: "Bug presence", 1203 }, 1204 { 1205 Label: SubsystemLabel, 1206 Name: "Subsystems", 1207 }, 1208 { 1209 Label: EmptyLabel, // all the rest 1210 Name: "Labels", 1211 }, 1212 } 1213 1214 func getLabelGroups(c context.Context, bug *Bug) []*uiBugLabelGroup { 1215 var ret []*uiBugLabelGroup 1216 seenLabel := map[string]bool{} 1217 for _, info := range labelGroupOrder { 1218 obj := &uiBugLabelGroup{ 1219 Name: info.Name, 1220 } 1221 for _, entry := range bug.Labels { 1222 if seenLabel[entry.String()] { 1223 continue 1224 } 1225 if entry.Label == info.Label || info.Label == EmptyLabel { 1226 seenLabel[entry.String()] = true 1227 obj.Labels = append(obj.Labels, makeBugLabelUI(c, bug, entry)) 1228 } 1229 } 1230 if len(obj.Labels) == 0 { 1231 continue 1232 } 1233 ret = append(ret, obj) 1234 } 1235 return ret 1236 } 1237 1238 func debugBugSubsystems(c context.Context, w http.ResponseWriter, bug *Bug) error { 1239 service := getNsConfig(c, bug.Namespace).Subsystems.Service 1240 if service == nil { 1241 w.Write([]byte("Subsystem service was not found.")) 1242 return nil 1243 } 1244 _, err := inferSubsystems(c, bug, bug.key(c), &debugtracer.GenericTracer{ 1245 TraceWriter: w, 1246 }) 1247 if err != nil { 1248 w.Write([]byte(fmt.Sprintf("%s", err))) 1249 } 1250 return nil 1251 } 1252 1253 func makeBugLabelUI(c context.Context, bug *Bug, entry BugLabel) *uiBugLabel { 1254 url := getCurrentURL(c) 1255 filterValue := entry.String() 1256 1257 // If we're on a main/terminal/subsystem page, let's stay there. 1258 link := url 1259 if !strings.HasPrefix(url, "/"+bug.Namespace) { 1260 link = fmt.Sprintf("/%s", bug.Namespace) 1261 } 1262 link = html.TransformURL(link, "label", func(oldLabels []string) []string { 1263 return mergeLabelSet(oldLabels, entry.String()) 1264 }) 1265 ret := &uiBugLabel{ 1266 Name: filterValue, 1267 Link: link, 1268 } 1269 // Patch depending on the specific label type. 1270 switch entry.Label { 1271 case SubsystemLabel: 1272 // Use just the subsystem name. 1273 ret.Name = entry.Value 1274 // Prefer link to the per-subsystem page. 1275 if !strings.HasPrefix(url, "/"+bug.Namespace) || strings.Contains(url, "/s/") { 1276 ret.Link = fmt.Sprintf("/%s/s/%s", bug.Namespace, entry.Value) 1277 } 1278 } 1279 return ret 1280 } 1281 1282 func mergeLabelSet(oldLabels []string, newLabel string) []string { 1283 // Leave only one label for each type. 1284 labelsMap := map[BugLabelType]string{} 1285 for _, rawLabel := range append(oldLabels, newLabel) { 1286 label, value := splitLabel(rawLabel) 1287 labelsMap[label] = value 1288 } 1289 var ret []string 1290 for label, value := range labelsMap { 1291 ret = append(ret, BugLabel{ 1292 Label: label, 1293 Value: value, 1294 }.String()) 1295 } 1296 return ret 1297 } 1298 1299 func getBugDiscussionsUI(c context.Context, bug *Bug) ([]*uiBugDiscussion, error) { 1300 // TODO: also include dup bug discussions. 1301 // TODO: limit the number of DiscussionReminder type entries, e.g. all with 1302 // external replies + one latest. 1303 var list []*uiBugDiscussion 1304 discussions, err := discussionsForBug(c, bug.key(c)) 1305 if err != nil { 1306 return nil, err 1307 } 1308 for _, d := range discussions { 1309 list = append(list, &uiBugDiscussion{ 1310 Subject: d.Subject, 1311 Link: d.link(), 1312 Total: d.Summary.AllMessages, 1313 External: d.Summary.ExternalMessages, 1314 Last: d.Summary.LastMessage, 1315 }) 1316 } 1317 sort.SliceStable(list, func(i, j int) bool { 1318 return list[i].Last.After(list[j].Last) 1319 }) 1320 return list, nil 1321 } 1322 1323 func handleBugSummaries(c context.Context, w http.ResponseWriter, r *http.Request) error { 1324 if accessLevel(c, r) != AccessAdmin { 1325 return fmt.Errorf("admin only") 1326 } 1327 hdr, err := commonHeader(c, r, w, "") 1328 if err != nil { 1329 return err 1330 } 1331 stage := r.FormValue("stage") 1332 if stage == "" { 1333 return fmt.Errorf("stage must be specified") 1334 } 1335 list, err := getBugSummaries(c, hdr.Namespace, stage) 1336 if err != nil { 1337 return err 1338 } 1339 w.Header().Set("Content-Type", "application/json") 1340 return json.NewEncoder(w).Encode(list) 1341 } 1342 1343 func writeJSONVersionOf(writer http.ResponseWriter, page interface{}) error { 1344 data, err := GetJSONDescrFor(page) 1345 if err != nil { 1346 return err 1347 } 1348 _, err = writer.Write(data) 1349 return err 1350 } 1351 1352 func findBugByID(c context.Context, r *http.Request) (*Bug, error) { 1353 if id := r.FormValue("id"); id != "" { 1354 bug := new(Bug) 1355 bugKey := db.NewKey(c, "Bug", id, 0, nil) 1356 err := db.Get(c, bugKey, bug) 1357 return bug, err 1358 } 1359 if extID := r.FormValue("extid"); extID != "" { 1360 bug, _, err := findBugByReportingID(c, extID) 1361 return bug, err 1362 } 1363 return nil, fmt.Errorf("mandatory parameter id/extid is missing") 1364 } 1365 1366 func handleSubsystemsList(c context.Context, w http.ResponseWriter, r *http.Request) error { 1367 hdr, err := commonHeader(c, r, w, "") 1368 if err != nil { 1369 return err 1370 } 1371 cached, err := CacheGet(c, r, hdr.Namespace) 1372 if err != nil { 1373 return err 1374 } 1375 service := getNsConfig(c, hdr.Namespace).Subsystems.Service 1376 if service == nil { 1377 return fmt.Errorf("%w: the namespace does not have subsystems", ErrClientBadRequest) 1378 } 1379 nonEmpty := r.FormValue("all") != "true" 1380 list := []*uiSubsystem{} 1381 someHidden := false 1382 for _, item := range service.List() { 1383 record := createUISubsystem(hdr.Namespace, item, cached) 1384 if nonEmpty && (record.Open.Count+record.Fixed.Count) == 0 { 1385 someHidden = true 1386 continue 1387 } 1388 list = append(list, record) 1389 } 1390 unclassified := &uiSubsystem{ 1391 Name: "", 1392 Open: uiSubsystemStats{ 1393 Count: cached.NoSubsystem.Open, 1394 Link: html.AmendURL("/"+hdr.Namespace, "no_subsystem", "true"), 1395 }, 1396 Fixed: uiSubsystemStats{ 1397 Count: cached.NoSubsystem.Fixed, 1398 Link: html.AmendURL("/"+hdr.Namespace+"/fixed", "no_subsystem", "true"), // nolint: goconst 1399 }, 1400 } 1401 sort.Slice(list, func(i, j int) bool { return list[i].Name < list[j].Name }) 1402 return serveTemplate(w, "subsystems.html", &uiSubsystemsPage{ 1403 Header: hdr, 1404 List: list, 1405 Unclassified: unclassified, 1406 SomeHidden: someHidden, 1407 ShowAllURL: html.AmendURL(getCurrentURL(c), "all", "true"), 1408 }) 1409 } 1410 1411 func createUISubsystem(ns string, item *subsystem.Subsystem, cached *Cached) *uiSubsystem { 1412 stats := cached.Subsystems[item.Name] 1413 return &uiSubsystem{ 1414 Name: item.Name, 1415 Lists: strings.Join(item.Lists, ", "), 1416 Maintainers: strings.Join(item.Maintainers, ", "), 1417 Open: uiSubsystemStats{ 1418 Count: stats.Open, 1419 Link: "/" + ns + "/s/" + item.Name, 1420 }, 1421 Fixed: uiSubsystemStats{ 1422 Count: stats.Fixed, 1423 Link: html.AmendURL("/"+ns+"/fixed", "label", BugLabel{ // nolint: goconst 1424 Label: SubsystemLabel, 1425 Value: item.Name, 1426 }.String()), 1427 }, 1428 } 1429 } 1430 1431 // handleText serves plain text blobs (crash logs, reports, reproducers, etc). 1432 func handleTextImpl(c context.Context, w http.ResponseWriter, r *http.Request, tag string) error { 1433 var id int64 1434 if x := r.FormValue("x"); x != "" { 1435 xid, err := strconv.ParseUint(x, 16, 64) 1436 if err != nil || xid == 0 { 1437 return fmt.Errorf("%w: failed to parse text id: %w", ErrClientBadRequest, err) 1438 } 1439 id = int64(xid) 1440 } else { 1441 // Old link support, don't remove. 1442 xid, err := strconv.ParseInt(r.FormValue("id"), 10, 64) 1443 if err != nil || xid == 0 { 1444 return fmt.Errorf("%w: failed to parse text id: %w", ErrClientBadRequest, err) 1445 } 1446 id = xid 1447 } 1448 bug, crash, err := checkTextAccess(c, r, tag, id) 1449 if err != nil { 1450 return err 1451 } 1452 data, ns, err := getText(c, tag, id) 1453 if err != nil { 1454 if strings.Contains(err.Error(), "datastore: no such entity") { 1455 err = fmt.Errorf("%w: %w", ErrClientNotFound, err) 1456 } 1457 return err 1458 } 1459 if err := checkAccessLevel(c, r, getNsConfig(c, ns).AccessLevel); err != nil { 1460 return err 1461 } 1462 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 1463 // Unfortunately filename does not work in chrome on linux due to: 1464 // https://bugs.chromium.org/p/chromium/issues/detail?id=608342 1465 w.Header().Set("Content-Disposition", "inline; filename="+textFilename(tag)) 1466 augmentRepro(c, w, tag, bug, crash) 1467 w.Write(data) 1468 return nil 1469 } 1470 1471 func augmentRepro(c context.Context, w http.ResponseWriter, tag string, bug *Bug, crash *Crash) { 1472 if tag == textReproSyz || tag == textReproC { 1473 // Users asked for the bug link in reproducers (in case you only saved the repro link). 1474 if bug != nil { 1475 prefix := "#" 1476 if tag == textReproC { 1477 prefix = "//" 1478 } 1479 fmt.Fprintf(w, "%v %v/bug?id=%v\n", prefix, appURL(c), bug.keyHash(c)) 1480 } 1481 } 1482 if tag == textReproSyz { 1483 // Add link to documentation and repro opts for syzkaller reproducers. 1484 w.Write([]byte(syzReproPrefix)) 1485 if crash != nil { 1486 fmt.Fprintf(w, "#%s\n", crash.ReproOpts) 1487 } 1488 } 1489 } 1490 1491 func handleText(c context.Context, w http.ResponseWriter, r *http.Request) error { 1492 return handleTextImpl(c, w, r, r.FormValue("tag")) 1493 } 1494 1495 func handleTextX(tag string) contextHandler { 1496 return func(c context.Context, w http.ResponseWriter, r *http.Request) error { 1497 return handleTextImpl(c, w, r, tag) 1498 } 1499 } 1500 1501 func textFilename(tag string) string { 1502 switch tag { 1503 case textKernelConfig: 1504 return ".config" 1505 case textCrashLog: 1506 return "log.txt" 1507 case textCrashReport: 1508 return "report.txt" 1509 case textReproSyz: 1510 return "repro.syz" 1511 case textReproC: 1512 return "repro.c" 1513 case textPatch: 1514 return "patch.diff" 1515 case textLog: 1516 return "bisect.txt" 1517 case textError: 1518 return "error.txt" 1519 case textMachineInfo: 1520 return "minfo.txt" 1521 case textReproLog: 1522 return "repro.log" 1523 default: 1524 panic(fmt.Sprintf("unknown tag %v", tag)) 1525 } 1526 } 1527 1528 func fetchFixPendingBugs(c context.Context, ns, manager string) ([]*Bug, error) { 1529 filter := func(query *db.Query) *db.Query { 1530 query = query.Filter("Namespace=", ns). 1531 Filter("Status=", BugStatusOpen). 1532 Filter("Commits>", "") 1533 if manager != "" { 1534 query = query.Filter("HappenedOn=", manager) 1535 } 1536 return query 1537 } 1538 rawBugs, _, err := loadAllBugs(c, filter) 1539 if err != nil { 1540 return nil, err 1541 } 1542 return rawBugs, nil 1543 } 1544 1545 func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string, 1546 filter *userBugFilter) ([]*uiBugGroup, error) { 1547 if !filter.Any() && getNsConfig(c, ns).CacheUIPages { 1548 // If there's no filter, try to fetch data from cache. 1549 cached, err := CachedBugGroups(c, ns, accessLevel) 1550 if err != nil { 1551 log.Errorf(c, "failed to fetch from bug groups cache: %v", err) 1552 } else if cached != nil { 1553 return cached, nil 1554 } 1555 } 1556 bugs, err := loadVisibleBugs(c, ns, filter) 1557 if err != nil { 1558 return nil, err 1559 } 1560 managers, err := CachedManagerList(c, ns) 1561 if err != nil { 1562 return nil, err 1563 } 1564 return prepareBugGroups(c, bugs, managers, accessLevel, ns) 1565 } 1566 1567 func prepareBugGroups(c context.Context, bugs []*Bug, managers []string, 1568 accessLevel AccessLevel, ns string) ([]*uiBugGroup, error) { 1569 state, err := loadReportingState(c) 1570 if err != nil { 1571 return nil, err 1572 } 1573 groups := make(map[int][]*uiBug) 1574 bugMap := make(map[string]*uiBug) 1575 var dups []*Bug 1576 for _, bug := range bugs { 1577 if accessLevel < bug.sanitizeAccess(c, accessLevel) { 1578 continue 1579 } 1580 if bug.Status == BugStatusDup { 1581 dups = append(dups, bug) 1582 continue 1583 } 1584 uiBug := createUIBug(c, bug, state, managers) 1585 if len(uiBug.Commits) != 0 { 1586 // Don't show "fix pending" bugs on the main page. 1587 continue 1588 } 1589 bugMap[bug.keyHash(c)] = uiBug 1590 id := uiBug.ReportingIndex 1591 groups[id] = append(groups[id], uiBug) 1592 } 1593 for _, dup := range dups { 1594 bug := bugMap[dup.DupOf] 1595 if bug == nil { 1596 continue // this can be an invalid bug which we filtered above 1597 } 1598 mergeUIBug(c, bug, dup) 1599 } 1600 cfg := getNsConfig(c, ns) 1601 var uiGroups []*uiBugGroup 1602 for index, bugs := range groups { 1603 sort.Slice(bugs, func(i, j int) bool { 1604 if bugs[i].Namespace != bugs[j].Namespace { 1605 return bugs[i].Namespace < bugs[j].Namespace 1606 } 1607 if bugs[i].ClosedTime != bugs[j].ClosedTime { 1608 return bugs[i].ClosedTime.After(bugs[j].ClosedTime) 1609 } 1610 return bugs[i].ReportedTime.After(bugs[j].ReportedTime) 1611 }) 1612 caption, fragment := "", "" 1613 switch index { 1614 case len(cfg.Reporting) - 1: 1615 caption = "open" 1616 fragment = "open" 1617 default: 1618 reporting := &cfg.Reporting[index] 1619 caption = reporting.DisplayTitle 1620 fragment = reporting.Name 1621 } 1622 uiGroups = append(uiGroups, &uiBugGroup{ 1623 Now: timeNow(c), 1624 Caption: caption, 1625 Fragment: fragment, 1626 Namespace: ns, 1627 ShowIndex: index, 1628 Bugs: bugs, 1629 }) 1630 } 1631 sort.Slice(uiGroups, func(i, j int) bool { 1632 return uiGroups[i].ShowIndex > uiGroups[j].ShowIndex 1633 }) 1634 return uiGroups, nil 1635 } 1636 1637 func loadVisibleBugs(c context.Context, ns string, bugFilter *userBugFilter) ([]*Bug, error) { 1638 // Load open and dup bugs in in 2 separate queries. 1639 // Ideally we load them in one query with a suitable filter, 1640 // but unfortunately status values don't allow one query (<BugStatusFixed || >BugStatusInvalid). 1641 // Ideally we also have separate status for "dup of a closed bug" as we don't need to fetch them. 1642 // Potentially changing "dup" to "dup of a closed bug" can be done in background. 1643 // But 2 queries is still much faster than fetching all bugs and we can do this in parallel. 1644 errc := make(chan error) 1645 var dups []*Bug 1646 go func() { 1647 // Don't apply bugFilter to dups -- they need to be joined unconditionally. 1648 filter := func(query *db.Query) *db.Query { 1649 return query.Filter("Namespace=", ns). 1650 Filter("Status=", BugStatusDup) 1651 } 1652 var err error 1653 dups, _, err = loadAllBugs(c, filter) 1654 errc <- err 1655 }() 1656 filter := func(query *db.Query) *db.Query { 1657 return applyBugFilter( 1658 query.Filter("Namespace=", ns). 1659 Filter("Status<", BugStatusFixed), 1660 bugFilter, 1661 ) 1662 } 1663 bugs, _, err := loadAllBugs(c, filter) 1664 if err != nil { 1665 return nil, err 1666 } 1667 if err := <-errc; err != nil { 1668 return nil, err 1669 } 1670 var filteredBugs []*Bug 1671 for _, bug := range bugs { 1672 if bugFilter.MatchBug(bug) { 1673 filteredBugs = append(filteredBugs, bug) 1674 } 1675 } 1676 return append(filteredBugs, dups...), nil 1677 } 1678 1679 func fetchTerminalBugs(c context.Context, accessLevel AccessLevel, 1680 ns string, typ *TerminalBug, extraBugs []*Bug) (*uiBugGroup, *uiBugStats, error) { 1681 bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { 1682 return applyBugFilter( 1683 query.Filter("Namespace=", ns).Filter("Status=", typ.Status), 1684 typ.Filter, 1685 ) 1686 }) 1687 if err != nil { 1688 return nil, nil, err 1689 } 1690 bugs = append(bugs, extraBugs...) 1691 state, err := loadReportingState(c) 1692 if err != nil { 1693 return nil, nil, err 1694 } 1695 managers, err := CachedManagerList(c, ns) 1696 if err != nil { 1697 return nil, nil, err 1698 } 1699 sort.Slice(bugs, func(i, j int) bool { 1700 iFixed := bugs[i].Status == BugStatusFixed 1701 jFixed := bugs[j].Status == BugStatusFixed 1702 if iFixed != jFixed { 1703 // Not-yet-fully-patched bugs come first. 1704 return jFixed 1705 } 1706 return bugs[i].Closed.After(bugs[j].Closed) 1707 }) 1708 stats := &uiBugStats{} 1709 res := &uiBugGroup{ 1710 Now: timeNow(c), 1711 ShowPatch: typ.ShowPatch, 1712 ShowPatched: typ.ShowPatched, 1713 Namespace: ns, 1714 } 1715 for _, bug := range bugs { 1716 if accessLevel < bug.sanitizeAccess(c, accessLevel) { 1717 continue 1718 } 1719 if !typ.Filter.MatchBug(bug) { 1720 continue 1721 } 1722 uiBug := createUIBug(c, bug, state, managers) 1723 res.Bugs = append(res.Bugs, uiBug) 1724 stats.Record(bug, &bug.Reporting[uiBug.ReportingIndex]) 1725 } 1726 return res, stats, nil 1727 } 1728 1729 func applyBugFilter(query *db.Query, filter *userBugFilter) *db.Query { 1730 if filter == nil { 1731 return query 1732 } 1733 manager := filter.ManagerName() 1734 if len(filter.Labels) > 0 { 1735 // Take just the first one. 1736 label, value := splitLabel(filter.Labels[0]) 1737 query = query.Filter("Labels.Label=", string(label)) 1738 query = query.Filter("Labels.Value=", value) 1739 } else if manager != "" { 1740 query = query.Filter("HappenedOn=", manager) 1741 } 1742 return query 1743 } 1744 1745 func loadDupsForBug(c context.Context, r *http.Request, bug *Bug, state *ReportingState, managers []string) ( 1746 *uiBugGroup, error) { 1747 bugHash := bug.keyHash(c) 1748 var dups []*Bug 1749 _, err := db.NewQuery("Bug"). 1750 Filter("Status=", BugStatusDup). 1751 Filter("DupOf=", bugHash). 1752 GetAll(c, &dups) 1753 if err != nil { 1754 return nil, err 1755 } 1756 var results []*uiBug 1757 accessLevel := accessLevel(c, r) 1758 for _, dup := range dups { 1759 if accessLevel < dup.sanitizeAccess(c, accessLevel) { 1760 continue 1761 } 1762 results = append(results, createUIBug(c, dup, state, managers)) 1763 } 1764 group := &uiBugGroup{ 1765 Now: timeNow(c), 1766 Caption: "duplicates", 1767 ShowPatched: true, 1768 ShowStatus: true, 1769 Bugs: results, 1770 } 1771 return group, nil 1772 } 1773 1774 func loadSimilarBugsUI(c context.Context, r *http.Request, bug *Bug, state *ReportingState) (*uiBugGroup, error) { 1775 managers := make(map[string][]string) 1776 accessLevel := accessLevel(c, r) 1777 similarBugs, err := loadSimilarBugs(c, bug) 1778 if err != nil { 1779 return nil, err 1780 } 1781 var results []*uiBug 1782 for _, similar := range similarBugs { 1783 if accessLevel < similar.sanitizeAccess(c, accessLevel) { 1784 continue 1785 } 1786 if managers[similar.Namespace] == nil { 1787 mgrs, err := CachedManagerList(c, similar.Namespace) 1788 if err != nil { 1789 return nil, err 1790 } 1791 managers[similar.Namespace] = mgrs 1792 } 1793 results = append(results, createUIBug(c, similar, state, managers[similar.Namespace])) 1794 } 1795 group := &uiBugGroup{ 1796 Now: timeNow(c), 1797 ShowNamespace: true, 1798 ShowPatched: true, 1799 ShowStatus: true, 1800 Bugs: results, 1801 } 1802 return group, nil 1803 } 1804 1805 func closedBugStatus(bug *Bug, bugReporting *BugReporting) string { 1806 status := "" 1807 switch bug.Status { 1808 case BugStatusInvalid: 1809 switch bug.StatusReason { 1810 case dashapi.InvalidatedByNoActivity: 1811 fallthrough 1812 case dashapi.InvalidatedByRevokedRepro: 1813 status = "obsoleted due to no activity" 1814 default: 1815 status = "closed as invalid" 1816 } 1817 if bugReporting.Auto { 1818 status = "auto-" + status 1819 } 1820 case BugStatusFixed: 1821 status = "fixed" 1822 case BugStatusDup: 1823 status = "closed as dup" 1824 default: 1825 status = fmt.Sprintf("unknown (%v)", bug.Status) 1826 } 1827 return fmt.Sprintf("%v on %v", status, html.FormatTime(bug.Closed)) 1828 } 1829 1830 func createUIBug(c context.Context, bug *Bug, state *ReportingState, managers []string) *uiBug { 1831 reportingIdx, status, link := 0, "", "" 1832 var reported time.Time 1833 var err error 1834 if bug.Status == BugStatusOpen && state != nil { 1835 _, _, reportingIdx, status, link, err = needReport(c, "", state, bug) 1836 reported = bug.Reporting[reportingIdx].Reported 1837 if err != nil { 1838 status = err.Error() 1839 } 1840 if status == "" { 1841 status = "???" 1842 } 1843 } else { 1844 for i := range bug.Reporting { 1845 bugReporting := &bug.Reporting[i] 1846 if i == len(bug.Reporting)-1 || 1847 bug.Status == BugStatusInvalid && !bugReporting.Closed.IsZero() && 1848 bug.Reporting[i+1].Closed.IsZero() || 1849 (bug.Status == BugStatusFixed || bug.Status == BugStatusDup) && 1850 bugReporting.Closed.IsZero() { 1851 reportingIdx = i 1852 reported = bugReporting.Reported 1853 link = bugReporting.Link 1854 status = closedBugStatus(bug, bugReporting) 1855 break 1856 } 1857 } 1858 } 1859 creditEmail := "" 1860 if bug.Reporting[reportingIdx].ID != "" { 1861 // If the bug was never reported to the public, sanitizeReporting() would clear IDs 1862 // for non-authorized users. In such case, don't show CreditEmail at all. 1863 creditEmail, err = email.AddAddrContext(ownEmail(c), bug.Reporting[reportingIdx].ID) 1864 if err != nil { 1865 log.Errorf(c, "failed to generate credit email: %v", err) 1866 } 1867 } 1868 uiBug := &uiBug{ 1869 Namespace: bug.Namespace, 1870 Title: bug.displayTitle(), 1871 BisectCause: bug.BisectCause, 1872 BisectFix: bug.BisectFix, 1873 NumCrashes: bug.NumCrashes, 1874 FirstTime: bug.FirstTime, 1875 LastTime: bug.LastTime, 1876 ReportedTime: reported, 1877 ClosedTime: bug.Closed, 1878 ReproLevel: bug.ReproLevel, 1879 ReportingIndex: reportingIdx, 1880 Status: status, 1881 Link: bugExtLink(c, bug), 1882 ExternalLink: link, 1883 CreditEmail: creditEmail, 1884 NumManagers: len(managers), 1885 LastActivity: bug.LastActivity, 1886 Discussions: bug.discussionSummary(), 1887 ID: bug.keyHash(c), 1888 } 1889 for _, entry := range bug.Labels { 1890 uiBug.Labels = append(uiBug.Labels, makeBugLabelUI(c, bug, entry)) 1891 } 1892 updateBugBadness(c, uiBug) 1893 if len(bug.Commits) != 0 { 1894 for i, com := range bug.Commits { 1895 cfg := getNsConfig(c, bug.Namespace) 1896 info := bug.getCommitInfo(i) 1897 uiBug.Commits = append(uiBug.Commits, &uiCommit{ 1898 Hash: info.Hash, 1899 Title: com, 1900 Link: vcs.CommitLink(cfg.Repos[0].URL, info.Hash), 1901 Repo: cfg.Repos[0].URL, 1902 Branch: cfg.Repos[0].Branch, 1903 }) 1904 } 1905 for _, mgr := range managers { 1906 found := false 1907 for _, mgr1 := range bug.PatchedOn { 1908 if mgr == mgr1 { 1909 found = true 1910 break 1911 } 1912 } 1913 if found { 1914 uiBug.PatchedOn = append(uiBug.PatchedOn, mgr) 1915 } else { 1916 uiBug.MissingOn = append(uiBug.MissingOn, mgr) 1917 } 1918 } 1919 sort.Strings(uiBug.PatchedOn) 1920 sort.Strings(uiBug.MissingOn) 1921 } 1922 return uiBug 1923 } 1924 1925 func mergeUIBug(c context.Context, bug *uiBug, dup *Bug) { 1926 bug.NumCrashes += dup.NumCrashes 1927 bug.BisectCause = mergeBisectStatus(bug.BisectCause, dup.BisectCause) 1928 bug.BisectFix = mergeBisectStatus(bug.BisectFix, dup.BisectFix) 1929 if bug.LastTime.Before(dup.LastTime) { 1930 bug.LastTime = dup.LastTime 1931 } 1932 if bug.ReproLevel < dup.ReproLevel { 1933 bug.ReproLevel = dup.ReproLevel 1934 } 1935 updateBugBadness(c, bug) 1936 } 1937 1938 func mergeBisectStatus(a, b BisectStatus) BisectStatus { 1939 // The statuses are stored in the datastore, so we can't reorder them. 1940 // But if one of bisections is Yes, then we want to show Yes. 1941 bisectPriority := [bisectStatusLast]int{0, 1, 2, 6, 5, 4, 3} 1942 if bisectPriority[a] >= bisectPriority[b] { 1943 return a 1944 } 1945 return b 1946 } 1947 1948 func updateBugBadness(c context.Context, bug *uiBug) { 1949 bug.NumCrashesBad = bug.NumCrashes >= 10000 && timeNow(c).Sub(bug.LastTime) < 24*time.Hour 1950 } 1951 1952 func loadCrashesForBug(c context.Context, bug *Bug) ([]*uiCrash, template.HTML, error) { 1953 bugKey := bug.key(c) 1954 // We can have more than maxCrashes crashes, if we have lots of reproducers. 1955 crashes, _, err := queryCrashesForBug(c, bugKey, 2*maxCrashes()+200) 1956 if err != nil || len(crashes) == 0 { 1957 return nil, "", err 1958 } 1959 builds := make(map[string]*Build) 1960 var results []*uiCrash 1961 for _, crash := range crashes { 1962 build := builds[crash.BuildID] 1963 if build == nil { 1964 build, err = loadBuild(c, bug.Namespace, crash.BuildID) 1965 if err != nil { 1966 return nil, "", err 1967 } 1968 builds[crash.BuildID] = build 1969 } 1970 results = append(results, makeUICrash(c, crash, build)) 1971 } 1972 sampleReport, _, err := getText(c, textCrashReport, crashes[0].Report) 1973 if err != nil { 1974 return nil, "", err 1975 } 1976 sampleBuild := builds[crashes[0].BuildID] 1977 linkifiedReport := linkifyReport(sampleReport, sampleBuild.KernelRepo, sampleBuild.KernelCommit) 1978 return results, linkifiedReport, nil 1979 } 1980 1981 func linkifyReport(report []byte, repo, commit string) template.HTML { 1982 escaped := template.HTMLEscapeString(string(report)) 1983 return template.HTML(sourceFileRe.ReplaceAllStringFunc(escaped, func(match string) string { 1984 sub := sourceFileRe.FindStringSubmatch(match) 1985 line, _ := strconv.Atoi(sub[3]) 1986 url := vcs.FileLink(repo, commit, sub[2], line) 1987 return fmt.Sprintf("%v<a href='%v'>%v:%v</a>%v", sub[1], url, sub[2], sub[3], sub[4]) 1988 })) 1989 } 1990 1991 var sourceFileRe = regexp.MustCompile("( |\t|\n)([a-zA-Z0-9/_.-]+\\.(?:h|c|cc|cpp|s|S|go|rs)):([0-9]+)( |!|\\)|\t|\n)") 1992 1993 func makeUIAssets(build *Build, crash *Crash, forReport bool) []*uiAsset { 1994 var uiAssets []*uiAsset 1995 for _, asset := range createAssetList(build, crash, forReport) { 1996 uiAssets = append(uiAssets, &uiAsset{ 1997 Title: asset.Title, 1998 DownloadURL: asset.DownloadURL, 1999 }) 2000 } 2001 return uiAssets 2002 } 2003 2004 func makeUICrash(c context.Context, crash *Crash, build *Build) *uiCrash { 2005 ui := &uiCrash{ 2006 Title: crash.Title, 2007 Manager: crash.Manager, 2008 Time: crash.Time, 2009 Maintainers: strings.Join(crash.Maintainers, ", "), 2010 LogLink: textLink(textCrashLog, crash.Log), 2011 LogHasStrace: dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0, 2012 ReportLink: textLink(textCrashReport, crash.Report), 2013 ReproSyzLink: textLink(textReproSyz, crash.ReproSyz), 2014 ReproCLink: textLink(textReproC, crash.ReproC), 2015 ReproIsRevoked: crash.ReproIsRevoked, 2016 MachineInfoLink: textLink(textMachineInfo, crash.MachineInfo), 2017 Assets: makeUIAssets(build, crash, true), 2018 } 2019 if build != nil { 2020 ui.uiBuild = makeUIBuild(c, build, true) 2021 } 2022 return ui 2023 } 2024 2025 func makeUIBuild(c context.Context, build *Build, forReport bool) *uiBuild { 2026 return &uiBuild{ 2027 Time: build.Time, 2028 SyzkallerCommit: build.SyzkallerCommit, 2029 SyzkallerCommitLink: vcs.LogLink(vcs.SyzkallerRepo, build.SyzkallerCommit), 2030 SyzkallerCommitDate: build.SyzkallerCommitDate, 2031 KernelRepo: build.KernelRepo, 2032 KernelBranch: build.KernelBranch, 2033 KernelAlias: kernelRepoInfo(c, build).Alias, 2034 KernelCommit: build.KernelCommit, 2035 KernelCommitLink: vcs.LogLink(build.KernelRepo, build.KernelCommit), 2036 KernelCommitTitle: build.KernelCommitTitle, 2037 KernelCommitDate: build.KernelCommitDate, 2038 KernelConfigLink: textLink(textKernelConfig, build.KernelConfig), 2039 Assets: makeUIAssets(build, nil, forReport), 2040 } 2041 } 2042 2043 func loadRepos(c context.Context, ns string) ([]*uiRepo, error) { 2044 managers, _, err := loadNsManagerList(c, ns, nil) 2045 if err != nil { 2046 return nil, err 2047 } 2048 var buildKeys []*db.Key 2049 for _, mgr := range managers { 2050 if mgr.CurrentBuild != "" { 2051 buildKeys = append(buildKeys, buildKey(c, mgr.Namespace, mgr.CurrentBuild)) 2052 } 2053 } 2054 builds := make([]*Build, len(buildKeys)) 2055 err = db.GetMulti(c, buildKeys, builds) 2056 if err != nil { 2057 return nil, err 2058 } 2059 ret := []*uiRepo{} 2060 dedupRepos := map[string]bool{} 2061 for _, build := range builds { 2062 if build == nil { 2063 continue 2064 } 2065 repo := &uiRepo{ 2066 URL: build.KernelRepo, 2067 Branch: build.KernelBranch, 2068 } 2069 hash := repo.String() 2070 if dedupRepos[hash] { 2071 continue 2072 } 2073 dedupRepos[hash] = true 2074 ret = append(ret, repo) 2075 } 2076 sort.Slice(ret, func(i, j int) bool { 2077 if ret[i].URL != ret[j].URL { 2078 return ret[i].URL < ret[j].URL 2079 } 2080 return ret[i].Branch < ret[j].Branch 2081 }) 2082 return ret, nil 2083 } 2084 2085 func loadManagers(c context.Context, accessLevel AccessLevel, ns string, filter *userBugFilter) ([]*uiManager, error) { 2086 now := timeNow(c) 2087 date := timeDate(now) 2088 managers, managerKeys, err := loadNsManagerList(c, ns, filter) 2089 if err != nil { 2090 return nil, err 2091 } 2092 var buildKeys []*db.Key 2093 var statsKeys []*db.Key 2094 for i, mgr := range managers { 2095 if mgr.CurrentBuild != "" { 2096 buildKeys = append(buildKeys, buildKey(c, mgr.Namespace, mgr.CurrentBuild)) 2097 } 2098 if timeDate(mgr.LastAlive) == date { 2099 statsKeys = append(statsKeys, 2100 db.NewKey(c, "ManagerStats", "", int64(date), managerKeys[i])) 2101 } 2102 } 2103 builds := make([]*Build, len(buildKeys)) 2104 stats := make([]*ManagerStats, len(statsKeys)) 2105 coverAssets := map[string]Asset{} 2106 g, _ := errgroup.WithContext(context.Background()) 2107 g.Go(func() error { 2108 return db.GetMulti(c, buildKeys, builds) 2109 }) 2110 g.Go(func() error { 2111 return db.GetMulti(c, statsKeys, stats) 2112 }) 2113 g.Go(func() error { 2114 // Get the last coverage report asset for the last week. 2115 const maxDuration = time.Hour * 24 * 7 2116 var err error 2117 coverAssets, err = queryLatestManagerAssets(c, ns, dashapi.HTMLCoverageReport, maxDuration) 2118 return err 2119 }) 2120 err = g.Wait() 2121 if err != nil { 2122 return nil, fmt.Errorf("failed to query manager-related info: %w", err) 2123 } 2124 uiBuilds := make(map[string]*uiBuild) 2125 for _, build := range builds { 2126 uiBuilds[build.Namespace+"|"+build.ID] = makeUIBuild(c, build, true) 2127 } 2128 var fullStats []*ManagerStats 2129 for _, mgr := range managers { 2130 if timeDate(mgr.LastAlive) != date { 2131 fullStats = append(fullStats, &ManagerStats{}) 2132 continue 2133 } 2134 fullStats = append(fullStats, stats[0]) 2135 stats = stats[1:] 2136 } 2137 var results []*uiManager 2138 for i, mgr := range managers { 2139 stats := fullStats[i] 2140 link := mgr.Link 2141 if accessLevel < AccessUser { 2142 link = "" 2143 } 2144 uptime := mgr.CurrentUpTime 2145 if now.Sub(mgr.LastAlive) > 6*time.Hour { 2146 uptime = 0 2147 } 2148 // TODO: also display how fresh the coverage report is (to display it on 2149 // the main page -- this will reduce confusion). 2150 coverURL := "" 2151 if asset, ok := coverAssets[mgr.Name]; ok { 2152 coverURL = asset.DownloadURL 2153 } else if getConfig(c).CoverPath != "" { 2154 coverURL = getConfig(c).CoverPath + mgr.Name + ".html" 2155 } 2156 ui := &uiManager{ 2157 Now: timeNow(c), 2158 Namespace: mgr.Namespace, 2159 Name: mgr.Name, 2160 Link: link, 2161 PageLink: mgr.Namespace + "/manager/" + mgr.Name, 2162 CoverLink: coverURL, 2163 CurrentBuild: uiBuilds[mgr.Namespace+"|"+mgr.CurrentBuild], 2164 FailedBuildBugLink: bugLink(mgr.FailedBuildBug), 2165 FailedSyzBuildBugLink: bugLink(mgr.FailedSyzBuildBug), 2166 LastActive: mgr.LastAlive, 2167 CurrentUpTime: uptime, 2168 MaxCorpus: stats.MaxCorpus, 2169 MaxCover: stats.MaxCover, 2170 TotalFuzzingTime: stats.TotalFuzzingTime, 2171 TotalCrashes: stats.TotalCrashes, 2172 TotalExecs: stats.TotalExecs, 2173 TotalExecsBad: stats.TotalExecs == 0, 2174 } 2175 results = append(results, ui) 2176 } 2177 sort.Slice(results, func(i, j int) bool { 2178 if results[i].Namespace != results[j].Namespace { 2179 return results[i].Namespace < results[j].Namespace 2180 } 2181 return results[i].Name < results[j].Name 2182 }) 2183 return results, nil 2184 } 2185 2186 func loadNsManagerList(c context.Context, ns string, filter *userBugFilter) ([]*Manager, []*db.Key, error) { 2187 managers, keys, err := loadAllManagers(c, ns) 2188 if err != nil { 2189 return nil, nil, err 2190 } 2191 var filtered []*Manager 2192 var filteredKeys []*db.Key 2193 for i, mgr := range managers { 2194 cfg := getNsConfig(c, mgr.Namespace) 2195 if ns == "" && cfg.Decommissioned { 2196 continue 2197 } 2198 if !filter.MatchManagerName(mgr.Name) { 2199 continue 2200 } 2201 filtered = append(filtered, mgr) 2202 filteredKeys = append(filteredKeys, keys[i]) 2203 } 2204 return filtered, filteredKeys, nil 2205 } 2206 2207 func loadRecentJobs(c context.Context) ([]*uiJob, error) { 2208 var jobs []*Job 2209 keys, err := db.NewQuery("Job"). 2210 Order("-Created"). 2211 Limit(80). 2212 GetAll(c, &jobs) 2213 if err != nil { 2214 return nil, err 2215 } 2216 return getUIJobs(c, keys, jobs), nil 2217 } 2218 2219 func loadPendingJobs(c context.Context) ([]*uiJob, error) { 2220 var jobs []*Job 2221 keys, err := db.NewQuery("Job"). 2222 Filter("Started=", time.Time{}). 2223 Limit(50). 2224 GetAll(c, &jobs) 2225 if err != nil { 2226 return nil, err 2227 } 2228 return getUIJobs(c, keys, jobs), nil 2229 } 2230 2231 func loadRunningJobs(c context.Context) ([]*uiJob, error) { 2232 var jobs []*Job 2233 keys, err := db.NewQuery("Job"). 2234 Filter("IsRunning=", true). 2235 Limit(50). 2236 GetAll(c, &jobs) 2237 if err != nil { 2238 return nil, err 2239 } 2240 return getUIJobs(c, keys, jobs), nil 2241 } 2242 2243 func loadJobsOfType(c context.Context, t JobType) ([]*uiJob, error) { 2244 var jobs []*Job 2245 keys, err := db.NewQuery("Job"). 2246 Filter("Type=", t). 2247 Order("-Finished"). 2248 Limit(50). 2249 GetAll(c, &jobs) 2250 if err != nil { 2251 return nil, err 2252 } 2253 return getUIJobs(c, keys, jobs), nil 2254 } 2255 2256 func getUIJobs(c context.Context, keys []*db.Key, jobs []*Job) []*uiJob { 2257 var results []*uiJob 2258 for i, job := range jobs { 2259 results = append(results, makeUIJob(c, job, keys[i], nil, nil, nil)) 2260 } 2261 return results 2262 } 2263 2264 func loadTestPatchJobs(c context.Context, bug *Bug) ([]*uiJob, error) { 2265 bugKey := bug.key(c) 2266 2267 var jobs []*Job 2268 keys, err := db.NewQuery("Job"). 2269 Ancestor(bugKey). 2270 Filter("Type=", JobTestPatch). 2271 Filter("Finished>=", time.Time{}). 2272 Order("-Finished"). 2273 GetAll(c, &jobs) 2274 if err != nil { 2275 return nil, err 2276 } 2277 const maxAutomaticJobs = 10 2278 autoJobsLeft := maxAutomaticJobs 2279 var results []*uiJob 2280 for i, job := range jobs { 2281 if job.User == "" { 2282 if autoJobsLeft == 0 { 2283 continue 2284 } 2285 autoJobsLeft-- 2286 } 2287 if job.TreeOrigin && !job.Finished.IsZero() { 2288 continue 2289 } 2290 var build *Build 2291 if job.BuildID != "" { 2292 if build, err = loadBuild(c, bug.Namespace, job.BuildID); err != nil { 2293 return nil, err 2294 } 2295 } 2296 results = append(results, makeUIJob(c, job, keys[i], nil, nil, build)) 2297 } 2298 return results, nil 2299 } 2300 2301 func makeUIJob(c context.Context, job *Job, jobKey *db.Key, bug *Bug, crash *Crash, build *Build) *uiJob { 2302 ui := &uiJob{ 2303 JobInfo: makeJobInfo(c, job, jobKey, bug, build, crash), 2304 InvalidateJobLink: invalidateJobLink(c, job, jobKey, false), 2305 RestartJobLink: invalidateJobLink(c, job, jobKey, true), 2306 FixCandidate: job.IsCrossTree(), 2307 } 2308 if crash != nil { 2309 ui.Crash = makeUICrash(c, crash, build) 2310 } 2311 return ui 2312 } 2313 2314 func invalidateJobLink(c context.Context, job *Job, jobKey *db.Key, restart bool) string { 2315 if !user.IsAdmin(c) { 2316 return "" 2317 } 2318 if job.InvalidatedBy != "" || job.Finished.IsZero() { 2319 return "" 2320 } 2321 if job.Type != JobBisectCause && job.Type != JobBisectFix { 2322 return "" 2323 } 2324 params := url.Values{} 2325 params.Add("action", "invalidate_bisection") 2326 params.Add("key", jobKey.Encode()) 2327 if restart { 2328 params.Add("restart", "1") 2329 } 2330 return "/admin?" + params.Encode() 2331 } 2332 2333 func formatLogLine(line string) string { 2334 const maxLineLen = 1000 2335 2336 line = strings.Replace(line, "\n", " ", -1) 2337 line = strings.Replace(line, "\r", "", -1) 2338 if len(line) > maxLineLen { 2339 line = line[:maxLineLen] 2340 line += "..." 2341 } 2342 return line + "\n" 2343 } 2344 2345 func fetchErrorLogs(c context.Context) ([]byte, error) { 2346 if !appengine.IsAppEngine() { 2347 return nil, nil 2348 } 2349 2350 const ( 2351 maxLines = 100 2352 ) 2353 projID := os.Getenv("GOOGLE_CLOUD_PROJECT") 2354 2355 adminClient, err := logadmin.NewClient(c, projID) 2356 if err != nil { 2357 return nil, fmt.Errorf("failed to create the logging client: %w", err) 2358 } 2359 defer adminClient.Close() 2360 2361 lastWeek := time.Now().Add(-1 * 7 * 24 * time.Hour).Format(time.RFC3339) 2362 iter := adminClient.Entries(c, 2363 logadmin.Filter( 2364 // We filter our instances.delete errors as false positives. Delete event happens every second. 2365 fmt.Sprintf(`(NOT protoPayload.methodName:v1.compute.instances.delete) AND timestamp > "%s" AND severity>="ERROR"`, 2366 lastWeek)), 2367 logadmin.NewestFirst(), 2368 ) 2369 2370 var entries []*logging.Entry 2371 for len(entries) < maxLines { 2372 entry, err := iter.Next() 2373 if err == iterator.Done { 2374 break 2375 } 2376 if err != nil { 2377 return nil, err 2378 } 2379 entries = append(entries, entry) 2380 } 2381 2382 var lines []string 2383 for _, entry := range entries { 2384 requestLog, isRequestLog := entry.Payload.(*proto.RequestLog) 2385 if isRequestLog { 2386 for _, logLine := range requestLog.Line { 2387 if logLine.GetSeverity() < ltype.LogSeverity_ERROR { 2388 continue 2389 } 2390 line := fmt.Sprintf("%v: %v %v %v \"%v\"", 2391 entry.Timestamp.Format(time.Stamp), 2392 requestLog.GetStatus(), 2393 requestLog.GetMethod(), 2394 requestLog.GetResource(), 2395 logLine.GetLogMessage()) 2396 lines = append(lines, formatLogLine(line)) 2397 } 2398 } else { 2399 line := fmt.Sprintf("%v: %v", 2400 entry.Timestamp.Format(time.Stamp), 2401 entry.Payload) 2402 lines = append(lines, formatLogLine(line)) 2403 } 2404 } 2405 2406 buf := new(bytes.Buffer) 2407 for i := len(lines) - 1; i >= 0; i-- { 2408 buf.WriteString(lines[i]) 2409 } 2410 return buf.Bytes(), nil 2411 } 2412 2413 func (j *bugJob) ui(c context.Context) (*uiJob, error) { 2414 err := j.load(c) 2415 if err != nil { 2416 return nil, err 2417 } 2418 return makeUIJob(c, j.job, j.key, j.bug, j.crash, j.build), nil 2419 } 2420 2421 func (b *bugJobs) uiAll(c context.Context) ([]*uiJob, error) { 2422 var ret []*uiJob 2423 for _, j := range b.all() { 2424 obj, err := j.ui(c) 2425 if err != nil { 2426 return nil, err 2427 } 2428 ret = append(ret, obj) 2429 } 2430 return ret, nil 2431 } 2432 2433 func (b *bugJobs) uiBestBisection(c context.Context) (*uiJob, error) { 2434 j := b.bestBisection() 2435 if j == nil { 2436 return nil, nil 2437 } 2438 return j.ui(c) 2439 } 2440 2441 func (b *bugJobs) uiBestFixCandidate(c context.Context) (*uiJob, error) { 2442 j := b.bestFixCandidate() 2443 if j == nil { 2444 return nil, nil 2445 } 2446 return j.ui(c) 2447 } 2448 2449 // bugExtLink should be preferred to bugLink since it provides a URL that's more consistent with 2450 // links from email addresses. 2451 func bugExtLink(c context.Context, bug *Bug) string { 2452 _, bugReporting, _, _, _ := currentReporting(c, bug) 2453 if bugReporting == nil || bugReporting.ID == "" { 2454 return bugLink(bug.keyHash(c)) 2455 } 2456 return "/bug?extid=" + bugReporting.ID 2457 } 2458 2459 // bugLink should only be used when it's too inconvenient to actually load the bug from the DB. 2460 func bugLink(id string) string { 2461 if id == "" { 2462 return "" 2463 } 2464 return "/bug?id=" + id 2465 }