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