206 lines
6.2 KiB
Python
Executable File
206 lines
6.2 KiB
Python
Executable File
"""
|
|
|
|
Socket Client
|
|
=============
|
|
|
|
|
|
|
|
"""
|
|
|
|
from abots.helpers import eprint, cast, jots, jsto, utc_now_timestamp, coroutine
|
|
|
|
from struct import pack, unpack
|
|
from socket import socket, timeout as sock_timeout
|
|
from socket import AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR
|
|
from ssl import wrap_socket
|
|
from threading import Thread, Event
|
|
from queue import Queue, Empty
|
|
from time import sleep
|
|
from random import randint
|
|
|
|
class SocketClient(Thread):
|
|
def __init__(self, host, port, buffer_size=4096, secure=False,
|
|
timeout=None, daemon=False, reconnects=10):
|
|
super().__init__()
|
|
self.setDaemon(daemon)
|
|
|
|
self.host = host
|
|
self.port = port
|
|
self.buffer_size = buffer_size
|
|
self.secure = secure
|
|
self.timeout = timeout
|
|
self.reconnects = reconnects
|
|
self.sock = socket(AF_INET, SOCK_STREAM)
|
|
if self.secure:
|
|
self.sock = wrap_socket(self.sock)
|
|
|
|
self.connection = (self.host, self.port)
|
|
self.running = True
|
|
|
|
self.kill_switch = Event()
|
|
self.ready = Event()
|
|
self.stopped = Event()
|
|
self.broken = Event()
|
|
self.reconnecting = Event()
|
|
|
|
self._inbox = Queue()
|
|
self._events = Queue()
|
|
self._outbox = Queue()
|
|
self.queues = dict()
|
|
self.queues["inbox"] = self._inbox
|
|
self.queues["outbox"] = self._outbox
|
|
self.queues["events"] = self._events
|
|
|
|
self._bridge = None
|
|
|
|
def _send_event(self, message):
|
|
self._events.put(jots(message))
|
|
cast(self._bridge, "send", ("events", message))
|
|
|
|
def _prepare(self):
|
|
self.sock.setblocking(False)
|
|
self.sock.settimeout(1)
|
|
try:
|
|
self.sock.connect(self.connection)
|
|
except Exception as e:
|
|
return True, e
|
|
return False, None
|
|
|
|
def _obtain(self, queue_name, timeout=False):
|
|
queue = self.queues[queue_name]
|
|
if timeout is False:
|
|
timeout = self.timeout
|
|
while True:
|
|
try:
|
|
if timeout is not None:
|
|
message = queue.get(timeout=timeout)
|
|
else:
|
|
message = queue.get_nowait()
|
|
yield message
|
|
cast(self._bridge, "send", (queue_name, message))
|
|
queue.task_done()
|
|
except Empty:
|
|
break
|
|
|
|
def _queue_thread(self, inbox, timeout):
|
|
while not self.kill_switch.is_set():
|
|
for message in self._obtain("inbox", timeout):
|
|
if self.broken.is_set():
|
|
self.reconnecting.wait()
|
|
self.send_message(message)
|
|
|
|
def _get_message(self, decode=True):
|
|
try:
|
|
packet = self.sock.recv(self.buffer_size)
|
|
result = packet.decode() if decode else packet
|
|
self._outbox.put(result)
|
|
cast(self._bridge, "send", ("outbox", result))
|
|
except (BrokenPipeError, OSError) as e:
|
|
pass
|
|
|
|
def _format_message(self, message, *args):
|
|
if len(args) > 0:
|
|
formatted = message.format(*args)
|
|
else:
|
|
formatted = message
|
|
return formatted.encode()
|
|
|
|
def _attempt_reconnect(self):
|
|
if self.kill_switch.is_set():
|
|
return
|
|
print("BROKEN!")
|
|
self.reconnecting.clear()
|
|
self.broken.set()
|
|
event = dict()
|
|
event["name"] = "socket-down"
|
|
event["data"] = dict()
|
|
event["data"]["when"] = utc_now_timestamp()
|
|
self._send_event(event)
|
|
attempts = 0
|
|
while attempts <= self.reconnects or not self.kill_switch.is_set():
|
|
# Need to be run to prevent ConnectionAbortedError
|
|
self.sock.__init__()
|
|
err, report = self._prepare()
|
|
if not err:
|
|
self.reconnecting.set()
|
|
self.broken.clear()
|
|
event = dict()
|
|
event["name"] = "socket-up"
|
|
event["data"] = dict()
|
|
event["data"]["when"] = utc_now_timestamp()
|
|
self._send_event(event)
|
|
return
|
|
# Exponential backoff
|
|
attempts = attempts + 1
|
|
max_delay = (2**attempts) - 1
|
|
delay = randint(0, max_delay)
|
|
sleep(delay)
|
|
self.stop()
|
|
|
|
def send_message(self, message, *args):
|
|
formatted = self._format_message(message, *args)
|
|
try:
|
|
self.sock.send(formatted)
|
|
except (BrokenPipeError, OSError) as e:
|
|
if not isinstance(e, sock_timeout):
|
|
self._attempt_reconnect()
|
|
self._attempt_reconnect()
|
|
|
|
def recv(self):
|
|
yield from self._obtain("outbox")
|
|
|
|
def check(self):
|
|
for letter in self.recv():
|
|
if letter is not None and len(letter) > 0:
|
|
print(letter)
|
|
|
|
def send(self, message):
|
|
self._inbox.put(message)
|
|
cast(self._bridge, "send", ("inbox", message))
|
|
|
|
def connect(self, bridge):
|
|
self._bridge = bridge()
|
|
|
|
@coroutine
|
|
def bridge(self, inbox, outbox, events):
|
|
try:
|
|
while True:
|
|
task = (yield)
|
|
source, message = task
|
|
if source == "inbox":
|
|
outbox.puts(message)
|
|
elif source == "outbox":
|
|
inbox.puts(message)
|
|
elif source == "events":
|
|
events.put(message)
|
|
except GeneratorExit:
|
|
pass
|
|
|
|
def run(self):
|
|
err, report = self._prepare()
|
|
if err:
|
|
eprint(report)
|
|
return report
|
|
queue_args = self._inbox, self.timeout
|
|
Thread(target=self._queue_thread, args=queue_args).start()
|
|
print("Client ready!")
|
|
self.ready.set()
|
|
while self.running:
|
|
if self.broken.is_set():
|
|
self.reconnecting.wait()
|
|
self._get_message()
|
|
|
|
def stop(self, done=None):
|
|
# print("Stopping client!")
|
|
self.kill_switch.set()
|
|
event = dict()
|
|
event["name"] = "closing"
|
|
event["data"] = dict()
|
|
event["data"]["when"] = utc_now_timestamp()
|
|
self._send_event(event)
|
|
self.running = False
|
|
self.sock.close()
|
|
self.stopped.set()
|
|
cast(done, "set")
|
|
# print("Stopped client!")
|