github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/dashboard/app/build/handler.go (about) 1 // Copyright 2011 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // +build appengine 6 7 package build 8 9 import ( 10 "bytes" 11 "crypto/hmac" 12 "crypto/md5" 13 "encoding/json" 14 "errors" 15 "fmt" 16 "io/ioutil" 17 "net/http" 18 "strconv" 19 "strings" 20 "unicode/utf8" 21 22 "appengine" 23 "appengine/datastore" 24 25 "cache" 26 "key" 27 ) 28 29 const ( 30 commitsPerPage = 30 31 watcherVersion = 3 // must match dashboard/watcher/watcher.go 32 builderVersion = 1 // must match dashboard/builder/http.go 33 ) 34 35 // commitHandler retrieves commit data or records a new commit. 36 // 37 // For GET requests it returns a Commit value for the specified 38 // packagePath and hash. 39 // 40 // For POST requests it reads a JSON-encoded Commit value from the request 41 // body and creates a new Commit entity. It also updates the "tip" Tag for 42 // each new commit at tip. 43 // 44 // This handler is used by a gobuilder process in -commit mode. 45 func commitHandler(r *http.Request) (interface{}, error) { 46 c := contextForRequest(r) 47 com := new(Commit) 48 49 if r.Method == "GET" { 50 com.PackagePath = r.FormValue("packagePath") 51 com.Hash = r.FormValue("hash") 52 err := datastore.Get(c, com.Key(c), com) 53 if com.Num == 0 && com.Desc == "" { 54 // Perf builder might have written an incomplete Commit. 55 // Pretend it doesn't exist, so that we can get complete details. 56 err = datastore.ErrNoSuchEntity 57 } 58 if err != nil { 59 if err == datastore.ErrNoSuchEntity { 60 // This error string is special. 61 // The commit watcher expects it. 62 // Do not change it. 63 return nil, errors.New("Commit not found") 64 } 65 return nil, fmt.Errorf("getting Commit: %v", err) 66 } 67 if com.Num == 0 { 68 // Corrupt state which shouldn't happen but does. 69 // Return an error so builders' commit loops will 70 // be willing to retry submitting this commit. 71 return nil, errors.New("in datastore with zero Num") 72 } 73 if com.Desc == "" || com.User == "" { 74 // Also shouldn't happen, but at least happened 75 // once on a single commit when trying to fix data 76 // in the datastore viewer UI? 77 return nil, errors.New("missing field") 78 } 79 // Strip potentially large and unnecessary fields. 80 com.ResultData = nil 81 com.PerfResults = nil 82 return com, nil 83 } 84 if r.Method != "POST" { 85 return nil, errBadMethod(r.Method) 86 } 87 if !isMasterKey(c, r.FormValue("key")) { 88 return nil, errors.New("can only POST commits with master key") 89 } 90 91 // For now, the commit watcher doesn't support gccgo. 92 // TODO(adg,cmang): remove this exception when gccgo is supported. 93 if dashboardForRequest(r) != gccgoDash { 94 v, _ := strconv.Atoi(r.FormValue("version")) 95 if v != watcherVersion { 96 return nil, fmt.Errorf("rejecting POST from commit watcher; need version %v", watcherVersion) 97 } 98 } 99 100 // POST request 101 body, err := ioutil.ReadAll(r.Body) 102 r.Body.Close() 103 if err != nil { 104 return nil, fmt.Errorf("reading Body: %v", err) 105 } 106 if !bytes.Contains(body, needsBenchmarkingBytes) { 107 c.Warningf("old builder detected at %v", r.RemoteAddr) 108 return nil, fmt.Errorf("rejecting old builder request, body does not contain %s: %q", needsBenchmarkingBytes, body) 109 } 110 if err := json.Unmarshal(body, com); err != nil { 111 return nil, fmt.Errorf("unmarshaling body %q: %v", body, err) 112 } 113 com.Desc = limitStringLength(com.Desc, maxDatastoreStringLen) 114 if err := com.Valid(); err != nil { 115 return nil, fmt.Errorf("validating Commit: %v", err) 116 } 117 defer cache.Tick(c) 118 tx := func(c appengine.Context) error { 119 return addCommit(c, com) 120 } 121 return nil, datastore.RunInTransaction(c, tx, nil) 122 } 123 124 var needsBenchmarkingBytes = []byte(`"NeedsBenchmarking"`) 125 126 // addCommit adds the Commit entity to the datastore and updates the tip Tag. 127 // It must be run inside a datastore transaction. 128 func addCommit(c appengine.Context, com *Commit) error { 129 var ec Commit // existing commit 130 isUpdate := false 131 err := datastore.Get(c, com.Key(c), &ec) 132 if err != nil && err != datastore.ErrNoSuchEntity { 133 return fmt.Errorf("getting Commit: %v", err) 134 } 135 if err == nil { 136 // Commit already in the datastore. Any fields different? 137 // If not, don't do anything. 138 changes := (com.Num != 0 && com.Num != ec.Num) || 139 com.ParentHash != ec.ParentHash || 140 com.Desc != ec.Desc || 141 com.User != ec.User || 142 !com.Time.Equal(ec.Time) 143 if !changes { 144 return nil 145 } 146 ec.ParentHash = com.ParentHash 147 ec.Desc = com.Desc 148 ec.User = com.User 149 if !com.Time.IsZero() { 150 ec.Time = com.Time 151 } 152 if com.Num != 0 { 153 ec.Num = com.Num 154 } 155 isUpdate = true 156 com = &ec 157 } 158 p, err := GetPackage(c, com.PackagePath) 159 if err != nil { 160 return fmt.Errorf("GetPackage: %v", err) 161 } 162 if com.Num == 0 { 163 // get the next commit number 164 com.Num = p.NextNum 165 p.NextNum++ 166 if _, err := datastore.Put(c, p.Key(c), p); err != nil { 167 return fmt.Errorf("putting Package: %v", err) 168 } 169 } else if com.Num >= p.NextNum { 170 p.NextNum = com.Num + 1 171 if _, err := datastore.Put(c, p.Key(c), p); err != nil { 172 return fmt.Errorf("putting Package: %v", err) 173 } 174 } 175 // if this isn't the first Commit test the parent commit exists. 176 // The all zeros are returned by hg's p1node template for parentless commits. 177 if com.ParentHash != "" && com.ParentHash != "0000000000000000000000000000000000000000" && com.ParentHash != "0000" { 178 n, err := datastore.NewQuery("Commit"). 179 Filter("Hash =", com.ParentHash). 180 Ancestor(p.Key(c)). 181 Count(c) 182 if err != nil { 183 return fmt.Errorf("testing for parent Commit: %v", err) 184 } 185 if n == 0 { 186 return errors.New("parent commit not found") 187 } 188 } else if com.Num != 1 { 189 // This is the first commit; fail if it is not number 1. 190 // (This will happen if we try to upload a new/different repo 191 // where there is already commit data. A bad thing to do.) 192 return errors.New("this package already has a first commit; aborting") 193 } 194 // update the tip Tag if this is the Go repo and this isn't on a release branch 195 if p.Path == "" && !strings.HasPrefix(com.Desc, "[") && !isUpdate { 196 t := &Tag{Kind: "tip", Hash: com.Hash} 197 if _, err = datastore.Put(c, t.Key(c), t); err != nil { 198 return fmt.Errorf("putting Tag: %v", err) 199 } 200 } 201 // put the Commit 202 if err = putCommit(c, com); err != nil { 203 return err 204 } 205 if com.NeedsBenchmarking { 206 // add to CommitRun 207 cr, err := GetCommitRun(c, com.Num) 208 if err != nil { 209 return err 210 } 211 if err = cr.AddCommit(c, com); err != nil { 212 return err 213 } 214 // create PerfResult 215 res := &PerfResult{CommitHash: com.Hash, CommitNum: com.Num} 216 if _, err := datastore.Put(c, res.Key(c), res); err != nil { 217 return fmt.Errorf("putting PerfResult: %v", err) 218 } 219 // Update perf todo if necessary. 220 if err = AddCommitToPerfTodo(c, com); err != nil { 221 return err 222 } 223 } 224 return nil 225 } 226 227 // tagHandler records a new tag. It reads a JSON-encoded Tag value from the 228 // request body and updates the Tag entity for the Kind of tag provided. 229 // 230 // This handler is used by a gobuilder process in -commit mode. 231 func tagHandler(r *http.Request) (interface{}, error) { 232 if r.Method != "POST" { 233 return nil, errBadMethod(r.Method) 234 } 235 236 t := new(Tag) 237 defer r.Body.Close() 238 if err := json.NewDecoder(r.Body).Decode(t); err != nil { 239 return nil, err 240 } 241 if err := t.Valid(); err != nil { 242 return nil, err 243 } 244 c := contextForRequest(r) 245 defer cache.Tick(c) 246 _, err := datastore.Put(c, t.Key(c), t) 247 return nil, err 248 } 249 250 // Todo is a todoHandler response. 251 type Todo struct { 252 Kind string // "build-go-commit" or "build-package" 253 Data interface{} 254 } 255 256 // todoHandler returns the next action to be performed by a builder. 257 // It expects "builder" and "kind" query parameters and returns a *Todo value. 258 // Multiple "kind" parameters may be specified. 259 func todoHandler(r *http.Request) (interface{}, error) { 260 c := contextForRequest(r) 261 now := cache.Now(c) 262 key := "build-todo-" + r.Form.Encode() 263 var todo *Todo 264 if cache.Get(r, now, key, &todo) { 265 return todo, nil 266 } 267 var err error 268 builder := r.FormValue("builder") 269 for _, kind := range r.Form["kind"] { 270 var com *Commit 271 switch kind { 272 case "build-go-commit": 273 com, err = buildTodo(c, builder, "", "") 274 if com != nil { 275 com.PerfResults = []string{} 276 } 277 case "build-package": 278 packagePath := r.FormValue("packagePath") 279 goHash := r.FormValue("goHash") 280 com, err = buildTodo(c, builder, packagePath, goHash) 281 if com != nil { 282 com.PerfResults = []string{} 283 } 284 case "benchmark-go-commit": 285 com, err = perfTodo(c, builder) 286 } 287 if com != nil || err != nil { 288 if com != nil { 289 // ResultData can be large and not needed on builder. 290 com.ResultData = []string{} 291 } 292 todo = &Todo{Kind: kind, Data: com} 293 break 294 } 295 } 296 if err == nil { 297 cache.Set(r, now, key, todo) 298 } 299 return todo, err 300 } 301 302 // buildTodo returns the next Commit to be built (or nil if none available). 303 // 304 // If packagePath and goHash are empty, it scans the first 20 Go Commits in 305 // Num-descending order and returns the first one it finds that doesn't have a 306 // Result for this builder. 307 // 308 // If provided with non-empty packagePath and goHash args, it scans the first 309 // 20 Commits in Num-descending order for the specified packagePath and 310 // returns the first that doesn't have a Result for this builder and goHash. 311 func buildTodo(c appengine.Context, builder, packagePath, goHash string) (*Commit, error) { 312 p, err := GetPackage(c, packagePath) 313 if err != nil { 314 return nil, err 315 } 316 317 t := datastore.NewQuery("Commit"). 318 Ancestor(p.Key(c)). 319 Limit(commitsPerPage). 320 Order("-Num"). 321 Run(c) 322 for { 323 com := new(Commit) 324 if _, err := t.Next(com); err == datastore.Done { 325 break 326 } else if err != nil { 327 return nil, err 328 } 329 if com.Result(builder, goHash) == nil { 330 return com, nil 331 } 332 } 333 334 // Nothing left to do if this is a package (not the Go tree). 335 if packagePath != "" { 336 return nil, nil 337 } 338 339 // If there are no Go tree commits left to build, 340 // see if there are any subrepo commits that need to be built at tip. 341 // If so, ask the builder to build a go tree at the tip commit. 342 // TODO(adg): do the same for "weekly" and "release" tags. 343 344 tag, err := GetTag(c, "tip") 345 if err != nil { 346 return nil, err 347 } 348 349 // Check that this Go commit builds OK for this builder. 350 // If not, don't re-build as the subrepos will never get built anyway. 351 com, err := tag.Commit(c) 352 if err != nil { 353 return nil, err 354 } 355 if r := com.Result(builder, ""); r != nil && !r.OK { 356 return nil, nil 357 } 358 359 pkgs, err := Packages(c, "subrepo") 360 if err != nil { 361 return nil, err 362 } 363 for _, pkg := range pkgs { 364 com, err := pkg.LastCommit(c) 365 if err != nil { 366 c.Warningf("%v: no Commit found: %v", pkg, err) 367 continue 368 } 369 if com.Result(builder, tag.Hash) == nil { 370 return tag.Commit(c) 371 } 372 } 373 374 return nil, nil 375 } 376 377 // perfTodo returns the next Commit to be benchmarked (or nil if none available). 378 func perfTodo(c appengine.Context, builder string) (*Commit, error) { 379 p := &Package{} 380 todo := &PerfTodo{Builder: builder} 381 err := datastore.Get(c, todo.Key(c), todo) 382 if err != nil && err != datastore.ErrNoSuchEntity { 383 return nil, fmt.Errorf("fetching PerfTodo: %v", err) 384 } 385 if err == datastore.ErrNoSuchEntity { 386 todo, err = buildPerfTodo(c, builder) 387 if err != nil { 388 return nil, err 389 } 390 } 391 if len(todo.CommitNums) == 0 { 392 return nil, nil 393 } 394 395 // Have commit to benchmark, fetch it. 396 num := todo.CommitNums[len(todo.CommitNums)-1] 397 t := datastore.NewQuery("Commit"). 398 Ancestor(p.Key(c)). 399 Filter("Num =", num). 400 Limit(1). 401 Run(c) 402 com := new(Commit) 403 if _, err := t.Next(com); err != nil { 404 return nil, err 405 } 406 if !com.NeedsBenchmarking { 407 return nil, fmt.Errorf("commit from perf todo queue is not intended for benchmarking") 408 } 409 410 // Remove benchmarks from other builders. 411 var benchs []string 412 for _, b := range com.PerfResults { 413 bb := strings.Split(b, "|") 414 if bb[0] == builder && bb[1] != "meta-done" { 415 benchs = append(benchs, bb[1]) 416 } 417 } 418 com.PerfResults = benchs 419 420 return com, nil 421 } 422 423 // buildPerfTodo creates PerfTodo for the builder with all commits. In a transaction. 424 func buildPerfTodo(c appengine.Context, builder string) (*PerfTodo, error) { 425 todo := &PerfTodo{Builder: builder} 426 tx := func(c appengine.Context) error { 427 err := datastore.Get(c, todo.Key(c), todo) 428 if err != nil && err != datastore.ErrNoSuchEntity { 429 return fmt.Errorf("fetching PerfTodo: %v", err) 430 } 431 if err == nil { 432 return nil 433 } 434 t := datastore.NewQuery("CommitRun"). 435 Ancestor((&Package{}).Key(c)). 436 Order("-StartCommitNum"). 437 Run(c) 438 var nums []int 439 var releaseNums []int 440 loop: 441 for { 442 cr := new(CommitRun) 443 if _, err := t.Next(cr); err == datastore.Done { 444 break 445 } else if err != nil { 446 return fmt.Errorf("scanning commit runs for perf todo: %v", err) 447 } 448 for i := len(cr.Hash) - 1; i >= 0; i-- { 449 if !cr.NeedsBenchmarking[i] || cr.Hash[i] == "" { 450 continue // There's nothing to see here. Move along. 451 } 452 num := cr.StartCommitNum + i 453 for k, v := range knownTags { 454 // Releases are benchmarked first, because they are important (and there are few of them). 455 if cr.Hash[i] == v { 456 releaseNums = append(releaseNums, num) 457 if k == "go1" { 458 break loop // Point of no benchmark: test/bench/shootout: update timing.log to Go 1. 459 } 460 } 461 } 462 nums = append(nums, num) 463 } 464 } 465 todo.CommitNums = orderPerfTodo(nums) 466 todo.CommitNums = append(todo.CommitNums, releaseNums...) 467 if _, err = datastore.Put(c, todo.Key(c), todo); err != nil { 468 return fmt.Errorf("putting PerfTodo: %v", err) 469 } 470 return nil 471 } 472 return todo, datastore.RunInTransaction(c, tx, nil) 473 } 474 475 func removeCommitFromPerfTodo(c appengine.Context, builder string, num int) error { 476 todo := &PerfTodo{Builder: builder} 477 err := datastore.Get(c, todo.Key(c), todo) 478 if err != nil && err != datastore.ErrNoSuchEntity { 479 return fmt.Errorf("fetching PerfTodo: %v", err) 480 } 481 if err == datastore.ErrNoSuchEntity { 482 return nil 483 } 484 for i := len(todo.CommitNums) - 1; i >= 0; i-- { 485 if todo.CommitNums[i] == num { 486 for ; i < len(todo.CommitNums)-1; i++ { 487 todo.CommitNums[i] = todo.CommitNums[i+1] 488 } 489 todo.CommitNums = todo.CommitNums[:i] 490 _, err = datastore.Put(c, todo.Key(c), todo) 491 if err != nil { 492 return fmt.Errorf("putting PerfTodo: %v", err) 493 } 494 break 495 } 496 } 497 return nil 498 } 499 500 // packagesHandler returns a list of the non-Go Packages monitored 501 // by the dashboard. 502 func packagesHandler(r *http.Request) (interface{}, error) { 503 kind := r.FormValue("kind") 504 c := contextForRequest(r) 505 now := cache.Now(c) 506 key := "build-packages-" + kind 507 var p []*Package 508 if cache.Get(r, now, key, &p) { 509 return p, nil 510 } 511 p, err := Packages(c, kind) 512 if err != nil { 513 return nil, err 514 } 515 cache.Set(r, now, key, p) 516 return p, nil 517 } 518 519 // resultHandler records a build result. 520 // It reads a JSON-encoded Result value from the request body, 521 // creates a new Result entity, and updates the relevant Commit entity. 522 // If the Log field is not empty, resultHandler creates a new Log entity 523 // and updates the LogHash field before putting the Commit entity. 524 func resultHandler(r *http.Request) (interface{}, error) { 525 if r.Method != "POST" { 526 return nil, errBadMethod(r.Method) 527 } 528 529 // For now, the gccgo builders are using the old stuff. 530 // TODO(adg,cmang): remove this exception when gccgo is updated. 531 if dashboardForRequest(r) != gccgoDash { 532 v, _ := strconv.Atoi(r.FormValue("version")) 533 if v != builderVersion { 534 return nil, fmt.Errorf("rejecting POST from builder; need version %v", builderVersion) 535 } 536 } 537 538 c := contextForRequest(r) 539 res := new(Result) 540 defer r.Body.Close() 541 if err := json.NewDecoder(r.Body).Decode(res); err != nil { 542 return nil, fmt.Errorf("decoding Body: %v", err) 543 } 544 if err := res.Valid(); err != nil { 545 return nil, fmt.Errorf("validating Result: %v", err) 546 } 547 defer cache.Tick(c) 548 // store the Log text if supplied 549 if len(res.Log) > 0 { 550 hash, err := PutLog(c, res.Log) 551 if err != nil { 552 return nil, fmt.Errorf("putting Log: %v", err) 553 } 554 res.LogHash = hash 555 } 556 tx := func(c appengine.Context) error { 557 // check Package exists 558 if _, err := GetPackage(c, res.PackagePath); err != nil { 559 return fmt.Errorf("GetPackage: %v", err) 560 } 561 // put Result 562 if _, err := datastore.Put(c, res.Key(c), res); err != nil { 563 return fmt.Errorf("putting Result: %v", err) 564 } 565 // add Result to Commit 566 com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash} 567 if err := com.AddResult(c, res); err != nil { 568 return fmt.Errorf("AddResult: %v", err) 569 } 570 // Send build failure notifications, if necessary. 571 // Note this must run after the call AddResult, which 572 // populates the Commit's ResultData field. 573 return notifyOnFailure(c, com, res.Builder) 574 } 575 return nil, datastore.RunInTransaction(c, tx, nil) 576 } 577 578 // perf-result request payload 579 type PerfRequest struct { 580 Builder string 581 Benchmark string 582 Hash string 583 OK bool 584 Metrics []PerfMetric 585 Artifacts []PerfArtifact 586 } 587 588 type PerfMetric struct { 589 Type string 590 Val uint64 591 } 592 593 type PerfArtifact struct { 594 Type string 595 Body string 596 } 597 598 // perfResultHandler records a becnhmarking result. 599 func perfResultHandler(r *http.Request) (interface{}, error) { 600 defer r.Body.Close() 601 if r.Method != "POST" { 602 return nil, errBadMethod(r.Method) 603 } 604 605 req := new(PerfRequest) 606 if err := json.NewDecoder(r.Body).Decode(req); err != nil { 607 return nil, fmt.Errorf("decoding Body: %v", err) 608 } 609 610 c := contextForRequest(r) 611 defer cache.Tick(c) 612 613 // store the text files if supplied 614 for i, a := range req.Artifacts { 615 hash, err := PutLog(c, a.Body) 616 if err != nil { 617 return nil, fmt.Errorf("putting Log: %v", err) 618 } 619 req.Artifacts[i].Body = hash 620 } 621 tx := func(c appengine.Context) error { 622 return addPerfResult(c, r, req) 623 } 624 return nil, datastore.RunInTransaction(c, tx, nil) 625 } 626 627 // addPerfResult creates PerfResult and updates Commit, PerfTodo, 628 // PerfMetricRun and PerfConfig. 629 // MUST be called from inside a transaction. 630 func addPerfResult(c appengine.Context, r *http.Request, req *PerfRequest) error { 631 // check Package exists 632 p, err := GetPackage(c, "") 633 if err != nil { 634 return fmt.Errorf("GetPackage: %v", err) 635 } 636 // add result to Commit 637 com := &Commit{Hash: req.Hash} 638 if err := com.AddPerfResult(c, req.Builder, req.Benchmark); err != nil { 639 return fmt.Errorf("AddPerfResult: %v", err) 640 } 641 642 // add the result to PerfResult 643 res := &PerfResult{CommitHash: req.Hash} 644 if err := datastore.Get(c, res.Key(c), res); err != nil { 645 return fmt.Errorf("getting PerfResult: %v", err) 646 } 647 present := res.AddResult(req) 648 if _, err := datastore.Put(c, res.Key(c), res); err != nil { 649 return fmt.Errorf("putting PerfResult: %v", err) 650 } 651 652 // Meta-done denotes that there are no benchmarks left. 653 if req.Benchmark == "meta-done" { 654 // Don't send duplicate emails for the same commit/builder. 655 // And don't send emails about too old commits. 656 if !present && com.Num >= p.NextNum-commitsPerPage { 657 if err := checkPerfChanges(c, r, com, req.Builder, res); err != nil { 658 return err 659 } 660 } 661 if err := removeCommitFromPerfTodo(c, req.Builder, com.Num); err != nil { 662 return nil 663 } 664 return nil 665 } 666 667 // update PerfConfig 668 newBenchmark, err := UpdatePerfConfig(c, r, req) 669 if err != nil { 670 return fmt.Errorf("updating PerfConfig: %v", err) 671 } 672 if newBenchmark { 673 // If this is a new benchmark on the builder, delete PerfTodo. 674 // It will be recreated later with all commits again. 675 todo := &PerfTodo{Builder: req.Builder} 676 err = datastore.Delete(c, todo.Key(c)) 677 if err != nil && err != datastore.ErrNoSuchEntity { 678 return fmt.Errorf("deleting PerfTodo: %v", err) 679 } 680 } 681 682 // add perf metrics 683 for _, metric := range req.Metrics { 684 m, err := GetPerfMetricRun(c, req.Builder, req.Benchmark, metric.Type, com.Num) 685 if err != nil { 686 return fmt.Errorf("GetPerfMetrics: %v", err) 687 } 688 if err = m.AddMetric(c, com.Num, metric.Val); err != nil { 689 return fmt.Errorf("AddMetric: %v", err) 690 } 691 } 692 693 return nil 694 } 695 696 // MUST be called from inside a transaction. 697 func checkPerfChanges(c appengine.Context, r *http.Request, com *Commit, builder string, res *PerfResult) error { 698 pc, err := GetPerfConfig(c, r) 699 if err != nil { 700 return err 701 } 702 703 results := res.ParseData()[builder] 704 rcNewer := MakePerfResultCache(c, com, true) 705 rcOlder := MakePerfResultCache(c, com, false) 706 707 // Check whether we need to send failure notification email. 708 if results["meta-done"].OK { 709 // This one is successful, see if the next is failed. 710 nextRes, err := rcNewer.Next(com.Num) 711 if err != nil { 712 return err 713 } 714 if nextRes != nil && isPerfFailed(nextRes, builder) { 715 sendPerfFailMail(c, builder, nextRes) 716 } 717 } else { 718 // This one is failed, see if the previous is successful. 719 prevRes, err := rcOlder.Next(com.Num) 720 if err != nil { 721 return err 722 } 723 if prevRes != nil && !isPerfFailed(prevRes, builder) { 724 sendPerfFailMail(c, builder, res) 725 } 726 } 727 728 // Now see if there are any performance changes. 729 // Find the previous and the next results for performance comparison. 730 prevRes, err := rcOlder.NextForComparison(com.Num, builder) 731 if err != nil { 732 return err 733 } 734 nextRes, err := rcNewer.NextForComparison(com.Num, builder) 735 if err != nil { 736 return err 737 } 738 if results["meta-done"].OK { 739 // This one is successful, compare with a previous one. 740 if prevRes != nil { 741 if err := comparePerfResults(c, pc, builder, prevRes, res); err != nil { 742 return err 743 } 744 } 745 // Compare a next one with the current. 746 if nextRes != nil { 747 if err := comparePerfResults(c, pc, builder, res, nextRes); err != nil { 748 return err 749 } 750 } 751 } else { 752 // This one is failed, compare a previous one with a next one. 753 if prevRes != nil && nextRes != nil { 754 if err := comparePerfResults(c, pc, builder, prevRes, nextRes); err != nil { 755 return err 756 } 757 } 758 } 759 760 return nil 761 } 762 763 func comparePerfResults(c appengine.Context, pc *PerfConfig, builder string, prevRes, res *PerfResult) error { 764 changes := significantPerfChanges(pc, builder, prevRes, res) 765 if len(changes) == 0 { 766 return nil 767 } 768 com := &Commit{Hash: res.CommitHash} 769 if err := datastore.Get(c, com.Key(c), com); err != nil { 770 return fmt.Errorf("getting commit %v: %v", com.Hash, err) 771 } 772 sendPerfMailLater.Call(c, com, prevRes.CommitHash, builder, changes) // add task to queue 773 return nil 774 } 775 776 // logHandler displays log text for a given hash. 777 // It handles paths like "/log/hash". 778 func logHandler(w http.ResponseWriter, r *http.Request) { 779 w.Header().Set("Content-type", "text/plain; charset=utf-8") 780 c := contextForRequest(r) 781 hash := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] 782 key := datastore.NewKey(c, "Log", hash, 0, nil) 783 l := new(Log) 784 if err := datastore.Get(c, key, l); err != nil { 785 logErr(w, r, err) 786 return 787 } 788 b, err := l.Text() 789 if err != nil { 790 logErr(w, r, err) 791 return 792 } 793 w.Write(b) 794 } 795 796 type dashHandler func(*http.Request) (interface{}, error) 797 798 type dashResponse struct { 799 Response interface{} 800 Error string 801 } 802 803 // errBadMethod is returned by a dashHandler when 804 // the request has an unsuitable method. 805 type errBadMethod string 806 807 func (e errBadMethod) Error() string { 808 return "bad method: " + string(e) 809 } 810 811 // AuthHandler wraps a http.HandlerFunc with a handler that validates the 812 // supplied key and builder query parameters. 813 func AuthHandler(h dashHandler) http.HandlerFunc { 814 return func(w http.ResponseWriter, r *http.Request) { 815 c := contextForRequest(r) 816 817 // Put the URL Query values into r.Form to avoid parsing the 818 // request body when calling r.FormValue. 819 r.Form = r.URL.Query() 820 821 var err error 822 var resp interface{} 823 824 // Validate key query parameter for POST requests only. 825 key := r.FormValue("key") 826 builder := r.FormValue("builder") 827 if r.Method == "POST" && !validKey(c, key, builder) { 828 err = fmt.Errorf("invalid key %q for builder %q", key, builder) 829 } 830 831 // Call the original HandlerFunc and return the response. 832 if err == nil { 833 resp, err = h(r) 834 } 835 836 // Write JSON response. 837 dashResp := &dashResponse{Response: resp} 838 if err != nil { 839 c.Errorf("%v", err) 840 dashResp.Error = err.Error() 841 } 842 w.Header().Set("Content-Type", "application/json") 843 if err = json.NewEncoder(w).Encode(dashResp); err != nil { 844 c.Criticalf("encoding response: %v", err) 845 } 846 } 847 } 848 849 func keyHandler(w http.ResponseWriter, r *http.Request) { 850 builder := r.FormValue("builder") 851 if builder == "" { 852 logErr(w, r, errors.New("must supply builder in query string")) 853 return 854 } 855 c := contextForRequest(r) 856 fmt.Fprint(w, builderKey(c, builder)) 857 } 858 859 func init() { 860 // admin handlers 861 handleFunc("/init", initHandler) 862 handleFunc("/key", keyHandler) 863 864 // authenticated handlers 865 handleFunc("/commit", AuthHandler(commitHandler)) 866 handleFunc("/packages", AuthHandler(packagesHandler)) 867 handleFunc("/result", AuthHandler(resultHandler)) 868 handleFunc("/perf-result", AuthHandler(perfResultHandler)) 869 handleFunc("/tag", AuthHandler(tagHandler)) 870 handleFunc("/todo", AuthHandler(todoHandler)) 871 872 // public handlers 873 handleFunc("/log/", logHandler) 874 } 875 876 func validHash(hash string) bool { 877 // TODO(adg): correctly validate a hash 878 return hash != "" 879 } 880 881 func validKey(c appengine.Context, key, builder string) bool { 882 return isMasterKey(c, key) || key == builderKey(c, builder) 883 } 884 885 func isMasterKey(c appengine.Context, k string) bool { 886 return appengine.IsDevAppServer() || k == key.Secret(c) 887 } 888 889 func builderKey(c appengine.Context, builder string) string { 890 h := hmac.New(md5.New, []byte(key.Secret(c))) 891 h.Write([]byte(builder)) 892 return fmt.Sprintf("%x", h.Sum(nil)) 893 } 894 895 func logErr(w http.ResponseWriter, r *http.Request, err error) { 896 contextForRequest(r).Errorf("Error: %v", err) 897 w.WriteHeader(http.StatusInternalServerError) 898 fmt.Fprint(w, "Error: ", err) 899 } 900 901 func contextForRequest(r *http.Request) appengine.Context { 902 return dashboardForRequest(r).Context(appengine.NewContext(r)) 903 } 904 905 // limitStringLength essentially does return s[:max], 906 // but it ensures that we dot not split UTF-8 rune in half. 907 // Otherwise appengine python scripts will break badly. 908 func limitStringLength(s string, max int) string { 909 if len(s) <= max { 910 return s 911 } 912 for { 913 s = s[:max] 914 r, size := utf8.DecodeLastRuneInString(s) 915 if r != utf8.RuneError || size != 1 { 916 return s 917 } 918 max-- 919 } 920 }