• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1package main
2
3import (
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
31import (
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
38const (
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
55var (
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
125var (
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.
131func 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
141func 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
306func 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.
334type Titlebar struct {
335	GitHash string
336	GitInfo string
337}
338
339// userCode is used in template expansion.
340type 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.
349func 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.
359func 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.
364func 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
377func 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.
382type 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.
391func 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.
424func 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.
431func 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
446func 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
460type Sources struct {
461	Id int `json:"id"`
462}
463
464// sourcesHandler serves up the PNG of a specific try.
465func 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.
537func 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
553type Try struct {
554	Hash     string `json:"hash"`
555	Source   int
556	CreateTS string `json:"create_ts"`
557}
558
559type Recent struct {
560	Tries    []Try
561	Titlebar Titlebar
562}
563
564// recentHandler shows the last 20 tries.
565func 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
590type 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.
600func 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.
616func 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
626func 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.
674func 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
684type 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.
691func 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
720type 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.
727func 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
758func 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.
765func 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
879func 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}
896