From 6912cc36b82d704992e94efabae849ffb97b1aa8 Mon Sep 17 00:00:00 2001
From: Michael Zanger <michaelzanger@Michaels-MacBook-Air.fritz.box>
Date: Fri, 12 Jan 2024 19:47:53 +0100
Subject: [PATCH] MacOS und Heartbeats

---
 chat_server.py |  33 ++++++++---
 main_server.py |  14 ++++-
 server.py      | 145 ++++++++++++++++++++++++++++++++++++++++++-------
 3 files changed, 161 insertions(+), 31 deletions(-)

diff --git a/chat_server.py b/chat_server.py
index 00b9172..8ad5315 100644
--- a/chat_server.py
+++ b/chat_server.py
@@ -4,7 +4,19 @@ import socket
 import re
 
 if __name__ == '__main__':
+    # Create an instance of the Server class
+    server_instance = Server(client_address="", server_id="", server_port=0, server_cache=dict(), clients_cache=dict())
+    
+    # Get the broadcast address from the existing server_instance
+    broadcast_address = server_instance.get_broadcast_address()
+    
+    if broadcast_address is None:
+        print("Failed to obtain broadcast address. Exiting.")
+        exit(1)
+    
+    print(broadcast_address)
 
+    os = Server.get_os_type()
     client_address = ""
     server_id = ""
     server_port = 0
@@ -13,21 +25,26 @@ if __name__ == '__main__':
 
     BROADCAST_PORT = 49154
 
-    print("im alive")
+    print("I'm alive")
     MSG = bytes("HI MAIN SERVER", 'utf-8')
 
     broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
     broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-    broadcast_socket.sendto(MSG, ('<broadcast>', BROADCAST_PORT))
-    print("successfully send message")
-    
+    broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    if os == "macOS":
+        broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+        broadcast_socket.sendto(MSG, (broadcast_address, BROADCAST_PORT))
+    else:
+        broadcast_socket.sendto(MSG, ('<broadcast>', BROADCAST_PORT))
+    print("Successfully sent message")
+
     message, server = broadcast_socket.recvfrom(1024)
     match = re.search(r'\b([A-Za-z])\b$', message.decode('utf-8'))
     server_id = match.group(1)
     
     print('Received message from server: ', message.decode('utf-8'))
     
-    server = Server(client_address, server_id, server_port, server_cache, clients_cache)
-    time.sleep(20)
-
-
+    # You can use the existing server_instance here, no need to create another one
+    server_instance.start()  # Start the server process
+    
+    time.sleep(20)
\ No newline at end of file
diff --git a/main_server.py b/main_server.py
index 3d3a325..3a460b3 100644
--- a/main_server.py
+++ b/main_server.py
@@ -2,13 +2,23 @@ from server import Server
 import time
 
 if __name__ == '__main__':
-
     client_address = ""
     server_id = "MAIN"
     server_port = 1000
     server_cache = dict()
     clients_cache = dict()
+
+    # Create the Server process
     server = Server(client_address, server_id, server_port, server_cache, clients_cache)
-    time.sleep(20)
 
+    # Start the Server process
+    server.start()
+
+    # Optionally, if you want the main program to wait for a certain duration 
+    # and then terminate the server, you can use:
+    # time.sleep(20)
+    # server.terminate()  # This will terminate the server process after 20 seconds
 
+    # If you want the main program to wait indefinitely until the server process
+    # completes its execution, use:
+    server.join()
diff --git a/server.py b/server.py
index e7be751..2f0ae56 100644
--- a/server.py
+++ b/server.py
@@ -3,11 +3,14 @@ import socket
 import threading
 import json
 import time
+import ipaddress
+import netifaces as ni
+import platform
 
 client_broadcast_listener_port = 49153
 server_broadcast_listener_port = 49154
 
-server_heartbeat_tcp_listener_port = 49155
+server_heartbeat_tcp_listener_port = 49152
 
 client_receive_chat_tcp_port = 50001
 client_forward_message_multicast_port = 51000
@@ -16,21 +19,90 @@ multicast_group_ip = '224.0.1.1'
 
 
 class Server(multiprocessing.Process):
-    #server_address = '127.0.0.1'
-    host = socket.gethostname()
-    server_address = socket.gethostbyname(host)
     client_cache_key_offset = 0
     local_servers_cache = dict()
     local_clients_cache = dict()
 
     def __init__(self, client_address, server_id, server_port, server_cache, clients_cache):
         super(Server, self).__init__()
+        self.os = self.get_os_type()
+        print(self.os)
+        self.active_interface = self.get_active_interface()
+        self.server_address = self.get_local_ip_address()
+        self.subnet_mask = self.get_subnet_mask(self.active_interface)
+        print(self.active_interface)
         self.client_address = client_address
         self.server_id = server_id
         self.server_port = server_port
         self.local_servers_cache = server_cache
         self.local_clients_cache = clients_cache
-        self.run( )
+
+    @staticmethod
+    def get_local_ip_address():
+        """ Get the local IP address of the machine. """
+        try:
+            with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
+                print("Attempting to connect to an external host for IP resolution...")
+                s.connect(("8.8.8.8", 80))  # Google's public DNS server
+                local_ip = s.getsockname()[0]
+                print(f"Local IP obtained: {local_ip}")
+                return local_ip
+        except Exception as e:
+            print(f"Error obtaining local IP address: {e}")
+            return '127.0.0.1'  # Fallback to localhost
+        
+    @staticmethod
+    def get_active_interface():
+        interfaces = ni.interfaces()
+        for interface in interfaces:
+            if interface != 'lo0':
+                addr = ni.ifaddresses(interface)
+                try:
+                    # Check if the interface has an IPv4 configuration
+                    if ni.AF_INET in addr:
+                        ipv4_info = addr[ni.AF_INET][0]
+                        ip = ipv4_info['addr']
+                        netmask = ipv4_info['netmask']
+
+                        # Optionally check for an active internet connection
+                        # This attempts to create a socket using the interface's IP
+                        try:
+                            with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
+                                s.bind((ip, 0))
+                                s.connect(("8.8.8.8", 80))  # Google's DNS server
+                                return interface
+                        except socket.error:
+                            pass  # Interface is not suitable
+
+                except KeyError:
+                    # Interface does not have IPv4 configuration
+                    pass
+
+        return None
+    
+    @staticmethod
+    def get_subnet_mask(interface):
+        try:
+            addr = ni.ifaddresses(interface)
+            if ni.AF_INET in addr:
+                ipv4_info = addr[ni.AF_INET][0]
+                subnet_mask = ipv4_info['netmask']
+                return subnet_mask
+            else:
+                return None  # Interface does not have IPv4 configuration
+        except KeyError:
+            return None  # Interface does not have IPv4 configuration
+        
+    @staticmethod
+    def get_os_type():
+        system = platform.system()
+        if system == "Windows":
+            return "Windows"
+        elif system == "Darwin":
+            return "macOS"
+        else:
+            return "Unknown"
+
 
     def run(self):
         print(self.server_id+": "+"Up and running")
@@ -52,11 +124,22 @@ class Server(multiprocessing.Process):
             cache_update_listener_thread.start()
             client_message_listener_thread.start()
             heartbeat_receive_thread.start()
+    
+    def get_broadcast_address(self):
+        IP = self.server_address
+        MASK = self.subnet_mask
+        host = ipaddress.IPv4Address(IP)
+        net = ipaddress.IPv4Network(IP + '/' + MASK, False)
+
+        print('Host:', ipaddress.IPv4Address(int(host) & int(net.hostmask)))
+        print('Broadcast:', net.broadcast_address)
+        broadcast_address = str(net.broadcast_address) 
+        return broadcast_address
             
     def send_heartbeat(self):
         while True:
             time.sleep(10)
-            for server_id, server_port in self.local_servers_cache.items():
+            for server_id in self.local_servers_cache.items():
                 if server_id != self.server_id:
                     acknowledgment_received = self.send_heartbeat_to_server(self.server_address, server_heartbeat_tcp_listener_port)
                     #acknowledgment_received = "YES"
@@ -69,6 +152,7 @@ class Server(multiprocessing.Process):
         acknowledgment_received = False
         try:
             with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+                s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
                 s.settimeout(2)  # Timeout for the connection
                 # Combine server address and port into a tuple
                 server_address_with_port = (server_address, server_port)
@@ -87,13 +171,13 @@ class Server(multiprocessing.Process):
         while True:
             try:
                 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+                    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
                     s.bind((self.server_address, server_heartbeat_tcp_listener_port))  # Bind to any available port
                     actual_port = s.getsockname()[1]
                     print(f"Heartbeat Listener Started on port {actual_port}")
                     s.listen()
                     conn, addr = s.accept()
                     with conn:
-                        print("Hallo")
                         data = conn.recv(1024)
                         if data == b'HEARTBEAT':
                             # Hier Code zum Verarbeiten des Heartbeats
@@ -116,18 +200,18 @@ class Server(multiprocessing.Process):
     def listen_for_servers(self):
 
         BROADCAST_PORT = 49154
-
-        # Local host information
-        MY_HOST = socket.gethostname()
-        MY_IP = socket.gethostbyname(MY_HOST)
+        BROADCAST_ADDRESS = self.get_broadcast_address()
 
         # Create a UDP socket
         listen_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         # Set the socket to broadcast and enable reusing addresses
         listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
         listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-        # Bind socket to address and port
-        listen_socket.bind((self.server_address, BROADCAST_PORT))
+        if self.os == "macOS":
+            listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+            listen_socket.bind((BROADCAST_ADDRESS, BROADCAST_PORT))
+        else:
+            listen_socket.bind(('', BROADCAST_PORT))
 
         print(self.server_id+": "+"Listening to server register broadcast messages")
 
@@ -150,9 +234,14 @@ class Server(multiprocessing.Process):
                     update_cache_thread.start()
 
     def register_server(self, addr, server_id):
+        BROADCAST_ADDRESS = self.get_broadcast_address()
         message = 'Hi ' + addr[0] + ' this is your chat-group ID: ' + str(server_id)
         server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-        server_socket.connect((self.server_address, self.server_port))
+        if self.os == "macOS":
+            server_socket.connect((BROADCAST_ADDRESS, self.server_port))
+        else:
+            server_socket.connect((self.server_address, self.server_port))
+            
         server_socket.sendto(str.encode(message), addr)
         server_socket.close()
         
@@ -160,18 +249,22 @@ class Server(multiprocessing.Process):
     def listen_for_clients(self):
         
         BROADCAST_PORT = 49153
+        BROADCAST_ADDRESS = self.get_broadcast_address()
 
         # Local host information
-        MY_HOST = socket.gethostname()
-        MY_IP = socket.gethostbyname(MY_HOST)
+        #MY_HOST = socket.gethostname()
+        #MY_IP = socket.gethostbyname(MY_HOST)
 
         # Create a UDP socket
         listen_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         # Set the socket to broadcast and enable reusing addresses
         listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
         listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-        # Bind socket to address and port
-        listen_socket.bind((self.server_address, BROADCAST_PORT))
+        if self.os == "macOS":
+            listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+            listen_socket.bind((BROADCAST_ADDRESS, BROADCAST_PORT))
+        else:
+            listen_socket.bind((self.server_address, BROADCAST_PORT))
 
         print(self.server_id+": "+"Listening to client register broadcast messages")
 
@@ -261,6 +354,7 @@ class Server(multiprocessing.Process):
 
     def updateCacheList(self):
         PORT = 5980
+        BROADCAST_ADDRESS = self.get_broadcast_address()
         servers_cache_as_string = json.dumps(self.local_servers_cache, indent=2).encode('utf-8')
         clients_cache_as_string = json.dumps(self.local_clients_cache, indent=2).encode('utf-8')
         separator = "_"
@@ -270,10 +364,15 @@ class Server(multiprocessing.Process):
         broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
 
-        broadcast_socket.sendto(MSG, ('<broadcast>', PORT))
+        if self.os == "macOS":
+            broadcast_socket.sendto(MSG, (BROADCAST_ADDRESS, PORT))
+            broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+        else:
+            broadcast_socket.sendto(MSG, ('<broadcast>', PORT))
         broadcast_socket.close()
 
     def listen_for_cache_update(self):
+        BROADCAST_ADDRESS = self.get_broadcast_address()
         BROADCAST_PORT = 5980
 
         # Local host information
@@ -285,8 +384,12 @@ class Server(multiprocessing.Process):
         # Set the socket to broadcast and enable reusing addresses
         listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
         listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-        # Bind socket to address and port
-        listen_socket.bind((MY_IP, BROADCAST_PORT))
+
+        if self.os == "macOS":
+            listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+            listen_socket.bind((BROADCAST_ADDRESS, BROADCAST_PORT))
+        else:
+            listen_socket.bind((MY_IP, BROADCAST_PORT))
 
         print(self.server_id+": "+"Listening to cache update broadcast messages")
 
-- 
GitLab