Home | History | Annotate | Download | only in webtry
      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 }
    896