def test_property_setters_getters(self):
        """Test Timer property setters and getters."""
        timer = Timer(0)
        self.assertEqual(timer.timeout_seconds, 0)
        self.assertFalse(timer.is_expired)
        self.assertEqual(timer.time_remaining, 0)

        timer = Timer(None)
        self.assertEqual(timer.timeout_seconds, None)
        self.assertFalse(timer.is_expired)
        self.assertEqual(timer.time_remaining, -1)

        timer.timeout_seconds = 10
        self.assertEqual(timer.timeout_seconds, 10)
        self.assertFalse(timer.is_expired)
        self.assertEqual(timer.time_remaining, 10)

        timer.timeout_seconds = 0.2
        timer.start()
        time.sleep(0.1)
        self.assertTrue(timer.time_remaining < 0.1)
        self.assertFalse(timer.is_expired)
        time.sleep(0.1)
        self.assertTrue(timer.is_expired)

        timer.timeout_seconds = None
        self.assertEqual(timer.timeout_seconds, None)
 def test_start_stop(self):
     """Test Timer stops."""
     timer = Timer(0.2)
     timer.start()
     time.sleep(0.1)
     timer.stop()
     time.sleep(0.2)
     self.assertFalse(timer.is_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)
     self.assertFalse(timer.is_expired)
     time.sleep(0.05)
     self.assertTrue(timer.is_expired)
Exemple #4
0
    def receive_msg(self, wait=False):
        """Receive a DIMSE message from the peer.

        Set the DIMSE provider in a mode ready to receive a response from the
        peer

        Parameters
        ----------
        wait : bool, optional
            Wait until a response has been received (default: False).

        Returns
        -------
        pynetdicom3.dimse_messages.DIMSEMessage, int or None, None
            Returns the complete DIMSE message and its presentation context ID
            or None, None.
        """
        if self.message is None:
            self.message = DIMSEMessage()

        timeout = Timer(self.dimse_timeout)
        timeout.start()

        if wait:
            # Loop until complete DIMSE message is received
            #   message may be split into 1 or more fragments
            while True:

                # Fix for issue #38
                # Because we only progress once the next PDU arrives to be
                #   peeked at, the DIMSE timeout in receive_pdu() doesn't
                #   actually do anything.
                if timeout.is_expired:
                    return None, None

                # Race condition: sometimes the DUL will be killed before the
                #   loop exits
                if not self.dul.is_alive():
                    return None, None

                time.sleep(0.001)

                nxt = self.dul.peek_next_pdu()
                if nxt is None:
                    continue

                if nxt.__class__ is not P_DATA:
                    return None, None

                pdu = self.dul.receive_pdu(wait, self.dimse_timeout)

                if self.message.decode_msg(pdu):
                    # Callback
                    self.on_receive_dimse_message(self.message)

                    context_id = self.message.ID
                    primitive = self.message.message_to_primitive()

                    # Fix for memory leak, Issue #41
                    #   Reset the DIMSE message, ready for the next one
                    self.message.encoded_command_set = BytesIO()
                    self.message.data_set = BytesIO()
                    self.message = None

                    return primitive, context_id
                else:
                    return None, None

        else:
            cls = self.dul.peek_next_pdu().__class__

            if cls not in (type(None), P_DATA):
                return None, None

            pdu = self.dul.receive_pdu(wait, self.dimse_timeout)

            if self.message.decode_msg(pdu):
                # Callback
                self.on_receive_dimse_message(self.message)

                context_id = self.message.ID
                primitive = self.message.message_to_primitive()

                # Fix for memory leak, Issue #41
                #   Reset the DIMSE message, ready for the next one
                self.message.encoded_command_set = BytesIO()
                self.message.data_set = BytesIO()
                self.message = None

                return primitive, context_id
            else:
                return None, None
Exemple #5
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.)

    Attributes
    ----------
    artim_timer : pynetdicom3.timer.Timer
        The ARTIM timer
    association : pynetdicom3.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 : pynetdicom3.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 : pynetdicom3.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:
                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(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.ToParams()

    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 : pynetdicom3.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 : pynetdicom3.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 : pynetdicom3.pdu.PDU
            The decoded data as a PDU object
        """
        pdutype = unpack('B', data[0: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 = pdu_types[pdutype][0]
            pdu.Decode(data)

            # ACSE callbacks
            pdu_types[pdutype][1](pdu)

            return pdu

        return None