Mailix / terminal_manager.py
ernestmindres's picture
Upload terminal_manager.py
48ef31f verified
# 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.'})