Spaces:
Build error
Build error
| # 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. | |
| """ | |
| 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}'}) | |
| def handle_disconnect(): | |
| """ | |
| Gère la déconnexion d'un client. | |
| """ | |
| sid = request.sid | |
| cleanup_terminal(sid, socketio) | |
| 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.'}) |