Beispiel #1
0
 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
Beispiel #2
0
    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
Beispiel #3
0
    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
Beispiel #4
0
 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
Beispiel #5
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
Beispiel #6
0
    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
Beispiel #7
0
 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
Beispiel #8
0
 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
Beispiel #9
0
 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
Beispiel #10
0
 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
Beispiel #11
0
 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
Beispiel #12
0
 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
Beispiel #13
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
Beispiel #14
0
    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
Beispiel #15
0
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
Beispiel #16
0
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
Beispiel #17
0
    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()
Beispiel #18
0
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