blob: 573d9c67e86b2dc32b78eb454b3006a071b67aa4 [file] [log] [blame]
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +00001package main
2
3import (
4 "bytes"
5 "crypto/md5"
commit-bot@chromium.org282333f2014-04-14 14:54:07 +00006 "database/sql"
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +00007 "encoding/base64"
8 "encoding/json"
9 "flag"
10 "fmt"
commit-bot@chromium.org282333f2014-04-14 14:54:07 +000011 _ "github.com/go-sql-driver/mysql"
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000012 _ "github.com/mattn/go-sqlite3"
13 htemplate "html/template"
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000014 "io/ioutil"
15 "log"
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000016 "math/rand"
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000017 "net/http"
18 "os"
19 "os/exec"
20 "path/filepath"
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000021 "regexp"
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000022 "strings"
23 "text/template"
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000024 "time"
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000025)
26
27const (
28 RESULT_COMPILE = `c++ -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 -Wno-c++11-extensions -Werror -m64 -fno-rtti -Wnon-virtual-dtor -c ../../../cache/%s.cpp -o ../../../cache/%s.o`
commit-bot@chromium.orgd6cab4a2014-04-09 21:35:18 +000029 LINK = `c++ -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/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`
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000030 DEFAULT_SAMPLE = `SkPaint p;
31p.setColor(SK_ColorRED);
32p.setAntiAlias(true);
33p.setStyle(SkPaint::kStroke_Style);
34p.setStrokeWidth(10);
35
36canvas->drawLine(20, 20, 100, 100, p);
37`
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +000038 // Don't increase above 2^16 w/o altering the db tables to accept something bigger than TEXT.
39 MAX_TRY_SIZE = 64000
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000040)
41
42var (
43 // codeTemplate is the cpp code template the user's code is copied into.
44 codeTemplate *template.Template = nil
45
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000046 // indexTemplate is the main index.html page we serve.
47 indexTemplate *htemplate.Template = nil
48
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +000049 // iframeTemplate is the main index.html page we serve.
50 iframeTemplate *htemplate.Template = nil
51
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000052 // recentTemplate is a list of recent images.
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000053 recentTemplate *htemplate.Template = nil
commit-bot@chromium.org282333f2014-04-14 14:54:07 +000054
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000055 // workspaceTemplate is the page for workspaces, a series of webtrys.
56 workspaceTemplate *htemplate.Template = nil
57
commit-bot@chromium.org282333f2014-04-14 14:54:07 +000058 // db is the database, nil if we don't have an SQL database to store data into.
59 db *sql.DB = nil
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000060
61 // directLink is the regex that matches URLs paths that are direct links.
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000062 directLink = regexp.MustCompile("^/c/([a-f0-9]+)$")
63
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +000064 // iframeLink is the regex that matches URLs paths that are links to iframes.
65 iframeLink = regexp.MustCompile("^/iframe/([a-f0-9]+)$")
66
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000067 // imageLink is the regex that matches URLs paths that are direct links to PNGs.
68 imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$")
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000069
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +000070 // tryInfoLink is the regex that matches URLs paths that are direct links to data about a single try.
71 tryInfoLink = regexp.MustCompile("^/json/([a-f0-9]+)$")
72
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000073 // workspaceLink is the regex that matches URLs paths for workspaces.
74 workspaceLink = regexp.MustCompile("^/w/([a-z0-9-]+)$")
75
76 // workspaceNameAdj is a list of adjectives for building workspace names.
77 workspaceNameAdj = []string{
78 "autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark",
79 "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter",
80 "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue",
81 "billowing", "broken", "cold", "damp", "falling", "frosty", "green",
82 "long", "late", "lingering", "bold", "little", "morning", "muddy", "old",
83 "red", "rough", "still", "small", "sparkling", "throbbing", "shy",
84 "wandering", "withered", "wild", "black", "young", "holy", "solitary",
85 "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine",
86 "polished", "ancient", "purple", "lively", "nameless",
87 }
88
89 // workspaceNameNoun is a list of nouns for building workspace names.
90 workspaceNameNoun = []string{
91 "waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning",
92 "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter",
93 "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook",
94 "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly",
95 "feather", "grass", "haze", "mountain", "night", "pond", "darkness",
96 "snowflake", "silence", "sound", "sky", "shape", "surf", "thunder",
97 "violet", "water", "wildflower", "wave", "water", "resonance", "sun",
98 "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "paper",
99 "frog", "smoke", "star",
100 }
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000101
102 gitHash = ""
103 gitInfo = ""
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000104)
105
106// flags
107var (
108 useChroot = flag.Bool("use_chroot", false, "Run the compiled code in the schroot jail.")
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000109 port = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000110)
111
112// lineNumbers adds #line numbering to the user's code.
113func LineNumbers(c string) string {
114 lines := strings.Split(c, "\n")
115 ret := []string{}
116 for i, line := range lines {
117 ret = append(ret, fmt.Sprintf("#line %d", i+1))
118 ret = append(ret, line)
119 }
120 return strings.Join(ret, "\n")
121}
122
123func init() {
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000124 rand.Seed(time.Now().UnixNano())
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000125
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000126 // Change the current working directory to the directory of the executable.
127 var err error
128 cwd, err := filepath.Abs(filepath.Dir(os.Args[0]))
129 if err != nil {
130 log.Fatal(err)
131 }
132 os.Chdir(cwd)
133
134 codeTemplate, err = template.ParseFiles(filepath.Join(cwd, "templates/template.cpp"))
135 if err != nil {
136 panic(err)
137 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000138 indexTemplate, err = htemplate.ParseFiles(
139 filepath.Join(cwd, "templates/index.html"),
140 filepath.Join(cwd, "templates/titlebar.html"),
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000141 filepath.Join(cwd, "templates/content.html"),
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000142 )
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000143 if err != nil {
144 panic(err)
145 }
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000146 iframeTemplate, err = htemplate.ParseFiles(
147 filepath.Join(cwd, "templates/iframe.html"),
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000148 filepath.Join(cwd, "templates/content.html"),
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000149 )
150 if err != nil {
151 panic(err)
152 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000153 recentTemplate, err = htemplate.ParseFiles(
154 filepath.Join(cwd, "templates/recent.html"),
155 filepath.Join(cwd, "templates/titlebar.html"),
156 )
157 if err != nil {
158 panic(err)
159 }
160 workspaceTemplate, err = htemplate.ParseFiles(
161 filepath.Join(cwd, "templates/workspace.html"),
162 filepath.Join(cwd, "templates/titlebar.html"),
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000163 filepath.Join(cwd, "templates/content.html"),
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000164 )
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000165 if err != nil {
166 panic(err)
167 }
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000168
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000169 // The git command returns output of the format:
170 //
171 // f672cead70404080a991ebfb86c38316a4589b23 2014-04-27 19:21:51 +0000
172 //
173 logOutput, err := doCmd(`git log --format=%H%x20%ai HEAD^..HEAD`, true)
174 if err != nil {
175 panic(err)
176 }
177 logInfo := strings.Split(logOutput, " ")
178 gitHash = logInfo[0]
179 gitInfo = logInfo[1] + " " + logInfo[2] + " " + logInfo[0][0:6]
180
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000181 // Connect to MySQL server. First, get the password from the metadata server.
182 // See https://developers.google.com/compute/docs/metadata#custom.
183 req, err := http.NewRequest("GET", "http://metadata/computeMetadata/v1/instance/attributes/password", nil)
184 if err != nil {
185 panic(err)
186 }
187 client := http.Client{}
188 req.Header.Add("X-Google-Metadata-Request", "True")
189 if resp, err := client.Do(req); err == nil {
190 password, err := ioutil.ReadAll(resp.Body)
191 if err != nil {
192 log.Printf("ERROR: Failed to read password from metadata server: %q\n", err)
193 panic(err)
194 }
195 // The IP address of the database is found here:
196 // https://console.developers.google.com/project/31977622648/sql/instances/webtry/overview
197 // And 3306 is the default port for MySQL.
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000198 db, err = sql.Open("mysql", fmt.Sprintf("webtry:%s@tcp(173.194.83.52:3306)/webtry?parseTime=true", password))
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000199 if err != nil {
200 log.Printf("ERROR: Failed to open connection to SQL server: %q\n", err)
201 panic(err)
202 }
203 } else {
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000204 log.Printf("INFO: Failed to find metadata, unable to connect to MySQL server (Expected when running locally): %q\n", err)
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000205 // Fallback to sqlite for local use.
206 db, err = sql.Open("sqlite3", "./webtry.db")
207 if err != nil {
208 log.Printf("ERROR: Failed to open: %q\n", err)
209 panic(err)
210 }
211 sql := `CREATE TABLE webtry (
212 code TEXT DEFAULT '' NOT NULL,
213 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
214 hash CHAR(64) DEFAULT '' NOT NULL,
215 PRIMARY KEY(hash)
216 )`
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000217 _, err = db.Exec(sql)
218 log.Printf("Info: status creating sqlite table for webtry: %q\n", err)
219 sql = `CREATE TABLE workspace (
220 name CHAR(64) DEFAULT '' NOT NULL,
221 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
222 PRIMARY KEY(name)
223 )`
224 _, err = db.Exec(sql)
225 log.Printf("Info: status creating sqlite table for workspace: %q\n", err)
226 sql = `CREATE TABLE workspacetry (
227 name CHAR(64) DEFAULT '' NOT NULL,
228 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
229 hash CHAR(64) DEFAULT '' NOT NULL,
230 hidden INTEGER DEFAULT 0 NOT NULL,
231
232 FOREIGN KEY (name) REFERENCES workspace(name)
233 )`
234 _, err = db.Exec(sql)
235 log.Printf("Info: status creating sqlite table for workspace try: %q\n", err)
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000236 }
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000237}
238
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000239// Titlebar is used in titlebar template expansion.
240type Titlebar struct {
241 GitHash string
242 GitInfo string
243}
244
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000245// userCode is used in template expansion.
246type userCode struct {
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000247 Code string
248 Hash string
249 Titlebar Titlebar
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000250}
251
252// expandToFile expands the template and writes the result to the file.
253func expandToFile(filename string, code string, t *template.Template) error {
254 f, err := os.Create(filename)
255 if err != nil {
256 return err
257 }
258 defer f.Close()
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000259 return t.Execute(f, userCode{Code: code, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}})
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000260}
261
262// expandCode expands the template into a file and calculate the MD5 hash.
263func expandCode(code string) (string, error) {
264 h := md5.New()
265 h.Write([]byte(code))
266 hash := fmt.Sprintf("%x", h.Sum(nil))
267 // At this point we are running in skia/experimental/webtry, making cache a
268 // peer directory to skia.
269 // TODO(jcgregorio) Make all relative directories into flags.
270 err := expandToFile(fmt.Sprintf("../../../cache/%s.cpp", hash), code, codeTemplate)
271 return hash, err
272}
273
274// response is serialized to JSON as a response to POSTs.
275type response struct {
276 Message string `json:"message"`
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000277 StdOut string `json:"stdout"`
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000278 Img string `json:"img"`
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000279 Hash string `json:"hash"`
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000280}
281
282// doCmd executes the given command line string in either the out/Debug
commit-bot@chromium.org15b29812014-04-28 14:56:32 +0000283// directory or the inout directory. Returns the stdout and stderr.
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000284func doCmd(commandLine string, moveToDebug bool) (string, error) {
285 log.Printf("Command: %q\n", commandLine)
286 programAndArgs := strings.SplitN(commandLine, " ", 2)
287 program := programAndArgs[0]
288 args := []string{}
289 if len(programAndArgs) > 1 {
290 args = strings.Split(programAndArgs[1], " ")
291 }
292 cmd := exec.Command(program, args...)
293 abs, err := filepath.Abs("../../out/Debug")
294 if err != nil {
295 return "", fmt.Errorf("Failed to find absolute path to Debug directory.")
296 }
297 if moveToDebug {
298 cmd.Dir = abs
299 } else if !*useChroot { // Don't set cmd.Dir when using chroot.
300 abs, err := filepath.Abs("../../../inout")
301 if err != nil {
302 return "", fmt.Errorf("Failed to find absolute path to inout directory.")
303 }
304 cmd.Dir = abs
305 }
306 log.Printf("Run in directory: %q\n", cmd.Dir)
commit-bot@chromium.org15b29812014-04-28 14:56:32 +0000307 message, err := cmd.CombinedOutput()
308 log.Printf("StdOut + StdErr: %s\n", string(message))
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000309 if err != nil {
310 log.Printf("Exit status: %s\n", err.Error())
commit-bot@chromium.org15b29812014-04-28 14:56:32 +0000311 return string(message), fmt.Errorf("Failed to run command.")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000312 }
commit-bot@chromium.org15b29812014-04-28 14:56:32 +0000313 return string(message), nil
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000314}
315
316// reportError formats an HTTP error response and also logs the detailed error message.
317func reportError(w http.ResponseWriter, r *http.Request, err error, message string) {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000318 log.Printf("Error: %s\n%s", message, err.Error())
319 http.Error(w, message, 500)
320}
321
322// reportTryError formats an HTTP error response in JSON and also logs the detailed error message.
323func reportTryError(w http.ResponseWriter, r *http.Request, err error, message, hash string) {
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000324 m := response{
325 Message: message,
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000326 Hash: hash,
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000327 }
328 log.Printf("Error: %s\n%s", message, err.Error())
329 resp, err := json.Marshal(m)
330 if err != nil {
331 http.Error(w, "Failed to serialize a response", 500)
332 return
333 }
334 w.Write(resp)
335}
336
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000337func writeToDatabase(hash string, code string, workspaceName string) {
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000338 if db == nil {
339 return
340 }
341 if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", code, hash); err != nil {
342 log.Printf("ERROR: Failed to insert code into database: %q\n", err)
343 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000344 if workspaceName != "" {
345 if _, err := db.Exec("INSERT INTO workspacetry (name, hash) VALUES(?, ?)", workspaceName, hash); err != nil {
346 log.Printf("ERROR: Failed to insert into workspacetry table: %q\n", err)
347 }
348 }
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000349}
350
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000351func cssHandler(w http.ResponseWriter, r *http.Request) {
352 http.ServeFile(w, r, "css/webtry.css")
353}
354
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000355func jsHandler(w http.ResponseWriter, r *http.Request) {
356 http.ServeFile(w, r, "js/run.js")
357}
358
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000359// imageHandler serves up the PNG of a specific try.
360func imageHandler(w http.ResponseWriter, r *http.Request) {
361 log.Printf("Image Handler: %q\n", r.URL.Path)
362 if r.Method != "GET" {
363 http.NotFound(w, r)
364 return
365 }
366 match := imageLink.FindStringSubmatch(r.URL.Path)
367 if len(match) != 2 {
368 http.NotFound(w, r)
369 return
370 }
371 filename := match[1]
372 http.ServeFile(w, r, fmt.Sprintf("../../../inout/%s", filename))
373}
374
375type Try struct {
commit-bot@chromium.orgc3b738a2014-04-21 17:36:44 +0000376 Hash string `json:"hash"`
377 CreateTS string `json:"create_ts"`
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000378}
379
380type Recent struct {
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000381 Tries []Try
382 Titlebar Titlebar
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000383}
384
385// recentHandler shows the last 20 tries.
386func recentHandler(w http.ResponseWriter, r *http.Request) {
387 log.Printf("Recent Handler: %q\n", r.URL.Path)
388
389 var err error
390 rows, err := db.Query("SELECT create_ts, hash FROM webtry ORDER BY create_ts DESC LIMIT 20")
391 if err != nil {
392 http.NotFound(w, r)
393 return
394 }
395 recent := []Try{}
396 for rows.Next() {
397 var hash string
398 var create_ts time.Time
399 if err := rows.Scan(&create_ts, &hash); err != nil {
400 log.Printf("Error: failed to fetch from database: %q", err)
401 continue
402 }
403 recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
404 }
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000405 if err := recentTemplate.Execute(w, Recent{Tries: recent, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000406 log.Printf("ERROR: Failed to expand template: %q\n", err)
407 }
408}
409
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000410type Workspace struct {
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000411 Name string
412 Code string
413 Hash string
414 Tries []Try
415 Titlebar Titlebar
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000416}
417
418// newWorkspace generates a new random workspace name and stores it in the database.
419func newWorkspace() (string, error) {
420 for i := 0; i < 10; i++ {
421 adj := workspaceNameAdj[rand.Intn(len(workspaceNameAdj))]
422 noun := workspaceNameNoun[rand.Intn(len(workspaceNameNoun))]
423 suffix := rand.Intn(1000)
424 name := fmt.Sprintf("%s-%s-%d", adj, noun, suffix)
425 if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", name); err == nil {
426 return name, nil
427 } else {
428 log.Printf("ERROR: Failed to insert workspace into database: %q\n", err)
429 }
430 }
431 return "", fmt.Errorf("Failed to create a new workspace")
432}
433
434// getCode returns the code for a given hash, or the empty string if not found.
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000435func getCode(hash string) (string, error) {
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000436 code := ""
437 if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil {
438 log.Printf("ERROR: Code for hash is missing: %q\n", err)
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000439 return code, err
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000440 }
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000441 return code, nil
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000442}
443
444func workspaceHandler(w http.ResponseWriter, r *http.Request) {
445 log.Printf("Workspace Handler: %q\n", r.URL.Path)
446 if r.Method == "GET" {
447 tries := []Try{}
448 match := workspaceLink.FindStringSubmatch(r.URL.Path)
449 name := ""
450 if len(match) == 2 {
451 name = match[1]
commit-bot@chromium.orgc3b738a2014-04-21 17:36:44 +0000452 rows, err := db.Query("SELECT create_ts, hash FROM workspacetry WHERE name=? ORDER BY create_ts", name)
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000453 if err != nil {
454 reportError(w, r, err, "Failed to select.")
455 return
456 }
457 for rows.Next() {
458 var hash string
459 var create_ts time.Time
460 if err := rows.Scan(&create_ts, &hash); err != nil {
461 log.Printf("Error: failed to fetch from database: %q", err)
462 continue
463 }
464 tries = append(tries, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
465 }
466 }
467 var code string
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000468 var hash string
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000469 if len(tries) == 0 {
470 code = DEFAULT_SAMPLE
471 } else {
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000472 hash = tries[len(tries)-1].Hash
473 code, _ = getCode(hash)
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000474 }
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000475 if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, Code: code, Name: name, Hash: hash, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000476 log.Printf("ERROR: Failed to expand template: %q\n", err)
477 }
478 } else if r.Method == "POST" {
479 name, err := newWorkspace()
480 if err != nil {
481 http.Error(w, "Failed to create a new workspace.", 500)
482 return
483 }
484 http.Redirect(w, r, "/w/"+name, 302)
485 }
486}
487
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000488// hasPreProcessor returns true if any line in the code begins with a # char.
489func hasPreProcessor(code string) bool {
490 lines := strings.Split(code, "\n")
491 for _, s := range lines {
492 if strings.HasPrefix(strings.TrimSpace(s), "#") {
493 return true
494 }
495 }
496 return false
497}
498
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000499type TryRequest struct {
500 Code string `json:"code"`
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000501 Name string `json:"name"` // Optional name of the workspace the code is in.
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000502}
503
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000504// iframeHandler handles the GET and POST of the main page.
505func iframeHandler(w http.ResponseWriter, r *http.Request) {
506 log.Printf("IFrame Handler: %q\n", r.URL.Path)
507 if r.Method != "GET" {
508 http.NotFound(w, r)
509 return
510 }
511 match := iframeLink.FindStringSubmatch(r.URL.Path)
512 if len(match) != 2 {
513 http.NotFound(w, r)
514 return
515 }
516 hash := match[1]
517 if db == nil {
518 http.NotFound(w, r)
519 return
520 }
521 var code string
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000522 code, err := getCode(hash)
523 if err != nil {
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000524 http.NotFound(w, r)
525 return
526 }
527 // Expand the template.
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000528 if err := iframeTemplate.Execute(w, userCode{Code: code, Hash: hash}); err != nil {
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000529 log.Printf("ERROR: Failed to expand template: %q\n", err)
530 }
531}
532
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000533type TryInfo struct {
534 Hash string `json:"hash"`
535 Code string `json:"code"`
536}
537
538// tryInfoHandler returns information about a specific try.
539func tryInfoHandler(w http.ResponseWriter, r *http.Request) {
540 log.Printf("Try Info Handler: %q\n", r.URL.Path)
541 if r.Method != "GET" {
542 http.NotFound(w, r)
543 return
544 }
545 match := tryInfoLink.FindStringSubmatch(r.URL.Path)
546 if len(match) != 2 {
547 http.NotFound(w, r)
548 return
549 }
550 hash := match[1]
551 code, err := getCode(hash)
552 if err != nil {
553 http.NotFound(w, r)
554 return
555 }
556 m := TryInfo{
557 Hash: hash,
558 Code: code,
559 }
560 resp, err := json.Marshal(m)
561 if err != nil {
562 reportError(w, r, err, "Failed to serialize a response.")
563 return
564 }
565 w.Header().Set("Content-Type", "application/json")
566 w.Write(resp)
567}
568
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000569func cleanCompileOutput(s, hash string) string {
570 old := "../../../cache/" + hash + ".cpp:"
571 log.Printf("INFO: replacing %q\n", old)
572 return strings.Replace(s, old, "usercode.cpp:", -1)
573}
574
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000575// mainHandler handles the GET and POST of the main page.
576func mainHandler(w http.ResponseWriter, r *http.Request) {
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000577 log.Printf("Main Handler: %q\n", r.URL.Path)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000578 if r.Method == "GET" {
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000579 code := DEFAULT_SAMPLE
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000580 match := directLink.FindStringSubmatch(r.URL.Path)
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000581 var hash string
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000582 if len(match) == 2 && r.URL.Path != "/" {
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000583 hash = match[1]
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000584 if db == nil {
585 http.NotFound(w, r)
586 return
587 }
588 // Update 'code' with the code found in the database.
589 if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil {
590 http.NotFound(w, r)
591 return
592 }
593 }
594 // Expand the template.
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000595 if err := indexTemplate.Execute(w, userCode{Code: code, Hash: hash, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000596 log.Printf("ERROR: Failed to expand template: %q\n", err)
597 }
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000598 } else if r.Method == "POST" {
599 w.Header().Set("Content-Type", "application/json")
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000600 buf := bytes.NewBuffer(make([]byte, 0, MAX_TRY_SIZE))
601 n, err := buf.ReadFrom(r.Body)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000602 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000603 reportTryError(w, r, err, "Failed to read a request body.", "")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000604 return
605 }
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000606 if n == MAX_TRY_SIZE {
607 err := fmt.Errorf("Code length equal to, or exceeded, %d", MAX_TRY_SIZE)
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000608 reportTryError(w, r, err, "Code too large.", "")
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000609 return
610 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000611 request := TryRequest{}
612 if err := json.Unmarshal(buf.Bytes(), &request); err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000613 reportTryError(w, r, err, "Coulnd't decode JSON.", "")
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000614 return
615 }
616 if hasPreProcessor(request.Code) {
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000617 err := fmt.Errorf("Found preprocessor macro in code.")
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000618 reportTryError(w, r, err, "Preprocessor macros aren't allowed.", "")
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000619 return
620 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000621 hash, err := expandCode(LineNumbers(request.Code))
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000622 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000623 reportTryError(w, r, err, "Failed to write the code to compile.", hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000624 return
625 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000626 writeToDatabase(hash, request.Code, request.Name)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000627 message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), true)
628 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000629 message = cleanCompileOutput(message, hash)
630 reportTryError(w, r, err, message, hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000631 return
632 }
633 linkMessage, err := doCmd(fmt.Sprintf(LINK, hash, hash), true)
634 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000635 linkMessage = cleanCompileOutput(linkMessage, hash)
636 reportTryError(w, r, err, linkMessage, hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000637 return
638 }
639 message += linkMessage
640 cmd := hash + " --out " + hash + ".png"
641 if *useChroot {
642 cmd = "schroot -c webtry --directory=/inout -- /inout/" + cmd
643 } else {
644 abs, err := filepath.Abs("../../../inout")
645 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000646 reportTryError(w, r, err, "Failed to find executable directory.", hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000647 return
648 }
649 cmd = abs + "/" + cmd
650 }
651
652 execMessage, err := doCmd(cmd, false)
653 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000654 reportTryError(w, r, err, "Failed to run the code:\n"+execMessage, hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000655 return
656 }
657 png, err := ioutil.ReadFile("../../../inout/" + hash + ".png")
658 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000659 reportTryError(w, r, err, "Failed to open the generated PNG.", hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000660 return
661 }
662
663 m := response{
664 Message: message,
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000665 StdOut: execMessage,
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000666 Img: base64.StdEncoding.EncodeToString([]byte(png)),
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000667 Hash: hash,
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000668 }
669 resp, err := json.Marshal(m)
670 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000671 reportTryError(w, r, err, "Failed to serialize a response.", hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000672 return
673 }
674 w.Write(resp)
675 }
676}
677
678func main() {
679 flag.Parse()
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000680 http.HandleFunc("/i/", imageHandler)
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000681 http.HandleFunc("/w/", workspaceHandler)
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000682 http.HandleFunc("/recent/", recentHandler)
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000683 http.HandleFunc("/iframe/", iframeHandler)
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000684 http.HandleFunc("/json/", tryInfoHandler)
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000685 http.HandleFunc("/css/", cssHandler)
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000686 http.HandleFunc("/js/", jsHandler)
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000687 // TODO Break out /c/ as it's own handler.
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000688 http.HandleFunc("/", mainHandler)
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000689 log.Fatal(http.ListenAndServe(*port, nil))
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000690}