async def _broadcaster_evaluate(self, addr, commands): search_replies = [] version_requested = False for command in commands: if isinstance(command, ca.VersionRequest): version_requested = True if isinstance(command, ca.SearchRequest): pv_name = command.name try: known_pv = self[pv_name] is not None except KeyError: known_pv = False if known_pv: # responding with an IP of `None` tells client to get IP # address from the datagram. search_replies.append( ca.SearchResponse(self.port, None, command.cid, ca.DEFAULT_PROTOCOL_VERSION)) else: if command.reply == ca.DO_REPLY: search_replies.append( ca.NotFoundResponse( version=ca.DEFAULT_PROTOCOL_VERSION, cid=command.cid)) else: # Not a known PV and no reply required ... if search_replies: if version_requested: bytes_to_send = self.broadcaster.send(ca.VersionResponse(13), *search_replies) else: bytes_to_send = self.broadcaster.send(*search_replies) for udp_sock in self.udp_socks.values(): await udp_sock.sendto(bytes_to_send, addr)
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