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
async def _process_command(self, command): '''Process a command from a client, and return the server response''' def get_db_entry(): chan = self.circuit.channels_sid[command.sid] db_entry = self.context[chan.name] return chan, db_entry if command is ca.DISCONNECTED: raise DisconnectedCircuit() elif isinstance(command, ca.VersionRequest): return [ca.VersionResponse(ca.DEFAULT_PROTOCOL_VERSION)] elif isinstance(command, ca.CreateChanRequest): db_entry = self.context[command.name] access = db_entry.check_access(self.client_hostname, self.client_username) return [ ca.AccessRightsResponse(cid=command.cid, access_rights=access), ca.CreateChanResponse(data_type=db_entry.data_type, data_count=len(db_entry), cid=command.cid, sid=self.circuit.new_channel_id()), ] elif isinstance(command, ca.HostNameRequest): self.client_hostname = command.name elif isinstance(command, ca.ClientNameRequest): self.client_username = command.name elif isinstance(command, (ca.ReadNotifyRequest, ca.ReadRequest)): chan, db_entry = get_db_entry() metadata, data = await db_entry.auth_read( self.client_hostname, self.client_username, command.data_type, user_address=self.circuit.address) # 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) return [ chan.read(data=data, data_type=command.data_type, data_count=len(data), status=1, ioid=command.ioid, metadata=metadata, notify=notify) ] elif isinstance(command, (ca.WriteRequest, ca.WriteNotifyRequest)): chan, db_entry = get_db_entry() 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_INTERNAL, error_message=('Python exception: {} {}' ''.format(type(ex).__name__, ex))) else: 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) if client_waiting: await self.send(response_command) await self._start_write_task(handle_write) # TODO pretty sure using the taskgroup will bog things down, # but it suppresses an annoying warning message, so... there elif isinstance(command, ca.EventAddRequest): chan, db_entry = get_db_entry() # TODO no support for deprecated low/high/to sub = Subscription(mask=command.mask, channel_filter=chan.channel_filter, channel=chan, circuit=self, data_type=command.data_type, data_count=command.data_count, subscriptionid=command.subscriptionid, db_entry=db_entry) sub_spec = SubscriptionSpec(db_entry=db_entry, data_type=command.data_type, mask=command.mask, channel_filter=chan.channel_filter) self.subscriptions[sub_spec].append(sub) self.context.subscriptions[sub_spec].append(sub) await db_entry.subscribe(self.context.subscription_queue, sub_spec) elif isinstance(command, ca.EventCancelRequest): chan, db_entry = get_db_entry() await self._cull_subscriptions( db_entry, lambda sub: sub.subscriptionid == command.subscriptionid) return [ chan.unsubscribe(command.subscriptionid, data_type=command.data_type) ] elif isinstance(command, ca.ClearChannelRequest): chan, db_entry = get_db_entry() await self._cull_subscriptions( db_entry, lambda sub: sub.channel == command.sid) return [chan.disconnect()] elif isinstance(command, ca.EchoRequest): return [ca.EchoResponse()]
async def _process_command(self, command): '''Process a command from a client, and return the server response''' def get_db_entry(): chan = self.circuit.channels_sid[command.sid] db_entry = self.context.pvdb[chan.name.decode(STR_ENC)] return chan, db_entry if command is ca.DISCONNECTED: raise DisconnectedCircuit() elif isinstance(command, ca.VersionRequest): return [ca.VersionResponse(13)] elif isinstance(command, ca.CreateChanRequest): db_entry = self.context.pvdb[command.name.decode(STR_ENC)] access = db_entry.check_access(self.client_hostname, self.client_username) return [ca.AccessRightsResponse(cid=command.cid, access_rights=access), ca.CreateChanResponse(data_type=db_entry.data_type, data_count=len(db_entry), cid=command.cid, sid=self.circuit.new_channel_id()), ] elif isinstance(command, ca.HostNameRequest): self.client_hostname = command.name.decode(STR_ENC) elif isinstance(command, ca.ClientNameRequest): self.client_username = command.name.decode(STR_ENC) elif isinstance(command, ca.ReadNotifyRequest): chan, db_entry = get_db_entry() metadata, data = await db_entry.auth_read( self.client_hostname, self.client_username, command.data_type) return [chan.read(data=data, data_type=command.data_type, data_count=len(data), status=1, ioid=command.ioid, metadata=metadata) ] elif isinstance(command, (ca.WriteRequest, ca.WriteNotifyRequest)): chan, db_entry = get_db_entry() 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) except Exception as ex: cid = self.circuit.channels_sid[command.sid].cid response_command = ca.ErrorResponse( command, cid, status_code=ca.ECA_INTERNAL.code_with_severity, error_message=('Python exception: {} {}' ''.format(type(ex).__name__, ex)) ) else: 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) if client_waiting: await self.send(response_command) await self.pending_tasks.spawn(handle_write, ignore_result=True) # TODO pretty sure using the taskgroup will bog things down, # but it suppresses an annoying warning message, so... there elif isinstance(command, ca.EventAddRequest): chan, db_entry = get_db_entry() # TODO no support for deprecated low/high/to sub = Subscription(mask=command.mask, channel=chan, circuit=self, data_type=command.data_type, data_count=command.data_count, subscriptionid=command.subscriptionid) sub_spec = SubscriptionSpec(db_entry=db_entry, data_type=command.data_type) self.subscriptions[sub_spec].append(sub) self.context.subscriptions[sub_spec].append(sub) await db_entry.subscribe(self.context.subscription_queue, sub_spec) elif isinstance(command, ca.EventCancelRequest): chan, db_entry = get_db_entry() # Search self.subscriptions for a Subscription with a matching id. for _sub_spec, _subs in self.subscriptions.items(): for _sub in _subs: if _sub.subscriptionid == command.subscriptionid: sub_spec = _sub_spec sub = _sub unsub_response = chan.unsubscribe(command.subscriptionid) if sub: self.subscriptions[sub_spec].remove(sub) self.context.subscriptions[sub_spec].remove(sub) # Does anything else on the Context still care about sub_spec? # If not unsubscribe the Context's queue from the db_entry. if not self.context.subscriptions[sub_spec]: queue = self.context.subscription_queue await sub_spec.db_entry.unsubscribe(queue, sub_spec) return [unsub_response] elif isinstance(command, ca.ClearChannelRequest): chan, db_entry = get_db_entry() return [chan.disconnect()] elif isinstance(command, ca.EchoRequest): return [ca.EchoResponse()]
async def _process_command(self, command): '''Process a command from a client, and return the server response''' def get_db_entry(): chan = self.circuit.channels_sid[command.sid] db_entry = self.context.pvdb[chan.name.decode(SERVER_ENCODING)] return chan, db_entry if command is ca.DISCONNECTED: raise DisconnectedCircuit() elif isinstance(command, ca.CreateChanRequest): db_entry = self.context.pvdb[command.name.decode(SERVER_ENCODING)] access = db_entry.check_access(self.client_hostname, self.client_username) return [ ca.VersionResponse(13), ca.AccessRightsResponse(cid=command.cid, access_rights=access), ca.CreateChanResponse(data_type=db_entry.data_type, data_count=len(db_entry), cid=command.cid, sid=self.circuit.new_channel_id()), ] elif isinstance(command, ca.HostNameRequest): self.client_hostname = command.name.decode(SERVER_ENCODING) elif isinstance(command, ca.ClientNameRequest): self.client_username = command.name.decode(SERVER_ENCODING) elif isinstance(command, ca.ReadNotifyRequest): chan, db_entry = get_db_entry() metadata, data = await db_entry.get_dbr_data(command.data_type) return [ chan.read(data=data, data_type=command.data_type, data_count=len(data), status=1, ioid=command.ioid, metadata=metadata) ] elif isinstance(command, (ca.WriteRequest, ca.WriteNotifyRequest)): chan, db_entry = get_db_entry() client_waiting = isinstance(command, ca.WriteNotifyRequest) async def handle_write(): '''Wait for an asynchronous caput to finish''' try: write_status = await db_entry.set_dbr_data( command.data, command.data_type, command.metadata) except Exception as ex: cid = self.circuit.channels_sid[command.sid].cid response_command = ca.ErrorResponse( command, cid, status_code=ca.ECA_INTERNAL.code_with_severity, error_message=('Python exception: {} {}' ''.format(type(ex).__name__, ex))) else: 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) if client_waiting: await self.send(response_command) await self.pending_tasks.spawn(handle_write, ignore_result=True) # TODO pretty sure using the taskgroup will bog things down, # but it suppresses an annoying warning message, so... there elif isinstance(command, ca.EventAddRequest): chan, db_entry = get_db_entry() # TODO no support for deprecated low/high/to sub = Subscription(mask=command.mask, circuit=self, data_type=command.data_type, subscription_id=command.subscriptionid) if db_entry not in self.context.subscriptions: self.context.subscriptions[db_entry] = [] db_entry.subscribe(self.context.subscription_queue, chan) self.context.subscriptions[db_entry].append(sub) if db_entry not in self.subscriptions: self.subscriptions[db_entry] = [] self.subscriptions[db_entry].append(sub) # send back a first monitor always metadata, data = await db_entry.get_dbr_data(command.data_type) return [ chan.subscribe(data=data, data_type=command.data_type, data_count=len(data), subscriptionid=command.subscriptionid, metadata=metadata, status_code=1) ] elif isinstance(command, ca.EventCancelRequest): chan, db_entry = get_db_entry() sub = [ sub for sub in self.subscriptions[db_entry] if sub.subscription_id == command.subscriptionid ] if sub: sub = sub[0] unsub_response = chan.unsubscribe(command.subscriptionid) self.context.subscriptions[db_entry].remove(sub) if not self.context.subscriptions[db_entry]: db_entry.subscribe(None) del self.context.subscriptions[db_entry] self.subscriptions[db_entry].remove(sub) return [unsub_response] elif isinstance(command, ca.ClearChannelRequest): chan, db_entry = get_db_entry() return [chan.disconnect()] elif isinstance(command, ca.EchoRequest): return [ca.EchoResponse()]