Example #1
0
async def controller_error_middleware(request: web.Request,
                                      handler) -> web.Response:
    try:
        return await handler(request)

    except web.HTTPError:  # pragma: no cover
        raise

    except Exception as ex:
        app = request.app
        message = strex(ex)
        debug = app['config']['debug']
        LOGGER.error(f'[{request.url}] => {message}', exc_info=debug)

        response = {'error': message}

        if debug:
            response['traceback'] = traceback.format_tb(ex.__traceback__)

        if isinstance(ex, BrewbloxException):
            http_error = ex.http_error
        else:  # pragma: no cover
            http_error = web.HTTPInternalServerError

        raise http_error(text=json.dumps(response),
                         content_type='application/json')
Example #2
0
async def connect_subprocess(proc: subprocess.Popen,
                             address: str) -> ConnectionResult:
    host = SUBPROCESS_HOST
    port = SUBPROCESS_PORT
    message = None

    # We just started a subprocess
    # Give it some time to get started and respond to the port
    for _ in range(SUBPROCESS_CONNECT_RETRY_COUNT):
        await asyncio.sleep(SUBPROCESS_CONNECT_INTERVAL_S)

        if proc.poll() is not None:
            raise ChildProcessError(
                f'Subprocess exited with return code {proc.returncode}')

        try:
            reader, writer = await asyncio.open_connection(host, port)
            return ConnectionResult(host=host,
                                    port=port,
                                    address=address,
                                    process=proc,
                                    reader=reader,
                                    writer=writer)
        except OSError as ex:
            message = strex(ex)
            LOGGER.debug(f'Subprocess connection error: {message}')

    # Kill off leftovers
    with suppress(Exception):
        proc.terminate()

    raise ConnectionError(message)
Example #3
0
    async def _broadcast(self):
        LOGGER.info(f'Starting {self}')

        try:
            api = ObjectApi(self.app)
            spark_status: status.SparkStatus = status.get_status(self.app)

        except Exception as ex:
            LOGGER.error(strex(ex), exc_info=True)
            raise ex

        while True:
            try:
                await spark_status.synchronized.wait()
                await asyncio.sleep(PUBLISH_INTERVAL_S)

                if not self._queues:
                    self._current = None
                    continue

                self._current = await api.all()
                coros = [q.put(self._current) for q in self._queues]
                await asyncio.wait_for(
                    asyncio.gather(*coros, return_exceptions=True),
                    PUBLISH_INTERVAL_S)

            except asyncio.CancelledError:
                break

            except Exception as ex:
                self._current = None
                warnings.warn(f'{self} encountered an error: {strex(ex)}')
Example #4
0
    async def decode(
        self,
        obj_type: ObjType_,
        values: Optional[Encoded_] = ...,
        opts: Optional[CodecOpts] = None
    ) -> Union[ObjType_, Tuple[ObjType_, Decoded_]]:
        """
        Decodes given data to a Python-compatible type.

        Does not guarantee perfect symmetry with `encode()`, only symmetric compatibility.
        `encode()` can correctly interpret the return values of `decode()`, and vice versa.

        Args:
            obj_type (ObjType_):
                The unique identifier of the codec type.
                This determines how `values` are decoded.

            values (Optional[Encoded_]):
                Encoded representation of the message.
                If not set, only decoded object type will be returned.

            opts (Optional[CodecOpts]):
                Additional options that are passed to the transcoder.

        Returns:
            ObjType_:
                If `values` is not set, only decoded object type is returned.

            Tuple[ObjType_, Decoded_]:
                Python-compatible values of both object type and values.
        """
        if not isinstance(values, (bytes, list, type(...))):
            raise TypeError(
                f'Unable to decode [{type(values).__name__}] values')

        if opts is not None and not isinstance(opts, CodecOpts):
            raise TypeError(f'Invalid codec opts: {opts}')

        type_name = obj_type
        no_content = values == ...

        try:
            opts = opts or CodecOpts()
            trc = Transcoder.get(obj_type, self._mod)
            type_name = trc.type_str()
            return type_name if no_content else (type_name,
                                                 trc.decode(values, opts))

        except asyncio.CancelledError:  # pragma: no cover
            raise

        except Exception as ex:
            msg = strex(ex)
            LOGGER.debug(msg, exc_info=True)
            return 'UnknownType' if no_content else ('ErrorObject', {
                'error': msg,
                'type': type_name
            })
Example #5
0
    async def decode(
        self,
        identifier: Identifier_,
        data: Optional[Union[str, bytes]],
        opts: Optional[DecodeOpts] = None
    ) -> tuple[Identifier_, Optional[dict]]:
        """
        Decodes given data to a Python-compatible type.

        Does not guarantee perfect symmetry with `encode()`, only symmetric compatibility.
        `encode()` can correctly interpret the return values of `decode()`, and vice versa.

        Args:
            identifier (Identifier_):
                The unique identifier of the codec type.
                This determines how `values` are decoded.

            data (Optional[Union[str, bytes]]):
                Base-64 representation of the message bytes.
                A byte string is acceptable.

            opts (Optional[DecodeOpts]):
                Additional options that are passed to the transcoder.

        Returns:
            tuple[Identifier_, Optional[dict]]:
                Decoded identifier, and decoded data.
                Data will be None if it was None in args.
        """
        if data is not None and not isinstance(data, (str, bytes)):
            raise TypeError(f'Unable to decode [{type(data).__name__}]')

        if opts is not None and not isinstance(opts, DecodeOpts):
            raise TypeError(f'Invalid codec opts: {opts}')

        decoded_identifier = identifier

        try:
            opts = opts or DecodeOpts()
            trc = Transcoder.get(identifier, self._proto_proc)
            decoded_identifier = (trc.type_str(), trc.subtype_str())
            if data is None:
                return (decoded_identifier, None)
            else:
                data = data if isinstance(data, str) else data.decode()
                data = b''.join((b64decode(subs) for subs in data.split(',')))
                return (decoded_identifier, trc.decode(data, opts))

        except Exception as ex:
            msg = strex(ex)
            LOGGER.debug(msg, exc_info=True)
            if data is None:
                return (('UnknownType', None), None)
            else:
                return (('ErrorObject', None), {
                    'error': msg,
                    'identifier': decoded_identifier
                })
 async def prompt_handshake():
     while True:
         try:
             await asyncio.sleep(PING_INTERVAL_S)
             LOGGER.info('prompting handshake...')
             await self.commander.version()
         except Exception as ex:
             LOGGER.error(strex(ex))
             pass
Example #7
0
    async def get(self) -> web.WebSocketResponse:
        """
        Open a WebSocket to stream values from the database as they are added.

        When the socket is open, it supports commands for ranges and metrics.
        Each command starts a separate stream, but all streams share the same socket.
        Streams are identified by a command-defined ID.

        Tags: TimeSeries
        """
        self.ws = web.WebSocketResponse()
        streams = {}

        try:
            await self.ws.prepare(self.request)
            socket_closer.add(self.app, self.ws)

            async for msg in self.ws:  # pragma: no branch
                try:
                    cmd = TimeSeriesStreamCommand.parse_raw(msg.data)

                    existing: asyncio.Task = streams.pop(cmd.id, None)
                    existing and existing.cancel()

                    if cmd.command == 'ranges':
                        query = TimeSeriesRangesQuery(**cmd.query)
                        streams[cmd.id] = asyncio.create_task(
                            self._stream_ranges(cmd.id, query))

                    elif cmd.command == 'metrics':
                        query = TimeSeriesMetricsQuery(**cmd.query)
                        streams[cmd.id] = asyncio.create_task(
                            self._stream_metrics(cmd.id, query))

                    elif cmd.command == 'stop':
                        pass  # We already removed any pre-existing task from streams

                    # Pydantic validates commands
                    # This path should never be reached
                    else:  # pragma: no cover
                        raise NotImplementedError('Unknown command')

                except Exception as ex:
                    LOGGER.error(f'Stream read error {strex(ex)}')
                    await self.ws.send_json({
                        'error': strex(ex),
                        'message': msg,
                    })

        finally:
            socket_closer.discard(self.app, self.ws)
            # Coverage complains about next line -> exit not being covered
            for task in streams.values():  # pragma: no cover
                task.cancel()

        return self.ws
def error_response(request: web.Request, ex: Exception, status: int) -> web.Response:
    app = request.app
    message = strex(ex)
    debug = app['config']['debug']
    LOGGER.error(f'[{request.url}] => {message}', exc_info=debug)

    response = {'error': message}

    if debug:
        response['traceback'] = traceback.format_tb(ex.__traceback__)

    return web.json_response(response, status=status)
Example #9
0
async def check_remote(app: web.Application):
    if app['config']['volatile']:
        return
    num_attempts = 0
    while True:
        try:
            await http.session(app).get(f'{STORE_URL}/ping')
            return
        except Exception as ex:
            LOGGER.error(strex(ex))
            num_attempts += 1
            if num_attempts % 10 == 0:
                LOGGER.info(f'Waiting for datastore... ({strex(ex)})')
            await asyncio.sleep(RETRY_INTERVAL_S)
    async def flash(self) -> dict:  # pragma: no cover
        sender = ymodem.FileSender(self._notify)
        cmder = commander.fget(self.app)
        address = service_status.desc(self.app).device_address

        self._notify(
            f'Started updating {self.name}@{address} to version {self.version} ({self.date})'
        )

        try:
            if not service_status.desc(self.app).is_connected:
                self._notify('Controller is not connected. Aborting update.')
                raise exceptions.NotConnected()

            if self.simulation:
                raise NotImplementedError(
                    'Firmware updates not available for simulation controllers'
                )

            self._notify('Sending update command to controller')
            service_status.set_updating(self.app)
            await asyncio.sleep(FLUSH_PERIOD_S
                                )  # Wait for in-progress commands to finish
            await cmder.execute(commands.FirmwareUpdateCommand.from_args())

            self._notify('Shutting down normal communication')
            await cmder.shutdown(self.app)

            self._notify('Waiting for normal connection to close')
            await asyncio.wait_for(service_status.wait_disconnected(self.app),
                                   STATE_TIMEOUT_S)

            self._notify(f'Connecting to {address}')
            conn = await self._connect(address)

            with conn.autoclose():
                await asyncio.wait_for(sender.transfer(conn),
                                       TRANSFER_TIMEOUT_S)

        except Exception as ex:
            self._notify(f'Failed to update firmware: {strex(ex)}')
            raise exceptions.FirmwareUpdateFailed(strex(ex))

        finally:
            self._notify('Scheduling service reboot')
            await shutdown_soon(self.app, UPDATE_SHUTDOWN_DELAY_S)

        self._notify('Firmware updated!')
        return {'address': address, 'version': self.version}
    async def run(self):
        try:
            await service_status.wait_autoconnecting(self.app)
            service_status.set_connected(self.app, self._address)
            self.update_ticks()
            await self.welcome()

            while True:
                await asyncio.sleep(3600)

        except Exception as ex:  # pragma: no cover
            LOGGER.error(strex(ex))
            raise ex

        finally:
            service_status.set_disconnected(self.app)
Example #12
0
    async def encode(
        self,
        obj_type: ObjType_,
        values: Optional[Decoded_] = ...,
        opts: Optional[CodecOpts] = None
    ) -> Union[ObjType_, Tuple[ObjType_, Encoded_]]:
        """
        Encode given data to a serializable type.

        Does not guarantee perfect symmetry with `decode()`, only symmetric compatibility.
        `decode()` can correctly interpret the return values of `encode()`, and vice versa.

        Args:
            obj_type (ObjType_):
                The unique identifier of the codec type.
                This determines how `values` are encoded.

            values (Optional(Decoded_)):
                Decoded representation of the message.
                If not set, only encoded object type will be returned.

            opts (Optional[CodecOpts]):
                Additional options that are passed to the transcoder.

        Returns:
            ObjType_:
                If `values` is not set, only encoded object type is returned.

            Tuple[ObjType_, Encoded_]:
                Serializable values of both object type and values.
        """
        if not isinstance(values, (dict, type(...))):
            raise TypeError(
                f'Unable to encode [{type(values).__name__}] values')

        if opts is not None and not isinstance(opts, CodecOpts):
            raise TypeError(f'Invalid codec opts: {opts}')

        try:
            opts = opts or CodecOpts()
            trc = Transcoder.get(obj_type, self._mod)
            return trc.type_int() if values is ... \
                else (trc.type_int(), trc.encode(deepcopy(values), opts))
        except Exception as ex:
            msg = strex(ex)
            LOGGER.debug(msg, exc_info=True)
            raise exceptions.EncodeException(msg)
Example #13
0
async def connect_serial(address) -> Connection:
    LOGGER.info(f'Creating bridge for {address}')

    proc = subprocess.Popen([
        '/usr/bin/socat', 'tcp-listen:8332,reuseaddr,fork',
        f'file:{address},raw,echo=0,b{YMODEM_TRANSFER_BAUD_RATE}'
    ])

    last_err = None
    for _ in range(5):
        try:
            await asyncio.sleep(1)
            return await connect_tcp('localhost:8332', proc)
        except OSError as ex:
            last_err = strex(ex)
            LOGGER.debug(f'Subprocess connection error: {last_err}')

    raise ConnectionError(last_err)
Example #14
0
    async def encode(
        self,
        identifier: Identifier_,
        data: Optional[dict],
    ) -> tuple[Identifier_, Optional[str]]:
        """
        Encode given data to a serializable type.

        Does not guarantee perfect symmetry with `decode()`, only symmetric compatibility.
        `decode()` can correctly interpret the return values of `encode()`, and vice versa.

        Args:
            identifier (Identifier_):
                The fully qualified identifier of the codec type.
                This determines how `data` is encoded.

            data (Optional(dict)):
                Decoded representation of the message.
                If not set, only encoded object type will be returned.

        Returns:
            tuple[Identifier, Optional[str]]:
                Numeric identifier, and encoded data.
                Data will be None if it was None in args.
        """
        if data is not None and not isinstance(data, dict):
            raise TypeError(f'Unable to encode [{type(data).__name__}]')

        try:
            trc = Transcoder.get(identifier, self._proto_proc)
            encoded_identifier = (trc.type_int(), trc.subtype_int())
            if data is None:
                return (encoded_identifier, None)
            else:
                return (encoded_identifier,
                        b64encode(trc.encode(deepcopy(data))).decode())

        except Exception as ex:
            msg = strex(ex)
            LOGGER.debug(msg, exc_info=True)
            raise exceptions.EncodeException(msg)
Example #15
0
    async def flash(self) -> dict:  # pragma: no cover
        ota = ymodem.OtaClient(self._notify)
        cmder = commander.fget(self.app)
        status_desc = service_status.desc(self.app)
        address = status_desc.device_address
        platform = status_desc.device_info.platform

        self._notify(
            f'Started updating {self.name}@{address} to version {self.version} ({self.date})'
        )

        try:
            if not status_desc.is_connected:
                self._notify('Controller is not connected. Aborting update.')
                raise exceptions.NotConnected()

            if self.simulation:
                raise NotImplementedError(
                    'Firmware updates not available for simulation controllers'
                )

            self._notify('Preparing update')
            service_status.set_updating(self.app)
            await asyncio.sleep(FLUSH_PERIOD_S
                                )  # Wait for in-progress commands to finish

            if platform != 'esp32':  # pragma: no cover
                self._notify('Sending update command to controller')
                await cmder.execute(commands.FirmwareUpdateCommand.from_args())

            self._notify('Waiting for normal connection to close')
            await cmder.shutdown(self.app)
            await asyncio.wait_for(service_status.wait_disconnected(self.app),
                                   STATE_TIMEOUT_S)

            if platform == 'esp32':  # pragma: no cover
                # ESP connections will always be a TCP address
                host, _ = address.split(':')
                self._notify(f'Sending update prompt to {host}')
                self._notify(
                    'The Spark will now download and apply the new firmware')
                self._notify('The update is done when the service reconnects')
                fw_url = ESP_URL_FMT.format(**self.app['ini'])
                await http.session(self.app
                                   ).post(f'http://{host}:80/firmware_update',
                                          data=fw_url)

            else:
                self._notify(f'Connecting to {address}')
                conn = await ymodem.connect(address)

                with conn.autoclose():
                    await asyncio.wait_for(
                        ota.send(conn, f'firmware/brewblox-{platform}.bin'),
                        TRANSFER_TIMEOUT_S)
                    self._notify('Update done!')

        except Exception as ex:
            self._notify(f'Failed to update firmware: {strex(ex)}')
            raise exceptions.FirmwareUpdateFailed(strex(ex))

        finally:
            self._notify('Restarting service...')
            await shutdown_soon(self.app, UPDATE_SHUTDOWN_DELAY_S)

        return {'address': address, 'version': self.version}
    async def write(self, request_b64: Union[str, bytes]):  # pragma: no cover
        try:
            self.update_ticks()
            _, dict_content = await self._codec.decode(
                (codec.REQUEST_TYPE, None),
                request_b64,
            )
            request = EncodedRequest(**dict_content)
            payload = request.payload
            if payload:
                (in_blockType, _), in_content = await self._codec.decode(
                    (payload.blockType, payload.subtype),
                    payload.content,
                )
            else:
                in_blockType = 0
                in_content = None

            response = EncodedResponse(msgId=request.msgId,
                                       error=ErrorCode.OK,
                                       payload=[])

            if self.next_error:
                error = self.next_error.pop(0)
                if error is None:
                    return  # No response at all
                else:
                    response.error = error

            elif request.opcode in [
                    Opcode.NONE,
                    Opcode.VERSION,
            ]:
                await self.welcome()

            elif request.opcode in [Opcode.BLOCK_READ, Opcode.STORAGE_READ]:
                block = self._blocks.get(payload.blockId)
                if not block:
                    response.error = ErrorCode.INVALID_BLOCK_ID
                else:
                    (blockType, subtype), content = await self._codec.encode(
                        (block.type, None),
                        block.data,
                    )

                    response.payload = [
                        EncodedPayload(blockId=block.nid,
                                       blockType=blockType,
                                       subtype=subtype,
                                       content=content)
                    ]

            elif request.opcode in [
                    Opcode.BLOCK_READ_ALL,
                    Opcode.STORAGE_READ_ALL,
            ]:
                for block in self._blocks.values():
                    (blockType, subtype), content = await self._codec.encode(
                        (block.type, None),
                        block.data,
                    )
                    response.payload.append(
                        EncodedPayload(
                            blockId=block.nid,
                            blockType=blockType,
                            subtype=subtype,
                            content=content,
                        ))

            elif request.opcode == Opcode.BLOCK_WRITE:
                block = self._blocks.get(payload.blockId)
                if not block:
                    response.error = ErrorCode.INVALID_BLOCK_ID
                elif not in_content:
                    response.error = ErrorCode.INVALID_BLOCK
                elif in_blockType != block.type:
                    response.error = ErrorCode.INVALID_BLOCK_TYPE
                else:
                    block.data = in_content
                    (blockType, subtype), data = await self._codec.encode(
                        (block.type, None),
                        block.data,
                    )
                    response.payload = [
                        EncodedPayload(blockId=block.nid,
                                       blockType=blockType,
                                       subtype=subtype,
                                       data=data)
                    ]

            elif request.opcode == Opcode.BLOCK_CREATE:
                nid = payload.blockId
                block = self._blocks.get(nid)
                if block:
                    response.error = ErrorCode.BLOCK_NOT_CREATABLE
                elif nid > 0 and nid < const.USER_NID_START:
                    response.error = ErrorCode.BLOCK_NOT_CREATABLE
                elif not in_content:
                    response.error = ErrorCode.INVALID_BLOCK
                else:
                    nid = nid or next(self._id_counter)
                    block = FirmwareBlock(
                        nid=nid,
                        type=in_blockType,
                        data=in_content,
                    )
                    self._blocks[nid] = block
                    (blockType, subtype), content = await self._codec.encode(
                        (block.type, None),
                        block.data,
                    )
                    response.payload = [
                        EncodedPayload(
                            blockId=block.nid,
                            blockType=blockType,
                            subtype=subtype,
                            content=content,
                        )
                    ]

            elif request.opcode == Opcode.BLOCK_DELETE:
                nid = payload.blockId
                block = self._blocks.get(nid)
                if not block:
                    response.error = ErrorCode.INVALID_BLOCK_ID
                elif nid < const.USER_NID_START:
                    response.error = ErrorCode.BLOCK_NOT_DELETABLE
                else:
                    del self._blocks[nid]

            elif request.opcode == Opcode.BLOCK_DISCOVER:
                # Always return spark pins when discovering blocks
                block = self._blocks[const.SPARK_PINS_NID]
                (blockType, subtype), content = await self._codec.encode(
                    (block.type, None),
                    block.data,
                )
                response.payload = [
                    EncodedPayload(
                        blockId=block.nid,
                        blockType=blockType,
                        subtype=subtype,
                        content=content,
                    )
                ]

            elif request.opcode == Opcode.REBOOT:
                self._start_time = datetime.now()
                self.update_ticks()

            elif request.opcode == Opcode.CLEAR_BLOCKS:
                self._blocks = default_blocks()
                self.update_ticks()

            elif request.opcode == Opcode.CLEAR_WIFI:
                self._blocks[const.WIFI_SETTINGS_NID].data.clear()

            elif request.opcode == Opcode.FACTORY_RESET:
                self._blocks = default_blocks()
                self.update_ticks()

            elif request.opcode == Opcode.FIRMWARE_UPDATE:
                pass

            else:
                response.error = ErrorCode.INVALID_OPCODE

            _, response_b64 = await self._codec.encode(
                (codec.RESPONSE_TYPE, None), response.dict())
            await self._on_data(response_b64)

        except Exception as ex:
            LOGGER.error(strex(ex))
            raise ex
Example #17
0
async def controller_error_middleware(request: web.Request, handler: web.RequestHandler) -> web.Response:
    try:
        return await handler(request)
    except Exception as ex:
        LOGGER.error(f'REST error: {strex(ex)}', exc_info=request.app['config']['debug'])
        return web.json_response({'error': strex(ex)}, status=500)