Ejemplo n.º 1
0
 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)
Ejemplo n.º 2
0
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()