Example #1
0
    def __getitem__(self, pvname):
        try:
            return self.pvdb[pvname]
        except KeyError as ex:
            try:
                (rec_field, rec, field, mods) = ca.parse_record_field(pvname)
            except ValueError:
                raise ex

            if not field and not mods:
                # No field or modifiers, so there's nothing left to check
                raise

        # Without the modifiers, try 'record[.field]'
        try:
            inst = self.pvdb[rec_field]
        except KeyError:
            # Finally, access 'record', see if it has 'field'
            try:
                inst = self.pvdb[rec]
            except KeyError:
                raise CaprotoKeyError(f'Neither record nor field exists: '
                                      f'{rec_field}')

            try:
                inst = inst.get_field(field)
            except (AttributeError, KeyError):
                raise CaprotoKeyError(f'Neither record nor field exists: '
                                      f'{rec_field}')

            # Cache record.FIELD for later usage
            self.pvdb[rec_field] = inst
        return inst
Example #2
0
def test_parse_record(pvname, expected_tuple):
    parsed = ca.parse_record_field(pvname)
    print('parsed:  ', tuple(parsed))
    print('expected:', expected_tuple)
    assert tuple(parsed) == expected_tuple

    if parsed.modifiers:
        modifiers, filter_text = parsed.modifiers
        if filter_text:
            # smoke test these
            ca.parse_channel_filter(filter_text)
Example #3
0
def test_parse_record_bad_filters(pvname, expected_tuple):
    parsed = ca.parse_record_field(pvname)
    print('parsed:  ', tuple(parsed))
    print('expected:', expected_tuple)
    assert tuple(parsed) == expected_tuple

    modifiers, filter_text = parsed.modifiers
    try:
        filter_ = ca.parse_channel_filter(filter_text)
    except ValueError:
        # expected failure
        ...
    else:
        raise ValueError(f'Expected failure, instead returned {filter_}')
Example #4
0
    def __getitem__(self, pvname):
        try:
            return self.pvdb[pvname]
        except KeyError as ex:
            try:
                (rec_field, rec, field, mods) = ca.parse_record_field(pvname)
            except ValueError:
                raise ex from None

            if not field and not mods:
                # No field or modifiers, but a trailing '.' is valid
                return self.pvdb[rec]

        # Without the modifiers, try 'record[.field]'
        try:
            inst = self.pvdb[rec_field]
        except KeyError:
            # Finally, access 'record', see if it has 'field'
            try:
                inst = self.pvdb[rec]
            except KeyError:
                raise CaprotoKeyError(f'Neither record nor field exists: '
                                      f'{rec_field}')

            try:
                inst = inst.get_field(field)
            except (AttributeError, KeyError):
                raise CaprotoKeyError(f'Neither record nor field exists: '
                                      f'{rec_field}')

        # Verify the modifiers are usable BEFORE caching rec_field in the pvdb:
        if ca.RecordModifiers.long_string in (mods or {}):
            if inst.data_type not in (ChannelType.STRING,
                                      ChannelType.CHAR):
                raise CaprotoKeyError(
                    f'Long-string modifier not supported with types '
                    f'other than string or char ({inst.data_type})'
                )

        # Cache record.FIELD for later usage
        self.pvdb[rec_field] = inst
        return inst
Example #5
0
    async def _process_command(self, command):
        '''Process a command from a client, and return the server response'''
        tags = self._tags
        if command is ca.DISCONNECTED:
            raise DisconnectedCircuit()
        elif isinstance(command, ca.VersionRequest):
            to_send = [ca.VersionResponse(ca.DEFAULT_PROTOCOL_VERSION)]
        elif isinstance(command, ca.SearchRequest):
            pv_name = command.name
            try:
                self.context[pv_name]
            except KeyError:
                if command.reply == ca.DO_REPLY:
                    to_send = [
                        ca.NotFoundResponse(
                            version=ca.DEFAULT_PROTOCOL_VERSION,
                            cid=command.cid)
                    ]
                else:
                    to_send = []
            else:
                to_send = [
                    ca.SearchResponse(self.context.port, None, command.cid,
                                      ca.DEFAULT_PROTOCOL_VERSION)
                ]
        elif isinstance(command, ca.CreateChanRequest):
            pvname = command.name
            try:
                db_entry = self.context[pvname]
            except KeyError:
                self.log.debug('Client requested invalid channel name: %s',
                               pvname)
                to_send = [ca.CreateChFailResponse(cid=command.cid)]
            else:

                access = db_entry.check_access(self.client_hostname,
                                               self.client_username)

                modifiers = ca.parse_record_field(pvname).modifiers
                data_type = db_entry.data_type
                data_count = db_entry.max_length
                if ca.RecordModifiers.long_string in (modifiers or {}):
                    if data_type in (ChannelType.STRING, ):
                        data_type = ChannelType.CHAR
                        data_count = db_entry.long_string_max_length

                to_send = [
                    ca.AccessRightsResponse(cid=command.cid,
                                            access_rights=access),
                    ca.CreateChanResponse(data_type=data_type,
                                          data_count=data_count,
                                          cid=command.cid,
                                          sid=self.circuit.new_channel_id()),
                ]
        elif isinstance(command, ca.HostNameRequest):
            self.client_hostname = command.name
            to_send = []
        elif isinstance(command, ca.ClientNameRequest):
            self.client_username = command.name
            to_send = []
        elif isinstance(command, (ca.ReadNotifyRequest, ca.ReadRequest)):
            chan, db_entry = self._get_db_entry_from_command(command)
            try:
                data_type = command.data_type
            except ValueError:
                raise ca.RemoteProtocolError('Invalid data type')

            # If we are in the middle of processing a Write[Notify]Request,
            # allow a bit of time for that to (maybe) finish. Some requests
            # may take a long time, so give up rather quickly to avoid
            # introducing too much latency.
            await self.write_event.wait(timeout=WRITE_LOCK_TIMEOUT)

            read_data_type = data_type
            if chan.name.endswith('$'):
                try:
                    read_data_type = _LongStringChannelType(read_data_type)
                except ValueError:
                    # Not requesting a LONG_STRING type
                    ...

            metadata, data = await db_entry.auth_read(
                self.client_hostname,
                self.client_username,
                read_data_type,
                user_address=self.circuit.address,
            )

            old_version = self.circuit.protocol_version < 13
            if command.data_count > 0 or old_version:
                data = data[:command.data_count]

            # This is a pass-through if arr is None.
            data = apply_arr_filter(chan.channel_filter.arr, data)
            # If the timestamp feature is active swap the timestamp.
            # Information must copied because not all clients will have the
            # timestamp filter
            if chan.channel_filter.ts and command.data_type in ca.time_types:
                time_type = type(metadata)
                now = ca.TimeStamp.from_unix_timestamp(time.time())
                metadata = time_type(
                    **ChainMap({'stamp': now},
                               dict((field, getattr(metadata, field))
                                    for field, _ in time_type._fields_)))
            notify = isinstance(command, ca.ReadNotifyRequest)
            data_count = db_entry.calculate_length(data)
            to_send = [
                chan.read(data=data,
                          data_type=command.data_type,
                          data_count=data_count,
                          status=1,
                          ioid=command.ioid,
                          metadata=metadata,
                          notify=notify)
            ]
        elif isinstance(command, (ca.WriteRequest, ca.WriteNotifyRequest)):
            chan, db_entry = self._get_db_entry_from_command(command)
            client_waiting = isinstance(command, ca.WriteNotifyRequest)

            async def handle_write():
                '''Wait for an asynchronous caput to finish'''
                try:
                    write_status = await db_entry.auth_write(
                        self.client_hostname,
                        self.client_username,
                        command.data,
                        command.data_type,
                        command.metadata,
                        user_address=self.circuit.address)
                except Exception as ex:
                    self.log.exception('Invalid write request by %s (%s): %r',
                                       self.client_username,
                                       self.client_hostname, command)
                    cid = self.circuit.channels_sid[command.sid].cid
                    response_command = ca.ErrorResponse(
                        command,
                        cid,
                        status=ca.CAStatus.ECA_PUTFAIL,
                        error_message=('Python exception: {} {}'
                                       ''.format(type(ex).__name__, ex)))
                    await self.send(response_command)
                else:
                    if client_waiting:
                        if write_status is None:
                            # errors can be passed back by exceptions, and
                            # returning none for write_status can just be
                            # considered laziness
                            write_status = True

                        response_command = chan.write(
                            ioid=command.ioid,
                            status=write_status,
                            data_count=db_entry.length)
                        await self.send(response_command)
                finally:
                    maybe_awaitable = self.write_event.set()
                    # The curio backend makes this an awaitable thing.
                    if maybe_awaitable is not None:
                        await maybe_awaitable

            self.write_event.clear()
            await self._start_write_task(handle_write)
            to_send = []
        elif isinstance(command, ca.EventAddRequest):
            chan, db_entry = self._get_db_entry_from_command(command)
            # TODO no support for deprecated low/high/to

            read_data_type = command.data_type
            if chan.name.endswith('$'):
                try:
                    read_data_type = _LongStringChannelType(read_data_type)
                except ValueError:
                    # Not requesting a LONG_STRING type
                    ...

            sub = Subscription(mask=command.mask,
                               channel_filter=chan.channel_filter,
                               channel=chan,
                               circuit=self,
                               data_type=read_data_type,
                               data_count=command.data_count,
                               subscriptionid=command.subscriptionid,
                               db_entry=db_entry)
            sub_spec = SubscriptionSpec(db_entry=db_entry,
                                        data_type_name=read_data_type.name,
                                        mask=command.mask,
                                        channel_filter=chan.channel_filter)
            self.subscriptions[sub_spec].append(sub)
            self.context.subscriptions[sub_spec].append(sub)

            # If we are in the middle of processing a Write[Notify]Request,
            # allow a bit of time for that to (maybe) finish. Some requests
            # may take a long time, so give up rather quickly to avoid
            # introducing too much latency.
            if not self.write_event.is_set():
                await self.write_event.wait(timeout=WRITE_LOCK_TIMEOUT)

            await db_entry.subscribe(self.context.subscription_queue, sub_spec,
                                     sub)
            to_send = []
        elif isinstance(command, ca.EventCancelRequest):
            chan, db_entry = self._get_db_entry_from_command(command)
            removed = await self._cull_subscriptions(
                db_entry,
                lambda sub: sub.subscriptionid == command.subscriptionid)
            if removed:
                _, removed_sub = removed[0]
                data_count = removed_sub.data_count
            else:
                data_count = db_entry.length
            to_send = [
                chan.unsubscribe(command.subscriptionid,
                                 data_type=command.data_type,
                                 data_count=data_count)
            ]
        elif isinstance(command, ca.EventsOnRequest):
            # Immediately send most recent updates for all subscriptions.
            most_recent_updates = list(self.most_recent_updates.values())
            self.most_recent_updates.clear()
            if most_recent_updates:
                await self.send(*most_recent_updates)
            maybe_awaitable = self.events_on.set()
            # The curio backend makes this an awaitable thing.
            if maybe_awaitable is not None:
                await maybe_awaitable
            self.circuit.log.info("Client at %s:%d has turned events on.",
                                  *self.circuit.address)
            to_send = []
        elif isinstance(command, ca.EventsOffRequest):
            # The client has signaled that it does not think it will be able to
            # catch up to the backlog. Clear all updates queued to be sent...
            self.unexpired_updates.clear()
            # ...and tell the Context that any future updates from ChannelData
            # should not be added to this circuit's queue until further notice.
            self.events_on.clear()
            self.circuit.log.info("Client at %s:%d has turned events off.",
                                  *self.circuit.address)
            to_send = []
        elif isinstance(command, ca.ClearChannelRequest):
            chan, db_entry = self._get_db_entry_from_command(command)
            await self._cull_subscriptions(
                db_entry, lambda sub: sub.channel == command.sid)
            to_send = [chan.clear()]
        elif isinstance(command, ca.EchoRequest):
            to_send = [ca.EchoResponse()]
        if isinstance(command, ca.Message):
            tags['bytesize'] = len(command)
            self.log.debug("%r", command, extra=tags)
        return to_send