# terminal_manager.py import os import pty import sys import subprocess from threading import Thread from flask import request from flask_socketio import SocketIO, emit # Dictionnaire pour stocker les processus PTY actifs par ID de session SocketIO active_terminals = {} def read_and_send_output(sid, master_fd, socketio): """ Fonction cible du Thread : Lit en continu le PTY et émet la sortie au client. """ while True: try: # Lire les données du PTY (la taille de lecture peut être ajustée) # Utilisation de 'errors=ignore' pour éviter les problèmes d'encodage binaires output = os.read(master_fd, 1024).decode(errors='ignore') if output: # Émettre les données lues uniquement au client concerné socketio.emit('terminal_output', {'output': output}, room=sid) else: # Si os.read retourne 0, le processus est terminé break except OSError: # Le descripteur de fichier est fermé (par le disconnect handler) break except Exception as e: # Journalisation de l'erreur print(f"Erreur de lecture/écriture du terminal pour {sid}: {e}", file=sys.stderr) break # Nettoyage à la fin du thread cleanup_terminal(sid, socketio) def cleanup_terminal(sid, socketio): """ Fonction de nettoyage pour le processus PTY. """ if sid in active_terminals: # Tuer le processus du shell s'il est toujours en cours try: pid = active_terminals[sid]['pid'] os.kill(pid, 9) # Envoyer un SIGKILL pour s'assurer qu'il s'arrête except OSError: pass # Le processus est déjà terminé # Fermer le descripteur de fichier maître try: os.close(active_terminals[sid]['master_fd']) except OSError: pass del active_terminals[sid] # Informer le client que la connexion est perdue socketio.emit('terminal_output', {'output': '\r\n--- Session terminal terminée ---\r\n'}, room=sid) print(f"Nettoyage complet du terminal pour la session {sid}", file=sys.stderr) def setup_terminal_events(socketio: SocketIO): """ Initialise et enregistre les gestionnaires d'événements SocketIO pour le terminal. """ @socketio.on('connect') def handle_connect(): sid = request.sid try: # 1. Créer le Pseudo-Terminal (master/slave) master_fd, slave_fd = pty.openpty() # 2. Définir la commande du shell à lancer (ex: /bin/bash sur Linux) # Pour l'exécution de code Python, on pourrait aussi utiliser 'python' ou 'jupyter console' shell_command = os.environ.get('SHELL', '/bin/bash') # 3. Forker le processus pour lancer le shell pid = os.fork() if pid == 0: # Processus enfant (le shell) os.close(master_fd) # Fermer le master # Dupliquer le slave vers stdin, stdout, stderr os.dup2(slave_fd, sys.stdin.fileno()) os.dup2(slave_fd, sys.stdout.fileno()) os.dup2(slave_fd, sys.stderr.fileno()) os.close(slave_fd) # Exécuter le shell, en s'assurant qu'il est exécuté os.execv(shell_command, [shell_command]) os._exit(0) # Sécurité si execv échoue else: # Processus parent (le serveur) os.close(slave_fd) # Fermer le slave # Stocker l'information du terminal actif active_terminals[sid] = {'pid': pid, 'master_fd': master_fd} # 4. Lancer le thread de lecture et d'émission de sortie thread = Thread(target=read_and_send_output, args=(sid, master_fd, socketio)) thread.daemon = True # Permet au thread de s'arrêter lorsque le programme principal s'arrête thread.start() emit('terminal_output', {'output': '\r\nTerminal connecté. Tapez votre code ou une commande.\r\n'}) except Exception as e: # Échec de l'initialisation du terminal print(f"Erreur d'initialisation du terminal: {e}", file=sys.stderr) emit('terminal_error', {'message': f'Erreur de connexion au terminal: {e}'}) @socketio.on('disconnect') def handle_disconnect(): """ Gère la déconnexion d'un client. """ sid = request.sid cleanup_terminal(sid, socketio) @socketio.on('terminal_input') def handle_terminal_input(data): """ Reçoit l'entrée utilisateur du client et l'écrit dans le PTY. """ sid = request.sid if sid in active_terminals: master_fd = active_terminals[sid]['master_fd'] input_data = data.get('input', '') try: # Écrire l'entrée utilisateur dans le PTY (comme si l'utilisateur tapait) os.write(master_fd, input_data.encode()) except OSError as e: # Si le descripteur de fichier est fermé print(f"Erreur d'écriture dans le PTY (processus terminé ?): {e}", file=sys.stderr) emit('terminal_output', {'output': '\r\nErreur de communication avec le terminal.'})