forked from aewens/abots
166 lines
5.9 KiB
Python
Executable File
166 lines
5.9 KiB
Python
Executable File
"""
|
|
|
|
Socket Server Handlers
|
|
======================
|
|
|
|
The socket server was made to be versitile where the handler can be swapped out
|
|
in favor of another handler. This handler is the default provided if one is not
|
|
passed to get the basic client-server relationship working for either starting
|
|
with something simple, testing, and/or providing a template to build other
|
|
handlers from.
|
|
|
|
"""
|
|
|
|
class SocketServerHandler:
|
|
def __init__(self, server):
|
|
self.server = server
|
|
|
|
# These are used when processing inbox messages
|
|
self.send_verb = "SEND"
|
|
self.broadcast_verb = "CAST"
|
|
|
|
self.close_verb = "STOP"
|
|
|
|
# Tells all clients that a node joined the socket server
|
|
def open_client(self, alias):
|
|
alias = message[5:]
|
|
client = self.server.clients.get(alias, None)
|
|
if client is None:
|
|
return 1
|
|
self.server.broadcast_message(client, message)
|
|
return 0
|
|
|
|
# Informs the other clients a client left and closes that client's socket
|
|
def close_client(self, alias):
|
|
client = self.server.clients.get(alias, None)
|
|
if client is None:
|
|
return 1
|
|
message = "LEFT {}".format(alias)
|
|
self.server.broadcast_message(self.server.sock, message)
|
|
self.server.close_sock(alias)
|
|
return 0
|
|
|
|
# Lets the clients know the server is intentionally closing
|
|
def close_server(self):
|
|
self.server.broadcast_message(self.server.sock, self.close_verb)
|
|
return -1
|
|
|
|
# Sends a heartbeat to the client to detect if it is still responding
|
|
def send_heartbeat(self, alias):
|
|
client = self.server.clients.get(alias, None)
|
|
if client is None:
|
|
return 1
|
|
sock = client.get("sock", None)
|
|
if sock is None:
|
|
return 1
|
|
self.server.send_message(sock, "PING")
|
|
return 0
|
|
|
|
# Format a message before sending to client(s)
|
|
# Prepends message size code along with replacing variables in message
|
|
def format_message(self, message, *args):
|
|
formatted = None
|
|
if len(args) > 0:
|
|
formatted = message.format(*args) + self.server.end_of_line
|
|
else:
|
|
formatted = message + self.server.end_of_line
|
|
|
|
# Puts message size at the front of the message
|
|
prefixed = pack(">I", len(formatted)) + formatted.encode()
|
|
return prefixed
|
|
|
|
# Get message from socket with `format_message` in mind
|
|
def get_message(self, sock):
|
|
raw_message_size = self.server.receive_bytes(sock, 4, False)
|
|
if raw_message_size is None:
|
|
return None
|
|
message_size = unpack(">I", raw_message_size)[0]
|
|
if self.max_message_size > 0 and message_size > self.max_message_size:
|
|
return None
|
|
eol = self.server.end_of_line
|
|
return self.server.receive_bytes(sock, message_size).strip(eol)
|
|
|
|
# Takes the server object, the client socket, and a message to process
|
|
# Each message returns a status code:
|
|
# -1 : Going offline
|
|
# 0 : Success
|
|
# 1 : Failure
|
|
# 2 : Invalid
|
|
def message(self, sock, message):
|
|
# print("DEBUG:", message)
|
|
|
|
send = self.send_verb + " "
|
|
cast = self.broadcast_verb + " "
|
|
send_size = len(send)
|
|
cast_size = len(cast)
|
|
|
|
# React to heartbeat from client
|
|
if message == "PONG":
|
|
client_fd = self.server.get_client_fd(sock)
|
|
client = self.server.clients.get(client_fd, dict())
|
|
client_alive = client.get("alive", None)
|
|
if client_aliave is None:
|
|
return 1
|
|
elif client_alive:
|
|
return 1
|
|
# Setting this to True is what tells the server the heartbeat worked
|
|
client["alive"] = True
|
|
return 0
|
|
|
|
# Tell the clients to stop before server itself stops
|
|
elif message == self.close_verb:
|
|
status = self.close_server()
|
|
self.server.stop()
|
|
return status
|
|
|
|
# Informs the other clients one left and closes that client's socket
|
|
elif message == "QUIT":
|
|
client_alias = self.server.get_client_alias_by_sock(sock)
|
|
if client_alias is None:
|
|
return 1
|
|
return self.close_client(client_alias)
|
|
|
|
# Lists all client alises, puts itself first and the server second
|
|
elif message == "LIST":
|
|
aliases = list()
|
|
client_alias = self.server.get_client_alias_by_sock(sock)
|
|
if client_alias is None:
|
|
return 1
|
|
self.server_alias = self.server.sock_alias
|
|
for alias in self.server.clients.keys():
|
|
# We need to skip ints since the file descriptors are also keys
|
|
if type(alias) is int:
|
|
continue
|
|
# The server and sending client are skipped to retain ordering
|
|
elif alias == self.server.sock_alias:
|
|
continue
|
|
elif alias == client_alias:
|
|
continue
|
|
else:
|
|
aliases.append(alias)
|
|
listed = ",".join([client_alias, self.server_alias] + aliases)
|
|
self.server.send_message(sock, listed)
|
|
return 0
|
|
|
|
# Sends a message to the client with the specified client (via an alias)
|
|
elif message[:send_size] == send:
|
|
params = message[(send_size + 1):].split(" ", 1)
|
|
if len(params) < 2:
|
|
return 1
|
|
alias, response = params
|
|
client = self.server.clients.get(alias, dict())
|
|
client_sock = client.get("sock", None)
|
|
if client_sock is None:
|
|
return 1
|
|
self.server.send_message(client_sock, response)
|
|
return 0
|
|
|
|
# Broadcasts a message to all other clients
|
|
elif message[:cast_size] == cast:
|
|
response = message[(cast_size + 1):]
|
|
self.server.broadcast_message(sock, response)
|
|
return 0
|
|
|
|
# All other commands are invalid
|
|
else:
|
|
return 2 |