From d04e1dd9e1dbe7e458932a60801fb31a72b49267 Mon Sep 17 00:00:00 2001 From: "commit-bot@chromium.org" Date: Sat, 19 Apr 2014 13:55:50 +0000 Subject: [PATCH] First pass at workspaces. Ability to create new workspaces. Run tries in a workspace, each try is added to a history of a workspace. BUG=skia: R=mtklein@google.com Author: jcgregorio@google.com Review URL: https://codereview.chromium.org/240773003 git-svn-id: http://skia.googlecode.com/svn/trunk@14265 2bbb7eff-a529-9590-31e7-b0007b416f81 --- experimental/webtry/DESIGN.md | 92 +++++++++----- experimental/webtry/css/webtry.css | 11 ++ experimental/webtry/js/run.js | 90 ++++++++++++++ experimental/webtry/main.cpp | 2 +- experimental/webtry/templates/index.html | 60 +-------- experimental/webtry/templates/recent.html | 24 ++-- experimental/webtry/templates/titlebar.html | 6 + experimental/webtry/templates/workspace.html | 50 ++++++++ experimental/webtry/webtry.go | 178 +++++++++++++++++++++++++-- 9 files changed, 401 insertions(+), 112 deletions(-) create mode 100644 experimental/webtry/js/run.js create mode 100644 experimental/webtry/templates/titlebar.html create mode 100644 experimental/webtry/templates/workspace.html diff --git a/experimental/webtry/DESIGN.md b/experimental/webtry/DESIGN.md index 0c4f3a1..3aaf2e7 100644 --- a/experimental/webtry/DESIGN.md +++ b/experimental/webtry/DESIGN.md @@ -36,35 +36,33 @@ Architecture The server runs on GCE, and consists of a Go Web Server that calls out to the c++ compiler and executes code in a chroot jail. See the diagram below: -                             -    +–––––––––––––+          -    |             |          -    |  Browser    |          -    |             |          -    +––––––+––––––+          -           |                 -    +––––––+––––––+          -    |             |          -    |             |          -    | Web Server  |          -    |             |          -    |   (Go)      |          -    |             |          -    |             |          -    +–––––––+–––––+          -            |                -    +–––––––+––––––––––+     -    | chroot jail      |     -    |  +––––––––––––––+|     -    |  | seccomp      ||     -    |  |  +––––––––––+||     -    |  |  |User code |||     -    |  |  |          |||     -    |  |  +––––––––––+||     -    |  +––––––––––––––+|     -    |                  |     -    +––––––––––––––––––+     -                             +    +–––––––––––––+ +    |             | +    |  Browser    | +    |             | +    +––––––+––––––+ +           | +    +––––––+––––––+ +    |             | +    |             | +    | Web Server  | +    |             | +    |   (Go)      | +    |             | +    |             | +    +–––––––+–––––+ +            | +    +–––––––+––––––––––+ +    | chroot jail      | +    |  +––––––––––––––+| +    |  | seccomp      || +    |  |  +––––––––––+|| +    |  |  |User code ||| +    |  |  |          ||| +    |  |  +––––––––––+|| +    |  +––––––––––––––+| +    |                  | +    +––––––––––––––––––+ The user code is expanded into a simple template and linked against libskia and a couple other .o files that contain main() and the code that sets up the @@ -147,6 +145,21 @@ Initial setup of the database, the user, and the only table: PRIMARY KEY(hash) ); + CREATE TABLE workspace ( + name CHAR(64) DEFAULT '' NOT NULL, + create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY(name) + ); + + CREATE TABLE workspacetry ( + name CHAR(64) DEFAULT '' NOT NULL, + create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + hash CHAR(64) DEFAULT '' NOT NULL, + hidden INTEGER DEFAULT 0 NOT NULL, + + FOREIGN KEY (name) REFERENCES workspace(name) + ); + Common queries webtry.go will use: INSERT INTO webtry (code, hash) VALUES('int i = 0;...', 'abcdef...'); @@ -161,9 +174,18 @@ Common queries webtry.go will use: // Run before and after to confirm the password changed: SELECT Host, User, Password FROM mysql.user; +Common queries for workspaces: + + SELECT hash, create_ts FROM workspace ORDER BY create_ts DESC; + + INSERT INTO workspace (name, hash) VALUES('autumn-river-12354', 'abcdef...'); + + SELECT name FROM workspace GROUP BY name; + Password for the database will be stored in the metadata instance, if the -metadata server can't be found, i.e. running locally, then data will not be -stored. To see the current password stored in metadata and the fingerprint: +metadata server can't be found, i.e. running locally, then a local sqlite +database will be used. To see the current password stored in metadata and the +fingerprint: gcutil --project=google.com:skia-buildbots getinstance skia-webtry-b @@ -179,6 +201,14 @@ the metadata server: N.B. If you need to change the MySQL password that webtry uses, you must change it both in MySQL and the value stored in the metadata server. +Workspaces +---------- + +Workspaces are implemented by the workspace and workspacetry tables. The +workspace table keeps the unique list of all workspaces. The workspacetry table +keeps track of all the tries that have occured in a workspace. Right now the +hidden column of workspacetry is not used, it's for future functionality. + Installation ------------ See the README file. diff --git a/experimental/webtry/css/webtry.css b/experimental/webtry/css/webtry.css index ee87b94..d9487c6 100644 --- a/experimental/webtry/css/webtry.css +++ b/experimental/webtry/css/webtry.css @@ -58,3 +58,14 @@ pre, code { #content { padding: 1em; } + +#tryHistory { + position: absolute; + top: 3em; + right: 10px; + width: 75px; +} + +#tryHistory .tries { + float: none; +} diff --git a/experimental/webtry/js/run.js b/experimental/webtry/js/run.js new file mode 100644 index 0000000..165aae3 --- /dev/null +++ b/experimental/webtry/js/run.js @@ -0,0 +1,90 @@ +/** + * Common JS that talks XHR back to the server and runs the code and receives + * the results. + */ + +/** + * All the functionality is wrapped up in this anonymous closure, but we need + * to be told if we are on the workspace page or a normal try page, so the + * workspaceName is passed into the closure, it must be set in the global + * namespace. If workspaceName is the empty string then we know we aren't + * running on a workspace page. + */ +(function(workspaceName) { + var run = document.getElementById('run'); + var code = document.getElementById('code'); + var output = document.getElementById('output'); + var img = document.getElementById('img'); + var tryHistory = document.getElementById('tryHistory'); + var parser = new DOMParser(); + + + function beginWait() { + document.body.classList.add('waiting'); + run.disabled = true; + } + + + function endWait() { + document.body.classList.remove('waiting'); + run.disabled = false; + } + + + /** + * Callback for when the XHR returns after attempting to run the code. + * @param e The callback event. + */ + function codeComplete(e) { + // The response is JSON of the form: + // { + // "message": "you had an error...", + // "img": "" + // } + // + // The img is optional and only appears if there is a valid + // image to display. + endWait(); + console.log(e.target.response); + body = JSON.parse(e.target.response); + output.innerText = body.message; + if (body.hasOwnProperty('img')) { + img.src = 'data:image/png;base64,' + body.img; + } else { + img.src = ''; + } + // Add the image to the history if we are on a workspace page. + if (tryHistory) { + var newHistoryStr = ''; + + var newHistory = parser.parseFromString(newHistoryStr, "text/html"); + tryHistory.insertBefore(newHistory.body.firstChild, tryHistory.firstChild); + } + } + + + /** + * Callback where there's an XHR error. + * @param e The callback event. + */ + function codeError(e) { + endWait(); + alert('Something bad happened: ' + e); + } + + + function onSubmitCode() { + beginWait(); + var req = new XMLHttpRequest(); + req.addEventListener('load', codeComplete); + req.addEventListener('error', codeError); + req.overrideMimeType('application/json'); + req.open('POST', '/', true); + req.setRequestHeader('content-type', 'application/json'); + req.send(JSON.stringify({"code": code.value, "name": workspaceName})); + } + run.addEventListener('click', onSubmitCode); +})(workspaceName); diff --git a/experimental/webtry/main.cpp b/experimental/webtry/main.cpp index 9e7df14..7ccb932 100644 --- a/experimental/webtry/main.cpp +++ b/experimental/webtry/main.cpp @@ -91,7 +91,7 @@ int main(int argc, char** argv) { } SkFILEWStream stream(FLAGS_out[0]); - SkImageInfo info = SkImageInfo::MakeN32(300, 300, kPremul_SkAlphaType); + SkImageInfo info = SkImageInfo::MakeN32(256, 256, kPremul_SkAlphaType); SkAutoTUnref surface(SkSurface::NewRaster(info)); SkCanvas* canvas = surface->getCanvas(); diff --git a/experimental/webtry/templates/index.html b/experimental/webtry/templates/index.html index d2ba859..c79dc12 100644 --- a/experimental/webtry/templates/index.html +++ b/experimental/webtry/templates/index.html @@ -6,11 +6,7 @@ -
- Home - Recent - Code -
+ {{template "titlebar.html"}}
#include "SkCanvas.h"
 
@@ -28,57 +24,9 @@ void draw(SkCanvas* canvas) {
 
   
+ diff --git a/experimental/webtry/templates/recent.html b/experimental/webtry/templates/recent.html index 051ac3f..96be714 100644 --- a/experimental/webtry/templates/recent.html +++ b/experimental/webtry/templates/recent.html @@ -6,21 +6,19 @@ -
- Home - Recent - Code -
+ {{template "titlebar.html"}}

Recent Activity

- {{range .Tries}} -
-

{{.CreateTS}}

- - - -
- {{end}} +
+ {{range .Tries}} + + {{end}} +
diff --git a/experimental/webtry/templates/titlebar.html b/experimental/webtry/templates/titlebar.html new file mode 100644 index 0000000..93f6410 --- /dev/null +++ b/experimental/webtry/templates/titlebar.html @@ -0,0 +1,6 @@ +
+ Home + Recent + Workspace + Code +
diff --git a/experimental/webtry/templates/workspace.html b/experimental/webtry/templates/workspace.html new file mode 100644 index 0000000..3d70035 --- /dev/null +++ b/experimental/webtry/templates/workspace.html @@ -0,0 +1,50 @@ + + + + Workspace + + + + + {{template "titlebar.html"}} +
+

Create

+{{if .Name}} +
#include "SkCanvas.h"
+
+void draw(SkCanvas* canvas) {
+  
+}
+
+ + + +

Image appears here:

+ + +
+
+
+ {{range .Tries}} +
+ + + +
+ {{end}} +
+ + + +{{else}} + Create a new workspace: +
+

+
+{{end}} + + + diff --git a/experimental/webtry/webtry.go b/experimental/webtry/webtry.go index 1b678ec..16f6f4f 100644 --- a/experimental/webtry/webtry.go +++ b/experimental/webtry/webtry.go @@ -13,6 +13,7 @@ import ( htemplate "html/template" "io/ioutil" "log" + "math/rand" "net/http" "os" "os/exec" @@ -45,9 +46,12 @@ var ( // indexTemplate is the main index.html page we serve. indexTemplate *htemplate.Template = nil - // recentTemplate is a list of recent images. + // recentTemplate is a list of recent images. recentTemplate *htemplate.Template = nil + // workspaceTemplate is the page for workspaces, a series of webtrys. + workspaceTemplate *htemplate.Template = nil + // db is the database, nil if we don't have an SQL database to store data into. db *sql.DB = nil @@ -56,6 +60,35 @@ var ( // imageLink is the regex that matches URLs paths that are direct links to PNGs. imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$") + + // workspaceLink is the regex that matches URLs paths for workspaces. + workspaceLink = regexp.MustCompile("^/w/([a-z0-9-]+)$") + + // workspaceNameAdj is a list of adjectives for building workspace names. + workspaceNameAdj = []string{ + "autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark", + "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter", + "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue", + "billowing", "broken", "cold", "damp", "falling", "frosty", "green", + "long", "late", "lingering", "bold", "little", "morning", "muddy", "old", + "red", "rough", "still", "small", "sparkling", "throbbing", "shy", + "wandering", "withered", "wild", "black", "young", "holy", "solitary", + "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine", + "polished", "ancient", "purple", "lively", "nameless", + } + + // workspaceNameNoun is a list of nouns for building workspace names. + workspaceNameNoun = []string{ + "waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning", + "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter", + "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook", + "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly", + "feather", "grass", "haze", "mountain", "night", "pond", "darkness", + "snowflake", "silence", "sound", "sky", "shape", "surf", "thunder", + "violet", "water", "wildflower", "wave", "water", "resonance", "sun", + "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "paper", + "frog", "smoke", "star", + } ) // flags @@ -90,12 +123,24 @@ func init() { panic(err) } // Convert index.html into a template, which is expanded with the code. - indexTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates/index.html")) + indexTemplate, err = htemplate.ParseFiles( + filepath.Join(cwd, "templates/index.html"), + filepath.Join(cwd, "templates/titlebar.html"), + ) if err != nil { panic(err) } - - recentTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates/recent.html")) + recentTemplate, err = htemplate.ParseFiles( + filepath.Join(cwd, "templates/recent.html"), + filepath.Join(cwd, "templates/titlebar.html"), + ) + if err != nil { + panic(err) + } + workspaceTemplate, err = htemplate.ParseFiles( + filepath.Join(cwd, "templates/workspace.html"), + filepath.Join(cwd, "templates/titlebar.html"), + ) if err != nil { panic(err) } @@ -123,6 +168,7 @@ func init() { panic(err) } } else { + log.Printf("INFO: Failed to find metadata, unable to connect to MySQL server (Expected when running locally): %q\n", err) // Fallback to sqlite for local use. db, err = sql.Open("sqlite3", "./webtry.db") if err != nil { @@ -135,8 +181,25 @@ func init() { hash CHAR(64) DEFAULT '' NOT NULL, PRIMARY KEY(hash) )` - db.Exec(sql) - log.Printf("INFO: Failed to find metadata, unable to connect to MySQL server (Expected when running locally): %q\n", err) + _, err = db.Exec(sql) + log.Printf("Info: status creating sqlite table for webtry: %q\n", err) + sql = `CREATE TABLE workspace ( + name CHAR(64) DEFAULT '' NOT NULL, + create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY(name) + )` + _, err = db.Exec(sql) + log.Printf("Info: status creating sqlite table for workspace: %q\n", err) + sql = `CREATE TABLE workspacetry ( + name CHAR(64) DEFAULT '' NOT NULL, + create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + hash CHAR(64) DEFAULT '' NOT NULL, + hidden INTEGER DEFAULT 0 NOT NULL, + + FOREIGN KEY (name) REFERENCES workspace(name) + )` + _, err = db.Exec(sql) + log.Printf("Info: status creating sqlite table for workspace try: %q\n", err) } } @@ -231,19 +294,28 @@ func reportError(w http.ResponseWriter, r *http.Request, err error, message stri w.Write(resp) } -func writeToDatabase(hash string, code string) { +func writeToDatabase(hash string, code string, workspaceName string) { if db == nil { return } if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", code, hash); err != nil { log.Printf("ERROR: Failed to insert code into database: %q\n", err) } + if workspaceName != "" { + if _, err := db.Exec("INSERT INTO workspacetry (name, hash) VALUES(?, ?)", workspaceName, hash); err != nil { + log.Printf("ERROR: Failed to insert into workspacetry table: %q\n", err) + } + } } func cssHandler(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "css/webtry.css") } +func jsHandler(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "js/run.js") +} + // imageHandler serves up the PNG of a specific try. func imageHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Image Handler: %q\n", r.URL.Path) @@ -294,6 +366,79 @@ func recentHandler(w http.ResponseWriter, r *http.Request) { } } +type Workspace struct { + Name string + Code string + Tries []Try +} + +// newWorkspace generates a new random workspace name and stores it in the database. +func newWorkspace() (string, error) { + for i := 0; i < 10; i++ { + adj := workspaceNameAdj[rand.Intn(len(workspaceNameAdj))] + noun := workspaceNameNoun[rand.Intn(len(workspaceNameNoun))] + suffix := rand.Intn(1000) + name := fmt.Sprintf("%s-%s-%d", adj, noun, suffix) + if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", name); err == nil { + return name, nil + } else { + log.Printf("ERROR: Failed to insert workspace into database: %q\n", err) + } + } + return "", fmt.Errorf("Failed to create a new workspace") +} + +// getCode returns the code for a given hash, or the empty string if not found. +func getCode(hash string) string { + code := "" + if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil { + log.Printf("ERROR: Code for hash is missing: %q\n", err) + } + return code +} + +func workspaceHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("Workspace Handler: %q\n", r.URL.Path) + if r.Method == "GET" { + tries := []Try{} + match := workspaceLink.FindStringSubmatch(r.URL.Path) + name := "" + if len(match) == 2 { + name = match[1] + rows, err := db.Query("SELECT create_ts, hash FROM workspacetry WHERE name=? ORDER BY create_ts DESC ", name) + if err != nil { + reportError(w, r, err, "Failed to select.") + return + } + for rows.Next() { + var hash string + var create_ts time.Time + if err := rows.Scan(&create_ts, &hash); err != nil { + log.Printf("Error: failed to fetch from database: %q", err) + continue + } + tries = append(tries, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")}) + } + } + var code string + if len(tries) == 0 { + code = DEFAULT_SAMPLE + } else { + code = getCode(tries[len(tries)-1].Hash) + } + if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, Code: code, Name: name}); err != nil { + log.Printf("ERROR: Failed to expand template: %q\n", err) + } + } else if r.Method == "POST" { + name, err := newWorkspace() + if err != nil { + http.Error(w, "Failed to create a new workspace.", 500) + return + } + http.Redirect(w, r, "/w/"+name, 302) + } +} + // hasPreProcessor returns true if any line in the code begins with a # char. func hasPreProcessor(code string) bool { lines := strings.Split(code, "\n") @@ -305,6 +450,11 @@ func hasPreProcessor(code string) bool { return false } +type TryRequest struct { + Code string `json:"code"` + Name string `json:"name"` +} + // mainHandler handles the GET and POST of the main page. func mainHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Main Handler: %q\n", r.URL.Path) @@ -340,18 +490,22 @@ func mainHandler(w http.ResponseWriter, r *http.Request) { reportError(w, r, err, "Code too large.") return } - code := string(buf.Bytes()) - if hasPreProcessor(code) { + request := TryRequest{} + if err := json.Unmarshal(buf.Bytes(), &request); err != nil { + reportError(w, r, err, "Coulnd't decode JSON.") + return + } + if hasPreProcessor(request.Code) { err := fmt.Errorf("Found preprocessor macro in code.") reportError(w, r, err, "Preprocessor macros aren't allowed.") return } - hash, err := expandCode(LineNumbers(code)) + hash, err := expandCode(LineNumbers(request.Code)) if err != nil { reportError(w, r, err, "Failed to write the code to compile.") return } - writeToDatabase(hash, code) + writeToDatabase(hash, request.Code, request.Name) message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), true) if err != nil { reportError(w, r, err, "Failed to compile the code:\n"+message) @@ -403,8 +557,10 @@ func mainHandler(w http.ResponseWriter, r *http.Request) { func main() { flag.Parse() http.HandleFunc("/i/", imageHandler) + http.HandleFunc("/w/", workspaceHandler) http.HandleFunc("/recent/", recentHandler) http.HandleFunc("/css/", cssHandler) + http.HandleFunc("/js/", jsHandler) http.HandleFunc("/", mainHandler) log.Fatal(http.ListenAndServe(*port, nil)) } -- 2.7.4