# compatibile Windows 11 # compatibile Ubuntu 24.10 # compatibile python 3.12.7 import socket import time import select import threading import errno import sys import queue import platform # Import specific for Windows input handling if platform.system() == "Windows": import msvcrt # --- START OF TCPClient CLASS (Same as before) --- class TCPClient: """ Un client TCP rigoroso per comunicazioni affidabili. """ def __init__(self, host, port, timeout=5): """ Inizializza l'oggetto TCPClient. Args: host (str): L'indirizzo IP o nome host del server. port (int): La porta del server. timeout (int, optional): Il timeout per le operazioni di socket in secondi. Il valore predefinito è 5 secondi. """ self.host = host self.port = port self.timeout = timeout # Timeout per le operazioni di I/O self.socket = None # Socket TCP self.receive_thread = None # Thread dedicato alla ricezione dei messaggi self.on_message = None # Callback per i messaggi ricevuti self.on_error = None # Callback per la gestione degli errori self.on_connect = None # Callback per la gestione della connessione self.on_disconnect = None # Callback per la gestione della disconnessione self.buffer = bytearray() # Buffer per accumulare i dati parziali ricevuti self._stop_receive = threading.Event() # Evento per segnalare al thread di ricezione di terminare self._closing = False # Flag che indica se il client è in fase di chiusura def connect(self): """ Connette il client al server. """ try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Crea un socket TCP self.socket.settimeout(self.timeout) # imposta il timeout della socket self.socket.connect((self.host, self.port)) # Connette al server self._start_receive_thread() # Avvia il thread per la ricezione dei dati if self.on_connect: # Se è presente una funzione di callback per la connessione self.on_connect(self) # Invoca la funzione di callback con l'oggetto client come parametro # print(f"Client connesso al server {self.host}:{self.port}") # Moved to handle_connect except socket.timeout: self._handle_error(f"Timeout durante la connessione a {self.host}:{self.port}") self.close() # Chiude la socket in caso di timeout except socket.error as e: self._handle_error(f"Errore socket durante la connessione: {e}") self.close() # Chiude la socket in caso di errore except Exception as e: self._handle_error(f"Errore generico durante la connessione: {e}") self.close() # Chiude la socket in caso di errore def _start_receive_thread(self): """Avvia il thread dedicato alla ricezione dei messaggi. """ self.receive_thread = threading.Thread(target=self._receive_loop) # Crea il thread per la ricezione dei dati self._stop_receive.clear() # Resetta l'evento self.receive_thread.start() # Avvia il thread di ricezione def _receive_loop(self): """Loop che riceve dati dal server fino alla chiusura della connessione. """ try: while not self._stop_receive.is_set() and self.socket: try: # Use a shorter timeout for select to make the loop more responsive to _stop_receive ready = select.select([self.socket], [], [], 0.5) if ready[0]: data = self.socket.recv(4096) if not data: # Connection closed by server if not self._closing: # Avoid handling disconnect if we are closing deliberately self._handle_disconnect() break self.buffer.extend(data) self._process_buffer() # No need for else, select handles the timeout except (socket.error, OSError) as e: if self._stop_receive.is_set() or self._closing: # Expected error during shutdown break elif isinstance(e, socket.timeout): # select timeout, just continue the loop continue # Check for specific non-blocking errors if socket was set non-blocking (it's not here) # elif e.errno in (errno.EWOULDBLOCK, errno.EAGAIN): # time.sleep(0.01) # Short pause if non-blocking recv would block # continue elif e.errno == errno.ECONNRESET: if not self._closing: self._handle_error(f"Connessione resettata dal server: {e}") self._handle_disconnect() break elif e.errno == errno.ESHUTDOWN or getattr(e, 'winerror', 0) == 10058: # WSAESHUTDOWN # Socket has been shut down (likely during close) if not self._closing: self._handle_error(f"Socket già chiuso o in shutdown: {e}") self._handle_disconnect() break else: # Other socket/OS errors if not self._closing: self._handle_error(f"Errore socket/OS durante la ricezione: {e} (errno={e.errno})") self._handle_disconnect() # Assume disconnect on unknown error break # Exit loop on other errors except Exception as ex: # Catch any other unexpected error in the receive loop if not self._closing: self._handle_error(f"Errore generico imprevisto nel thread di ricezione: {ex}") # Attempt graceful shutdown if an unexpected error occurs self._handle_disconnect() # Trigger disconnect handling and cleanup finally: # Ensure cleanup happens, but primary close logic is in self.close() # print("DEBUG: Receive loop finished.") pass def _process_buffer(self): """Elabora il buffer per estrarre messaggi completi delimitati da \n o \f. """ while True: delimiter_index = -1 try: # Find the first occurrence of either \n (0x0A) or \f (0x0C) lf_index = self.buffer.find(b'\n') ff_index = self.buffer.find(b'\f') if lf_index != -1 and ff_index != -1: delimiter_index = min(lf_index, ff_index) elif lf_index != -1: delimiter_index = lf_index elif ff_index != -1: delimiter_index = ff_index else: # No complete message delimiter found in the buffer break # Exit the while loop, wait for more data # Extract the message bytes up to (but not including) the delimiter message_bytes = self.buffer[:delimiter_index] # Remove the message and the delimiter from the buffer self.buffer = self.buffer[delimiter_index + 1:] # Decode the message try: message = message_bytes.decode('utf-8').rstrip('\r') # Also strip trailing CR if present except UnicodeDecodeError: message = message_bytes.decode('ascii', errors='ignore').rstrip('\r') # Handle the message if it's not empty after stripping if message and self.on_message: self.on_message(self, message) except Exception as e: self._handle_error(f"Errore durante l'elaborazione del buffer: {e}") # It might be safer to clear the buffer or break if processing fails repeatedly break # Exit processing loop on error def send(self, message): """ Invia un messaggio al server. Assicura che termini con CRLF. Args: message (str): Il messaggio da inviare. """ if not self.socket or self._closing: self._handle_error("Client non connesso o in chiusura, impossibile inviare.") return try: # Ensure the message ends with CRLF for many text protocols if not message.endswith('\r\n'): message += '\r\n' message_bytes = message.encode('utf-8') self.socket.sendall(message_bytes) except socket.error as e: # Handle specific errors like broken pipe more gracefully if e.errno == errno.EPIPE or e.errno == errno.ECONNRESET or getattr(e, 'winerror', 0) == 10053: # WSAECONNABORTED/WSAECONNRESET self._handle_error(f"Errore di connessione durante l'invio (Pipe/Reset): {e}") self._handle_disconnect() # Trigger disconnect logic else: self._handle_error(f"Errore socket durante l'invio: {e}") self._handle_disconnect() # Assume disconnect on other send errors except Exception as e: self._handle_error(f"Errore generico durante l'invio: {e}") self._handle_disconnect() # Assume disconnect def close(self): """ Chiude la connessione con il server e rilascia le risorse in modo idempotente. """ if self._closing: # print("DEBUG: Close called while already closing.") return # Already in the process of closing # print("DEBUG: Close method entered.") self._closing = True # Mark as closing # 1. Signal the receive thread to stop self._stop_receive.set() # 2. Close the socket sock = self.socket if sock: self.socket = None # Prevent further use immediately try: # Shutdown signal can sometimes help peers notice closure faster, # but close() usually suffices and shutdown can raise errors on already closed sockets. # sock.shutdown(socket.SHUT_RDWR) pass except (socket.error, OSError) as e: # Ignore errors on shutdown, as socket might be already closed/broken # print(f"DEBUG: Error during socket shutdown (ignoring): {e}") pass try: sock.close() # print("DEBUG: Socket closed.") except (socket.error, OSError) as e: # print(f"DEBUG: Error during socket close (ignoring): {e}") pass # Ignore errors during close # 3. Wait for the receive thread to finish rt = self.receive_thread if rt and rt.is_alive(): # print("DEBUG: Joining receive thread...") rt.join(timeout=1.0) # Wait max 1 second # if rt.is_alive(): # print("DEBUG: Receive thread did not terminate in time.") self.receive_thread = None # 4. Reset state self.buffer = bytearray() # Clear buffer # print("DEBUG: Close method finished.") self._closing = False # Mark as finished closing (allow potential reconnect) def _handle_error(self, message): """ Gestisce gli errori, invocando il callback `on_error` se definito. """ # Avoid printing errors if closing is initiated elsewhere if not self._closing: if self.on_error: try: self.on_error(self, message) except Exception as cb_err: print(f"Errore nel callback on_error: {cb_err}") else: # Default error printing if no callback print(f"\nErrore client: {message}", file=sys.stderr) print("> ", end='', flush=True) # Try to restore prompt def _handle_disconnect(self): """Gestisce la disconnessione dal server, chiama il callback e avvia la chiusura.""" if not self._closing: # Check if we aren't already handling a close request was_connected = self.socket is not None # print(f"DEBUG: _handle_disconnect called. Was connected: {was_connected}") # Call the disconnect callback *before* cleaning up resources if self.on_disconnect: try: self.on_disconnect(self) except Exception as cb_err: print(f"Errore nel callback on_disconnect: {cb_err}") # Ensure close is called to clean up, even if callback exists self.close() # --- END OF TCPClient CLASS --- # --- START OF MAIN SCRIPT LOGIC --- # Use a lock for thread-safe printing to avoid garbled output print_lock = threading.Lock() def safe_print(*args, **kwargs): """Prints messages safely from multiple threads.""" with print_lock: print(*args, **kwargs) # Ensure the prompt is reprinted correctly after any message # This might still have race conditions if input is happening *exactly* # during a print, but it's better than nothing. # We only reprint the prompt if the client is likely still expecting input. # A better approach might involve a dedicated screen management library (like curses). if 'client' in globals() and client.socket and not client._closing: print("> ", end='', flush=True) def handle_message(client_instance, message): """Callback per gestire i messaggi ricevuti dal server.""" # Erase potential user input before printing the message with print_lock: print(f"\r{' ' * 60}\r", end='') # Clear current line print(f"Ricevuto: {message}") print("> ", end='', flush=True) # Reprint prompt def handle_error(client_instance, message): """Callback per gestire gli errori del client.""" with print_lock: print(f"\r{' ' * 60}\r", end='') # Clear current line print(f"Errore: {message}", file=sys.stderr) print("> ", end='', flush=True) # Reprint prompt def handle_connect(client_instance): """Callback chiamato quando la connessione al server ha successo.""" with print_lock: print(f"\r{' ' * 60}\r", end='') # Clear potential leftover chars print("Connesso al server.") print("> ", end='', flush=True) # Stampa il prompt iniziale def handle_disconnect(client_instance): """Callback chiamato quando la connessione al server viene persa.""" with print_lock: print(f"\r{' ' * 60}\r", end='') # Clear current line print("Disconnesso dal server.") # Don't print prompt here, as we are disconnected. def read_input(input_queue, stop_event): """ Legge l'input dell'utente in un thread separato. Gestisce Windows (msvcrt) e altri OS (select/readline). Termina quando stop_event è impostato. """ user_input = "" # Buffer per l'input corrente (Windows) while not stop_event.is_set(): try: if platform.system() == "Windows": # Check for key press without blocking indefinitely if msvcrt.kbhit(): ch_byte = msvcrt.getch() # Handle special keys (like arrows, function keys - typically start with \x00 or \xe0) if ch_byte == b'\x00' or ch_byte == b'\xe0': msvcrt.getch() # Consume the second byte of the special key code continue # Ignore special keys for this example # Handle Enter key (CR or LF) elif ch_byte == b'\r' or ch_byte == b'\n': with print_lock: # Need lock to print newline and put in queue atomically print() # Move cursor to next line if user_input: # Only queue if there's actual input input_queue.put(user_input) user_input = "" # Reset buffer for next input print("> ", end='', flush=True) # Print prompt for new input # Handle Backspace elif ch_byte == b'\x08': if user_input: # Only if there's something to delete user_input = user_input[:-1] # Erase the character on the screen: move back, write space, move back with print_lock: print('\b \b', end='', flush=True) # Handle Ctrl+C (optional, usually caught by main thread) elif ch_byte == b'\x03': # Could signal main thread, but KeyboardInterrupt is standard pass # Handle regular characters else: try: # Decode using preferred encoding, fallback to ascii ignore encoding = sys.stdout.encoding or 'utf-8' ch_decoded = ch_byte.decode(encoding, errors='ignore') if ch_decoded: # If decoding was successful user_input += ch_decoded # Echo the character to the screen with print_lock: print(ch_decoded, end='', flush=True) except Exception: pass # Ignore characters that cause decoding/printing errors else: # No key pressed, sleep briefly to yield CPU and check stop_event time.sleep(0.05) # 50ms idle sleep else: # Non-Windows (Linux, macOS) # Use select for non-blocking check on stdin readable, _, _ = select.select([sys.stdin], [], [], 0.1) # 100ms timeout if readable: line = sys.stdin.readline() if not line: # EOF (e.g., Ctrl+D or pipe closed) safe_print("\nInput stream chiuso (EOF).") break # Exit thread input_str = line.strip() if input_str: # Put non-empty lines into the queue input_queue.put(input_str) # Print prompt again after readline consumes the line with print_lock: print("> ", end='', flush=True) # No explicit sleep needed here, select timeout handles yielding except EOFError: # Can happen on non-Windows if stdin is abruptly closed safe_print("\nInput stream chiuso (EOFError).") break except Exception as e: # Log unexpected errors in the input thread safe_print(f"\nErrore nel thread di input: {e}") time.sleep(0.1) # Prevent fast error loops # print("DEBUG: Input thread finishing.") if __name__ == '__main__': client = TCPClient(host='127.0.0.1', port=5000) # Assign callbacks client.on_message = handle_message client.on_error = handle_error client.on_connect = handle_connect client.on_disconnect = handle_disconnect input_queue = queue.Queue() stop_input_event = threading.Event() input_thread = None # Initialize try: safe_print("Tentativo di connessione...") client.connect() if client.socket: # Proceed only if connection was successful # Start the input thread *after* successful connection input_thread = threading.Thread(target=read_input, args=(input_queue, stop_input_event), daemon=True) input_thread.start() running = True while running and client.socket and not client._closing: try: # Wait for user input or check status periodically message = input_queue.get(block=True, timeout=0.2) # Check queue every 200ms if message.lower() == 'quit': safe_print("Comando 'quit' ricevuto. Chiusura...") running = False # Signal loop to stop break # Exit main loop immediately else: # Send the user message (send method handles CRLF) client.send(message) # Prompt is handled by read_input or message handlers now except queue.Empty: # Timeout - no user input. Loop continues. # The loop condition (client.socket and not client._closing) handles disconnects. pass else: # Connection failed safe_print("Connessione fallita. Uscita.") except KeyboardInterrupt: safe_print("\nInterruzione da tastiera (Ctrl+C). Chiusura...") running = False # Ensure loop terminates if it was somehow still active finally: safe_print("Avvio procedura di pulizia...") # 1. Signal input thread to stop stop_input_event.set() # 2. Close the client connection (signals receive thread, closes socket) # Do this *before* joining the input thread in case input thread is blocked # waiting for the queue which might never get filled if client is closing. if 'client' in globals(): # Ensure client object exists client.close() # 3. Wait briefly for the input thread to finish if input_thread and input_thread.is_alive(): # print("DEBUG: Joining input thread...") input_thread.join(timeout=1.0) # Wait max 1 second # if input_thread.is_alive(): # print("DEBUG: Input thread did not terminate in time.") # 4. Wait briefly for receive thread (already joined in client.close) - Redundant check # rt = client.receive_thread # Access might fail if client already None # if rt and rt.is_alive(): # print("DEBUG: Receive thread still alive after close?") # Should not happen safe_print("Client terminato.")