def __init__(self, factory, socket, *args, **kwargs): super(CliRosBridgeProtocol, self).__init__(*args, **kwargs) self.factory = factory self.socket = socket # According to docs, exactly one send and one receive is supported on each ClientWebSocket object in parallel. # https://msdn.microsoft.com/en-us/library/system.net.websockets.clientwebsocket.receiveasync(v=vs.110).aspx # So we configure the semaphore to allow for 2 concurrent requests # User-code might still end up in a race if multiple requests are triggered from different threads self.semaphore = SemaphoreSlim(2)
class CliRosBridgeProtocol(RosBridgeProtocol): """Implements the ROS Bridge protocol on top of CLI WebSockets. This implementation is mainly intended to be used on IronPython implementations and makes use of the Tasks library of .NET for most internal scheduling and cancellation signals.""" def __init__(self, factory, socket, *args, **kwargs): super(CliRosBridgeProtocol, self).__init__(*args, **kwargs) self.factory = factory self.socket = socket # According to docs, exactly one send and one receive is supported on each ClientWebSocket object in parallel. # https://msdn.microsoft.com/en-us/library/system.net.websockets.clientwebsocket.receiveasync(v=vs.110).aspx # So we configure the semaphore to allow for 2 concurrent requests # User-code might still end up in a race if multiple requests are triggered from different threads self.semaphore = SemaphoreSlim(2) def on_open(self, task): """Triggered when the socket connection has been established. This will kick-start the listening thread.""" LOGGER.info('Connection to ROS MASTER ready.') self.factory.ready(self) self.factory.manager.call_in_thread(self.start_listening) def receive_chunk_async(self, task_result, context): """Handle the reception of a message chuck asynchronously.""" try: if task_result: result = task_result.Result if result.MessageType == WebSocketMessageType.Close: LOGGER.info( 'WebSocket connection closed: [Code=%s] Description=%s', result.CloseStatus, result.CloseStatusDescription) return self.send_close() else: chunk = Encoding.UTF8.GetString(context['buffer'], 0, result.Count) context['content'].append(chunk) # Signal the listener thread if we're done parsing chunks if result.EndOfMessage: # NOTE: Once we reach the end of the message # we release the lock (Semaphore) self.semaphore.Release() # And signal the manual reset event context['mre'].Set() return task_result # NOTE: We will enter the lock (Semaphore) at the start of receive # to make sure we're accessing the socket read/writes at most from # two threads, one for receiving and one for sending if not task_result: self.semaphore.Wait(self.factory.manager.cancellation_token) receive_task = self.socket.ReceiveAsync( ArraySegment[Byte](context['buffer']), self.factory.manager.cancellation_token) receive_task.ContinueWith.Overloads[ Action[Task[WebSocketReceiveResult], object], object](self.receive_chunk_async, context) except Exception: LOGGER.exception( 'Exception on receive_chunk_async, processing will be aborted') if task_result and task_result.Exception: LOGGER.debug('Inner exception: %s', task_result.Exception) raise def start_listening(self): """Starts listening asynchronously while the socket is open. The inter-thread synchronization between this and the async reception threads is sync'd with a manual reset event.""" try: LOGGER.debug('About to start listening, socket state: %s', self.socket.State) while self.socket and self.socket.State == WebSocketState.Open: mre = ManualResetEventSlim(False) content = [] buffer = Array.CreateInstance(Byte, RECEIVE_CHUNK_SIZE) self.receive_chunk_async( None, dict(buffer=buffer, content=content, mre=mre)) LOGGER.debug('Waiting for messages...') try: mre.Wait(self.factory.manager.cancellation_token) except SystemError: LOGGER.debug( 'Cancelation detected on listening thread, exiting...') break try: message_payload = ''.join(content) LOGGER.debug('Message reception completed|<pre>%s</pre>', message_payload) self.on_message(message_payload) except Exception: LOGGER.exception( 'Exception on start_listening while trying to handle message received.' + 'It could indicate a bug in user code on message handlers. Message skipped.' ) except Exception: LOGGER.exception( 'Exception on start_listening, processing will be aborted') raise finally: LOGGER.debug('Leaving the listening thread') def send_close(self): """Trigger the closure of the websocket indicating normal closing process.""" if self.socket: close_task = self.socket.CloseAsync( WebSocketCloseStatus.NormalClosure, '', CancellationToken.None ) # noqa: E999 (disable flake8 error, which incorrectly parses None as the python keyword) self.factory.emit('close', self) # NOTE: Make sure reconnets are possible. # Reconnection needs to be handled on a higher layer. return close_task def send_chunk_async(self, task_result, message_data): """Send a message chuck asynchronously.""" try: if not task_result: self.semaphore.Wait(self.factory.manager.cancellation_token) message_buffer, message_length, chunks_count, i = message_data offset = SEND_CHUNK_SIZE * i is_last_message = (i == chunks_count - 1) if is_last_message: count = message_length - offset else: count = SEND_CHUNK_SIZE message_chunk = ArraySegment[Byte](message_buffer, offset, count) LOGGER.debug( 'Chunk %d of %d|From offset=%d, byte count=%d, Is last=%s', i + 1, chunks_count, offset, count, str(is_last_message)) task = self.socket.SendAsync( message_chunk, WebSocketMessageType.Text, is_last_message, self.factory.manager.cancellation_token) if not is_last_message: task.ContinueWith( self.send_chunk_async, [message_buffer, message_length, chunks_count, i + 1]) else: # NOTE: If we've reached the last chunck of the message # we can release the lock (Semaphore) again. task.ContinueWith(lambda _res: self.semaphore.Release()) return task except Exception: LOGGER.exception('Exception while on send_chunk_async') raise def send_message(self, payload): """Start sending a message over the websocket asynchronously.""" if self.socket.State != WebSocketState.Open: raise RosBridgeException( 'Connection is not open. Socket state: %s' % self.socket.State) try: message_buffer = Encoding.UTF8.GetBytes(payload) message_length = len(message_buffer) chunks_count = int( math.ceil(float(message_length) / SEND_CHUNK_SIZE)) send_task = self.send_chunk_async( None, [message_buffer, message_length, chunks_count, 0]) return send_task except Exception: LOGGER.exception('Exception while sending message') raise def dispose(self, *args): """Dispose the resources held by this protocol instance, i.e. socket.""" self.factory.manager.terminate() if self.socket: self.socket.Dispose() self.socket = None LOGGER.debug('Websocket disposed') def __del__(self): """Dispose correctly the connection.""" self.dispose()