Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

397 linhas
10KB

  1. package server
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "io/ioutil"
  7. "log"
  8. "net/http"
  9. "os"
  10. "runtime/pprof"
  11. "strings"
  12. "sync"
  13. "github.com/elazarl/go-bindata-assetfs"
  14. "golang.org/x/net/websocket"
  15. "mcquay.me/idg"
  16. )
  17. // JsonHandler is a function type that allows setting the Content-Type
  18. // appropriately for views destined to serve JSON
  19. type JsonHandler func(http.ResponseWriter, *http.Request)
  20. // ServeHTTP is JsonHandler's http.Handler implementation.
  21. func (h JsonHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  22. w.Header().Set("Content-Type", "application/json")
  23. h(w, req)
  24. }
  25. // Controller is the shepherd of a collection of games. The main package in
  26. // server simply populates one of these and starts an http server.
  27. type Controller struct {
  28. Idg *idg.Generator
  29. Conf Config
  30. Games MapLock
  31. Memprofile string
  32. Profile string
  33. }
  34. var prefix map[string]string
  35. // NewController takes a populated Config, and some parameters to determine
  36. // what sorts of profiling to deal with and returns a freshly populated
  37. // Controller.
  38. func NewController(conf Config, mprof, pprof, staticFiles string) *http.ServeMux {
  39. c := &Controller{
  40. Idg: idg.NewGenerator(),
  41. Conf: conf,
  42. Games: MapLock{
  43. M: make(map[string]*Game),
  44. },
  45. Memprofile: mprof,
  46. Profile: pprof,
  47. }
  48. go c.Run()
  49. prefix = map[string]string{
  50. "ui": "/ui/",
  51. "websocket": "/ws/",
  52. "list": "/api/v0/game/list/",
  53. "start": "/api/v0/game/start/",
  54. "stats": "/api/v0/game/stats/",
  55. "stop": "/api/v0/game/stop/",
  56. "bandwidth": "/api/v0/game/bw/",
  57. "fsu": "/api/v0/fsu/",
  58. "info": "/api/v0/info/",
  59. "metrics": "/metrics",
  60. }
  61. sm := http.NewServeMux()
  62. sm.HandleFunc(
  63. "/",
  64. func(w http.ResponseWriter, r *http.Request) {
  65. http.Redirect(w, r, prefix["ui"], http.StatusMovedPermanently)
  66. },
  67. )
  68. if staticFiles == "" {
  69. sm.Handle(
  70. prefix["ui"],
  71. http.FileServer(
  72. &assetfs.AssetFS{
  73. Asset: Asset,
  74. AssetDir: AssetDir,
  75. AssetInfo: AssetInfo,
  76. },
  77. ),
  78. )
  79. } else {
  80. sm.Handle(
  81. prefix["ui"],
  82. http.StripPrefix(
  83. prefix["ui"],
  84. http.FileServer(http.Dir(staticFiles)),
  85. ),
  86. )
  87. }
  88. sm.Handle(prefix["websocket"], websocket.Handler(c.AddPlayer))
  89. sm.Handle(prefix["list"], JsonHandler(c.ListGames))
  90. sm.Handle(prefix["start"], JsonHandler(c.StartGame))
  91. sm.Handle(prefix["stats"], JsonHandler(c.GameStats))
  92. sm.Handle(prefix["stop"], JsonHandler(c.StopGame))
  93. sm.Handle(prefix["bandwidth"], JsonHandler(c.BW))
  94. sm.HandleFunc(prefix["fsu"], c.KillServer)
  95. sm.HandleFunc(prefix["info"], c.Info)
  96. sm.HandleFunc(prefix["metrics"], func(w http.ResponseWriter, req *http.Request) {
  97. fmt.Fprintf(w, "up 1\n")
  98. })
  99. return sm
  100. }
  101. // TODO Eventually this thing will have a select loop for dealing with game
  102. // access in a more lock-free manner?
  103. func (c *Controller) Run() {
  104. c.Idg.Run()
  105. }
  106. // StartGame is the http route responsible for responding to requests to start
  107. // games under this controller. Creates a default Config object, and populates
  108. // it according to data POSTed.
  109. func (c *Controller) StartGame(w http.ResponseWriter, req *http.Request) {
  110. log.Println("asked to create a game")
  111. requested_game_name := c.Idg.Hash()
  112. width, height := float64(c.Conf.Width), float64(c.Conf.Height)
  113. obstacleCount := 0
  114. obstacles := []Obstacle{}
  115. maxPoints := c.Conf.MaxPoints
  116. mode := "deathmatch"
  117. // here we determine if we are going to run with defaults or pick them off
  118. // a posted json blob
  119. if req.Method == "POST" {
  120. body, err := ioutil.ReadAll(req.Body)
  121. if err != nil {
  122. log.Printf("unable to read request body: %v", err)
  123. }
  124. req.Body.Close()
  125. cfg := struct {
  126. Name string `json:"name"`
  127. Config
  128. }{}
  129. err = json.Unmarshal(body, &cfg)
  130. if err != nil {
  131. if err := json.NewEncoder(w).Encode(NewFailure(err.Error())); err != nil {
  132. http.Error(w, err.Error(), http.StatusInternalServerError)
  133. }
  134. return
  135. }
  136. requested_game_name = cfg.Name
  137. width = float64(cfg.Width)
  138. height = float64(cfg.Height)
  139. obstacleCount = cfg.ObstacleCount
  140. obstacles = cfg.Obstacles
  141. maxPoints = cfg.MaxPoints
  142. mode = cfg.Mode
  143. }
  144. g := c.Games.Get(requested_game_name)
  145. if g == nil {
  146. log.Printf("Game '%s' non-existant; making it now", requested_game_name)
  147. var err error
  148. g, err = NewGame(
  149. requested_game_name,
  150. width,
  151. height,
  152. c.Conf.Tick,
  153. maxPoints,
  154. mode,
  155. )
  156. g.obstacleCount = obstacleCount
  157. g.obstacles = obstacles
  158. g.defaultObstacles = obstacles
  159. if len(g.defaultObstacles) == 0 {
  160. g.obstacles = GenerateObstacles(
  161. g.obstacleCount,
  162. g.width,
  163. g.height,
  164. )
  165. } else {
  166. g.obstacles = c.Conf.Obstacles
  167. }
  168. if err != nil {
  169. log.Printf("problem creating game: %s: %s", requested_game_name, err)
  170. b, _ := json.Marshal(NewFailure("game creation failure"))
  171. http.Error(w, string(b), http.StatusConflict)
  172. return
  173. }
  174. go g.run()
  175. c.Games.Add(g)
  176. } else {
  177. log.Printf("Game '%s' already exists: %p", requested_game_name, g)
  178. b, _ := json.Marshal(NewFailure("game already exists"))
  179. http.Error(w, string(b), http.StatusConflict)
  180. return
  181. }
  182. game_json := struct {
  183. Id string `json:"id"`
  184. }{
  185. Id: g.id,
  186. }
  187. if err := json.NewEncoder(w).Encode(game_json); err != nil {
  188. http.Error(w, err.Error(), http.StatusInternalServerError)
  189. }
  190. }
  191. // ListGames makes a reasonable JSON response based on the games currently
  192. // being run.
  193. func (c *Controller) ListGames(w http.ResponseWriter, req *http.Request) {
  194. log.Println("games list requested")
  195. c.Games.RLock()
  196. defer c.Games.RUnlock()
  197. type pout struct {
  198. Name string `json:"name"`
  199. Id string `json:"id"`
  200. }
  201. type gl struct {
  202. Id string `json:"id"`
  203. Players []pout `json:"players"`
  204. }
  205. ids := make([]gl, 0)
  206. for id, g := range c.Games.M {
  207. players := make([]pout, 0)
  208. // TODO - players instead of robots?
  209. for p := range g.players {
  210. for _, r := range p.Robots {
  211. players = append(players, pout{
  212. Name: r.Name,
  213. Id: r.Id,
  214. })
  215. }
  216. }
  217. ids = append(ids, gl{
  218. Id: id,
  219. Players: players,
  220. })
  221. }
  222. if err := json.NewEncoder(w).Encode(ids); err != nil {
  223. http.Error(w, err.Error(), http.StatusInternalServerError)
  224. }
  225. }
  226. // GameStats provides an control mechanism to query for the stats of a single
  227. // game
  228. func (c *Controller) GameStats(w http.ResponseWriter, req *http.Request) {
  229. // TODO: wrap this up in something similar to the JsonHandler to verify the
  230. // url? Look at gorilla routing?
  231. key, err := c.getGameId(req.URL.Path)
  232. if err != nil {
  233. b, _ := json.Marshal(NewFailure(err.Error()))
  234. http.Error(w, string(b), http.StatusBadRequest)
  235. return
  236. }
  237. log.Printf("requested stats for game: %s", key)
  238. c.Games.RLock()
  239. g, ok := c.Games.M[key]
  240. c.Games.RUnlock()
  241. if !ok {
  242. b, _ := json.Marshal(NewFailure("game not found"))
  243. http.Error(w, string(b), http.StatusNotFound)
  244. return
  245. }
  246. g.stats.RLock()
  247. defer g.stats.RUnlock()
  248. if err := json.NewEncoder(w).Encode(g.stats.PlayerStats); err != nil {
  249. http.Error(w, err.Error(), http.StatusInternalServerError)
  250. }
  251. }
  252. // BW provides a route to query for current bandwidth utilization for a single
  253. // game.
  254. func (c *Controller) BW(w http.ResponseWriter, req *http.Request) {
  255. // TODO: wrap this up in something similar to the JsonHandler to verify the
  256. // url? Look at gorilla routing?
  257. key, err := c.getGameId(req.URL.Path)
  258. if err != nil {
  259. b, _ := json.Marshal(NewFailure(err.Error()))
  260. http.Error(w, string(b), http.StatusBadRequest)
  261. return
  262. }
  263. log.Printf("requested bandwidth for game: %s", key)
  264. c.Games.RLock()
  265. g, ok := c.Games.M[key]
  266. c.Games.RUnlock()
  267. if !ok {
  268. b, _ := json.Marshal(NewFailure("game not found"))
  269. http.Error(w, string(b), http.StatusNotFound)
  270. return
  271. }
  272. s := map[string][]float64{
  273. "tx": <-g.bw.Tx,
  274. "rx": <-g.bw.Rx,
  275. }
  276. if err := json.NewEncoder(w).Encode(s); err != nil {
  277. http.Error(w, err.Error(), http.StatusInternalServerError)
  278. }
  279. }
  280. // StopGame is the only mechanism to decrease the number of running games in
  281. // a Controller
  282. func (c *Controller) StopGame(w http.ResponseWriter, req *http.Request) {
  283. key, err := c.getGameId(req.URL.Path)
  284. if err != nil {
  285. b, _ := json.Marshal(NewFailure(err.Error()))
  286. http.Error(w, string(b), http.StatusBadRequest)
  287. return
  288. }
  289. c.Games.Lock()
  290. g, ok := c.Games.M[key]
  291. defer c.Games.Unlock()
  292. if !ok {
  293. http.NotFound(w, req)
  294. return
  295. }
  296. g.kill <- true
  297. delete(c.Games.M, key)
  298. message := struct {
  299. Ok bool `json:"ok"`
  300. Message string `json:"message"`
  301. }{
  302. Ok: true,
  303. Message: fmt.Sprintf("Successfully stopped game: %s", key),
  304. }
  305. if err := json.NewEncoder(w).Encode(message); err != nil {
  306. http.Error(w, err.Error(), http.StatusInternalServerError)
  307. }
  308. log.Printf("returning from StopGame")
  309. }
  310. // KillServer is my favorite method of all the methods in server: it shuts
  311. // things down respecting profiling requests.
  312. func (c *Controller) KillServer(w http.ResponseWriter, req *http.Request) {
  313. if c.Profile != "" {
  314. log.Print("trying to stop cpu profile")
  315. pprof.StopCPUProfile()
  316. log.Print("stopped cpu profile")
  317. }
  318. if c.Memprofile != "" {
  319. log.Print("trying to dump memory profile")
  320. f, err := os.Create(c.Memprofile)
  321. if err != nil {
  322. log.Fatal(err)
  323. }
  324. pprof.WriteHeapProfile(f)
  325. f.Close()
  326. log.Print("stopped memory profile dump")
  327. }
  328. log.Fatal("shit got fucked up")
  329. }
  330. // getGameId trims the gameid off of the url. This is hokey, and makes me miss
  331. // django regex-specified routes.
  332. func (c *Controller) getGameId(path string) (string, error) {
  333. var err error
  334. trimmed := strings.Trim(path, "/")
  335. fullPath := strings.Split(trimmed, "/")
  336. log.Printf("%+v: %d", fullPath, len(fullPath))
  337. if len(fullPath) != 5 {
  338. return "", errors.New("improperly formed url")
  339. }
  340. key := fullPath[len(fullPath)-1]
  341. return key, err
  342. }
  343. // MapLock is simply a map and a RWMutex
  344. // TODO: obviate the need for this in Controller.Run
  345. type MapLock struct {
  346. M map[string]*Game
  347. sync.RWMutex
  348. }
  349. // get is a function that returns a game if found, and creates one if
  350. // not found and force is true. In order to get a hash (rather than use
  351. // the string you pass) send "" for id.
  352. func (ml *MapLock) Get(id string) *Game {
  353. ml.Lock()
  354. g, _ := ml.M[id]
  355. ml.Unlock()
  356. return g
  357. }
  358. // add is used to insert a new game into this MapLock
  359. func (ml *MapLock) Add(g *Game) {
  360. ml.Lock()
  361. ml.M[g.id] = g
  362. ml.Unlock()
  363. }