Update app.py
Browse files
app.py
CHANGED
|
@@ -10,6 +10,7 @@ from typing import Dict, Any, Optional
|
|
| 10 |
import threading
|
| 11 |
import asyncio
|
| 12 |
import signal
|
|
|
|
| 13 |
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
|
| 14 |
from pydantic import BaseModel
|
| 15 |
from starlette.responses import HTMLResponse
|
|
@@ -77,6 +78,7 @@ class WebSocketInputOutput:
|
|
| 77 |
"""Synchronous wrapper for readline for use with exec."""
|
| 78 |
try:
|
| 79 |
print("DEBUG: Synchronous input wrapper called.")
|
|
|
|
| 80 |
return asyncio.run(self.readline())
|
| 81 |
except Exception as e:
|
| 82 |
print(f"ERROR: Synchronous input wrapper error: {e}")
|
|
@@ -387,17 +389,16 @@ class SecurePythonExecutor:
|
|
| 387 |
original_stdout = sys.stdout
|
| 388 |
original_stderr = sys.stderr
|
| 389 |
|
| 390 |
-
#
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
def target():
|
| 394 |
nonlocal result
|
| 395 |
try:
|
| 396 |
# Redirect sys.stdin, sys.stdout, sys.stderr for this thread
|
| 397 |
sys.stdin = websocket_io
|
| 398 |
sys.stdout = websocket_io
|
| 399 |
sys.stderr = websocket_io
|
| 400 |
-
print(f"DEBUG:
|
| 401 |
|
| 402 |
# Create restricted execution environment
|
| 403 |
restricted_globals = {
|
|
@@ -405,65 +406,73 @@ class SecurePythonExecutor:
|
|
| 405 |
'__name__': '__main__',
|
| 406 |
'__doc__': None,
|
| 407 |
}
|
| 408 |
-
print("DEBUG: Restricted globals prepared.") # ADDED THIS LINE
|
| 409 |
|
| 410 |
-
print(f"DEBUG: Preparing to execute code (thread): {code[:100]}...")
|
| 411 |
exec(code, restricted_globals)
|
| 412 |
-
print("DEBUG: Code execution completed successfully in thread.")
|
| 413 |
result['success'] = True
|
| 414 |
|
| 415 |
except EOFError as e:
|
| 416 |
# This happens if the WebSocket disconnects during input()
|
| 417 |
result['error'] = f"Input stream closed: {str(e)}"
|
| 418 |
result['success'] = False
|
| 419 |
-
print(f"ERROR: EOFError during execution in thread: {e}")
|
| 420 |
except Exception as e:
|
| 421 |
error_msg = f"{type(e).__name__}: {str(e)}\n{traceback.format_exc()}"
|
| 422 |
-
print(f"ERROR: Unhandled exception during execution in thread: {error_msg}")
|
| 423 |
-
# Write error to WebSocket and internal buffer
|
| 424 |
try:
|
| 425 |
-
#
|
| 426 |
-
# This
|
| 427 |
asyncio.run(websocket_io.write(f"Error: {error_msg}\n"))
|
| 428 |
-
print("DEBUG: Attempted to write error via websocket_io.write from thread.")
|
| 429 |
except Exception as write_e:
|
| 430 |
-
print(f"ERROR: Failed to write error to WebSocket from thread (inner): {write_e}")
|
| 431 |
websocket_io.error_buffer.write(error_msg)
|
| 432 |
result['success'] = False
|
| 433 |
finally:
|
| 434 |
-
# Restore original stdin/stdout/stderr for the thread pool
|
| 435 |
sys.stdin = original_stdin
|
| 436 |
sys.stdout = original_stdout
|
| 437 |
sys.stderr = original_stderr
|
| 438 |
-
|
| 439 |
-
print(f"DEBUG: Thread '{threading.current_thread().name}' finished. IO restored.")
|
| 440 |
|
| 441 |
-
# Run the execution in a separate thread to allow for timeout
|
| 442 |
-
thread = threading.Thread(target=target, name=f"CodeExecutorThread-{id(websocket_io)}")
|
| 443 |
-
thread.daemon = True # Allow the program to exit even if thread is running
|
| 444 |
-
thread.start()
|
| 445 |
-
print(f"DEBUG: Code execution thread '{thread.name}' started. Waiting for join.")
|
| 446 |
|
| 447 |
-
#
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
result['error'] = f"Code execution timed out after {self.timeout} seconds"
|
| 453 |
result['success'] = False
|
| 454 |
-
# Attempt to send timeout message to client
|
| 455 |
try:
|
| 456 |
await websocket_io.write(f"Error: {result['error']}\n")
|
| 457 |
except Exception:
|
| 458 |
pass # Ignore if websocket is already closed
|
| 459 |
-
|
| 460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
result['output'] = websocket_io.output_buffer.getvalue()
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
else:
|
| 466 |
-
result['success'] = True
|
| 467 |
|
| 468 |
# Send a final message to the client indicating execution is complete
|
| 469 |
try:
|
|
@@ -474,7 +483,7 @@ class SecurePythonExecutor:
|
|
| 474 |
print("DEBUG: WebSocketDisconnect when sending execution_complete.")
|
| 475 |
pass # Ignore if websocket is already closed
|
| 476 |
except Exception as e:
|
| 477 |
-
print(f"ERROR: Error sending execution_complete message: {e}")
|
| 478 |
|
| 479 |
print(f"DEBUG: run_with_timeout returning result for client: {websocket_io.websocket.client}")
|
| 480 |
return result
|
|
@@ -495,7 +504,7 @@ app = FastAPI(
|
|
| 495 |
async def get_index():
|
| 496 |
"""Serves the interactive frontend."""
|
| 497 |
# This assumes index.html is in the same directory as app.py
|
| 498 |
-
#
|
| 499 |
with open("index.html", "r") as f:
|
| 500 |
return HTMLResponse(content=f.read())
|
| 501 |
|
|
@@ -542,7 +551,7 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 542 |
websocket_io.input_queue.get_nowait()
|
| 543 |
except asyncio.QueueEmpty:
|
| 544 |
pass
|
| 545 |
-
print(f"WebSocket connection closed for {websocket.client}")
|
| 546 |
|
| 547 |
# This block is for local development using `uvicorn`
|
| 548 |
# Hugging Face Spaces will use `uvicorn` directly via the Dockerfile CMD
|
|
|
|
| 10 |
import threading
|
| 11 |
import asyncio
|
| 12 |
import signal
|
| 13 |
+
from concurrent.futures import ThreadPoolExecutor # NEW: for loop.run_in_executor
|
| 14 |
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
|
| 15 |
from pydantic import BaseModel
|
| 16 |
from starlette.responses import HTMLResponse
|
|
|
|
| 78 |
"""Synchronous wrapper for readline for use with exec."""
|
| 79 |
try:
|
| 80 |
print("DEBUG: Synchronous input wrapper called.")
|
| 81 |
+
# This is called from the executor thread, so asyncio.run is appropriate here
|
| 82 |
return asyncio.run(self.readline())
|
| 83 |
except Exception as e:
|
| 84 |
print(f"ERROR: Synchronous input wrapper error: {e}")
|
|
|
|
| 389 |
original_stdout = sys.stdout
|
| 390 |
original_stderr = sys.stderr
|
| 391 |
|
| 392 |
+
# Define the synchronous target function to be run in the executor thread
|
| 393 |
+
def _execute_in_thread():
|
| 394 |
+
# This function runs in a separate thread managed by the event loop's executor
|
|
|
|
| 395 |
nonlocal result
|
| 396 |
try:
|
| 397 |
# Redirect sys.stdin, sys.stdout, sys.stderr for this thread
|
| 398 |
sys.stdin = websocket_io
|
| 399 |
sys.stdout = websocket_io
|
| 400 |
sys.stderr = websocket_io
|
| 401 |
+
print(f"DEBUG: Executor thread '{threading.current_thread().name}' started. IO redirected.")
|
| 402 |
|
| 403 |
# Create restricted execution environment
|
| 404 |
restricted_globals = {
|
|
|
|
| 406 |
'__name__': '__main__',
|
| 407 |
'__doc__': None,
|
| 408 |
}
|
| 409 |
+
print("DEBUG: Restricted globals prepared in executor thread.") # ADDED THIS LINE
|
| 410 |
|
| 411 |
+
print(f"DEBUG: Preparing to execute code (executor thread): {code[:100]}...")
|
| 412 |
exec(code, restricted_globals)
|
| 413 |
+
print("DEBUG: Code execution completed successfully in executor thread.")
|
| 414 |
result['success'] = True
|
| 415 |
|
| 416 |
except EOFError as e:
|
| 417 |
# This happens if the WebSocket disconnects during input()
|
| 418 |
result['error'] = f"Input stream closed: {str(e)}"
|
| 419 |
result['success'] = False
|
| 420 |
+
print(f"ERROR: EOFError during execution in executor thread: {e}")
|
| 421 |
except Exception as e:
|
| 422 |
error_msg = f"{type(e).__name__}: {str(e)}\n{traceback.format_exc()}"
|
| 423 |
+
print(f"ERROR: Unhandled exception during execution in executor thread: {error_msg}")
|
|
|
|
| 424 |
try:
|
| 425 |
+
# If this write fails, it's still good to log the error to stderr buffer
|
| 426 |
+
# This asyncio.run call is crucial and runs a new event loop for this async call
|
| 427 |
asyncio.run(websocket_io.write(f"Error: {error_msg}\n"))
|
| 428 |
+
print("DEBUG: Attempted to write error via websocket_io.write from executor thread.")
|
| 429 |
except Exception as write_e:
|
| 430 |
+
print(f"ERROR: Failed to write error to WebSocket from executor thread (inner): {write_e}")
|
| 431 |
websocket_io.error_buffer.write(error_msg)
|
| 432 |
result['success'] = False
|
| 433 |
finally:
|
| 434 |
+
# Restore original stdin/stdout/stderr for the thread pool
|
| 435 |
sys.stdin = original_stdin
|
| 436 |
sys.stdout = original_stdout
|
| 437 |
sys.stderr = original_stderr
|
| 438 |
+
print(f"DEBUG: Executor thread '{threading.current_thread().name}' finished. IO restored.")
|
|
|
|
| 439 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
|
| 441 |
+
# Use the event loop's default ThreadPoolExecutor to run the synchronous code
|
| 442 |
+
loop = asyncio.get_running_loop()
|
| 443 |
+
try:
|
| 444 |
+
print(f"DEBUG: Scheduling _execute_in_thread in executor with timeout {self.timeout}s.")
|
| 445 |
+
# Run the synchronous _execute_in_thread in a separate thread
|
| 446 |
+
await asyncio.wait_for(
|
| 447 |
+
loop.run_in_executor(None, _execute_in_thread), # None uses the default ThreadPoolExecutor
|
| 448 |
+
timeout=self.timeout
|
| 449 |
+
)
|
| 450 |
+
print("DEBUG: _execute_in_thread completed via executor.")
|
| 451 |
+
except asyncio.TimeoutError:
|
| 452 |
+
print(f"ERROR: Code execution timed out after {self.timeout} seconds in run_in_executor.")
|
| 453 |
result['error'] = f"Code execution timed out after {self.timeout} seconds"
|
| 454 |
result['success'] = False
|
|
|
|
| 455 |
try:
|
| 456 |
await websocket_io.write(f"Error: {result['error']}\n")
|
| 457 |
except Exception:
|
| 458 |
pass # Ignore if websocket is already closed
|
| 459 |
+
except Exception as e:
|
| 460 |
+
# This catches exceptions from _execute_in_thread that propagate up
|
| 461 |
+
# (e.g., if the thread itself crashes in an unrecoverable way before our try/except)
|
| 462 |
+
print(f"ERROR: Unexpected exception from executor: {e}")
|
| 463 |
+
result['error'] = f"Unexpected executor error: {type(e).__name__}: {str(e)}"
|
| 464 |
+
result['success'] = False
|
| 465 |
+
try:
|
| 466 |
+
await websocket_io.write(f"Error: {result['error']}\n")
|
| 467 |
+
except Exception:
|
| 468 |
+
pass
|
| 469 |
+
|
| 470 |
+
# After execution (or timeout), collect output
|
| 471 |
+
if result['success']: # Only if execution was successful in _execute_in_thread
|
| 472 |
result['output'] = websocket_io.output_buffer.getvalue()
|
| 473 |
+
if websocket_io.error_buffer.getvalue(): # Error buffer always checked
|
| 474 |
+
result['error'] = result['error'] + "\n" + websocket_io.error_buffer.getvalue() if result['error'] else websocket_io.error_buffer.getvalue()
|
| 475 |
+
result['success'] = False # Ensure success is false if there's any error output
|
|
|
|
|
|
|
| 476 |
|
| 477 |
# Send a final message to the client indicating execution is complete
|
| 478 |
try:
|
|
|
|
| 483 |
print("DEBUG: WebSocketDisconnect when sending execution_complete.")
|
| 484 |
pass # Ignore if websocket is already closed
|
| 485 |
except Exception as e:
|
| 486 |
+
print(f"ERROR: Error sending execution_complete message: {e}")
|
| 487 |
|
| 488 |
print(f"DEBUG: run_with_timeout returning result for client: {websocket_io.websocket.client}")
|
| 489 |
return result
|
|
|
|
| 504 |
async def get_index():
|
| 505 |
"""Serves the interactive frontend."""
|
| 506 |
# This assumes index.html is in the same directory as app.py
|
| 507 |
+
# Explicitly copy index.html to /app/index.html to avoid any ambiguity with '.'
|
| 508 |
with open("index.html", "r") as f:
|
| 509 |
return HTMLResponse(content=f.read())
|
| 510 |
|
|
|
|
| 551 |
websocket_io.input_queue.get_nowait()
|
| 552 |
except asyncio.QueueEmpty:
|
| 553 |
pass
|
| 554 |
+
print(f"WebSocket connection closed for {websocket_io.websocket.client}") # Use websocket_io.websocket.client here too
|
| 555 |
|
| 556 |
# This block is for local development using `uvicorn`
|
| 557 |
# Hugging Face Spaces will use `uvicorn` directly via the Dockerfile CMD
|