diff --git a/setup.py b/setup.py index 36420e9..8846128 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ url='https://github.com/python-smpplib/python-smpplib', description='SMPP library for python', packages=find_packages(), - install_requires=['six'], + install_requires=['six', 'monotonic'], extras_require=dict( tests=('typing; python_version < "3.5"', 'pytest', 'mock'), ), diff --git a/smpplib/client.py b/smpplib/client.py index ebad492..c3825d0 100644 --- a/smpplib/client.py +++ b/smpplib/client.py @@ -19,12 +19,14 @@ """SMPP client module""" import binascii +import collections import logging import select import socket import struct import warnings +from monotonic import monotonic from smpplib import consts, exceptions, smpp @@ -91,7 +93,6 @@ def __init__( else: self.allow_unknown_opt_params = allow_unknown_opt_params - self._socket = self._create_socket() def __enter__(self): @@ -301,7 +302,7 @@ def set_message_received_handler(self, func): def set_message_sent_handler(self, func): """Set new function to handle message sent event""" self.message_sent_handler = func - + def set_query_resp_handler(self, func): """Set new function to handle query resp event""" self.query_resp_handler = func @@ -421,3 +422,73 @@ def query_message(self, **kwargs): qsm = smpp.make_pdu('query_sm', client=self, **kwargs) self.send_pdu(qsm) return qsm + + +class ThreadSafeClient(Client): + should_stop = False + + def __init__(self, *args, **kwargs): + # Socket polling period + select_timeout = kwargs.get('select_timeout', 1.0) + + super(ThreadSafeClient, self).__init__(*args, **kwargs) + + self._select_timeout = select_timeout + + self._send_queue = collections.deque() + self._read_sock, self._send_sock = socket.socketpair() + + # It will help not to spam the server + self._last_active_time = 0.0 + + def accept(self, obj): + """Accept an object""" + raise NotImplementedError('not implemented') + + def send_pdu(self, pdu, send_later=False): + if send_later: + self._send_queue.append(pdu) + self._send_sock.send(b'\x00') + return True + else: + pdu_sent = super(ThreadSafeClient, self).send_pdu(pdu) + self._last_active_time = monotonic() + return pdu_sent + + def send_message(self, send_later=True, **kwargs): + submit_sm_pdu = smpp.make_pdu('submit_sm', client=self, **kwargs) + self.send_pdu(submit_sm_pdu, send_later=send_later) + return submit_sm_pdu + + def _should_prolong_session(self): + # We need some time to send enquire_link before the next `select` call comes + passed_from_last_message = monotonic() - self._last_active_time + + return self.timeout - self._select_timeout <= passed_from_last_message + + def observe(self, ignore_error_codes=None, auto_send_enquire_link=True): + while not self.should_stop: + rlist, _, _ = select.select( + [self._socket, self._read_sock], [], [], self._select_timeout, + ) + + if self.should_stop: + break + + if not rlist: + if self._should_prolong_session(): + if not auto_send_enquire_link: + raise exceptions.SessionProlongationDisabled() + + self.logger.debug('Sending enquire_link') + pdu = smpp.make_pdu('enquire_link', client=self) + self.send_pdu(pdu) + else: + for ready_socket in rlist: + if ready_socket is self._socket: + self.read_once(ignore_error_codes, auto_send_enquire_link) + else: + self._read_sock.recv(1) + self.send_pdu(self._send_queue.pop()) + + self.logger.info('Finished observing...') diff --git a/smpplib/exceptions.py b/smpplib/exceptions.py index 8f06f64..a6ddc55 100644 --- a/smpplib/exceptions.py +++ b/smpplib/exceptions.py @@ -17,3 +17,7 @@ class PDUError(RuntimeError): class MessageTooLong(ValueError): """Text too long to fit 255 SMS""" + + +class SessionProlongationDisabled(Exception): + """Server send nothing and we do not want to continue""" diff --git a/smpplib/tests/test_client.py b/smpplib/tests/test_client.py index a10666f..f2580c0 100644 --- a/smpplib/tests/test_client.py +++ b/smpplib/tests/test_client.py @@ -1,11 +1,14 @@ +import time import warnings + import pytest -from mock import Mock, call +from mock import call, Mock +from monotonic import monotonic -from smpplib.client import Client -from smpplib.smpp import make_pdu from smpplib import consts from smpplib import exceptions +from smpplib.client import Client, ThreadSafeClient +from smpplib.smpp import make_pdu def test_client_construction_allow_unknown_opt_params_warning(): @@ -44,3 +47,13 @@ def test_client_error_pdu_custom_handler(): client.read_once() assert mock_error_pdu_handler.mock_calls == [call(error_pdu)] + + +def test_prolongation(): + client = ThreadSafeClient("localhost", 5679) + client._last_active_time = monotonic() + assert not client._should_prolong_session() + + time.sleep(client.timeout - client._select_timeout) + + assert client._should_prolong_session()