First pass at workspaces.
authorcommit-bot@chromium.org <commit-bot@chromium.org@2bbb7eff-a529-9590-31e7-b0007b416f81>
Sat, 19 Apr 2014 13:55:50 +0000 (13:55 +0000)
committercommit-bot@chromium.org <commit-bot@chromium.org@2bbb7eff-a529-9590-31e7-b0007b416f81>
Sat, 19 Apr 2014 13:55:50 +0000 (13:55 +0000)
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
experimental/webtry/css/webtry.css
experimental/webtry/js/run.js [new file with mode: 0644]
experimental/webtry/main.cpp
experimental/webtry/templates/index.html
experimental/webtry/templates/recent.html
experimental/webtry/templates/titlebar.html [new file with mode: 0644]
experimental/webtry/templates/workspace.html [new file with mode: 0644]
experimental/webtry/webtry.go

index 0c4f3a1..3aaf2e7 100644 (file)
@@ -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.
index ee87b94..d9487c6 100644 (file)
@@ -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 (file)
index 0000000..165aae3
--- /dev/null
@@ -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": "<base64 encoded image but only on success>"
+      // }
+      //
+      // 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 = '<div class=tries>' +
+          '<a href="/c/' + body.hash + '">' +
+          '  <img width=64 height=64 src="/i/' + body.hash +  '.png">' +
+          '</a></div>';
+
+        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);
index 9e7df14..7ccb932 100644 (file)
@@ -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<SkSurface> surface(SkSurface::NewRaster(info));
     SkCanvas* canvas = surface->getCanvas();
 
index d2ba859..c79dc12 100644 (file)
@@ -6,11 +6,7 @@
     <link rel="stylesheet" href="/css/" type="text/css" media="screen">
 </head>
 <body>
-  <section id=title>
-    <a href="/">Home</a>
-    <a href="/recent">Recent</a>
-    <a href="https://github.com/google/skia/tree/master/experimental/webtry">Code</a>
-  </section>
+  {{template "titlebar.html"}}
   <section id=content>
   <pre><code>#include "SkCanvas.h"
 
@@ -28,57 +24,9 @@ void draw(SkCanvas* canvas) {
 
   </section>
   <script type='text/javascript' charset='utf-8'>
-      var run = document.getElementById('run');
-      var code = document.getElementById('code');
-      var output = document.getElementById('output');
-      var img = document.getElementById('img');
-
-      function beginWait() {
-        document.body.classList.add('waiting');
-        run.disabled = true;
-      }
-
-      function endWait() {
-        document.body.classList.remove('waiting');
-        run.disabled = false;
-      }
-
-      function codeComplete(e) {
-        // The response is JSON of the form:
-        // {
-        //   "message": "you had an error...",
-        //   "img": "<base64 encoded image but only on success>"
-        // }
-        //
-        // 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 = '';
-        }
-        window.history.pushState(null, null, "/c/" + body.hash);
-      }
-
-      function codeError(e) {
-        endWait();
-        alert('Something bad happened: ' + e);
-      }
-
-      run.addEventListener('click', onSubmitCode);
-      function onSubmitCode() {
-        beginWait();
-        var req = new XMLHttpRequest();
-        req.addEventListener('load', codeComplete);
-        req.addEventListener('error', codeError);
-        req.overrideMimeType('application/json');
-        req.open('POST', '.', true);
-        req.send(code.value);
-      }
+      // Not running in a workspace.
+      var workspaceName = "";
   </script>
+  <script src="/js/run.js" type="text/javascript" charset="utf-8"></script>
 </body>
 </html>
index 051ac3f..96be714 100644 (file)
@@ -6,21 +6,19 @@
     <link rel="stylesheet" href="/css/" type="text/css" media="screen">
 </head>
 <body>
-  <section id=title>
-    <a href="/">Home</a>
-    <a href="/recent">Recent</a>
-    <a href="https://github.com/google/skia/tree/master/experimental/webtry">Code</a>
-  </section>
+  {{template "titlebar.html"}}
   <section id=content>
   <h1>Recent Activity</h1>
-  {{range .Tries}}
-    <section class=tries>
-    <h2><a href="/c/{{.Hash}}">{{.CreateTS}}</a></h2>
-    <a href="/c/{{.Hash}}">
-      <img width=100 height=100 src="/i/{{.Hash}}.png">
-    </a>
-    </section>
-  {{end}}
+  <section>
+    {{range .Tries}}
+      <div class=tries>
+        <h2><a href="/c/{{.Hash}}">{{.CreateTS}}</a></h2>
+        <a href="/c/{{.Hash}}">
+          <img width=128 height=128 src="/i/{{.Hash}}.png">
+        </a>
+      </div>
+    {{end}}
+  </section>
   </section>
 </body>
 </html>
diff --git a/experimental/webtry/templates/titlebar.html b/experimental/webtry/templates/titlebar.html
new file mode 100644 (file)
index 0000000..93f6410
--- /dev/null
@@ -0,0 +1,6 @@
+  <section id=title>
+    <a href="/">Home</a>
+    <a href="/recent/">Recent</a>
+    <a href="/w/">Workspace</a>
+    <a href="https://github.com/google/skia/tree/master/experimental/webtry">Code</a>
+  </section>
diff --git a/experimental/webtry/templates/workspace.html b/experimental/webtry/templates/workspace.html
new file mode 100644 (file)
index 0000000..3d70035
--- /dev/null
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Workspace</title>
+    <meta charset='utf-8' />
+    <link rel="stylesheet" href="/css/" type="text/css" media="screen">
+</head>
+<body>
+  {{template "titlebar.html"}}
+  <section id=content>
+  <h1>Create</h1>
+{{if .Name}}
+  <pre><code>#include "SkCanvas.h"
+
+void draw(SkCanvas* canvas) {
+  <textarea name='code' id='code' rows='15' cols='80'>{{.Code}}</textarea>
+}
+</code></pre>
+
+  <input type='button' value='Run' id='run'>
+
+  <p>Image appears here:</p>
+  <img id='img' src=''/>
+
+  <pre><code id='output'></code></pre>
+  </section>
+  <section id=tryHistory>
+    {{range .Tries}}
+      <div class=tries>
+        <a href="/c/{{.Hash}}">
+          <img width=64 height=64 src="/i/{{.Hash}}.png">
+        </a>
+      </div>
+    {{end}}
+  </section>
+
+  <script type='text/javascript' charset='utf-8'>
+      // Set the workspace name so run.js also updates the history.
+      var workspaceName = "{{.Name}}";
+  </script>
+  <script src="/js/run.js" type="text/javascript" charset="utf-8"></script>
+{{else}}
+  Create a new workspace:
+  <form action="." method="POST" accept-charset="utf-8">
+    <p><input type="submit" value="Create"></p>
+  </form>
+{{end}}
+  </section>
+</body>
+</html>
index 1b678ec..16f6f4f 100644 (file)
@@ -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))
 }