async def send(self, port, *commands): """ Process a command and tranport it over the UDP socket. """ bytes_to_send = self.broadcaster.send(*commands) for host in ca.get_address_list(): if ':' in host: host, _, specified_port = host.partition(':') is_register = isinstance(commands[0], ca.RepeaterRegisterRequest) if not self.registered and is_register: logger.debug( 'Specific IOC host/port designated in address' 'list: %s:%s. Repeater registration ' 'requirement ignored', host, specified_port) async with self.broadcaster_command_condition: # TODO how does this work with multiple addresses # listed? response = (('127.0.0.1', ca.EPICS_CA1_PORT), [ ca.RepeaterConfirmResponse( repeater_address='127.0.0.1') ]) await self.command_bundle_queue.put(response[1]) await self.broadcaster_command_condition.notify_all() continue await self.udp_sock.sendto(bytes_to_send, (host, int(specified_port))) else: await self.udp_sock.sendto(bytes_to_send, (host, port))
def test_broadcaster_checks(): b = ca.Broadcaster(ca.CLIENT) with pytest.raises(ca.LocalProtocolError): b.send( ca.SearchRequest(name='LIRR', cid=0, version=ca.DEFAULT_PROTOCOL_VERSION)) b.send(ca.RepeaterRegisterRequest('1.2.3.4')) res = ca.RepeaterConfirmResponse('5.6.7.8') commands = b.recv(bytes(res), ('5.6.7.8', 6666)) assert commands[0] == res b.process_commands(commands) req = ca.SearchRequest(name='LIRR', cid=0, version=ca.DEFAULT_PROTOCOL_VERSION) with pytest.raises(ca.LocalProtocolError): b.send(req) b.send(ca.VersionRequest(priority=0, version=ca.DEFAULT_PROTOCOL_VERSION), req) res = ca.SearchResponse(port=6666, ip='1.2.3.4', cid=0, version=ca.DEFAULT_PROTOCOL_VERSION) addr = ('1.2.3.4', 6666) commands = b.recv(bytes(res), addr) # see changes to _broadcaster.py. Apparently rsrv does not always conform # to the protocol and include a version response before search responsesq # with pytest.raises(ca.RemoteProtocolError): # b.process_commands(commands) # commands = b.recv(bytes(ca.VersionResponse(version=ca.DEFAULT_PROTOCOL_VERSION)) + bytes(res), addr) b.process_commands(commands) # this gets both
def test_broadcaster_checks(): b = ca.Broadcaster(ca.CLIENT) with pytest.raises(ca.LocalProtocolError): b.send(ca.SearchRequest(name='LIRR', cid=0, version=13)) b.send(ca.RepeaterRegisterRequest('1.2.3.4')) res = ca.RepeaterConfirmResponse('5.6.7.8') commands = b.recv(bytes(res), ('5.6.7.8', 6666)) assert commands[0] == res b.process_commands(commands) req = ca.SearchRequest(name='LIRR', cid=0, version=13) with pytest.raises(ca.LocalProtocolError): b.send(req) b.send(ca.VersionRequest(priority=0, version=13), req) res = ca.SearchResponse(port=6666, ip='1.2.3.4', cid=0, version=13) addr = ('1.2.3.4', 6666) commands = b.recv(bytes(res), addr) with pytest.raises(ca.RemoteProtocolError): b.process_commands(commands) commands = b.recv(bytes(ca.VersionResponse(version=13)) + bytes(res), addr) b.process_commands(commands) # this gets both
def test_nonet(): # Register with the repeater. assert not cli_b._registered bytes_to_send = cli_b.send(ca.RepeaterRegisterRequest('0.0.0.0')) assert not cli_b._registered # Receive response data = bytes(ca.RepeaterConfirmResponse('127.0.0.1')) commands = cli_b.recv(data, cli_addr) cli_b.process_commands(commands) assert cli_b._registered # Search for pv1. # CA requires us to send a VersionRequest and a SearchRequest bundled into # one datagram. bytes_to_send = cli_b.send(ca.VersionRequest(0, ca.DEFAULT_PROTOCOL_VERSION), ca.SearchRequest(pv1, 0, ca.DEFAULT_PROTOCOL_VERSION)) commands = srv_b.recv(bytes_to_send, cli_addr) srv_b.process_commands(commands) ver_req, search_req = commands bytes_to_send = srv_b.send( ca.VersionResponse(ca.DEFAULT_PROTOCOL_VERSION), ca.SearchResponse(5064, None, search_req.cid, ca.DEFAULT_PROTOCOL_VERSION)) # Receive a VersionResponse and SearchResponse. commands = iter(cli_b.recv(bytes_to_send, cli_addr)) command = next(commands) assert type(command) is ca.VersionResponse command = next(commands) assert type(command) is ca.SearchResponse address = ca.extract_address(command) circuit = ca.VirtualCircuit(our_role=ca.CLIENT, address=address, priority=0) circuit.log.setLevel('DEBUG') chan1 = ca.ClientChannel(pv1, circuit) assert chan1.states[ca.CLIENT] is ca.SEND_CREATE_CHAN_REQUEST assert chan1.states[ca.SERVER] is ca.IDLE srv_circuit = ca.VirtualCircuit(our_role=ca.SERVER, address=address, priority=None) cli_send(chan1.circuit, ca.VersionRequest(priority=0, version=ca.DEFAULT_PROTOCOL_VERSION)) srv_recv(srv_circuit) srv_send(srv_circuit, ca.VersionResponse(version=ca.DEFAULT_PROTOCOL_VERSION)) cli_recv(chan1.circuit) cli_send(chan1.circuit, ca.HostNameRequest('localhost')) cli_send(chan1.circuit, ca.ClientNameRequest('username')) cli_send(chan1.circuit, ca.CreateChanRequest(name=pv1, cid=chan1.cid, version=ca.DEFAULT_PROTOCOL_VERSION)) assert chan1.states[ca.CLIENT] is ca.AWAIT_CREATE_CHAN_RESPONSE assert chan1.states[ca.SERVER] is ca.SEND_CREATE_CHAN_RESPONSE srv_recv(srv_circuit) assert chan1.states[ca.CLIENT] is ca.AWAIT_CREATE_CHAN_RESPONSE assert chan1.states[ca.SERVER] is ca.SEND_CREATE_CHAN_RESPONSE srv_chan1, = srv_circuit.channels.values() assert srv_chan1.states[ca.CLIENT] is ca.AWAIT_CREATE_CHAN_RESPONSE assert srv_chan1.states[ca.SERVER] is ca.SEND_CREATE_CHAN_RESPONSE srv_send(srv_circuit, ca.CreateChanResponse(cid=chan1.cid, sid=1, data_type=5, data_count=1)) assert srv_chan1.states[ca.CLIENT] is ca.CONNECTED assert srv_chan1.states[ca.SERVER] is ca.CONNECTED # At this point the CLIENT is not aware that we are CONNECTED because it # has not yet received the CreateChanResponse. It should not be allowed to # read or write. assert chan1.states[ca.CLIENT] is ca.AWAIT_CREATE_CHAN_RESPONSE assert chan1.states[ca.SERVER] is ca.SEND_CREATE_CHAN_RESPONSE # Try sending a premature read request. read_req = ca.ReadNotifyRequest(sid=srv_chan1.sid, data_type=srv_chan1.native_data_type, data_count=srv_chan1.native_data_count, ioid=0) with pytest.raises(ca.LocalProtocolError): cli_send(chan1.circuit, read_req) # The above failed because the sid is not recognized. Remove that failure # by editing the sid cache, and check that it *still* fails, this time # because of the state machine prohibiting this command before the channel # is in a CONNECTED state. chan1.circuit.channels_sid[1] = chan1 with pytest.raises(ca.LocalProtocolError): cli_send(chan1.circuit, read_req) cli_recv(chan1.circuit) assert chan1.states[ca.CLIENT] is ca.CONNECTED assert chan1.states[ca.SERVER] is ca.CONNECTED # Test subscriptions. assert chan1.native_data_type and chan1.native_data_count add_req = ca.EventAddRequest(data_type=chan1.native_data_type, data_count=chan1.native_data_count, sid=chan1.sid, subscriptionid=0, low=0, high=0, to=0, mask=1) cli_send(chan1.circuit, add_req) srv_recv(srv_circuit) add_res = ca.EventAddResponse(data=(3,), data_type=chan1.native_data_type, data_count=chan1.native_data_count, subscriptionid=0, status=1) srv_send(srv_circuit, add_res) cli_recv(chan1.circuit) cancel_req = ca.EventCancelRequest(data_type=add_req.data_type, sid=add_req.sid, subscriptionid=add_req.subscriptionid) cli_send(chan1.circuit, cancel_req) srv_recv(srv_circuit) # Test reading. cli_send(chan1.circuit, ca.ReadNotifyRequest(data_type=5, data_count=1, sid=chan1.sid, ioid=12)) srv_recv(srv_circuit) srv_send(srv_circuit, ca.ReadNotifyResponse(data=(3,), data_type=5, data_count=1, ioid=12, status=1)) cli_recv(chan1.circuit) # Test writing. request = ca.WriteNotifyRequest(data_type=2, data_count=1, sid=chan1.sid, ioid=13, data=(4,)) cli_send(chan1.circuit, request) srv_recv(srv_circuit) srv_send(srv_circuit, ca.WriteNotifyResponse(data_type=5, data_count=1, ioid=13, status=1)) cli_recv(chan1.circuit) # Test "clearing" (closing) the channel. cli_send(chan1.circuit, ca.ClearChannelRequest(sid=chan1.sid, cid=chan1.cid)) assert chan1.states[ca.CLIENT] is ca.MUST_CLOSE assert chan1.states[ca.SERVER] is ca.MUST_CLOSE srv_recv(srv_circuit) assert srv_chan1.states[ca.CLIENT] is ca.MUST_CLOSE assert srv_chan1.states[ca.SERVER] is ca.MUST_CLOSE srv_send(srv_circuit, ca.ClearChannelResponse(sid=chan1.sid, cid=chan1.cid)) assert srv_chan1.states[ca.CLIENT] is ca.CLOSED assert srv_chan1.states[ca.SERVER] is ca.CLOSED
def _run_repeater(server_sock, bind_addr): 'Run the repeater using server_socket and bind_address' bind_host, bind_port = bind_addr servers = {} clients = {} broadcaster = caproto.Broadcaster(our_role=caproto.SERVER) logger.info("Repeater is listening on %s:%d", bind_host, bind_port) while True: msg, addr = server_sock.recvfrom(MAX_UDP_RECV) host, port = addr if port in clients and clients[port] != host: # broadcast only from one interface continue elif (port in servers and servers[port]['host'] != host): continue try: commands = broadcaster.recv(msg, addr) broadcaster.process_commands(commands) except Exception: logger.exception('Failed to process incoming datagram') continue if not commands: # NOTE: additional valid way of registration is an empty # message, according to broadcaster source if port not in clients: clients[port] = host logger.debug('New client %s (zero-length registration)', addr) continue to_forward = [] for command in commands: if isinstance(command, caproto.Beacon): # Update our records of the last time each server checked in # (i.e. issued a heartbeat). servers[command.server_port] = dict(up_at=time.time(), host=host) # The sender of this command may leave the IP field empty (0), # leaving it up to the repeater to fill in the address so that # the ultimate recipient knows the correct origin. By leaving # that up to the repeater, the sender avoids providing the # wrong return address (i.e. picking the wrong interface). It # is safer to let the repeater determine the return address # by inspection. if command.header.parameter2 == 0: updated_ip = caproto.ipv4_to_int32(host) command.header.parameter2 = updated_ip to_forward.append(command) elif isinstance(command, caproto.RepeaterRegisterRequest): if port not in clients: clients[port] = host logger.debug('New client %s', addr) confirmation_bytes = broadcaster.send( caproto.RepeaterConfirmResponse(host)) try: server_sock.sendto(confirmation_bytes, (host, port)) except OSError as exc: raise caproto.CaprotoNetworkError( f"Failed to send to {host}:{port}") from exc remove_clients = list(check_clients(clients, skip=port)) _update_all(clients, servers, remove_clients=remove_clients) # Do not broadcast registration requests to other clients. # Omit it from `to_forward`. else: to_forward.append(command) bytes_to_broadcast = b''.join(bytes(cmd) for cmd in to_forward) to_remove = [] for other_port, other_host in clients.items(): if other_port != port: try: server_sock.sendto(bytes_to_broadcast, (other_host, other_port)) except Exception: to_remove.append((other_host, other_port)) if to_remove: _update_all(clients, servers, remove_clients=to_remove)
def main(*, skip_monitor_section=False): # A broadcast socket udp_sock = ca.bcast_socket() # Register with the repeater. bytes_to_send = b.send(ca.RepeaterRegisterRequest('0.0.0.0')) # TODO: for test environment with specific hosts listed in # EPICS_CA_ADDR_LIST if False: fake_reg = (('127.0.0.1', ca.EPICS_CA1_PORT), [ca.RepeaterConfirmResponse(repeater_address='127.0.0.1')]) b.command_queue.put(fake_reg) else: udp_sock.sendto(bytes_to_send, ('', CA_REPEATER_PORT)) # Receive response data, address = udp_sock.recvfrom(1024) commands = b.recv(data, address) b.process_commands(commands) # Search for pv1. # CA requires us to send a VersionRequest and a SearchRequest bundled into # one datagram. bytes_to_send = b.send(ca.VersionRequest(0, 13), ca.SearchRequest(pv1, 0, 13)) for host in ca.get_address_list(): if ':' in host: host, _, specified_port = host.partition(':') udp_sock.sendto(bytes_to_send, (host, int(specified_port))) else: udp_sock.sendto(bytes_to_send, (host, CA_SERVER_PORT)) print('searching for %s' % pv1) # Receive a VersionResponse and SearchResponse. bytes_received, address = udp_sock.recvfrom(1024) commands = b.recv(bytes_received, address) b.process_commands(commands) c1, c2 = commands assert type(c1) is ca.VersionResponse assert type(c2) is ca.SearchResponse address = ca.extract_address(c2) circuit = ca.VirtualCircuit(our_role=ca.CLIENT, address=address, priority=0) circuit.log.setLevel('DEBUG') chan1 = ca.ClientChannel(pv1, circuit) sockets[chan1.circuit] = socket.create_connection(chan1.circuit.address) # Initialize our new TCP-based CA connection with a VersionRequest. send(chan1.circuit, ca.VersionRequest(priority=0, version=13)) recv(chan1.circuit) # Send info about us. send(chan1.circuit, ca.HostNameRequest('localhost')) send(chan1.circuit, ca.ClientNameRequest('username')) send(chan1.circuit, ca.CreateChanRequest(name=pv1, cid=chan1.cid, version=13)) commands = recv(chan1.circuit) # Test subscriptions. assert chan1.native_data_type and chan1.native_data_count add_req = ca.EventAddRequest(data_type=chan1.native_data_type, data_count=chan1.native_data_count, sid=chan1.sid, subscriptionid=0, low=0, high=0, to=0, mask=1) send(chan1.circuit, add_req) commands = recv(chan1.circuit) if not skip_monitor_section: try: print('Monitoring until Ctrl-C is hit. Meanwhile, use caput to ' 'change the value and watch for commands to arrive here.') while True: commands = recv(chan1.circuit) if commands: print(commands) except KeyboardInterrupt: pass cancel_req = ca.EventCancelRequest(data_type=add_req.data_type, sid=add_req.sid, subscriptionid=add_req.subscriptionid) send(chan1.circuit, cancel_req) commands, = recv(chan1.circuit) # Test reading. send( chan1.circuit, ca.ReadNotifyRequest(data_type=2, data_count=1, sid=chan1.sid, ioid=12)) commands, = recv(chan1.circuit) # Test writing. request = ca.WriteNotifyRequest(data_type=2, data_count=1, sid=chan1.sid, ioid=13, data=(4, )) send(chan1.circuit, request) recv(chan1.circuit) time.sleep(2) send( chan1.circuit, ca.ReadNotifyRequest(data_type=2, data_count=1, sid=chan1.sid, ioid=14)) recv(chan1.circuit) # Test "clearing" (closing) the channel. send(chan1.circuit, ca.ClearChannelRequest(chan1.sid, chan1.cid)) recv(chan1.circuit) sockets.pop(chan1.circuit).close() udp_sock.close()
def connection_made(self, transport): self.transport = transport confirmation = caproto.RepeaterConfirmResponse(self.addr[0]) confirmation_bytes = self.proxy.broadcaster.send(confirmation) self.transport.sendto(confirmation_bytes)