You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
346 lines
9.0 KiB
346 lines
9.0 KiB
package server |
|
|
|
import ( |
|
"encoding/json" |
|
"errors" |
|
"fmt" |
|
"io/ioutil" |
|
"log" |
|
"net/http" |
|
"os" |
|
"runtime/pprof" |
|
"strings" |
|
"sync" |
|
|
|
"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 |
|
} |
|
|
|
// 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 string) *http.ServeMux { |
|
c := &Controller{ |
|
Idg: idg.NewGenerator(), |
|
Conf: conf, |
|
Games: MapLock{ |
|
M: make(map[string]*Game), |
|
}, |
|
Memprofile: mprof, |
|
Profile: pprof, |
|
} |
|
|
|
go c.Run() |
|
|
|
sm := http.NewServeMux() |
|
sm.Handle("/", JsonHandler(c.Info)) |
|
sm.Handle("/ws/", websocket.Handler(c.AddPlayer)) |
|
sm.Handle("/api/v0/game/start/", JsonHandler(c.StartGame)) |
|
sm.Handle("/api/v0/game/list/", JsonHandler(c.ListGames)) |
|
sm.Handle("/api/v0/game/stats/", JsonHandler(c.GameStats)) |
|
sm.Handle("/api/v0/game/bw/", JsonHandler(c.BW)) |
|
sm.Handle("/api/v0/game/stop/", JsonHandler(c.StopGame)) |
|
sm.HandleFunc("/api/v0/fsu/", c.KillServer) |
|
sm.HandleFunc("/api/v0/info/", c.Info) |
|
|
|
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, "/") |
|
if len(fullPath) != 3 { |
|
return "", errors.New("improperly formed url") |
|
} |
|
key := fullPath[2] |
|
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() |
|
}
|
|
|