blob: 2c3d7c92e6e07e234a35d2a69da33744b8e10836 [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.orgc81d1c42014-04-14 18:53:10 +000011 htemplate "html/template"
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000012 "io/ioutil"
13 "log"
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000014 "math/rand"
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000015 "net/http"
16 "os"
17 "os/exec"
18 "path/filepath"
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000019 "regexp"
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000020 "strings"
21 "text/template"
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000022 "time"
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000023)
24
commit-bot@chromium.orga4c36772014-05-19 17:38:52 +000025import (
26 "github.com/fiorix/go-web/autogzip"
27 _ "github.com/go-sql-driver/mysql"
28 _ "github.com/mattn/go-sqlite3"
29)
30
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000031const (
commit-bot@chromium.org89126a62014-04-30 19:38:51 +000032 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`
33 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 +000034 DEFAULT_SAMPLE = `void draw(SkCanvas* canvas) {
35 SkPaint p;
36 p.setColor(SK_ColorRED);
37 p.setAntiAlias(true);
38 p.setStyle(SkPaint::kStroke_Style);
39 p.setStrokeWidth(10);
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000040
fmalita@google.com950306c2014-05-01 15:14:56 +000041 canvas->drawLine(20, 20, 100, 100, p);
42}`
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +000043 // Don't increase above 2^16 w/o altering the db tables to accept something bigger than TEXT.
44 MAX_TRY_SIZE = 64000
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000045)
46
47var (
48 // codeTemplate is the cpp code template the user's code is copied into.
49 codeTemplate *template.Template = nil
50
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000051 // indexTemplate is the main index.html page we serve.
52 indexTemplate *htemplate.Template = nil
53
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +000054 // iframeTemplate is the main index.html page we serve.
55 iframeTemplate *htemplate.Template = nil
56
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000057 // recentTemplate is a list of recent images.
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000058 recentTemplate *htemplate.Template = nil
commit-bot@chromium.org282333f2014-04-14 14:54:07 +000059
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000060 // workspaceTemplate is the page for workspaces, a series of webtrys.
61 workspaceTemplate *htemplate.Template = nil
62
commit-bot@chromium.org282333f2014-04-14 14:54:07 +000063 // db is the database, nil if we don't have an SQL database to store data into.
64 db *sql.DB = nil
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000065
66 // directLink is the regex that matches URLs paths that are direct links.
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000067 directLink = regexp.MustCompile("^/c/([a-f0-9]+)$")
68
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +000069 // iframeLink is the regex that matches URLs paths that are links to iframes.
70 iframeLink = regexp.MustCompile("^/iframe/([a-f0-9]+)$")
71
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000072 // imageLink is the regex that matches URLs paths that are direct links to PNGs.
73 imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$")
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000074
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +000075 // tryInfoLink is the regex that matches URLs paths that are direct links to data about a single try.
76 tryInfoLink = regexp.MustCompile("^/json/([a-f0-9]+)$")
77
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +000078 // workspaceLink is the regex that matches URLs paths for workspaces.
79 workspaceLink = regexp.MustCompile("^/w/([a-z0-9-]+)$")
80
81 // workspaceNameAdj is a list of adjectives for building workspace names.
82 workspaceNameAdj = []string{
83 "autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark",
84 "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter",
85 "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue",
86 "billowing", "broken", "cold", "damp", "falling", "frosty", "green",
87 "long", "late", "lingering", "bold", "little", "morning", "muddy", "old",
88 "red", "rough", "still", "small", "sparkling", "throbbing", "shy",
89 "wandering", "withered", "wild", "black", "young", "holy", "solitary",
90 "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine",
91 "polished", "ancient", "purple", "lively", "nameless",
92 }
93
94 // workspaceNameNoun is a list of nouns for building workspace names.
95 workspaceNameNoun = []string{
96 "waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning",
97 "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter",
98 "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook",
99 "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly",
100 "feather", "grass", "haze", "mountain", "night", "pond", "darkness",
101 "snowflake", "silence", "sound", "sky", "shape", "surf", "thunder",
102 "violet", "water", "wildflower", "wave", "water", "resonance", "sun",
103 "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "paper",
104 "frog", "smoke", "star",
105 }
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000106
107 gitHash = ""
108 gitInfo = ""
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000109)
110
111// flags
112var (
113 useChroot = flag.Bool("use_chroot", false, "Run the compiled code in the schroot jail.")
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000114 port = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000115)
116
117// lineNumbers adds #line numbering to the user's code.
118func LineNumbers(c string) string {
119 lines := strings.Split(c, "\n")
120 ret := []string{}
121 for i, line := range lines {
122 ret = append(ret, fmt.Sprintf("#line %d", i+1))
123 ret = append(ret, line)
124 }
125 return strings.Join(ret, "\n")
126}
127
128func init() {
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000129 rand.Seed(time.Now().UnixNano())
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000130
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000131 // Change the current working directory to the directory of the executable.
132 var err error
133 cwd, err := filepath.Abs(filepath.Dir(os.Args[0]))
134 if err != nil {
135 log.Fatal(err)
136 }
137 os.Chdir(cwd)
138
139 codeTemplate, err = template.ParseFiles(filepath.Join(cwd, "templates/template.cpp"))
140 if err != nil {
141 panic(err)
142 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000143 indexTemplate, err = htemplate.ParseFiles(
144 filepath.Join(cwd, "templates/index.html"),
145 filepath.Join(cwd, "templates/titlebar.html"),
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000146 filepath.Join(cwd, "templates/content.html"),
commit-bot@chromium.org015c08e2014-05-19 16:40:08 +0000147 filepath.Join(cwd, "templates/headercommon.html"),
148 filepath.Join(cwd, "templates/footercommon.html"),
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000149 )
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000150 if err != nil {
151 panic(err)
152 }
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000153 iframeTemplate, err = htemplate.ParseFiles(
154 filepath.Join(cwd, "templates/iframe.html"),
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000155 filepath.Join(cwd, "templates/content.html"),
commit-bot@chromium.org015c08e2014-05-19 16:40:08 +0000156 filepath.Join(cwd, "templates/headercommon.html"),
157 filepath.Join(cwd, "templates/footercommon.html"),
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000158 )
159 if err != nil {
160 panic(err)
161 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000162 recentTemplate, err = htemplate.ParseFiles(
163 filepath.Join(cwd, "templates/recent.html"),
164 filepath.Join(cwd, "templates/titlebar.html"),
commit-bot@chromium.org015c08e2014-05-19 16:40:08 +0000165 filepath.Join(cwd, "templates/headercommon.html"),
166 filepath.Join(cwd, "templates/footercommon.html"),
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000167 )
168 if err != nil {
169 panic(err)
170 }
171 workspaceTemplate, err = htemplate.ParseFiles(
172 filepath.Join(cwd, "templates/workspace.html"),
173 filepath.Join(cwd, "templates/titlebar.html"),
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000174 filepath.Join(cwd, "templates/content.html"),
commit-bot@chromium.org015c08e2014-05-19 16:40:08 +0000175 filepath.Join(cwd, "templates/headercommon.html"),
176 filepath.Join(cwd, "templates/footercommon.html"),
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000177 )
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000178 if err != nil {
179 panic(err)
180 }
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000181
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000182 // The git command returns output of the format:
183 //
184 // f672cead70404080a991ebfb86c38316a4589b23 2014-04-27 19:21:51 +0000
185 //
186 logOutput, err := doCmd(`git log --format=%H%x20%ai HEAD^..HEAD`, true)
187 if err != nil {
188 panic(err)
189 }
190 logInfo := strings.Split(logOutput, " ")
191 gitHash = logInfo[0]
192 gitInfo = logInfo[1] + " " + logInfo[2] + " " + logInfo[0][0:6]
193
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000194 // Connect to MySQL server. First, get the password from the metadata server.
195 // See https://developers.google.com/compute/docs/metadata#custom.
196 req, err := http.NewRequest("GET", "http://metadata/computeMetadata/v1/instance/attributes/password", nil)
197 if err != nil {
198 panic(err)
199 }
200 client := http.Client{}
201 req.Header.Add("X-Google-Metadata-Request", "True")
202 if resp, err := client.Do(req); err == nil {
203 password, err := ioutil.ReadAll(resp.Body)
204 if err != nil {
205 log.Printf("ERROR: Failed to read password from metadata server: %q\n", err)
206 panic(err)
207 }
208 // The IP address of the database is found here:
209 // https://console.developers.google.com/project/31977622648/sql/instances/webtry/overview
210 // And 3306 is the default port for MySQL.
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000211 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 +0000212 if err != nil {
213 log.Printf("ERROR: Failed to open connection to SQL server: %q\n", err)
214 panic(err)
215 }
216 } else {
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000217 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 +0000218 // Fallback to sqlite for local use.
219 db, err = sql.Open("sqlite3", "./webtry.db")
220 if err != nil {
221 log.Printf("ERROR: Failed to open: %q\n", err)
222 panic(err)
223 }
224 sql := `CREATE TABLE webtry (
225 code TEXT DEFAULT '' NOT NULL,
226 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
227 hash CHAR(64) DEFAULT '' NOT NULL,
228 PRIMARY KEY(hash)
229 )`
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000230 _, err = db.Exec(sql)
231 log.Printf("Info: status creating sqlite table for webtry: %q\n", err)
232 sql = `CREATE TABLE workspace (
233 name CHAR(64) DEFAULT '' NOT NULL,
234 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
235 PRIMARY KEY(name)
236 )`
237 _, err = db.Exec(sql)
238 log.Printf("Info: status creating sqlite table for workspace: %q\n", err)
239 sql = `CREATE TABLE workspacetry (
240 name CHAR(64) DEFAULT '' NOT NULL,
241 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
242 hash CHAR(64) DEFAULT '' NOT NULL,
243 hidden INTEGER DEFAULT 0 NOT NULL,
244
245 FOREIGN KEY (name) REFERENCES workspace(name)
246 )`
247 _, err = db.Exec(sql)
248 log.Printf("Info: status creating sqlite table for workspace try: %q\n", err)
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000249 }
commit-bot@chromium.org9f3b9252014-05-14 12:55:34 +0000250
251 // Ping the database to keep the connection fresh.
252 go func() {
253 c := time.Tick(1 * time.Minute)
254 for _ = range c {
255 if err := db.Ping(); err != nil {
256 log.Printf("ERROR: Database failed to respond: %q\n", err)
257 }
258 }
259 }()
260
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000261}
262
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000263// Titlebar is used in titlebar template expansion.
264type Titlebar struct {
265 GitHash string
266 GitInfo string
267}
268
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000269// userCode is used in template expansion.
270type userCode struct {
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000271 Code string
272 Hash string
273 Titlebar Titlebar
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000274}
275
276// expandToFile expands the template and writes the result to the file.
277func expandToFile(filename string, code string, t *template.Template) error {
278 f, err := os.Create(filename)
279 if err != nil {
280 return err
281 }
282 defer f.Close()
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000283 return t.Execute(f, userCode{Code: code, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}})
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000284}
285
286// expandCode expands the template into a file and calculate the MD5 hash.
287func expandCode(code string) (string, error) {
288 h := md5.New()
289 h.Write([]byte(code))
290 hash := fmt.Sprintf("%x", h.Sum(nil))
291 // At this point we are running in skia/experimental/webtry, making cache a
292 // peer directory to skia.
293 // TODO(jcgregorio) Make all relative directories into flags.
294 err := expandToFile(fmt.Sprintf("../../../cache/%s.cpp", hash), code, codeTemplate)
295 return hash, err
296}
297
298// response is serialized to JSON as a response to POSTs.
299type response struct {
300 Message string `json:"message"`
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000301 StdOut string `json:"stdout"`
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000302 Img string `json:"img"`
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000303 Hash string `json:"hash"`
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000304}
305
306// doCmd executes the given command line string in either the out/Debug
commit-bot@chromium.org15b29812014-04-28 14:56:32 +0000307// directory or the inout directory. Returns the stdout and stderr.
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000308func doCmd(commandLine string, moveToDebug bool) (string, error) {
309 log.Printf("Command: %q\n", commandLine)
310 programAndArgs := strings.SplitN(commandLine, " ", 2)
311 program := programAndArgs[0]
312 args := []string{}
313 if len(programAndArgs) > 1 {
314 args = strings.Split(programAndArgs[1], " ")
315 }
316 cmd := exec.Command(program, args...)
317 abs, err := filepath.Abs("../../out/Debug")
318 if err != nil {
319 return "", fmt.Errorf("Failed to find absolute path to Debug directory.")
320 }
321 if moveToDebug {
322 cmd.Dir = abs
323 } else if !*useChroot { // Don't set cmd.Dir when using chroot.
324 abs, err := filepath.Abs("../../../inout")
325 if err != nil {
326 return "", fmt.Errorf("Failed to find absolute path to inout directory.")
327 }
328 cmd.Dir = abs
329 }
330 log.Printf("Run in directory: %q\n", cmd.Dir)
commit-bot@chromium.org15b29812014-04-28 14:56:32 +0000331 message, err := cmd.CombinedOutput()
332 log.Printf("StdOut + StdErr: %s\n", string(message))
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000333 if err != nil {
334 log.Printf("Exit status: %s\n", err.Error())
commit-bot@chromium.org15b29812014-04-28 14:56:32 +0000335 return string(message), fmt.Errorf("Failed to run command.")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000336 }
commit-bot@chromium.org15b29812014-04-28 14:56:32 +0000337 return string(message), nil
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000338}
339
340// reportError formats an HTTP error response and also logs the detailed error message.
341func reportError(w http.ResponseWriter, r *http.Request, err error, message string) {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000342 log.Printf("Error: %s\n%s", message, err.Error())
commit-bot@chromium.orga4c36772014-05-19 17:38:52 +0000343 w.Header().Set("Content-Type", "text/plain")
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000344 http.Error(w, message, 500)
345}
346
347// reportTryError formats an HTTP error response in JSON and also logs the detailed error message.
348func reportTryError(w http.ResponseWriter, r *http.Request, err error, message, hash string) {
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000349 m := response{
350 Message: message,
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000351 Hash: hash,
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000352 }
353 log.Printf("Error: %s\n%s", message, err.Error())
354 resp, err := json.Marshal(m)
355 if err != nil {
356 http.Error(w, "Failed to serialize a response", 500)
357 return
358 }
commit-bot@chromium.orga4c36772014-05-19 17:38:52 +0000359 w.Header().Set("Content-Type", "text/plain")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000360 w.Write(resp)
361}
362
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000363func writeToDatabase(hash string, code string, workspaceName string) {
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000364 if db == nil {
365 return
366 }
367 if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", code, hash); err != nil {
368 log.Printf("ERROR: Failed to insert code into database: %q\n", err)
369 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000370 if workspaceName != "" {
371 if _, err := db.Exec("INSERT INTO workspacetry (name, hash) VALUES(?, ?)", workspaceName, hash); err != nil {
372 log.Printf("ERROR: Failed to insert into workspacetry table: %q\n", err)
373 }
374 }
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000375}
376
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000377// imageHandler serves up the PNG of a specific try.
378func imageHandler(w http.ResponseWriter, r *http.Request) {
379 log.Printf("Image Handler: %q\n", r.URL.Path)
380 if r.Method != "GET" {
381 http.NotFound(w, r)
382 return
383 }
384 match := imageLink.FindStringSubmatch(r.URL.Path)
385 if len(match) != 2 {
386 http.NotFound(w, r)
387 return
388 }
389 filename := match[1]
commit-bot@chromium.orga4c36772014-05-19 17:38:52 +0000390 w.Header().Set("Content-Type", "image/png")
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000391 http.ServeFile(w, r, fmt.Sprintf("../../../inout/%s", filename))
392}
393
394type Try struct {
commit-bot@chromium.orgc3b738a2014-04-21 17:36:44 +0000395 Hash string `json:"hash"`
396 CreateTS string `json:"create_ts"`
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000397}
398
399type Recent struct {
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000400 Tries []Try
401 Titlebar Titlebar
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000402}
403
404// recentHandler shows the last 20 tries.
405func recentHandler(w http.ResponseWriter, r *http.Request) {
406 log.Printf("Recent Handler: %q\n", r.URL.Path)
407
408 var err error
409 rows, err := db.Query("SELECT create_ts, hash FROM webtry ORDER BY create_ts DESC LIMIT 20")
410 if err != nil {
411 http.NotFound(w, r)
412 return
413 }
414 recent := []Try{}
415 for rows.Next() {
416 var hash string
417 var create_ts time.Time
418 if err := rows.Scan(&create_ts, &hash); err != nil {
419 log.Printf("Error: failed to fetch from database: %q", err)
420 continue
421 }
422 recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
423 }
commit-bot@chromium.orga4c36772014-05-19 17:38:52 +0000424 w.Header().Set("Content-Type", "text/html")
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000425 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 +0000426 log.Printf("ERROR: Failed to expand template: %q\n", err)
427 }
428}
429
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000430type Workspace struct {
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000431 Name string
432 Code string
433 Hash string
434 Tries []Try
435 Titlebar Titlebar
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000436}
437
438// newWorkspace generates a new random workspace name and stores it in the database.
439func newWorkspace() (string, error) {
440 for i := 0; i < 10; i++ {
441 adj := workspaceNameAdj[rand.Intn(len(workspaceNameAdj))]
442 noun := workspaceNameNoun[rand.Intn(len(workspaceNameNoun))]
443 suffix := rand.Intn(1000)
444 name := fmt.Sprintf("%s-%s-%d", adj, noun, suffix)
445 if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", name); err == nil {
446 return name, nil
447 } else {
448 log.Printf("ERROR: Failed to insert workspace into database: %q\n", err)
449 }
450 }
451 return "", fmt.Errorf("Failed to create a new workspace")
452}
453
454// getCode returns the code for a given hash, or the empty string if not found.
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000455func getCode(hash string) (string, error) {
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000456 code := ""
457 if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil {
458 log.Printf("ERROR: Code for hash is missing: %q\n", err)
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000459 return code, err
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000460 }
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000461 return code, nil
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000462}
463
464func workspaceHandler(w http.ResponseWriter, r *http.Request) {
465 log.Printf("Workspace Handler: %q\n", r.URL.Path)
466 if r.Method == "GET" {
467 tries := []Try{}
468 match := workspaceLink.FindStringSubmatch(r.URL.Path)
469 name := ""
470 if len(match) == 2 {
471 name = match[1]
commit-bot@chromium.orgc3b738a2014-04-21 17:36:44 +0000472 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 +0000473 if err != nil {
474 reportError(w, r, err, "Failed to select.")
475 return
476 }
477 for rows.Next() {
478 var hash string
479 var create_ts time.Time
480 if err := rows.Scan(&create_ts, &hash); err != nil {
481 log.Printf("Error: failed to fetch from database: %q", err)
482 continue
483 }
484 tries = append(tries, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
485 }
486 }
487 var code string
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000488 var hash string
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000489 if len(tries) == 0 {
490 code = DEFAULT_SAMPLE
491 } else {
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000492 hash = tries[len(tries)-1].Hash
493 code, _ = getCode(hash)
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000494 }
commit-bot@chromium.orga4c36772014-05-19 17:38:52 +0000495 w.Header().Set("Content-Type", "text/html")
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000496 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 +0000497 log.Printf("ERROR: Failed to expand template: %q\n", err)
498 }
499 } else if r.Method == "POST" {
500 name, err := newWorkspace()
501 if err != nil {
502 http.Error(w, "Failed to create a new workspace.", 500)
503 return
504 }
505 http.Redirect(w, r, "/w/"+name, 302)
506 }
507}
508
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000509// hasPreProcessor returns true if any line in the code begins with a # char.
510func hasPreProcessor(code string) bool {
511 lines := strings.Split(code, "\n")
512 for _, s := range lines {
513 if strings.HasPrefix(strings.TrimSpace(s), "#") {
514 return true
515 }
516 }
517 return false
518}
519
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000520type TryRequest struct {
521 Code string `json:"code"`
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000522 Name string `json:"name"` // Optional name of the workspace the code is in.
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000523}
524
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000525// iframeHandler handles the GET and POST of the main page.
526func iframeHandler(w http.ResponseWriter, r *http.Request) {
527 log.Printf("IFrame Handler: %q\n", r.URL.Path)
528 if r.Method != "GET" {
529 http.NotFound(w, r)
530 return
531 }
532 match := iframeLink.FindStringSubmatch(r.URL.Path)
533 if len(match) != 2 {
534 http.NotFound(w, r)
535 return
536 }
537 hash := match[1]
538 if db == nil {
539 http.NotFound(w, r)
540 return
541 }
542 var code string
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000543 code, err := getCode(hash)
544 if err != nil {
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000545 http.NotFound(w, r)
546 return
547 }
548 // Expand the template.
commit-bot@chromium.orga4c36772014-05-19 17:38:52 +0000549 w.Header().Set("Content-Type", "text/html")
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000550 if err := iframeTemplate.Execute(w, userCode{Code: code, Hash: hash}); err != nil {
commit-bot@chromium.org2dceeda2014-04-19 14:50:23 +0000551 log.Printf("ERROR: Failed to expand template: %q\n", err)
552 }
553}
554
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000555type TryInfo struct {
556 Hash string `json:"hash"`
557 Code string `json:"code"`
558}
559
560// tryInfoHandler returns information about a specific try.
561func tryInfoHandler(w http.ResponseWriter, r *http.Request) {
562 log.Printf("Try Info Handler: %q\n", r.URL.Path)
563 if r.Method != "GET" {
564 http.NotFound(w, r)
565 return
566 }
567 match := tryInfoLink.FindStringSubmatch(r.URL.Path)
568 if len(match) != 2 {
569 http.NotFound(w, r)
570 return
571 }
572 hash := match[1]
573 code, err := getCode(hash)
574 if err != nil {
575 http.NotFound(w, r)
576 return
577 }
578 m := TryInfo{
579 Hash: hash,
580 Code: code,
581 }
582 resp, err := json.Marshal(m)
583 if err != nil {
584 reportError(w, r, err, "Failed to serialize a response.")
585 return
586 }
587 w.Header().Set("Content-Type", "application/json")
588 w.Write(resp)
589}
590
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000591func cleanCompileOutput(s, hash string) string {
592 old := "../../../cache/" + hash + ".cpp:"
593 log.Printf("INFO: replacing %q\n", old)
594 return strings.Replace(s, old, "usercode.cpp:", -1)
595}
596
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000597// mainHandler handles the GET and POST of the main page.
598func mainHandler(w http.ResponseWriter, r *http.Request) {
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000599 log.Printf("Main Handler: %q\n", r.URL.Path)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000600 if r.Method == "GET" {
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000601 code := DEFAULT_SAMPLE
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000602 match := directLink.FindStringSubmatch(r.URL.Path)
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000603 var hash string
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000604 if len(match) == 2 && r.URL.Path != "/" {
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000605 hash = match[1]
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000606 if db == nil {
607 http.NotFound(w, r)
608 return
609 }
610 // Update 'code' with the code found in the database.
611 if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil {
612 http.NotFound(w, r)
613 return
614 }
615 }
616 // Expand the template.
commit-bot@chromium.orga4c36772014-05-19 17:38:52 +0000617 w.Header().Set("Content-Type", "text/html")
commit-bot@chromium.org472f8302014-04-28 15:33:31 +0000618 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 +0000619 log.Printf("ERROR: Failed to expand template: %q\n", err)
620 }
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000621 } else if r.Method == "POST" {
622 w.Header().Set("Content-Type", "application/json")
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000623 buf := bytes.NewBuffer(make([]byte, 0, MAX_TRY_SIZE))
624 n, err := buf.ReadFrom(r.Body)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000625 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000626 reportTryError(w, r, err, "Failed to read a request body.", "")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000627 return
628 }
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000629 if n == MAX_TRY_SIZE {
630 err := fmt.Errorf("Code length equal to, or exceeded, %d", MAX_TRY_SIZE)
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000631 reportTryError(w, r, err, "Code too large.", "")
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000632 return
633 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000634 request := TryRequest{}
635 if err := json.Unmarshal(buf.Bytes(), &request); err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000636 reportTryError(w, r, err, "Coulnd't decode JSON.", "")
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000637 return
638 }
639 if hasPreProcessor(request.Code) {
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000640 err := fmt.Errorf("Found preprocessor macro in code.")
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000641 reportTryError(w, r, err, "Preprocessor macros aren't allowed.", "")
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000642 return
643 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000644 hash, err := expandCode(LineNumbers(request.Code))
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000645 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000646 reportTryError(w, r, err, "Failed to write the code to compile.", hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000647 return
648 }
commit-bot@chromium.orgd04e1dd2014-04-19 13:55:50 +0000649 writeToDatabase(hash, request.Code, request.Name)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000650 message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), true)
651 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000652 message = cleanCompileOutput(message, hash)
653 reportTryError(w, r, err, message, hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000654 return
655 }
656 linkMessage, err := doCmd(fmt.Sprintf(LINK, hash, hash), true)
657 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000658 linkMessage = cleanCompileOutput(linkMessage, hash)
659 reportTryError(w, r, err, linkMessage, hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000660 return
661 }
662 message += linkMessage
663 cmd := hash + " --out " + hash + ".png"
664 if *useChroot {
665 cmd = "schroot -c webtry --directory=/inout -- /inout/" + cmd
666 } else {
667 abs, err := filepath.Abs("../../../inout")
668 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000669 reportTryError(w, r, err, "Failed to find executable directory.", hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000670 return
671 }
672 cmd = abs + "/" + cmd
673 }
674
675 execMessage, err := doCmd(cmd, false)
676 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000677 reportTryError(w, r, err, "Failed to run the code:\n"+execMessage, hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000678 return
679 }
680 png, err := ioutil.ReadFile("../../../inout/" + hash + ".png")
681 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000682 reportTryError(w, r, err, "Failed to open the generated PNG.", hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000683 return
684 }
685
686 m := response{
687 Message: message,
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000688 StdOut: execMessage,
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000689 Img: base64.StdEncoding.EncodeToString([]byte(png)),
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000690 Hash: hash,
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000691 }
692 resp, err := json.Marshal(m)
693 if err != nil {
commit-bot@chromium.org90041922014-04-22 21:13:45 +0000694 reportTryError(w, r, err, "Failed to serialize a response.", hash)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000695 return
696 }
commit-bot@chromium.orga4c36772014-05-19 17:38:52 +0000697 w.Header().Set("Content-Type", "application/json")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000698 w.Write(resp)
699 }
700}
701
702func main() {
703 flag.Parse()
commit-bot@chromium.orga4c36772014-05-19 17:38:52 +0000704 http.HandleFunc("/i/", autogzip.HandleFunc(imageHandler))
705 http.HandleFunc("/w/", autogzip.HandleFunc(workspaceHandler))
706 http.HandleFunc("/recent/", autogzip.HandleFunc(recentHandler))
707 http.HandleFunc("/iframe/", autogzip.HandleFunc(iframeHandler))
708 http.HandleFunc("/json/", autogzip.HandleFunc(tryInfoHandler))
fmalita@google.com950306c2014-05-01 15:14:56 +0000709
710 // Resources are served directly
711 // TODO add support for caching/etags/gzip
commit-bot@chromium.orga4c36772014-05-19 17:38:52 +0000712 http.Handle("/res/", autogzip.Handle(http.FileServer(http.Dir("./"))))
fmalita@google.com950306c2014-05-01 15:14:56 +0000713
commit-bot@chromium.org35ffc442014-04-22 19:32:06 +0000714 // TODO Break out /c/ as it's own handler.
commit-bot@chromium.orga4c36772014-05-19 17:38:52 +0000715 http.HandleFunc("/", autogzip.HandleFunc(mainHandler))
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000716 log.Fatal(http.ListenAndServe(*port, nil))
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000717}