def test_init(self): """Test Timer initialisation""" timer = Timer(10) assert timer.timeout == 10 timer = Timer(0) assert timer.timeout == 0 timer = Timer(None) assert timer.timeout is None
def test_property_setters_getters(self): """Test Timer property setters and getters.""" timer = Timer(0) assert timer.timeout == 0 assert not timer.expired assert timer.remaining == 0 timer = Timer(None) assert timer.timeout is None assert not timer.expired assert timer.remaining == 1 timer.timeout = 10 assert timer.timeout == 10 assert not timer.expired assert timer.remaining == 10 timer.timeout = 0.2 timer.start() sleep(0.15) assert timer.remaining < 0.1 assert not timer.expired time.sleep(0.1) assert timer.expired timer.timeout = None assert timer.timeout is None
def test_property_setters_getters(self): """Test Timer property setters and getters.""" timer = Timer(0) assert timer.timeout_seconds == 0 assert not timer.is_expired assert timer.time_remaining == 0 timer = Timer(None) assert timer.timeout_seconds is None assert not timer.is_expired assert timer.time_remaining == -1 timer.timeout_seconds = 10 assert timer.timeout_seconds == 10 assert not timer.is_expired assert timer.time_remaining == 10 timer.timeout_seconds = 0.2 timer.start() time.sleep(0.1) assert timer.time_remaining < 0.1 assert not timer.is_expired time.sleep(0.1) assert timer.is_expired timer.timeout_seconds = None assert timer.timeout_seconds is None
def test_timeout_stop(self): """Test stopping the timer.""" timer = Timer(0.1) assert timer.timeout == 0.1 assert timer.expired is False assert timer.remaining == 0.1 timer.start() timer.stop() assert timer.timeout == 0.1 assert timer.expired is False assert timer.remaining > 0 timer.start() time.sleep(0.2) timer.stop() assert timer.timeout == 0.1 assert timer.expired is True assert timer.remaining < 0
def __init__(self, assoc: "Association") -> None: """Create a new DUL service provider for `assoc`. Parameters ---------- assoc : association.Association The DUL's parent :class:`~pynetdicom.association.Association` instance. """ # The association thread self._assoc = assoc self.socket: Optional["AssociationSocket"] = None # Current primitive and PDU # TODO: Don't do it this way self.primitive: Optional[_PDUPrimitiveType] = None self.pdu: Optional[_PDUType] = None # Tracks the events the state machine needs to process self.event_queue: "queue.Queue[str]" = queue.Queue() # These queues provide communication between the DUL service # user and the DUL service provider. # An event occurs when the DUL service user adds to # the to_provider_queue self.to_provider_queue: "queue.Queue[_PDUPrimitiveType]" = ( queue.Queue()) # A primitive is sent to the service user when the DUL service provider # adds to the to_user_queue. self.to_user_queue: "queue.Queue[_PDUPrimitiveType]" = queue.Queue() # Set the (network) idle and ARTIM timers # Timeouts gets set after DUL init so these are temporary self._idle_timer = Timer(60) self.artim_timer = Timer(30) # State machine - PS3.8 Section 9.2 self.state_machine = StateMachine(self) # Controls the minimum delay between loops in run() in seconds # TODO: try and make this event based rather than running loops self._run_loop_delay = 0.001 Thread.__init__(self, target=make_target(self.run_reactor)) self.daemon = False self._kill_thread = False
def __init__(self, assoc): """ Parameters ---------- assoc : association.Association The DUL's parent Association instance. """ # The association thread self._assoc = assoc self.socket = None # Current primitive and PDU # TODO: Don't do it this way self.primitive = None self.pdu = None # Tracks the events the state machine needs to process self.event_queue = queue.Queue() # These queues provide communication between the DUL service # user and the DUL service provider. # An event occurs when the DUL service user adds to # the to_provider_queue self.to_provider_queue = queue.Queue() # A primitive is sent to the service user when the DUL service provider # adds to the to_user_queue. self.to_user_queue = queue.Queue() # Set the (network) idle and ARTIM timers # Timeouts gets set after DUL init so these are temporary self._idle_timer = Timer(60) self.artim_timer = Timer(30) # State machine - PS3.8 Section 9.2 self.state_machine = StateMachine(self) # Controls the minimum delay between loops in run() # TODO: try and make this event based rather than running loops self._run_loop_delay = 0.001 Thread.__init__(self) self.daemon = False self._kill_thread = False
def test_start_stop(self): """Test Timer stops.""" timer = Timer(0.2) timer.start() time.sleep(0.1) timer.stop() time.sleep(0.5) assert not timer.expired
def test_restart(self): """Test Timer restarts correctly.""" timer = Timer(0.2) timer.start() time.sleep(0.1) timer.restart() time.sleep(0.15) assert not timer.is_expired time.sleep(0.05) assert timer.is_expired
def test_restart(self): """Test Timer restarts correctly.""" timer = Timer(0.2) timer.start() time.sleep(0.1) assert timer.expired is False timer.restart() time.sleep(0.15) assert timer.expired is False time.sleep(0.1) assert timer.expired is True
def test_restart(self): """Test Timer restarts correctly.""" timer = Timer(0.2) timer.start() # time.sleep() may be longer than the called amount - whoof timeout = 0 sleep(0.1) assert timer.expired is False timer.restart() sleep(0.15) assert timer.expired is False time.sleep(0.1) assert timer.expired is True
def test_no_timeout(self): """Test the timer with no time out.""" timer = Timer(None) assert timer.timeout is None assert timer.expired is False assert timer.remaining == 1 timer.start() assert timer.expired is False assert timer.remaining == 1 time.sleep(0.5) assert timer.expired is False assert timer.remaining == 1 timer.stop() assert timer.expired is False assert timer.remaining == 1
def test_timeout(self): """Test the timer with a time out.""" timer = Timer(0.1) assert timer.timeout == 0.1 assert timer.expired is False assert timer.remaining == 0.1 timer.start() assert timer.expired is False assert timer.remaining > 0 time.sleep(0.2) assert timer.expired is True assert timer.remaining < 0 timer.stop() assert timer.expired is True assert timer.remaining < 0
class DULServiceProvider(Thread): """ Three ways to call DULServiceProvider: - If a port number is given, the DUL will wait for incoming connections on this port. - If a socket is given, the DUL will use this socket as the client socket. - If neither is given, the DUL will not be able to accept connections (but will be able to initiate them.) Parameters ---------- Socket : socket.socket, optional The local AE's listen socket Port : int, optional The port number on which to wait for incoming connections Name : str, optional Used help identify the DUL service provider dul_timeout : float, optional The maximum amount of time to wait for connection responses (in seconds) local_ae : pynetdicom.applicationentity.ApplicationEntity The local AE instance assoc : pynetdicom.association.Association The DUL's current Association Attributes ---------- artim_timer : pynetdicom.timer.Timer The ARTIM timer dul_from_user_queue : queue.Queue Queue of PDUs from the DUL service user to be processed by the DUL provider dul_to_user_queue : queue.Queue Queue of primitives from the DUL service to be processed by the DUL user event_queue : queue.Queue List of queued events to be processed by the state machine scp_socket : socket.socket() If the local AE is acting as an SCP, this is the connection from the peer AE to the SCP scu_socket : socket.socket() If the local AE is acting as an SCU, this is the connection from the local AE to the peer AE SCP state_machine : pynetdicom.fsm.StateMachine The DICOM Upper Layer's State Machine """ def __init__(self, Socket=None, Port=None, Name='', dul_timeout=None, acse_timeout=30, local_ae=None, assoc=None): if Socket and Port: raise ValueError("DULServiceProvider can't be instantiated with " "both Socket and Port parameters") # The local AE self.local_ae = local_ae self.association = assoc Thread.__init__(self, name=Name) # Current primitive and PDU self.primitive = None self.pdu = None # The event_queue tracks the events the DUL state machine needs to # process self.event_queue = queue.Queue() # These queues provide communication between the DUL service # user and the DUL service provider. # An event occurs when the DUL service user adds to # the to_provider_queue self.to_provider_queue = queue.Queue() # A primitive is sent to the service user when the DUL service provider # adds to the to_user_queue. self.to_user_queue = queue.Queue() # Setup the idle timer, ARTIM timer and finite state machine self._idle_timer = None if dul_timeout is not None and dul_timeout > 0: self._idle_timer = Timer(dul_timeout) # ARTIM timer self.artim_timer = Timer(acse_timeout) # State machine - PS3.8 Section 9.2 self.state_machine = StateMachine(self) if Socket: # A client socket has been given, so the local AE is acting as # an SCP # generate an event 5 self.event_queue.put('Evt5') self.scu_socket = Socket self.peer_address = None self.scp_socket = None elif Port: # A port number has been given, so the local AE is acting as an # SCU. Create a new socket using the given port number self.scp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.scp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # The port number for the local AE to listen on self.local_port = Port if self.local_port: try: local_address = os.popen('hostname').read()[:-1] self.scp_socket.bind((local_address, self.local_port)) except: #logger.error("Already bound") # FIXME: If already bound then warn? # Why would it already be bound? # A: Another process may be using it pass self.scp_socket.listen(1) else: self.scp_socket = None self.scu_socket = None self.peer_address = None else: # No port nor socket self.scp_socket = None self.scu_socket = None self.peer_address = None self.kill = False self.daemon = False self.start() def Kill(self): """Immediately interrupts the thread""" self.kill = True def Stop(self): """ Interrupts the thread if state is "Sta1" Returns ------- bool True if Sta1, False otherwise """ if self.state_machine.current_state == 'Sta1': self.kill = True # Fix for Issue 39 # Give the DUL thread time to exit while self.is_alive(): time.sleep(0.001) return True return False def Send(self, params): """ Parameters ---------- params - The parameters to put on FromServiceUser [FIXME] """ self.to_provider_queue.put(params) def Receive(self, Wait=False, Timeout=None): """ Get the next item to be processed out of the queue of items sent from the DUL service provider to the service user Parameters ---------- Wait - bool, optional If `Wait` is True and `Timeout` is None, blocks until an item is available. If `Timeout` is a positive number, blocks at most `Timeout` seconds. Otherwise returns an item if one is immediately available. Timeout - int, optional See the definition of `Wait` Returns ------- queue_item The next object in the to_user_queue [FIXME] None If the queue is empty """ try: queue_item = self.to_user_queue.get(block=Wait, timeout=Timeout) return queue_item except queue.Empty: return None def Peek(self): """Look at next item to be returned by get""" try: return self.to_user_queue.queue[0] except: return None def CheckIncomingPDU(self): """ Converts an incoming PDU from the peer AE back into a primitive (ie one of the following: A-ASSOCIATE, A-RELEASE, A-ABORT, P-DATA, A-P-ABORT) """ bytestream = bytes() # Try and read data from the socket try: # Get the data from the socket bytestream = self.scu_socket.recv(1) except socket.error: self.event_queue.put('Evt17') self.scu_socket.close() self.scu_socket = None logger.error('DUL: Error reading data from the socket') return # Remote port has been closed if bytestream == bytes(): self.event_queue.put('Evt17') self.scu_socket.close() self.scu_socket = None logger.error('Peer has closed transport connection') return # Incoming data is OK else: # First byte is always PDU type # 0x01 - A-ASSOCIATE-RQ 1, 2, 3-6 # 0x02 - A-ASSOCIATE-AC 1, 2, 3-6 # 0x03 - A-ASSOCIATE-RJ 1, 2, 3-6 # 0x04 - P-DATA-TF 1, 2, 3-6 # 0x05 - A-RELEASE-RQ 1, 2, 3-6 # 0x06 - A-RELEASE-RP 1, 2, 3-6 # 0x07 - A-ABORT 1, 2, 3-6 # We do all this just to get the length of the PDU # Byte 1 is PDU type # (value, ) pdu_type = unpack('B', bytestream) # Unrecognised PDU type - Evt19 in the State Machine if pdu_type[0] not in [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]: logger.error("Unrecognised PDU type: 0x%s" %pdu_type) self.event_queue.put('Evt19') return # Byte 2 is Reserved result = recvn(self.scu_socket, 1) bytestream += result # Bytes 3-6 is the PDU length result = unpack('B', result) length = recvn(self.scu_socket, 4) bytestream += length length = unpack('>L', length) # Bytes 7-xxxx is the rest of the PDU result = recvn(self.scu_socket, length[0]) bytestream += result # Determine the type of PDU coming on remote port, then decode # the raw bytestream to the corresponding PDU class self.pdu = Socket2PDU(bytestream, self) # Put the event corresponding to the incoming PDU on the queue self.event_queue.put(PDU2Event(self.pdu)) # Convert the incoming PDU to a corresponding ServiceParameters # object self.primitive = self.pdu.ToParams() def CheckTimer(self): """ Check if the state machine's ARTIM timer has expired. If it has then Evt18 is added to the event queue. Returns ------- bool True if the ARTIM timer has expired, False otherwise """ if self.artim_timer.is_expired(): #logger.debug('%s: timer expired' % (self.name)) self.event_queue.put('Evt18') return True return False def idle_timer_expired(self): """ Checks if the idle timer has expired Returns ------- bool True if the idle timer has expired, False otherwise """ if self._idle_timer is None: return False if self._idle_timer.is_expired() == True: return True return False def CheckIncomingPrimitive(self): """ """ #logger.debug('%s: checking incoming primitive' % (self.name)) # look at self.ReceivePrimitive for incoming primitives try: # Check the queue and see if there are any primitives # If so then put the corresponding event on the event queue self.primitive = self.to_provider_queue.get(False, None) self.event_queue.put(primitive2event(self.primitive)) return True except queue.Empty: return False def CheckNetwork(self): return self.is_transport_connection_event() def is_transport_connection_event(self): """ Check to see if the transport connection has incoming data Returns ------- bool True if an event has been added, False otherwise """ # Sta13 is waiting for the transport connection to close if self.state_machine.current_state == 'Sta13': # If we have no connection to the SCU if self.scu_socket is None: return False # If we are still connected to the SCU try: # socket.Socket().recv(bufsize) # If we are still receiving data from the socket # wait until its done while self.scu_socket.recv(1) != b'': continue except socket.error: return False # Once we have no more incoming data close the socket and # add the corresponding event to the queue self.scu_socket.close() self.scu_socket = None # Issue the Transport connection closed indication (AR-5 -> Sta1) self.event_queue.put('Evt17') return True # If the local AE is an SCP, listen for incoming data # The local AE is in Sta1, i.e. listening for Transport Connection # Indications if self.scp_socket and not self.scu_socket: read_list, _, _ = select.select([self.scp_socket], [], [], 0) # If theres incoming connection request, accept it if read_list: self.scu_socket, address = self.scp_socket.accept() # Add to event queue (Sta1 + Evt5 -> AE-5 -> Sta2 self.event_queue.put('Evt5') return True # If a local AE is an SCU, listen for incoming data elif self.scu_socket: # If we are awaiting transport connection opening to complete # (from local transport service) then issue the corresponding # indication (Sta4 + Evt2 -> AE-2 -> Sta5) if self.state_machine.current_state == 'Sta4': self.event_queue.put('Evt2') return True # By this point the connection is established # If theres incoming data on the connection then check the PDU # type # # FIXME: bug related to socket closing, see socket_bug.note # #try: #print(self.scu_socket) read_list, _, _ = select.select([self.scu_socket], [], [], 0) if read_list: self.CheckIncomingPDU() return True #except ValueError: # self.event_queue.put('Evt17') # return False else: return False def run(self): """ The main threading.Thread run loop. Runs constantly, checking the connection for incoming data. When incoming data is received it categorises it and add its to the `to_user_queue`. """ #logger.debug('Starting DICOM UL service "%s"' %self.name) # Main DUL loop while True: if self._idle_timer is not None: self._idle_timer.start() # Required for some reason time.sleep(0.001) if self.kill: break # Check the connection for incoming data try: # If local AE is SCU also calls CheckIncomingPDU() if self.CheckNetwork(): if self._idle_timer is not None: self._idle_timer.restart() elif self.CheckIncomingPrimitive(): pass elif self.CheckTimer(): self.kill = True except: self.kill = True raise # Check the event queue to see if there is anything to do try: event = self.event_queue.get(False) # If the queue is empty, return to the start of the loop except queue.Empty: continue self.state_machine.do_action(event) #logger.debug('DICOM UL service "%s" stopped' %self.name) def on_receive_pdu(self): """ Callback function that is called after the first byte of an incoming PDU is read """ pass
def __init__(self, socket=None, port=None, dul_timeout=None, assoc=None): """ Parameters ---------- socket : socket.socket, optional The local AE's listen socket port : int, optional The port number on which to wait for incoming connections dul_timeout : float, optional The maximum amount of time to wait for connection responses (in seconds) assoc : association.Association The DUL's current Association """ if socket and port: raise ValueError("DULServiceProvider can't be instantiated with " "both socket and port parameters") # The association thread self.assoc = assoc Thread.__init__(self) # Current primitive and PDU self.primitive = None self.pdu = None # The event_queue tracks the events the DUL state machine needs to # process self.event_queue = queue.Queue() # These queues provide communication between the DUL service # user and the DUL service provider. # An event occurs when the DUL service user adds to # the to_provider_queue self.to_provider_queue = queue.Queue() # A primitive is sent to the service user when the DUL service provider # adds to the to_user_queue. self.to_user_queue = queue.Queue() # Setup the idle timer, ARTIM timer and finite state machine # FIXME: Why do we have an idle timer? self._idle_timer = Timer(dul_timeout) # ARTIM timer self.artim_timer = Timer(dul_timeout) # State machine - PS3.8 Section 9.2 self.state_machine = StateMachine(self) if socket: # A client socket has been given, so the local AE is acting as # an SCP # generate an event 5 self.event_queue.put('Evt5') self.scu_socket = socket self.peer_address = None self.scp_socket = None elif port: # A port number has been given, so the local AE is acting as an # SCU. Create a new socket using the given port number self.scp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.scp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # The port number for the local AE to listen on self.local_port = port if self.local_port: # pylint: disable=broad-except try: local_address = os.popen('hostname').read()[:-1] self.scp_socket.bind((local_address, self.local_port)) except Exception as ex: LOGGER.exception(ex) self.scp_socket.listen(1) else: self.scp_socket = None self.scu_socket = None self.peer_address = None else: # No port nor socket self.scp_socket = None self.scu_socket = None self.peer_address = None self._kill_thread = False self.daemon = False # Controls the minimum delay between loops in run() self._run_loop_delay = 0.001
class DULServiceProvider(Thread): """The DICOM Upper Layer Service Provider. Attributes ---------- artim_timer : timer.Timer The ARTIM timer socket : transport.AssociationSocket A wrapped socket.socket object used to communicate with the peer. to_provider_queue : queue.Queue Queue of PDUs from the DUL service user to be processed by the DUL provider to_user_queue : queue.Queue Queue of primitives from the DUL service to be processed by the DUL user event_queue : queue.Queue List of queued events to be processed by the state machine state_machine : fsm.StateMachine The DICOM Upper Layer's State Machine """ def __init__(self, assoc): """ Parameters ---------- assoc : association.Association The DUL's parent Association instance. """ # The association thread self._assoc = assoc self.socket = None # Current primitive and PDU # TODO: Don't do it this way self.primitive = None self.pdu = None # Tracks the events the state machine needs to process self.event_queue = queue.Queue() # These queues provide communication between the DUL service # user and the DUL service provider. # An event occurs when the DUL service user adds to # the to_provider_queue self.to_provider_queue = queue.Queue() # A primitive is sent to the service user when the DUL service provider # adds to the to_user_queue. self.to_user_queue = queue.Queue() # Set the (network) idle and ARTIM timers # Timeouts gets set after DUL init so these are temporary self._idle_timer = Timer(60) self.artim_timer = Timer(30) # State machine - PS3.8 Section 9.2 self.state_machine = StateMachine(self) # Controls the minimum delay between loops in run() # TODO: try and make this event based rather than running loops self._run_loop_delay = 0.001 Thread.__init__(self) self.daemon = False self._kill_thread = False @property def assoc(self): """Return the Association we are providing DUL services for.""" return self._assoc def _check_incoming_primitive(self): """Check the incoming primitive.""" try: # Check the queue and see if there are any primitives # If so then put the corresponding event on the event queue self.primitive = self.to_provider_queue.get(False) self.event_queue.put(self._primitive_to_event(self.primitive)) return True except queue.Empty: return False def _decode_pdu(self, bytestream): """Decode a received PDU. Parameters ---------- bytestream : bytearray The received PDU. Returns ------- pdu.PDU subclass, str The PDU subclass corresponding to the PDU and the event string corresponding to receiving that PDU type. """ # Trigger before data is decoded in case of exception in decoding bytestream = bytes(bytestream) evt.trigger(self.assoc, evt.EVT_DATA_RECV, {'data': bytestream}) pdu_types = { b'\x01': (A_ASSOCIATE_RQ, 'Evt6'), b'\x02': (A_ASSOCIATE_AC, 'Evt3'), b'\x03': (A_ASSOCIATE_RJ, 'Evt4'), b'\x04': (P_DATA_TF, 'Evt10'), b'\x05': (A_RELEASE_RQ, 'Evt12'), b'\x06': (A_RELEASE_RP, 'Evt13'), b'\x07': (A_ABORT_RQ, 'Evt16') } pdu, event = pdu_types[bytestream[0:1]] pdu = pdu() pdu.decode(bytestream) evt.trigger(self.assoc, evt.EVT_PDU_RECV, {'pdu': pdu}) return pdu, event def idle_timer_expired(self): """ Checks if the idle timer has expired Returns ------- bool True if the idle timer has expired, False otherwise. """ return self._idle_timer.expired def _is_transport_event(self): """Check to see if the socket has incoming data Returns ------- bool True if an event has been added to the event queue, False otherwise. Returning True restarts the idle timer and skips the incoming primitive check. """ # Sta13: waiting for the transport connection to close # however it may still receive data that needs to be acted on if self.state_machine.current_state == 'Sta13': # Check to see if there's more data to be read # Might be any incoming PDU or valid/invalid data if self.socket and self.socket.ready: # Data still available, grab it self._read_pdu_data() return True # Once we have no more incoming data close the socket and # add the corresponding event to the queue self.socket.close() return True # By this point the connection should be established # If theres incoming data on the connection then check the PDU # type # Fix for #28 - caused by peer disconnecting before run loop is # stopped by assoc.release() if self.socket and self.socket.ready: self._read_pdu_data() return True return False def kill_dul(self): """Immediately interrupts the thread""" self._kill_thread = True @property def network_timeout(self): """Return the network_timeout.""" return self.assoc.network_timeout def peek_next_pdu(self): """Check the next PDU to be processed.""" try: return self.to_user_queue.queue[0] except (queue.Empty, IndexError): return None @staticmethod def _primitive_to_event(primitive): """Returns the state machine event associated with sending a primitive. Parameters ---------- primitive : pdu_primitives.ServiceParameter The Association primitive Returns ------- str The event associated with the primitive """ if primitive.__class__ == A_ASSOCIATE: if primitive.result is None: # A-ASSOCIATE Request event_str = 'Evt1' elif primitive.result == 0x00: # A-ASSOCIATE Response (accept) event_str = 'Evt7' else: # A-ASSOCIATE Response (reject) event_str = 'Evt8' elif primitive.__class__ == A_RELEASE: if primitive.result is None: # A-Release Request event_str = 'Evt11' else: # A-Release Response # result is 'affirmative' event_str = 'Evt14' elif primitive.__class__ in (A_ABORT, A_P_ABORT): event_str = 'Evt15' elif primitive.__class__ == P_DATA: event_str = 'Evt9' else: raise ValueError("_primitive_to_event(): invalid primitive") return event_str def _read_pdu_data(self): """Read PDU data sent by the peer from the socket. Receives the PDU, attempts to decode it, places the corresponding event in the event queue and and converts it a primitive (if possible). If the decoding and conversion is successful then `pdu` and `primitive` are set to corresponding class instances. **Events Emitted** - Evt6: A-ASSOCIATE-RQ PDU received - Evt3: A-ASSOCIATE-AC PDU received - Evt4: A-ASSOCIATE-RJ PDU received - Evt10: P-DATA-TF PDU received - Evt12: A-RELEASE-RQ PDU received - Evt13: A-RELEASE-RP PDU received - Evt16: A-ABORT PDU received - Evt17: Transport connection closed - Evt19: Invalid or unrecognised PDU """ bytestream = bytearray() # Try and read the PDU type and length from the socket try: bytestream.extend(self.socket.recv(6)) except (socket.error, socket.timeout): # Evt17: Transport connection closed self.event_queue.put('Evt17') return try: # Byte 1 is always the PDU type # Byte 2 is always reserved # Bytes 3-6 are always the PDU length pdu_type, _, pdu_length = unpack('>BBL', bytestream) except struct.error: # Raised if there's not enough data # Evt17: Transport connection closed self.event_queue.put('Evt17') return # If the `pdu_type` is unrecognised if pdu_type not in (0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07): # Evt19: Unrecognised or invalid PDU received self.event_queue.put('Evt19') return # Try and read the rest of the PDU try: bytestream += self.socket.recv(pdu_length) except (socket.error, socket.timeout): # Evt17: Transport connection closed self.event_queue.put('Evt17') return # Check that the PDU data was completely read if len(bytestream) != 6 + pdu_length: # Evt17: Transport connection closed self.event_queue.put('Evt17') return try: # Decode the PDU data, get corresponding FSM event pdu, event = self._decode_pdu(bytestream) self.event_queue.put(event) except Exception as exc: LOGGER.error('Unable to decode the received PDU data') LOGGER.exception(exc) # Evt19: Unrecognised or invalid PDU received self.event_queue.put('Evt19') return self.pdu = pdu self.primitive = self.pdu.to_primitive() def receive_pdu(self, wait=False, timeout=None): """Return an item from the queue if one is available. Get the next item to be processed out of the queue of items sent from the DUL service provider to the service user Parameters ---------- wait : bool, optional If `wait` is True and `timeout` is None, blocks until an item is available. If `timeout` is a positive number, blocks at most `timeout` seconds. Otherwise returns an item if one is immediately available. timeout : int or None See the definition of `wait` Returns ------- queue_item The next object in the to_user_queue. None If the queue is empty. """ try: # If block is True and timeout is None then block until an item # is available. # If timeout is a positive number, blocks timeout seconds and # raises queue.Empty if no item was available in that time. # If block is False, return an item if one is immediately # available, otherwise raise queue.Empty queue_item = self.to_user_queue.get(block=wait, timeout=timeout) # Event handler - ACSE received primitive from DUL service acse_primitives = (A_ASSOCIATE, A_RELEASE, A_ABORT, A_P_ABORT) if isinstance(queue_item, acse_primitives): evt.trigger(self.assoc, evt.EVT_ACSE_RECV, {'primitive': queue_item}) return queue_item except queue.Empty: return None def run(self): """ The main threading.Thread run loop. Runs constantly, checking the connection for incoming data. When incoming data is received it categorises it and add its to the `to_user_queue`. """ # Main DUL loop self._idle_timer.start() while True: # Let the assoc reactor off the leash if not self.assoc._dul_ready.is_set(): self.assoc._dul_ready.set() # This effectively controls how quickly the DUL does anything time.sleep(self._run_loop_delay) if self._kill_thread: break # Check the ARTIM timer first so its event is placed on the queue # ahead of any other events this loop if self.artim_timer.expired: self.event_queue.put('Evt18') # Check the connection for incoming data try: # We can either encode and send a primitive **OR** # receive and decode a PDU per loop of the reactor if self._check_incoming_primitive(): pass elif self._is_transport_event(): self._idle_timer.restart() except Exception as exc: LOGGER.error("Exception in DUL.run(), aborting association") LOGGER.exception(exc) # Bypass the state machine and send an A-ABORT # we do it this way because an exception here will mess up # the state machine and we can't guarantee it'll get sent # otherwise abort_pdu = A_ABORT_RQ() abort_pdu.source = 0x02 abort_pdu.reason_diagnostic = 0x00 self.socket.send(abort_pdu.encode()) self.assoc.is_aborted = True self.assoc.is_established = False # Hard shutdown of the Association and DUL reactors self.assoc._kill = True self._kill_thread = True return # Check the event queue to see if there is anything to do try: event = self.event_queue.get(block=False) # If the queue is empty, return to the start of the loop except queue.Empty: continue self.state_machine.do_action(event) def send_pdu(self, primitive): """Place a primitive in the provider queue to be sent to the peer. Primitives are converted to the corresponding PDU and encoded before sending. Parameters ---------- primitive - pdu_primitives class A service primitive, one of A_ASSOCIATE, A_RELEASE, A_ABORT, A_P_ABORT or P_DATA. """ # Event handler - ACSE sent primitive to the DUL service acse_primitives = (A_ASSOCIATE, A_RELEASE, A_ABORT, A_P_ABORT) if isinstance(primitive, acse_primitives): evt.trigger(self.assoc, evt.EVT_ACSE_SENT, {'primitive': primitive}) self.to_provider_queue.put(primitive) def stop_dul(self): """ Interrupts the thread if state is "Sta1" Returns ------- bool True if Sta1, False otherwise """ if self.state_machine.current_state == 'Sta1': self._kill_thread = True # Fix for Issue 39 # Give the DUL thread time to exit while self.is_alive(): time.sleep(0.001) return True return False
class DULServiceProvider(Thread): """The DICOM Upper Layer Service Provider. Attributes ---------- artim_timer : timer.Timer The :dcm:`ARTIM<part08/chapter_9.html#sect_9.1.5>` timer. socket : transport.AssociationSocket A wrapped `socket <https://docs.python.org/3/library/socket.html#socket-objects>`_ object used to communicate with the peer. to_provider_queue : queue.Queue Queue of primitives received from the peer to be processed by the service user. to_user_queue : queue.Queue Queue of processed PDUs for the DUL service user. event_queue : queue.Queue List of queued events to be processed by the state machine. state_machine : fsm.StateMachine The DICOM Upper Layer's State Machine. """ def __init__(self, assoc: "Association") -> None: """Create a new DUL service provider for `assoc`. Parameters ---------- assoc : association.Association The DUL's parent :class:`~pynetdicom.association.Association` instance. """ # The association thread self._assoc = assoc self.socket: Optional["AssociationSocket"] = None # Tracks the events the state machine needs to process self.event_queue: "queue.Queue[str]" = queue.Queue() # These queues provide communication between the DUL service # user and the DUL service provider. # An event occurs when the DUL service user adds to # the to_provider_queue self.to_provider_queue: "_QueueType" = queue.Queue() # A primitive is sent to the service user when the DUL service provider # adds to the to_user_queue. self.to_user_queue: "queue.Queue[_PDUPrimitiveType]" = queue.Queue() # A queue storing PDUs received from the peer self._recv_pdu: "queue.Queue[_PDUType]" = queue.Queue() # Set the (network) idle and ARTIM timers # Timeouts gets set after DUL init so these are temporary self._idle_timer = Timer(60) self.artim_timer = Timer(30) # State machine - PS3.8 Section 9.2 self.state_machine = StateMachine(self) # Controls the minimum delay between loops in run() in seconds # TODO: try and make this event based rather than running loops self._run_loop_delay = 0.001 Thread.__init__(self, target=make_target(self.run_reactor)) self.daemon = False self._kill_thread = False @property def assoc(self) -> "Association": """Return the parent :class:`~pynetdicom.association.Association`.""" return self._assoc def _decode_pdu(self, bytestream: bytearray) -> Tuple[_PDUType, str]: """Decode a received PDU. Parameters ---------- bytestream : bytearray The received PDU. Returns ------- pdu.PDU subclass, str The PDU subclass corresponding to the PDU and the event string corresponding to receiving that PDU type. """ # Trigger before data is decoded in case of exception in decoding b = bytes(bytestream) evt.trigger(self.assoc, evt.EVT_DATA_RECV, {"data": b}) pdu_cls, event = _PDU_TYPES[b[0:1]] pdu = pdu_cls() pdu.decode(b) evt.trigger(self.assoc, evt.EVT_PDU_RECV, {"pdu": pdu}) return pdu, event def idle_timer_expired(self) -> bool: """Return ``True`` if the network idle timer has expired.""" return self._idle_timer.expired def _is_transport_event(self) -> bool: """Check to see if the socket has incoming data Returns ------- bool True if an event has been added to the event queue, False otherwise. Returning True restarts the idle timer and skips the incoming primitive check. """ # Sta13: waiting for the transport connection to close # however it may still receive data that needs to be acted on self.socket = cast("AssociationSocket", self.socket) if self.state_machine.current_state == "Sta13": # Check to see if there's more data to be read # Might be any incoming PDU or valid/invalid data if self.socket and self.socket.ready: # Data still available, grab it self._read_pdu_data() return True # Once we have no more incoming data close the socket and # add the corresponding event to the queue self.socket.close() return True # By this point the connection should be established # If there's incoming data on the connection then check the PDU # type # Fix for #28 - caused by peer disconnecting before run loop is # stopped by assoc.release() if self.socket and self.socket.ready: self._read_pdu_data() return True return False def kill_dul(self) -> None: """Kill the DUL reactor and stop the thread""" self._kill_thread = True @property def network_timeout(self) -> Optional[float]: """Return the network timeout (in seconds).""" return self.assoc.network_timeout def peek_next_pdu(self) -> Optional[_PDUPrimitiveType]: """Check the next PDU to be processed.""" try: return cast(_PDUPrimitiveType, self.to_user_queue.queue[0]) except (queue.Empty, IndexError): return None def _process_recv_primitive(self) -> bool: """Check to see if the local user has sent any primitives to the DUL""" # Check the queue and see if there are any primitives # If so then put the corresponding event on the event queue try: primitive = self.to_provider_queue.queue[0] except (queue.Empty, IndexError): return False if isinstance(primitive, T_CONNECT): # Evt2 or Evt17, depending on whether successful or not event = primitive.result elif isinstance(primitive, A_ASSOCIATE): if primitive.result is None: # A-ASSOCIATE Request event = "Evt1" elif primitive.result == 0x00: # A-ASSOCIATE Response (accept) event = "Evt7" else: # A-ASSOCIATE Response (reject) event = "Evt8" elif isinstance(primitive, A_RELEASE): if primitive.result is None: # A-Release Request event = "Evt11" else: # A-Release Response # result is 'affirmative' event = "Evt14" elif isinstance(primitive, (A_ABORT, A_P_ABORT)): event = "Evt15" elif isinstance(primitive, P_DATA): event = "Evt9" else: raise ValueError( f"Unknown primitive type '{primitive.__class__.__name__}' received" ) self.event_queue.put(event) return True def _read_pdu_data(self) -> None: """Read PDU data sent by the peer from the socket. Receives the PDU, attempts to decode it, places the corresponding event in the event queue and converts it a primitive (if possible). If the decoding and conversion is successful then `pdu` and `primitive` are set to corresponding class instances. **Events Emitted** - Evt6: A-ASSOCIATE-RQ PDU received - Evt3: A-ASSOCIATE-AC PDU received - Evt4: A-ASSOCIATE-RJ PDU received - Evt10: P-DATA-TF PDU received - Evt12: A-RELEASE-RQ PDU received - Evt13: A-RELEASE-RP PDU received - Evt16: A-ABORT PDU received - Evt17: Transport connection closed - Evt19: Invalid or unrecognised PDU """ bytestream = bytearray() self.socket = cast("AssociationSocket", self.socket) # Try and read the PDU type and length from the socket try: bytestream.extend(self.socket.recv(6)) except (socket.error, socket.timeout) as exc: # READ_PDU_EXC_A LOGGER.error( "Connection closed before the entire PDU was received") LOGGER.exception(exc) # Evt17: Transport connection closed self.event_queue.put("Evt17") return try: # Byte 1 is always the PDU type # Byte 2 is always reserved # Bytes 3-6 are always the PDU length pdu_type, _, pdu_length = struct.unpack(">BBL", bytestream) except struct.error as exc: # READ_PDU_EXC_B # LOGGER.error("Insufficient data received to decode the PDU") # Evt17: Transport connection closed self.event_queue.put("Evt17") return # If the `pdu_type` is unrecognised if pdu_type not in (0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07): # READ_PDU_EXC_C LOGGER.error(f"Unknown PDU type received '0x{pdu_type:02X}'") # Evt19: Unrecognised or invalid PDU received self.event_queue.put("Evt19") return # Try and read the rest of the PDU try: bytestream += self.socket.recv(pdu_length) except (socket.error, socket.timeout) as exc: # READ_PDU_EXC_D LOGGER.error( "Connection closed before the entire PDU was received") LOGGER.exception(exc) # Evt17: Transport connection closed self.event_queue.put("Evt17") return # Check that the PDU data was completely read if len(bytestream) != 6 + pdu_length: # READ_PDU_EXC_E # Evt17: Transport connection closed LOGGER.error( f"The received PDU is shorter than expected ({len(bytestream)} of " f"{6 + pdu_length} bytes received)") self.event_queue.put("Evt17") return try: # Decode the PDU data, get corresponding FSM event pdu, event = self._decode_pdu(bytestream) self.event_queue.put(event) except Exception as exc: # READ_PDU_EXC_F LOGGER.error("Unable to decode the received PDU data") LOGGER.exception(exc) # Evt19: Unrecognised or invalid PDU received self.event_queue.put("Evt19") return self._recv_pdu.put(pdu) def receive_pdu( self, wait: bool = False, timeout: Optional[float] = None) -> Optional[_PDUPrimitiveType]: """Return an item from the queue if one is available. Get the next service primitive to be processed out of the queue of items sent from the DUL service provider to the service user Parameters ---------- wait : bool, optional If `wait` is ``True`` and `timeout` is ``None``, blocks until an item is available. If `timeout` is a positive number, blocks at most `timeout` seconds. Otherwise returns an item if one is immediately available. timeout : int or None See the definition of `wait` Returns ------- Optional[Union[A_ASSOCIATE, A_RELEASE, A_ABORT, A_P_ABORT]] The next primitive in the :attr:`~DULServiceProvider.to_user_queue`, or ``None`` if the queue is empty. """ try: # If block is True and timeout is None then block until an item # is available. # If timeout is a positive number, blocks timeout seconds and # raises queue.Empty if no item was available in that time. # If block is False, return an item if one is immediately # available, otherwise raise queue.Empty primitive = self.to_user_queue.get(block=wait, timeout=timeout) # Event handler - ACSE received primitive from DUL service if isinstance(primitive, (A_ASSOCIATE, A_RELEASE, A_ABORT, A_P_ABORT)): evt.trigger(self.assoc, evt.EVT_ACSE_RECV, {"primitive": primitive}) return primitive except queue.Empty: return None def run_reactor(self) -> None: """Run the DUL reactor. The main :class:`threading.Thread` run loop. Runs constantly, checking the connection for incoming data. When incoming data is received it categorises it and add its to the :attr:`~DULServiceProvider.to_user_queue`. """ # Main DUL loop self._idle_timer.start() self.socket = cast("AssociationSocket", self.socket) sleep = False while True: # Let the assoc reactor off the leash if not self.assoc._dul_ready.is_set(): self.assoc._dul_ready.set() # When single-stepping the reactor, sleep between events so that # test code has time to run. sleep = True if sleep: # If there were no events to process on the previous loop, # sleep before checking again, otherwise check immediately # Setting `_run_loop_delay` higher will use less CPU when idle, but # will also increase the latency to respond to new requests time.sleep(self._run_loop_delay) if self._kill_thread: break # Check the ARTIM timer first so its event is placed on the queue # ahead of any other events this loop if self.artim_timer.expired: self.event_queue.put("Evt18") # Check the connection for incoming data try: # We can either encode and send a primitive **OR** # receive and decode a PDU per loop of the reactor if self._process_recv_primitive( ): # encode (sent by state machine) pass elif self._is_transport_event(): # receive and decode PDU self._idle_timer.restart() except Exception as exc: LOGGER.error("Exception in DUL.run(), aborting association") LOGGER.exception(exc) # Bypass the state machine and send an A-ABORT # we do it this way because an exception here will mess up # the state machine and we can't guarantee it'll get sent # otherwise abort_pdu = A_ABORT_RQ() abort_pdu.source = 0x02 abort_pdu.reason_diagnostic = 0x00 self.socket.send(abort_pdu.encode()) self.assoc.is_aborted = True self.assoc.is_established = False # Hard shutdown of the Association and DUL reactors self.assoc._kill = True self._kill_thread = True return # Check the event queue to see if there is anything to do try: event = self.event_queue.get(block=False) # If the queue is empty, return to the start of the loop except queue.Empty: sleep = True continue self.state_machine.do_action(event) sleep = False def _send(self, pdu: _PDUType) -> None: """Encode and send a PDU to the peer. Parameters ---------- pdu : pynetdicom.pdu.PDU The PDU to be encoded and sent to the peer. """ if self.socket is not None: self.socket.send(pdu.encode()) evt.trigger(self.assoc, evt.EVT_PDU_SENT, {"pdu": pdu}) else: LOGGER.warning("Attempted to send data over closed connection") def send_pdu(self, primitive: _PDUPrimitiveType) -> None: """Place a primitive in the provider queue to be sent to the peer. Primitives are converted to the corresponding PDU and encoded before sending. Parameters ---------- primitive : pdu_primitives.PDU sub-class A service primitive, one of: .. currentmodule:: pynetdicom.pdu_primitives * :class:`A_ASSOCIATE` * :class:`A_RELEASE` * :class:`A_ABORT` * :class:`A_P_ABORT` * :class:`P_DATA` """ # Event handler - ACSE sent primitive to the DUL service if isinstance(primitive, (A_ASSOCIATE, A_RELEASE, A_ABORT, A_P_ABORT)): evt.trigger(self.assoc, evt.EVT_ACSE_SENT, {"primitive": primitive}) self.to_provider_queue.put(primitive) def stop_dul(self) -> bool: """Stop the reactor if current state is ``'Sta1'`` Returns ------- bool ``True`` if ``'Sta1'`` and the reactor has stopped, ``False`` otherwise """ if self.state_machine.current_state == "Sta1": self._kill_thread = True # Fix for Issue 39 # Give the DUL thread time to exit while self.is_alive(): time.sleep(self._run_loop_delay) return True return False
def __init__(self, Socket=None, Port=None, Name='', dul_timeout=None, acse_timeout=30, local_ae=None, assoc=None): if Socket and Port: raise ValueError("DULServiceProvider can't be instantiated with " "both Socket and Port parameters") # The local AE self.local_ae = local_ae self.association = assoc Thread.__init__(self, name=Name) # Current primitive and PDU self.primitive = None self.pdu = None # The event_queue tracks the events the DUL state machine needs to # process self.event_queue = queue.Queue() # These queues provide communication between the DUL service # user and the DUL service provider. # An event occurs when the DUL service user adds to # the to_provider_queue self.to_provider_queue = queue.Queue() # A primitive is sent to the service user when the DUL service provider # adds to the to_user_queue. self.to_user_queue = queue.Queue() # Setup the idle timer, ARTIM timer and finite state machine self._idle_timer = None if dul_timeout is not None and dul_timeout > 0: self._idle_timer = Timer(dul_timeout) # ARTIM timer self.artim_timer = Timer(acse_timeout) # State machine - PS3.8 Section 9.2 self.state_machine = StateMachine(self) if Socket: # A client socket has been given, so the local AE is acting as # an SCP # generate an event 5 self.event_queue.put('Evt5') self.scu_socket = Socket self.peer_address = None self.scp_socket = None elif Port: # A port number has been given, so the local AE is acting as an # SCU. Create a new socket using the given port number self.scp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.scp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # The port number for the local AE to listen on self.local_port = Port if self.local_port: try: local_address = os.popen('hostname').read()[:-1] self.scp_socket.bind((local_address, self.local_port)) except: #logger.error("Already bound") # FIXME: If already bound then warn? # Why would it already be bound? # A: Another process may be using it pass self.scp_socket.listen(1) else: self.scp_socket = None self.scu_socket = None self.peer_address = None else: # No port nor socket self.scp_socket = None self.scu_socket = None self.peer_address = None self.kill = False self.daemon = False self.start()
class DULServiceProvider(Thread): """The DICOM Upper Layer Service Provider. Three ways to call DULServiceProvider: - If a port number is given, the DUL will wait for incoming connections on this port. - If a socket is given, the DUL will use this socket as the client socket. - If neither is given, the DUL will not be able to accept connections (but will be able to initiate them.) Attributes ---------- artim_timer : timer.Timer The ARTIM timer association : association.Association The DUL's current Association dul_from_user_queue : queue.Queue Queue of PDUs from the DUL service user to be processed by the DUL provider dul_to_user_queue : queue.Queue Queue of primitives from the DUL service to be processed by the DUL user event_queue : queue.Queue List of queued events to be processed by the state machine scp_socket : socket.socket() If the local AE is acting as an SCP, this is the connection from the peer AE to the SCP scu_socket : socket.socket() If the local AE is acting as an SCU, this is the connection from the local AE to the peer AE SCP state_machine : fsm.StateMachine The DICOM Upper Layer's State Machine """ def __init__(self, socket=None, port=None, dul_timeout=None, assoc=None): """ Parameters ---------- socket : socket.socket, optional The local AE's listen socket port : int, optional The port number on which to wait for incoming connections dul_timeout : float, optional The maximum amount of time to wait for connection responses (in seconds) assoc : association.Association The DUL's current Association """ if socket and port: raise ValueError("DULServiceProvider can't be instantiated with " "both socket and port parameters") # The association thread self.assoc = assoc Thread.__init__(self) # Current primitive and PDU self.primitive = None self.pdu = None # The event_queue tracks the events the DUL state machine needs to # process self.event_queue = queue.Queue() # These queues provide communication between the DUL service # user and the DUL service provider. # An event occurs when the DUL service user adds to # the to_provider_queue self.to_provider_queue = queue.Queue() # A primitive is sent to the service user when the DUL service provider # adds to the to_user_queue. self.to_user_queue = queue.Queue() # Setup the idle timer, ARTIM timer and finite state machine # FIXME: Why do we have an idle timer? self._idle_timer = Timer(dul_timeout) # ARTIM timer self.artim_timer = Timer(dul_timeout) # State machine - PS3.8 Section 9.2 self.state_machine = StateMachine(self) if socket: # A client socket has been given, so the local AE is acting as # an SCP # generate an event 5 self.event_queue.put('Evt5') self.scu_socket = socket self.peer_address = None self.scp_socket = None elif port: # A port number has been given, so the local AE is acting as an # SCU. Create a new socket using the given port number self.scp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.scp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # The port number for the local AE to listen on self.local_port = port if self.local_port: # pylint: disable=broad-except try: local_address = os.popen('hostname').read()[:-1] self.scp_socket.bind((local_address, self.local_port)) except Exception as ex: LOGGER.exception(ex) self.scp_socket.listen(1) else: self.scp_socket = None self.scu_socket = None self.peer_address = None else: # No port nor socket self.scp_socket = None self.scu_socket = None self.peer_address = None self._kill_thread = False self.daemon = False # Controls the minimum delay between loops in run() self._run_loop_delay = 0.001 def idle_timer_expired(self): """ Checks if the idle timer has expired Returns ------- bool True if the idle timer has expired, False otherwise """ if self._idle_timer is None: return False if self._idle_timer.is_expired: return True return False def kill_dul(self): """Immediately interrupts the thread""" self._kill_thread = True def on_receive_pdu(self): """Called after the first byte of an incoming PDU is read. """ pass def peek_next_pdu(self): """Check the next PDU to be processed.""" try: return self.to_user_queue.queue[0] except (queue.Empty, IndexError): return None def receive_pdu(self, wait=False, timeout=None): """ Get the next item to be processed out of the queue of items sent from the DUL service provider to the service user Parameters ---------- wait : bool, optional If `wait` is True and `timeout` is None, blocks until an item is available. If `timeout` is a positive number, blocks at most `timeout` seconds. Otherwise returns an item if one is immediately available. timeout : int or None See the definition of `Wait` Returns ------- queue_item The next object in the to_user_queue [FIXME] None If the queue is empty """ try: # Remove and return an item from the queue # If block is True and timeout is None then block until an item # is available. # If timeout is a positive number, blocks timeout seconds and # raises queue.Empty if no item was available in that time. # If block is False, return an item if one is immediately # available, otherwise raise queue.Empty queue_item = self.to_user_queue.get(block=wait, timeout=timeout) return queue_item except queue.Empty: return None def run(self): """ The main threading.Thread run loop. Runs constantly, checking the connection for incoming data. When incoming data is received it categorises it and add its to the `to_user_queue`. Ripping out this loop and replacing it with event-driven reactor would be nice. """ # Main DUL loop if self._idle_timer is not None: self._idle_timer.start() while True: # This effectively controls how often the DUL checks the network time.sleep(self._run_loop_delay) if self._kill_thread: break # Check the connection for incoming data try: # If local AE is SCU also calls _check_incoming_pdu() if self._is_transport_event() and self._idle_timer is not None: self._idle_timer.restart() elif self._check_incoming_primitive(): pass elif self._is_artim_expired(): self._kill_thread = True except: # FIXME: This catch all should be removed self._kill_thread = True raise # Check the event queue to see if there is anything to do try: event = self.event_queue.get(block=False) # If the queue is empty, return to the start of the loop except queue.Empty: continue self.state_machine.do_action(event) def send_pdu(self, params): """ Parameters ---------- params - The parameters to put on FromServiceUser [FIXME] """ self.to_provider_queue.put(params) def stop_dul(self): """ Interrupts the thread if state is "Sta1" Returns ------- bool True if Sta1, False otherwise """ if self.state_machine.current_state == 'Sta1': self._kill_thread = True # Fix for Issue 39 # Give the DUL thread time to exit while self.is_alive(): time.sleep(0.001) return True return False def _check_incoming_pdu(self): """ Converts an incoming PDU from the peer AE back into a primitive (ie one of the following: A-ASSOCIATE, A-RELEASE, A-ABORT, P-DATA, A-P-ABORT) """ bytestream = bytes() # Try and read data from the socket try: # Get the data from the socket bytestream = self.scu_socket.recv(1) except socket.error: self.event_queue.put('Evt17') self.scu_socket.close() self.scu_socket = None LOGGER.error('DUL: Error reading data from the socket') return # Remote port has been closed if bytestream == bytes(): self.event_queue.put('Evt17') self.scu_socket.close() self.scu_socket = None LOGGER.error('Peer has closed transport connection') return # Incoming data is OK else: # First byte is always PDU type # 0x01 - A-ASSOCIATE-RQ 1, 2, 3-6 # 0x02 - A-ASSOCIATE-AC 1, 2, 3-6 # 0x03 - A-ASSOCIATE-RJ 1, 2, 3-6 # 0x04 - P-DATA-TF 1, 2, 3-6 # 0x05 - A-RELEASE-RQ 1, 2, 3-6 # 0x06 - A-RELEASE-RP 1, 2, 3-6 # 0x07 - A-ABORT 1, 2, 3-6 # We do all this just to get the length of the PDU # Byte 1 is PDU type # (value, ) pdu_type = unpack('B', bytestream)[0] # Unrecognised PDU type - Evt19 in the State Machine if pdu_type not in [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]: LOGGER.error("Unrecognised PDU type: 0x%02x", pdu_type) self.event_queue.put('Evt19') return # Byte 2 is Reserved result = self._recvn(self.scu_socket, 1) bytestream += result # Bytes 3-6 is the PDU length result = unpack('B', result) length = self._recvn(self.scu_socket, 4) bytestream += length length = unpack('>L', length) # Bytes 7-xxxx is the rest of the PDU result = self._recvn(self.scu_socket, length[0]) bytestream += result # Determine the type of PDU coming on remote port, then decode # the raw bytestream to the corresponding PDU class self.pdu = self._socket_to_pdu(bytestream) # Put the event corresponding to the incoming PDU on the queue self.event_queue.put(self._pdu_to_event(self.pdu)) # Convert the incoming PDU to a corresponding ServiceParameters # object self.primitive = self.pdu.to_primitive() def _check_incoming_primitive(self): """Check the incoming primitive.""" try: # Check the queue and see if there are any primitives # If so then put the corresponding event on the event queue self.primitive = self.to_provider_queue.get(False) self.event_queue.put(self._primitive_to_event(self.primitive)) return True except queue.Empty: return False def _is_artim_expired(self): """Return if the state machine's ARTIM timer has expired. If it has then 'Evt18' is added to the event queue. Returns ------- bool True if the ARTIM timer has expired, False otherwise """ if self.artim_timer.is_expired: #LOGGER.debug('%s: timer expired' % (self.name)) self.event_queue.put('Evt18') return True return False def _is_transport_event(self): """Check to see if the transport connection has incoming data Returns ------- bool True if an event has been added, False otherwise """ # Sta13 is waiting for the transport connection to close if self.state_machine.current_state == 'Sta13': # If we have no connection to the SCU if self.scu_socket is None: return False # If we are still connected to the SCU try: # socket.Socket().recv(bufsize) # If we are still receiving data from the socket # wait until its done while self.scu_socket.recv(1) != b'': continue except socket.error: return False # Once we have no more incoming data close the socket and # add the corresponding event to the queue self.scu_socket.close() self.scu_socket = None # Issue the Transport connection closed indication (AR-5 -> Sta1) self.event_queue.put('Evt17') return True # If the local AE is an SCP, listen for incoming data # The local AE is in Sta1, i.e. listening for Transport Connection # Indications if self.scp_socket and not self.scu_socket: read_list, _, _ = select.select([self.scp_socket], [], [], 0) # If theres incoming connection request, accept it if read_list: self.scu_socket, _ = self.scp_socket.accept() # Add to event queue (Sta1 + Evt5 -> AE-5 -> Sta2 self.event_queue.put('Evt5') return True # If a local AE is an SCU, listen for incoming data elif self.scu_socket: # If we are awaiting transport connection opening to complete # (from local transport service) then issue the corresponding # indication (Sta4 + Evt2 -> AE-2 -> Sta5) if self.state_machine.current_state == 'Sta4': self.event_queue.put('Evt2') return True # By this point the connection should be established # If theres incoming data on the connection then check the PDU # type # Fix for #28 - caused by peer disconnecting before run loop is # stopped by assoc.release() try: read_list, _, _ = select.select([self.scu_socket], [], [], 0) except (socket.error, ValueError): return False if read_list: self._check_incoming_pdu() return True else: return False @staticmethod def _pdu_to_event(pdu): """Returns the event associated with the PDU. Parameters ---------- pdu : pdu.PDU The PDU Returns ------- str The event str associated with the PDU """ if pdu.__class__ == A_ASSOCIATE_RQ: event_str = 'Evt6' elif pdu.__class__ == A_ASSOCIATE_AC: event_str = 'Evt3' elif pdu.__class__ == A_ASSOCIATE_RJ: event_str = 'Evt4' elif pdu.__class__ == P_DATA_TF: event_str = 'Evt10' elif pdu.__class__ == A_RELEASE_RQ: event_str = 'Evt12' elif pdu.__class__ == A_RELEASE_RP: event_str = 'Evt13' elif pdu.__class__ == A_ABORT_RQ: event_str = 'Evt16' else: #"Unrecognized or invalid PDU" event_str = 'Evt19' return event_str @staticmethod def _primitive_to_event(primitive): """Returns the state machine event associated with sending a primitive. Parameters ---------- primitive : pdu_primitives.ServiceParameter The Association primitive Returns ------- str The event associated with the primitive """ if primitive.__class__ == A_ASSOCIATE: if primitive.result is None: # A-ASSOCIATE Request event_str = 'Evt1' elif primitive.result == 0x00: # A-ASSOCIATE Response (accept) event_str = 'Evt7' else: # A-ASSOCIATE Response (reject) event_str = 'Evt8' elif primitive.__class__ == A_RELEASE: if primitive.result is None: # A-Release Request event_str = 'Evt11' else: # A-Release Response # result is 'affirmative' event_str = 'Evt14' elif primitive.__class__ == A_ABORT: event_str = 'Evt15' elif primitive.__class__ == P_DATA: event_str = 'Evt9' else: raise ValueError("_primitive_to_event(): invalid primitive") return event_str @staticmethod def _recvn(sock, n_bytes): """Read `n_bytes` from a socket. Parameters ---------- sock : socket.socket The socket to read from n_bytes : int The number of bytes to read """ ret = b'' read_length = 0 while read_length < n_bytes: tmp = sock.recv(n_bytes - read_length) if not tmp: return ret ret += tmp read_length += len(tmp) if read_length != n_bytes: raise RuntimeError("_recvn(socket, {}) - Error reading data from " "socket.".format(n_bytes)) return ret def _socket_to_pdu(self, data): """Returns the PDU object associated with an incoming data stream. Parameters ---------- data : bytes The incoming data stream Returns ------- pdu : pdu.PDU The decoded data as a PDU object """ pdutype = unpack('B', data[:1])[0] acse = self.assoc.acse _pdu_types = { 0x01: (A_ASSOCIATE_RQ, acse.debug_receive_associate_rq), 0x02: (A_ASSOCIATE_AC, acse.debug_receive_associate_ac), 0x03: (A_ASSOCIATE_RJ, acse.debug_receive_associate_rj), 0x04: (P_DATA_TF, acse.debug_receive_data_tf), 0x05: (A_RELEASE_RQ, acse.debug_receive_release_rq), 0x06: (A_RELEASE_RP, acse.debug_receive_release_rp), 0x07: (A_ABORT_RQ, acse.debug_receive_abort) } if pdutype in _pdu_types: (pdu, acse_callback) = _pdu_types[pdutype] pdu = pdu() pdu.decode(data) # ACSE callbacks acse_callback(pdu) return pdu return None