Merge branch 'constant_read'

Since the secondary thread for constant read is not part of the
TelegramClient anymore, there is no need to restart it. It will
be ran when connecting again.
This commit is contained in:
Lonami Exo 2017-09-02 21:51:11 +02:00
commit 69d182815f
6 changed files with 178 additions and 337 deletions

View File

@ -10,14 +10,15 @@ from ..errors import ReadCancelledError
class TcpClient: class TcpClient:
def __init__(self, proxy=None): def __init__(self, proxy=None, timeout=timedelta(seconds=5)):
self._proxy = proxy self._proxy = proxy
self._socket = None self._socket = None
if isinstance(timeout, timedelta):
# Support for multi-threading advantages and safety self._timeout = timeout.seconds
self.cancelled = Event() # Has the read operation been cancelled? elif isinstance(timeout, int) or isinstance(timeout, float):
self.delay = 0.1 # Read delay when there was no data available self._timeout = float(timeout)
self._lock = Lock() else:
raise ValueError('Invalid timeout type', type(timeout))
def _recreate_socket(self, mode): def _recreate_socket(self, mode):
if self._proxy is None: if self._proxy is None:
@ -30,20 +31,19 @@ class TcpClient:
else: # tuple, list, etc. else: # tuple, list, etc.
self._socket.set_proxy(*self._proxy) self._socket.set_proxy(*self._proxy)
def connect(self, ip, port, timeout): def connect(self, ip, port):
"""Connects to the specified IP and port number. """Connects to the specified IP and port number.
'timeout' must be given in seconds 'timeout' must be given in seconds
""" """
if not self.connected: if not self.connected:
if ':' in ip: # IPv6 if ':' in ip: # IPv6
self._recreate_socket(socket.AF_INET6) mode, address = socket.AF_INET6, (ip, port, 0, 0)
self._socket.settimeout(timeout)
self._socket.connect((ip, port, 0, 0))
else: else:
self._recreate_socket(socket.AF_INET) mode, address = socket.AF_INET, (ip, port)
self._socket.settimeout(timeout)
self._socket.connect((ip, port)) self._recreate_socket(mode)
self._socket.setblocking(False) self._socket.settimeout(self._timeout)
self._socket.connect(address)
def _get_connected(self): def _get_connected(self):
return self._socket is not None return self._socket is not None
@ -65,27 +65,15 @@ class TcpClient:
def write(self, data): def write(self, data):
"""Writes (sends) the specified bytes to the connected peer""" """Writes (sends) the specified bytes to the connected peer"""
# Ensure that only one thread can send data at once # TODO Timeout may be an issue when sending the data, Changed in v3.5:
with self._lock: # The socket timeout is now the maximum total duration to send all data.
try: try:
view = memoryview(data) self._socket.sendall(data)
total_sent, total = 0, len(data)
while total_sent < total:
try:
sent = self._socket.send(view[total_sent:])
if sent == 0:
self.close()
raise ConnectionResetError(
'The server has closed the connection.')
total_sent += sent
except BlockingIOError:
time.sleep(self.delay)
except BrokenPipeError: except BrokenPipeError:
self.close() self.close()
raise raise
def read(self, size, timeout=timedelta(seconds=5)): def read(self, size):
"""Reads (receives) a whole block of 'size bytes """Reads (receives) a whole block of 'size bytes
from the connected peer. from the connected peer.
@ -94,25 +82,10 @@ class TcpClient:
and it's waiting for more, the timeout will NOT cancel the and it's waiting for more, the timeout will NOT cancel the
operation. Set to None for no timeout operation. Set to None for no timeout
""" """
# TODO Remove the timeout from this method, always use previous one
# Ensure that only one thread can receive data at once
with self._lock:
# Ensure it is not cancelled at first, so we can enter the loop
self.cancelled.clear()
# Set the starting time so we can
# calculate whether the timeout should fire
start_time = datetime.now() if timeout is not None else None
with BufferedWriter(BytesIO(), buffer_size=size) as buffer: with BufferedWriter(BytesIO(), buffer_size=size) as buffer:
bytes_left = size bytes_left = size
while bytes_left != 0: while bytes_left != 0:
# Only do cancel if no data was read yet
# Otherwise, carry on reading and finish
if self.cancelled.is_set() and bytes_left == size:
raise ReadCancelledError()
try:
partial = self._socket.recv(bytes_left) partial = self._socket.recv(bytes_left)
if len(partial) == 0: if len(partial) == 0:
self.close() self.close()
@ -122,22 +95,6 @@ class TcpClient:
buffer.write(partial) buffer.write(partial)
bytes_left -= len(partial) bytes_left -= len(partial)
except BlockingIOError as error:
# No data available yet, sleep a bit
time.sleep(self.delay)
# Check if the timeout finished
if timeout is not None:
time_passed = datetime.now() - start_time
if time_passed > timeout:
raise TimeoutError(
'The read operation exceeded the timeout.') from error
# If everything went fine, return the read bytes # If everything went fine, return the read bytes
buffer.flush() buffer.flush()
return buffer.raw.getvalue() return buffer.raw.getvalue()
def cancel_read(self):
"""Cancels the read operation IF it hasn't yet
started, raising a ReadCancelledError"""
self.cancelled.set()

View File

@ -22,13 +22,12 @@ class Connection:
self.ip = ip self.ip = ip
self.port = port self.port = port
self._mode = mode self._mode = mode
self.timeout = timeout
self._send_counter = 0 self._send_counter = 0
self._aes_encrypt, self._aes_decrypt = None, None self._aes_encrypt, self._aes_decrypt = None, None
# TODO Rename "TcpClient" as some sort of generic socket? # TODO Rename "TcpClient" as some sort of generic socket?
self.conn = TcpClient(proxy=proxy) self.conn = TcpClient(proxy=proxy, timeout=timeout)
# Sending messages # Sending messages
if mode == 'tcp_full': if mode == 'tcp_full':
@ -53,8 +52,7 @@ class Connection:
def connect(self): def connect(self):
self._send_counter = 0 self._send_counter = 0
self.conn.connect(self.ip, self.port, self.conn.connect(self.ip, self.port)
timeout=round(self.timeout.seconds))
if self._mode == 'tcp_abridged': if self._mode == 'tcp_abridged':
self.conn.write(b'\xef') self.conn.write(b'\xef')
@ -96,24 +94,18 @@ class Connection:
def close(self): def close(self):
self.conn.close() self.conn.close()
def cancel_receive(self):
"""Cancels (stops) trying to receive from the
remote peer and raises a ReadCancelledError"""
self.conn.cancel_read()
def get_client_delay(self): def get_client_delay(self):
"""Gets the client read delay""" """Gets the client read delay"""
return self.conn.delay return self.conn.delay
# region Receive message implementations # region Receive message implementations
def recv(self, **kwargs): def recv(self):
"""Receives and unpacks a message""" """Receives and unpacks a message"""
# TODO Don't ignore kwargs['timeout']?
# Default implementation is just an error # Default implementation is just an error
raise ValueError('Invalid connection mode specified: ' + self._mode) raise ValueError('Invalid connection mode specified: ' + self._mode)
def _recv_tcp_full(self, **kwargs): def _recv_tcp_full(self):
packet_length_bytes = self.read(4) packet_length_bytes = self.read(4)
packet_length = int.from_bytes(packet_length_bytes, 'little') packet_length = int.from_bytes(packet_length_bytes, 'little')
@ -129,10 +121,10 @@ class Connection:
return body return body
def _recv_intermediate(self, **kwargs): def _recv_intermediate(self):
return self.read(int.from_bytes(self.read(4), 'little')) return self.read(int.from_bytes(self.read(4), 'little'))
def _recv_abridged(self, **kwargs): def _recv_abridged(self):
length = int.from_bytes(self.read(1), 'little') length = int.from_bytes(self.read(1), 'little')
if length >= 127: if length >= 127:
length = int.from_bytes(self.read(3) + b'\0', 'little') length = int.from_bytes(self.read(3) + b'\0', 'little')
@ -185,11 +177,11 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + self._mode) raise ValueError('Invalid connection mode specified: ' + self._mode)
def _read_plain(self, length): def _read_plain(self, length):
return self.conn.read(length, timeout=self.timeout) return self.conn.read(length)
def _read_obfuscated(self, length): def _read_obfuscated(self, length):
return self._aes_decrypt.encrypt( return self._aes_decrypt.encrypt(
self.conn.read(length, timeout=self.timeout) self.conn.read(length)
) )
# endregion # endregion

View File

@ -1,6 +1,5 @@
import gzip import gzip
from datetime import timedelta from threading import RLock, Thread
from threading import RLock
from .. import helpers as utils from .. import helpers as utils
from ..crypto import AES from ..crypto import AES
@ -14,9 +13,22 @@ logging.getLogger(__name__).addHandler(logging.NullHandler())
class MtProtoSender: class MtProtoSender:
"""MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)""" """MTProto Mobile Protocol sender
(https://core.telegram.org/mtproto/description)
"""
def __init__(self, connection, session): def __init__(self, connection, session, constant_read):
"""Creates a new MtProtoSender configured to send messages through
'connection' and using the parameters from 'session'.
If 'constant_read' is set to True, another thread will be
created and started upon connection to constantly read
from the other end. Otherwise, manual calls to .receive()
must be performed. The MtProtoSender cannot be connected,
or an error will be thrown.
This way, sending and receiving will be completely independent.
"""
self.connection = connection self.connection = connection
self.session = session self.session = session
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
@ -31,16 +43,45 @@ class MtProtoSender:
# TODO There might be a better way to handle msgs_ack requests # TODO There might be a better way to handle msgs_ack requests
self.logging_out = False self.logging_out = False
# Will create a new _recv_thread when connecting if set
self._constant_read = constant_read
self._recv_thread = None
# Every unhandled result gets passed to these callbacks, which
# should be functions accepting a single parameter: a TLObject.
# This should only be Update(s), although it can actually be any type.
#
# The thread from which these callbacks are called can be any.
#
# The creator of the MtProtoSender is responsible for setting this
# to point to the list wherever their callbacks reside.
self.unhandled_callbacks = None
def connect(self): def connect(self):
"""Connects to the server""" """Connects to the server"""
if not self.is_connected():
self.connection.connect() self.connection.connect()
if self._constant_read:
self._recv_thread = Thread(
name='ReadThread', daemon=True,
target=self._recv_thread_impl
)
self._recv_thread.start()
def is_connected(self): def is_connected(self):
return self.connection.is_connected() return self.connection.is_connected()
def disconnect(self): def disconnect(self):
"""Disconnects from the server""" """Disconnects from the server"""
if self.is_connected():
self.connection.close() self.connection.close()
if self._constant_read:
# The existing thread will close eventually, since it's
# only running while the MtProtoSender.is_connected()
self._recv_thread = None
def is_constant_read(self):
return self._constant_read
# region Send and receive # region Send and receive
@ -76,57 +117,31 @@ class MtProtoSender:
del self._need_confirmation[:] del self._need_confirmation[:]
def receive(self, request=None, updates=None, **kwargs): def _recv_thread_impl(self):
"""Receives the specified MTProtoRequest ("fills in it" while self.is_connected():
the received data). This also restores the updates thread. try:
self.receive()
except TimeoutError:
# No problem.
pass
An optional named parameter 'timeout' can be specified if def receive(self):
one desires to override 'self.connection.timeout'. """Receives a single message from the connected endpoint.
If 'request' is None, a single item will be read into This method returns nothing, and will only affect other parts
the 'updates' list (which cannot be None). of the MtProtoSender such as the updates callback being fired
or a pending request being confirmed.
If 'request' is not None, any update received before
reading the request's result will be put there unless
it's None, in which case updates will be ignored.
""" """
if request is None and updates is None: # TODO Don't ignore updates
raise ValueError('Both the "request" and "updates"' self._logger.debug('Receiving a message...')
'parameters cannot be None at the same time.') body = self.connection.recv()
with self._lock:
self._logger.debug('receive() acquired the lock')
# Don't stop trying to receive until we get the request we wanted
# or, if there is no request, until we read an update
while (request and not request.confirm_received) or \
(not request and not updates):
self._logger.debug('Trying to .receive() the request result...')
body = self.connection.recv(**kwargs)
message, remote_msg_id, remote_seq = self._decode_msg(body) message, remote_msg_id, remote_seq = self._decode_msg(body)
with BinaryReader(message) as reader: with BinaryReader(message) as reader:
self._process_msg( self._process_msg(
remote_msg_id, remote_seq, reader, updates) remote_msg_id, remote_seq, reader, updates=None)
# We're done receiving, remove the request from pending, if any self._logger.debug('Received message.')
if request:
try:
self._pending_receive.remove(request)
except ValueError: pass
self._logger.debug('Request result received')
self._logger.debug('receive() released the lock')
def receive_updates(self, **kwargs):
"""Wrapper for .receive(request=None, updates=[])"""
updates = []
self.receive(updates=updates, **kwargs)
return updates
def cancel_receive(self):
"""Cancels any pending receive operation
by raising a ReadCancelledError"""
self.connection.cancel_receive()
# endregion # endregion
@ -230,20 +245,19 @@ class MtProtoSender:
if self.logging_out: if self.logging_out:
self._logger.debug('Message ack confirmed a request') self._logger.debug('Message ack confirmed a request')
r.confirm_received = True r.confirm_received.set()
return True return True
# If the code is not parsed manually, then it was parsed by the code generator! # If the code is not parsed manually then it should be a TLObject.
# In this case, we will simply treat the incoming TLObject as an Update,
# if we can first find a matching TLObject
if code in tlobjects: if code in tlobjects:
result = reader.tgread_object() result = reader.tgread_object()
if updates is None: if self.unhandled_callbacks:
self._logger.debug('Ignored update for %s', repr(result)) self._logger.debug('Passing TLObject to callbacks %s', repr(result))
for callback in self.unhandled_callbacks:
callback(result)
else: else:
self._logger.debug('Read update for %s', repr(result)) self._logger.debug('Ignoring unhandled TLObject %s', repr(result))
updates.append(result)
return True return True
@ -264,7 +278,7 @@ class MtProtoSender:
if r.request_msg_id == received_msg_id) if r.request_msg_id == received_msg_id)
self._logger.debug('Pong confirmed a request') self._logger.debug('Pong confirmed a request')
request.confirm_received = True request.confirm_received.set()
except StopIteration: pass except StopIteration: pass
return True return True
@ -338,8 +352,6 @@ class MtProtoSender:
try: try:
request = next(r for r in self._pending_receive request = next(r for r in self._pending_receive
if r.request_msg_id == request_id) if r.request_msg_id == request_id)
request.confirm_received = True
except StopIteration: except StopIteration:
request = None request = None
@ -358,13 +370,12 @@ class MtProtoSender:
self._need_confirmation.append(request_id) self._need_confirmation.append(request_id)
self._send_acknowledges() self._send_acknowledges()
self._logger.debug('Read RPC error: %s', str(error))
if isinstance(error, InvalidDCError):
# Must resend this request, if any
if request: if request:
request.confirm_received = False request.error = error
request.confirm_received.set()
raise error # else TODO Where should this error be reported?
# Read may be async. Can an error not-belong to a request?
self._logger.debug('Read RPC error: %s', str(error))
else: else:
if request: if request:
self._logger.debug('Reading request response') self._logger.debug('Reading request response')
@ -376,6 +387,7 @@ class MtProtoSender:
reader.seek(-4) reader.seek(-4)
request.on_response(reader) request.on_response(reader)
request.confirm_received.set()
return True return True
else: else:
# If it's really a result for RPC from previous connection # If it's really a result for RPC from previous connection

View File

@ -1,5 +1,5 @@
import logging import logging
import pyaes from time import sleep
from datetime import timedelta from datetime import timedelta
from hashlib import md5 from hashlib import md5
from os import path from os import path
@ -83,6 +83,12 @@ class TelegramBareClient:
# the time since it's a (somewhat expensive) process. # the time since it's a (somewhat expensive) process.
self._cached_clients = {} self._cached_clients = {}
# Update callbacks (functions accepting a single TLObject) go here
#
# Note that changing the list to which this variable points to
# will not reflect the changes on the existing senders.
self._update_callbacks = []
# These will be set later # These will be set later
self.dc_options = None self.dc_options = None
self._sender = None self._sender = None
@ -91,7 +97,8 @@ class TelegramBareClient:
# region Connecting # region Connecting
def connect(self, exported_auth=None, initial_query=None): def connect(self, exported_auth=None, initial_query=None,
constant_read=False):
"""Connects to the Telegram servers, executing authentication if """Connects to the Telegram servers, executing authentication if
required. Note that authenticating to the Telegram servers is required. Note that authenticating to the Telegram servers is
not the same as authenticating the desired user itself, which not the same as authenticating the desired user itself, which
@ -103,6 +110,9 @@ class TelegramBareClient:
If 'initial_query' is not None, it will override the default If 'initial_query' is not None, it will override the default
'GetConfigRequest()', and its result will be returned ONLY 'GetConfigRequest()', and its result will be returned ONLY
if the client wasn't connected already. if the client wasn't connected already.
The 'constant_read' parameter will be used when creating
the MtProtoSender. Refer to it for more information.
""" """
if self._sender and self._sender.is_connected(): if self._sender and self._sender.is_connected():
# Try sending a ping to make sure we're connected already # Try sending a ping to make sure we're connected already
@ -129,7 +139,10 @@ class TelegramBareClient:
self.session.save() self.session.save()
self._sender = MtProtoSender(connection, self.session) self._sender = MtProtoSender(
connection, self.session, constant_read=constant_read
)
self._sender.unhandled_callbacks = self._update_callbacks
self._sender.connect() self._sender.connect()
# Now it's time to send an InitConnectionRequest # Now it's time to send an InitConnectionRequest
@ -204,30 +217,6 @@ class TelegramBareClient:
# endregion # endregion
# region Properties
def set_timeout(self, timeout):
if timeout is None:
self._timeout = None
elif isinstance(timeout, int) or isinstance(timeout, float):
self._timeout = timedelta(seconds=timeout)
elif isinstance(timeout, timedelta):
self._timeout = timeout
else:
raise ValueError(
'{} is not a valid type for a timeout'.format(type(timeout))
)
if self._sender:
self._sender.transport.timeout = self._timeout
def get_timeout(self):
return self._timeout
timeout = property(get_timeout, set_timeout)
# endregion
# region Working with different Data Centers # region Working with different Data Centers
def _get_dc(self, dc_id, ipv6=False, cdn=False): def _get_dc(self, dc_id, ipv6=False, cdn=False):
@ -318,7 +307,18 @@ class TelegramBareClient:
try: try:
self._sender.send(request) self._sender.send(request)
self._sender.receive(request, updates=updates) if self._sender.is_constant_read():
# TODO This will be slightly troublesome if we allow
# switching between constant read or not on the fly.
# Must also watch out for calling .read() from two places,
# in which case a Lock would be required for .receive().
request.confirm_received.wait() # TODO Optional timeout here?
else:
while not request.confirm_received.is_set():
self._sender.receive()
if request.rpc_error:
raise request.rpc_error
return request.result return request.result
except ConnectionResetError: except ConnectionResetError:

View File

@ -98,14 +98,6 @@ class TelegramClient(TelegramBareClient):
# Safety across multiple threads (for the updates thread) # Safety across multiple threads (for the updates thread)
self._lock = RLock() self._lock = RLock()
# Updates-related members
self._update_handlers = []
self._updates_thread_running = Event()
self._updates_thread_receiving = Event()
self._next_ping_at = 0
self.ping_interval = 60 # Seconds
# Used on connection - the user may modify these and reconnect # Used on connection - the user may modify these and reconnect
kwargs['app_version'] = kwargs.get('app_version', self.__version__) kwargs['app_version'] = kwargs.get('app_version', self.__version__)
for name, value in kwargs.items(): for name, value in kwargs.items():
@ -129,24 +121,22 @@ class TelegramClient(TelegramBareClient):
not the same as authenticating the desired user itself, which not the same as authenticating the desired user itself, which
may require a call (or several) to 'sign_in' for the first time. may require a call (or several) to 'sign_in' for the first time.
The specified timeout will be used on internal .invoke()'s.
*args will be ignored. *args will be ignored.
""" """
result = super().connect() # The main TelegramClient is the only one that will have
# constant_read, since it's also the only one who receives
# Checking if there are update_handlers and if true, start running updates thread. # updates and need to be processed as soon as they occur.
# This situation may occur on reconnecting. #
if result and self._update_handlers: # TODO Allow to disable this to avoid the creation of a new thread
self._set_updates_thread(running=True) # if the user is not going to work with updates at all? Whether to
# read constantly or not for updates needs to be known before hand,
return result # and further updates won't be able to be added unless allowing to
# switch the mode on the fly.
return super().connect(constant_read=True)
def disconnect(self): def disconnect(self):
"""Disconnects from the Telegram server """Disconnects from the Telegram server
and stops all the spawned threads""" and stops all the spawned threads"""
self._set_updates_thread(running=False)
super().disconnect() super().disconnect()
# Also disconnect all the cached senders # Also disconnect all the cached senders
@ -159,7 +149,7 @@ class TelegramClient(TelegramBareClient):
# region Working with different connections # region Working with different connections
def create_new_connection(self, on_dc=None): def create_new_connection(self, on_dc=None, timeout=timedelta(seconds=5)):
"""Creates a new connection which can be used in parallel """Creates a new connection which can be used in parallel
with the original TelegramClient. A TelegramBareClient with the original TelegramClient. A TelegramBareClient
will be returned already connected, and the caller is will be returned already connected, and the caller is
@ -173,7 +163,9 @@ class TelegramClient(TelegramBareClient):
""" """
if on_dc is None: if on_dc is None:
client = TelegramBareClient( client = TelegramBareClient(
self.session, self.api_id, self.api_hash, proxy=self.proxy) self.session, self.api_id, self.api_hash,
proxy=self.proxy, timeout=timeout
)
client.connect() client.connect()
else: else:
client = self._get_exported_client(on_dc, bypass_cache=True) client = self._get_exported_client(on_dc, bypass_cache=True)
@ -187,29 +179,13 @@ class TelegramClient(TelegramBareClient):
def invoke(self, request, *args): def invoke(self, request, *args):
"""Invokes (sends) a MTProtoRequest and returns (receives) its result. """Invokes (sends) a MTProtoRequest and returns (receives) its result.
An optional timeout can be specified to cancel the operation if no
result is received within such time, or None to disable any timeout.
*args will be ignored. *args will be ignored.
""" """
if self._updates_thread_receiving.is_set():
self._sender.cancel_receive()
try: try:
self._lock.acquire() self._lock.acquire()
updates = [] if self._update_handlers else None
result = super().invoke(
request, updates=updates
)
if updates:
for update in updates:
for handler in self._update_handlers:
handler(update)
# TODO Retry if 'result' is None? # TODO Retry if 'result' is None?
return result return super().invoke(request)
except (PhoneMigrateError, NetworkMigrateError, UserMigrateError) as e: except (PhoneMigrateError, NetworkMigrateError, UserMigrateError) as e:
self._logger.debug('DC error when invoking request, ' self._logger.debug('DC error when invoking request, '
@ -399,8 +375,8 @@ class TelegramClient(TelegramBareClient):
no_webpage=not link_preview no_webpage=not link_preview
) )
result = self(request) result = self(request)
for handler in self._update_handlers: for callback in self._update_callbacks:
handler(result) callback(result)
return request.random_id return request.random_id
def get_message_history(self, def get_message_history(self,
@ -891,110 +867,12 @@ class TelegramClient(TelegramBareClient):
def add_update_handler(self, handler): def add_update_handler(self, handler):
"""Adds an update handler (a function which takes a TLObject, """Adds an update handler (a function which takes a TLObject,
an update, as its parameter) and listens for updates""" an update, as its parameter) and listens for updates"""
if not self._sender: self._update_callbacks.append(handler)
raise RuntimeError("You can't add update handlers until you've "
"successfully connected to the server.")
first_handler = not self._update_handlers
self._update_handlers.append(handler)
if first_handler:
self._set_updates_thread(running=True)
def remove_update_handler(self, handler): def remove_update_handler(self, handler):
self._update_handlers.remove(handler) self._update_callbacks.remove(handler)
if not self._update_handlers:
self._set_updates_thread(running=False)
def list_update_handlers(self): def list_update_handlers(self):
return self._update_handlers[:] return self._update_callbacks[:]
def _set_updates_thread(self, running):
"""Sets the updates thread status (running or not)"""
if running == self._updates_thread_running.is_set():
return
# Different state, update the saved value and behave as required
self._logger.debug('Changing updates thread running status to %s', running)
if running:
self._updates_thread_running.set()
if not self._updates_thread:
self._updates_thread = Thread(
name='UpdatesThread', daemon=True,
target=self._updates_thread_method)
self._updates_thread.start()
else:
self._updates_thread_running.clear()
if self._updates_thread_receiving.is_set():
self._sender.cancel_receive()
def _updates_thread_method(self):
"""This method will run until specified and listen for incoming updates"""
# Set a reasonable timeout when checking for updates
timeout = timedelta(minutes=1)
while self._updates_thread_running.is_set():
# Always sleep a bit before each iteration to relax the CPU,
# since it's possible to early 'continue' the loop to reach
# the next iteration, but we still should to sleep.
sleep(0.1)
with self._lock:
self._logger.debug('Updates thread acquired the lock')
try:
self._updates_thread_receiving.set()
self._logger.debug(
'Trying to receive updates from the updates thread'
)
if time() > self._next_ping_at:
self._next_ping_at = time() + self.ping_interval
self(PingRequest(utils.generate_random_long()))
updates = self._sender.receive_updates(timeout=timeout)
self._updates_thread_receiving.clear()
self._logger.debug(
'Received {} update(s) from the updates thread'
.format(len(updates))
)
for update in updates:
for handler in self._update_handlers:
handler(update)
except ConnectionResetError:
self._logger.debug('Server disconnected us. Reconnecting...')
self.reconnect()
except TimeoutError:
self._logger.debug('Receiving updates timed out')
except ReadCancelledError:
self._logger.debug('Receiving updates cancelled')
except BrokenPipeError:
self._logger.debug('Tcp session is broken. Reconnecting...')
self.reconnect()
except InvalidChecksumError:
self._logger.debug('MTProto session is broken. Reconnecting...')
self.reconnect()
except OSError:
self._logger.debug('OSError on updates thread, %s logging out',
'was' if self._sender.logging_out else 'was not')
if self._sender.logging_out:
# This error is okay when logging out, means we got disconnected
# TODO Not sure why this happens because we call disconnect()...
self._set_updates_thread(running=False)
else:
raise
self._logger.debug('Updates thread released the lock')
# Thread is over, so clean unset its variable
self._updates_thread = None
# endregion # endregion

View File

@ -1,4 +1,5 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from threading import Event
class TLObject: class TLObject:
@ -10,7 +11,8 @@ class TLObject:
self.dirty = False self.dirty = False
self.send_time = None self.send_time = None
self.confirm_received = False self.confirm_received = Event()
self.rpc_error = None
# These should be overrode # These should be overrode
self.constructor_id = 0 self.constructor_id = 0
@ -23,11 +25,11 @@ class TLObject:
self.sent = True self.sent = True
def on_confirm(self): def on_confirm(self):
self.confirm_received = True self.confirm_received.set()
def need_resend(self): def need_resend(self):
return self.dirty or ( return self.dirty or (
self.content_related and not self.confirm_received and self.content_related and not self.confirm_received.is_set() and
datetime.now() - self.send_time > timedelta(seconds=3)) datetime.now() - self.send_time > timedelta(seconds=3))
@staticmethod @staticmethod