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