| Index: experimental/webtry/webtry.go
|
| diff --git a/experimental/webtry/webtry.go b/experimental/webtry/webtry.go
|
| deleted file mode 100644
|
| index 9b148343f3261851b3b74e14b3e4749dee7d81af..0000000000000000000000000000000000000000
|
| --- a/experimental/webtry/webtry.go
|
| +++ /dev/null
|
| @@ -1,987 +0,0 @@
|
| -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"
|
| - "math/rand"
|
| - "net"
|
| - "net/http"
|
| - "os"
|
| - "os/exec"
|
| - "path/filepath"
|
| - "regexp"
|
| - "strconv"
|
| - "strings"
|
| - "text/template"
|
| - "time"
|
| -)
|
| -
|
| -import (
|
| - "github.com/fiorix/go-web/autogzip"
|
| - _ "github.com/go-sql-driver/mysql"
|
| - "github.com/golang/glog"
|
| - _ "github.com/mattn/go-sqlite3"
|
| - "github.com/rcrowley/go-metrics"
|
| -)
|
| -
|
| -const (
|
| - 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-]+)$")
|
| -
|
| - // errorRE is ther regex that matches compiler errors and extracts the line / column information.
|
| - errorRE = regexp.MustCompile("^.*.cpp:(\\d+):(\\d+):\\s*(.*)")
|
| -
|
| - // 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.
|
| - cwd, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
| - if err != nil {
|
| - glog.Fatal(err)
|
| - }
|
| - if err := os.Chdir(cwd); err != nil {
|
| - glog.Fatal(err)
|
| - }
|
| -
|
| - codeTemplate = template.Must(template.ParseFiles(filepath.Join(cwd, "templates/template.cpp")))
|
| - gypTemplate = template.Must(template.ParseFiles(filepath.Join(cwd, "templates/template.gyp")))
|
| - indexTemplate = htemplate.Must(htemplate.ParseFiles(
|
| - filepath.Join(cwd, "templates/index.html"),
|
| - filepath.Join(cwd, "templates/titlebar.html"),
|
| - filepath.Join(cwd, "templates/sidebar.html"),
|
| - filepath.Join(cwd, "templates/content.html"),
|
| - filepath.Join(cwd, "templates/headercommon.html"),
|
| - filepath.Join(cwd, "templates/footercommon.html"),
|
| - ))
|
| - iframeTemplate = htemplate.Must(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"),
|
| - ))
|
| - recentTemplate = htemplate.Must(htemplate.ParseFiles(
|
| - filepath.Join(cwd, "templates/recent.html"),
|
| - filepath.Join(cwd, "templates/titlebar.html"),
|
| - filepath.Join(cwd, "templates/sidebar.html"),
|
| - filepath.Join(cwd, "templates/headercommon.html"),
|
| - filepath.Join(cwd, "templates/footercommon.html"),
|
| - ))
|
| - workspaceTemplate = htemplate.Must(htemplate.ParseFiles(
|
| - filepath.Join(cwd, "templates/workspace.html"),
|
| - filepath.Join(cwd, "templates/titlebar.html"),
|
| - filepath.Join(cwd, "templates/sidebar.html"),
|
| - filepath.Join(cwd, "templates/content.html"),
|
| - filepath.Join(cwd, "templates/headercommon.html"),
|
| - filepath.Join(cwd, "templates/footercommon.html"),
|
| - ))
|
| -
|
| - // 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`)
|
| - 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 {
|
| - glog.Errorf("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 {
|
| - glog.Errorf("ERROR: Failed to open connection to SQL server: %q\n", err)
|
| - panic(err)
|
| - }
|
| - } else {
|
| - glog.Infof("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 {
|
| - glog.Errorf("Failed to open: %q\n", err)
|
| - panic(err)
|
| - }
|
| - sql := `CREATE TABLE IF NOT EXISTS 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)
|
| - if err != nil {
|
| - glog.Errorf("Creating source_images table failed: %s", err)
|
| - }
|
| -
|
| - sql = `CREATE TABLE IF NOT EXISTS webtry (
|
| - code TEXT DEFAULT '' NOT NULL,
|
| - create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
| - hash CHAR(64) DEFAULT '' NOT NULL,
|
| - width INTEGER DEFAULT 256 NOT NULL,
|
| - height INTEGER DEFAULT 256 NOT NULL,
|
| - source_image_id INTEGER DEFAULT 0 NOT NULL,
|
| -
|
| - PRIMARY KEY(hash)
|
| - )`
|
| - _, err = db.Exec(sql)
|
| - if err != nil {
|
| - glog.Errorf("Creating webtry table failed: %s", err)
|
| - }
|
| -
|
| - sql = `CREATE TABLE IF NOT EXISTS workspace (
|
| - name CHAR(64) DEFAULT '' NOT NULL,
|
| - create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
| - PRIMARY KEY(name)
|
| - )`
|
| - _, err = db.Exec(sql)
|
| - if err != nil {
|
| - glog.Errorf("Creating workspace table failed: %s", err)
|
| - }
|
| -
|
| - sql = `CREATE TABLE IF NOT EXISTS workspacetry (
|
| - name CHAR(64) DEFAULT '' NOT NULL,
|
| - create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
| - hash CHAR(64) DEFAULT '' NOT NULL,
|
| - width INTEGER DEFAULT 256 NOT NULL,
|
| - height INTEGER DEFAULT 256 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)
|
| - if err != nil {
|
| - glog.Errorf("Creating workspacetry table failed: %s", 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 {
|
| - glog.Errorf("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 {
|
| - glog.Errorf("Failed to open connection to SQL server: %q\n", err)
|
| - panic(err)
|
| - }
|
| - defer rows.Close()
|
| - for rows.Next() {
|
| - var id int
|
| - var image []byte
|
| - var create_ts time.Time
|
| - if err := rows.Scan(&id, &image, &create_ts); err != nil {
|
| - glog.Errorf("failed to fetch from database: %q", err)
|
| - continue
|
| - }
|
| - filename := fmt.Sprintf("../../../inout/image-%d.png", id)
|
| - if _, err := os.Stat(filename); os.IsExist(err) {
|
| - glog.Infof("Skipping write since file exists: %q", filename)
|
| - continue
|
| - }
|
| - if err := ioutil.WriteFile(filename, image, 0666); err != nil {
|
| - glog.Errorf("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
|
| - Width int
|
| - Height int
|
| - 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.
|
| -// We include the width and height here so that a single hash can capture
|
| -// both the code and the supplied width/height parameters.
|
| -func expandCode(code string, source int, width, height int) (string, error) {
|
| - // in order to support fonts in the chroot jail, we need to make sure
|
| - // we're using portable typefaces.
|
| - // TODO(humper): Make this more robust, supporting things like setTypeface
|
| -
|
| - inputCodeLines := strings.Split(code, "\n")
|
| - outputCodeLines := []string{
|
| - "DECLARE_bool(portableFonts);",
|
| - fmt.Sprintf("// WxH: %d, %d", width, height),
|
| - }
|
| - for _, line := range inputCodeLines {
|
| - outputCodeLines = append(outputCodeLines, line)
|
| - if strings.HasPrefix(strings.TrimSpace(line), "SkPaint p") {
|
| - outputCodeLines = append(outputCodeLines, "FLAGS_portableFonts = true;")
|
| - outputCodeLines = append(outputCodeLines, "sk_tool_utils::set_portable_typeface(&p, \"Helvetica\", SkTypeface::kNormal);")
|
| - }
|
| - }
|
| -
|
| - fontFriendlyCode := strings.Join(outputCodeLines, "\n")
|
| -
|
| - h := md5.New()
|
| - h.Write([]byte(fontFriendlyCode))
|
| - 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), fontFriendlyCode, 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"`
|
| - CompileErrors []compileError `json:"compileErrors"`
|
| - RasterImg string `json:"rasterImg"`
|
| - GPUImg string `json:"gpuImg"`
|
| - Hash string `json:"hash"`
|
| -}
|
| -
|
| -// doCmd executes the given command line string; the command being
|
| -// run is expected to not care what its current working directory is.
|
| -// Returns the stdout and stderr.
|
| -func doCmd(commandLine string) (string, error) {
|
| - glog.Infof("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...)
|
| - message, err := cmd.CombinedOutput()
|
| - glog.Infof("StdOut + StdErr: %s\n", string(message))
|
| - if err != nil {
|
| - glog.Errorf("Exit status: %s\n", err)
|
| - 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) {
|
| - glog.Errorf("%s\n%s", message, err)
|
| - 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,
|
| - }
|
| - glog.Errorf("%s\n%s", message, err)
|
| - 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 reportCompileError(w http.ResponseWriter, r *http.Request, compileErrors []compileError, hash string) {
|
| - m := response{
|
| - CompileErrors: compileErrors,
|
| - Hash: hash,
|
| - }
|
| -
|
| - 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, width, height int) {
|
| - if db == nil {
|
| - return
|
| - }
|
| - if _, err := db.Exec("INSERT INTO webtry (code, hash, width, height, source_image_id) VALUES(?, ?, ?, ?, ?)", code, hash, width, height, source); err != nil {
|
| - glog.Errorf("Failed to insert code into database: %q\n", err)
|
| - }
|
| - if workspaceName != "" {
|
| - if _, err := db.Exec("INSERT INTO workspacetry (name, hash, width, height, source_image_id) VALUES(?, ?, ?, ?, ?)", workspaceName, hash, width, height, source); err != nil {
|
| - glog.Errorf("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) {
|
| - glog.Infof("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)
|
| - }
|
| - defer rows.Close()
|
| - 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 {
|
| - glog.Errorf("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 {
|
| - glog.Errorf("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) {
|
| - glog.Infof("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) {
|
| - glog.Infof("Recent Handler: %q\n", r.URL.Path)
|
| -
|
| - 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
|
| - }
|
| - defer rows.Close()
|
| - recent := []Try{}
|
| - for rows.Next() {
|
| - var hash string
|
| - var create_ts time.Time
|
| - if err := rows.Scan(&create_ts, &hash); err != nil {
|
| - glog.Errorf("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 {
|
| - glog.Errorf("Failed to expand template: %q\n", err)
|
| - }
|
| -}
|
| -
|
| -type Workspace struct {
|
| - Name string
|
| - Code string
|
| - Hash string
|
| - Width int
|
| - Height int
|
| - 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 {
|
| - glog.Errorf("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, int, int, error) {
|
| - code := ""
|
| - width := 0
|
| - height := 0
|
| - source := 0
|
| - if err := db.QueryRow("SELECT code, width, height, source_image_id FROM webtry WHERE hash=?", hash).Scan(&code, &width, &height, &source); err != nil {
|
| - glog.Errorf("Code for hash is missing: %q\n", err)
|
| - return code, width, height, source, err
|
| - }
|
| - return code, width, height, source, nil
|
| -}
|
| -
|
| -func workspaceHandler(w http.ResponseWriter, r *http.Request) {
|
| - glog.Infof("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
|
| - }
|
| - defer rows.Close()
|
| - for rows.Next() {
|
| - var hash string
|
| - var create_ts time.Time
|
| - var source int
|
| - if err := rows.Scan(&create_ts, &hash, &source); err != nil {
|
| - glog.Errorf("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
|
| - var width int
|
| - var height int
|
| - source := 0
|
| - if len(tries) == 0 {
|
| - code = DEFAULT_SAMPLE
|
| - width = 256
|
| - height = 256
|
| - } else {
|
| - hash = tries[len(tries)-1].Hash
|
| - code, width, height, source, _ = getCode(hash)
|
| - }
|
| - w.Header().Set("Content-Type", "text/html")
|
| - if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, Code: code, Name: name, Hash: hash, Width: width, Height: height, Source: source, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
|
| - glog.Errorf("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"`
|
| - Width int `json:"width"`
|
| - Height int `json:"height"`
|
| - GPU bool `json:"gpu"`
|
| - Raster bool `json:"raster"`
|
| - PDF bool `json:"pdf"`
|
| - 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) {
|
| - glog.Infof("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, width, height, 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, Width: width, Height: height, Hash: hash, Source: source}); err != nil {
|
| - glog.Errorf("Failed to expand template: %q\n", err)
|
| - }
|
| -}
|
| -
|
| -type TryInfo struct {
|
| - Hash string `json:"hash"`
|
| - Code string `json:"code"`
|
| - Width int `json:"width"`
|
| - Height int `json:"height"`
|
| - Source int `json:"source"`
|
| -}
|
| -
|
| -// tryInfoHandler returns information about a specific try.
|
| -func tryInfoHandler(w http.ResponseWriter, r *http.Request) {
|
| - glog.Infof("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, width, height, source, err := getCode(hash)
|
| - if err != nil {
|
| - http.NotFound(w, r)
|
| - return
|
| - }
|
| - m := TryInfo{
|
| - Hash: hash,
|
| - Code: code,
|
| - Width: width,
|
| - Height: height,
|
| - 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:"
|
| - glog.Infof("replacing %q\n", old)
|
| - return strings.Replace(s, old, "usercode.cpp:", -1)
|
| -}
|
| -
|
| -type compileError struct {
|
| - Line int `json:"line"`
|
| - Column int `json:"column"`
|
| - Error string `json:"error"`
|
| -}
|
| -
|
| -// mainHandler handles the GET and POST of the main page.
|
| -func mainHandler(w http.ResponseWriter, r *http.Request) {
|
| - glog.Infof("Main Handler: %q\n", r.URL.Path)
|
| - requestsCounter.Inc(1)
|
| - if r.Method == "GET" {
|
| - code := DEFAULT_SAMPLE
|
| - source := 0
|
| - width := 256
|
| - height := 256
|
| - 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, width, height, source_image_id FROM webtry WHERE hash=?", hash).Scan(&code, &width, &height, &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, Width: width, Height: height, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
|
| - glog.Errorf("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 !(request.GPU || request.Raster || request.PDF) {
|
| - reportTryError(w, r, nil, "No run configuration supplied...", "")
|
| - 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, request.Width, request.Height)
|
| - if err != nil {
|
| - reportTryError(w, r, err, "Failed to write the code to compile.", hash)
|
| - return
|
| - }
|
| - writeToDatabase(hash, request.Code, request.Name, request.Source, request.Width, request.Height)
|
| - err = expandGyp(hash)
|
| - if err != nil {
|
| - reportTryError(w, r, err, "Failed to write the gyp file.", hash)
|
| - return
|
| - }
|
| - cmd := fmt.Sprintf("scripts/fiddle_wrapper %s --width %d --height %d", hash, request.Width, request.Height)
|
| - if request.Raster {
|
| - cmd += " --raster"
|
| - }
|
| - if request.GPU {
|
| - cmd += " --gpu"
|
| - }
|
| - if request.PDF {
|
| - cmd += " --pdf"
|
| - }
|
| - if *useChroot {
|
| - cmd = "schroot -c webtry --directory=/ -- /skia_build/skia/experimental/webtry/" + cmd
|
| - }
|
| - if request.Source > 0 {
|
| - cmd += fmt.Sprintf(" --source image-%d.png", request.Source)
|
| - }
|
| -
|
| - message, err := doCmd(cmd)
|
| -
|
| - outputLines := strings.Split(message, "\n")
|
| - errorLines := []compileError{}
|
| - for _, line := range outputLines {
|
| - match := errorRE.FindStringSubmatch(line)
|
| - if len(match) > 0 {
|
| - lineNumber, parseError := strconv.Atoi(match[1])
|
| - if parseError != nil {
|
| - glog.Errorf("ERROR: Couldn't parse line number from %s\n", match[1])
|
| - continue
|
| - }
|
| - columnNumber, parseError := strconv.Atoi(match[2])
|
| - if parseError != nil {
|
| - glog.Errorf("ERROR: Couldn't parse column number from %s\n", match[2])
|
| - continue
|
| - }
|
| - errorLines = append(errorLines,
|
| - compileError{
|
| - Line: lineNumber,
|
| - Column: columnNumber,
|
| - Error: match[3],
|
| - })
|
| - }
|
| - }
|
| -
|
| - if err != nil {
|
| - if len(errorLines) > 0 {
|
| - reportCompileError(w, r, errorLines, hash)
|
| - } else {
|
| - reportTryError(w, r, err, "Failed to run the code:\n"+message, hash)
|
| - }
|
| - return
|
| - }
|
| -
|
| - m := response{
|
| - Hash: hash,
|
| - }
|
| -
|
| - if request.Raster {
|
| - png, err := ioutil.ReadFile("../../../inout/" + hash + "_raster.png")
|
| - if err != nil {
|
| - reportTryError(w, r, err, "Failed to open the raster-generated PNG.", hash)
|
| - return
|
| - }
|
| -
|
| - m.RasterImg = base64.StdEncoding.EncodeToString([]byte(png))
|
| - }
|
| -
|
| - if request.GPU {
|
| - png, err := ioutil.ReadFile("../../../inout/" + hash + "_gpu.png")
|
| - if err != nil {
|
| - reportTryError(w, r, err, "Failed to open the GPU-generated PNG.", hash)
|
| - return
|
| - }
|
| -
|
| - m.GPUImg = base64.StdEncoding.EncodeToString([]byte(png))
|
| - }
|
| -
|
| - 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()
|
| - Init()
|
| - 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))
|
| - glog.Fatal(http.ListenAndServe(*port, nil))
|
| -}
|
|
|