|
|
|
package sim
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"github.com/jordanorelli/astro-domu/internal/errors"
|
|
|
|
"github.com/jordanorelli/astro-domu/internal/wire"
|
|
|
|
"github.com/jordanorelli/blammo"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Server struct {
|
|
|
|
*blammo.Log
|
|
|
|
http *http.Server
|
|
|
|
world *world
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) Start(host string, port int) error {
|
|
|
|
if s.Log == nil {
|
|
|
|
s.Log = defaultLog().Child("server")
|
|
|
|
}
|
|
|
|
|
|
|
|
s.world = newWorld(s.Log.Child("world"))
|
|
|
|
go s.world.run(30)
|
|
|
|
|
|
|
|
addr := fmt.Sprintf("%s:%d", host, port)
|
|
|
|
lis, err := net.Listen("tcp", addr)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("server failed to start a listener: %w", err)
|
|
|
|
}
|
|
|
|
s.Log.Info("listening for TCP traffic on %q", addr)
|
|
|
|
|
|
|
|
go s.runHTTPServer(lis)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) runHTTPServer(lis net.Listener) {
|
|
|
|
srv := http.Server{
|
|
|
|
Handler: s,
|
|
|
|
}
|
|
|
|
s.http = &srv
|
|
|
|
err := srv.Serve(lis)
|
|
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
|
|
s.Error("error in http.Serve: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
log := s.Log.Child("login")
|
|
|
|
upgrader := websocket.Upgrader{
|
|
|
|
HandshakeTimeout: 3 * time.Second,
|
|
|
|
ReadBufferSize: 2 << 12,
|
|
|
|
WriteBufferSize: 2 << 12,
|
|
|
|
Subprotocols: []string{"astrodomu@v0"},
|
|
|
|
}
|
|
|
|
|
|
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("upgrade error: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
t, rd, err := conn.NextReader()
|
|
|
|
if err != nil {
|
|
|
|
log.Error("unable to get a reader: %v", err)
|
|
|
|
conn.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if t != websocket.TextMessage {
|
|
|
|
log.Error("first message is not text")
|
|
|
|
// TODO: send websocket close frame here
|
|
|
|
conn.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var req wire.Request
|
|
|
|
if err := json.NewDecoder(rd).Decode(&req); err != nil {
|
|
|
|
log.Error("unable to parse initial request: %v", err)
|
|
|
|
// TODO: send websocket close frame here
|
|
|
|
conn.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
login, ok := req.Body.(*wire.Login)
|
|
|
|
if !ok {
|
|
|
|
log.Error("first request is not wire.Login, is %T", req.Body)
|
|
|
|
// TODO: send websocket close frame here
|
|
|
|
conn.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Info("login requested: %v", *login)
|
|
|
|
|
|
|
|
failed := make(chan error, 1)
|
|
|
|
s.world.connect <- connect{
|
|
|
|
conn: conn,
|
|
|
|
login: *login,
|
|
|
|
failed: failed,
|
|
|
|
}
|
|
|
|
e := <-failed
|
|
|
|
if e != nil {
|
|
|
|
log.Error("connect failed: %v", err)
|
|
|
|
// TODO: send websocket close frame here
|
|
|
|
conn.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) Shutdown() {
|
|
|
|
s.Info("starting shutdown procedure")
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
wg.Add(2)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
if err := s.world.stop(); err != nil {
|
|
|
|
s.Error("error stopping the simulation: %v", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
log := s.Child("http")
|
|
|
|
log.Info("shutting down http server")
|
|
|
|
if err := s.http.Shutdown(context.Background()); err != nil {
|
|
|
|
log.Error("error shutting down http server: %v", err)
|
|
|
|
} else {
|
|
|
|
log.Info("http server has shut down")
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
s.Info("shutdown procedure complete")
|
|
|
|
}
|