Spaces:
Build error
Build error
Upload terminal_manager.py
Browse files- terminal_manager.py +139 -0
terminal_manager.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# terminal_manager.py
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import pty
|
| 5 |
+
import sys
|
| 6 |
+
import subprocess
|
| 7 |
+
from threading import Thread
|
| 8 |
+
from flask import request
|
| 9 |
+
from flask_socketio import SocketIO, emit
|
| 10 |
+
|
| 11 |
+
# Dictionnaire pour stocker les processus PTY actifs par ID de session SocketIO
|
| 12 |
+
active_terminals = {}
|
| 13 |
+
|
| 14 |
+
def read_and_send_output(sid, master_fd, socketio):
|
| 15 |
+
"""
|
| 16 |
+
Fonction cible du Thread : Lit en continu le PTY et émet la sortie au client.
|
| 17 |
+
"""
|
| 18 |
+
while True:
|
| 19 |
+
try:
|
| 20 |
+
# Lire les données du PTY (la taille de lecture peut être ajustée)
|
| 21 |
+
# Utilisation de 'errors=ignore' pour éviter les problèmes d'encodage binaires
|
| 22 |
+
output = os.read(master_fd, 1024).decode(errors='ignore')
|
| 23 |
+
|
| 24 |
+
if output:
|
| 25 |
+
# Émettre les données lues uniquement au client concerné
|
| 26 |
+
socketio.emit('terminal_output', {'output': output}, room=sid)
|
| 27 |
+
else:
|
| 28 |
+
# Si os.read retourne 0, le processus est terminé
|
| 29 |
+
break
|
| 30 |
+
except OSError:
|
| 31 |
+
# Le descripteur de fichier est fermé (par le disconnect handler)
|
| 32 |
+
break
|
| 33 |
+
except Exception as e:
|
| 34 |
+
# Journalisation de l'erreur
|
| 35 |
+
print(f"Erreur de lecture/écriture du terminal pour {sid}: {e}", file=sys.stderr)
|
| 36 |
+
break
|
| 37 |
+
|
| 38 |
+
# Nettoyage à la fin du thread
|
| 39 |
+
cleanup_terminal(sid, socketio)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def cleanup_terminal(sid, socketio):
|
| 43 |
+
"""
|
| 44 |
+
Fonction de nettoyage pour le processus PTY.
|
| 45 |
+
"""
|
| 46 |
+
if sid in active_terminals:
|
| 47 |
+
# Tuer le processus du shell s'il est toujours en cours
|
| 48 |
+
try:
|
| 49 |
+
pid = active_terminals[sid]['pid']
|
| 50 |
+
os.kill(pid, 9) # Envoyer un SIGKILL pour s'assurer qu'il s'arrête
|
| 51 |
+
except OSError:
|
| 52 |
+
pass # Le processus est déjà terminé
|
| 53 |
+
|
| 54 |
+
# Fermer le descripteur de fichier maître
|
| 55 |
+
try:
|
| 56 |
+
os.close(active_terminals[sid]['master_fd'])
|
| 57 |
+
except OSError:
|
| 58 |
+
pass
|
| 59 |
+
|
| 60 |
+
del active_terminals[sid]
|
| 61 |
+
|
| 62 |
+
# Informer le client que la connexion est perdue
|
| 63 |
+
socketio.emit('terminal_output', {'output': '\r\n--- Session terminal terminée ---\r\n'}, room=sid)
|
| 64 |
+
print(f"Nettoyage complet du terminal pour la session {sid}", file=sys.stderr)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def setup_terminal_events(socketio: SocketIO):
|
| 68 |
+
"""
|
| 69 |
+
Initialise et enregistre les gestionnaires d'événements SocketIO pour le terminal.
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
@socketio.on('connect')
|
| 73 |
+
def handle_connect():
|
| 74 |
+
sid = request.sid
|
| 75 |
+
try:
|
| 76 |
+
# 1. Créer le Pseudo-Terminal (master/slave)
|
| 77 |
+
master_fd, slave_fd = pty.openpty()
|
| 78 |
+
|
| 79 |
+
# 2. Définir la commande du shell à lancer (ex: /bin/bash sur Linux)
|
| 80 |
+
# Pour l'exécution de code Python, on pourrait aussi utiliser 'python' ou 'jupyter console'
|
| 81 |
+
shell_command = os.environ.get('SHELL', '/bin/bash')
|
| 82 |
+
|
| 83 |
+
# 3. Forker le processus pour lancer le shell
|
| 84 |
+
pid = os.fork()
|
| 85 |
+
if pid == 0:
|
| 86 |
+
# Processus enfant (le shell)
|
| 87 |
+
os.close(master_fd) # Fermer le master
|
| 88 |
+
# Dupliquer le slave vers stdin, stdout, stderr
|
| 89 |
+
os.dup2(slave_fd, sys.stdin.fileno())
|
| 90 |
+
os.dup2(slave_fd, sys.stdout.fileno())
|
| 91 |
+
os.dup2(slave_fd, sys.stderr.fileno())
|
| 92 |
+
os.close(slave_fd)
|
| 93 |
+
|
| 94 |
+
# Exécuter le shell, en s'assurant qu'il est exécuté
|
| 95 |
+
os.execv(shell_command, [shell_command])
|
| 96 |
+
os._exit(0) # Sécurité si execv échoue
|
| 97 |
+
else:
|
| 98 |
+
# Processus parent (le serveur)
|
| 99 |
+
os.close(slave_fd) # Fermer le slave
|
| 100 |
+
|
| 101 |
+
# Stocker l'information du terminal actif
|
| 102 |
+
active_terminals[sid] = {'pid': pid, 'master_fd': master_fd}
|
| 103 |
+
|
| 104 |
+
# 4. Lancer le thread de lecture et d'émission de sortie
|
| 105 |
+
thread = Thread(target=read_and_send_output, args=(sid, master_fd, socketio))
|
| 106 |
+
thread.daemon = True # Permet au thread de s'arrêter lorsque le programme principal s'arrête
|
| 107 |
+
thread.start()
|
| 108 |
+
|
| 109 |
+
emit('terminal_output', {'output': '\r\nTerminal connecté. Tapez votre code ou une commande.\r\n'})
|
| 110 |
+
|
| 111 |
+
except Exception as e:
|
| 112 |
+
# Échec de l'initialisation du terminal
|
| 113 |
+
print(f"Erreur d'initialisation du terminal: {e}", file=sys.stderr)
|
| 114 |
+
emit('terminal_error', {'message': f'Erreur de connexion au terminal: {e}'})
|
| 115 |
+
|
| 116 |
+
@socketio.on('disconnect')
|
| 117 |
+
def handle_disconnect():
|
| 118 |
+
"""
|
| 119 |
+
Gère la déconnexion d'un client.
|
| 120 |
+
"""
|
| 121 |
+
sid = request.sid
|
| 122 |
+
cleanup_terminal(sid, socketio)
|
| 123 |
+
|
| 124 |
+
@socketio.on('terminal_input')
|
| 125 |
+
def handle_terminal_input(data):
|
| 126 |
+
"""
|
| 127 |
+
Reçoit l'entrée utilisateur du client et l'écrit dans le PTY.
|
| 128 |
+
"""
|
| 129 |
+
sid = request.sid
|
| 130 |
+
if sid in active_terminals:
|
| 131 |
+
master_fd = active_terminals[sid]['master_fd']
|
| 132 |
+
input_data = data.get('input', '')
|
| 133 |
+
try:
|
| 134 |
+
# Écrire l'entrée utilisateur dans le PTY (comme si l'utilisateur tapait)
|
| 135 |
+
os.write(master_fd, input_data.encode())
|
| 136 |
+
except OSError as e:
|
| 137 |
+
# Si le descripteur de fichier est fermé
|
| 138 |
+
print(f"Erreur d'écriture dans le PTY (processus terminé ?): {e}", file=sys.stderr)
|
| 139 |
+
emit('terminal_output', {'output': '\r\nErreur de communication avec le terminal.'})
|