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 	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 
     55 var (
     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
    122 var (
    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.
    128 func 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 
    138 func 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 
    299 func 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.
    327 type Titlebar struct {
    328 	GitHash string
    329 	GitInfo string
    330 }
    331 
    332 // userCode is used in template expansion.
    333 type 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.
    341 func 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.
    351 func 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.
    364 type 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.
    373 func 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.
    406 func 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.
    413 func 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 
    428 func 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 
    442 type Sources struct {
    443 	Id int `json:"id"`
    444 }
    445 
    446 // sourcesHandler serves up the PNG of a specific try.
    447 func 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.
    519 func 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 
    535 type Try struct {
    536 	Hash     string `json:"hash"`
    537 	Source   int
    538 	CreateTS string `json:"create_ts"`
    539 }
    540 
    541 type Recent struct {
    542 	Tries    []Try
    543 	Titlebar Titlebar
    544 }
    545 
    546 // recentHandler shows the last 20 tries.
    547 func 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 
    572 type 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.
    582 func 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.
    598 func 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 
    608 func 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.
    656 func 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 
    666 type 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.
    673 func 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 
    702 type 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.
    709 func 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 
    740 func 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.
    747 func 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 
    856 func 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