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