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