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_counter_skipping(circuit_pair): circuit, _ = circuit_pair broadcaster = ca.Broadcaster(ca.CLIENT) # Force the initial value of search IDs to make this test work broadcaster._search_id_counter.value = -1 circuit._channel_id_counter.value = -1 broadcaster.unanswered_searches[0] = 'placeholder' broadcaster.unanswered_searches[2] = 'placeholder' assert broadcaster.new_search_id() == 1 assert list(broadcaster.unanswered_searches) == [0, 2] assert broadcaster.new_search_id() == 3 circuit.channels[2] = 'placeholder' assert circuit.new_channel_id() == 0 assert circuit.new_channel_id() == 1 # should skip 2 assert circuit.new_channel_id() == 3 circuit._ioids[0] = 'placeholder' # should skip 0 circuit.new_ioid() == 1 circuit.event_add_commands[0] = 'placeholder' # should skip 0 circuit.new_subscriptionid() == 1
def __init__(self, pvdb, interfaces=None): if interfaces is None: interfaces = ca.get_server_address_list() self.interfaces = interfaces self.udp_socks = {} # map each interface to a UDP socket for searches self.beacon_socks = {} # map each interface to a UDP socket for beacons self.pvdb = pvdb self.log = logging.getLogger(f'caproto.ctx.{id(self)}') self.circuits = set() self.broadcaster = ca.Broadcaster(our_role=ca.SERVER) self.subscriptions = defaultdict(deque) # Map Subscription to {'before': last_update, 'after': last_update} # to silence duplicates for Subscriptions that use edge-triggered sync # Channel Filter. self.last_sync_edge_update = defaultdict(lambda: defaultdict(dict)) self.last_dead_band = {} self.beacon_count = 0 self.environ = get_environment_variables() # ca_server_port: the default tcp/udp port from the environment self.ca_server_port = self.environ['EPICS_CA_SERVER_PORT'] # the specific tcp port in use by this server self.port = None self.log.debug('EPICS_CA_SERVER_PORT set to %d. This is the UDP port ' 'to be used for searches, and the first TCP server port' ' to be tried.', self.ca_server_port) ignore_addresses = self.environ['EPICS_CAS_IGNORE_ADDR_LIST'] self.ignore_addresses = ignore_addresses.split(' ')
def test_counter_wraparound(circuit_pair): circuit, _ = circuit_pair broadcaster = ca.Broadcaster(ca.CLIENT) MAX = 2**16 for i in range(MAX + 2): assert i % MAX == circuit.new_channel_id() assert i % MAX == circuit.new_subscriptionid() assert i % MAX == circuit.new_ioid() assert i % MAX == broadcaster.new_search_id()
def __init__(self): self.broadcaster = ca.Broadcaster(our_role=ca.CLIENT) self.log = self.broadcaster.log self.command_bundle_queue = curio.Queue() self.broadcaster_command_condition = curio.Condition() # UDP socket broadcasting to CA servers self.udp_sock = ca.bcast_socket(socket) self.registered = False # refers to RepeaterRegisterRequest self.loop_ready_event = curio.Event() self.unanswered_searches = {} # map search id (cid) to name self.search_results = {} # map name to address
def test_counter_wraparound(circuit_pair): circuit, _ = circuit_pair broadcaster = ca.Broadcaster(ca.CLIENT) # Force the initial value of search IDs to make this test work broadcaster._search_id_counter.value = -1 MAX = 2**16 for i in range(MAX + 2): assert i % MAX == circuit.new_channel_id() assert i % MAX == circuit.new_subscriptionid() assert i % MAX == circuit.new_ioid() assert i % MAX == broadcaster.new_search_id()
def __init__(self): self.broadcaster = ca.Broadcaster(our_role=ca.CLIENT) self.log = self.broadcaster.log self.command_bundle_queue = curio.Queue() self.broadcaster_command_condition = curio.Condition() # UDP socket broadcasting to CA servers self.udp_sock = ca.bcast_socket(socket) self.broadcaster.our_address = safe_getsockname(self.udp_sock) self.registered = False # refers to RepeaterRegisterRequest self.loop_ready_event = curio.Event() self.unanswered_searches = {} # map search id (cid) to name self.search_results = {} # map name to address self.environ = ca.get_environment_variables() self.ca_server_port = self.environ['EPICS_CA_SERVER_PORT']
def __init__(self, *, nursery): self.nursery = nursery self.broadcaster = ca.Broadcaster(our_role=ca.CLIENT) self.log = self.broadcaster.log self.command_bundle_queue = trio.Queue(capacity=1000) self.broadcaster_command_condition = trio.Condition() self._cleanup_condition = trio.Condition() self._cleanup_event = trio.Event() # UDP socket broadcasting to CA servers self.udp_sock = None self.registered = False # refers to RepeaterRegisterRequest self.unanswered_searches = {} # map search id (cid) to name self.search_results = {} # map name to address self.new_id = ThreadsafeCounter( dont_clash_with=self.unanswered_searches)
def __init__(self, *, log_level='ERROR'): self.log_level = log_level self.udp_sock = None self.sock_thread = None self._search_lock = threading.RLock() self.search_results = {} # map name to (time, address) self.unanswered_searches = {} # map search id (cid) to name self.listeners = weakref.WeakSet() self.broadcaster = ca.Broadcaster(our_role=ca.CLIENT) self.broadcaster.log.setLevel(self.log_level) self.command_bundle_queue = queue.Queue() self.command_cond = threading.Condition() # ConditionType self.command_thread = threading.Thread(target=self.command_loop, daemon=True) self.command_thread.start()
def __init__(self, host, port, pvdb, *, log_level='ERROR'): self.host = host self.port = port self.pvdb = pvdb self.circuits = set() self.log_level = log_level self.broadcaster = ca.Broadcaster(our_role=ca.SERVER) self.broadcaster.log.setLevel(self.log_level) self.command_bundle_queue = curio.Queue() self.subscriptions = defaultdict(deque) self.subscription_queue = curio.UniversalQueue() self.beacon_count = 0 self.environ = get_environment_variables() ignore_addresses = self.environ['EPICS_CAS_IGNORE_ADDR_LIST'] self.ignore_addresses = ignore_addresses.split(' ')
def __init__(self, *, nursery): self.nursery = nursery self.broadcaster = ca.Broadcaster(our_role=ca.CLIENT) self.log = self.broadcaster.log self.command_chan = open_memory_channel(1000) self.broadcaster_command_condition = trio.Condition() self._cleanup_condition = trio.Condition() self._cleanup_event = trio.Event() # UDP socket broadcasting to CA servers self.udp_sock = None self.registered = False # refers to RepeaterRegisterRequest self.unanswered_searches = {} # map search id (cid) to name self.search_results = {} # map name to address self.new_id = ThreadsafeCounter( dont_clash_with=self.unanswered_searches) self.environ = get_environment_variables() self.ca_server_port = self.environ['EPICS_CA_SERVER_PORT']
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 __init__(self, pvdb, interfaces=None): if interfaces is None: interfaces = ca.get_server_address_list() self.interfaces = interfaces self.udp_socks = {} # map each interface to a UDP socket for searches self.beacon_socks = { } # map each interface to a UDP socket for beacons self.pvdb = pvdb self.log = logging.getLogger(f'caproto.ctx.{id(self)}') self.circuits = set() self.broadcaster = ca.Broadcaster(our_role=ca.SERVER) self.subscriptions = defaultdict(deque) # Map Subscription to {'before': last_update, 'after': last_update} # to silence duplicates for Subscriptions that use edge-triggered sync # Channel Filter. self.last_sync_edge_update = defaultdict(lambda: defaultdict(dict)) self.last_dead_band = {} self.beacon_count = 0 self.environ = get_environment_variables() ignore_addresses = self.environ['EPICS_CAS_IGNORE_ADDR_LIST'] self.ignore_addresses = ignore_addresses.split(' ')
def test_register_convenience_method(): broadcaster = ca.Broadcaster(ca.CLIENT) broadcaster.register()
import caproto as ca import time import socket CA_REPEATER_PORT = 5065 CA_SERVER_PORT = 5064 pv1 = "XF:31IDA-OP{Tbl-Ax:X1}Mtr.VAL" ip = '127.0.0.1' # Make a Broadcaster. b = ca.Broadcaster(our_role=ca.CLIENT) b.log.setLevel('DEBUG') # Make a dict to hold our tcp sockets. sockets = {} # Convenience functions that do both transport caproto validation/ingest. def send(circuit, command): buffers_to_send = circuit.send(command) sockets[circuit].sendmsg(buffers_to_send) def recv(circuit): bytes_received = sockets[circuit].recv(4096) commands, _ = circuit.recv(bytes_received) for c in commands: circuit.process_command(c) return commands
import caproto as ca import pytest pv1 = "synctest1" cli_addr = ('127.0.0.1', 6666) repeater_addr = ('127.0.0.1', 5065) # Make a Broadcaster for the client and one for the server. cli_b = ca.Broadcaster(our_role=ca.CLIENT) srv_b = ca.Broadcaster(our_role=ca.SERVER) cli_b.log.setLevel('DEBUG') srv_b.log.setLevel('DEBUG') req_cache = bytearray() res_cache = bytearray() def srv_send(circuit, command): buffers_to_send = circuit.send(command) for buffer in buffers_to_send: res_cache.extend(bytes(buffer)) def srv_recv(circuit): bytes_received = bytes(req_cache) assert len(bytes_received) req_cache.clear() commands, num_bytes_needed = circuit.recv(bytes_received) for command in commands: circuit.process_command(command)
def __init__(self): self.remotes = {} self.broadcaster = caproto.Broadcaster(our_role=caproto.SERVER) super().__init__()
def search(pv_name, udp_sock, timeout, *, max_retries=2): # Set Broadcaster log level to match our logger. b = ca.Broadcaster(our_role=ca.CLIENT) b.client_address = safe_getsockname(udp_sock) # Send registration request to the repeater logger.debug('Registering with the Channel Access repeater.') bytes_to_send = b.send(ca.RepeaterRegisterRequest()) env = get_environment_variables() repeater_port = env['EPICS_CA_REPEATER_PORT'] client_address_list = ca.get_client_address_list() local_address = ca.get_local_address() try: udp_sock.sendto(bytes_to_send, (local_address, repeater_port)) except OSError as exc: raise ca.CaprotoNetworkError( f"Failed to send to {local_address}:{repeater_port}") from exc logger.debug("Searching for %r....", pv_name) commands = (ca.VersionRequest(0, ca.DEFAULT_PROTOCOL_VERSION), ca.SearchRequest(pv_name, 0, ca.DEFAULT_PROTOCOL_VERSION)) bytes_to_send = b.send(*commands) tags = { 'role': 'CLIENT', 'our_address': b.client_address, 'direction': '--->>>' } def send_search(): for dest in client_address_list: tags['their_address'] = dest b.log.debug('%d commands %dB', len(commands), len(bytes_to_send), extra=tags) try: udp_sock.sendto(bytes_to_send, dest) except OSError as exc: host, port = dest raise ca.CaprotoNetworkError( f"Failed to send to {host}:{port}") from exc def check_timeout(): nonlocal retry_at if time.monotonic() >= retry_at: send_search() retry_at = time.monotonic() + retry_timeout if time.monotonic() - t > timeout: raise CaprotoTimeoutError( f"Timed out while awaiting a response " f"from the search for {pv_name!r}. Search " f"requests were sent to this address list: " f"{ca.get_address_list()}.") # Initial search attempt send_search() # Await a search response, and keep track of registration status retry_timeout = timeout / max((max_retries, 1)) t = time.monotonic() retry_at = t + retry_timeout try: orig_timeout = udp_sock.gettimeout() udp_sock.settimeout(retry_timeout) while True: try: bytes_received, address = udp_sock.recvfrom(ca.MAX_UDP_RECV) except socket.timeout: check_timeout() continue check_timeout() commands = b.recv(bytes_received, address) b.process_commands(commands) for command in commands: if isinstance(command, ca.SearchResponse) and command.cid == 0: address = ca.extract_address(command) logger.debug('Found %r at %s:%d', pv_name, *address) return address else: # None of the commands we have seen are a reply to our request. # Receive more data. continue finally: udp_sock.settimeout(orig_timeout)
def test_broadcaster(): with pytest.raises(ca.CaprotoValueError): ca.Broadcaster(our_role=None)
def __init__(self): self.host = socket.gethostbyname(socket.gethostname()) self.remotes = {} self.broadcaster = caproto.Broadcaster(our_role=caproto.SERVER) super().__init__()
def search(pv_name, udp_sock, timeout, *, max_retries=2): # Set Broadcaster log level to match our logger. b = ca.Broadcaster(our_role=ca.CLIENT) # Send registration request to the repeater logger.debug('Registering with the Channel Access repeater.') bytes_to_send = b.send(ca.RepeaterRegisterRequest()) repeater_port = os.environ.get('EPICS_CA_REPEATER_PORT', 5065) for host in ca.get_address_list(): udp_sock.sendto(bytes_to_send, (host, repeater_port)) logger.debug("Searching for '%s'....", pv_name) bytes_to_send = b.send( ca.VersionRequest(0, ca.DEFAULT_PROTOCOL_VERSION), ca.SearchRequest(pv_name, 0, ca.DEFAULT_PROTOCOL_VERSION)) def send_search(): for host in ca.get_address_list(): if ':' in host: host, _, specified_port = host.partition(':') dest = (host, int(specified_port)) else: dest = (host, CA_SERVER_PORT) udp_sock.sendto(bytes_to_send, dest) logger.debug('Search request sent to %r.', dest) def check_timeout(): nonlocal retry_at if time.monotonic() >= retry_at: send_search() retry_at = time.monotonic() + retry_timeout if time.monotonic() - t > timeout: raise TimeoutError(f"Timed out while awaiting a response " f"from the search for {pv_name!r}. Search " f"requests were sent to this address list: " f"{ca.get_address_list()}.") # Initial search attempt send_search() # Await a search response, and keep track of registration status retry_timeout = timeout / max((max_retries, 1)) t = time.monotonic() retry_at = t + retry_timeout try: orig_timeout = udp_sock.gettimeout() udp_sock.settimeout(retry_timeout) while True: try: bytes_received, address = udp_sock.recvfrom(ca.MAX_UDP_RECV) except socket.timeout: check_timeout() continue check_timeout() commands = b.recv(bytes_received, address) b.process_commands(commands) for command in commands: if isinstance(command, ca.SearchResponse) and command.cid == 0: address = ca.extract_address(command) logger.debug('Found %s at %s', pv_name, address) return address else: # None of the commands we have seen are a reply to our request. # Receive more data. continue finally: udp_sock.settimeout(orig_timeout)
def test_empty_datagram(): broadcaster = ca.Broadcaster(ca.CLIENT) commands = broadcaster.recv(b'', ('127.0.0.1', 6666)) assert commands == []
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)