55"""
66
77import io
8+ import sys
9+ import eel
810import traceback
911import threading
1012import queue
1113import importlib
1214from contextlib import redirect_stdout
1315from typing import Dict , Any
1416
17+
18+ input_queue = queue .Queue ()
19+ is_waiting_for_user = False
20+
1521# -------------------------------------
1622# CONFIGURACIÓN DE SEGURIDAD
1723
@@ -43,6 +49,18 @@ def safe_import(name, globals=None, locals=None, fromlist=(), level=0):
4349# ---------------------------------------------
4450# EJECUTOR PRINCIPAL
4551
52+ class RealTimeStdout :
53+ """Intercepta los prints y los envía a la interfaz de React al instante."""
54+ def write (self , text ):
55+ # Filtramos los saltos de línea vacíos que hace print por defecto
56+ if text and text != '\n ' :
57+ try :
58+ eel .api_realtime_log (str (text ))()
59+ except Exception :
60+ pass
61+ def flush (self ):
62+ pass
63+
4664def sanitize_code (code : str ) -> str :
4765 """Evita palabras clave peligrosas antes de la ejecución."""
4866 forbidden = [
@@ -58,114 +76,100 @@ def sanitize_code(code: str) -> str:
5876 return code
5977
6078def execute_user_code (code : str , timeout : int = 5 ) -> Dict [str , Any ]:
61- """
62- Ejecuta el código de usuario en un hilo separado con stdout capturado.
63-
64- Args:
65- code (str): Código Python a ejecutar.
66- timeout (int): Tiempo máximo de ejecución en segundos.
67-
68- Returns:
69- dict: {'success': bool, 'output': str, 'error': str}
70- """
79+ global is_waiting_for_user
7180 result = {'success' : False , 'output' : '' , 'error' : '' }
7281
82+ # Limpiar cualquier residuo de inputs de ejecuciones pasadas
83+ while not input_queue .empty ():
84+ input_queue .get_nowait ()
85+
7386 def target (q ):
74- buffer = io . StringIO ()
87+ global is_waiting_for_user
7588 try :
76- # Saneamiento básico
7789 safe_code = sanitize_code (code )
78-
79- def dummy_input (prompt = "" ):
80- # Imprime el mensaje del estudiante de forma natural y limpia
81- if prompt :
82- print (prompt )
83- # Retornamos "" silenciosamente para que la ejecución no se detenga
84- # (Útil por si luego conectan esto a un nodo de matemáticas)
85- return ""
8690
87- # Configuración del entorno restringido (Sandbox)
88- # Definimos qué funciones 'built-in' puede ver el estudiante
89- safe_builtins_map = {
90- # Básicos
91- 'print' : print ,
92- 'input' : dummy_input ,
93- 'range' : range ,
94- 'len' : len ,
95- 'int' : int ,
96- 'float' : float ,
97- 'str' : str ,
98- 'list' : list ,
99- 'dict' : dict ,
100- 'set' : set ,
101- 'tuple' : tuple ,
102- 'bool' : bool ,
91+ def interactive_input (prompt = "" ):
92+ global is_waiting_for_user
93+ if prompt :
94+ try : eel .api_realtime_log (str (prompt ))()
95+ except : pass
10396
104- # Matemáticas y Utilidades
105- 'abs' : abs ,
106- 'min' : min ,
107- 'max' : max ,
108- 'sum' : sum ,
109- 'round' : round ,
110- 'zip' : zip ,
111- 'map' : map ,
112- 'filter' : filter ,
113- 'sorted' : sorted ,
114- 'enumerate' : enumerate ,
97+ # Avisar a React que habilite la caja de texto
98+ try : eel .trigger_frontend_input ()()
99+ except : pass
115100
116- # Excepciones
117- 'Exception' : Exception ,
118- 'ValueError' : ValueError ,
119- 'TypeError' : TypeError ,
101+ # Bloquear el hilo de Python hasta que React responda
102+ is_waiting_for_user = True
103+ user_response = input_queue . get ()
104+ is_waiting_for_user = False
120105
121- # Soporte para Clases y POO
122- '__build_class__' : __build_class__ ,
123- 'object' : object , # Necesario para herencia base
124- 'super' : super , # Necesario para llamar al padre
125- 'classmethod' : classmethod , # Decorador útil para enseñar POO
126- 'staticmethod' : staticmethod , # Decorador útil para enseñar POO
127- 'property' : property , # Getters/Setters pythonicos
128- 'type' : type , # Para introspección de tipos
129- 'isinstance' : isinstance , # Validación de tipos
106+ # Hacer eco de la respuesta en la terminal visual
107+ try : eel .api_realtime_log (f"❯ { user_response } " )()
108+ except : pass
130109
131- # Sistema
110+ return str (user_response )
111+
112+ safe_builtins_map = {
113+ 'print' : print ,
114+ 'input' : interactive_input , # <- Nueva función interactiva
115+ 'range' : range , 'len' : len , 'int' : int , 'float' : float , 'str' : str ,
116+ 'list' : list , 'dict' : dict , 'set' : set , 'tuple' : tuple , 'bool' : bool ,
117+ 'abs' : abs , 'min' : min , 'max' : max , 'sum' : sum , 'round' : round ,
118+ 'zip' : zip , 'map' : map , 'filter' : filter , 'sorted' : sorted , 'enumerate' : enumerate ,
119+ 'Exception' : Exception , 'ValueError' : ValueError , 'TypeError' : TypeError ,
120+ '__build_class__' : __build_class__ , # Vital para los nodos Class
121+ 'object' : object ,
122+ 'super' : super ,
123+ 'classmethod' : classmethod ,
124+ 'staticmethod' : staticmethod ,
125+ 'property' : property ,
126+ 'type' : type ,
127+ 'isinstance' : isinstance ,
132128 '__import__' : safe_import ,
133- '__name__' : '__main__' # Evita errores en algunos contextos de ejecución
129+ '__name__' : '__main__'
134130 }
135131
136- # Entorno global inicial
137132 env = {'__builtins__' : safe_builtins_map }
138133
139- # Ejecución
140- with redirect_stdout (buffer ):
134+ # Aplicar la redirección de consola en tiempo real
135+ original_stdout = sys .stdout
136+ sys .stdout = RealTimeStdout ()
137+
138+ try :
141139 exec (safe_code , env )
140+ q .put ({'success' : True , 'output' : '' , 'error' : '' })
141+ finally :
142+ sys .stdout = original_stdout # Restaurar stdout por seguridad
142143
143- q .put ({'success' : True , 'output' : buffer .getvalue (), 'error' : '' })
144-
144+ #Captura específica de errores
145145 except SyntaxError as se :
146- q .put ({'success' : False , 'output' : buffer . getvalue () , 'error' : f"Error de Sintaxis: { se } " })
146+ q .put ({'success' : False , 'output' : '' , 'error' : f"Error de Sintaxis: { se } " })
147147 except ImportError as ie :
148- q .put ({'success' : False , 'output' : buffer . getvalue () , 'error' : f"Error de Importación: { ie } " })
148+ q .put ({'success' : False , 'output' : '' , 'error' : f"Error de Importación: { ie } " })
149149 except Exception :
150- q .put ({'success' : False , 'output' : buffer . getvalue () , 'error' : traceback .format_exc ()})
150+ q .put ({'success' : False , 'output' : '' , 'error' : traceback .format_exc ()})
151151
152- # Gestión de Hilos (Threading) para evitar bloqueos infinitos (while True)
152+ # Ejecución en hilo secundario
153153 q = queue .Queue ()
154154 thread = threading .Thread (target = target , args = (q ,))
155155 thread .start ()
156156
157- try :
158- thread .join (timeout )
159- if thread .is_alive ():
160- result ['error' ] = "Tiempo de ejecución excedido (Timeout). ¿Tienes un bucle infinito?"
161- # Nota: Python threads no se pueden matar forzosamente de forma segura,
162- # pero el frontend dejará de esperar.
157+ # TIMEOUT INTELIGENTE
158+ time_elapsed = 0.0
159+ while thread .is_alive () and time_elapsed < timeout :
160+ # eel.sleep cede el control temporalmente a los WebSockets
161+ # permitiendo que la interfaz reaccione en tiempo real sin bloquear el hilo principal.
162+ eel .sleep (0.1 )
163+
164+ if not is_waiting_for_user :
165+ time_elapsed += 0.1
166+
167+ if thread .is_alive ():
168+ result ['error' ] = "Tiempo de ejecución excedido (Timeout). ¿Tienes un bucle infinito?"
169+ else :
170+ if not q .empty ():
171+ result = q .get ()
163172 else :
164- if not q .empty ():
165- result = q .get ()
166- else :
167- result ['error' ] = "Error desconocido: No se recibió respuesta del hilo."
168- except Exception as e :
169- result ['error' ] = str (e )
170-
173+ result ['error' ] = "Error desconocido: No se recibió respuesta del hilo."
174+
171175 return result
0 commit comments