aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--dashboard/app/api.go53
-rw-r--r--dashboard/app/entities_datastore.go9
-rw-r--r--dashboard/app/index.yaml6
-rw-r--r--dashboard/app/main.go20
-rw-r--r--dashboard/app/main_test.go20
-rw-r--r--dashboard/app/manager.html23
-rw-r--r--dashboard/app/repro_test.go29
-rw-r--r--dashboard/app/util_test.go23
-rw-r--r--pkg/html/pages/style.css5
9 files changed, 180 insertions, 8 deletions
diff --git a/dashboard/app/api.go b/dashboard/app/api.go
index 409fb023c..f9b6816db 100644
--- a/dashboard/app/api.go
+++ b/dashboard/app/api.go
@@ -1740,6 +1740,17 @@ func apiLogToReproduce(c context.Context, ns string, r *http.Request, payload []
if err != nil {
return nil, err
}
+ // First check if there have been any manual requests.
+ log, err := takeReproTask(c, ns, build.Manager)
+ if err != nil {
+ return nil, err
+ }
+ if log != nil {
+ return &dashapi.LogToReproResp{
+ CrashLog: log,
+ }, nil
+ }
+
bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query {
return query.Filter("Namespace=", ns).
Filter("HappenedOn=", build.Manager).
@@ -1796,3 +1807,45 @@ func logToReproForBug(c context.Context, bug *Bug, manager string) (*dashapi.Log
}
return nil, nil
}
+
+func saveReproTask(c context.Context, ns, manager string, repro []byte) error {
+ log, err := putText(c, ns, textCrashLog, repro, false)
+ if err != nil {
+ return err
+ }
+ // We don't control the status of each attempt, so let's just try twice.
+ const attempts = 2
+ obj := &ReproTask{
+ Namespace: ns,
+ Manager: manager,
+ Log: log,
+ AttemptsLeft: attempts,
+ }
+ key := db.NewIncompleteKey(c, "ReproTask", nil)
+ _, err = db.Put(c, key, obj)
+ return err
+}
+
+func takeReproTask(c context.Context, ns, manager string) ([]byte, error) {
+ var tasks []*ReproTask
+ keys, err := db.NewQuery("ReproTask").
+ Filter("Namespace=", ns).
+ Filter("Manager=", manager).
+ Filter("AttemptsLeft>", 0).
+ GetAll(c, &tasks)
+ if err != nil || len(keys) == 0 {
+ return nil, err
+ }
+
+ // Yes, it's possible that the entity will be modified simultaneously, and we
+ // ideall need a transaction, but let's just ignore this possibility -- in the
+ // worst case we'd just try to reproduce it once more.
+ key, task := keys[0], tasks[0]
+ task.AttemptsLeft--
+ task.LastAttempt = timeNow(c)
+ if _, err := db.Put(c, key, task); err != nil {
+ return nil, err
+ }
+ log, _, err := getText(c, textCrashLog, task.Log)
+ return log, err
+}
diff --git a/dashboard/app/entities_datastore.go b/dashboard/app/entities_datastore.go
index 3d81722fc..87c8bd643 100644
--- a/dashboard/app/entities_datastore.go
+++ b/dashboard/app/entities_datastore.go
@@ -722,6 +722,15 @@ func (status BisectStatus) String() string {
}
}
+// ReproTask is a manually requested reproduction attempt.
+type ReproTask struct {
+ Namespace string
+ Manager string
+ Log int64 // Reference to CrashLog text entity.
+ AttemptsLeft int64
+ LastAttempt time.Time
+}
+
func mgrKey(c context.Context, ns, name string) *db.Key {
return db.NewKey(c, "Manager", fmt.Sprintf("%v-%v", ns, name), 0, nil)
}
diff --git a/dashboard/app/index.yaml b/dashboard/app/index.yaml
index f56095bcd..eae3d3c13 100644
--- a/dashboard/app/index.yaml
+++ b/dashboard/app/index.yaml
@@ -256,3 +256,9 @@ indexes:
properties:
- name: Namespace
- name: Type
+
+- kind: ReproTask
+ properties:
+ - name: Namespace
+ - name: Manager
+ - name: AttemptsLeft
diff --git a/dashboard/app/main.go b/dashboard/app/main.go
index 42a0a9da3..4d68b14cd 100644
--- a/dashboard/app/main.go
+++ b/dashboard/app/main.go
@@ -211,9 +211,11 @@ type uiAdminPage struct {
}
type uiManagerPage struct {
- Header *uiHeader
- Manager *uiManager
- Builds []*uiBuild
+ Header *uiHeader
+ Manager *uiManager
+ Message string
+ ShowReproForm bool
+ Builds []*uiBuild
}
type uiManager struct {
@@ -583,6 +585,18 @@ func handleManagerPage(c context.Context, w http.ResponseWriter, r *http.Request
return fmt.Errorf("failed to query builds: %w", err)
}
managerPage := &uiManagerPage{Manager: manager, Header: hdr}
+ accessLevel := accessLevel(c, r)
+ if accessLevel >= AccessUser {
+ managerPage.ShowReproForm = true
+ if repro := r.FormValue("send-repro"); repro != "" {
+ err := saveReproTask(c, hdr.Namespace, manager.Name, []byte(repro))
+ if err != nil {
+ return fmt.Errorf("failed to request reproduction: %w", err)
+ }
+ managerPage.Message = "Repro request was saved!"
+ }
+ }
+
for _, build := range builds {
managerPage.Builds = append(managerPage.Builds, makeUIBuild(c, build, false))
}
diff --git a/dashboard/app/main_test.go b/dashboard/app/main_test.go
index f8d35ff56..bbca35dae 100644
--- a/dashboard/app/main_test.go
+++ b/dashboard/app/main_test.go
@@ -465,3 +465,23 @@ func TestManagerPage(t *testing.T) {
c.expectTrue(errors.As(err, &httpErr))
c.expectEQ(httpErr.Code, http.StatusBadRequest)
}
+
+func TestReproSubmitAccess(t *testing.T) {
+ c := NewCtx(t)
+ defer c.Close()
+ client := c.makeClient(clientPublic, keyPublic, true)
+
+ build := testBuild(1)
+ build.Manager = "test-manager"
+ client.UploadBuild(build)
+
+ reply, err := c.AuthGET(AccessPublic, "/access-public/manager/test-manager")
+ c.expectOK(err)
+ assert.NotContains(t, string(reply), "Send a reproducer")
+
+ for _, access := range []AccessLevel{AccessUser, AccessAdmin} {
+ reply, err := c.AuthGET(access, "/access-public/manager/test-manager")
+ c.expectOK(err)
+ assert.Contains(t, string(reply), "Send a reproducer")
+ }
+}
diff --git a/dashboard/app/manager.html b/dashboard/app/manager.html
index 98b865172..65c3d9cb7 100644
--- a/dashboard/app/manager.html
+++ b/dashboard/app/manager.html
@@ -11,7 +11,28 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the
</head>
<body>
{{template "header" .Header}}
- {{optlink .Manager.Link "[Syz-Manager]"}}
+ <div>{{optlink .Manager.Link "[Syz-Manager]"}}</div><br>
+ {{if .Message}}<b>{{.Message}}</b><br>{{end}}
+ {{if .ShowReproForm}}
+ <div class="collapsible collapsible-hide">
+ <div class="head">
+ <span class="show-icon">▶</span>
+ <span class="hide-icon">▼</span>
+ <span>Send a reproducer to {{.Manager.Name}}</span>
+ </div>
+ <div class="content">
+ <div class="input-values">
+ <form method="POST">
+ <span class="input-group">
+ <textarea name="send-repro"></textarea>
+ </span>
+ <input type="submit" name="show-crashers" value="Submit"></div>
+ </form>
+ </div>
+ </div>
+ </div>
+ {{end}}
+ <br><b>Kernel images history:</b><br>
<table class="list_table">
<tr>
<th>Time</th>
diff --git a/dashboard/app/repro_test.go b/dashboard/app/repro_test.go
index db37046d6..51b9d1049 100644
--- a/dashboard/app/repro_test.go
+++ b/dashboard/app/repro_test.go
@@ -6,6 +6,7 @@ package main
import (
"fmt"
"net/http"
+ "net/url"
"testing"
"time"
@@ -485,3 +486,31 @@ func TestReproForDifferentCrash(t *testing.T) {
dbBug, _, _ := c.loadBug(oldBug.ID)
c.expectEQ(len(dbBug.ReproAttempts), 1)
}
+
+func TestReproTask(t *testing.T) {
+ c := NewCtx(t)
+ defer c.Close()
+ client := c.client
+
+ build := testBuild(1)
+ build.Manager = "test-manager"
+ client.UploadBuild(build)
+
+ form := url.Values{}
+ const reproValue = "Some repro text"
+ form.Add("send-repro", reproValue)
+
+ c.POSTForm("/test1/manager/test-manager", form)
+
+ // We run the reproducer request 2 times.
+ for i := 0; i < 2; i++ {
+ resp, err := client.LogToRepro(&dashapi.LogToReproReq{BuildID: build.ID})
+ c.expectOK(err)
+ c.expectEQ(string(resp.CrashLog), reproValue)
+ }
+
+ // But no more.
+ resp, err := client.LogToRepro(&dashapi.LogToReproReq{BuildID: build.ID})
+ c.expectOK(err)
+ c.expectEQ(resp.CrashLog, []byte(nil))
+}
diff --git a/dashboard/app/util_test.go b/dashboard/app/util_test.go
index ff74187a9..c2f64ff67 100644
--- a/dashboard/app/util_test.go
+++ b/dashboard/app/util_test.go
@@ -14,6 +14,7 @@ import (
"io"
"net/http"
"net/http/httptest"
+ "net/url"
"os"
"os/exec"
"path/filepath"
@@ -292,7 +293,7 @@ func (c *Ctx) GET(url string) ([]byte, error) {
// AuthGET sends HTTP GET request to the app with the specified authorization.
func (c *Ctx) AuthGET(access AccessLevel, url string) ([]byte, error) {
- w, err := c.httpRequest("GET", url, "", access)
+ w, err := c.httpRequest("GET", url, "", "", access)
if err != nil {
return nil, err
}
@@ -301,7 +302,17 @@ func (c *Ctx) AuthGET(access AccessLevel, url string) ([]byte, error) {
// POST sends admin-authorized HTTP POST requestd to the app.
func (c *Ctx) POST(url, body string) ([]byte, error) {
- w, err := c.httpRequest("POST", url, body, AccessAdmin)
+ w, err := c.httpRequest("POST", url, body, "", AccessAdmin)
+ if err != nil {
+ return nil, err
+ }
+ return w.Body.Bytes(), nil
+}
+
+// POST sends an admin-authorized HTTP POST form to the app.
+func (c *Ctx) POSTForm(url string, form url.Values) ([]byte, error) {
+ w, err := c.httpRequest("POST", url, form.Encode(),
+ "application/x-www-form-urlencoded", AccessAdmin)
if err != nil {
return nil, err
}
@@ -310,7 +321,7 @@ func (c *Ctx) POST(url, body string) ([]byte, error) {
// ContentType returns the response Content-Type header value.
func (c *Ctx) ContentType(url string) (string, error) {
- w, err := c.httpRequest("HEAD", url, "", AccessAdmin)
+ w, err := c.httpRequest("HEAD", url, "", "", AccessAdmin)
if err != nil {
return "", err
}
@@ -321,13 +332,17 @@ func (c *Ctx) ContentType(url string) (string, error) {
return values[0], nil
}
-func (c *Ctx) httpRequest(method, url, body string, access AccessLevel) (*httptest.ResponseRecorder, error) {
+func (c *Ctx) httpRequest(method, url, body, contentType string,
+ access AccessLevel) (*httptest.ResponseRecorder, error) {
c.t.Logf("%v: %v", method, url)
r, err := c.inst.NewRequest(method, url, strings.NewReader(body))
if err != nil {
c.t.Fatal(err)
}
r.Header.Add("X-Appengine-User-IP", "127.0.0.1")
+ if contentType != "" {
+ r.Header.Add("Content-Type", contentType)
+ }
r = registerRequest(r, c)
r = r.WithContext(c.transformContext(r.Context()))
if access == AccessAdmin || access == AccessUser {
diff --git a/pkg/html/pages/style.css b/pkg/html/pages/style.css
index 9ec565616..60e523ffa 100644
--- a/pkg/html/pages/style.css
+++ b/pkg/html/pages/style.css
@@ -354,6 +354,11 @@ aside {
margin-bottom: 7px;
}
+.input-values textarea {
+ width: 750pt;
+ height: 250pt;
+}
+
.input-group {
margin-top: 7px;
margin-bottom: 7px;