Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(680)

Side by Side Diff: experimental/webtry/webtry.go

Issue 688713002: delete webtry from main skia repo; it's been moved to buildbots (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: Created 6 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « experimental/webtry/templates/workspace.html ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « experimental/webtry/templates/workspace.html ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698