| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // HTTP server for saving, loading, and executing playground examples. |
| |
| // TODO(nlacasse,ivanpi): The word "compile" is no longer appropriate for what |
| // this server does. Rename to something better. |
| |
| package main |
| |
| import ( |
| "bytes" |
| crand "crypto/rand" |
| "encoding/binary" |
| "flag" |
| "fmt" |
| "io" |
| "math/rand" |
| "net/http" |
| "os" |
| "os/signal" |
| "runtime" |
| "syscall" |
| "time" |
| |
| "v.io/x/lib/dbutil" |
| "v.io/x/playground/compilerd/storage" |
| "v.io/x/playground/lib/log" |
| ) |
| |
| func init() { |
| if os.Getenv("GOMAXPROCS") == "" { |
| runtime.GOMAXPROCS(runtime.NumCPU()) |
| } |
| } |
| |
| var ( |
| // This channel is closed when clean exit is triggered. |
| // No values are ever sent to it. |
| lameduck chan bool = make(chan bool) |
| |
| address = flag.String("address", ":8181", "Address to listen on.") |
| |
| // compilerd exits cleanly on SIGTERM or after a random amount of time, |
| // between listenTimeout/2 and listenTimeout. |
| listenTimeout = flag.Duration("listen-timeout", 60*time.Minute, "Maximum amount of time to listen for before exiting. A value of 0 disables the timeout.") |
| |
| // Maximum request and output size. |
| // 1<<16 is the same limit as imposed by Go tour. |
| // Note: The response includes error and status messages as well as output, |
| // so it can be larger (usually by a small constant, hard limited to |
| // 2*maxSize). |
| // maxSize should be large enough to fit all error and status messages |
| // written by compilerd to prevent reaching the hard limit. |
| maxSize = flag.Int("max-size", 1<<16, "Maximum request and output size.") |
| |
| // Maximum time to finish serving currently running requests before exiting |
| // cleanly. No new requests are accepted during this time. |
| exitDelay = 30 * time.Second |
| |
| // Path to SQL configuration file, as described in v.io/x/lib/dbutil/mysql.go. |
| sqlConf = flag.String("sqlconf", "", "Path to SQL configuration file. If empty, load and save requests are disabled. "+dbutil.SqlConfigFileDescription) |
| ) |
| |
| // Seeds the non-secure random number generator. |
| func seedRNG() error { |
| var seed int64 |
| err := binary.Read(crand.Reader, binary.LittleEndian, &seed) |
| if err != nil { |
| return fmt.Errorf("reseed failed: %v", err) |
| } |
| rand.Seed(seed) |
| return nil |
| } |
| |
| func main() { |
| log.InitSyslogLoggers() |
| |
| log.Debugf("Compilerd starting.\n") |
| flag.Parse() |
| |
| if err := seedRNG(); err != nil { |
| log.Panic(err) |
| } |
| |
| c := newCompiler() |
| |
| listenForNs := listenTimeout.Nanoseconds() |
| if listenForNs > 0 { |
| delayNs := listenForNs/2 + rand.Int63n(listenForNs/2) |
| |
| // VMs will be periodically killed to prevent any owned VMs from causing |
| // damage. We want to exit cleanly before then so we don't cause requests |
| // to fail. When compilerd exits, a watchdog will shut the machine down |
| // after a short delay. |
| go waitForExit(c, time.Nanosecond*time.Duration(delayNs)) |
| } |
| |
| serveMux := http.NewServeMux() |
| |
| if *sqlConf != "" { |
| log.Debugf("Using sql config %v\n", *sqlConf) |
| |
| // Parse SQL configuration file and set up TLS. |
| dbConfig, err := dbutil.ActivateSqlConfigFromFile(*sqlConf) |
| if err != nil { |
| log.Panic(err) |
| } |
| |
| // Connect to storage backend. |
| if err := storage.Connect(dbConfig); err != nil { |
| log.Panic(err) |
| } |
| |
| // Add routes for storage. |
| serveMux.HandleFunc("/load", handlerLoad) |
| serveMux.HandleFunc("/save", handlerSave) |
| } else { |
| log.Debugln("No sql config provided. Disabling /load and /save routes.") |
| |
| // Return 501 Not Implemented for the /load and /save routes. |
| serveMux.HandleFunc("/load", handlerNotImplemented) |
| serveMux.HandleFunc("/save", handlerNotImplemented) |
| } |
| |
| serveMux.HandleFunc("/compile", c.handlerCompile) |
| serveMux.HandleFunc("/healthz", handlerHealthz) |
| |
| log.Debugf("Serving %s\n", *address) |
| s := http.Server{ |
| Addr: *address, |
| Handler: serveMux, |
| ErrorLog: log.ErrorLogger, |
| } |
| |
| if err := s.ListenAndServe(); err != nil { |
| log.Panic(err) |
| } |
| } |
| |
| func waitForExit(c *compiler, limit time.Duration) { |
| // Exit if we get a SIGTERM. |
| term := make(chan os.Signal, 1) |
| signal.Notify(term, syscall.SIGTERM) |
| |
| // Or if the time limit expires. |
| deadline := time.After(limit) |
| log.Debugln("Exiting at", time.Now().Add(limit)) |
| Loop: |
| for { |
| select { |
| case <-deadline: |
| log.Debugln("Deadline expired, exiting in at most", exitDelay) |
| break Loop |
| case <-term: |
| log.Debugln("Got SIGTERM, exiting in at most", exitDelay) |
| break Loop |
| } |
| } |
| |
| // Fail health checks so we stop getting requests. |
| close(lameduck) |
| |
| go func() { |
| select { |
| case <-time.After(exitDelay): |
| log.Warnf("Dispatcher did not stop in %v, exiting.", exitDelay) |
| os.Exit(1) |
| } |
| }() |
| |
| // Stop the compiler and wait for all in-progress jobs to finish. |
| c.stop() |
| |
| // Give the server some extra time to send any remaning responses that are |
| // queued to be sent. |
| time.Sleep(2 * time.Second) |
| |
| // Close database connections. |
| if *sqlConf != "" { |
| if err := storage.Close(); err != nil { |
| log.Errorf("storage.Close() failed: %v", err) |
| } |
| } |
| |
| os.Exit(0) |
| } |
| |
| ////////////////////////////////////////// |
| // HTTP request helpers |
| |
| // Handles CORS options and pre-flight requests. |
| // Returns false iff response processing should not continue. |
| func handleCORS(w http.ResponseWriter, r *http.Request) bool { |
| // CORS headers. |
| // TODO(nlacasse): Fill the origin header in with actual playground origin |
| // before going to production. |
| w.Header().Set("Access-Control-Allow-Origin", "*") |
| w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") |
| w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding") |
| |
| // CORS sends an OPTIONS pre-flight request to make sure the request will be |
| // allowed. |
| if r.Method == "OPTIONS" { |
| w.WriteHeader(http.StatusOK) |
| return false |
| } |
| |
| return true |
| } |
| |
| // Checks if the GET method was used. |
| // Returns false iff response processing should not continue. |
| func checkGetMethod(w http.ResponseWriter, r *http.Request) bool { |
| if r.Method != "GET" { |
| w.WriteHeader(http.StatusBadRequest) |
| return false |
| } |
| |
| return true |
| } |
| |
| // Checks if the POST method was used and returns the first limit bytes of the |
| // request body. |
| // Returns nil iff response processing should not continue. |
| func getPostBody(w http.ResponseWriter, r *http.Request, limit int) []byte { |
| if r.Body == nil || r.Method != "POST" { |
| w.WriteHeader(http.StatusBadRequest) |
| return nil |
| } |
| |
| buf := new(bytes.Buffer) |
| buf.ReadFrom(io.LimitReader(r.Body, int64(limit))) |
| return buf.Bytes() |
| } |
| |
| func handlerHealthz(w http.ResponseWriter, r *http.Request) { |
| select { |
| case <-lameduck: |
| w.WriteHeader(http.StatusInternalServerError) |
| default: |
| w.Write([]byte("ok")) |
| } |
| } |
| |
| func handlerNotImplemented(w http.ResponseWriter, r *http.Request) { |
| if !handleCORS(w, r) { |
| return |
| } |
| |
| w.WriteHeader(http.StatusNotImplemented) |
| } |