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