OLD | NEW |
| (Empty) |
1 package main | |
2 | |
3 import ( | |
4 "bytes" | |
5 "crypto/md5" | |
6 "database/sql" | |
7 "encoding/base64" | |
8 "encoding/binary" | |
9 "encoding/json" | |
10 "flag" | |
11 "fmt" | |
12 htemplate "html/template" | |
13 "image" | |
14 _ "image/gif" | |
15 _ "image/jpeg" | |
16 "image/png" | |
17 "io/ioutil" | |
18 "math/rand" | |
19 "net" | |
20 "net/http" | |
21 "os" | |
22 "os/exec" | |
23 "path/filepath" | |
24 "regexp" | |
25 "strconv" | |
26 "strings" | |
27 "text/template" | |
28 "time" | |
29 ) | |
30 | |
31 import ( | |
32 "github.com/fiorix/go-web/autogzip" | |
33 _ "github.com/go-sql-driver/mysql" | |
34 "github.com/golang/glog" | |
35 _ "github.com/mattn/go-sqlite3" | |
36 "github.com/rcrowley/go-metrics" | |
37 ) | |
38 | |
39 const ( | |
40 DEFAULT_SAMPLE = `void draw(SkCanvas* canvas) { | |
41 SkPaint p; | |
42 p.setColor(SK_ColorRED); | |
43 p.setAntiAlias(true); | |
44 p.setStyle(SkPaint::kStroke_Style); | |
45 p.setStrokeWidth(10); | |
46 | |
47 canvas->drawLine(20, 20, 100, 100, p); | |
48 }` | |
49 // Don't increase above 2^16 w/o altering the db tables to accept someth
ing bigger than TEXT. | |
50 MAX_TRY_SIZE = 64000 | |
51 ) | |
52 | |
53 var ( | |
54 // codeTemplate is the cpp code template the user's code is copied into. | |
55 codeTemplate *template.Template = nil | |
56 | |
57 // gypTemplate is the GYP file to build the executable containing the us
er's code. | |
58 gypTemplate *template.Template = nil | |
59 | |
60 // indexTemplate is the main index.html page we serve. | |
61 indexTemplate *htemplate.Template = nil | |
62 | |
63 // iframeTemplate is the main index.html page we serve. | |
64 iframeTemplate *htemplate.Template = nil | |
65 | |
66 // recentTemplate is a list of recent images. | |
67 recentTemplate *htemplate.Template = nil | |
68 | |
69 // workspaceTemplate is the page for workspaces, a series of webtrys. | |
70 workspaceTemplate *htemplate.Template = nil | |
71 | |
72 // db is the database, nil if we don't have an SQL database to store dat
a into. | |
73 db *sql.DB = nil | |
74 | |
75 // directLink is the regex that matches URLs paths that are direct links
. | |
76 directLink = regexp.MustCompile("^/c/([a-f0-9]+)$") | |
77 | |
78 // iframeLink is the regex that matches URLs paths that are links to ifr
ames. | |
79 iframeLink = regexp.MustCompile("^/iframe/([a-f0-9]+)$") | |
80 | |
81 // imageLink is the regex that matches URLs paths that are direct links
to PNGs. | |
82 imageLink = regexp.MustCompile("^/i/([a-z0-9-_]+.png)$") | |
83 | |
84 // tryInfoLink is the regex that matches URLs paths that are direct link
s to data about a single try. | |
85 tryInfoLink = regexp.MustCompile("^/json/([a-f0-9]+)$") | |
86 | |
87 // workspaceLink is the regex that matches URLs paths for workspaces. | |
88 workspaceLink = regexp.MustCompile("^/w/([a-z0-9-]+)$") | |
89 | |
90 // errorRE is ther regex that matches compiler errors and extracts the l
ine / column information. | |
91 errorRE = regexp.MustCompile("^.*.cpp:(\\d+):(\\d+):\\s*(.*)") | |
92 | |
93 // workspaceNameAdj is a list of adjectives for building workspace names
. | |
94 workspaceNameAdj = []string{ | |
95 "autumn", "hidden", "bitter", "misty", "silent", "empty", "dry",
"dark", | |
96 "summer", "icy", "delicate", "quiet", "white", "cool", "spring",
"winter", | |
97 "patient", "twilight", "dawn", "crimson", "wispy", "weathered",
"blue", | |
98 "billowing", "broken", "cold", "damp", "falling", "frosty", "gre
en", | |
99 "long", "late", "lingering", "bold", "little", "morning", "muddy
", "old", | |
100 "red", "rough", "still", "small", "sparkling", "throbbing", "shy
", | |
101 "wandering", "withered", "wild", "black", "young", "holy", "soli
tary", | |
102 "fragrant", "aged", "snowy", "proud", "floral", "restless", "div
ine", | |
103 "polished", "ancient", "purple", "lively", "nameless", | |
104 } | |
105 | |
106 // workspaceNameNoun is a list of nouns for building workspace names. | |
107 workspaceNameNoun = []string{ | |
108 "waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "
morning", | |
109 "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "gli
tter", | |
110 "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "br
ook", | |
111 "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "
firefly", | |
112 "feather", "grass", "haze", "mountain", "night", "pond", "darkne
ss", | |
113 "snowflake", "silence", "sound", "sky", "shape", "surf", "thunde
r", | |
114 "violet", "water", "wildflower", "wave", "water", "resonance", "
sun", | |
115 "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "pap
er", | |
116 "frog", "smoke", "star", | |
117 } | |
118 | |
119 gitHash = "" | |
120 gitInfo = "" | |
121 | |
122 requestsCounter = metrics.NewRegisteredCounter("requests", metrics.Defau
ltRegistry) | |
123 ) | |
124 | |
125 // flags | |
126 var ( | |
127 useChroot = flag.Bool("use_chroot", false, "Run the compiled code in the
schroot jail.") | |
128 port = flag.String("port", ":8000", "HTTP service address (e.g., ':
8000')") | |
129 ) | |
130 | |
131 // lineNumbers adds #line numbering to the user's code. | |
132 func LineNumbers(c string) string { | |
133 lines := strings.Split(c, "\n") | |
134 ret := []string{} | |
135 for i, line := range lines { | |
136 ret = append(ret, fmt.Sprintf("#line %d", i+1)) | |
137 ret = append(ret, line) | |
138 } | |
139 return strings.Join(ret, "\n") | |
140 } | |
141 | |
142 func Init() { | |
143 rand.Seed(time.Now().UnixNano()) | |
144 | |
145 // Change the current working directory to the directory of the executab
le. | |
146 cwd, err := filepath.Abs(filepath.Dir(os.Args[0])) | |
147 if err != nil { | |
148 glog.Fatal(err) | |
149 } | |
150 if err := os.Chdir(cwd); err != nil { | |
151 glog.Fatal(err) | |
152 } | |
153 | |
154 codeTemplate = template.Must(template.ParseFiles(filepath.Join(cwd, "tem
plates/template.cpp"))) | |
155 gypTemplate = template.Must(template.ParseFiles(filepath.Join(cwd, "temp
lates/template.gyp"))) | |
156 indexTemplate = htemplate.Must(htemplate.ParseFiles( | |
157 filepath.Join(cwd, "templates/index.html"), | |
158 filepath.Join(cwd, "templates/titlebar.html"), | |
159 filepath.Join(cwd, "templates/sidebar.html"), | |
160 filepath.Join(cwd, "templates/content.html"), | |
161 filepath.Join(cwd, "templates/headercommon.html"), | |
162 filepath.Join(cwd, "templates/footercommon.html"), | |
163 )) | |
164 iframeTemplate = htemplate.Must(htemplate.ParseFiles( | |
165 filepath.Join(cwd, "templates/iframe.html"), | |
166 filepath.Join(cwd, "templates/content.html"), | |
167 filepath.Join(cwd, "templates/headercommon.html"), | |
168 filepath.Join(cwd, "templates/footercommon.html"), | |
169 )) | |
170 recentTemplate = htemplate.Must(htemplate.ParseFiles( | |
171 filepath.Join(cwd, "templates/recent.html"), | |
172 filepath.Join(cwd, "templates/titlebar.html"), | |
173 filepath.Join(cwd, "templates/sidebar.html"), | |
174 filepath.Join(cwd, "templates/headercommon.html"), | |
175 filepath.Join(cwd, "templates/footercommon.html"), | |
176 )) | |
177 workspaceTemplate = htemplate.Must(htemplate.ParseFiles( | |
178 filepath.Join(cwd, "templates/workspace.html"), | |
179 filepath.Join(cwd, "templates/titlebar.html"), | |
180 filepath.Join(cwd, "templates/sidebar.html"), | |
181 filepath.Join(cwd, "templates/content.html"), | |
182 filepath.Join(cwd, "templates/headercommon.html"), | |
183 filepath.Join(cwd, "templates/footercommon.html"), | |
184 )) | |
185 | |
186 // The git command returns output of the format: | |
187 // | |
188 // f672cead70404080a991ebfb86c38316a4589b23 2014-04-27 19:21:51 +0000 | |
189 // | |
190 logOutput, err := doCmd(`git log --format=%H%x20%ai HEAD^..HEAD`) | |
191 if err != nil { | |
192 panic(err) | |
193 } | |
194 logInfo := strings.Split(logOutput, " ") | |
195 gitHash = logInfo[0] | |
196 gitInfo = logInfo[1] + " " + logInfo[2] + " " + logInfo[0][0:6] | |
197 | |
198 // Connect to MySQL server. First, get the password from the metadata se
rver. | |
199 // See https://developers.google.com/compute/docs/metadata#custom. | |
200 req, err := http.NewRequest("GET", "http://metadata/computeMetadata/v1/i
nstance/attributes/password", nil) | |
201 if err != nil { | |
202 panic(err) | |
203 } | |
204 client := http.Client{} | |
205 req.Header.Add("X-Google-Metadata-Request", "True") | |
206 if resp, err := client.Do(req); err == nil { | |
207 password, err := ioutil.ReadAll(resp.Body) | |
208 if err != nil { | |
209 glog.Errorf("Failed to read password from metadata serve
r: %q\n", err) | |
210 panic(err) | |
211 } | |
212 // The IP address of the database is found here: | |
213 // https://console.developers.google.com/project/31977622648/
sql/instances/webtry/overview | |
214 // And 3306 is the default port for MySQL. | |
215 db, err = sql.Open("mysql", fmt.Sprintf("webtry:%s@tcp(173.194.8
3.52:3306)/webtry?parseTime=true", password)) | |
216 if err != nil { | |
217 glog.Errorf("ERROR: Failed to open connection to SQL ser
ver: %q\n", err) | |
218 panic(err) | |
219 } | |
220 } else { | |
221 glog.Infof("Failed to find metadata, unable to connect to MySQL
server (Expected when running locally): %q\n", err) | |
222 // Fallback to sqlite for local use. | |
223 db, err = sql.Open("sqlite3", "./webtry.db") | |
224 if err != nil { | |
225 glog.Errorf("Failed to open: %q\n", err) | |
226 panic(err) | |
227 } | |
228 sql := `CREATE TABLE IF NOT EXISTS source_images ( | |
229 id INTEGER PRIMARY KEY NOT NULL, | |
230 image MEDIUMBLOB DEFAULT '' NOT NULL, -- forma
tted as a PNG. | |
231 width INTEGER DEFAULT 0 NOT NULL, | |
232 height INTEGER DEFAULT 0 NOT NULL, | |
233 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, | |
234 hidden INTEGER DEFAULT 0 NOT NULL | |
235 )` | |
236 _, err = db.Exec(sql) | |
237 if err != nil { | |
238 glog.Errorf("Creating source_images table failed: %s", e
rr) | |
239 } | |
240 | |
241 sql = `CREATE TABLE IF NOT EXISTS webtry ( | |
242 code TEXT DEFAULT '' NOT NULL, | |
243 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, | |
244 hash CHAR(64) DEFAULT '' NOT NULL, | |
245 width INTEGER DEFAULT 256 NOT NULL, | |
246 height INTEGER DEFAULT 256 NOT NULL, | |
247 source_image_id INTEGER DEFAULT 0 NOT NULL, | |
248 | |
249 PRIMARY KEY(hash) | |
250 )` | |
251 _, err = db.Exec(sql) | |
252 if err != nil { | |
253 glog.Errorf("Creating webtry table failed: %s", err) | |
254 } | |
255 | |
256 sql = `CREATE TABLE IF NOT EXISTS workspace ( | |
257 name CHAR(64) DEFAULT '' NOT NULL, | |
258 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, | |
259 PRIMARY KEY(name) | |
260 )` | |
261 _, err = db.Exec(sql) | |
262 if err != nil { | |
263 glog.Errorf("Creating workspace table failed: %s", err) | |
264 } | |
265 | |
266 sql = `CREATE TABLE IF NOT EXISTS workspacetry ( | |
267 name CHAR(64) DEFAULT '' NOT NULL, | |
268 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, | |
269 hash CHAR(64) DEFAULT '' NOT NULL, | |
270 width INTEGER DEFAULT 256 NOT NULL, | |
271 height INTEGER DEFAULT 256 NOT NULL, | |
272 hidden INTEGER DEFAULT 0 NOT NULL, | |
273 source_image_id INTEGER DEFAULT 0 NOT NULL, | |
274 | |
275 FOREIGN KEY (name) REFERENCES workspace(name) | |
276 )` | |
277 _, err = db.Exec(sql) | |
278 if err != nil { | |
279 glog.Errorf("Creating workspacetry table failed: %s", er
r) | |
280 } | |
281 } | |
282 | |
283 // Ping the database to keep the connection fresh. | |
284 go func() { | |
285 c := time.Tick(1 * time.Minute) | |
286 for _ = range c { | |
287 if err := db.Ping(); err != nil { | |
288 glog.Errorf("Database failed to respond: %q\n",
err) | |
289 } | |
290 } | |
291 }() | |
292 | |
293 metrics.RegisterRuntimeMemStats(metrics.DefaultRegistry) | |
294 go metrics.CaptureRuntimeMemStats(metrics.DefaultRegistry, 1*time.Minute
) | |
295 | |
296 // Start reporting metrics. | |
297 // TODO(jcgregorio) We need a centrialized config server for storing thi
ngs | |
298 // like the IP address of the Graphite monitor. | |
299 addr, _ := net.ResolveTCPAddr("tcp", "skia-monitoring-b:2003") | |
300 go metrics.Graphite(metrics.DefaultRegistry, 1*time.Minute, "webtry", ad
dr) | |
301 | |
302 writeOutAllSourceImages() | |
303 } | |
304 | |
305 func writeOutAllSourceImages() { | |
306 // Pull all the source images from the db and write them out to inout. | |
307 rows, err := db.Query("SELECT id, image, create_ts FROM source_images OR
DER BY create_ts DESC") | |
308 if err != nil { | |
309 glog.Errorf("Failed to open connection to SQL server: %q\n", err
) | |
310 panic(err) | |
311 } | |
312 defer rows.Close() | |
313 for rows.Next() { | |
314 var id int | |
315 var image []byte | |
316 var create_ts time.Time | |
317 if err := rows.Scan(&id, &image, &create_ts); err != nil { | |
318 glog.Errorf("failed to fetch from database: %q", err) | |
319 continue | |
320 } | |
321 filename := fmt.Sprintf("../../../inout/image-%d.png", id) | |
322 if _, err := os.Stat(filename); os.IsExist(err) { | |
323 glog.Infof("Skipping write since file exists: %q", filen
ame) | |
324 continue | |
325 } | |
326 if err := ioutil.WriteFile(filename, image, 0666); err != nil { | |
327 glog.Errorf("failed to write image file: %q", err) | |
328 } | |
329 } | |
330 } | |
331 | |
332 // Titlebar is used in titlebar template expansion. | |
333 type Titlebar struct { | |
334 GitHash string | |
335 GitInfo string | |
336 } | |
337 | |
338 // userCode is used in template expansion. | |
339 type userCode struct { | |
340 Code string | |
341 Hash string | |
342 Width int | |
343 Height int | |
344 Source int | |
345 Titlebar Titlebar | |
346 } | |
347 | |
348 // writeTemplate creates a given output file and writes the template | |
349 // result there. | |
350 func writeTemplate(filename string, t *template.Template, context interface{}) e
rror { | |
351 f, err := os.Create(filename) | |
352 if err != nil { | |
353 return err | |
354 } | |
355 defer f.Close() | |
356 return t.Execute(f, context) | |
357 } | |
358 | |
359 // expandToFile expands the template and writes the result to the file. | |
360 func expandToFile(filename string, code string, t *template.Template) error { | |
361 return writeTemplate(filename, t, userCode{ | |
362 Code: code, | |
363 Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}, | |
364 }) | |
365 } | |
366 | |
367 // expandCode expands the template into a file and calculates the MD5 hash. | |
368 // We include the width and height here so that a single hash can capture | |
369 // both the code and the supplied width/height parameters. | |
370 func expandCode(code string, source int, width, height int) (string, error) { | |
371 // in order to support fonts in the chroot jail, we need to make sure | |
372 // we're using portable typefaces. | |
373 // TODO(humper): Make this more robust, supporting things like setTypef
ace | |
374 | |
375 inputCodeLines := strings.Split(code, "\n") | |
376 outputCodeLines := []string{ | |
377 "DECLARE_bool(portableFonts);", | |
378 fmt.Sprintf("// WxH: %d, %d", width, height), | |
379 } | |
380 for _, line := range inputCodeLines { | |
381 outputCodeLines = append(outputCodeLines, line) | |
382 if strings.HasPrefix(strings.TrimSpace(line), "SkPaint p") { | |
383 outputCodeLines = append(outputCodeLines, "FLAGS_portabl
eFonts = true;") | |
384 outputCodeLines = append(outputCodeLines, "sk_tool_utils
::set_portable_typeface(&p, \"Helvetica\", SkTypeface::kNormal);") | |
385 } | |
386 } | |
387 | |
388 fontFriendlyCode := strings.Join(outputCodeLines, "\n") | |
389 | |
390 h := md5.New() | |
391 h.Write([]byte(fontFriendlyCode)) | |
392 binary.Write(h, binary.LittleEndian, int64(source)) | |
393 hash := fmt.Sprintf("%x", h.Sum(nil)) | |
394 // At this point we are running in skia/experimental/webtry, making cach
e a | |
395 // peer directory to skia. | |
396 // TODO(jcgregorio) Make all relative directories into flags. | |
397 err := expandToFile(fmt.Sprintf("../../../cache/src/%s.cpp", hash), font
FriendlyCode, codeTemplate) | |
398 return hash, err | |
399 } | |
400 | |
401 // expandGyp produces the GYP file needed to build the code | |
402 func expandGyp(hash string) error { | |
403 return writeTemplate(fmt.Sprintf("../../../cache/%s.gyp", hash), gypTemp
late, struct{ Hash string }{hash}) | |
404 } | |
405 | |
406 // response is serialized to JSON as a response to POSTs. | |
407 type response struct { | |
408 Message string `json:"message"` | |
409 CompileErrors []compileError `json:"compileErrors"` | |
410 RasterImg string `json:"rasterImg"` | |
411 GPUImg string `json:"gpuImg"` | |
412 Hash string `json:"hash"` | |
413 } | |
414 | |
415 // doCmd executes the given command line string; the command being | |
416 // run is expected to not care what its current working directory is. | |
417 // Returns the stdout and stderr. | |
418 func doCmd(commandLine string) (string, error) { | |
419 glog.Infof("Command: %q\n", commandLine) | |
420 programAndArgs := strings.SplitN(commandLine, " ", 2) | |
421 program := programAndArgs[0] | |
422 args := []string{} | |
423 if len(programAndArgs) > 1 { | |
424 args = strings.Split(programAndArgs[1], " ") | |
425 } | |
426 cmd := exec.Command(program, args...) | |
427 message, err := cmd.CombinedOutput() | |
428 glog.Infof("StdOut + StdErr: %s\n", string(message)) | |
429 if err != nil { | |
430 glog.Errorf("Exit status: %s\n", err) | |
431 return string(message), fmt.Errorf("Failed to run command.") | |
432 } | |
433 return string(message), nil | |
434 } | |
435 | |
436 // reportError formats an HTTP error response and also logs the detailed error m
essage. | |
437 func reportError(w http.ResponseWriter, r *http.Request, err error, message stri
ng) { | |
438 glog.Errorf("%s\n%s", message, err) | |
439 w.Header().Set("Content-Type", "text/plain") | |
440 http.Error(w, message, 500) | |
441 } | |
442 | |
443 // reportTryError formats an HTTP error response in JSON and also logs the detai
led error message. | |
444 func reportTryError(w http.ResponseWriter, r *http.Request, err error, message,
hash string) { | |
445 m := response{ | |
446 Message: message, | |
447 Hash: hash, | |
448 } | |
449 glog.Errorf("%s\n%s", message, err) | |
450 resp, err := json.Marshal(m) | |
451 | |
452 if err != nil { | |
453 http.Error(w, "Failed to serialize a response", 500) | |
454 return | |
455 } | |
456 w.Header().Set("Content-Type", "text/plain") | |
457 w.Write(resp) | |
458 } | |
459 | |
460 func reportCompileError(w http.ResponseWriter, r *http.Request, compileErrors []
compileError, hash string) { | |
461 m := response{ | |
462 CompileErrors: compileErrors, | |
463 Hash: hash, | |
464 } | |
465 | |
466 resp, err := json.Marshal(m) | |
467 | |
468 if err != nil { | |
469 http.Error(w, "Failed to serialize a response", 500) | |
470 return | |
471 } | |
472 w.Header().Set("Content-Type", "text/plain") | |
473 w.Write(resp) | |
474 } | |
475 | |
476 func writeToDatabase(hash string, code string, workspaceName string, source int,
width, height int) { | |
477 if db == nil { | |
478 return | |
479 } | |
480 if _, err := db.Exec("INSERT INTO webtry (code, hash, width, height, sou
rce_image_id) VALUES(?, ?, ?, ?, ?)", code, hash, width, height, source); err !=
nil { | |
481 glog.Errorf("Failed to insert code into database: %q\n", err) | |
482 } | |
483 if workspaceName != "" { | |
484 if _, err := db.Exec("INSERT INTO workspacetry (name, hash, widt
h, height, source_image_id) VALUES(?, ?, ?, ?, ?)", workspaceName, hash, width,
height, source); err != nil { | |
485 glog.Errorf("Failed to insert into workspacetry table: %
q\n", err) | |
486 } | |
487 } | |
488 } | |
489 | |
490 type Sources struct { | |
491 Id int `json:"id"` | |
492 } | |
493 | |
494 // sourcesHandler serves up the PNG of a specific try. | |
495 func sourcesHandler(w http.ResponseWriter, r *http.Request) { | |
496 glog.Infof("Sources Handler: %q\n", r.URL.Path) | |
497 if r.Method == "GET" { | |
498 rows, err := db.Query("SELECT id, create_ts FROM source_images W
HERE hidden=0 ORDER BY create_ts DESC") | |
499 if err != nil { | |
500 http.Error(w, fmt.Sprintf("Failed to query sources: %s."
, err), 500) | |
501 } | |
502 defer rows.Close() | |
503 sources := make([]Sources, 0, 0) | |
504 for rows.Next() { | |
505 var id int | |
506 var create_ts time.Time | |
507 if err := rows.Scan(&id, &create_ts); err != nil { | |
508 glog.Errorf("failed to fetch from database: %q",
err) | |
509 continue | |
510 } | |
511 sources = append(sources, Sources{Id: id}) | |
512 } | |
513 | |
514 resp, err := json.Marshal(sources) | |
515 if err != nil { | |
516 reportError(w, r, err, "Failed to serialize a response."
) | |
517 return | |
518 } | |
519 w.Header().Set("Content-Type", "application/json") | |
520 w.Write(resp) | |
521 | |
522 } else if r.Method == "POST" { | |
523 if err := r.ParseMultipartForm(1000000); err != nil { | |
524 http.Error(w, fmt.Sprintf("Failed to load image: %s.", e
rr), 500) | |
525 return | |
526 } | |
527 if _, ok := r.MultipartForm.File["upload"]; !ok { | |
528 http.Error(w, "Invalid upload.", 500) | |
529 return | |
530 } | |
531 if len(r.MultipartForm.File["upload"]) != 1 { | |
532 http.Error(w, "Wrong number of uploads.", 500) | |
533 return | |
534 } | |
535 f, err := r.MultipartForm.File["upload"][0].Open() | |
536 if err != nil { | |
537 http.Error(w, fmt.Sprintf("Failed to load image: %s.", e
rr), 500) | |
538 return | |
539 } | |
540 defer f.Close() | |
541 m, _, err := image.Decode(f) | |
542 if err != nil { | |
543 http.Error(w, fmt.Sprintf("Failed to decode image: %s.",
err), 500) | |
544 return | |
545 } | |
546 var b bytes.Buffer | |
547 png.Encode(&b, m) | |
548 bounds := m.Bounds() | |
549 width := bounds.Max.Y - bounds.Min.Y | |
550 height := bounds.Max.X - bounds.Min.X | |
551 if _, err := db.Exec("INSERT INTO source_images (image, width, h
eight) VALUES(?, ?, ?)", b.Bytes(), width, height); err != nil { | |
552 glog.Errorf("Failed to insert sources into database: %q\
n", err) | |
553 http.Error(w, fmt.Sprintf("Failed to store image: %s.",
err), 500) | |
554 return | |
555 } | |
556 go writeOutAllSourceImages() | |
557 | |
558 // Now redirect back to where we came from. | |
559 http.Redirect(w, r, r.Referer(), 302) | |
560 } else { | |
561 http.NotFound(w, r) | |
562 return | |
563 } | |
564 } | |
565 | |
566 // imageHandler serves up the PNG of a specific try. | |
567 func imageHandler(w http.ResponseWriter, r *http.Request) { | |
568 glog.Infof("Image Handler: %q\n", r.URL.Path) | |
569 if r.Method != "GET" { | |
570 http.NotFound(w, r) | |
571 return | |
572 } | |
573 match := imageLink.FindStringSubmatch(r.URL.Path) | |
574 if len(match) != 2 { | |
575 http.NotFound(w, r) | |
576 return | |
577 } | |
578 filename := match[1] | |
579 w.Header().Set("Content-Type", "image/png") | |
580 http.ServeFile(w, r, fmt.Sprintf("../../../inout/%s", filename)) | |
581 } | |
582 | |
583 type Try struct { | |
584 Hash string `json:"hash"` | |
585 Source int | |
586 CreateTS string `json:"create_ts"` | |
587 } | |
588 | |
589 type Recent struct { | |
590 Tries []Try | |
591 Titlebar Titlebar | |
592 } | |
593 | |
594 // recentHandler shows the last 20 tries. | |
595 func recentHandler(w http.ResponseWriter, r *http.Request) { | |
596 glog.Infof("Recent Handler: %q\n", r.URL.Path) | |
597 | |
598 rows, err := db.Query("SELECT create_ts, hash FROM webtry ORDER BY creat
e_ts DESC LIMIT 20") | |
599 if err != nil { | |
600 http.NotFound(w, r) | |
601 return | |
602 } | |
603 defer rows.Close() | |
604 recent := []Try{} | |
605 for rows.Next() { | |
606 var hash string | |
607 var create_ts time.Time | |
608 if err := rows.Scan(&create_ts, &hash); err != nil { | |
609 glog.Errorf("failed to fetch from database: %q", err) | |
610 continue | |
611 } | |
612 recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Form
at("2006-02-01")}) | |
613 } | |
614 w.Header().Set("Content-Type", "text/html") | |
615 if err := recentTemplate.Execute(w, Recent{Tries: recent, Titlebar: Titl
ebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil { | |
616 glog.Errorf("Failed to expand template: %q\n", err) | |
617 } | |
618 } | |
619 | |
620 type Workspace struct { | |
621 Name string | |
622 Code string | |
623 Hash string | |
624 Width int | |
625 Height int | |
626 Source int | |
627 Tries []Try | |
628 Titlebar Titlebar | |
629 } | |
630 | |
631 // newWorkspace generates a new random workspace name and stores it in the datab
ase. | |
632 func newWorkspace() (string, error) { | |
633 for i := 0; i < 10; i++ { | |
634 adj := workspaceNameAdj[rand.Intn(len(workspaceNameAdj))] | |
635 noun := workspaceNameNoun[rand.Intn(len(workspaceNameNoun))] | |
636 suffix := rand.Intn(1000) | |
637 name := fmt.Sprintf("%s-%s-%d", adj, noun, suffix) | |
638 if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", n
ame); err == nil { | |
639 return name, nil | |
640 } else { | |
641 glog.Errorf("Failed to insert workspace into database: %
q\n", err) | |
642 } | |
643 } | |
644 return "", fmt.Errorf("Failed to create a new workspace") | |
645 } | |
646 | |
647 // getCode returns the code for a given hash, or the empty string if not found. | |
648 func getCode(hash string) (string, int, int, int, error) { | |
649 code := "" | |
650 width := 0 | |
651 height := 0 | |
652 source := 0 | |
653 if err := db.QueryRow("SELECT code, width, height, source_image_id FROM
webtry WHERE hash=?", hash).Scan(&code, &width, &height, &source); err != nil { | |
654 glog.Errorf("Code for hash is missing: %q\n", err) | |
655 return code, width, height, source, err | |
656 } | |
657 return code, width, height, source, nil | |
658 } | |
659 | |
660 func workspaceHandler(w http.ResponseWriter, r *http.Request) { | |
661 glog.Infof("Workspace Handler: %q\n", r.URL.Path) | |
662 if r.Method == "GET" { | |
663 tries := []Try{} | |
664 match := workspaceLink.FindStringSubmatch(r.URL.Path) | |
665 name := "" | |
666 if len(match) == 2 { | |
667 name = match[1] | |
668 rows, err := db.Query("SELECT create_ts, hash, source_im
age_id FROM workspacetry WHERE name=? ORDER BY create_ts", name) | |
669 if err != nil { | |
670 reportError(w, r, err, "Failed to select.") | |
671 return | |
672 } | |
673 defer rows.Close() | |
674 for rows.Next() { | |
675 var hash string | |
676 var create_ts time.Time | |
677 var source int | |
678 if err := rows.Scan(&create_ts, &hash, &source);
err != nil { | |
679 glog.Errorf("failed to fetch from databa
se: %q", err) | |
680 continue | |
681 } | |
682 tries = append(tries, Try{Hash: hash, Source: so
urce, CreateTS: create_ts.Format("2006-02-01")}) | |
683 } | |
684 } | |
685 var code string | |
686 var hash string | |
687 var width int | |
688 var height int | |
689 source := 0 | |
690 if len(tries) == 0 { | |
691 code = DEFAULT_SAMPLE | |
692 width = 256 | |
693 height = 256 | |
694 } else { | |
695 hash = tries[len(tries)-1].Hash | |
696 code, width, height, source, _ = getCode(hash) | |
697 } | |
698 w.Header().Set("Content-Type", "text/html") | |
699 if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, C
ode: code, Name: name, Hash: hash, Width: width, Height: height, Source: source,
Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil { | |
700 glog.Errorf("Failed to expand template: %q\n", err) | |
701 } | |
702 } else if r.Method == "POST" { | |
703 name, err := newWorkspace() | |
704 if err != nil { | |
705 http.Error(w, "Failed to create a new workspace.", 500) | |
706 return | |
707 } | |
708 http.Redirect(w, r, "/w/"+name, 302) | |
709 } | |
710 } | |
711 | |
712 // hasPreProcessor returns true if any line in the code begins with a # char. | |
713 func hasPreProcessor(code string) bool { | |
714 lines := strings.Split(code, "\n") | |
715 for _, s := range lines { | |
716 if strings.HasPrefix(strings.TrimSpace(s), "#") { | |
717 return true | |
718 } | |
719 } | |
720 return false | |
721 } | |
722 | |
723 type TryRequest struct { | |
724 Code string `json:"code"` | |
725 Width int `json:"width"` | |
726 Height int `json:"height"` | |
727 GPU bool `json:"gpu"` | |
728 Raster bool `json:"raster"` | |
729 PDF bool `json:"pdf"` | |
730 Name string `json:"name"` // Optional name of the workspace the code
is in. | |
731 Source int `json:"source"` // ID of the source image, 0 if none. | |
732 } | |
733 | |
734 // iframeHandler handles the GET and POST of the main page. | |
735 func iframeHandler(w http.ResponseWriter, r *http.Request) { | |
736 glog.Infof("IFrame Handler: %q\n", r.URL.Path) | |
737 if r.Method != "GET" { | |
738 http.NotFound(w, r) | |
739 return | |
740 } | |
741 match := iframeLink.FindStringSubmatch(r.URL.Path) | |
742 if len(match) != 2 { | |
743 http.NotFound(w, r) | |
744 return | |
745 } | |
746 hash := match[1] | |
747 if db == nil { | |
748 http.NotFound(w, r) | |
749 return | |
750 } | |
751 var code string | |
752 code, width, height, source, err := getCode(hash) | |
753 if err != nil { | |
754 http.NotFound(w, r) | |
755 return | |
756 } | |
757 // Expand the template. | |
758 w.Header().Set("Content-Type", "text/html") | |
759 if err := iframeTemplate.Execute(w, userCode{Code: code, Width: width, H
eight: height, Hash: hash, Source: source}); err != nil { | |
760 glog.Errorf("Failed to expand template: %q\n", err) | |
761 } | |
762 } | |
763 | |
764 type TryInfo struct { | |
765 Hash string `json:"hash"` | |
766 Code string `json:"code"` | |
767 Width int `json:"width"` | |
768 Height int `json:"height"` | |
769 Source int `json:"source"` | |
770 } | |
771 | |
772 // tryInfoHandler returns information about a specific try. | |
773 func tryInfoHandler(w http.ResponseWriter, r *http.Request) { | |
774 glog.Infof("Try Info Handler: %q\n", r.URL.Path) | |
775 if r.Method != "GET" { | |
776 http.NotFound(w, r) | |
777 return | |
778 } | |
779 match := tryInfoLink.FindStringSubmatch(r.URL.Path) | |
780 if len(match) != 2 { | |
781 http.NotFound(w, r) | |
782 return | |
783 } | |
784 hash := match[1] | |
785 code, width, height, source, err := getCode(hash) | |
786 if err != nil { | |
787 http.NotFound(w, r) | |
788 return | |
789 } | |
790 m := TryInfo{ | |
791 Hash: hash, | |
792 Code: code, | |
793 Width: width, | |
794 Height: height, | |
795 Source: source, | |
796 } | |
797 resp, err := json.Marshal(m) | |
798 if err != nil { | |
799 reportError(w, r, err, "Failed to serialize a response.") | |
800 return | |
801 } | |
802 w.Header().Set("Content-Type", "application/json") | |
803 w.Write(resp) | |
804 } | |
805 | |
806 func cleanCompileOutput(s, hash string) string { | |
807 old := "../../../cache/src/" + hash + ".cpp:" | |
808 glog.Infof("replacing %q\n", old) | |
809 return strings.Replace(s, old, "usercode.cpp:", -1) | |
810 } | |
811 | |
812 type compileError struct { | |
813 Line int `json:"line"` | |
814 Column int `json:"column"` | |
815 Error string `json:"error"` | |
816 } | |
817 | |
818 // mainHandler handles the GET and POST of the main page. | |
819 func mainHandler(w http.ResponseWriter, r *http.Request) { | |
820 glog.Infof("Main Handler: %q\n", r.URL.Path) | |
821 requestsCounter.Inc(1) | |
822 if r.Method == "GET" { | |
823 code := DEFAULT_SAMPLE | |
824 source := 0 | |
825 width := 256 | |
826 height := 256 | |
827 match := directLink.FindStringSubmatch(r.URL.Path) | |
828 var hash string | |
829 if len(match) == 2 && r.URL.Path != "/" { | |
830 hash = match[1] | |
831 if db == nil { | |
832 http.NotFound(w, r) | |
833 return | |
834 } | |
835 // Update 'code' with the code found in the database. | |
836 if err := db.QueryRow("SELECT code, width, height, sourc
e_image_id FROM webtry WHERE hash=?", hash).Scan(&code, &width, &height, &source
); err != nil { | |
837 http.NotFound(w, r) | |
838 return | |
839 } | |
840 } | |
841 // Expand the template. | |
842 w.Header().Set("Content-Type", "text/html") | |
843 if err := indexTemplate.Execute(w, userCode{Code: code, Hash: ha
sh, Source: source, Width: width, Height: height, Titlebar: Titlebar{GitHash: gi
tHash, GitInfo: gitInfo}}); err != nil { | |
844 glog.Errorf("Failed to expand template: %q\n", err) | |
845 } | |
846 } else if r.Method == "POST" { | |
847 w.Header().Set("Content-Type", "application/json") | |
848 buf := bytes.NewBuffer(make([]byte, 0, MAX_TRY_SIZE)) | |
849 n, err := buf.ReadFrom(r.Body) | |
850 if err != nil { | |
851 reportTryError(w, r, err, "Failed to read a request body
.", "") | |
852 return | |
853 } | |
854 if n == MAX_TRY_SIZE { | |
855 err := fmt.Errorf("Code length equal to, or exceeded, %d
", MAX_TRY_SIZE) | |
856 reportTryError(w, r, err, "Code too large.", "") | |
857 return | |
858 } | |
859 request := TryRequest{} | |
860 if err := json.Unmarshal(buf.Bytes(), &request); err != nil { | |
861 reportTryError(w, r, err, "Coulnd't decode JSON.", "") | |
862 return | |
863 } | |
864 if !(request.GPU || request.Raster || request.PDF) { | |
865 reportTryError(w, r, nil, "No run configuration supplied
...", "") | |
866 return | |
867 } | |
868 if hasPreProcessor(request.Code) { | |
869 err := fmt.Errorf("Found preprocessor macro in code.") | |
870 reportTryError(w, r, err, "Preprocessor macros aren't al
lowed.", "") | |
871 return | |
872 } | |
873 hash, err := expandCode(LineNumbers(request.Code), request.Sourc
e, request.Width, request.Height) | |
874 if err != nil { | |
875 reportTryError(w, r, err, "Failed to write the code to c
ompile.", hash) | |
876 return | |
877 } | |
878 writeToDatabase(hash, request.Code, request.Name, request.Source
, request.Width, request.Height) | |
879 err = expandGyp(hash) | |
880 if err != nil { | |
881 reportTryError(w, r, err, "Failed to write the gyp file.
", hash) | |
882 return | |
883 } | |
884 cmd := fmt.Sprintf("scripts/fiddle_wrapper %s --width %d --heigh
t %d", hash, request.Width, request.Height) | |
885 if request.Raster { | |
886 cmd += " --raster" | |
887 } | |
888 if request.GPU { | |
889 cmd += " --gpu" | |
890 } | |
891 if request.PDF { | |
892 cmd += " --pdf" | |
893 } | |
894 if *useChroot { | |
895 cmd = "schroot -c webtry --directory=/ -- /skia_build/sk
ia/experimental/webtry/" + cmd | |
896 } | |
897 if request.Source > 0 { | |
898 cmd += fmt.Sprintf(" --source image-%d.png", request.Sou
rce) | |
899 } | |
900 | |
901 message, err := doCmd(cmd) | |
902 | |
903 outputLines := strings.Split(message, "\n") | |
904 errorLines := []compileError{} | |
905 for _, line := range outputLines { | |
906 match := errorRE.FindStringSubmatch(line) | |
907 if len(match) > 0 { | |
908 lineNumber, parseError := strconv.Atoi(match[1]) | |
909 if parseError != nil { | |
910 glog.Errorf("ERROR: Couldn't parse line
number from %s\n", match[1]) | |
911 continue | |
912 } | |
913 columnNumber, parseError := strconv.Atoi(match[2
]) | |
914 if parseError != nil { | |
915 glog.Errorf("ERROR: Couldn't parse colum
n number from %s\n", match[2]) | |
916 continue | |
917 } | |
918 errorLines = append(errorLines, | |
919 compileError{ | |
920 Line: lineNumber, | |
921 Column: columnNumber, | |
922 Error: match[3], | |
923 }) | |
924 } | |
925 } | |
926 | |
927 if err != nil { | |
928 if len(errorLines) > 0 { | |
929 reportCompileError(w, r, errorLines, hash) | |
930 } else { | |
931 reportTryError(w, r, err, "Failed to run the cod
e:\n"+message, hash) | |
932 } | |
933 return | |
934 } | |
935 | |
936 m := response{ | |
937 Hash: hash, | |
938 } | |
939 | |
940 if request.Raster { | |
941 png, err := ioutil.ReadFile("../../../inout/" + hash + "
_raster.png") | |
942 if err != nil { | |
943 reportTryError(w, r, err, "Failed to open the ra
ster-generated PNG.", hash) | |
944 return | |
945 } | |
946 | |
947 m.RasterImg = base64.StdEncoding.EncodeToString([]byte(p
ng)) | |
948 } | |
949 | |
950 if request.GPU { | |
951 png, err := ioutil.ReadFile("../../../inout/" + hash + "
_gpu.png") | |
952 if err != nil { | |
953 reportTryError(w, r, err, "Failed to open the GP
U-generated PNG.", hash) | |
954 return | |
955 } | |
956 | |
957 m.GPUImg = base64.StdEncoding.EncodeToString([]byte(png)
) | |
958 } | |
959 | |
960 resp, err := json.Marshal(m) | |
961 if err != nil { | |
962 reportTryError(w, r, err, "Failed to serialize a respons
e.", hash) | |
963 return | |
964 } | |
965 w.Header().Set("Content-Type", "application/json") | |
966 w.Write(resp) | |
967 } | |
968 } | |
969 | |
970 func main() { | |
971 flag.Parse() | |
972 Init() | |
973 http.HandleFunc("/i/", autogzip.HandleFunc(imageHandler)) | |
974 http.HandleFunc("/w/", autogzip.HandleFunc(workspaceHandler)) | |
975 http.HandleFunc("/recent/", autogzip.HandleFunc(recentHandler)) | |
976 http.HandleFunc("/iframe/", autogzip.HandleFunc(iframeHandler)) | |
977 http.HandleFunc("/json/", autogzip.HandleFunc(tryInfoHandler)) | |
978 http.HandleFunc("/sources/", autogzip.HandleFunc(sourcesHandler)) | |
979 | |
980 // Resources are served directly | |
981 // TODO add support for caching/etags/gzip | |
982 http.Handle("/res/", autogzip.Handle(http.FileServer(http.Dir("./")))) | |
983 | |
984 // TODO Break out /c/ as it's own handler. | |
985 http.HandleFunc("/", autogzip.HandleFunc(mainHandler)) | |
986 glog.Fatal(http.ListenAndServe(*port, nil)) | |
987 } | |
OLD | NEW |