diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 5ad8a1a..72ae656 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -10,6 +10,7 @@ jobs: build: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@v2 diff --git a/main.go b/main.go index b4978ec..2992e4e 100644 --- a/main.go +++ b/main.go @@ -78,8 +78,8 @@ OPTIONS: } else { outputHandler = log.StreamHandler(os.Stdout, log.TerminalFormat()) } - logLvl, err_level := log.LvlFromString(*logLevel) - if err_level != nil { + logLvl, errLevel := log.LvlFromString(*logLevel) + if errLevel != nil { fmt.Fprintf(os.Stderr, "Error: Unknown loglevel\n\n") flag.Usage() os.Exit(1) diff --git a/plugins/lines/lines.go b/plugins/lines/lines.go index 0292750..baee935 100644 --- a/plugins/lines/lines.go +++ b/plugins/lines/lines.go @@ -24,7 +24,7 @@ import "github.com/ts2/ts2-sim-server/simulation" type StandardManager struct{} // IsFailed returns whether the track circuit of the given line item is failed or not -func (sm StandardManager) IsFailed(p *simulation.LineItem) bool { +func (sm StandardManager) IsFailed(_ *simulation.LineItem) bool { return false } diff --git a/plugins/points/points.go b/plugins/points/points.go index 0829d3b..fdf8c75 100644 --- a/plugins/points/points.go +++ b/plugins/points/points.go @@ -20,20 +20,16 @@ package points import ( "github.com/ts2/ts2-sim-server/simulation" - "sync" ) // StandardManager is a points manager that performs points change // immediately and never fails. type StandardManager struct { - sync.RWMutex directions map[string]simulation.PointDirection } // Direction returns the direction of the points func (sm *StandardManager) Direction(p *simulation.PointsItem) simulation.PointDirection { - sm.RLock() - defer sm.RUnlock() return sm.directions[p.ID()] } @@ -45,8 +41,6 @@ func (sm *StandardManager) SetDirection(p *simulation.PointsItem, dir simulation if dir == simulation.DirectionCurrent { return } - sm.Lock() - defer sm.Unlock() sm.directions[p.ID()] = dir if p.PairedItem() != nil { sm.directions[p.PairedItem().ID()] = dir diff --git a/plugins/routes/routes.go b/plugins/routes/routes.go index 9bfe707..481ac28 100644 --- a/plugins/routes/routes.go +++ b/plugins/routes/routes.go @@ -73,7 +73,7 @@ func (sm StandardManager) CanActivate(r *simulation.Route) error { // CanDeactivate returns an error if the given route cannot be deactivated. // In this implementation, it always returns true. -func (sm StandardManager) CanDeactivate(r *simulation.Route) error { +func (sm StandardManager) CanDeactivate(_ *simulation.Route) error { return nil } diff --git a/server/connection.go b/server/connection.go index 311ac99..29a8cd0 100644 --- a/server/connection.go +++ b/server/connection.go @@ -23,6 +23,7 @@ import ( "encoding/json" "fmt" "net" + "sync" "github.com/gorilla/websocket" ) @@ -37,12 +38,13 @@ type ManagerType string // connection is a wrapper around the websocket.Conn type connection struct { - websocket.Conn + *websocket.Conn + sync.RWMutex // pushChan is the channel on which pushed messaged are sent pushChan chan interface{} clientType ClientType - ManagerType ManagerType - Requests []Request + managerType ManagerType + requests []Request } // loop starts the reading and writing loops of the connection. @@ -82,7 +84,7 @@ func (conn *connection) processRead(ctx context.Context) { continue } } - conn.Requests = append(conn.Requests, req) + conn.appendRequest(req) hub.readChan <- conn } } @@ -130,10 +132,33 @@ func (conn *connection) registerClient() (error, *Request) { logger.Info("Error while writing", "connection", conn.RemoteAddr(), "request", "NewOkResponse", "error", err) } hub.registerChan <- conn - logger.Info("Registered client", "connection", conn.RemoteAddr(), "clientType", conn.clientType, "managerType", conn.ManagerType) + logger.Info("Registered client", "connection", conn.RemoteAddr(), "clientType", conn.clientType, "managerType", conn.managerType) return nil, req } +// popRequest pops the first request of this connection +func (conn *connection) popRequest() Request { + conn.Lock() + defer conn.Unlock() + req := conn.requests[0] + conn.requests = conn.requests[1:] + return req +} + +// getRequest returns the first request of this connection +func (conn *connection) getRequest() Request { + conn.RLock() + defer conn.RUnlock() + return conn.requests[0] +} + +// appendRequest appends the given request to the requests list +func (conn *connection) appendRequest(req Request) { + conn.Lock() + defer conn.Unlock() + conn.requests = append(conn.requests, req) +} + // Close terminates the websocket connection and closes associated resources func (conn *connection) Close() error { _ = conn.Conn.Close() diff --git a/server/connection_test.go b/server/connection_test.go index f39a86e..93f6be0 100644 --- a/server/connection_test.go +++ b/server/connection_test.go @@ -24,12 +24,31 @@ import ( "github.com/gorilla/websocket" . "github.com/smartystreets/goconvey/convey" + "github.com/ts2/ts2-sim-server/simulation" ) func TestConnection(t *testing.T) { // Wait for server to come up time.Sleep(2 * time.Second) Convey("Testing server connection", t, func() { + stopChan := make(chan struct{}) + // Creating a few concurrent connections + for i := 0; i < nbGoroutines; i++ { + go func() { + conn := clientDial(t) + defer conn.Close() + register(t, conn, Client, "", "client-secret") + addListener(t, conn, simulation.TrackItemChangedEvent) + addListener(t, conn, simulation.RouteActivatedEvent) + addListener(t, conn, simulation.RouteDeactivatedEvent) + addListener(t, conn, simulation.ClockEvent) + addListener(t, conn, simulation.TrainChangedEvent) + addListener(t, conn, simulation.MessageReceivedEvent) + addListener(t, conn, simulation.OptionsChangedEvent) + addListener(t, conn, simulation.StateChangedEvent) + <-stopChan + }() + } c := clientDial(t) Convey("Login test", func() { Convey("First request that is not a register request should fail", func() { @@ -70,6 +89,7 @@ func TestConnection(t *testing.T) { }) Reset(func() { err := c.Close() + close(stopChan) So(err, ShouldBeNil) }) }) diff --git a/server/doc.go b/server/doc.go index bbe24af..2c699c8 100644 --- a/server/doc.go +++ b/server/doc.go @@ -16,5 +16,5 @@ // Free Software Foundation, Inc., // 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -// Package `server` implements the http/websocket front end for the ts2 simulator +// Package server implements the http/websocket front end for the ts2 simulator package server diff --git a/server/hub.go b/server/hub.go index 2aa9343..9be4694 100644 --- a/server/hub.go +++ b/server/hub.go @@ -20,7 +20,6 @@ package server import ( "fmt" - "sync" "github.com/ts2/ts2-sim-server/simulation" ) @@ -33,14 +32,8 @@ type Hub struct { // Registry of client listeners registry map[registryEntry]map[*connection]bool - // registryMutex protects the registry - registryMutex sync.RWMutex - // lastEvents holds the last event sent for each registryEntry - lastEvents map[registryEntry]*simulation.Event - - // lastEventsMutex protects the lastEvents map - lastEventsMutex sync.RWMutex + lastEvents map[registryEntry]simulation.Event // Register requests from the connection registerChan chan *connection @@ -64,28 +57,31 @@ func (h *Hub) run(hubUp chan bool) { hubUp <- true var ( - e *simulation.Event + e simulation.Event c *connection ) for { select { - case e = <-sim.EventChan: - logger.Debug("Received event from simulation", "submodule", "hub", "event", e.Name, "object", e.Object) - h.notifyClients(e) case c = <-h.readChan: - logger.Debug("Reading request from client", "submodule", "hub", "data", c.Requests[0]) - go h.dispatchObject(c) + logger.Debug("Reading request from client", "submodule", "hub", "data", c.getRequest()) + h.dispatchObject(c) case c = <-h.registerChan: logger.Debug("Registering connection", "submodule", "hub", "connection", c.RemoteAddr()) h.register(c) case c = <-h.unregisterChan: logger.Info("Unregistering connection", "submodule", "hub", "connection", c.RemoteAddr()) h.unregister(c) + case e = <-sim.EventChan: + logger.Debug("Received event from simulation", "submodule", "hub", "event", e.Name, "object", e.Object) + if e.Name == simulation.ClockEvent { + sim.ProcessTimeStep() + } + h.notifyClients(e) } } } -// register registers the given connection to this hub +// register the given connection to this hub func (h *Hub) register(c *connection) { switch c.clientType { case Client: @@ -95,8 +91,6 @@ func (h *Hub) register(c *connection) { // addConnectionToRegistry adds this connection to the registry for eventName and id. func (h *Hub) addConnectionToRegistry(conn *connection, eventName simulation.EventName, id string) { - h.registryMutex.Lock() - defer h.registryMutex.Unlock() re := registryEntry{eventName: eventName, id: id} if _, ok := h.registry[re]; !ok { h.registry[re] = make(map[*connection]bool) @@ -106,24 +100,20 @@ func (h *Hub) addConnectionToRegistry(conn *connection, eventName simulation.Eve // removeEntryFromRegistry removes this connection from the registry for eventName and id. func (h *Hub) removeEntryFromRegistry(conn *connection, eventName simulation.EventName, id string) { - h.registryMutex.Lock() - defer h.registryMutex.Unlock() re := registryEntry{eventName: eventName, id: id} delete(h.registry[re], conn) } // removeConnectionFromRegistry removes all entries of this connection in the registry. func (h *Hub) removeConnectionFromRegistry(conn *connection) { - h.registryMutex.Lock() - defer h.registryMutex.Unlock() for re, rv := range h.registry { if _, ok := rv[conn]; ok { - delete(h.registry, re) + delete(h.registry[re], conn) } } } -// unregister unregisters the connection to this hub +// unregister the connection to this hub func (h *Hub) unregister(c *connection) { switch c.clientType { case Client: @@ -135,14 +125,16 @@ func (h *Hub) unregister(c *connection) { } // notifyClients sends the given event to all registered clients. -func (h *Hub) notifyClients(e *simulation.Event) { +func (h *Hub) notifyClients(e simulation.Event) { logger.Debug("Notifying clients", "submodule", "hub", "event", e) h.updateLastEvents(e) - h.registryMutex.RLock() - defer h.registryMutex.RUnlock() // Notify clients that subscribed to all objects for conn := range h.registry[registryEntry{eventName: e.Name, id: ""}] { - conn.pushChan <- NewNotificationResponse(e) + resp := NewNotificationResponse(e) + // Send notification in another goroutine so as not to block + go func(c* connection) { + c.pushChan <- resp + }(conn) } if e.Object.ID() == "" { // Object has no ID. Don't send twice @@ -150,24 +142,22 @@ func (h *Hub) notifyClients(e *simulation.Event) { } // Notify clients that subscribed to specific object IDs for conn := range h.registry[registryEntry{eventName: e.Name, id: e.Object.ID()}] { - conn.pushChan <- NewNotificationResponse(e) + resp := NewNotificationResponse(e) + // Send notification in another goroutine so as not to block + go func(c* connection) { + c.pushChan <- resp + }(conn) } } // updateLastEvents updates the lastEvents map in a concurrently safe way -func (h *Hub) updateLastEvents(e *simulation.Event) { - h.lastEventsMutex.Lock() - defer h.lastEventsMutex.Unlock() +func (h *Hub) updateLastEvents(e simulation.Event) { h.lastEvents[registryEntry{eventName: e.Name, id: e.Object.ID()}] = e } // dispatchObject process a request. -// -// - req is the request to process -// - ch is the channel on which to send the response func (h *Hub) dispatchObject(conn *connection) { - req := conn.Requests[0] - conn.Requests = conn.Requests[1:] + req := conn.popRequest() obj, ok := h.objects[req.Object] if !ok { conn.pushChan <- NewErrorResponse(req.ID, fmt.Errorf("unknown object %s", req.Object)) @@ -184,7 +174,7 @@ func newHub() *Hub { h.clientConnections = make(map[*connection]bool) // make registry map h.registry = make(map[registryEntry]map[*connection]bool) - h.lastEvents = make(map[registryEntry]*simulation.Event) + h.lastEvents = make(map[registryEntry]simulation.Event) // make channels h.registerChan = make(chan *connection) h.unregisterChan = make(chan *connection) diff --git a/server/hub_option.go b/server/hub_option.go index c2b0556..05aeb08 100644 --- a/server/hub_option.go +++ b/server/hub_option.go @@ -26,7 +26,7 @@ import ( type optionObject struct{} // dispatch processes requests made on the Option object -func (s *optionObject) dispatch(h *Hub, req Request, conn *connection) { +func (s *optionObject) dispatch(_ *Hub, req Request, conn *connection) { ch := conn.pushChan switch req.Action { case "list": diff --git a/server/hub_place.go b/server/hub_place.go index 2469c5b..01e5f28 100644 --- a/server/hub_place.go +++ b/server/hub_place.go @@ -28,7 +28,7 @@ import ( type placeObject struct{} // dispatch processes requests made on the Place object -func (s *placeObject) dispatch(h *Hub, req Request, conn *connection) { +func (s *placeObject) dispatch(_ *Hub, req Request, conn *connection) { ch := conn.pushChan switch req.Action { case "list": diff --git a/server/hub_route.go b/server/hub_route.go index d01117f..502cfb3 100644 --- a/server/hub_route.go +++ b/server/hub_route.go @@ -28,7 +28,7 @@ import ( type routeObject struct{} // dispatch processes requests made on the route object -func (r *routeObject) dispatch(h *Hub, req Request, conn *connection) { +func (r *routeObject) dispatch(_ *Hub, req Request, conn *connection) { ch := conn.pushChan switch req.Action { case "list": diff --git a/server/hub_server.go b/server/hub_server.go index ac1258e..855db0e 100644 --- a/server/hub_server.go +++ b/server/hub_server.go @@ -98,9 +98,7 @@ func (h *Hub) removeRegistryEntry(req Request, conn *connection) error { } // renotifyClient will resend the last notification for each event and object ID -func (h *Hub) renotifyClient(req Request, conn *connection) error { - h.lastEventsMutex.RLock() - defer h.lastEventsMutex.RUnlock() +func (h *Hub) renotifyClient(_ Request, conn *connection) error { for re, event := range h.lastEvents { if _, ok := h.registry[registryEntry{eventName: re.eventName, id: ""}]; ok { if h.registry[registryEntry{eventName: event.Name, id: ""}][conn] { diff --git a/server/hub_service.go b/server/hub_service.go index 4faea61..cecdc8d 100644 --- a/server/hub_service.go +++ b/server/hub_service.go @@ -28,7 +28,7 @@ import ( type serviceObject struct{} // dispatch processes requests made on the Service object -func (s *serviceObject) dispatch(h *Hub, req Request, conn *connection) { +func (s *serviceObject) dispatch(_ *Hub, req Request, conn *connection) { ch := conn.pushChan switch req.Action { case "list": diff --git a/server/hub_simulation.go b/server/hub_simulation.go index 7abb8c7..d96b8d1 100644 --- a/server/hub_simulation.go +++ b/server/hub_simulation.go @@ -26,7 +26,7 @@ import ( type simulationObject struct{} // dispatch processes requests made on the Simulation object -func (s *simulationObject) dispatch(h *Hub, req Request, conn *connection) { +func (s *simulationObject) dispatch(_ *Hub, req Request, conn *connection) { ch := conn.pushChan logger.Debug("Request for simulation received", "submodule", "hub", "object", req.Object, "action", req.Action) switch req.Action { @@ -42,7 +42,7 @@ func (s *simulationObject) dispatch(h *Hub, req Request, conn *connection) { ch <- NewErrorResponse(req.ID, fmt.Errorf("internal error: %s", err)) return } - ch <- NewResponse(req.ID, RawJSON(j)) + ch <- NewResponse(req.ID, j) case "dump": data, err := json.Marshal(sim) if err != nil { diff --git a/server/hub_test.go b/server/hub_test.go index 8f6580d..b001c0b 100644 --- a/server/hub_test.go +++ b/server/hub_test.go @@ -33,6 +33,8 @@ import ( "github.com/ts2/ts2-sim-server/simulation" ) +const nbGoroutines = 10 + type trackStruct struct { ID string `json:"id"` TiType string `json:"__type__"` @@ -48,22 +50,56 @@ type trackStruct struct { } func sendRequestStatus(c *websocket.Conn, object, action, params string) ResponseStatus { + resp, err := sendRequestStatusWithError(c, object, action, params) + So(err, ShouldBeNil) + return resp +} + +func sendRequestStatusWithError(c *websocket.Conn, object, action, params string) (ResponseStatus, error) { if params == "" { params = "null" } - err := c.WriteJSON(Request{Object: object, Action: action, Params: RawJSON(params)}) - So(err, ShouldBeNil) + if err := c.WriteJSON(Request{Object: object, Action: action, Params: RawJSON(params)}); err != nil { + return ResponseStatus{}, err + } var resp ResponseStatus - err = c.ReadJSON(&resp) - So(err, ShouldBeNil) - So(resp.MsgType, ShouldEqual, TypeResponse) - return resp + for resp.MsgType != TypeResponse { + err := c.ReadJSON(&resp) + if err != nil { + return ResponseStatus{}, err + } + } + return resp, nil } func TestHub(t *testing.T) { // Wait for server to come up time.Sleep(100 * time.Millisecond) Convey("Testing hub functions", t, func() { + stopChan := make(chan struct{}) + // Creating a few concurrent connections + for i := 0; i < nbGoroutines; i++ { + go func() { + conn := clientDial(t) + defer func() { + if err := conn.Close(); err != nil { + t.Error(err) + } + }() + if err := register(t, conn, Client, "", "client-secret"); err != nil { + t.Error(err) + } + addListener(t, conn, simulation.TrackItemChangedEvent) + addListener(t, conn, simulation.RouteActivatedEvent) + addListener(t, conn, simulation.RouteDeactivatedEvent) + addListener(t, conn, simulation.ClockEvent) + addListener(t, conn, simulation.TrainChangedEvent) + addListener(t, conn, simulation.MessageReceivedEvent) + addListener(t, conn, simulation.OptionsChangedEvent) + addListener(t, conn, simulation.StateChangedEvent) + <-stopChan + }() + } c := clientDial(t) err := register(t, c, Client, "", "client-secret") So(err, ShouldBeNil) @@ -741,8 +777,79 @@ func TestHub(t *testing.T) { } }) }) + + Convey("Testing the whole system", func() { + Convey("Start simulation and execute a few actions concurrently", func() { + for i := 0; i < nbGoroutines; i++ { + go func() { + conn := clientDial(t) + defer func() { + if err := conn.Close(); err != nil { + t.Error(err) + } + }() + if err := register(t, conn, Client, "", "client-secret"); err != nil { + t.Error(err) + } + + addListener(t, conn, simulation.TrackItemChangedEvent) + addListener(t, conn, simulation.RouteActivatedEvent) + addListener(t, conn, simulation.RouteDeactivatedEvent) + addListener(t, conn, simulation.ClockEvent) + addListener(t, conn, simulation.TrainChangedEvent) + addListener(t, conn, simulation.MessageReceivedEvent) + addListener(t, conn, simulation.OptionsChangedEvent) + addListener(t, conn, simulation.StateChangedEvent) + + resp, err := sendRequestStatusWithError(conn, "simulation", "start", "") + if err != nil { + t.Error(err) + } + if resp.Data.Status != Ok { + t.Error("simulation did not start", resp) + } + + time.Sleep(500 * time.Millisecond) + if sim.Options.CurrentTime() != simulation.ParseTime("06:00:02.5") { + t.Error("simulation time is not evolving. Expected 06:00:02.5 got", sim.Options.CurrentTime()) + } + + resp, err = sendRequestStatusWithError(conn, "route", "deactivate", `{"id": "1"}`) + if err != nil { + t.Error(err) + } + + resp, err = sendRequestStatusWithError(conn, "route", "deactivate", `{"id": "2"}`) + if err != nil { + t.Error(err) + } + + resp, err = sendRequestStatusWithError(conn, "route", "activate", `{"id": "1"}`) + if err != nil { + t.Error(err) + } + + resp, err = sendRequestStatusWithError(conn, "route", "activate", `{"id": "2"}`) + if err != nil { + t.Error(err) + } + + time.Sleep(500 * time.Millisecond) + + resp, err = sendRequestStatusWithError(conn, "simulation", "pause", "") + if err != nil { + t.Error(err) + } + if resp.Data.Status != Ok { + t.Error("simulation did not pause", resp) + } + }() + } + }) + }) Reset(func() { err := c.Close() + close(stopChan) So(err, ShouldBeNil) }) }) diff --git a/server/hub_trackitem.go b/server/hub_trackitem.go index e3c7012..94121cc 100644 --- a/server/hub_trackitem.go +++ b/server/hub_trackitem.go @@ -28,7 +28,7 @@ import ( type trackItemObject struct{} // dispatch processes requests made on the TrackItem object -func (s *trackItemObject) dispatch(h *Hub, req Request, conn *connection) { +func (s *trackItemObject) dispatch(_ *Hub, req Request, conn *connection) { ch := conn.pushChan switch req.Action { case "list": diff --git a/server/hub_train.go b/server/hub_train.go index 1b517c8..d431904 100644 --- a/server/hub_train.go +++ b/server/hub_train.go @@ -28,7 +28,7 @@ import ( type trainObject struct{} // dispatch processes requests made on the Service object -func (t *trainObject) dispatch(h *Hub, req Request, conn *connection) { +func (t *trainObject) dispatch(_ *Hub, req Request, conn *connection) { logger.Debug("Request for train received", "submodule", "hub", "object", req.Object, "action", req.Action) ch := conn.pushChan switch req.Action { diff --git a/server/hub_traintype.go b/server/hub_traintype.go index f5d5eb3..4d17a39 100644 --- a/server/hub_traintype.go +++ b/server/hub_traintype.go @@ -28,7 +28,7 @@ import ( type trainTypeObject struct{} // dispatch processes requests made on the TrainType object -func (s *trainTypeObject) dispatch(h *Hub, req Request, conn *connection) { +func (s *trainTypeObject) dispatch(_ *Hub, req Request, conn *connection) { ch := conn.pushChan switch req.Action { case "list": diff --git a/server/main_test.go b/server/main_test.go index 1014ce9..6326199 100644 --- a/server/main_test.go +++ b/server/main_test.go @@ -59,7 +59,7 @@ func clientDial(t *testing.T) *websocket.Conn { } // register dials to the server and logs the client in -func register(t *testing.T, c *websocket.Conn, ct ClientType, mt ManagerType, token string) error { +func register(_ *testing.T, c *websocket.Conn, ct ClientType, mt ManagerType, token string) error { loginRequest := RequestRegister{1234, "server", "register", ParamsRegister{ct, mt, token}} if err := c.WriteJSON(loginRequest); err != nil { return err @@ -72,3 +72,27 @@ func register(t *testing.T, c *websocket.Conn, ct ClientType, mt ManagerType, to return fmt.Errorf(expectedResponse.Data.Message) } } + +// addListener for the given event +func addListener(t *testing.T, c *websocket.Conn, event simulation.EventName) { + err := c.WriteJSON(RequestListener{ + Object: "server", + Action: "addListener", + Params: ParamsListener{ + Event: event, + }, + }) + if err != nil { + t.Error(err) + } + var resp ResponseStatus + for resp.MsgType != TypeResponse { + err = c.ReadJSON(&resp) + if err != nil { + t.Error(err) + } + } + if resp.Data.Status != Ok { + t.Errorf("error while setting up listener: %v", resp) + } +} \ No newline at end of file diff --git a/server/responses.go b/server/responses.go index 1e5610c..6a0a1a5 100644 --- a/server/responses.go +++ b/server/responses.go @@ -19,6 +19,7 @@ package server import ( + "encoding/json" "fmt" "github.com/ts2/ts2-sim-server/simulation" @@ -62,7 +63,7 @@ type ResponseStatus struct { // DataEvent is the Data part of a ResponseNotification message type DataEvent struct { Name simulation.EventName `json:"name"` - Object interface{} `json:"object"` + Object RawJSON `json:"object"` } // ResponseNotification is a message sent by the server to the clients when an event is triggered in the simulation @@ -72,17 +73,17 @@ type ResponseNotification struct { } // NewResponse returns a Response with the given data -func NewResponse(id int, data RawJSON) *Response { +func NewResponse(id int, data RawJSON) Response { r := Response{ ID: id, MsgType: TypeResponse, Data: data, } - return &r + return r } // NewErrorResponse returns a ResponseStatus object corresponding to the given error. -func NewErrorResponse(id int, e error) *ResponseStatus { +func NewErrorResponse(id int, e error) ResponseStatus { sr := ResponseStatus{ ID: id, MsgType: TypeResponse, @@ -91,11 +92,11 @@ func NewErrorResponse(id int, e error) *ResponseStatus { fmt.Sprintf("Error: %s", e), }, } - return &sr + return sr } // NewOkResponse returns a new ResponseStatus object with OK status and empty message. -func NewOkResponse(id int, msg string) *ResponseStatus { +func NewOkResponse(id int, msg string) ResponseStatus { sr := ResponseStatus{ ID: id, MsgType: TypeResponse, @@ -104,17 +105,18 @@ func NewOkResponse(id int, msg string) *ResponseStatus { msg, }, } - return &sr + return sr } // NewNotificationResponse returns a new ResponseNotification object from the given Event -func NewNotificationResponse(e *simulation.Event) *ResponseNotification { +func NewNotificationResponse(e simulation.Event) ResponseNotification { + objRaw, _ := json.Marshal(e.Object) er := ResponseNotification{ MsgType: TypeNotification, Data: DataEvent{ Name: e.Name, - Object: e.Object, + Object: objRaw, }, } - return &er + return er } diff --git a/server/websocket.go b/server/websocket.go index 4cccee7..3764512 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -42,7 +42,7 @@ func serveWs(w http.ResponseWriter, r *http.Request) { return } conn := &connection{ - Conn: *ws, + Conn: ws, pushChan: make(chan interface{}, 256), } ctx, cancel := context.WithCancel(context.Background()) diff --git a/simulation/color.go b/simulation/color.go index 9d13fa1..9b7791e 100644 --- a/simulation/color.go +++ b/simulation/color.go @@ -23,12 +23,12 @@ import ( "fmt" ) -// A Colour stored as RGB values +// A Color stored as RGB values type Color struct { R, G, B uint8 } -// Implement the Go color.Color interface. +// RGBA implements the Go color.Color interface. func (c Color) RGBA() (r, g, b, a uint32) { r = uint32(c.R) g = uint32(c.G) @@ -37,12 +37,12 @@ func (c Color) RGBA() (r, g, b, a uint32) { return } -// Color.Hex() returns the hex "html" representation of the color, as in #ff0080. +// Hex returns the hex "html" representation of the color, as in #ff0080. func (c Color) Hex() string { - return fmt.Sprintf("#%02x%02x%02x", uint8(c.R), uint8(c.G), uint8(c.B)) + return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B) } -// FromHex() parses a "css/html" hex color-string, either in the 3 "#f0c" or 6 "#ff1034" digits form. +// FromHex parses a "css/html" hex color-string, either in the 3 "#f0c" or 6 "#ff1034" digits form. func FromHex(scol string) (Color, error) { format := "#%02x%02x%02x" diff --git a/simulation/doc.go b/simulation/doc.go index 9228225..a1bd03b 100644 --- a/simulation/doc.go +++ b/simulation/doc.go @@ -16,5 +16,5 @@ // Free Software Foundation, Inc., // 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -// package simulation contains core logic of ts2 +// Package simulation contains core logic of ts2 package simulation diff --git a/simulation/loading_test.go b/simulation/loading_test.go index 9d61d0d..71a8805 100644 --- a/simulation/loading_test.go +++ b/simulation/loading_test.go @@ -50,7 +50,7 @@ func TestSimulationLoading(t *testing.T) { So(err, ShouldBeNil) Convey("Options should be all loaded", func() { So(sim.Options.CurrentScore, ShouldEqual, 0) - So(sim.Options.CurrentTime, ShouldResemble, ParseTime("06:00:00")) + So(sim.Options.CurrentTime(), ShouldResemble, ParseTime("06:00:00")) So(sim.Options.DefaultDelayAtEntry.Equals(DelayGenerator{[]delayTuplet{{0, 0, 100}}}), ShouldBeTrue) So(sim.Options.DefaultMinimumStopTime.Equals(DelayGenerator{[]delayTuplet{{20, 40, 90}, {40, 120, 10}}}), ShouldBeTrue) So(sim.Options.DefaultMaxSpeed, ShouldEqual, 18.06) diff --git a/simulation/messages.go b/simulation/messages.go index 683954a..038011c 100644 --- a/simulation/messages.go +++ b/simulation/messages.go @@ -61,7 +61,7 @@ func (ml *MessageLogger) addMessage(msg string, typ MessageType) { if Logger != nil { Logger.Info(msg, "msgType", typ) } - ml.simulation.sendEvent(&Event{ + ml.simulation.sendEvent(Event{ Name: MessageReceivedEvent, Object: newMsg, }) diff --git a/simulation/options.go b/simulation/options.go index ce29d94..849c9ff 100644 --- a/simulation/options.go +++ b/simulation/options.go @@ -19,8 +19,11 @@ package simulation import ( + "encoding/json" "fmt" "reflect" + "sync" + "time" ) // Options struct for the simulation @@ -28,7 +31,6 @@ type Options struct { TrackCircuitBased bool `json:"trackCircuitBased"` ClientToken string `json:"clientToken"` CurrentScore int `json:"currentScore"` - CurrentTime Time `json:"currentTime"` DefaultDelayAtEntry DelayGenerator `json:"defaultDelayAtEntry"` DefaultMaxSpeed float64 `json:"defaultMaxSpeed"` DefaultMinimumStopTime DelayGenerator `json:"defaultMinimumStopTime"` @@ -42,11 +44,13 @@ type Options struct { WrongDestinationPenalty int `json:"wrongDestinationPenalty"` LatePenalty int `json:"latePenalty"` - simulation *Simulation + simulation *Simulation + currentTime Time + currentTimeMutex sync.RWMutex } // ID func for options to that it implements SimObject. Returns an empty string. -func (o Options) ID() string { +func (o *Options) ID() string { return "" } @@ -55,11 +59,21 @@ func (o Options) ID() string { // option can be either the struct field name or the json key of the struct field. func (o *Options) Set(option string, value interface{}) error { defer func() { - o.simulation.sendEvent(&Event{Name: OptionsChangedEvent, Object: o}) + o.simulation.sendEvent(Event{Name: OptionsChangedEvent, Object: o}) }() if value == nil { return fmt.Errorf("option %s cannot have nil value", option) } + if option == "CurrentTime" || option == "currentTime" { + o.currentTimeMutex.Lock() + defer o.currentTimeMutex.Unlock() + t, ok := value.(Time) + if !ok { + return fmt.Errorf("cannot assign %v (%T) to currentTime (Time)", value, value) + } + o.currentTime = t + return nil + } stVal := reflect.ValueOf(o).Elem() typ := stVal.Type() _, ok := typ.FieldByName(option) @@ -84,3 +98,63 @@ func (o *Options) Set(option string, value interface{}) error { } return fmt.Errorf("unknown option %s", option) } + +// UnmarshalJSON for the options type +func (o *Options) UnmarshalJSON(data []byte) error { + type optsUnmarshalType struct { + TrackCircuitBased bool `json:"trackCircuitBased"` + ClientToken string `json:"clientToken"` + CurrentScore int `json:"currentScore"` + DefaultDelayAtEntry DelayGenerator `json:"defaultDelayAtEntry"` + DefaultMaxSpeed float64 `json:"defaultMaxSpeed"` + DefaultMinimumStopTime DelayGenerator `json:"defaultMinimumStopTime"` + DefaultSignalVisibility float64 `json:"defaultSignalVisibility"` + Description string `json:"description"` + TimeFactor int `json:"timeFactor"` + Title string `json:"title"` + Version string `json:"version"` + WarningSpeed float64 `json:"warningSpeed"` + WrongPlatformPenalty int `json:"wrongPlatformPenalty"` + WrongDestinationPenalty int `json:"wrongDestinationPenalty"` + LatePenalty int `json:"latePenalty"` + CurrentTime Time `json:"CurrentTime"` + } + var auxOpts optsUnmarshalType + if err := json.Unmarshal(data, &auxOpts); err!= nil { + return err + } + o.TrackCircuitBased = auxOpts.TrackCircuitBased + o.ClientToken = auxOpts.ClientToken + o.CurrentScore = auxOpts.CurrentScore + o.DefaultDelayAtEntry = auxOpts.DefaultDelayAtEntry + o.DefaultMaxSpeed = auxOpts.DefaultMaxSpeed + o.DefaultMinimumStopTime = auxOpts.DefaultMinimumStopTime + o.DefaultSignalVisibility = auxOpts.DefaultSignalVisibility + o.Description = auxOpts.Description + o.TimeFactor = auxOpts.TimeFactor + o.Title = auxOpts.Title + o.Version = auxOpts.Version + o.WarningSpeed = auxOpts.WarningSpeed + o.WrongPlatformPenalty = auxOpts.WrongPlatformPenalty + o.WrongDestinationPenalty = auxOpts.WrongDestinationPenalty + o.LatePenalty = auxOpts.LatePenalty + + o.currentTimeMutex.Lock() + defer o.currentTimeMutex.Unlock() + o.currentTime = auxOpts.CurrentTime + return nil +} + +// CurrentTime returns the current time of the simulation +func (o *Options) CurrentTime() Time { + o.currentTimeMutex.RLock() + defer o.currentTimeMutex.RUnlock() + return o.currentTime +} + +// IncreaseTime increases the simulation time by the given step +func (o *Options) IncreaseTime(step time.Duration) { + o.currentTimeMutex.Lock() + defer o.currentTimeMutex.Unlock() + o.currentTime = o.currentTime.Add(step) +} diff --git a/simulation/position.go b/simulation/position.go index 9247a56..6041d06 100644 --- a/simulation/position.go +++ b/simulation/position.go @@ -92,7 +92,10 @@ func (pos Position) IsOut() bool { // Next is the first Position on the next TrackItem with regard to this Position func (pos Position) Next(dir PointDirection) Position { - nextTi, _ := pos.TrackItem().FollowingItem(pos.PreviousItem(), dir) + nextTi, err := pos.TrackItem().FollowingItem(pos.PreviousItem(), dir) + if err != nil { + panic(err) + } return Position{ simulation: pos.simulation, TrackItemID: nextTi.ID(), @@ -103,7 +106,10 @@ func (pos Position) Next(dir PointDirection) Position { // Previous is the last Position on the previous TrackItem with regard to this Position func (pos Position) Previous() Position { - previousTI, _ := pos.PreviousItem().FollowingItem(pos.TrackItem(), DirectionCurrent) + previousTI, err := pos.PreviousItem().FollowingItem(pos.TrackItem(), DirectionCurrent) + if err != nil { + panic(err) + } var previousTIID string if previousTI != nil { previousTIID = previousTI.ID() diff --git a/simulation/routes.go b/simulation/routes.go index 915e26c..cd12260 100644 --- a/simulation/routes.go +++ b/simulation/routes.go @@ -137,7 +137,7 @@ func (r *Route) Activate(persistent bool) error { for _, t := range r.triggers { t(r) } - r.simulation.sendEvent(&Event{ + r.simulation.sendEvent(Event{ Name: RouteActivatedEvent, Object: r, }) @@ -163,7 +163,7 @@ func (r *Route) Deactivate() error { for _, t := range r.triggers { t(r) } - r.simulation.sendEvent(&Event{ + r.simulation.sendEvent(Event{ Name: RouteDeactivatedEvent, Object: r, }) diff --git a/simulation/signal_conditions.go b/simulation/signal_conditions.go index b2dcdfc..b20ddaf 100644 --- a/simulation/signal_conditions.go +++ b/simulation/signal_conditions.go @@ -23,7 +23,7 @@ import ( "strings" ) -// nextActiveRoute is true if a route starting from this Signal is active +// NextActiveRoute is true if a route starting from this Signal is active type NextActiveRoute struct{} // Code of the ConditionType, uniquely defines this ConditionType @@ -32,17 +32,17 @@ func (nar NextActiveRoute) Code() string { } // Solve returns if the condition is met for the given SignalItem and parameters -func (nar NextActiveRoute) Solve(item *SignalItem, values []string, params []string) bool { +func (nar NextActiveRoute) Solve(item *SignalItem, _ []string, _ []string) bool { return item.nextActiveRoute != nil } // SetupTriggers installs needed triggers for the given SignalItem, with the // given Condition. -func (nar NextActiveRoute) SetupTriggers(item *SignalItem, params []string) {} +func (nar NextActiveRoute) SetupTriggers(_ *SignalItem, _ []string) {} // --------------------------------------------------------------------------------------------------------------- -// previousActiveRoute is true if a route ending at this Signal is active +// PreviousActiveRoute is true if a route ending at this Signal is active type PreviousActiveRoute struct{} // Code of the ConditionType, uniquely defines this ConditionType @@ -51,13 +51,13 @@ func (par PreviousActiveRoute) Code() string { } // Solve returns if the condition is met for the given SignalItem and parameters -func (par PreviousActiveRoute) Solve(item *SignalItem, values []string, params []string) bool { +func (par PreviousActiveRoute) Solve(item *SignalItem, _ []string, _ []string) bool { return item.previousActiveRoute != nil } // SetupTriggers installs needed triggers for the given SignalItem, with the // given Condition. -func (par PreviousActiveRoute) SetupTriggers(item *SignalItem, params []string) {} +func (par PreviousActiveRoute) SetupTriggers(_ *SignalItem, _ []string) {} // --------------------------------------------------------------------------------------------------------------- @@ -71,7 +71,7 @@ func (rsa RouteSetAcross) Code() string { } // Solve returns if the condition is met for the given SignalItem and parameters -func (rsa RouteSetAcross) Solve(item *SignalItem, values []string, params []string) bool { +func (rsa RouteSetAcross) Solve(item *SignalItem, _ []string, _ []string) bool { if item.ActiveRoute() != nil { positions := item.ActiveRoute().Positions for _, pos := range positions[1 : len(positions)-1] { @@ -85,7 +85,7 @@ func (rsa RouteSetAcross) Solve(item *SignalItem, values []string, params []stri // SetupTriggers installs needed triggers for the given SignalItem, with the // given Condition. -func (rsa RouteSetAcross) SetupTriggers(item *SignalItem, params []string) {} +func (rsa RouteSetAcross) SetupTriggers(_ *SignalItem, _ []string) {} // --------------------------------------------------------------------------------------------------------------- @@ -99,7 +99,7 @@ func (tnpnr TrainNotPresentOnNextRoute) Code() string { } // Solve returns if the condition is met for the given SignalItem and parameters -func (tnpnr TrainNotPresentOnNextRoute) Solve(item *SignalItem, values []string, params []string) bool { +func (tnpnr TrainNotPresentOnNextRoute) Solve(item *SignalItem, _ []string, _ []string) bool { if item.nextActiveRoute == nil { return false } @@ -113,7 +113,7 @@ func (tnpnr TrainNotPresentOnNextRoute) Solve(item *SignalItem, values []string, // SetupTriggers installs needed triggers for the given SignalItem, with the // given Condition. -func (tnpnr TrainNotPresentOnNextRoute) SetupTriggers(item *SignalItem, params []string) {} +func (tnpnr TrainNotPresentOnNextRoute) SetupTriggers(_ *SignalItem, _ []string) {} // --------------------------------------------------------------------------------------------------------------- @@ -129,7 +129,7 @@ func (tnpbns TrainNotPresentBeforeNextSignal) Code() string { } // Solve returns if the condition is met for the given SignalItem and parameters -func (tnpbns TrainNotPresentBeforeNextSignal) Solve(item *SignalItem, values []string, params []string) bool { +func (tnpbns TrainNotPresentBeforeNextSignal) Solve(item *SignalItem, values []string, _ []string) bool { mainLoop: for cur := item.Position(); !cur.IsOut(); cur = cur.Next(DirectionCurrent) { if cur.TrackItem().TrainPresent() { @@ -154,7 +154,7 @@ mainLoop: // SetupTriggers installs needed triggers for the given SignalItem, with the // given Condition. -func (tnpbns TrainNotPresentBeforeNextSignal) SetupTriggers(item *SignalItem, params []string) {} +func (tnpbns TrainNotPresentBeforeNextSignal) SetupTriggers(_ *SignalItem, _ []string) {} // --------------------------------------------------------------------------------------------------------------- @@ -167,7 +167,7 @@ func (tnpoi TrainNotPresentOnItems) Code() string { } // Solve returns if the condition is met for the given SignalItem and parameters -func (tnpoi TrainNotPresentOnItems) Solve(item *SignalItem, values []string, params []string) bool { +func (tnpoi TrainNotPresentOnItems) Solve(item *SignalItem, _ []string, params []string) bool { for _, id := range params { if item.Simulation().TrackItems[id].TrainPresent() { return false @@ -203,7 +203,7 @@ func (tpoi TrainPresentOnItems) Code() string { } // Solve returns if the condition is met for the given SignalItem and parameters -func (tpoi TrainPresentOnItems) Solve(item *SignalItem, values []string, params []string) bool { +func (tpoi TrainPresentOnItems) Solve(item *SignalItem, _ []string, params []string) bool { for _, id := range params { if !item.Simulation().TrackItems[id].TrainPresent() { return false @@ -239,7 +239,7 @@ func (rs RouteSet) Code() string { } // Solve returns if the condition is met for the given SignalItem and parameters -func (rs RouteSet) Solve(item *SignalItem, values []string, params []string) bool { +func (rs RouteSet) Solve(item *SignalItem, _ []string, params []string) bool { for _, id := range params { if item.Simulation().Routes[id].IsActive() { return true @@ -300,13 +300,13 @@ func checkSignalAspect(signal *SignalItem, aspectNames []string, previous ...boo } // Solve returns if the condition is met for the given SignalItem and parameters -func (nsa NextSignalAspects) Solve(item *SignalItem, values []string, params []string) bool { +func (nsa NextSignalAspects) Solve(item *SignalItem, values []string, _ []string) bool { return checkSignalAspect(item.getNextSignal(), values) } // SetupTriggers installs needed triggers for the given SignalItem, with the // given Condition. -func (nsa NextSignalAspects) SetupTriggers(item *SignalItem, params []string) {} +func (nsa NextSignalAspects) SetupTriggers(_ *SignalItem, _ []string) {} // --------------------------------------------------------------------------------------------------------------- @@ -321,7 +321,7 @@ func (resa RouteExitSignalAspects) Code() string { } // Solve returns if the condition is met for the given SignalItem and parameters -func (resa RouteExitSignalAspects) Solve(item *SignalItem, values []string, params []string) bool { +func (resa RouteExitSignalAspects) Solve(item *SignalItem, values []string, _ []string) bool { if item.nextActiveRoute == nil { return false } @@ -338,7 +338,7 @@ func (resa RouteExitSignalAspects) Solve(item *SignalItem, values []string, para // SetupTriggers installs needed triggers for the given SignalItem, with the // given Condition. -func (resa RouteExitSignalAspects) SetupTriggers(item *SignalItem, params []string) {} +func (resa RouteExitSignalAspects) SetupTriggers(_ *SignalItem, _ []string) {} // --------------------------------------------------------------------------------------------------------------- diff --git a/simulation/simulation.go b/simulation/simulation.go index a8a67aa..3687b2e 100644 --- a/simulation/simulation.go +++ b/simulation/simulation.go @@ -54,13 +54,13 @@ type Simulation struct { SignalLib SignalLibrary TrackItems map[string]TrackItem Places map[string]*Place - Options Options + Options *Options Routes map[string]*Route TrainTypes map[string]*TrainType Services map[string]*Service Trains []*Train MessageLogger *MessageLogger - EventChan chan *Event + EventChan chan Event clockTicker *time.Ticker stopChan chan bool @@ -73,7 +73,7 @@ func (sim *Simulation) UnmarshalJSON(data []byte) error { type auxSim struct { TrackItems map[string]json.RawMessage - Options Options + Options *Options SignalLib SignalLibrary `json:"signalLibrary"` Routes map[string]*Route `json:"routes"` TrainTypes map[string]*TrainType `json:"trainTypes"` @@ -82,7 +82,7 @@ func (sim *Simulation) UnmarshalJSON(data []byte) error { MessageLogger *MessageLogger `json:"messageLogger"` } - sim.EventChan = make(chan *Event) + sim.EventChan = make(chan Event) sim.stopChan = make(chan bool) var rawSim auxSim @@ -286,7 +286,7 @@ func (sim *Simulation) Start() { } sim.started = true go sim.run() - sim.sendEvent(&Event{Name: StateChangedEvent, Object: BoolObject{Value: true}}) + sim.sendEvent(Event{Name: StateChangedEvent, Object: BoolObject{Value: true}}) Logger.Info("Simulation started") } @@ -297,13 +297,12 @@ func (sim *Simulation) run() { select { case <-sim.stopChan: clockTicker.Stop() - sim.sendEvent(&Event{Name: StateChangedEvent, Object: BoolObject{Value: false}}) + sim.sendEvent(Event{Name: StateChangedEvent, Object: BoolObject{Value: false}}) Logger.Info("Simulation paused") return case <-clockTicker.C: sim.increaseTime(timeStep) - sim.sendEvent(&Event{Name: ClockEvent, Object: sim.Options.CurrentTime}) - sim.updateTrains() + sim.sendEvent(Event{Name: ClockEvent, Object: sim.Options.CurrentTime()}) } } } @@ -321,15 +320,20 @@ func (sim *Simulation) IsStarted() bool { // sendEvent sends the given event on the event channel to notify clients. // Sending is done asynchronously so as not to block. -func (sim *Simulation) sendEvent(evt *Event) { - sim.EventChan <- evt +func (sim *Simulation) sendEvent(evt Event) { + go func() { + sim.EventChan <- evt + }() } // increaseTime adds the step to the simulation time. func (sim *Simulation) increaseTime(step time.Duration) { - sim.Options.CurrentTime.Lock() - defer sim.Options.CurrentTime.Unlock() - sim.Options.CurrentTime = sim.Options.CurrentTime.Add(time.Duration(sim.Options.TimeFactor) * step) + sim.Options.IncreaseTime(time.Duration(sim.Options.TimeFactor) * step) +} + +// ProcessTimeStep is called from the hub goroutine to update the simulation after a time change +func (sim *Simulation) ProcessTimeStep() { + sim.updateTrains() } // checks that all TrackItems are linked together. @@ -371,7 +375,7 @@ func (sim *Simulation) checkTrackItemsLinks() error { // updateTrains update all trains information such as status, position, speed, etc. func (sim *Simulation) updateTrains() { for _, train := range sim.Trains { - train.activate(sim.Options.CurrentTime) + train.activate(sim.Options.CurrentTime()) if !train.IsActive() { continue } @@ -382,7 +386,7 @@ func (sim *Simulation) updateTrains() { // updateScore updates the score by adding penalty and notifiying clients func (sim *Simulation) updateScore(penalty int) { sim.Options.CurrentScore += penalty - sim.sendEvent(&Event{ + sim.sendEvent(Event{ Name: OptionsChangedEvent, Object: sim.Options, }) diff --git a/simulation/simulation_test.go b/simulation/simulation_test.go index 281684d..e8cca62 100644 --- a/simulation/simulation_test.go +++ b/simulation/simulation_test.go @@ -61,23 +61,25 @@ func TestSimulationRun(t *testing.T) { sim.Trains[0].AppearTime = simulation.ParseTime("05:00:00") So(err, ShouldBeNil) Convey("Starting and stopping the simulation should work", func() { - So(sim.Options.CurrentTime, ShouldResemble, simulation.ParseTime("06:00:00")) + So(sim.Options.CurrentTime(), ShouldResemble, simulation.ParseTime("06:00:00")) So(sim.Trains[0].TrainHead.TrackItemID, ShouldEqual, "2") So(sim.Trains[0].TrainHead.PreviousItemID, ShouldEqual, "1") So(sim.Trains[0].TrainHead.PositionOnTI, ShouldEqual, 3) So(sim.TrackItems["3"].(*simulation.SignalItem).ActiveAspect().Name, ShouldEqual, "UK_CLEAR") sim.Start() time.Sleep(600 * time.Millisecond) + // We need to do this manually because there is no hub + sim.ProcessTimeStep() sim.Pause() sim.Options.TrackCircuitBased = true - So(sim.Options.CurrentTime, ShouldResemble, simulation.ParseTime("06:00:02.5")) + So(sim.Options.CurrentTime(), ShouldResemble, simulation.ParseTime("06:00:02.5")) So(sim.Trains[0].TrainHead.TrackItemID, ShouldEqual, "2") So(sim.Trains[0].TrainHead.PreviousItemID, ShouldEqual, "1") So(sim.Trains[0].TrainHead.PositionOnTI, ShouldEqual, 18.625) So(sim.Trains[0].Speed, ShouldEqual, 6.25) time.Sleep(600 * time.Millisecond) - So(sim.Options.CurrentTime, ShouldResemble, simulation.ParseTime("06:00:02.5")) - So(sim.Options.CurrentTime, ShouldResemble, simulation.ParseTime("06:00:02.5")) + So(sim.Options.CurrentTime(), ShouldResemble, simulation.ParseTime("06:00:02.5")) + So(sim.Options.CurrentTime(), ShouldResemble, simulation.ParseTime("06:00:02.5")) So(sim.Trains[0].TrainHead.TrackItemID, ShouldEqual, "2") So(sim.Trains[0].TrainHead.PreviousItemID, ShouldEqual, "1") So(sim.Trains[0].TrainHead.PositionOnTI, ShouldEqual, 18.625) @@ -90,7 +92,10 @@ func TestSimulationRun(t *testing.T) { So(err, ShouldBeNil) So(sim.TrackItems["5"].(*simulation.SignalItem).ActiveAspect().Name, ShouldEqual, "UK_DANGER") sim.Start() - time.Sleep(7 * time.Second) + for i := 0; i < 14; i++ { + time.Sleep(500 * time.Millisecond) + sim.ProcessTimeStep() + } sim.Pause() So(sim.TrackItems["5"].(*simulation.SignalItem).ActiveAspect().Name, ShouldEqual, "UK_CAUTION") So(sim.TrackItems["3"].(*simulation.SignalItem).ActiveAspect().Name, ShouldEqual, "UK_CLEAR") diff --git a/simulation/time.go b/simulation/time.go index 2444f6f..7e251a1 100644 --- a/simulation/time.go +++ b/simulation/time.go @@ -22,7 +22,6 @@ import ( "encoding/json" "fmt" "math/rand" - "sync" "time" ) @@ -130,7 +129,6 @@ func (dg DelayGenerator) IsNull() bool { // // Valid Time objects start on 0000-01-02. type Time struct { - sync.RWMutex time.Time } diff --git a/simulation/track_items.go b/simulation/track_items.go index 2090e2a..ac648cf 100644 --- a/simulation/track_items.go +++ b/simulation/track_items.go @@ -21,7 +21,6 @@ package simulation import ( "encoding/json" "fmt" - "sync" ) // bigFloat is a large number used for the length of an EndItem. It must be bigger @@ -106,7 +105,7 @@ func (i ItemInconsistentLinkError) Error() string { // // Every TrackItem has an Origin() Point defined by its X and Y values. type TrackItem interface { - // routeID returns the unique routeID of this TrackItem, which is the index of this + // ID returns the unique ID of this TrackItem, which is the index of this // item in the Simulation's TrackItems map. ID() string @@ -237,11 +236,10 @@ type trackStruct struct { selected bool trainEndsFW map[*Train]float64 trainEndsBK map[*Train]float64 - trainEndMutex sync.RWMutex triggers []func(TrackItem) } -// routeID returns the unique routeID of this TrackItem, which is the index of this +// ID returns the unique ID of this TrackItem, which is the index of this // item in the Simulation's TrackItems map. func (t *trackStruct) ID() string { return t.tsId @@ -324,7 +322,7 @@ func (t *trackStruct) TrackCode() string { // // The second argument will return a ItemsNotLinkedError if the given // precedingItem is not linked to this item. -func (t *trackStruct) FollowingItem(precedingItem TrackItem, dir PointDirection) (TrackItem, error) { +func (t *trackStruct) FollowingItem(precedingItem TrackItem, _ PointDirection) (TrackItem, error) { if precedingItem == TrackItem(t).PreviousItem() { return t.NextItem(), nil } @@ -368,7 +366,7 @@ func (t *trackStruct) underlying() *trackStruct { func (t *trackStruct) setActiveRoute(r *Route, previous TrackItem) { t.activeRoute = r t.arPreviousItem = previous - t.simulation.sendEvent(&Event{ + t.simulation.sendEvent(Event{ Name: TrackItemChangedEvent, Object: t.full(), }) @@ -385,14 +383,14 @@ func (t *trackStruct) ActiveRoutePreviousItem() TrackItem { } // trainHeadActions performs the actions to be done when a train head reaches this TrackItem -func (t *trackStruct) trainHeadActions(train *Train) { +func (t *trackStruct) trainHeadActions(_ *Train) { for _, trigger := range t.triggers { trigger(t) } } // trainTailActions performs the actions to be done when a train tail reaches this TrackItem -func (t *trackStruct) trainTailActions(train *Train) { +func (t *trackStruct) trainTailActions(_ *Train) { for _, trigger := range t.triggers { trigger(t) } @@ -421,8 +419,6 @@ func (t *trackStruct) releaseRouteBehind() { // TrainPresent returns true if at least one train is present on this TrackItem func (t *trackStruct) TrainPresent() bool { - t.trainEndMutex.RLock() - defer t.trainEndMutex.RUnlock() return len(t.trainEndsFW)+len(t.trainEndsBK) > 0 } @@ -430,7 +426,7 @@ func (t *trackStruct) TrainPresent() bool { func (t *trackStruct) resetActiveRoute() { t.activeRoute = nil t.arPreviousItem = nil - t.simulation.sendEvent(&Event{ + t.simulation.sendEvent(Event{ Name: TrackItemChangedEvent, Object: t.full(), }) @@ -445,8 +441,6 @@ func (t *trackStruct) IsOnPosition(pos Position) bool { // train tail) of the closest train when on pos. If no train is on this item, the // distance will be 0, and the second argument will be false. func (t *trackStruct) DistanceToTrainEnd(pos Position) (float64, bool) { - t.trainEndMutex.RLock() - defer t.trainEndMutex.RUnlock() var mdSet bool minDist := bigFloat if pos.PreviousItemID == t.PreviousTiID { @@ -487,8 +481,6 @@ func (t *trackStruct) Equals(ti TrackItem) bool { // initialize this track item func (t *trackStruct) initialize() error { - t.trainEndMutex.Lock() - defer t.trainEndMutex.Unlock() t.trainEndsFW = make(map[*Train]float64) t.trainEndsBK = make(map[*Train]float64) return nil @@ -500,13 +492,13 @@ func (t *trackStruct) full() TrackItem { } // MarshalJSON method for trackStruct -func (t *trackStruct) MarshalJSON() ([]byte, error) { +func (t trackStruct) MarshalJSON() ([]byte, error) { ai := t.asJSONStruct() return json.Marshal(ai) } // asJSONStruct returns this trackStruct as a jsonTrackStruct -func (t *trackStruct) asJSONStruct() jsonTrackStruct { +func (t trackStruct) asJSONStruct() jsonTrackStruct { var arID, arpiID string if t.activeRoute != nil { arID = t.activeRoute.ID() @@ -514,8 +506,6 @@ func (t *trackStruct) asJSONStruct() jsonTrackStruct { if t.arPreviousItem != nil { arpiID = t.arPreviousItem.ID() } - t.trainEndMutex.RLock() - defer t.trainEndMutex.RUnlock() tEndsFW := make(map[string]float64) for t, p := range t.trainEndsFW { tEndsFW[t.ID()] = p @@ -643,7 +633,7 @@ func (ei *EndItem) Type() TrackItemType { return TypeEnd } -// RealLength() is the length in meters that this TrackItem has in real life track length +// RealLength is the length in meters that this TrackItem has in real life track length func (ei *EndItem) RealLength() float64 { return bigFloat } @@ -655,7 +645,7 @@ func (ei *EndItem) MarshalJSON() ([]byte, error) { var _ TrackItem = new(EndItem) -// PlatformItem's are usually represented as a colored rectangle on the scene to +// PlatformItem are usually represented as a colored rectangle on the scene to // symbolise the platform. This colored rectangle can permit user interaction. type PlatformItem struct { LineItem diff --git a/simulation/track_points.go b/simulation/track_points.go index 925ad90..98ef34c 100644 --- a/simulation/track_points.go +++ b/simulation/track_points.go @@ -192,7 +192,7 @@ func (pi *PointsItem) setActiveRoute(r *Route, previous TrackItem) { } // Send event for pairedItem if pi.PairedItem() != nil { - pi.simulation.sendEvent(&Event{ + pi.simulation.sendEvent(Event{ Name: TrackItemChangedEvent, Object: pi.PairedItem(), }) diff --git a/simulation/track_signals.go b/simulation/track_signals.go index 803f450..66e82c3 100644 --- a/simulation/track_signals.go +++ b/simulation/track_signals.go @@ -290,7 +290,7 @@ func (si *SignalItem) SignalType() *SignalType { return si.simulation.SignalLib.Types[si.SignalTypeCode] } -// Reversed() return true if the SignalItem is for trains coming from the right +// Reversed return true if the SignalItem is for trains coming from the right func (si *SignalItem) Reversed() bool { return si.Reverse } @@ -316,7 +316,7 @@ func (si *SignalItem) setActiveRoute(r *Route, previous TrackItem) { // setTrainID sets the train associated with this signal train to display in berth. func (si *SignalItem) setTrain(t *Train) { si.train = t - si.simulation.sendEvent(&Event{ + si.simulation.sendEvent(Event{ Name: TrackItemChangedEvent, Object: si, }) @@ -439,7 +439,7 @@ func (si *SignalItem) updateSignalState(previous ...bool) { si.activeAspect = signalItemManager.GetAspect(si) } if !oldAspect.Equals(si.activeAspect) { - si.simulation.sendEvent(&Event{ + si.simulation.sendEvent(Event{ Name: SignalaspectChangedEvent, Object: si, }) @@ -449,7 +449,7 @@ func (si *SignalItem) updateSignalState(previous ...bool) { if previousSignal != nil { previousSignal.updateSignalState(append(previous, true)...) } - si.simulation.sendEvent(&Event{ + si.simulation.sendEvent(Event{ Name: TrackItemChangedEvent, Object: si, }) @@ -476,7 +476,7 @@ func (si *SignalItem) resetPreviousActiveRoute(r *Route) { } // MarshalJSON method for SignalItem -func (si *SignalItem) MarshalJSON() ([]byte, error) { +func (si SignalItem) MarshalJSON() ([]byte, error) { type jsonSignalItem struct { jsonTrackStruct Xb float64 `json:"xn"` diff --git a/simulation/train_types.go b/simulation/train_types.go index 31aa6ae..f81221f 100644 --- a/simulation/train_types.go +++ b/simulation/train_types.go @@ -49,7 +49,7 @@ func (tt *TrainType) initialize(code string) { tt.code = code } -// Elements() returns the train types this TrainType is composed of. +// Elements returns the train types this TrainType is composed of. func (tt *TrainType) Elements() []*TrainType { res := make([]*TrainType, 0) for _, code := range tt.ElementsStr { diff --git a/simulation/trains.go b/simulation/trains.go index 5619767..c30e4b7 100644 --- a/simulation/trains.go +++ b/simulation/trains.go @@ -67,7 +67,7 @@ const minRunningSpeed float64 = 0.25 // Train is a stock of `TrainType` running on a track at a certain speed and to which // is assigned a `Service`. type Train struct { - trainID string `json:"-"` + trainID string AppearTime Time `json:"appearTime"` InitialDelay DelayGenerator `json:"initialDelay"` InitialSpeed float64 `json:"initialSpeed"` @@ -112,6 +112,15 @@ func (t *Train) initialize(id string) { if t.trainManager == nil { t.trainManager = defaultTrainManager } + if t.Status == Running || t.Status == Stopped || t.Status == Waiting { + // Signal actions update + t.signalActions = []SignalAction{{ + Target: ASAP, + Speed: VeryHighSpeed, + }} + t.setActionIndex(0) + t.updateSignalActions() + } } // Service returns a pointer to the Service assigned to this Train, or nil if no @@ -218,7 +227,7 @@ func (t *Train) advance(timeElapsed time.Duration) { t.TrainHead = t.TrainHead.Add(advanceLength) t.updateStatus(timeElapsed) t.executeActions(advanceLength) - t.simulation.sendEvent(&Event{ + t.simulation.sendEvent(Event{ Name: TrainChangedEvent, Object: t, }) @@ -255,7 +264,7 @@ func (t *Train) executeActions(advanceLength float64) { t.logAndScoreTrainExited() } for ti := range toNotify { - t.simulation.sendEvent(&Event{ + t.simulation.sendEvent(Event{ Name: TrackItemChangedEvent, Object: ti, }) @@ -265,8 +274,6 @@ func (t *Train) executeActions(advanceLength float64) { // updateItemWithTrainHead updates the knowledge of this trackItem about this train's Head, // knowing that this item is between the former head and the current head of the train. func (t *Train) updateItemWithTrainHead(ti TrackItem) { - ti.underlying().trainEndMutex.Lock() - defer ti.underlying().trainEndMutex.Unlock() ti.underlying().trainEndsFW[t] = ti.RealLength() ti.underlying().trainEndsBK[t] = 0 if t.simulation.Options.TrackCircuitBased { @@ -284,8 +291,6 @@ func (t *Train) updateItemWithTrainHead(ti TrackItem) { // updateItemWithTrainTail updates the knowledge of this trackItem about this train's Tail, // knowing that this item is between the former tail and the current tail of the train. func (t *Train) updateItemWithTrainTail(ti TrackItem) { - ti.underlying().trainEndMutex.Lock() - defer ti.underlying().trainEndMutex.Unlock() if !ti.Equals(t.TrainHead.TrackItem()) { delete(ti.underlying().trainEndsBK, t) delete(ti.underlying().trainEndsFW, t) @@ -379,7 +384,7 @@ func (t *Train) updateSignalActions() { t.lastSignal = nextSignal } - currentTime := t.simulation.Options.CurrentTime + currentTime := t.simulation.Options.CurrentTime() if math.Abs(t.Speed-t.ApplicableAction().Speed) < 0.1 { // We have achieved the action's target speed. if t.actionTime.IsZero() { @@ -483,7 +488,7 @@ func (t *Train) AssignService(srv string) error { } else { t.Status = Running } - t.simulation.sendEvent(&Event{ + t.simulation.sendEvent(Event{ Name: TrainChangedEvent, Object: t, }) @@ -555,7 +560,7 @@ func (t *Train) updateStatus(timeElapsed time.Duration) { // Train just stopped t.Status = Stopped t.StoppedTime = 0 - t.simulation.sendEvent(&Event{ + t.simulation.sendEvent(Event{ Name: TrainStoppedAtStationEvent, Object: t, }) @@ -567,7 +572,7 @@ func (t *Train) updateStatus(timeElapsed time.Duration) { return } // Train is already stopped at the place - if line.ScheduledDepartureTime.Sub(t.simulation.Options.CurrentTime) > 0 || + if line.ScheduledDepartureTime.Sub(t.simulation.Options.CurrentTime()) > 0 || t.StoppedTime < t.minStopTime || line.ScheduledDepartureTime.IsZero() { // Conditions to depart are not met @@ -583,7 +588,7 @@ func (t *Train) updateStatus(timeElapsed time.Duration) { if t.TrainHead.TrackItem().Place().PlaceCode != t.Service().Lines[t.NextPlaceIndex].PlaceCode { // The first scheduled place of this new service is not here, so we depart t.Status = Running - t.simulation.sendEvent(&Event{ + t.simulation.sendEvent(Event{ Name: TrainDepartedFromStationEvent, Object: t, }) @@ -600,7 +605,7 @@ func (t *Train) updateStatus(timeElapsed time.Duration) { } // There are still places to call at t.Status = Running - t.simulation.sendEvent(&Event{ + t.simulation.sendEvent(Event{ Name: TrainDepartedFromStationEvent, Object: t, }) @@ -635,7 +640,7 @@ func (t *Train) logAndScoreTrainStoppedAtStation() { t.ServiceCode, place.Name(), actualPlatform, plannedPlatform), simulationMsg) } scheduledArrivalTime := serviceLine.ScheduledArrivalTime - currentTime := sim.Options.CurrentTime + currentTime := sim.Options.CurrentTime() delay := currentTime.Sub(scheduledArrivalTime) if delay > time.Minute { playerDelay := delay - t.effInitialDelay