blob: 0c5186e32adce39b4e8a1f392db2a4fcf870b0ce [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 (
commit-bot@chromium.org89126a62014-04-30 19:38:51 +000028 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`
29 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/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`
fmalita@google.com950306c2014-05-01 15:14:56 +000030 DEFAULT_SAMPLE = `void draw(SkCanvas* canvas) {
31 SkPaint p;
32 p.setColor(SK_ColorRED);
33 p.setAntiAlias(true);
34 p.setStyle(SkPaint::kStroke_Style);
35 p.setStrokeWidth(10);
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000036
fmalita@google.com950306c2014-05-01 15:14:56 +000037 canvas->drawLine(20, 20, 100, 100, p);
38}`
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +000039 // Don't increase above 2^16 w/o altering the db tables to accept something bigger than TEXT.
40 MAX_TRY_SIZE = 64000
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000041)
42
43var (
44 // codeTemplate is the cpp code template the user's code is copied into.
45 codeTemplate *template.Template = nil
46
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000047 // indexTemplate is the main index.html page we serve.
48 indexTemplate *htemplate.Template = nil
49
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +000050 // iframeTemplate is the main index.html page we serve.
51 iframeTemplate *htemplate.Template = nil
52
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000053 // recentTemplate is a list of recent images.
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000054 recentTemplate *htemplate.Template = nil
commit-bot@chromium.org282333f2014-04-14 14:54:07 +000055
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000056 // workspaceTemplate is the page for workspaces, a series of webtrys.
57 workspaceTemplate *htemplate.Template = nil
58
commit-bot@chromium.org282333f2014-04-14 14:54:07 +000059 // db is the database, nil if we don't have an SQL database to store data into.
60 db *sql.DB = nil
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000061
62 // directLink is the regex that matches URLs paths that are direct links.
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000063 directLink = regexp.MustCompile("^/c/([a-f0-9]+)$")
64
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +000065 // iframeLink is the regex that matches URLs paths that are links to iframes.
66 iframeLink = regexp.MustCompile("^/iframe/([a-f0-9]+)$")
67
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000068 // imageLink is the regex that matches URLs paths that are direct links to PNGs.
69 imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$")
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000070
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +000071 // tryInfoLink is the regex that matches URLs paths that are direct links to data about a single try.
72 tryInfoLink = regexp.MustCompile("^/json/([a-f0-9]+)$")
73
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000074 // workspaceLink is the regex that matches URLs paths for workspaces.
75 workspaceLink = regexp.MustCompile("^/w/([a-z0-9-]+)$")
76
77 // workspaceNameAdj is a list of adjectives for building workspace names.
78 workspaceNameAdj = []string{
79 "autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark",
80 "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter",
81 "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue",
82 "billowing", "broken", "cold", "damp", "falling", "frosty", "green",
83 "long", "late", "lingering", "bold", "little", "morning", "muddy", "old",
84 "red", "rough", "still", "small", "sparkling", "throbbing", "shy",
85 "wandering", "withered", "wild", "black", "young", "holy", "solitary",
86 "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine",
87 "polished", "ancient", "purple", "lively", "nameless",
88 }
89
90 // workspaceNameNoun is a list of nouns for building workspace names.
91 workspaceNameNoun = []string{
92 "waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning",
93 "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter",
94 "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook",
95 "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly",
96 "feather", "grass", "haze", "mountain", "night", "pond", "darkness",
97 "snowflake", "silence", "sound", "sky", "shape", "surf", "thunder",
98 "violet", "water", "wildflower", "wave", "water", "resonance", "sun",
99 "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "paper",
100 "frog", "smoke", "star",
101 }
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000102
103 gitHash = ""
104 gitInfo = ""
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000105)
106
107// flags
108var (
109 useChroot = flag.Bool("use_chroot", false, "Run the compiled code in the schroot jail.")
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000110 port = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000111)
112
113// lineNumbers adds #line numbering to the user's code.
114func LineNumbers(c string) string {
115 lines := strings.Split(c, "\n")
116 ret := []string{}
117 for i, line := range lines {
118 ret = append(ret, fmt.Sprintf("#line %d", i+1))
119 ret = append(ret, line)
120 }
121 return strings.Join(ret, "\n")
122}
123
124func init() {
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000125 rand.Seed(time.Now().UnixNano())
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000126
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000127 // Change the current working directory to the directory of the executable.
128 var err error
129 cwd, err := filepath.Abs(filepath.Dir(os.Args[0]))
130 if err != nil {
131 log.Fatal(err)
132 }
133 os.Chdir(cwd)
134
135 codeTemplate, err = template.ParseFiles(filepath.Join(cwd, "templates/template.cpp"))
136 if err != nil {
137 panic(err)
138 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000139 indexTemplate, err = htemplate.ParseFiles(
140 filepath.Join(cwd, "templates/index.html"),
141 filepath.Join(cwd, "templates/titlebar.html"),
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000142 filepath.Join(cwd, "templates/content.html"),
commit-bot@chromium.org015c08e2014-05-19 16:40:08 +0000143 filepath.Join(cwd, "templates/headercommon.html"),
144 filepath.Join(cwd, "templates/footercommon.html"),
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000145 )
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000146 if err != nil {
147 panic(err)
148 }
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000149 iframeTemplate, err = htemplate.ParseFiles(
150 filepath.Join(cwd, "templates/iframe.html"),
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000151 filepath.Join(cwd, "templates/content.html"),
commit-bot@chromium.org015c08e2014-05-19 16:40:08 +0000152 filepath.Join(cwd, "templates/headercommon.html"),
153 filepath.Join(cwd, "templates/footercommon.html"),
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000154 )
155 if err != nil {
156 panic(err)
157 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000158 recentTemplate, err = htemplate.ParseFiles(
159 filepath.Join(cwd, "templates/recent.html"),
160 filepath.Join(cwd, "templates/titlebar.html"),
commit-bot@chromium.org015c08e2014-05-19 16:40:08 +0000161 filepath.Join(cwd, "templates/headercommon.html"),
162 filepath.Join(cwd, "templates/footercommon.html"),
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000163 )
164 if err != nil {
165 panic(err)
166 }
167 workspaceTemplate, err = htemplate.ParseFiles(
168 filepath.Join(cwd, "templates/workspace.html"),
169 filepath.Join(cwd, "templates/titlebar.html"),
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000170 filepath.Join(cwd, "templates/content.html"),
commit-bot@chromium.org015c08e2014-05-19 16:40:08 +0000171 filepath.Join(cwd, "templates/headercommon.html"),
172 filepath.Join(cwd, "templates/footercommon.html"),
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000173 )
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000174 if err != nil {
175 panic(err)
176 }
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000177
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000178 // The git command returns output of the format:
179 //
180 // f672cead70404080a991ebfb86c38316a4589b23 2014-04-27 19:21:51 +0000
181 //
182 logOutput, err := doCmd(`git log --format=%H%x20%ai HEAD^..HEAD`, true)
183 if err != nil {
184 panic(err)
185 }
186 logInfo := strings.Split(logOutput, " ")
187 gitHash = logInfo[0]
188 gitInfo = logInfo[1] + " " + logInfo[2] + " " + logInfo[0][0:6]
189
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000190 // Connect to MySQL server. First, get the password from the metadata server.
191 // See https://developers.google.com/compute/docs/metadata#custom.
192 req, err := http.NewRequest("GET", "http://metadata/computeMetadata/v1/instance/attributes/password", nil)
193 if err != nil {
194 panic(err)
195 }
196 client := http.Client{}
197 req.Header.Add("X-Google-Metadata-Request", "True")
198 if resp, err := client.Do(req); err == nil {
199 password, err := ioutil.ReadAll(resp.Body)
200 if err != nil {
201 log.Printf("ERROR: Failed to read password from metadata server: %q\n", err)
202 panic(err)
203 }
204 // The IP address of the database is found here:
205 // https://console.developers.google.com/project/31977622648/sql/instances/webtry/overview
206 // And 3306 is the default port for MySQL.
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000207 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 +0000208 if err != nil {
209 log.Printf("ERROR: Failed to open connection to SQL server: %q\n", err)
210 panic(err)
211 }
212 } else {
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000213 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 +0000214 // Fallback to sqlite for local use.
215 db, err = sql.Open("sqlite3", "./webtry.db")
216 if err != nil {
217 log.Printf("ERROR: Failed to open: %q\n", err)
218 panic(err)
219 }
220 sql := `CREATE TABLE webtry (
221 code TEXT DEFAULT '' NOT NULL,
222 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
223 hash CHAR(64) DEFAULT '' NOT NULL,
224 PRIMARY KEY(hash)
225 )`
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000226 _, err = db.Exec(sql)
227 log.Printf("Info: status creating sqlite table for webtry: %q\n", err)
228 sql = `CREATE TABLE workspace (
229 name CHAR(64) DEFAULT '' NOT NULL,
230 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
231 PRIMARY KEY(name)
232 )`
233 _, err = db.Exec(sql)
234 log.Printf("Info: status creating sqlite table for workspace: %q\n", err)
235 sql = `CREATE TABLE workspacetry (
236 name CHAR(64) DEFAULT '' NOT NULL,
237 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
238 hash CHAR(64) DEFAULT '' NOT NULL,
239 hidden INTEGER DEFAULT 0 NOT NULL,
240
241 FOREIGN KEY (name) REFERENCES workspace(name)
242 )`
243 _, err = db.Exec(sql)
244 log.Printf("Info: status creating sqlite table for workspace try: %q\n", err)
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000245 }
commit-bot@chromium.org9f3b9252014-05-14 12:55:34 +0000246
247 // Ping the database to keep the connection fresh.
248 go func() {
249 c := time.Tick(1 * time.Minute)
250 for _ = range c {
251 if err := db.Ping(); err != nil {
252 log.Printf("ERROR: Database failed to respond: %q\n", err)
253 }
254 }
255 }()
256
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000257}
258
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000259// Titlebar is used in titlebar template expansion.
260type Titlebar struct {
261 GitHash string
262 GitInfo string
263}
264
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000265// userCode is used in template expansion.
266type userCode struct {
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000267 Code string
268 Hash string
269 Titlebar Titlebar
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000270}
271
272// expandToFile expands the template and writes the result to the file.
273func expandToFile(filename string, code string, t *template.Template) error {
274 f, err := os.Create(filename)
275 if err != nil {
276 return err
277 }
278 defer f.Close()
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000279 return t.Execute(f, userCode{Code: code, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}})
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000280}
281
282// expandCode expands the template into a file and calculate the MD5 hash.
283func expandCode(code string) (string, error) {
284 h := md5.New()
285 h.Write([]byte(code))
286 hash := fmt.Sprintf("%x", h.Sum(nil))
287 // At this point we are running in skia/experimental/webtry, making cache a
288 // peer directory to skia.
289 // TODO(jcgregorio) Make all relative directories into flags.
290 err := expandToFile(fmt.Sprintf("../../../cache/%s.cpp", hash), code, codeTemplate)
291 return hash, err
292}
293
294// response is serialized to JSON as a response to POSTs.
295type response struct {
296 Message string `json:"message"`
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000297 StdOut string `json:"stdout"`
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000298 Img string `json:"img"`
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000299 Hash string `json:"hash"`
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000300}
301
302// doCmd executes the given command line string in either the out/Debug
commit-bot@chromium.org15b29812014-04-28 14:56:32 +0000303// directory or the inout directory. Returns the stdout and stderr.
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000304func doCmd(commandLine string, moveToDebug bool) (string, error) {
305 log.Printf("Command: %q\n", commandLine)
306 programAndArgs := strings.SplitN(commandLine, " ", 2)
307 program := programAndArgs[0]
308 args := []string{}
309 if len(programAndArgs) > 1 {
310 args = strings.Split(programAndArgs[1], " ")
311 }
312 cmd := exec.Command(program, args...)
313 abs, err := filepath.Abs("../../out/Debug")
314 if err != nil {
315 return "", fmt.Errorf("Failed to find absolute path to Debug directory.")
316 }
317 if moveToDebug {
318 cmd.Dir = abs
319 } else if !*useChroot { // Don't set cmd.Dir when using chroot.
320 abs, err := filepath.Abs("../../../inout")
321 if err != nil {
322 return "", fmt.Errorf("Failed to find absolute path to inout directory.")
323 }
324 cmd.Dir = abs
325 }
326 log.Printf("Run in directory: %q\n", cmd.Dir)
commit-bot@chromium.org15b29812014-04-28 14:56:32 +0000327 message, err := cmd.CombinedOutput()
328 log.Printf("StdOut + StdErr: %s\n", string(message))
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000329 if err != nil {
330 log.Printf("Exit status: %s\n", err.Error())
commit-bot@chromium.org15b29812014-04-28 14:56:32 +0000331 return string(message), fmt.Errorf("Failed to run command.")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000332 }
commit-bot@chromium.org15b29812014-04-28 14:56:32 +0000333 return string(message), nil
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000334}
335
336// reportError formats an HTTP error response and also logs the detailed error message.
337func reportError(w http.ResponseWriter, r *http.Request, err error, message string) {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000338 log.Printf("Error: %s\n%s", message, err.Error())
339 http.Error(w, message, 500)
340}
341
342// reportTryError formats an HTTP error response in JSON and also logs the detailed error message.
343func reportTryError(w http.ResponseWriter, r *http.Request, err error, message, hash string) {
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000344 m := response{
345 Message: message,
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000346 Hash: hash,
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000347 }
348 log.Printf("Error: %s\n%s", message, err.Error())
349 resp, err := json.Marshal(m)
350 if err != nil {
351 http.Error(w, "Failed to serialize a response", 500)
352 return
353 }
354 w.Write(resp)
355}
356
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000357func writeToDatabase(hash string, code string, workspaceName string) {
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000358 if db == nil {
359 return
360 }
361 if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", code, hash); err != nil {
362 log.Printf("ERROR: Failed to insert code into database: %q\n", err)
363 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000364 if workspaceName != "" {
365 if _, err := db.Exec("INSERT INTO workspacetry (name, hash) VALUES(?, ?)", workspaceName, hash); err != nil {
366 log.Printf("ERROR: Failed to insert into workspacetry table: %q\n", err)
367 }
368 }
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000369}
370
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000371// imageHandler serves up the PNG of a specific try.
372func imageHandler(w http.ResponseWriter, r *http.Request) {
373 log.Printf("Image Handler: %q\n", r.URL.Path)
374 if r.Method != "GET" {
375 http.NotFound(w, r)
376 return
377 }
378 match := imageLink.FindStringSubmatch(r.URL.Path)
379 if len(match) != 2 {
380 http.NotFound(w, r)
381 return
382 }
383 filename := match[1]
384 http.ServeFile(w, r, fmt.Sprintf("../../../inout/%s", filename))
385}
386
387type Try struct {
commit-bot@chromium.orgc3b738a2014-04-21 17:36:44 +0000388 Hash string `json:"hash"`
389 CreateTS string `json:"create_ts"`
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000390}
391
392type Recent struct {
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000393 Tries []Try
394 Titlebar Titlebar
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000395}
396
397// recentHandler shows the last 20 tries.
398func recentHandler(w http.ResponseWriter, r *http.Request) {
399 log.Printf("Recent Handler: %q\n", r.URL.Path)
400
401 var err error
402 rows, err := db.Query("SELECT create_ts, hash FROM webtry ORDER BY create_ts DESC LIMIT 20")
403 if err != nil {
404 http.NotFound(w, r)
405 return
406 }
407 recent := []Try{}
408 for rows.Next() {
409 var hash string
410 var create_ts time.Time
411 if err := rows.Scan(&create_ts, &hash); err != nil {
412 log.Printf("Error: failed to fetch from database: %q", err)
413 continue
414 }
415 recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
416 }
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000417 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 +0000418 log.Printf("ERROR: Failed to expand template: %q\n", err)
419 }
420}
421
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000422type Workspace struct {
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000423 Name string
424 Code string
425 Hash string
426 Tries []Try
427 Titlebar Titlebar
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000428}
429
430// newWorkspace generates a new random workspace name and stores it in the database.
431func newWorkspace() (string, error) {
432 for i := 0; i < 10; i++ {
433 adj := workspaceNameAdj[rand.Intn(len(workspaceNameAdj))]
434 noun := workspaceNameNoun[rand.Intn(len(workspaceNameNoun))]
435 suffix := rand.Intn(1000)
436 name := fmt.Sprintf("%s-%s-%d", adj, noun, suffix)
437 if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", name); err == nil {
438 return name, nil
439 } else {
440 log.Printf("ERROR: Failed to insert workspace into database: %q\n", err)
441 }
442 }
443 return "", fmt.Errorf("Failed to create a new workspace")
444}
445
446// getCode returns the code for a given hash, or the empty string if not found.
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000447func getCode(hash string) (string, error) {
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000448 code := ""
449 if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil {
450 log.Printf("ERROR: Code for hash is missing: %q\n", err)
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000451 return code, err
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000452 }
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000453 return code, nil
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000454}
455
456func workspaceHandler(w http.ResponseWriter, r *http.Request) {
457 log.Printf("Workspace Handler: %q\n", r.URL.Path)
458 if r.Method == "GET" {
459 tries := []Try{}
460 match := workspaceLink.FindStringSubmatch(r.URL.Path)
461 name := ""
462 if len(match) == 2 {
463 name = match[1]
commit-bot@chromium.orgc3b738a2014-04-21 17:36:44 +0000464 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 +0000465 if err != nil {
466 reportError(w, r, err, "Failed to select.")
467 return
468 }
469 for rows.Next() {
470 var hash string
471 var create_ts time.Time
472 if err := rows.Scan(&create_ts, &hash); err != nil {
473 log.Printf("Error: failed to fetch from database: %q", err)
474 continue
475 }
476 tries = append(tries, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
477 }
478 }
479 var code string
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000480 var hash string
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000481 if len(tries) == 0 {
482 code = DEFAULT_SAMPLE
483 } else {
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000484 hash = tries[len(tries)-1].Hash
485 code, _ = getCode(hash)
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000486 }
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000487 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 +0000488 log.Printf("ERROR: Failed to expand template: %q\n", err)
489 }
490 } else if r.Method == "POST" {
491 name, err := newWorkspace()
492 if err != nil {
493 http.Error(w, "Failed to create a new workspace.", 500)
494 return
495 }
496 http.Redirect(w, r, "/w/"+name, 302)
497 }
498}
499
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000500// hasPreProcessor returns true if any line in the code begins with a # char.
501func hasPreProcessor(code string) bool {
502 lines := strings.Split(code, "\n")
503 for _, s := range lines {
504 if strings.HasPrefix(strings.TrimSpace(s), "#") {
505 return true
506 }
507 }
508 return false
509}
510
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000511type TryRequest struct {
512 Code string `json:"code"`
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000513 Name string `json:"name"` // Optional name of the workspace the code is in.
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000514}
515
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000516// iframeHandler handles the GET and POST of the main page.
517func iframeHandler(w http.ResponseWriter, r *http.Request) {
518 log.Printf("IFrame Handler: %q\n", r.URL.Path)
519 if r.Method != "GET" {
520 http.NotFound(w, r)
521 return
522 }
523 match := iframeLink.FindStringSubmatch(r.URL.Path)
524 if len(match) != 2 {
525 http.NotFound(w, r)
526 return
527 }
528 hash := match[1]
529 if db == nil {
530 http.NotFound(w, r)
531 return
532 }
533 var code string
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000534 code, err := getCode(hash)
535 if err != nil {
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000536 http.NotFound(w, r)
537 return
538 }
539 // Expand the template.
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000540 if err := iframeTemplate.Execute(w, userCode{Code: code, Hash: hash}); err != nil {
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000541 log.Printf("ERROR: Failed to expand template: %q\n", err)
542 }
543}
544
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000545type TryInfo struct {
546 Hash string `json:"hash"`
547 Code string `json:"code"`
548}
549
550// tryInfoHandler returns information about a specific try.
551func tryInfoHandler(w http.ResponseWriter, r *http.Request) {
552 log.Printf("Try Info Handler: %q\n", r.URL.Path)
553 if r.Method != "GET" {
554 http.NotFound(w, r)
555 return
556 }
557 match := tryInfoLink.FindStringSubmatch(r.URL.Path)
558 if len(match) != 2 {
559 http.NotFound(w, r)
560 return
561 }
562 hash := match[1]
563 code, err := getCode(hash)
564 if err != nil {
565 http.NotFound(w, r)
566 return
567 }
568 m := TryInfo{
569 Hash: hash,
570 Code: code,
571 }
572 resp, err := json.Marshal(m)
573 if err != nil {
574 reportError(w, r, err, "Failed to serialize a response.")
575 return
576 }
577 w.Header().Set("Content-Type", "application/json")
578 w.Write(resp)
579}
580
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000581func cleanCompileOutput(s, hash string) string {
582 old := "../../../cache/" + hash + ".cpp:"
583 log.Printf("INFO: replacing %q\n", old)
584 return strings.Replace(s, old, "usercode.cpp:", -1)
585}
586
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000587// mainHandler handles the GET and POST of the main page.
588func mainHandler(w http.ResponseWriter, r *http.Request) {
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000589 log.Printf("Main Handler: %q\n", r.URL.Path)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000590 if r.Method == "GET" {
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000591 code := DEFAULT_SAMPLE
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000592 match := directLink.FindStringSubmatch(r.URL.Path)
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000593 var hash string
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000594 if len(match) == 2 && r.URL.Path != "/" {
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000595 hash = match[1]
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000596 if db == nil {
597 http.NotFound(w, r)
598 return
599 }
600 // Update 'code' with the code found in the database.
601 if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil {
602 http.NotFound(w, r)
603 return
604 }
605 }
606 // Expand the template.
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000607 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 +0000608 log.Printf("ERROR: Failed to expand template: %q\n", err)
609 }
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000610 } else if r.Method == "POST" {
611 w.Header().Set("Content-Type", "application/json")
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000612 buf := bytes.NewBuffer(make([]byte, 0, MAX_TRY_SIZE))
613 n, err := buf.ReadFrom(r.Body)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000614 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000615 reportTryError(w, r, err, "Failed to read a request body.", "")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000616 return
617 }
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000618 if n == MAX_TRY_SIZE {
619 err := fmt.Errorf("Code length equal to, or exceeded, %d", MAX_TRY_SIZE)
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000620 reportTryError(w, r, err, "Code too large.", "")
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000621 return
622 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000623 request := TryRequest{}
624 if err := json.Unmarshal(buf.Bytes(), &request); err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000625 reportTryError(w, r, err, "Coulnd't decode JSON.", "")
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000626 return
627 }
628 if hasPreProcessor(request.Code) {
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000629 err := fmt.Errorf("Found preprocessor macro in code.")
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000630 reportTryError(w, r, err, "Preprocessor macros aren't allowed.", "")
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000631 return
632 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000633 hash, err := expandCode(LineNumbers(request.Code))
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000634 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000635 reportTryError(w, r, err, "Failed to write the code to compile.", hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000636 return
637 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000638 writeToDatabase(hash, request.Code, request.Name)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000639 message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), true)
640 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000641 message = cleanCompileOutput(message, hash)
642 reportTryError(w, r, err, message, hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000643 return
644 }
645 linkMessage, err := doCmd(fmt.Sprintf(LINK, hash, hash), true)
646 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000647 linkMessage = cleanCompileOutput(linkMessage, hash)
648 reportTryError(w, r, err, linkMessage, hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000649 return
650 }
651 message += linkMessage
652 cmd := hash + " --out " + hash + ".png"
653 if *useChroot {
654 cmd = "schroot -c webtry --directory=/inout -- /inout/" + cmd
655 } else {
656 abs, err := filepath.Abs("../../../inout")
657 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000658 reportTryError(w, r, err, "Failed to find executable directory.", hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000659 return
660 }
661 cmd = abs + "/" + cmd
662 }
663
664 execMessage, err := doCmd(cmd, false)
665 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000666 reportTryError(w, r, err, "Failed to run the code:\n"+execMessage, hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000667 return
668 }
669 png, err := ioutil.ReadFile("../../../inout/" + hash + ".png")
670 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000671 reportTryError(w, r, err, "Failed to open the generated PNG.", hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000672 return
673 }
674
675 m := response{
676 Message: message,
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000677 StdOut: execMessage,
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000678 Img: base64.StdEncoding.EncodeToString([]byte(png)),
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000679 Hash: hash,
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000680 }
681 resp, err := json.Marshal(m)
682 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000683 reportTryError(w, r, err, "Failed to serialize a response.", hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000684 return
685 }
686 w.Write(resp)
687 }
688}
689
690func main() {
691 flag.Parse()
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000692 http.HandleFunc("/i/", imageHandler)
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000693 http.HandleFunc("/w/", workspaceHandler)
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000694 http.HandleFunc("/recent/", recentHandler)
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000695 http.HandleFunc("/iframe/", iframeHandler)
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000696 http.HandleFunc("/json/", tryInfoHandler)
fmalita@google.com950306c2014-05-01 15:14:56 +0000697
698 // Resources are served directly
699 // TODO add support for caching/etags/gzip
700 http.Handle("/res/", http.FileServer(http.Dir("./")))
701
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000702 // TODO Break out /c/ as it's own handler.
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000703 http.HandleFunc("/", mainHandler)
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000704 log.Fatal(http.ListenAndServe(*port, nil))
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000705}