syz-manager: modernize web UI

1. Use dashboard style.
2. Allow sorting of tables.
3. Show old crashes in grey.
4. Use tables instead of text output for more pages.
5. Show corpus inputs on a separate page to allow copy-pasting.
6. Use standard JS sorting instead of custom bubble sort (much faster).
7. Fix off-by one in table sorting.

Fixes #694
This commit is contained in:
Dmitry Vyukov 2018-11-22 14:27:37 +01:00
parent 582e1f0d1d
commit 13ab4beeef
9 changed files with 525 additions and 271 deletions

View File

@ -175,7 +175,7 @@ generate: generate_go generate_sys
$(MAKE) format
generate_go: bin/syz-sysgen format_cpp
$(GO) generate ./pkg/csource ./executor ./pkg/ifuzz ./pkg/build
$(GO) generate ./pkg/csource ./executor ./pkg/ifuzz ./pkg/build ./pkg/html
generate_sys: bin/syz-sysgen
bin/syz-sysgen

View File

@ -5,12 +5,9 @@ package dash
import (
"bytes"
"fmt"
"html/template"
"net/http"
"time"
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/html"
"golang.org/x/net/context"
"google.golang.org/appengine"
"google.golang.org/appengine/log"
@ -90,84 +87,4 @@ func commonHeader(c context.Context, r *http.Request) *uiHeader {
return h
}
func formatTime(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format("2006/01/02 15:04")
}
func formatClock(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format("15:04")
}
func formatDuration(d time.Duration) string {
if d == 0 {
return ""
}
days := int(d / (24 * time.Hour))
hours := int(d / time.Hour % 24)
mins := int(d / time.Minute % 60)
if days >= 10 {
return fmt.Sprintf("%vd", days)
} else if days != 0 {
return fmt.Sprintf("%vd%02vh", days, hours)
} else if hours != 0 {
return fmt.Sprintf("%vh%02vm", hours, mins)
}
return fmt.Sprintf("%vm", mins)
}
func formatLateness(now, t time.Time) string {
if t.IsZero() {
return "never"
}
d := now.Sub(t)
if d < 5*time.Minute {
return "now"
}
return formatDuration(d)
}
func formatReproLevel(l dashapi.ReproLevel) string {
switch l {
case ReproLevelSyz:
return "syz"
case ReproLevelC:
return "C"
default:
return ""
}
}
func formatStat(v int64) string {
if v == 0 {
return ""
}
return fmt.Sprint(v)
}
func formatShortHash(v string) string {
const hashLen = 8
if len(v) <= hashLen {
return v
}
return v[:hashLen]
}
var (
templates = template.Must(template.New("").Funcs(templateFuncs).ParseGlob("*.html"))
templateFuncs = template.FuncMap{
"formatTime": formatTime,
"formatClock": formatClock,
"formatDuration": formatDuration,
"formatLateness": formatLateness,
"formatReproLevel": formatReproLevel,
"formatStat": formatStat,
"formatShortHash": formatShortHash,
}
)
var templates = html.CreateGlob("*.html")

View File

@ -14,6 +14,7 @@ import (
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/email"
"github.com/google/syzkaller/pkg/html"
"golang.org/x/net/context"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/log"
@ -570,7 +571,7 @@ func createUIBug(c context.Context, bug *Bug, state *ReportingState, managers []
default:
status = fmt.Sprintf("unknown (%v)", bug.Status)
}
status = fmt.Sprintf("%v on %v", status, formatTime(bug.Closed))
status = fmt.Sprintf("%v on %v", status, html.FormatTime(bug.Closed))
break
}
}

View File

@ -14,6 +14,7 @@ import (
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/email"
"github.com/google/syzkaller/pkg/html"
"golang.org/x/net/context"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/log"
@ -97,7 +98,7 @@ func needReport(c context.Context, typ string, state *ReportingState, bug *Bug)
if !bugReporting.Reported.IsZero() && bugReporting.ReproLevel >= bug.ReproLevel {
status = fmt.Sprintf("%v: reported%v on %v",
reporting.DisplayTitle, reproStr(bugReporting.ReproLevel),
formatTime(bugReporting.Reported))
html.FormatTime(bugReporting.Reported))
reporting, bugReporting = nil, nil
return
}
@ -149,7 +150,7 @@ func needReport(c context.Context, typ string, state *ReportingState, bug *Bug)
status = fmt.Sprintf("%v: ready to report", reporting.DisplayTitle)
if !bugReporting.Reported.IsZero() {
status += fmt.Sprintf(" (reported%v on %v)",
reproStr(bugReporting.ReproLevel), formatTime(bugReporting.Reported))
reproStr(bugReporting.ReproLevel), html.FormatTime(bugReporting.Reported))
}
return
}

View File

@ -3,28 +3,22 @@
function sortTable(item, colName, conv, desc = false) {
table = item.parentNode.parentNode.parentNode;
rows = table.getElementsByTagName("tr");
rows = table.rows;
col = findColumnByName(rows[0].getElementsByTagName("th"), colName);
values = new Array;
values = [];
for (i = 1; i < rows.length; i++)
values[i] = conv(rows[i].getElementsByTagName("td")[col].textContent);
values.push([conv(rows[i].getElementsByTagName("td")[col].textContent), rows[i]]);
if (desc)
desc = !isSorted(values.slice().reverse())
else
desc = isSorted(values);
do {
changed = false;
for (i = 1; i < values.length - 1; i++) {
v0 = values[i];
v1 = values[i + 1];
if (desc && v0 >= v1 || !desc && v0 <= v1)
continue;
changed = true;
values[i] = v1;
values[i + 1] = v0;
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
}
} while (changed);
values.sort(function(a, b) {
if (a[0] == b[0]) return 0;
if (desc && a[0] > b[0] || !desc && a[0] < b[0]) return -1;
return 1;
});
for (i = 0; i < values.length; i++)
table.appendChild(values[i][1]);
return false;
}
@ -37,8 +31,8 @@ function findColumnByName(headers, colName) {
}
function isSorted(values) {
for (i = 1; i < rows.length - 1; i++) {
if (values[i] > values[i + 1])
for (i = 0; i < values.length - 1; i++) {
if (values[i][0] > values[i + 1][0])
return false;
}
return true;
@ -46,6 +40,7 @@ function isSorted(values) {
function textSort(v) { return v.toLowerCase(); }
function numSort(v) { return -parseInt(v); }
function floatSort(v) { return -parseFloat(v); }
function reproSort(v) { return v == "C" ? 0 : v == "syz" ? 1 : 2; }
function patchedSort(v) { return v == "" ? -1 : parseInt(v); }

View File

@ -122,11 +122,27 @@ table td, table th {
text-align: right;
}
.list_table .stat_name {
width: 150pt;
max-width: 150pt;
font-family: monospace;
}
.list_table .stat_value {
width: 100pt;
max-width: 100pt;
font-family: monospace;
}
.bad {
color: #f00;
font-weight: bold;
}
.inactive {
color: #888;
}
.plain {
text-decoration: none;
}

221
pkg/html/generated.go Normal file
View File

@ -0,0 +1,221 @@
package html
const style = `
#topbar {
padding: 5px 10px;
background: #E0EBF5;
}
#topbar a {
color: #375EAB;
text-decoration: none;
}
h1, h2, h3, h4 {
margin: 0;
padding: 0;
color: #375EAB;
font-weight: bold;
}
table {
border: 1px solid #ccc;
margin: 20px 5px;
border-collapse: collapse;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
table caption {
font-weight: bold;
}
table td, table th {
vertical-align: top;
padding: 2px 8px;
text-overflow: ellipsis;
overflow: hidden;
}
.position_table {
border: 0px;
margin: 0px;
width: 100%;
border-collapse: collapse;
}
.position_table td, .position_table tr {
vertical-align: center;
padding: 0px;
}
.position_table .search {
text-align: right;
}
.list_table td, .list_table th {
border-left: 1px solid #ccc;
}
.list_table th {
background: #F4F4F4;
}
.list_table tr:nth-child(2n+1) {
background: #F4F4F4;
}
.list_table tr:hover {
background: #ffff99;
}
.list_table .namespace {
width: 100pt;
max-width: 100pt;
}
.list_table .title {
width: 350pt;
max-width: 350pt;
}
.list_table .tag {
font-family: monospace;
font-size: 8pt;
width: 40pt;
max-width: 40pt;
}
.list_table .opts {
width: 40pt;
max-width: 40pt;
}
.list_table .status {
width: 250pt;
max-width: 250pt;
}
.list_table .patched {
width: 60pt;
max-width: 60pt;
text-align: center;
}
.list_table .kernel {
width: 60pt;
max-width: 60pt;
}
.list_table .maintainers {
width: 150pt;
max-width: 150pt;
}
.list_table .result {
width: 60pt;
max-width: 60pt;
}
.list_table .stat {
width: 50pt;
max-width: 50pt;
font-family: monospace;
text-align: right;
}
.list_table .stat_name {
width: 150pt;
max-width: 150pt;
font-family: monospace;
}
.list_table .stat_value {
width: 100pt;
max-width: 100pt;
font-family: monospace;
}
.bad {
color: #f00;
font-weight: bold;
}
.inactive {
color: #888;
}
.plain {
text-decoration: none;
}
textarea {
width:100%;
font-family: monospace;
}
`
const js = `
// Copyright 2018 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
function sortTable(item, colName, conv, desc = false) {
table = item.parentNode.parentNode.parentNode;
rows = table.rows;
col = findColumnByName(rows[0].getElementsByTagName("th"), colName);
values = [];
for (i = 1; i < rows.length; i++)
values.push([conv(rows[i].getElementsByTagName("td")[col].textContent), rows[i]]);
if (desc)
desc = !isSorted(values.slice().reverse())
else
desc = isSorted(values);
values.sort(function(a, b) {
if (a[0] == b[0]) return 0;
if (desc && a[0] > b[0] || !desc && a[0] < b[0]) return -1;
return 1;
});
for (i = 0; i < values.length; i++)
table.appendChild(values[i][1]);
return false;
}
function findColumnByName(headers, colName) {
for (i = 0; i < headers.length; i++) {
if (headers[i].textContent == colName)
return i;
}
return 0;
}
function isSorted(values) {
for (i = 0; i < values.length - 1; i++) {
if (values[i][0] > values[i + 1][0])
return false;
}
return true;
}
function textSort(v) { return v.toLowerCase(); }
function numSort(v) { return -parseInt(v); }
function floatSort(v) { return -parseFloat(v); }
function reproSort(v) { return v == "C" ? 0 : v == "syz" ? 1 : 2; }
function patchedSort(v) { return v == "" ? -1 : parseInt(v); }
function timeSort(v) {
if (v == "now")
return 0;
m = v.indexOf('m');
h = v.indexOf('h');
d = v.indexOf('d');
if (m > 0 && h < 0)
return parseInt(v);
if (h > 0 && m > 0)
return parseInt(v) * 60 + parseInt(v.substring(h + 1));
if (d > 0 && h > 0)
return parseInt(v) * 60 * 24 + parseInt(v.substring(d + 1)) * 60;
if (d > 0)
return parseInt(v) * 60 * 24;
return 1000000000;
}
`

110
pkg/html/html.go Normal file
View File

@ -0,0 +1,110 @@
// Copyright 2018 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
//go:generate bash -c "echo '// AUTOGENERATED FILE' > generated.go"
//go:generate bash -c "echo 'package html' > generated.go"
//go:generate bash -c "echo 'const style = `' >> generated.go"
//go:generate bash -c "cat ../../dashboard/app/static/style.css >> generated.go"
//go:generate bash -c "echo '`' >> generated.go"
//go:generate bash -c "echo 'const js = `' >> generated.go"
//go:generate bash -c "cat ../../dashboard/app/static/common.js >> generated.go"
//go:generate bash -c "echo '`' >> generated.go"
package html
import (
"fmt"
"html/template"
"strings"
"time"
"github.com/google/syzkaller/dashboard/dashapi"
)
func CreatePage(page string) *template.Template {
const headTempl = `<style type="text/css" media="screen">%v</style><script>%v</script>`
page = strings.Replace(page, "{{HEAD}}", fmt.Sprintf(headTempl, style, js), 1)
return template.Must(template.New("").Funcs(funcs).Parse(page))
}
func CreateGlob(glob string) *template.Template {
return template.Must(template.New("").Funcs(funcs).ParseGlob(glob))
}
var funcs = template.FuncMap{
"formatTime": FormatTime,
"formatClock": formatClock,
"formatDuration": formatDuration,
"formatLateness": formatLateness,
"formatReproLevel": formatReproLevel,
"formatStat": formatStat,
"formatShortHash": formatShortHash,
}
func FormatTime(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format("2006/01/02 15:04")
}
func formatClock(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format("15:04")
}
func formatDuration(d time.Duration) string {
if d == 0 {
return ""
}
days := int(d / (24 * time.Hour))
hours := int(d / time.Hour % 24)
mins := int(d / time.Minute % 60)
if days >= 10 {
return fmt.Sprintf("%vd", days)
} else if days != 0 {
return fmt.Sprintf("%vd%02vh", days, hours)
} else if hours != 0 {
return fmt.Sprintf("%vh%02vm", hours, mins)
}
return fmt.Sprintf("%vm", mins)
}
func formatLateness(now, t time.Time) string {
if t.IsZero() {
return "never"
}
d := now.Sub(t)
if d < 5*time.Minute {
return "now"
}
return formatDuration(d)
}
func formatReproLevel(l dashapi.ReproLevel) string {
switch l {
case dashapi.ReproLevelSyz:
return "syz"
case dashapi.ReproLevelC:
return "C"
default:
return ""
}
}
func formatStat(v int64) string {
if v == 0 {
return ""
}
return fmt.Sprint(v)
}
func formatShortHash(v string) string {
const hashLen = 8
if len(v) <= hashLen {
return v
}
return v[:hashLen]
}

View File

@ -6,7 +6,6 @@ package main
import (
"bufio"
"fmt"
"html/template"
"io"
"io/ioutil"
"net"
@ -21,13 +20,12 @@ import (
"time"
"github.com/google/syzkaller/pkg/cover"
"github.com/google/syzkaller/pkg/html"
"github.com/google/syzkaller/pkg/log"
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/prog"
)
const dateFormat = "Jan 02 2006 15:04:05 MST"
func (mgr *Manager) initHTTP() {
http.HandleFunc("/", mgr.httpSummary)
http.HandleFunc("/syscalls", mgr.httpSyscalls)
@ -38,6 +36,7 @@ func (mgr *Manager) initHTTP() {
http.HandleFunc("/file", mgr.httpFile)
http.HandleFunc("/report", mgr.httpReport)
http.HandleFunc("/rawcover", mgr.httpRawCover)
http.HandleFunc("/input", mgr.httpInput)
// Browsers like to request this, without special handler this goes to / handler.
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {})
@ -83,8 +82,9 @@ func (mgr *Manager) httpSyscalls(w http.ResponseWriter, r *http.Request) {
Cover: len(cc.cov),
})
}
sort.Sort(UICallTypeArray(data.Calls))
sort.Slice(data.Calls, func(i, j int) bool {
return data.Calls[i].Name < data.Calls[j].Name
})
if err := syscallsTemplate.Execute(w, data); err != nil {
http.Error(w, fmt.Sprintf("failed to execute template: %v", err),
http.StatusInternalServerError)
@ -104,7 +104,7 @@ func (mgr *Manager) collectStats() []UIStat {
stats := []UIStat{
{Name: "uptime", Value: fmt.Sprint(time.Since(mgr.startTime) / 1e9 * 1e9)},
{Name: "fuzzing", Value: fmt.Sprint(mgr.fuzzingTime / 60e9 * 60e9)},
{Name: "corpus", Value: fmt.Sprint(len(mgr.corpus))},
{Name: "corpus", Value: fmt.Sprint(len(mgr.corpus)), Link: "/corpus"},
{Name: "triage queue", Value: fmt.Sprint(len(mgr.candidates))},
{Name: "cover", Value: fmt.Sprint(len(mgr.corpusCover)), Link: "/cover"},
{Name: "signal", Value: fmt.Sprint(mgr.corpusSignal.Len())},
@ -124,7 +124,9 @@ func (mgr *Manager) collectStats() []UIStat {
intStats := convertStats(mgr.stats.all(), secs)
intStats = append(intStats, convertStats(mgr.fuzzerStats, secs)...)
sort.Sort(UIStatArray(intStats))
sort.Slice(intStats, func(i, j int) bool {
return intStats[i].Name < intStats[j].Name
})
stats = append(stats, intStats...)
return stats
}
@ -164,7 +166,7 @@ func (mgr *Manager) collectSyscallInfo() map[string]*CallCov {
func (mgr *Manager) httpCrash(w http.ResponseWriter, r *http.Request) {
crashID := r.FormValue("id")
crash := readCrash(mgr.cfg.Workdir, crashID, nil, true)
crash := readCrash(mgr.cfg.Workdir, crashID, nil, mgr.startTime, true)
if crash == nil {
http.Error(w, fmt.Sprintf("failed to read crash info"), http.StatusInternalServerError)
return
@ -179,10 +181,11 @@ func (mgr *Manager) httpCorpus(w http.ResponseWriter, r *http.Request) {
mgr.mu.Lock()
defer mgr.mu.Unlock()
var data []UIInput
call := r.FormValue("call")
data := UICorpus{
Call: r.FormValue("call"),
}
for sig, inp := range mgr.corpus {
if call != inp.Call {
if data.Call != "" && data.Call != inp.Call {
continue
}
p, err := mgr.target.Deserialize(inp.Prog)
@ -190,14 +193,19 @@ func (mgr *Manager) httpCorpus(w http.ResponseWriter, r *http.Request) {
http.Error(w, fmt.Sprintf("failed to deserialize program: %v", err), http.StatusInternalServerError)
return
}
data = append(data, UIInput{
Short: p.String(),
Full: string(inp.Prog),
Cover: len(inp.Cover),
data.Inputs = append(data.Inputs, &UIInput{
Sig: sig,
Short: p.String(),
Cover: len(inp.Cover),
})
}
sort.Sort(UIInputArray(data))
sort.Slice(data.Inputs, func(i, j int) bool {
a, b := data.Inputs[i], data.Inputs[j]
if a.Cover != b.Cover {
return a.Cover > b.Cover
}
return a.Short < b.Short
})
if err := corpusTemplate.Execute(w, data); err != nil {
http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError)
@ -308,7 +316,9 @@ func (mgr *Manager) httpPrio(w http.ResponseWriter, r *http.Request) {
for i, p := range prios[idx] {
data.Prios = append(data.Prios, UIPrio{mgr.target.Syscalls[i].Name, p})
}
sort.Sort(UIPrioArray(data.Prios))
sort.Slice(data.Prios, func(i, j int) bool {
return data.Prios[i].Prio > data.Prios[j].Prio
})
if err := prioTemplate.Execute(w, data); err != nil {
http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError)
@ -333,6 +343,18 @@ func (mgr *Manager) httpFile(w http.ResponseWriter, r *http.Request) {
io.Copy(w, f)
}
func (mgr *Manager) httpInput(w http.ResponseWriter, r *http.Request) {
mgr.mu.Lock()
defer mgr.mu.Unlock()
inp, ok := mgr.corpus[r.FormValue("sig")]
if !ok {
http.Error(w, "can't find the input", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write(inp.Prog)
}
func (mgr *Manager) httpReport(w http.ResponseWriter, r *http.Request) {
mgr.mu.Lock()
defer mgr.mu.Unlock()
@ -413,16 +435,18 @@ func (mgr *Manager) collectCrashes(workdir string) ([]*UICrashType, error) {
}
var crashTypes []*UICrashType
for _, dir := range dirs {
crash := readCrash(workdir, dir, repros, false)
crash := readCrash(workdir, dir, repros, mgr.startTime, false)
if crash != nil {
crashTypes = append(crashTypes, crash)
}
}
sort.Sort(UICrashTypeArray(crashTypes))
sort.Slice(crashTypes, func(i, j int) bool {
return strings.ToLower(crashTypes[i].Description) < strings.ToLower(crashTypes[j].Description)
})
return crashTypes, nil
}
func readCrash(workdir, dir string, repros map[string]bool, full bool) *UICrashType {
func readCrash(workdir, dir string, repros map[string]bool, start time.Time, full bool) *UICrashType {
if len(dir) != 40 {
return nil
}
@ -478,7 +502,7 @@ func readCrash(workdir, dir string, repros map[string]bool, full bool) *UICrashT
crash.Log = filepath.Join("crashes", dir, "log"+index)
if stat, err := os.Stat(filepath.Join(workdir, crash.Log)); err == nil {
crash.Time = stat.ModTime()
crash.TimeStr = crash.Time.Format(dateFormat)
crash.Active = crash.Time.After(start)
}
tag, _ := ioutil.ReadFile(filepath.Join(crashdir, dir, "tag"+index))
crash.Tag = string(tag)
@ -487,13 +511,16 @@ func readCrash(workdir, dir string, repros map[string]bool, full bool) *UICrashT
crash.Report = reportFile
}
}
sort.Sort(UICrashArray(crashes))
sort.Slice(crashes, func(i, j int) bool {
return crashes[i].Time.After(crashes[j].Time)
})
}
triaged := reproStatus(hasRepro, hasCRepro, repros[desc], reproAttempts >= maxReproAttempts)
return &UICrashType{
Description: desc,
LastTime: modTime.Format(dateFormat),
LastTime: modTime,
Active: modTime.After(start),
ID: dir,
Count: len(crashes),
Triaged: triaged,
@ -537,7 +564,8 @@ type UISyscallsData struct {
type UICrashType struct {
Description string
LastTime string
LastTime time.Time
Active bool
ID string
Count int
Triaged string
@ -545,12 +573,12 @@ type UICrashType struct {
}
type UICrash struct {
Index int
Time time.Time
TimeStr string
Log string
Report string
Tag string
Index int
Time time.Time
Active bool
Log string
Report string
Tag string
}
type UIStat struct {
@ -565,84 +593,57 @@ type UICallType struct {
Cover int
}
type UIInput struct {
Short string
Full string
Calls int
Cover int
Sig string
type UICorpus struct {
Call string
Inputs []*UIInput
}
type UICallTypeArray []UICallType
type UIInput struct {
Sig string
Short string
Cover int
}
func (a UICallTypeArray) Len() int { return len(a) }
func (a UICallTypeArray) Less(i, j int) bool { return a[i].Name < a[j].Name }
func (a UICallTypeArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type UIInputArray []UIInput
func (a UIInputArray) Len() int { return len(a) }
func (a UIInputArray) Less(i, j int) bool { return a[i].Cover > a[j].Cover }
func (a UIInputArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type UIStatArray []UIStat
func (a UIStatArray) Len() int { return len(a) }
func (a UIStatArray) Less(i, j int) bool { return a[i].Name < a[j].Name }
func (a UIStatArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type UICrashTypeArray []*UICrashType
func (a UICrashTypeArray) Len() int { return len(a) }
func (a UICrashTypeArray) Less(i, j int) bool { return a[i].Description < a[j].Description }
func (a UICrashTypeArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type UICrashArray []*UICrash
func (a UICrashArray) Len() int { return len(a) }
func (a UICrashArray) Less(i, j int) bool { return a[i].Time.After(a[j].Time) }
func (a UICrashArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
var summaryTemplate = template.Must(template.New("").Parse(addStyle(`
var summaryTemplate = html.CreatePage(`
<!doctype html>
<html>
<head>
<title>{{.Name }} syzkaller</title>
{{STYLE}}
{{HEAD}}
</head>
<body>
<b>{{.Name }} syzkaller</b>
<br>
<br>
<table>
<table class="list_table">
<caption>Stats:</caption>
{{range $s := $.Stats}}
<tr>
<td>{{$s.Name}}</td>
{{if $s.Link}}
<td><a href="{{$s.Link}}">{{$s.Value}}</a></td>
{{else}}
<td>{{$s.Value}}</td>
{{end}}
<td class="stat_name">{{$s.Name}}</td>
<td class="stat_value">
{{if $s.Link}}
<a href="{{$s.Link}}">{{$s.Value}}</a>
{{else}}
{{$s.Value}}
{{end}}
</td>
</tr>
{{end}}
</table>
<br>
<table>
<table class="list_table">
<caption>Crashes:</caption>
<tr>
<th>Description</th>
<th>Count</th>
<th>Last Time</th>
<th>Report</th>
<th><a onclick="return sortTable(this, 'Description', textSort)" href="#">Description</a></th>
<th><a onclick="return sortTable(this, 'Count', numSort)" href="#">Count</a></th>
<th><a onclick="return sortTable(this, 'Last Time', textSort, true)" href="#">Last Time</a></th>
<th><a onclick="return sortTable(this, 'Report', textSort)" href="#">Report</a></th>
</tr>
{{range $c := $.Crashes}}
<tr>
<td><a href="/crash?id={{$c.ID}}">{{$c.Description}}</a></td>
<td>{{$c.Count}}</td>
<td>{{$c.LastTime}}</td>
<td class="title"><a href="/crash?id={{$c.ID}}">{{$c.Description}}</a></td>
<td class="stat {{if not $c.Active}}inactive{{end}}">{{$c.Count}}</td>
<td class="time {{if not $c.Active}}inactive{{end}}">{{formatTime $c.LastTime}}</td>
<td>
{{if $c.Triaged}}
<a href="/report?id={{$c.ID}}">{{$c.Triaged}}</a>
@ -651,11 +652,10 @@ var summaryTemplate = template.Must(template.New("").Parse(addStyle(`
</tr>
{{end}}
</table>
<br>
<b>Log:</b>
<br>
<textarea id="log_textarea" readonly rows="20">
<textarea id="log_textarea" readonly rows="20" wrap=off>
{{.Log}}
</textarea>
<script>
@ -663,44 +663,52 @@ var summaryTemplate = template.Must(template.New("").Parse(addStyle(`
textarea.scrollTop = textarea.scrollHeight;
</script>
</body></html>
`)))
`)
var syscallsTemplate = template.Must(template.New("").Parse(addStyle(`
var syscallsTemplate = html.CreatePage(`
<!doctype html>
<html>
<head>
<title>{{.Name }} syzkaller</title>
{{STYLE}}
{{HEAD}}
</head>
<body>
<b>Per-call coverage:</b>
<br>
{{range $c := $.Calls}}
{{$c.Name}}
<a href='/corpus?call={{$c.Name}}'>inputs:{{$c.Inputs}}</a>
<a href='/cover?call={{$c.Name}}'>cover:{{$c.Cover}}</a>
<a href='/prio?call={{$c.Name}}'>prio</a> <br>
{{end}}
</body></html>
`)))
var crashTemplate = template.Must(template.New("").Parse(addStyle(`
<table class="list_table">
<caption>Per-syscall coverage:</caption>
<tr>
<th><a onclick="return sortTable(this, 'Syscall', textSort)" href="#">Syscall</a></th>
<th><a onclick="return sortTable(this, 'Inputs', numSort)" href="#">Inputs</a></th>
<th><a onclick="return sortTable(this, 'Coverage', numSort)" href="#">Coverage</a></th>
<th>Prio</th>
</tr>
{{range $c := $.Calls}}
<tr>
<td>{{$c.Name}}</td>
<td><a href='/corpus?call={{$c.Name}}'>{{$c.Inputs}}</a></td>
<td><a href='/cover?call={{$c.Name}}'>{{$c.Cover}}</a></td>
<td><a href='/prio?call={{$c.Name}}'>prio</a></td>
</tr>
{{end}}
</table>
</body></html>
`)
var crashTemplate = html.CreatePage(`
<!doctype html>
<html>
<head>
<title>{{.Description}}</title>
{{STYLE}}
{{HEAD}}
</head>
<body>
<b>{{.Description}}</b>
<br><br>
{{if .Triaged}}
Report: <a href="/report?id={{.ID}}">{{.Triaged}}</a>
{{end}}
<br><br>
<table>
<table class="list_table">
<tr>
<th>#</th>
<th>Log</th>
@ -712,34 +720,43 @@ Report: <a href="/report?id={{.ID}}">{{.Triaged}}</a>
<tr>
<td>{{$c.Index}}</td>
<td><a href="/file?name={{$c.Log}}">log</a></td>
{{if $c.Report}}
<td><a href="/file?name={{$c.Report}}">report</a></td>
{{else}}
<td></td>
{{end}}
<td>{{$c.TimeStr}}</td>
<td>{{$c.Tag}}</td>
<td>
{{if $c.Report}}
<a href="/file?name={{$c.Report}}">report</a></td>
{{end}}
</td>
<td class="time {{if not $c.Active}}inactive{{end}}">{{formatTime $c.Time}}</td>
<td class="tag {{if not $c.Active}}inactive{{end}}" title="{{$c.Tag}}">{{formatShortHash $c.Tag}}</td>
</tr>
{{end}}
</table>
</body></html>
`)))
`)
var corpusTemplate = template.Must(template.New("").Parse(addStyle(`
var corpusTemplate = html.CreatePage(`
<!doctype html>
<html>
<head>
<title>syzkaller corpus</title>
{{STYLE}}
{{HEAD}}
</head>
<body>
{{range $c := $}}
<span title="{{$c.Full}}">{{$c.Short}}</span>
<a href='/cover?input={{$c.Sig}}'>cover:{{$c.Cover}}</a>
<br>
{{end}}
<table class="list_table">
<caption>Corpus{{if $.Call}} for {{$.Call}}{{end}}:</caption>
<tr>
<th>Coverage</th>
<th>Program</th>
</tr>
{{range $inp := $.Inputs}}
<tr>
<td><a href='/cover?input={{$inp.Sig}}'>{{$inp.Cover}}</a></td>
<td><a href="/input?sig={{$inp.Sig}}">{{$inp.Short}}</a></td>
</tr>
{{end}}
</table>
</body></html>
`)))
`)
type UIPrioData struct {
Call string
@ -751,26 +768,29 @@ type UIPrio struct {
Prio float32
}
type UIPrioArray []UIPrio
func (a UIPrioArray) Len() int { return len(a) }
func (a UIPrioArray) Less(i, j int) bool { return a[i].Prio > a[j].Prio }
func (a UIPrioArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
var prioTemplate = template.Must(template.New("").Parse(addStyle(`
var prioTemplate = html.CreatePage(`
<!doctype html>
<html>
<head>
<title>syzkaller priorities</title>
{{STYLE}}
{{HEAD}}
</head>
<body>
Priorities for {{$.Call}} <br> <br>
{{range $p := $.Prios}}
{{printf "%.4f\t%s" $p.Prio $p.Call}} <br>
{{end}}
<table class="list_table">
<caption>Priorities for {{$.Call}}:</caption>
<tr>
<th><a onclick="return sortTable(this, 'Prio', floatSort)" href="#">Prio</a></th>
<th><a onclick="return sortTable(this, 'Call', textSort)" href="#">Call</a></th>
</tr>
{{range $p := $.Prios}}
<tr>
<td>{{printf "%.4f" $p.Prio}}</td>
<td><a href='/prio?call={{$p.Call}}'>{{$p.Call}}</a></td>
</tr>
{{end}}
</table>
</body></html>
`)))
`)
type UIFallbackCoverData struct {
Calls []UIFallbackCall
@ -782,15 +802,15 @@ type UIFallbackCall struct {
Errnos []int
}
var fallbackCoverTemplate = template.Must(template.New("").Parse(addStyle(`
var fallbackCoverTemplate = html.CreatePage(`
<!doctype html>
<html>
<head>
<title>syzkaller coverage</title>
{{STYLE}}
{{HEAD}}
</head>
<body>
<table>
<table class="list_table">
<tr>
<th>Call</th>
<th>Successful</th>
@ -805,31 +825,4 @@ var fallbackCoverTemplate = template.Must(template.New("").Parse(addStyle(`
{{end}}
</table>
</body></html>
`)))
func addStyle(html string) string {
return strings.Replace(html, "{{STYLE}}", htmlStyle, -1)
}
const htmlStyle = `
<style type="text/css" media="screen">
table {
border-collapse:collapse;
border:1px solid;
}
table caption {
font-weight: bold;
}
table td {
border:1px solid;
padding: 3px;
}
table th {
border:1px solid;
padding: 3px;
}
textarea {
width:100%;
}
</style>
`
`)