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