func goodClient(b binder.Portal, expecting int, t *testing.T, wg *sync.WaitGroup) { changes := b.BaseVersion() + 1 seen := 0 for tform := range b.TransformReadChan() { seen++ if tform.Insert != fmt.Sprintf("%v", changes) { t.Errorf("Wrong order of transforms, expected %v, received %v", changes, tform.Insert) } changes++ } if seen != expecting { t.Errorf("Good client didn't receive all expected transforms: %v != %v", expecting, seen) } wg.Done() }
/* WebsocketHandler - Returns a websocket handler that routes new websockets to a curator. Use this with an HTTP server with the "golang.org/x/net/websocket" package. */ func WebsocketHandler( finder curator.Type, timeout time.Duration, logger log.Modular, stats metrics.Aggregator, ) func(ws *websocket.Conn) { return func(ws *websocket.Conn) { var err error var session binder.Portal defer func() { if err != nil { websocket.JSON.Send(ws, leapHTTPServerMessage{ Type: "error", Error: fmt.Sprintf("socket initialization failed: %v", err), }) } if err = ws.Close(); err != nil { logger.Errorf("Failed to close socket: %v\n", err) } stats.Decr("http.open_websockets", 1) }() stats.Incr("http.websocket.opened", 1) stats.Incr("http.open_websockets", 1) for session == nil && err == nil { var clientMsg leapHTTPClientMessage websocket.JSON.Receive(ws, &clientMsg) switch clientMsg.Command { case "create": if clientMsg.Document == nil { err = ErrInvalidDocument } else { session, err = finder.CreateDocument( clientMsg.UserID, clientMsg.Token, *clientMsg.Document, timeout) } case "read": if len(clientMsg.DocID) <= 0 { err = ErrInvalidDocument } else { session, err = finder.ReadDocument( clientMsg.UserID, clientMsg.Token, clientMsg.DocID, timeout) } case "edit": if len(clientMsg.DocID) <= 0 { err = ErrInvalidDocument } else { session, err = finder.EditDocument( clientMsg.UserID, clientMsg.Token, clientMsg.DocID, timeout) } case "ping": // Ignore and continue waiting for init message. default: err = fmt.Errorf( "first command must be init or ping, client sent: %v", clientMsg.Command, ) } } if session != nil && err == nil { version := session.BaseVersion() websocket.JSON.Send(ws, leapHTTPServerMessage{ Type: "document", Document: session.Document(), Version: &version, }) session.ReleaseDocument() // Begin serving websocket IO. serveWebsocketIO(ws, session, timeout, logger, stats) } } }
func serveWebsocketIO( ws *websocket.Conn, portal binder.Portal, timeout time.Duration, logger log.Modular, stats metrics.Aggregator, ) { defer portal.Exit(timeout) // Signal to close var incomingCloseInt uint32 outgoingCloseChan := make(chan struct{}) // Signals that goroutine is closing incomingClosedChan := make(chan struct{}) outgoingClosedChan := make(chan struct{}) // Loop incoming messages. go func() { var err error defer func() { if err != nil { websocket.JSON.Send(ws, leapSocketServerMessage{ Type: "error", Error: err.Error(), }) } close(incomingClosedChan) }() for atomic.LoadUint32(&incomingCloseInt) == 0 { var msg leapSocketClientMessage if socketErr := websocket.JSON.Receive(ws, &msg); socketErr != nil { return } switch msg.Command { case "submit": if msg.Transform == nil { err = errors.New("submit error: transform was nil") return } var ver int if ver, err = portal.SendTransform(*msg.Transform, timeout); err != nil { return } websocket.JSON.Send(ws, leapSocketServerMessage{ Type: "correction", Version: ver, }) case "update": if msg.Position != nil || len(msg.Message) > 0 { portal.SendMessage(binder.Message{ Content: msg.Message, Position: msg.Position, Active: true, }) } case "ping": // Do nothing default: err = errors.New("command not recognised") } } }() // Loop outgoing messages. go func() { defer close(outgoingClosedChan) for { select { case <-outgoingCloseChan: return case tform, open := <-portal.TransformReadChan(): if !open { return } websocket.JSON.Send(ws, leapSocketServerMessage{ Type: "transforms", Transforms: []text.OTransform{tform}, }) case msg, open := <-portal.UpdateReadChan(): if !open { return } websocket.JSON.Send(ws, leapSocketServerMessage{ Type: "update", Updates: []binder.ClientUpdate{msg}, }) } } }() // If one channel closes, close the other, if the socket is being closed then close both. select { case <-incomingClosedChan: close(outgoingCloseChan) <-outgoingClosedChan portal.SendMessage(binder.Message{ Active: false, }) case <-outgoingClosedChan: atomic.StoreUint32(&incomingCloseInt, 1) <-incomingClosedChan portal.SendMessage(binder.Message{ Active: false, }) } }