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  }