Upstream version 11.39.266.0
[platform/framework/web/crosswalk.git] / src / third_party / skia / experimental / webtry / webtry.go
1 package main
2
3 import (
4         "bytes"
5         "crypto/md5"
6         "database/sql"
7         "encoding/base64"
8         "encoding/binary"
9         "encoding/json"
10         "flag"
11         "fmt"
12         htemplate "html/template"
13         "image"
14         _ "image/gif"
15         _ "image/jpeg"
16         "image/png"
17         "io/ioutil"
18         "log"
19         "math/rand"
20         "net"
21         "net/http"
22         "os"
23         "os/exec"
24         "path/filepath"
25         "regexp"
26         "strings"
27         "text/template"
28         "time"
29 )
30
31 import (
32         "github.com/fiorix/go-web/autogzip"
33         _ "github.com/go-sql-driver/mysql"
34         _ "github.com/mattn/go-sqlite3"
35         "github.com/rcrowley/go-metrics"
36 )
37
38 const (
39         RUN_GYP   = `../../experimental/webtry/gyp_for_webtry %s -Dskia_gpu=0`
40         RUN_NINJA = `ninja -C ../../../inout/Release %s`
41
42         DEFAULT_SAMPLE = `void draw(SkCanvas* canvas) {
43     SkPaint p;
44     p.setColor(SK_ColorRED);
45     p.setAntiAlias(true);
46     p.setStyle(SkPaint::kStroke_Style);
47     p.setStrokeWidth(10);
48
49     canvas->drawLine(20, 20, 100, 100, p);
50 }`
51         // Don't increase above 2^16 w/o altering the db tables to accept something bigger than TEXT.
52         MAX_TRY_SIZE = 64000
53 )
54
55 var (
56         // codeTemplate is the cpp code template the user's code is copied into.
57         codeTemplate *template.Template = nil
58
59         // gypTemplate is the GYP file to build the executable containing the user's code.
60         gypTemplate *template.Template = nil
61
62         // indexTemplate is the main index.html page we serve.
63         indexTemplate *htemplate.Template = nil
64
65         // iframeTemplate is the main index.html page we serve.
66         iframeTemplate *htemplate.Template = nil
67
68         // recentTemplate is a list of recent images.
69         recentTemplate *htemplate.Template = nil
70
71         // workspaceTemplate is the page for workspaces, a series of webtrys.
72         workspaceTemplate *htemplate.Template = nil
73
74         // db is the database, nil if we don't have an SQL database to store data into.
75         db *sql.DB = nil
76
77         // directLink is the regex that matches URLs paths that are direct links.
78         directLink = regexp.MustCompile("^/c/([a-f0-9]+)$")
79
80         // iframeLink is the regex that matches URLs paths that are links to iframes.
81         iframeLink = regexp.MustCompile("^/iframe/([a-f0-9]+)$")
82
83         // imageLink is the regex that matches URLs paths that are direct links to PNGs.
84         imageLink = regexp.MustCompile("^/i/([a-z0-9-]+.png)$")
85
86         // tryInfoLink is the regex that matches URLs paths that are direct links to data about a single try.
87         tryInfoLink = regexp.MustCompile("^/json/([a-f0-9]+)$")
88
89         // workspaceLink is the regex that matches URLs paths for workspaces.
90         workspaceLink = regexp.MustCompile("^/w/([a-z0-9-]+)$")
91
92         // workspaceNameAdj is a list of adjectives for building workspace names.
93         workspaceNameAdj = []string{
94                 "autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark",
95                 "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter",
96                 "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue",
97                 "billowing", "broken", "cold", "damp", "falling", "frosty", "green",
98                 "long", "late", "lingering", "bold", "little", "morning", "muddy", "old",
99                 "red", "rough", "still", "small", "sparkling", "throbbing", "shy",
100                 "wandering", "withered", "wild", "black", "young", "holy", "solitary",
101                 "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine",
102                 "polished", "ancient", "purple", "lively", "nameless",
103         }
104
105         // workspaceNameNoun is a list of nouns for building workspace names.
106         workspaceNameNoun = []string{
107                 "waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning",
108                 "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter",
109                 "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook",
110                 "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly",
111                 "feather", "grass", "haze", "mountain", "night", "pond", "darkness",
112                 "snowflake", "silence", "sound", "sky", "shape", "surf", "thunder",
113                 "violet", "water", "wildflower", "wave", "water", "resonance", "sun",
114                 "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "paper",
115                 "frog", "smoke", "star",
116         }
117
118         gitHash = ""
119         gitInfo = ""
120
121         requestsCounter = metrics.NewRegisteredCounter("requests", metrics.DefaultRegistry)
122 )
123
124 // flags
125 var (
126         useChroot = flag.Bool("use_chroot", false, "Run the compiled code in the schroot jail.")
127         port      = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
128 )
129
130 // lineNumbers adds #line numbering to the user's code.
131 func LineNumbers(c string) string {
132         lines := strings.Split(c, "\n")
133         ret := []string{}
134         for i, line := range lines {
135                 ret = append(ret, fmt.Sprintf("#line %d", i+1))
136                 ret = append(ret, line)
137         }
138         return strings.Join(ret, "\n")
139 }
140
141 func init() {
142         rand.Seed(time.Now().UnixNano())
143
144         // Change the current working directory to the directory of the executable.
145         var err error
146         cwd, err := filepath.Abs(filepath.Dir(os.Args[0]))
147         if err != nil {
148                 log.Fatal(err)
149         }
150         os.Chdir(cwd)
151
152         codeTemplate, err = template.ParseFiles(filepath.Join(cwd, "templates/template.cpp"))
153         if err != nil {
154                 panic(err)
155         }
156         gypTemplate, err = template.ParseFiles(filepath.Join(cwd, "templates/template.gyp"))
157         if err != nil {
158                 panic(err)
159         }
160         indexTemplate, err = htemplate.ParseFiles(
161                 filepath.Join(cwd, "templates/index.html"),
162                 filepath.Join(cwd, "templates/titlebar.html"),
163                 filepath.Join(cwd, "templates/content.html"),
164                 filepath.Join(cwd, "templates/headercommon.html"),
165                 filepath.Join(cwd, "templates/footercommon.html"),
166         )
167         if err != nil {
168                 panic(err)
169         }
170         iframeTemplate, err = htemplate.ParseFiles(
171                 filepath.Join(cwd, "templates/iframe.html"),
172                 filepath.Join(cwd, "templates/content.html"),
173                 filepath.Join(cwd, "templates/headercommon.html"),
174                 filepath.Join(cwd, "templates/footercommon.html"),
175         )
176         if err != nil {
177                 panic(err)
178         }
179         recentTemplate, err = htemplate.ParseFiles(
180                 filepath.Join(cwd, "templates/recent.html"),
181                 filepath.Join(cwd, "templates/titlebar.html"),
182                 filepath.Join(cwd, "templates/headercommon.html"),
183                 filepath.Join(cwd, "templates/footercommon.html"),
184         )
185         if err != nil {
186                 panic(err)
187         }
188         workspaceTemplate, err = htemplate.ParseFiles(
189                 filepath.Join(cwd, "templates/workspace.html"),
190                 filepath.Join(cwd, "templates/titlebar.html"),
191                 filepath.Join(cwd, "templates/content.html"),
192                 filepath.Join(cwd, "templates/headercommon.html"),
193                 filepath.Join(cwd, "templates/footercommon.html"),
194         )
195         if err != nil {
196                 panic(err)
197         }
198
199         // The git command returns output of the format:
200         //
201         //   f672cead70404080a991ebfb86c38316a4589b23 2014-04-27 19:21:51 +0000
202         //
203         logOutput, err := doCmd(`git log --format=%H%x20%ai HEAD^..HEAD`, true)
204         if err != nil {
205                 panic(err)
206         }
207         logInfo := strings.Split(logOutput, " ")
208         gitHash = logInfo[0]
209         gitInfo = logInfo[1] + " " + logInfo[2] + " " + logInfo[0][0:6]
210
211         // Connect to MySQL server. First, get the password from the metadata server.
212         // See https://developers.google.com/compute/docs/metadata#custom.
213         req, err := http.NewRequest("GET", "http://metadata/computeMetadata/v1/instance/attributes/password", nil)
214         if err != nil {
215                 panic(err)
216         }
217         client := http.Client{}
218         req.Header.Add("X-Google-Metadata-Request", "True")
219         if resp, err := client.Do(req); err == nil {
220                 password, err := ioutil.ReadAll(resp.Body)
221                 if err != nil {
222                         log.Printf("ERROR: Failed to read password from metadata server: %q\n", err)
223                         panic(err)
224                 }
225                 // The IP address of the database is found here:
226                 //    https://console.developers.google.com/project/31977622648/sql/instances/webtry/overview
227                 // And 3306 is the default port for MySQL.
228                 db, err = sql.Open("mysql", fmt.Sprintf("webtry:%s@tcp(173.194.83.52:3306)/webtry?parseTime=true", password))
229                 if err != nil {
230                         log.Printf("ERROR: Failed to open connection to SQL server: %q\n", err)
231                         panic(err)
232                 }
233         } else {
234                 log.Printf("INFO: Failed to find metadata, unable to connect to MySQL server (Expected when running locally): %q\n", err)
235                 // Fallback to sqlite for local use.
236                 db, err = sql.Open("sqlite3", "./webtry.db")
237                 if err != nil {
238                         log.Printf("ERROR: Failed to open: %q\n", err)
239                         panic(err)
240                 }
241                 sql := `CREATE TABLE source_images (
242              id        INTEGER     PRIMARY KEY                NOT NULL,
243              image     MEDIUMBLOB  DEFAULT ''                 NOT NULL, -- formatted as a PNG.
244              width     INTEGER     DEFAULT 0                  NOT NULL,
245              height    INTEGER     DEFAULT 0                  NOT NULL,
246              create_ts TIMESTAMP   DEFAULT CURRENT_TIMESTAMP  NOT NULL,
247              hidden    INTEGER     DEFAULT 0                  NOT NULL
248              )`
249                 _, err = db.Exec(sql)
250                 log.Printf("Info: status creating sqlite table for sources: %q\n", err)
251
252                 sql = `CREATE TABLE webtry (
253              code               TEXT      DEFAULT ''                 NOT NULL,
254              create_ts          TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
255              hash               CHAR(64)  DEFAULT ''                 NOT NULL,
256              source_image_id    INTEGER   DEFAULT 0                  NOT NULL,
257
258              PRIMARY KEY(hash)
259             )`
260                 _, err = db.Exec(sql)
261                 log.Printf("Info: status creating sqlite table for webtry: %q\n", err)
262
263                 sql = `CREATE TABLE workspace (
264           name      CHAR(64)  DEFAULT ''                 NOT NULL,
265           create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
266           PRIMARY KEY(name)
267         )`
268                 _, err = db.Exec(sql)
269                 log.Printf("Info: status creating sqlite table for workspace: %q\n", err)
270
271                 sql = `CREATE TABLE workspacetry (
272           name               CHAR(64)  DEFAULT ''                 NOT NULL,
273           create_ts          TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
274           hash               CHAR(64)  DEFAULT ''                 NOT NULL,
275           hidden             INTEGER   DEFAULT 0                  NOT NULL,
276           source_image_id    INTEGER   DEFAULT 0                  NOT NULL,
277
278           FOREIGN KEY (name)   REFERENCES workspace(name)
279         )`
280                 _, err = db.Exec(sql)
281                 log.Printf("Info: status creating sqlite table for workspace try: %q\n", err)
282         }
283
284         // Ping the database to keep the connection fresh.
285         go func() {
286                 c := time.Tick(1 * time.Minute)
287                 for _ = range c {
288                         if err := db.Ping(); err != nil {
289                                 log.Printf("ERROR: Database failed to respond: %q\n", err)
290                         }
291                 }
292         }()
293
294         metrics.RegisterRuntimeMemStats(metrics.DefaultRegistry)
295         go metrics.CaptureRuntimeMemStats(metrics.DefaultRegistry, 1*time.Minute)
296
297         // Start reporting metrics.
298         // TODO(jcgregorio) We need a centrialized config server for storing things
299         // like the IP address of the Graphite monitor.
300         addr, _ := net.ResolveTCPAddr("tcp", "skia-monitoring-b:2003")
301         go metrics.Graphite(metrics.DefaultRegistry, 1*time.Minute, "webtry", addr)
302
303         writeOutAllSourceImages()
304 }
305
306 func writeOutAllSourceImages() {
307         // Pull all the source images from the db and write them out to inout.
308         rows, err := db.Query("SELECT id, image, create_ts FROM source_images ORDER BY create_ts DESC")
309
310         if err != nil {
311                 log.Printf("ERROR: Failed to open connection to SQL server: %q\n", err)
312                 panic(err)
313         }
314         for rows.Next() {
315                 var id int
316                 var image []byte
317                 var create_ts time.Time
318                 if err := rows.Scan(&id, &image, &create_ts); err != nil {
319                         log.Printf("Error: failed to fetch from database: %q", err)
320                         continue
321                 }
322                 filename := fmt.Sprintf("../../../inout/image-%d.png", id)
323                 if _, err := os.Stat(filename); os.IsExist(err) {
324                         log.Printf("Skipping write since file exists: %q", filename)
325                         continue
326                 }
327                 if err := ioutil.WriteFile(filename, image, 0666); err != nil {
328                         log.Printf("Error: failed to write image file: %q", err)
329                 }
330         }
331 }
332
333 // Titlebar is used in titlebar template expansion.
334 type Titlebar struct {
335         GitHash string
336         GitInfo string
337 }
338
339 // userCode is used in template expansion.
340 type userCode struct {
341         Code     string
342         Hash     string
343         Source   int
344         Titlebar Titlebar
345 }
346
347 // writeTemplate creates a given output file and writes the template
348 // result there.
349 func writeTemplate(filename string, t *template.Template, context interface{}) error {
350         f, err := os.Create(filename)
351         if err != nil {
352                 return err
353         }
354         defer f.Close()
355         return t.Execute(f, context)
356 }
357
358 // expandToFile expands the template and writes the result to the file.
359 func expandToFile(filename string, code string, t *template.Template) error {
360         return writeTemplate(filename, t, userCode{Code: code, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}})
361 }
362
363 // expandCode expands the template into a file and calculates the MD5 hash.
364 func expandCode(code string, source int) (string, error) {
365         h := md5.New()
366         h.Write([]byte(code))
367         binary.Write(h, binary.LittleEndian, int64(source))
368         hash := fmt.Sprintf("%x", h.Sum(nil))
369         // At this point we are running in skia/experimental/webtry, making cache a
370         // peer directory to skia.
371         // TODO(jcgregorio) Make all relative directories into flags.
372         err := expandToFile(fmt.Sprintf("../../../cache/src/%s.cpp", hash), code, codeTemplate)
373         return hash, err
374 }
375
376 // expandGyp produces the GYP file needed to build the code
377 func expandGyp(hash string) error {
378         return writeTemplate(fmt.Sprintf("../../../cache/%s.gyp", hash), gypTemplate, struct{ Hash string }{hash})
379 }
380
381 // response is serialized to JSON as a response to POSTs.
382 type response struct {
383         Message string `json:"message"`
384         StdOut  string `json:"stdout"`
385         Img     string `json:"img"`
386         Hash    string `json:"hash"`
387 }
388
389 // doCmd executes the given command line string in either the out/Debug
390 // directory or the inout directory. Returns the stdout and stderr.
391 func doCmd(commandLine string, moveToDebug bool) (string, error) {
392         log.Printf("Command: %q\n", commandLine)
393         programAndArgs := strings.SplitN(commandLine, " ", 2)
394         program := programAndArgs[0]
395         args := []string{}
396         if len(programAndArgs) > 1 {
397                 args = strings.Split(programAndArgs[1], " ")
398         }
399         cmd := exec.Command(program, args...)
400         abs, err := filepath.Abs("../../out/Debug")
401         if err != nil {
402                 return "", fmt.Errorf("Failed to find absolute path to Debug directory.")
403         }
404         if moveToDebug {
405                 cmd.Dir = abs
406         } else if !*useChroot { // Don't set cmd.Dir when using chroot.
407                 abs, err := filepath.Abs("../../../inout")
408                 if err != nil {
409                         return "", fmt.Errorf("Failed to find absolute path to inout directory.")
410                 }
411                 cmd.Dir = abs
412         }
413         log.Printf("Run in directory: %q\n", cmd.Dir)
414         message, err := cmd.CombinedOutput()
415         log.Printf("StdOut + StdErr: %s\n", string(message))
416         if err != nil {
417                 log.Printf("Exit status: %s\n", err.Error())
418                 return string(message), fmt.Errorf("Failed to run command.")
419         }
420         return string(message), nil
421 }
422
423 // reportError formats an HTTP error response and also logs the detailed error message.
424 func reportError(w http.ResponseWriter, r *http.Request, err error, message string) {
425         log.Printf("Error: %s\n%s", message, err.Error())
426         w.Header().Set("Content-Type", "text/plain")
427         http.Error(w, message, 500)
428 }
429
430 // reportTryError formats an HTTP error response in JSON and also logs the detailed error message.
431 func reportTryError(w http.ResponseWriter, r *http.Request, err error, message, hash string) {
432         m := response{
433                 Message: message,
434                 Hash:    hash,
435         }
436         log.Printf("Error: %s\n%s", message, err.Error())
437         resp, err := json.Marshal(m)
438         if err != nil {
439                 http.Error(w, "Failed to serialize a response", 500)
440                 return
441         }
442         w.Header().Set("Content-Type", "text/plain")
443         w.Write(resp)
444 }
445
446 func writeToDatabase(hash string, code string, workspaceName string, source int) {
447         if db == nil {
448                 return
449         }
450         if _, err := db.Exec("INSERT INTO webtry (code, hash, source_image_id) VALUES(?, ?, ?)", code, hash, source); err != nil {
451                 log.Printf("ERROR: Failed to insert code into database: %q\n", err)
452         }
453         if workspaceName != "" {
454                 if _, err := db.Exec("INSERT INTO workspacetry (name, hash, source_image_id) VALUES(?, ?, ?)", workspaceName, hash, source); err != nil {
455                         log.Printf("ERROR: Failed to insert into workspacetry table: %q\n", err)
456                 }
457         }
458 }
459
460 type Sources struct {
461         Id int `json:"id"`
462 }
463
464 // sourcesHandler serves up the PNG of a specific try.
465 func sourcesHandler(w http.ResponseWriter, r *http.Request) {
466         log.Printf("Sources Handler: %q\n", r.URL.Path)
467         if r.Method == "GET" {
468                 rows, err := db.Query("SELECT id, create_ts FROM source_images WHERE hidden=0 ORDER BY create_ts DESC")
469
470                 if err != nil {
471                         http.Error(w, fmt.Sprintf("Failed to query sources: %s.", err), 500)
472                 }
473                 sources := make([]Sources, 0, 0)
474                 for rows.Next() {
475                         var id int
476                         var create_ts time.Time
477                         if err := rows.Scan(&id, &create_ts); err != nil {
478                                 log.Printf("Error: failed to fetch from database: %q", err)
479                                 continue
480                         }
481                         sources = append(sources, Sources{Id: id})
482                 }
483
484                 resp, err := json.Marshal(sources)
485                 if err != nil {
486                         reportError(w, r, err, "Failed to serialize a response.")
487                         return
488                 }
489                 w.Header().Set("Content-Type", "application/json")
490                 w.Write(resp)
491
492         } else if r.Method == "POST" {
493                 if err := r.ParseMultipartForm(1000000); err != nil {
494                         http.Error(w, fmt.Sprintf("Failed to load image: %s.", err), 500)
495                         return
496                 }
497                 if _, ok := r.MultipartForm.File["upload"]; !ok {
498                         http.Error(w, "Invalid upload.", 500)
499                         return
500                 }
501                 if len(r.MultipartForm.File["upload"]) != 1 {
502                         http.Error(w, "Wrong number of uploads.", 500)
503                         return
504                 }
505                 f, err := r.MultipartForm.File["upload"][0].Open()
506                 if err != nil {
507                         http.Error(w, fmt.Sprintf("Failed to load image: %s.", err), 500)
508                         return
509                 }
510                 defer f.Close()
511                 m, _, err := image.Decode(f)
512                 if err != nil {
513                         http.Error(w, fmt.Sprintf("Failed to decode image: %s.", err), 500)
514                         return
515                 }
516                 var b bytes.Buffer
517                 png.Encode(&b, m)
518                 bounds := m.Bounds()
519                 width := bounds.Max.Y - bounds.Min.Y
520                 height := bounds.Max.X - bounds.Min.X
521                 if _, err := db.Exec("INSERT INTO source_images (image, width, height) VALUES(?, ?, ?)", b.Bytes(), width, height); err != nil {
522                         log.Printf("ERROR: Failed to insert sources into database: %q\n", err)
523                         http.Error(w, fmt.Sprintf("Failed to store image: %s.", err), 500)
524                         return
525                 }
526                 go writeOutAllSourceImages()
527
528                 // Now redirect back to where we came from.
529                 http.Redirect(w, r, r.Referer(), 302)
530         } else {
531                 http.NotFound(w, r)
532                 return
533         }
534 }
535
536 // imageHandler serves up the PNG of a specific try.
537 func imageHandler(w http.ResponseWriter, r *http.Request) {
538         log.Printf("Image Handler: %q\n", r.URL.Path)
539         if r.Method != "GET" {
540                 http.NotFound(w, r)
541                 return
542         }
543         match := imageLink.FindStringSubmatch(r.URL.Path)
544         if len(match) != 2 {
545                 http.NotFound(w, r)
546                 return
547         }
548         filename := match[1]
549         w.Header().Set("Content-Type", "image/png")
550         http.ServeFile(w, r, fmt.Sprintf("../../../inout/%s", filename))
551 }
552
553 type Try struct {
554         Hash     string `json:"hash"`
555         Source   int
556         CreateTS string `json:"create_ts"`
557 }
558
559 type Recent struct {
560         Tries    []Try
561         Titlebar Titlebar
562 }
563
564 // recentHandler shows the last 20 tries.
565 func recentHandler(w http.ResponseWriter, r *http.Request) {
566         log.Printf("Recent Handler: %q\n", r.URL.Path)
567
568         var err error
569         rows, err := db.Query("SELECT create_ts, hash FROM webtry ORDER BY create_ts DESC LIMIT 20")
570         if err != nil {
571                 http.NotFound(w, r)
572                 return
573         }
574         recent := []Try{}
575         for rows.Next() {
576                 var hash string
577                 var create_ts time.Time
578                 if err := rows.Scan(&create_ts, &hash); err != nil {
579                         log.Printf("Error: failed to fetch from database: %q", err)
580                         continue
581                 }
582                 recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
583         }
584         w.Header().Set("Content-Type", "text/html")
585         if err := recentTemplate.Execute(w, Recent{Tries: recent, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
586                 log.Printf("ERROR: Failed to expand template: %q\n", err)
587         }
588 }
589
590 type Workspace struct {
591         Name     string
592         Code     string
593         Hash     string
594         Source   int
595         Tries    []Try
596         Titlebar Titlebar
597 }
598
599 // newWorkspace generates a new random workspace name and stores it in the database.
600 func newWorkspace() (string, error) {
601         for i := 0; i < 10; i++ {
602                 adj := workspaceNameAdj[rand.Intn(len(workspaceNameAdj))]
603                 noun := workspaceNameNoun[rand.Intn(len(workspaceNameNoun))]
604                 suffix := rand.Intn(1000)
605                 name := fmt.Sprintf("%s-%s-%d", adj, noun, suffix)
606                 if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", name); err == nil {
607                         return name, nil
608                 } else {
609                         log.Printf("ERROR: Failed to insert workspace into database: %q\n", err)
610                 }
611         }
612         return "", fmt.Errorf("Failed to create a new workspace")
613 }
614
615 // getCode returns the code for a given hash, or the empty string if not found.
616 func getCode(hash string) (string, int, error) {
617         code := ""
618         source := 0
619         if err := db.QueryRow("SELECT code, source_image_id FROM webtry WHERE hash=?", hash).Scan(&code, &source); err != nil {
620                 log.Printf("ERROR: Code for hash is missing: %q\n", err)
621                 return code, source, err
622         }
623         return code, source, nil
624 }
625
626 func workspaceHandler(w http.ResponseWriter, r *http.Request) {
627         log.Printf("Workspace Handler: %q\n", r.URL.Path)
628         if r.Method == "GET" {
629                 tries := []Try{}
630                 match := workspaceLink.FindStringSubmatch(r.URL.Path)
631                 name := ""
632                 if len(match) == 2 {
633                         name = match[1]
634                         rows, err := db.Query("SELECT create_ts, hash, source_image_id FROM workspacetry WHERE name=? ORDER BY create_ts", name)
635                         if err != nil {
636                                 reportError(w, r, err, "Failed to select.")
637                                 return
638                         }
639                         for rows.Next() {
640                                 var hash string
641                                 var create_ts time.Time
642                                 var source int
643                                 if err := rows.Scan(&create_ts, &hash, &source); err != nil {
644                                         log.Printf("Error: failed to fetch from database: %q", err)
645                                         continue
646                                 }
647                                 tries = append(tries, Try{Hash: hash, Source: source, CreateTS: create_ts.Format("2006-02-01")})
648                         }
649                 }
650                 var code string
651                 var hash string
652                 source := 0
653                 if len(tries) == 0 {
654                         code = DEFAULT_SAMPLE
655                 } else {
656                         hash = tries[len(tries)-1].Hash
657                         code, source, _ = getCode(hash)
658                 }
659                 w.Header().Set("Content-Type", "text/html")
660                 if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, Code: code, Name: name, Hash: hash, Source: source, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
661                         log.Printf("ERROR: Failed to expand template: %q\n", err)
662                 }
663         } else if r.Method == "POST" {
664                 name, err := newWorkspace()
665                 if err != nil {
666                         http.Error(w, "Failed to create a new workspace.", 500)
667                         return
668                 }
669                 http.Redirect(w, r, "/w/"+name, 302)
670         }
671 }
672
673 // hasPreProcessor returns true if any line in the code begins with a # char.
674 func hasPreProcessor(code string) bool {
675         lines := strings.Split(code, "\n")
676         for _, s := range lines {
677                 if strings.HasPrefix(strings.TrimSpace(s), "#") {
678                         return true
679                 }
680         }
681         return false
682 }
683
684 type TryRequest struct {
685         Code   string `json:"code"`
686         Name   string `json:"name"`   // Optional name of the workspace the code is in.
687         Source int    `json:"source"` // ID of the source image, 0 if none.
688 }
689
690 // iframeHandler handles the GET and POST of the main page.
691 func iframeHandler(w http.ResponseWriter, r *http.Request) {
692         log.Printf("IFrame Handler: %q\n", r.URL.Path)
693         if r.Method != "GET" {
694                 http.NotFound(w, r)
695                 return
696         }
697         match := iframeLink.FindStringSubmatch(r.URL.Path)
698         if len(match) != 2 {
699                 http.NotFound(w, r)
700                 return
701         }
702         hash := match[1]
703         if db == nil {
704                 http.NotFound(w, r)
705                 return
706         }
707         var code string
708         code, source, err := getCode(hash)
709         if err != nil {
710                 http.NotFound(w, r)
711                 return
712         }
713         // Expand the template.
714         w.Header().Set("Content-Type", "text/html")
715         if err := iframeTemplate.Execute(w, userCode{Code: code, Hash: hash, Source: source}); err != nil {
716                 log.Printf("ERROR: Failed to expand template: %q\n", err)
717         }
718 }
719
720 type TryInfo struct {
721         Hash   string `json:"hash"`
722         Code   string `json:"code"`
723         Source int    `json:"source"`
724 }
725
726 // tryInfoHandler returns information about a specific try.
727 func tryInfoHandler(w http.ResponseWriter, r *http.Request) {
728         log.Printf("Try Info Handler: %q\n", r.URL.Path)
729         if r.Method != "GET" {
730                 http.NotFound(w, r)
731                 return
732         }
733         match := tryInfoLink.FindStringSubmatch(r.URL.Path)
734         if len(match) != 2 {
735                 http.NotFound(w, r)
736                 return
737         }
738         hash := match[1]
739         code, source, err := getCode(hash)
740         if err != nil {
741                 http.NotFound(w, r)
742                 return
743         }
744         m := TryInfo{
745                 Hash:   hash,
746                 Code:   code,
747                 Source: source,
748         }
749         resp, err := json.Marshal(m)
750         if err != nil {
751                 reportError(w, r, err, "Failed to serialize a response.")
752                 return
753         }
754         w.Header().Set("Content-Type", "application/json")
755         w.Write(resp)
756 }
757
758 func cleanCompileOutput(s, hash string) string {
759         old := "../../../cache/src/" + hash + ".cpp:"
760         log.Printf("INFO: replacing %q\n", old)
761         return strings.Replace(s, old, "usercode.cpp:", -1)
762 }
763
764 // mainHandler handles the GET and POST of the main page.
765 func mainHandler(w http.ResponseWriter, r *http.Request) {
766         log.Printf("Main Handler: %q\n", r.URL.Path)
767         requestsCounter.Inc(1)
768         if r.Method == "GET" {
769                 code := DEFAULT_SAMPLE
770                 source := 0
771                 match := directLink.FindStringSubmatch(r.URL.Path)
772                 var hash string
773                 if len(match) == 2 && r.URL.Path != "/" {
774                         hash = match[1]
775                         if db == nil {
776                                 http.NotFound(w, r)
777                                 return
778                         }
779                         // Update 'code' with the code found in the database.
780                         if err := db.QueryRow("SELECT code, source_image_id FROM webtry WHERE hash=?", hash).Scan(&code, &source); err != nil {
781                                 http.NotFound(w, r)
782                                 return
783                         }
784                 }
785                 // Expand the template.
786                 w.Header().Set("Content-Type", "text/html")
787                 if err := indexTemplate.Execute(w, userCode{Code: code, Hash: hash, Source: source, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
788                         log.Printf("ERROR: Failed to expand template: %q\n", err)
789                 }
790         } else if r.Method == "POST" {
791                 w.Header().Set("Content-Type", "application/json")
792                 buf := bytes.NewBuffer(make([]byte, 0, MAX_TRY_SIZE))
793                 n, err := buf.ReadFrom(r.Body)
794                 if err != nil {
795                         reportTryError(w, r, err, "Failed to read a request body.", "")
796                         return
797                 }
798                 if n == MAX_TRY_SIZE {
799                         err := fmt.Errorf("Code length equal to, or exceeded, %d", MAX_TRY_SIZE)
800                         reportTryError(w, r, err, "Code too large.", "")
801                         return
802                 }
803                 request := TryRequest{}
804                 if err := json.Unmarshal(buf.Bytes(), &request); err != nil {
805                         reportTryError(w, r, err, "Coulnd't decode JSON.", "")
806                         return
807                 }
808                 if hasPreProcessor(request.Code) {
809                         err := fmt.Errorf("Found preprocessor macro in code.")
810                         reportTryError(w, r, err, "Preprocessor macros aren't allowed.", "")
811                         return
812                 }
813                 hash, err := expandCode(LineNumbers(request.Code), request.Source)
814                 if err != nil {
815                         reportTryError(w, r, err, "Failed to write the code to compile.", hash)
816                         return
817                 }
818                 writeToDatabase(hash, request.Code, request.Name, request.Source)
819                 err = expandGyp(hash)
820                 if err != nil {
821                         reportTryError(w, r, err, "Failed to write the gyp file.", hash)
822                         return
823                 }
824                 message, err := doCmd(fmt.Sprintf(RUN_GYP, hash), true)
825                 if err != nil {
826                         message = cleanCompileOutput(message, hash)
827                         reportTryError(w, r, err, message, hash)
828                         return
829                 }
830                 linkMessage, err := doCmd(fmt.Sprintf(RUN_NINJA, hash), true)
831                 if err != nil {
832                         linkMessage = cleanCompileOutput(linkMessage, hash)
833                         reportTryError(w, r, err, linkMessage, hash)
834                         return
835                 }
836                 message += linkMessage
837                 cmd := hash + " --out " + hash + ".png"
838                 if request.Source > 0 {
839                         cmd += fmt.Sprintf("  --source image-%d.png", request.Source)
840                 }
841                 if *useChroot {
842                         cmd = "schroot -c webtry --directory=/inout -- /inout/Release/" + cmd
843                 } else {
844                         abs, err := filepath.Abs("../../../inout/Release")
845                         if err != nil {
846                                 reportTryError(w, r, err, "Failed to find executable directory.", hash)
847                                 return
848                         }
849                         cmd = abs + "/" + cmd
850                 }
851
852                 execMessage, err := doCmd(cmd, false)
853                 if err != nil {
854                         reportTryError(w, r, err, "Failed to run the code:\n"+execMessage, hash)
855                         return
856                 }
857                 png, err := ioutil.ReadFile("../../../inout/" + hash + ".png")
858                 if err != nil {
859                         reportTryError(w, r, err, "Failed to open the generated PNG.", hash)
860                         return
861                 }
862
863                 m := response{
864                         Message: message,
865                         StdOut:  execMessage,
866                         Img:     base64.StdEncoding.EncodeToString([]byte(png)),
867                         Hash:    hash,
868                 }
869                 resp, err := json.Marshal(m)
870                 if err != nil {
871                         reportTryError(w, r, err, "Failed to serialize a response.", hash)
872                         return
873                 }
874                 w.Header().Set("Content-Type", "application/json")
875                 w.Write(resp)
876         }
877 }
878
879 func main() {
880         flag.Parse()
881         http.HandleFunc("/i/", autogzip.HandleFunc(imageHandler))
882         http.HandleFunc("/w/", autogzip.HandleFunc(workspaceHandler))
883         http.HandleFunc("/recent/", autogzip.HandleFunc(recentHandler))
884         http.HandleFunc("/iframe/", autogzip.HandleFunc(iframeHandler))
885         http.HandleFunc("/json/", autogzip.HandleFunc(tryInfoHandler))
886         http.HandleFunc("/sources/", autogzip.HandleFunc(sourcesHandler))
887
888         // Resources are served directly
889         // TODO add support for caching/etags/gzip
890         http.Handle("/res/", autogzip.Handle(http.FileServer(http.Dir("./"))))
891
892         // TODO Break out /c/ as it's own handler.
893         http.HandleFunc("/", autogzip.HandleFunc(mainHandler))
894         log.Fatal(http.ListenAndServe(*port, nil))
895 }