| package main |
| |
| import ( |
| "bytes" |
| "crypto/md5" |
| "database/sql" |
| "encoding/base64" |
| "encoding/binary" |
| "encoding/json" |
| "flag" |
| "fmt" |
| htemplate "html/template" |
| "image" |
| _ "image/gif" |
| _ "image/jpeg" |
| "image/png" |
| "io/ioutil" |
| "log" |
| "math/rand" |
| "net" |
| "net/http" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "strings" |
| "text/template" |
| "time" |
| ) |
| |
| import ( |
| "github.com/fiorix/go-web/autogzip" |
| _ "github.com/go-sql-driver/mysql" |
| _ "github.com/mattn/go-sqlite3" |
| "github.com/rcrowley/go-metrics" |
| ) |
| |
| const ( |
| RUN_GYP = `../../experimental/webtry/gyp_for_webtry %s -Dskia_gpu=0` |
| RUN_NINJA = `ninja -C ../../../inout/Release %s` |
| |
| DEFAULT_SAMPLE = `void draw(SkCanvas* canvas) { |
| SkPaint p; |
| p.setColor(SK_ColorRED); |
| p.setAntiAlias(true); |
| p.setStyle(SkPaint::kStroke_Style); |
| p.setStrokeWidth(10); |
| |
| canvas->drawLine(20, 20, 100, 100, p); |
| }` |
| // Don't increase above 2^16 w/o altering the db tables to accept something bigger than TEXT. |
| MAX_TRY_SIZE = 64000 |
| ) |
| |
| var ( |
| // codeTemplate is the cpp code template the user's code is copied into. |
| codeTemplate *template.Template = nil |
| |
| // gypTemplate is the GYP file to build the executable containing the user's code. |
| gypTemplate *template.Template = nil |
| |
| // indexTemplate is the main index.html page we serve. |
| indexTemplate *htemplate.Template = nil |
| |
| // iframeTemplate is the main index.html page we serve. |
| iframeTemplate *htemplate.Template = nil |
| |
| // recentTemplate is a list of recent images. |
| recentTemplate *htemplate.Template = nil |
| |
| // workspaceTemplate is the page for workspaces, a series of webtrys. |
| workspaceTemplate *htemplate.Template = nil |
| |
| // db is the database, nil if we don't have an SQL database to store data into. |
| db *sql.DB = nil |
| |
| // directLink is the regex that matches URLs paths that are direct links. |
| directLink = regexp.MustCompile("^/c/([a-f0-9]+)$") |
| |
| // iframeLink is the regex that matches URLs paths that are links to iframes. |
| iframeLink = regexp.MustCompile("^/iframe/([a-f0-9]+)$") |
| |
| // imageLink is the regex that matches URLs paths that are direct links to PNGs. |
| imageLink = regexp.MustCompile("^/i/([a-z0-9-]+.png)$") |
| |
| // tryInfoLink is the regex that matches URLs paths that are direct links to data about a single try. |
| tryInfoLink = regexp.MustCompile("^/json/([a-f0-9]+)$") |
| |
| // workspaceLink is the regex that matches URLs paths for workspaces. |
| workspaceLink = regexp.MustCompile("^/w/([a-z0-9-]+)$") |
| |
| // workspaceNameAdj is a list of adjectives for building workspace names. |
| workspaceNameAdj = []string{ |
| "autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark", |
| "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter", |
| "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue", |
| "billowing", "broken", "cold", "damp", "falling", "frosty", "green", |
| "long", "late", "lingering", "bold", "little", "morning", "muddy", "old", |
| "red", "rough", "still", "small", "sparkling", "throbbing", "shy", |
| "wandering", "withered", "wild", "black", "young", "holy", "solitary", |
| "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine", |
| "polished", "ancient", "purple", "lively", "nameless", |
| } |
| |
| // workspaceNameNoun is a list of nouns for building workspace names. |
| workspaceNameNoun = []string{ |
| "waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning", |
| "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter", |
| "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook", |
| "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly", |
| "feather", "grass", "haze", "mountain", "night", "pond", "darkness", |
| "snowflake", "silence", "sound", "sky", "shape", "surf", "thunder", |
| "violet", "water", "wildflower", "wave", "water", "resonance", "sun", |
| "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "paper", |
| "frog", "smoke", "star", |
| } |
| |
| gitHash = "" |
| gitInfo = "" |
| |
| requestsCounter = metrics.NewRegisteredCounter("requests", metrics.DefaultRegistry) |
| ) |
| |
| // flags |
| var ( |
| useChroot = flag.Bool("use_chroot", false, "Run the compiled code in the schroot jail.") |
| port = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')") |
| ) |
| |
| // lineNumbers adds #line numbering to the user's code. |
| func LineNumbers(c string) string { |
| lines := strings.Split(c, "\n") |
| ret := []string{} |
| for i, line := range lines { |
| ret = append(ret, fmt.Sprintf("#line %d", i+1)) |
| ret = append(ret, line) |
| } |
| return strings.Join(ret, "\n") |
| } |
| |
| func init() { |
| rand.Seed(time.Now().UnixNano()) |
| |
| // Change the current working directory to the directory of the executable. |
| var err error |
| cwd, err := filepath.Abs(filepath.Dir(os.Args[0])) |
| if err != nil { |
| log.Fatal(err) |
| } |
| os.Chdir(cwd) |
| |
| codeTemplate, err = template.ParseFiles(filepath.Join(cwd, "templates/template.cpp")) |
| if err != nil { |
| panic(err) |
| } |
| gypTemplate, err = template.ParseFiles(filepath.Join(cwd, "templates/template.gyp")) |
| if err != nil { |
| panic(err) |
| } |
| indexTemplate, err = htemplate.ParseFiles( |
| filepath.Join(cwd, "templates/index.html"), |
| filepath.Join(cwd, "templates/titlebar.html"), |
| filepath.Join(cwd, "templates/content.html"), |
| filepath.Join(cwd, "templates/headercommon.html"), |
| filepath.Join(cwd, "templates/footercommon.html"), |
| ) |
| if err != nil { |
| panic(err) |
| } |
| iframeTemplate, err = htemplate.ParseFiles( |
| filepath.Join(cwd, "templates/iframe.html"), |
| filepath.Join(cwd, "templates/content.html"), |
| filepath.Join(cwd, "templates/headercommon.html"), |
| filepath.Join(cwd, "templates/footercommon.html"), |
| ) |
| if err != nil { |
| panic(err) |
| } |
| recentTemplate, err = htemplate.ParseFiles( |
| filepath.Join(cwd, "templates/recent.html"), |
| filepath.Join(cwd, "templates/titlebar.html"), |
| filepath.Join(cwd, "templates/headercommon.html"), |
| filepath.Join(cwd, "templates/footercommon.html"), |
| ) |
| if err != nil { |
| panic(err) |
| } |
| workspaceTemplate, err = htemplate.ParseFiles( |
| filepath.Join(cwd, "templates/workspace.html"), |
| filepath.Join(cwd, "templates/titlebar.html"), |
| filepath.Join(cwd, "templates/content.html"), |
| filepath.Join(cwd, "templates/headercommon.html"), |
| filepath.Join(cwd, "templates/footercommon.html"), |
| ) |
| if err != nil { |
| panic(err) |
| } |
| |
| // The git command returns output of the format: |
| // |
| // f672cead70404080a991ebfb86c38316a4589b23 2014-04-27 19:21:51 +0000 |
| // |
| logOutput, err := doCmd(`git log --format=%H%x20%ai HEAD^..HEAD`, true) |
| if err != nil { |
| panic(err) |
| } |
| logInfo := strings.Split(logOutput, " ") |
| gitHash = logInfo[0] |
| gitInfo = logInfo[1] + " " + logInfo[2] + " " + logInfo[0][0:6] |
| |
| // Connect to MySQL server. First, get the password from the metadata server. |
| // See https://developers.google.com/compute/docs/metadata#custom. |
| req, err := http.NewRequest("GET", "http://metadata/computeMetadata/v1/instance/attributes/password", nil) |
| if err != nil { |
| panic(err) |
| } |
| client := http.Client{} |
| req.Header.Add("X-Google-Metadata-Request", "True") |
| if resp, err := client.Do(req); err == nil { |
| password, err := ioutil.ReadAll(resp.Body) |
| if err != nil { |
| log.Printf("ERROR: Failed to read password from metadata server: %q\n", err) |
| panic(err) |
| } |
| // The IP address of the database is found here: |
| // https://console.developers.google.com/project/31977622648/sql/instances/webtry/overview |
| // And 3306 is the default port for MySQL. |
| db, err = sql.Open("mysql", fmt.Sprintf("webtry:%s@tcp(173.194.83.52:3306)/webtry?parseTime=true", password)) |
| if err != nil { |
| log.Printf("ERROR: Failed to open connection to SQL server: %q\n", err) |
| panic(err) |
| } |
| } else { |
| log.Printf("INFO: Failed to find metadata, unable to connect to MySQL server (Expected when running locally): %q\n", err) |
| // Fallback to sqlite for local use. |
| db, err = sql.Open("sqlite3", "./webtry.db") |
| if err != nil { |
| log.Printf("ERROR: Failed to open: %q\n", err) |
| panic(err) |
| } |
| sql := `CREATE TABLE source_images ( |
| id INTEGER PRIMARY KEY NOT NULL, |
| image MEDIUMBLOB DEFAULT '' NOT NULL, -- formatted as a PNG. |
| width INTEGER DEFAULT 0 NOT NULL, |
| height INTEGER DEFAULT 0 NOT NULL, |
| create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, |
| hidden INTEGER DEFAULT 0 NOT NULL |
| )` |
| _, err = db.Exec(sql) |
| log.Printf("Info: status creating sqlite table for sources: %q\n", err) |
| |
| sql = `CREATE TABLE webtry ( |
| code TEXT DEFAULT '' NOT NULL, |
| create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, |
| hash CHAR(64) DEFAULT '' NOT NULL, |
| source_image_id INTEGER DEFAULT 0 NOT NULL, |
| |
| PRIMARY KEY(hash) |
| )` |
| _, err = db.Exec(sql) |
| log.Printf("Info: status creating sqlite table for webtry: %q\n", err) |
| |
| sql = `CREATE TABLE workspace ( |
| name CHAR(64) DEFAULT '' NOT NULL, |
| create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, |
| PRIMARY KEY(name) |
| )` |
| _, err = db.Exec(sql) |
| log.Printf("Info: status creating sqlite table for workspace: %q\n", err) |
| |
| sql = `CREATE TABLE workspacetry ( |
| name CHAR(64) DEFAULT '' NOT NULL, |
| create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, |
| hash CHAR(64) DEFAULT '' NOT NULL, |
| hidden INTEGER DEFAULT 0 NOT NULL, |
| source_image_id INTEGER DEFAULT 0 NOT NULL, |
| |
| FOREIGN KEY (name) REFERENCES workspace(name) |
| )` |
| _, err = db.Exec(sql) |
| log.Printf("Info: status creating sqlite table for workspace try: %q\n", err) |
| } |
| |
| // Ping the database to keep the connection fresh. |
| go func() { |
| c := time.Tick(1 * time.Minute) |
| for _ = range c { |
| if err := db.Ping(); err != nil { |
| log.Printf("ERROR: Database failed to respond: %q\n", err) |
| } |
| } |
| }() |
| |
| metrics.RegisterRuntimeMemStats(metrics.DefaultRegistry) |
| go metrics.CaptureRuntimeMemStats(metrics.DefaultRegistry, 1*time.Minute) |
| |
| // Start reporting metrics. |
| // TODO(jcgregorio) We need a centrialized config server for storing things |
| // like the IP address of the Graphite monitor. |
| addr, _ := net.ResolveTCPAddr("tcp", "skia-monitoring-b:2003") |
| go metrics.Graphite(metrics.DefaultRegistry, 1*time.Minute, "webtry", addr) |
| |
| writeOutAllSourceImages() |
| } |
| |
| func writeOutAllSourceImages() { |
| // Pull all the source images from the db and write them out to inout. |
| rows, err := db.Query("SELECT id, image, create_ts FROM source_images ORDER BY create_ts DESC") |
| |
| if err != nil { |
| log.Printf("ERROR: Failed to open connection to SQL server: %q\n", err) |
| panic(err) |
| } |
| for rows.Next() { |
| var id int |
| var image []byte |
| var create_ts time.Time |
| if err := rows.Scan(&id, &image, &create_ts); err != nil { |
| log.Printf("Error: failed to fetch from database: %q", err) |
| continue |
| } |
| filename := fmt.Sprintf("../../../inout/image-%d.png", id) |
| if _, err := os.Stat(filename); os.IsExist(err) { |
| log.Printf("Skipping write since file exists: %q", filename) |
| continue |
| } |
| if err := ioutil.WriteFile(filename, image, 0666); err != nil { |
| log.Printf("Error: failed to write image file: %q", err) |
| } |
| } |
| } |
| |
| // Titlebar is used in titlebar template expansion. |
| type Titlebar struct { |
| GitHash string |
| GitInfo string |
| } |
| |
| // userCode is used in template expansion. |
| type userCode struct { |
| Code string |
| Hash string |
| Source int |
| Titlebar Titlebar |
| } |
| |
| // writeTemplate creates a given output file and writes the template |
| // result there. |
| func writeTemplate(filename string, t *template.Template, context interface{}) error { |
| f, err := os.Create(filename) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| return t.Execute(f, context) |
| } |
| |
| // expandToFile expands the template and writes the result to the file. |
| func expandToFile(filename string, code string, t *template.Template) error { |
| return writeTemplate(filename, t, userCode{Code: code, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}) |
| } |
| |
| // expandCode expands the template into a file and calculates the MD5 hash. |
| func expandCode(code string, source int) (string, error) { |
| h := md5.New() |
| h.Write([]byte(code)) |
| binary.Write(h, binary.LittleEndian, int64(source)) |
| hash := fmt.Sprintf("%x", h.Sum(nil)) |
| // At this point we are running in skia/experimental/webtry, making cache a |
| // peer directory to skia. |
| // TODO(jcgregorio) Make all relative directories into flags. |
| err := expandToFile(fmt.Sprintf("../../../cache/src/%s.cpp", hash), code, codeTemplate) |
| return hash, err |
| } |
| |
| // expandGyp produces the GYP file needed to build the code |
| func expandGyp(hash string) error { |
| return writeTemplate(fmt.Sprintf("../../../cache/%s.gyp", hash), gypTemplate, struct{ Hash string }{hash}) |
| } |
| |
| // response is serialized to JSON as a response to POSTs. |
| type response struct { |
| Message string `json:"message"` |
| StdOut string `json:"stdout"` |
| Img string `json:"img"` |
| Hash string `json:"hash"` |
| } |
| |
| // doCmd executes the given command line string in either the out/Debug |
| // directory or the inout directory. Returns the stdout and stderr. |
| func doCmd(commandLine string, moveToDebug bool) (string, error) { |
| log.Printf("Command: %q\n", commandLine) |
| programAndArgs := strings.SplitN(commandLine, " ", 2) |
| program := programAndArgs[0] |
| args := []string{} |
| if len(programAndArgs) > 1 { |
| args = strings.Split(programAndArgs[1], " ") |
| } |
| cmd := exec.Command(program, args...) |
| abs, err := filepath.Abs("../../out/Debug") |
| if err != nil { |
| return "", fmt.Errorf("Failed to find absolute path to Debug directory.") |
| } |
| if moveToDebug { |
| cmd.Dir = abs |
| } else if !*useChroot { // Don't set cmd.Dir when using chroot. |
| abs, err := filepath.Abs("../../../inout") |
| if err != nil { |
| return "", fmt.Errorf("Failed to find absolute path to inout directory.") |
| } |
| cmd.Dir = abs |
| } |
| log.Printf("Run in directory: %q\n", cmd.Dir) |
| message, err := cmd.CombinedOutput() |
| log.Printf("StdOut + StdErr: %s\n", string(message)) |
| if err != nil { |
| log.Printf("Exit status: %s\n", err.Error()) |
| return string(message), fmt.Errorf("Failed to run command.") |
| } |
| return string(message), nil |
| } |
| |
| // reportError formats an HTTP error response and also logs the detailed error message. |
| func reportError(w http.ResponseWriter, r *http.Request, err error, message string) { |
| log.Printf("Error: %s\n%s", message, err.Error()) |
| w.Header().Set("Content-Type", "text/plain") |
| http.Error(w, message, 500) |
| } |
| |
| // reportTryError formats an HTTP error response in JSON and also logs the detailed error message. |
| func reportTryError(w http.ResponseWriter, r *http.Request, err error, message, hash string) { |
| m := response{ |
| Message: message, |
| Hash: hash, |
| } |
| log.Printf("Error: %s\n%s", message, err.Error()) |
| resp, err := json.Marshal(m) |
| if err != nil { |
| http.Error(w, "Failed to serialize a response", 500) |
| return |
| } |
| w.Header().Set("Content-Type", "text/plain") |
| w.Write(resp) |
| } |
| |
| func writeToDatabase(hash string, code string, workspaceName string, source int) { |
| if db == nil { |
| return |
| } |
| if _, err := db.Exec("INSERT INTO webtry (code, hash, source_image_id) VALUES(?, ?, ?)", code, hash, source); err != nil { |
| log.Printf("ERROR: Failed to insert code into database: %q\n", err) |
| } |
| if workspaceName != "" { |
| if _, err := db.Exec("INSERT INTO workspacetry (name, hash, source_image_id) VALUES(?, ?, ?)", workspaceName, hash, source); err != nil { |
| log.Printf("ERROR: Failed to insert into workspacetry table: %q\n", err) |
| } |
| } |
| } |
| |
| type Sources struct { |
| Id int `json:"id"` |
| } |
| |
| // sourcesHandler serves up the PNG of a specific try. |
| func sourcesHandler(w http.ResponseWriter, r *http.Request) { |
| log.Printf("Sources Handler: %q\n", r.URL.Path) |
| if r.Method == "GET" { |
| rows, err := db.Query("SELECT id, create_ts FROM source_images WHERE hidden=0 ORDER BY create_ts DESC") |
| |
| if err != nil { |
| http.Error(w, fmt.Sprintf("Failed to query sources: %s.", err), 500) |
| } |
| sources := make([]Sources, 0, 0) |
| for rows.Next() { |
| var id int |
| var create_ts time.Time |
| if err := rows.Scan(&id, &create_ts); err != nil { |
| log.Printf("Error: failed to fetch from database: %q", err) |
| continue |
| } |
| sources = append(sources, Sources{Id: id}) |
| } |
| |
| resp, err := json.Marshal(sources) |
| if err != nil { |
| reportError(w, r, err, "Failed to serialize a response.") |
| return |
| } |
| w.Header().Set("Content-Type", "application/json") |
| w.Write(resp) |
| |
| } else if r.Method == "POST" { |
| if err := r.ParseMultipartForm(1000000); err != nil { |
| http.Error(w, fmt.Sprintf("Failed to load image: %s.", err), 500) |
| return |
| } |
| if _, ok := r.MultipartForm.File["upload"]; !ok { |
| http.Error(w, "Invalid upload.", 500) |
| return |
| } |
| if len(r.MultipartForm.File["upload"]) != 1 { |
| http.Error(w, "Wrong number of uploads.", 500) |
| return |
| } |
| f, err := r.MultipartForm.File["upload"][0].Open() |
| if err != nil { |
| http.Error(w, fmt.Sprintf("Failed to load image: %s.", err), 500) |
| return |
| } |
| defer f.Close() |
| m, _, err := image.Decode(f) |
| if err != nil { |
| http.Error(w, fmt.Sprintf("Failed to decode image: %s.", err), 500) |
| return |
| } |
| var b bytes.Buffer |
| png.Encode(&b, m) |
| bounds := m.Bounds() |
| width := bounds.Max.Y - bounds.Min.Y |
| height := bounds.Max.X - bounds.Min.X |
| if _, err := db.Exec("INSERT INTO source_images (image, width, height) VALUES(?, ?, ?)", b.Bytes(), width, height); err != nil { |
| log.Printf("ERROR: Failed to insert sources into database: %q\n", err) |
| http.Error(w, fmt.Sprintf("Failed to store image: %s.", err), 500) |
| return |
| } |
| go writeOutAllSourceImages() |
| |
| // Now redirect back to where we came from. |
| http.Redirect(w, r, r.Referer(), 302) |
| } else { |
| http.NotFound(w, r) |
| return |
| } |
| } |
| |
| // imageHandler serves up the PNG of a specific try. |
| func imageHandler(w http.ResponseWriter, r *http.Request) { |
| log.Printf("Image Handler: %q\n", r.URL.Path) |
| if r.Method != "GET" { |
| http.NotFound(w, r) |
| return |
| } |
| match := imageLink.FindStringSubmatch(r.URL.Path) |
| if len(match) != 2 { |
| http.NotFound(w, r) |
| return |
| } |
| filename := match[1] |
| w.Header().Set("Content-Type", "image/png") |
| http.ServeFile(w, r, fmt.Sprintf("../../../inout/%s", filename)) |
| } |
| |
| type Try struct { |
| Hash string `json:"hash"` |
| Source int |
| CreateTS string `json:"create_ts"` |
| } |
| |
| type Recent struct { |
| Tries []Try |
| Titlebar Titlebar |
| } |
| |
| // recentHandler shows the last 20 tries. |
| func recentHandler(w http.ResponseWriter, r *http.Request) { |
| log.Printf("Recent Handler: %q\n", r.URL.Path) |
| |
| var err error |
| rows, err := db.Query("SELECT create_ts, hash FROM webtry ORDER BY create_ts DESC LIMIT 20") |
| if err != nil { |
| http.NotFound(w, r) |
| return |
| } |
| recent := []Try{} |
| for rows.Next() { |
| var hash string |
| var create_ts time.Time |
| if err := rows.Scan(&create_ts, &hash); err != nil { |
| log.Printf("Error: failed to fetch from database: %q", err) |
| continue |
| } |
| recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")}) |
| } |
| w.Header().Set("Content-Type", "text/html") |
| if err := recentTemplate.Execute(w, Recent{Tries: recent, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil { |
| log.Printf("ERROR: Failed to expand template: %q\n", err) |
| } |
| } |
| |
| type Workspace struct { |
| Name string |
| Code string |
| Hash string |
| Source int |
| Tries []Try |
| Titlebar Titlebar |
| } |
| |
| // newWorkspace generates a new random workspace name and stores it in the database. |
| func newWorkspace() (string, error) { |
| for i := 0; i < 10; i++ { |
| adj := workspaceNameAdj[rand.Intn(len(workspaceNameAdj))] |
| noun := workspaceNameNoun[rand.Intn(len(workspaceNameNoun))] |
| suffix := rand.Intn(1000) |
| name := fmt.Sprintf("%s-%s-%d", adj, noun, suffix) |
| if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", name); err == nil { |
| return name, nil |
| } else { |
| log.Printf("ERROR: Failed to insert workspace into database: %q\n", err) |
| } |
| } |
| return "", fmt.Errorf("Failed to create a new workspace") |
| } |
| |
| // getCode returns the code for a given hash, or the empty string if not found. |
| func getCode(hash string) (string, int, error) { |
| code := "" |
| source := 0 |
| if err := db.QueryRow("SELECT code, source_image_id FROM webtry WHERE hash=?", hash).Scan(&code, &source); err != nil { |
| log.Printf("ERROR: Code for hash is missing: %q\n", err) |
| return code, source, err |
| } |
| return code, source, nil |
| } |
| |
| func workspaceHandler(w http.ResponseWriter, r *http.Request) { |
| log.Printf("Workspace Handler: %q\n", r.URL.Path) |
| if r.Method == "GET" { |
| tries := []Try{} |
| match := workspaceLink.FindStringSubmatch(r.URL.Path) |
| name := "" |
| if len(match) == 2 { |
| name = match[1] |
| rows, err := db.Query("SELECT create_ts, hash, source_image_id FROM workspacetry WHERE name=? ORDER BY create_ts", name) |
| if err != nil { |
| reportError(w, r, err, "Failed to select.") |
| return |
| } |
| for rows.Next() { |
| var hash string |
| var create_ts time.Time |
| var source int |
| if err := rows.Scan(&create_ts, &hash, &source); err != nil { |
| log.Printf("Error: failed to fetch from database: %q", err) |
| continue |
| } |
| tries = append(tries, Try{Hash: hash, Source: source, CreateTS: create_ts.Format("2006-02-01")}) |
| } |
| } |
| var code string |
| var hash string |
| source := 0 |
| if len(tries) == 0 { |
| code = DEFAULT_SAMPLE |
| } else { |
| hash = tries[len(tries)-1].Hash |
| code, source, _ = getCode(hash) |
| } |
| w.Header().Set("Content-Type", "text/html") |
| if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, Code: code, Name: name, Hash: hash, Source: source, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil { |
| log.Printf("ERROR: Failed to expand template: %q\n", err) |
| } |
| } else if r.Method == "POST" { |
| name, err := newWorkspace() |
| if err != nil { |
| http.Error(w, "Failed to create a new workspace.", 500) |
| return |
| } |
| http.Redirect(w, r, "/w/"+name, 302) |
| } |
| } |
| |
| // hasPreProcessor returns true if any line in the code begins with a # char. |
| func hasPreProcessor(code string) bool { |
| lines := strings.Split(code, "\n") |
| for _, s := range lines { |
| if strings.HasPrefix(strings.TrimSpace(s), "#") { |
| return true |
| } |
| } |
| return false |
| } |
| |
| type TryRequest struct { |
| Code string `json:"code"` |
| Name string `json:"name"` // Optional name of the workspace the code is in. |
| Source int `json:"source"` // ID of the source image, 0 if none. |
| } |
| |
| // iframeHandler handles the GET and POST of the main page. |
| func iframeHandler(w http.ResponseWriter, r *http.Request) { |
| log.Printf("IFrame Handler: %q\n", r.URL.Path) |
| if r.Method != "GET" { |
| http.NotFound(w, r) |
| return |
| } |
| match := iframeLink.FindStringSubmatch(r.URL.Path) |
| if len(match) != 2 { |
| http.NotFound(w, r) |
| return |
| } |
| hash := match[1] |
| if db == nil { |
| http.NotFound(w, r) |
| return |
| } |
| var code string |
| code, source, err := getCode(hash) |
| if err != nil { |
| http.NotFound(w, r) |
| return |
| } |
| // Expand the template. |
| w.Header().Set("Content-Type", "text/html") |
| if err := iframeTemplate.Execute(w, userCode{Code: code, Hash: hash, Source: source}); err != nil { |
| log.Printf("ERROR: Failed to expand template: %q\n", err) |
| } |
| } |
| |
| type TryInfo struct { |
| Hash string `json:"hash"` |
| Code string `json:"code"` |
| Source int `json:"source"` |
| } |
| |
| // tryInfoHandler returns information about a specific try. |
| func tryInfoHandler(w http.ResponseWriter, r *http.Request) { |
| log.Printf("Try Info Handler: %q\n", r.URL.Path) |
| if r.Method != "GET" { |
| http.NotFound(w, r) |
| return |
| } |
| match := tryInfoLink.FindStringSubmatch(r.URL.Path) |
| if len(match) != 2 { |
| http.NotFound(w, r) |
| return |
| } |
| hash := match[1] |
| code, source, err := getCode(hash) |
| if err != nil { |
| http.NotFound(w, r) |
| return |
| } |
| m := TryInfo{ |
| Hash: hash, |
| Code: code, |
| Source: source, |
| } |
| resp, err := json.Marshal(m) |
| if err != nil { |
| reportError(w, r, err, "Failed to serialize a response.") |
| return |
| } |
| w.Header().Set("Content-Type", "application/json") |
| w.Write(resp) |
| } |
| |
| func cleanCompileOutput(s, hash string) string { |
| old := "../../../cache/src/" + hash + ".cpp:" |
| log.Printf("INFO: replacing %q\n", old) |
| return strings.Replace(s, old, "usercode.cpp:", -1) |
| } |
| |
| // mainHandler handles the GET and POST of the main page. |
| func mainHandler(w http.ResponseWriter, r *http.Request) { |
| log.Printf("Main Handler: %q\n", r.URL.Path) |
| requestsCounter.Inc(1) |
| if r.Method == "GET" { |
| code := DEFAULT_SAMPLE |
| source := 0 |
| match := directLink.FindStringSubmatch(r.URL.Path) |
| var hash string |
| if len(match) == 2 && r.URL.Path != "/" { |
| hash = match[1] |
| if db == nil { |
| http.NotFound(w, r) |
| return |
| } |
| // Update 'code' with the code found in the database. |
| if err := db.QueryRow("SELECT code, source_image_id FROM webtry WHERE hash=?", hash).Scan(&code, &source); err != nil { |
| http.NotFound(w, r) |
| return |
| } |
| } |
| // Expand the template. |
| w.Header().Set("Content-Type", "text/html") |
| if err := indexTemplate.Execute(w, userCode{Code: code, Hash: hash, Source: source, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil { |
| log.Printf("ERROR: Failed to expand template: %q\n", err) |
| } |
| } else if r.Method == "POST" { |
| w.Header().Set("Content-Type", "application/json") |
| buf := bytes.NewBuffer(make([]byte, 0, MAX_TRY_SIZE)) |
| n, err := buf.ReadFrom(r.Body) |
| if err != nil { |
| reportTryError(w, r, err, "Failed to read a request body.", "") |
| return |
| } |
| if n == MAX_TRY_SIZE { |
| err := fmt.Errorf("Code length equal to, or exceeded, %d", MAX_TRY_SIZE) |
| reportTryError(w, r, err, "Code too large.", "") |
| return |
| } |
| request := TryRequest{} |
| if err := json.Unmarshal(buf.Bytes(), &request); err != nil { |
| reportTryError(w, r, err, "Coulnd't decode JSON.", "") |
| return |
| } |
| if hasPreProcessor(request.Code) { |
| err := fmt.Errorf("Found preprocessor macro in code.") |
| reportTryError(w, r, err, "Preprocessor macros aren't allowed.", "") |
| return |
| } |
| hash, err := expandCode(LineNumbers(request.Code), request.Source) |
| if err != nil { |
| reportTryError(w, r, err, "Failed to write the code to compile.", hash) |
| return |
| } |
| writeToDatabase(hash, request.Code, request.Name, request.Source) |
| err = expandGyp(hash) |
| if err != nil { |
| reportTryError(w, r, err, "Failed to write the gyp file.", hash) |
| return |
| } |
| message, err := doCmd(fmt.Sprintf(RUN_GYP, hash), true) |
| if err != nil { |
| message = cleanCompileOutput(message, hash) |
| reportTryError(w, r, err, message, hash) |
| return |
| } |
| linkMessage, err := doCmd(fmt.Sprintf(RUN_NINJA, hash), true) |
| if err != nil { |
| linkMessage = cleanCompileOutput(linkMessage, hash) |
| reportTryError(w, r, err, linkMessage, hash) |
| return |
| } |
| message += linkMessage |
| cmd := hash + " --out " + hash + ".png" |
| if request.Source > 0 { |
| cmd += fmt.Sprintf(" --source image-%d.png", request.Source) |
| } |
| if *useChroot { |
| cmd = "schroot -c webtry --directory=/inout -- /inout/Release/" + cmd |
| } else { |
| abs, err := filepath.Abs("../../../inout/Release") |
| if err != nil { |
| reportTryError(w, r, err, "Failed to find executable directory.", hash) |
| return |
| } |
| cmd = abs + "/" + cmd |
| } |
| |
| execMessage, err := doCmd(cmd, false) |
| if err != nil { |
| reportTryError(w, r, err, "Failed to run the code:\n"+execMessage, hash) |
| return |
| } |
| png, err := ioutil.ReadFile("../../../inout/" + hash + ".png") |
| if err != nil { |
| reportTryError(w, r, err, "Failed to open the generated PNG.", hash) |
| return |
| } |
| |
| m := response{ |
| Message: message, |
| StdOut: execMessage, |
| Img: base64.StdEncoding.EncodeToString([]byte(png)), |
| Hash: hash, |
| } |
| resp, err := json.Marshal(m) |
| if err != nil { |
| reportTryError(w, r, err, "Failed to serialize a response.", hash) |
| return |
| } |
| w.Header().Set("Content-Type", "application/json") |
| w.Write(resp) |
| } |
| } |
| |
| func main() { |
| flag.Parse() |
| http.HandleFunc("/i/", autogzip.HandleFunc(imageHandler)) |
| http.HandleFunc("/w/", autogzip.HandleFunc(workspaceHandler)) |
| http.HandleFunc("/recent/", autogzip.HandleFunc(recentHandler)) |
| http.HandleFunc("/iframe/", autogzip.HandleFunc(iframeHandler)) |
| http.HandleFunc("/json/", autogzip.HandleFunc(tryInfoHandler)) |
| http.HandleFunc("/sources/", autogzip.HandleFunc(sourcesHandler)) |
| |
| // Resources are served directly |
| // TODO add support for caching/etags/gzip |
| http.Handle("/res/", autogzip.Handle(http.FileServer(http.Dir("./")))) |
| |
| // TODO Break out /c/ as it's own handler. |
| http.HandleFunc("/", autogzip.HandleFunc(mainHandler)) |
| log.Fatal(http.ListenAndServe(*port, nil)) |
| } |