blob: 1b678ecb778334dcc6757afd34ee6322eb59b202 [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"
16 "net/http"
17 "os"
18 "os/exec"
19 "path/filepath"
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000020 "regexp"
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000021 "strings"
22 "text/template"
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000023 "time"
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000024)
25
26const (
27 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 +000028 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 +000029 DEFAULT_SAMPLE = `SkPaint p;
30p.setColor(SK_ColorRED);
31p.setAntiAlias(true);
32p.setStyle(SkPaint::kStroke_Style);
33p.setStrokeWidth(10);
34
35canvas->drawLine(20, 20, 100, 100, p);
36`
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +000037 // Don't increase above 2^16 w/o altering the db tables to accept something bigger than TEXT.
38 MAX_TRY_SIZE = 64000
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000039)
40
41var (
42 // codeTemplate is the cpp code template the user's code is copied into.
43 codeTemplate *template.Template = nil
44
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000045 // indexTemplate is the main index.html page we serve.
46 indexTemplate *htemplate.Template = nil
47
48 // recentTemplate is a list of recent images.
49 recentTemplate *htemplate.Template = nil
commit-bot@chromium.org282333f2014-04-14 14:54:07 +000050
51 // db is the database, nil if we don't have an SQL database to store data into.
52 db *sql.DB = nil
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000053
54 // directLink is the regex that matches URLs paths that are direct links.
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000055 directLink = regexp.MustCompile("^/c/([a-f0-9]+)$")
56
57 // imageLink is the regex that matches URLs paths that are direct links to PNGs.
58 imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000059)
60
61// flags
62var (
63 useChroot = flag.Bool("use_chroot", false, "Run the compiled code in the schroot jail.")
commit-bot@chromium.org282333f2014-04-14 14:54:07 +000064 port = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000065)
66
67// lineNumbers adds #line numbering to the user's code.
68func LineNumbers(c string) string {
69 lines := strings.Split(c, "\n")
70 ret := []string{}
71 for i, line := range lines {
72 ret = append(ret, fmt.Sprintf("#line %d", i+1))
73 ret = append(ret, line)
74 }
75 return strings.Join(ret, "\n")
76}
77
78func init() {
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000079
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000080 // Change the current working directory to the directory of the executable.
81 var err error
82 cwd, err := filepath.Abs(filepath.Dir(os.Args[0]))
83 if err != nil {
84 log.Fatal(err)
85 }
86 os.Chdir(cwd)
87
88 codeTemplate, err = template.ParseFiles(filepath.Join(cwd, "templates/template.cpp"))
89 if err != nil {
90 panic(err)
91 }
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +000092 // Convert index.html into a template, which is expanded with the code.
commit-bot@chromium.org06aca012014-04-14 20:12:08 +000093 indexTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates/index.html"))
94 if err != nil {
95 panic(err)
96 }
97
98 recentTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates/recent.html"))
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +000099 if err != nil {
100 panic(err)
101 }
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000102
103 // Connect to MySQL server. First, get the password from the metadata server.
104 // See https://developers.google.com/compute/docs/metadata#custom.
105 req, err := http.NewRequest("GET", "http://metadata/computeMetadata/v1/instance/attributes/password", nil)
106 if err != nil {
107 panic(err)
108 }
109 client := http.Client{}
110 req.Header.Add("X-Google-Metadata-Request", "True")
111 if resp, err := client.Do(req); err == nil {
112 password, err := ioutil.ReadAll(resp.Body)
113 if err != nil {
114 log.Printf("ERROR: Failed to read password from metadata server: %q\n", err)
115 panic(err)
116 }
117 // The IP address of the database is found here:
118 // https://console.developers.google.com/project/31977622648/sql/instances/webtry/overview
119 // And 3306 is the default port for MySQL.
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000120 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 +0000121 if err != nil {
122 log.Printf("ERROR: Failed to open connection to SQL server: %q\n", err)
123 panic(err)
124 }
125 } else {
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000126 // Fallback to sqlite for local use.
127 db, err = sql.Open("sqlite3", "./webtry.db")
128 if err != nil {
129 log.Printf("ERROR: Failed to open: %q\n", err)
130 panic(err)
131 }
132 sql := `CREATE TABLE webtry (
133 code TEXT DEFAULT '' NOT NULL,
134 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
135 hash CHAR(64) DEFAULT '' NOT NULL,
136 PRIMARY KEY(hash)
137 )`
138 db.Exec(sql)
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000139 log.Printf("INFO: Failed to find metadata, unable to connect to MySQL server (Expected when running locally): %q\n", err)
140 }
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000141}
142
143// userCode is used in template expansion.
144type userCode struct {
145 UserCode string
146}
147
148// expandToFile expands the template and writes the result to the file.
149func expandToFile(filename string, code string, t *template.Template) error {
150 f, err := os.Create(filename)
151 if err != nil {
152 return err
153 }
154 defer f.Close()
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000155 return t.Execute(f, userCode{UserCode: code})
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000156}
157
158// expandCode expands the template into a file and calculate the MD5 hash.
159func expandCode(code string) (string, error) {
160 h := md5.New()
161 h.Write([]byte(code))
162 hash := fmt.Sprintf("%x", h.Sum(nil))
163 // At this point we are running in skia/experimental/webtry, making cache a
164 // peer directory to skia.
165 // TODO(jcgregorio) Make all relative directories into flags.
166 err := expandToFile(fmt.Sprintf("../../../cache/%s.cpp", hash), code, codeTemplate)
167 return hash, err
168}
169
170// response is serialized to JSON as a response to POSTs.
171type response struct {
172 Message string `json:"message"`
173 Img string `json:"img"`
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000174 Hash string `json:"hash"`
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000175}
176
177// doCmd executes the given command line string in either the out/Debug
178// directory or the inout directory. Returns the stdout, and stderr in the case
179// of a non-zero exit code.
180func doCmd(commandLine string, moveToDebug bool) (string, error) {
181 log.Printf("Command: %q\n", commandLine)
182 programAndArgs := strings.SplitN(commandLine, " ", 2)
183 program := programAndArgs[0]
184 args := []string{}
185 if len(programAndArgs) > 1 {
186 args = strings.Split(programAndArgs[1], " ")
187 }
188 cmd := exec.Command(program, args...)
189 abs, err := filepath.Abs("../../out/Debug")
190 if err != nil {
191 return "", fmt.Errorf("Failed to find absolute path to Debug directory.")
192 }
193 if moveToDebug {
194 cmd.Dir = abs
195 } else if !*useChroot { // Don't set cmd.Dir when using chroot.
196 abs, err := filepath.Abs("../../../inout")
197 if err != nil {
198 return "", fmt.Errorf("Failed to find absolute path to inout directory.")
199 }
200 cmd.Dir = abs
201 }
202 log.Printf("Run in directory: %q\n", cmd.Dir)
203 var stdOut bytes.Buffer
204 cmd.Stdout = &stdOut
205 var stdErr bytes.Buffer
206 cmd.Stderr = &stdErr
207 cmd.Start()
208 err = cmd.Wait()
209 message := stdOut.String()
210 log.Printf("StdOut: %s\n", message)
211 if err != nil {
212 log.Printf("Exit status: %s\n", err.Error())
213 log.Printf("StdErr: %s\n", stdErr.String())
214 message += stdErr.String()
215 return message, fmt.Errorf("Failed to run command.")
216 }
217 return message, nil
218}
219
220// reportError formats an HTTP error response and also logs the detailed error message.
221func reportError(w http.ResponseWriter, r *http.Request, err error, message string) {
222 m := response{
223 Message: message,
224 }
225 log.Printf("Error: %s\n%s", message, err.Error())
226 resp, err := json.Marshal(m)
227 if err != nil {
228 http.Error(w, "Failed to serialize a response", 500)
229 return
230 }
231 w.Write(resp)
232}
233
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000234func writeToDatabase(hash string, code string) {
235 if db == nil {
236 return
237 }
238 if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", code, hash); err != nil {
239 log.Printf("ERROR: Failed to insert code into database: %q\n", err)
240 }
241}
242
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000243func cssHandler(w http.ResponseWriter, r *http.Request) {
244 http.ServeFile(w, r, "css/webtry.css")
245}
246
247// imageHandler serves up the PNG of a specific try.
248func imageHandler(w http.ResponseWriter, r *http.Request) {
249 log.Printf("Image Handler: %q\n", r.URL.Path)
250 if r.Method != "GET" {
251 http.NotFound(w, r)
252 return
253 }
254 match := imageLink.FindStringSubmatch(r.URL.Path)
255 if len(match) != 2 {
256 http.NotFound(w, r)
257 return
258 }
259 filename := match[1]
260 http.ServeFile(w, r, fmt.Sprintf("../../../inout/%s", filename))
261}
262
263type Try struct {
264 Hash string
265 CreateTS string
266}
267
268type Recent struct {
269 Tries []Try
270}
271
272// recentHandler shows the last 20 tries.
273func recentHandler(w http.ResponseWriter, r *http.Request) {
274 log.Printf("Recent Handler: %q\n", r.URL.Path)
275
276 var err error
277 rows, err := db.Query("SELECT create_ts, hash FROM webtry ORDER BY create_ts DESC LIMIT 20")
278 if err != nil {
279 http.NotFound(w, r)
280 return
281 }
282 recent := []Try{}
283 for rows.Next() {
284 var hash string
285 var create_ts time.Time
286 if err := rows.Scan(&create_ts, &hash); err != nil {
287 log.Printf("Error: failed to fetch from database: %q", err)
288 continue
289 }
290 recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
291 }
292 if err := recentTemplate.Execute(w, Recent{Tries: recent}); err != nil {
293 log.Printf("ERROR: Failed to expand template: %q\n", err)
294 }
295}
296
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000297// hasPreProcessor returns true if any line in the code begins with a # char.
298func hasPreProcessor(code string) bool {
299 lines := strings.Split(code, "\n")
300 for _, s := range lines {
301 if strings.HasPrefix(strings.TrimSpace(s), "#") {
302 return true
303 }
304 }
305 return false
306}
307
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000308// mainHandler handles the GET and POST of the main page.
309func mainHandler(w http.ResponseWriter, r *http.Request) {
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000310 log.Printf("Main Handler: %q\n", r.URL.Path)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000311 if r.Method == "GET" {
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000312 code := DEFAULT_SAMPLE
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000313 match := directLink.FindStringSubmatch(r.URL.Path)
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000314 if len(match) == 2 && r.URL.Path != "/" {
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000315 hash := match[1]
316 if db == nil {
317 http.NotFound(w, r)
318 return
319 }
320 // Update 'code' with the code found in the database.
321 if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil {
322 http.NotFound(w, r)
323 return
324 }
325 }
326 // Expand the template.
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000327 if err := indexTemplate.Execute(w, userCode{UserCode: code}); err != nil {
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000328 log.Printf("ERROR: Failed to expand template: %q\n", err)
329 }
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000330 } else if r.Method == "POST" {
331 w.Header().Set("Content-Type", "application/json")
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000332 buf := bytes.NewBuffer(make([]byte, 0, MAX_TRY_SIZE))
333 n, err := buf.ReadFrom(r.Body)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000334 if err != nil {
335 reportError(w, r, err, "Failed to read a request body.")
336 return
337 }
commit-bot@chromium.org4bd8fdc2014-04-15 00:43:51 +0000338 if n == MAX_TRY_SIZE {
339 err := fmt.Errorf("Code length equal to, or exceeded, %d", MAX_TRY_SIZE)
340 reportError(w, r, err, "Code too large.")
341 return
342 }
343 code := string(buf.Bytes())
344 if hasPreProcessor(code) {
345 err := fmt.Errorf("Found preprocessor macro in code.")
346 reportError(w, r, err, "Preprocessor macros aren't allowed.")
347 return
348 }
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000349 hash, err := expandCode(LineNumbers(code))
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000350 if err != nil {
351 reportError(w, r, err, "Failed to write the code to compile.")
352 return
353 }
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000354 writeToDatabase(hash, code)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000355 message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), true)
356 if err != nil {
357 reportError(w, r, err, "Failed to compile the code:\n"+message)
358 return
359 }
360 linkMessage, err := doCmd(fmt.Sprintf(LINK, hash, hash), true)
361 if err != nil {
362 reportError(w, r, err, "Failed to link the code:\n"+linkMessage)
363 return
364 }
365 message += linkMessage
366 cmd := hash + " --out " + hash + ".png"
367 if *useChroot {
368 cmd = "schroot -c webtry --directory=/inout -- /inout/" + cmd
369 } else {
370 abs, err := filepath.Abs("../../../inout")
371 if err != nil {
372 reportError(w, r, err, "Failed to find executable directory.")
373 return
374 }
375 cmd = abs + "/" + cmd
376 }
377
378 execMessage, err := doCmd(cmd, false)
379 if err != nil {
380 reportError(w, r, err, "Failed to run the code:\n"+execMessage)
381 return
382 }
383 png, err := ioutil.ReadFile("../../../inout/" + hash + ".png")
384 if err != nil {
385 reportError(w, r, err, "Failed to open the generated PNG.")
386 return
387 }
388
389 m := response{
390 Message: message,
391 Img: base64.StdEncoding.EncodeToString([]byte(png)),
commit-bot@chromium.orgc81d1c42014-04-14 18:53:10 +0000392 Hash: hash,
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000393 }
394 resp, err := json.Marshal(m)
395 if err != nil {
396 reportError(w, r, err, "Failed to serialize a response.")
397 return
398 }
399 w.Write(resp)
400 }
401}
402
403func main() {
404 flag.Parse()
commit-bot@chromium.org06aca012014-04-14 20:12:08 +0000405 http.HandleFunc("/i/", imageHandler)
406 http.HandleFunc("/recent/", recentHandler)
407 http.HandleFunc("/css/", cssHandler)
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000408 http.HandleFunc("/", mainHandler)
commit-bot@chromium.org282333f2014-04-14 14:54:07 +0000409 log.Fatal(http.ListenAndServe(*port, nil))
commit-bot@chromium.org6d036c22014-04-09 18:59:44 +0000410}