package server import ( "encoding/json" "errors" "fmt" "io/ioutil" "log" "net/http" "os" "runtime/pprof" "strings" "sync" "github.com/elazarl/go-bindata-assetfs" "golang.org/x/net/websocket" "mcquay.me/idg" ) // JsonHandler is a function type that allows setting the Content-Type // appropriately for views destined to serve JSON type JsonHandler func(http.ResponseWriter, *http.Request) // ServeHTTP is JsonHandler's http.Handler implementation. func (h JsonHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") h(w, req) } // Controller is the shepherd of a collection of games. The main package in // server simply populates one of these and starts an http server. type Controller struct { Idg *idg.Generator Conf Config Games MapLock Memprofile string Profile string } var prefix map[string]string // NewController takes a populated Config, and some parameters to determine // what sorts of profiling to deal with and returns a freshly populated // Controller. func NewController(conf Config, mprof, pprof, staticFiles string) *http.ServeMux { c := &Controller{ Idg: idg.NewGenerator(), Conf: conf, Games: MapLock{ M: make(map[string]*Game), }, Memprofile: mprof, Profile: pprof, } go c.Run() prefix = map[string]string{ "ui": "/ui/", "websocket": "/ws/", "list": "/api/v0/game/list/", "start": "/api/v0/game/start/", "stats": "/api/v0/game/stats/", "stop": "/api/v0/game/stop/", "bandwidth": "/api/v0/game/bw/", "fsu": "/api/v0/fsu/", "info": "/api/v0/info/", "metrics": "/metrics", } sm := http.NewServeMux() sm.HandleFunc( "/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, prefix["ui"], http.StatusMovedPermanently) }, ) if staticFiles == "" { sm.Handle( prefix["ui"], http.FileServer( &assetfs.AssetFS{ Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, }, ), ) } else { sm.Handle( prefix["ui"], http.StripPrefix( prefix["ui"], http.FileServer(http.Dir(staticFiles)), ), ) } sm.Handle(prefix["websocket"], websocket.Handler(c.AddPlayer)) sm.Handle(prefix["list"], JsonHandler(c.ListGames)) sm.Handle(prefix["start"], JsonHandler(c.StartGame)) sm.Handle(prefix["stats"], JsonHandler(c.GameStats)) sm.Handle(prefix["stop"], JsonHandler(c.StopGame)) sm.Handle(prefix["bandwidth"], JsonHandler(c.BW)) sm.HandleFunc(prefix["fsu"], c.KillServer) sm.HandleFunc(prefix["info"], c.Info) sm.HandleFunc(prefix["metrics"], func(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "up 1\n") }) return sm } // TODO Eventually this thing will have a select loop for dealing with game // access in a more lock-free manner? func (c *Controller) Run() { c.Idg.Run() } // StartGame is the http route responsible for responding to requests to start // games under this controller. Creates a default Config object, and populates // it according to data POSTed. func (c *Controller) StartGame(w http.ResponseWriter, req *http.Request) { log.Println("asked to create a game") requested_game_name := c.Idg.Hash() width, height := float64(c.Conf.Width), float64(c.Conf.Height) obstacleCount := 0 obstacles := []Obstacle{} maxPoints := c.Conf.MaxPoints mode := "deathmatch" // here we determine if we are going to run with defaults or pick them off // a posted json blob if req.Method == "POST" { body, err := ioutil.ReadAll(req.Body) if err != nil { log.Printf("unable to read request body: %v", err) } req.Body.Close() cfg := struct { Name string `json:"name"` Config }{} err = json.Unmarshal(body, &cfg) if err != nil { if err := json.NewEncoder(w).Encode(NewFailure(err.Error())); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } requested_game_name = cfg.Name width = float64(cfg.Width) height = float64(cfg.Height) obstacleCount = cfg.ObstacleCount obstacles = cfg.Obstacles maxPoints = cfg.MaxPoints mode = cfg.Mode } g := c.Games.Get(requested_game_name) if g == nil { log.Printf("Game '%s' non-existant; making it now", requested_game_name) var err error g, err = NewGame( requested_game_name, width, height, c.Conf.Tick, maxPoints, mode, ) g.obstacleCount = obstacleCount g.obstacles = obstacles g.defaultObstacles = obstacles if len(g.defaultObstacles) == 0 { g.obstacles = GenerateObstacles( g.obstacleCount, g.width, g.height, ) } else { g.obstacles = c.Conf.Obstacles } if err != nil { log.Printf("problem creating game: %s: %s", requested_game_name, err) b, _ := json.Marshal(NewFailure("game creation failure")) http.Error(w, string(b), http.StatusConflict) return } go g.run() c.Games.Add(g) } else { log.Printf("Game '%s' already exists: %p", requested_game_name, g) b, _ := json.Marshal(NewFailure("game already exists")) http.Error(w, string(b), http.StatusConflict) return } game_json := struct { Id string `json:"id"` }{ Id: g.id, } if err := json.NewEncoder(w).Encode(game_json); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // ListGames makes a reasonable JSON response based on the games currently // being run. func (c *Controller) ListGames(w http.ResponseWriter, req *http.Request) { log.Println("games list requested") c.Games.RLock() defer c.Games.RUnlock() type pout struct { Name string `json:"name"` Id string `json:"id"` } type gl struct { Id string `json:"id"` Players []pout `json:"players"` } ids := make([]gl, 0) for id, g := range c.Games.M { players := make([]pout, 0) // TODO - players instead of robots? for p := range g.players { for _, r := range p.Robots { players = append(players, pout{ Name: r.Name, Id: r.Id, }) } } ids = append(ids, gl{ Id: id, Players: players, }) } if err := json.NewEncoder(w).Encode(ids); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // GameStats provides an control mechanism to query for the stats of a single // game func (c *Controller) GameStats(w http.ResponseWriter, req *http.Request) { // TODO: wrap this up in something similar to the JsonHandler to verify the // url? Look at gorilla routing? key, err := c.getGameId(req.URL.Path) if err != nil { b, _ := json.Marshal(NewFailure(err.Error())) http.Error(w, string(b), http.StatusBadRequest) return } log.Printf("requested stats for game: %s", key) c.Games.RLock() g, ok := c.Games.M[key] c.Games.RUnlock() if !ok { b, _ := json.Marshal(NewFailure("game not found")) http.Error(w, string(b), http.StatusNotFound) return } g.stats.RLock() defer g.stats.RUnlock() if err := json.NewEncoder(w).Encode(g.stats.PlayerStats); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // BW provides a route to query for current bandwidth utilization for a single // game. func (c *Controller) BW(w http.ResponseWriter, req *http.Request) { // TODO: wrap this up in something similar to the JsonHandler to verify the // url? Look at gorilla routing? key, err := c.getGameId(req.URL.Path) if err != nil { b, _ := json.Marshal(NewFailure(err.Error())) http.Error(w, string(b), http.StatusBadRequest) return } log.Printf("requested bandwidth for game: %s", key) c.Games.RLock() g, ok := c.Games.M[key] c.Games.RUnlock() if !ok { b, _ := json.Marshal(NewFailure("game not found")) http.Error(w, string(b), http.StatusNotFound) return } s := map[string][]float64{ "tx": <-g.bw.Tx, "rx": <-g.bw.Rx, } if err := json.NewEncoder(w).Encode(s); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // StopGame is the only mechanism to decrease the number of running games in // a Controller func (c *Controller) StopGame(w http.ResponseWriter, req *http.Request) { key, err := c.getGameId(req.URL.Path) if err != nil { b, _ := json.Marshal(NewFailure(err.Error())) http.Error(w, string(b), http.StatusBadRequest) return } c.Games.Lock() g, ok := c.Games.M[key] defer c.Games.Unlock() if !ok { http.NotFound(w, req) return } g.kill <- true delete(c.Games.M, key) message := struct { Ok bool `json:"ok"` Message string `json:"message"` }{ Ok: true, Message: fmt.Sprintf("Successfully stopped game: %s", key), } if err := json.NewEncoder(w).Encode(message); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } log.Printf("returning from StopGame") } // KillServer is my favorite method of all the methods in server: it shuts // things down respecting profiling requests. func (c *Controller) KillServer(w http.ResponseWriter, req *http.Request) { if c.Profile != "" { log.Print("trying to stop cpu profile") pprof.StopCPUProfile() log.Print("stopped cpu profile") } if c.Memprofile != "" { log.Print("trying to dump memory profile") f, err := os.Create(c.Memprofile) if err != nil { log.Fatal(err) } pprof.WriteHeapProfile(f) f.Close() log.Print("stopped memory profile dump") } log.Fatal("shit got fucked up") } // getGameId trims the gameid off of the url. This is hokey, and makes me miss // django regex-specified routes. func (c *Controller) getGameId(path string) (string, error) { var err error trimmed := strings.Trim(path, "/") fullPath := strings.Split(trimmed, "/") log.Printf("%+v: %d", fullPath, len(fullPath)) if len(fullPath) != 5 { return "", errors.New("improperly formed url") } key := fullPath[len(fullPath)-1] return key, err } // MapLock is simply a map and a RWMutex // TODO: obviate the need for this in Controller.Run type MapLock struct { M map[string]*Game sync.RWMutex } // get is a function that returns a game if found, and creates one if // not found and force is true. In order to get a hash (rather than use // the string you pass) send "" for id. func (ml *MapLock) Get(id string) *Game { ml.Lock() g, _ := ml.M[id] ml.Unlock() return g } // add is used to insert a new game into this MapLock func (ml *MapLock) Add(g *Game) { ml.Lock() ml.M[g.id] = g ml.Unlock() }