DEV 1404: Support streaming CLI games to the browser board (#88)

* add minimal support for serving a game to the board UI

* refactor into new board package

* support reporting errors and author names to board

* support passing an alternate board URL

* avoid using IPv6 for local URL

* use rules.Point instead of a custom Point type for board package

* use zero for generic communication error code in cli

* rename createGameEvent to buildFrameEvent

* tests for conversion from boardState/snakeState to game frame
This commit is contained in:
Rob O'Dwyer 2022-06-28 16:17:44 -07:00 committed by GitHub
parent f6c3ed0791
commit a451cda9c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 504 additions and 22 deletions

130
board/server.go Normal file
View file

@ -0,0 +1,130 @@
package board
import (
"context"
"encoding/json"
"log"
"net"
"net/http"
"github.com/gorilla/websocket"
"github.com/rs/cors"
)
// A minimal server capable of handling the requests from a single browser client running the board viewer.
type BoardServer struct {
game Game
events chan GameEvent // channel for sending events from the game runner to the browser client
done chan bool // channel for signalling (via closing) that all events have been sent to the browser client
httpServer *http.Server
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func NewBoardServer(game Game) *BoardServer {
mux := http.NewServeMux()
server := &BoardServer{
game: game,
events: make(chan GameEvent, 1000), // buffered channel to allow game to run ahead of browser client
done: make(chan bool),
httpServer: &http.Server{
Handler: cors.Default().Handler(mux),
},
}
mux.HandleFunc("/games/"+game.ID, server.handleGame)
mux.HandleFunc("/games/"+game.ID+"/events", server.handleWebsocket)
return server
}
// Handle the /games/:id request made by the board to fetch the game metadata.
func (server *BoardServer) handleGame(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(struct {
Game Game
}{server.game})
if err != nil {
log.Printf("Unable to serialize game: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
// Handle the /games/:id/events websocket request made by the board to receive game events.
func (server *BoardServer) handleWebsocket(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Unable to upgrade connection: %v", err)
return
}
defer func() {
err = ws.Close()
if err != nil {
log.Printf("Unable to close websocket stream")
}
}()
for event := range server.events {
jsonStr, err := json.Marshal(event)
if err != nil {
log.Printf("Unable to serialize event for websocket: %v", err)
}
err = ws.WriteMessage(websocket.TextMessage, jsonStr)
if err != nil {
log.Printf("Unable to write to websocket: %v", err)
break
}
}
log.Printf("Finished writing all game events, signalling game server to stop")
close(server.done)
log.Printf("Sending websocket close message")
err = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
log.Printf("Problem closing websocket: %v", err)
}
}
func (server *BoardServer) Listen() (string, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", err
}
go func() {
err = server.httpServer.Serve(listener)
if err != http.ErrServerClosed {
log.Printf("Error in board HTTP server: %v", err)
}
}()
url := "http://" + listener.Addr().String()
return url, nil
}
func (server *BoardServer) Shutdown() {
close(server.events)
log.Printf("Waiting for websocket clients to finish")
<-server.done
log.Printf("Server is done, exiting")
err := server.httpServer.Shutdown(context.Background())
if err != nil {
log.Printf("Error shutting down HTTP server: %v", err)
}
}
func (server *BoardServer) SendEvent(event GameEvent) {
server.events <- event
}