aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2018-11-22 14:27:37 +0100
committerDmitry Vyukov <dvyukov@google.com>2018-11-22 14:36:32 +0100
commit13ab4beeefd2c49666ce771753fbb3a28c9d2f2c (patch)
tree95591888c2e60e548c30e111bdce6e0d2e6a3963
parent582e1f0d1d51b9237d2dedfcb4c1540b849da8c2 (diff)
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
-rw-r--r--Makefile2
-rw-r--r--dashboard/app/handler.go87
-rw-r--r--dashboard/app/main.go3
-rw-r--r--dashboard/app/reporting.go5
-rw-r--r--dashboard/app/static/common.js31
-rw-r--r--dashboard/app/static/style.css16
-rw-r--r--pkg/html/generated.go221
-rw-r--r--pkg/html/html.go110
-rw-r--r--syz-manager/html.go313
9 files changed, 521 insertions, 267 deletions
diff --git a/Makefile b/Makefile
index 61865eb42..99f7eeb43 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/dashboard/app/handler.go b/dashboard/app/handler.go
index 3bde1cab5..167ca5857 100644
--- a/dashboard/app/handler.go
+++ b/dashboard/app/handler.go
@@ -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")
diff --git a/dashboard/app/main.go b/dashboard/app/main.go
index 02dbfb8d9..525cd1c04 100644
--- a/dashboard/app/main.go
+++ b/dashboard/app/main.go
@@ -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
}
}
diff --git a/dashboard/app/reporting.go b/dashboard/app/reporting.go
index 0fd2d7aa3..4982c8ade 100644
--- a/dashboard/app/reporting.go
+++ b/dashboard/app/reporting.go
@@ -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
}
diff --git a/dashboard/app/static/common.js b/dashboard/app/static/common.js
index 5a146f3bd..5c20f7df9 100644
--- a/dashboard/app/static/common.js
+++ b/dashboard/app/static/common.js
@@ -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); }
diff --git a/dashboard/app/static/style.css b/dashboard/app/static/style.css
index cb9e6e378..536cd82ab 100644
--- a/dashboard/app/static/style.css
+++ b/dashboard/app/static/style.css
@@ -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;
}
diff --git a/pkg/html/generated.go b/pkg/html/generated.go
new file mode 100644
index 000000000..7c5157aab
--- /dev/null
+++ b/pkg/html/generated.go
@@ -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;
+}
+`
diff --git a/pkg/html/html.go b/pkg/html/html.go
new file mode 100644
index 000000000..c7f85da88
--- /dev/null
+++ b/pkg/html/html.go
@@ -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]
+}
diff --git a/syz-manager/html.go b/syz-manager/html.go
index f9f96933a..777dcd39b 100644
--- a/syz-manager/html.go
+++ b/syz-manager/html.go
@@ -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{
+ data.Inputs = append(data.Inputs, &UIInput{
+ Sig: sig,
Short: p.String(),
- Full: string(inp.Prog),
Cover: len(inp.Cover),
- Sig: sig,
})
}
- 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 UICorpus struct {
+ Call string
+ Inputs []*UIInput
+}
+
type UIInput struct {
+ Sig string
Short string
- Full string
- Calls int
Cover int
- Sig string
}
-type UICallTypeArray []UICallType
-
-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}}
+
+<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 = template.Must(template.New("").Parse(addStyle(`
+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>
-`
+`)