Update app.py
Browse files
app.py
CHANGED
|
@@ -35,6 +35,7 @@ class WebSocketInputOutput:
|
|
| 35 |
self.output_buffer = io.StringIO()
|
| 36 |
self.error_buffer = io.StringIO()
|
| 37 |
self.closed = False
|
|
|
|
| 38 |
|
| 39 |
async def write(self, s: str):
|
| 40 |
"""Writes to stdout/stderr and sends to WebSocket."""
|
|
@@ -42,11 +43,12 @@ class WebSocketInputOutput:
|
|
| 42 |
try:
|
| 43 |
# Send output in chunks or lines to prevent large messages
|
| 44 |
await self.websocket.send_text(json.dumps({"type": "output", "content": s}))
|
|
|
|
| 45 |
except WebSocketDisconnect:
|
| 46 |
self.closed = True
|
| 47 |
-
print("WebSocket disconnected during write.")
|
| 48 |
except Exception as e:
|
| 49 |
-
print(f"Error sending output over WebSocket: {e}")
|
| 50 |
self.closed = True
|
| 51 |
|
| 52 |
def flush(self):
|
|
@@ -56,44 +58,37 @@ class WebSocketInputOutput:
|
|
| 56 |
async def readline(self) -> str:
|
| 57 |
"""Reads a line from stdin, waiting for input from WebSocket."""
|
| 58 |
try:
|
|
|
|
| 59 |
await self.websocket.send_text(json.dumps({"type": "input_request"}))
|
| 60 |
line = await self.input_queue.get() # Wait for input from client
|
|
|
|
| 61 |
return line
|
| 62 |
except WebSocketDisconnect:
|
| 63 |
self.closed = True
|
| 64 |
-
print("WebSocket disconnected during readline.")
|
| 65 |
raise EOFError("Input stream closed due to WebSocket disconnect")
|
| 66 |
except Exception as e:
|
| 67 |
-
print(f"Error requesting input over WebSocket: {e}")
|
| 68 |
self.closed = True
|
| 69 |
raise EOFError(f"Input stream error: {e}")
|
| 70 |
|
| 71 |
# For compatibility with sys.stdin, which expects a file-like object
|
| 72 |
def _get_input_line(self):
|
| 73 |
"""Synchronous wrapper for readline for use with exec."""
|
| 74 |
-
# This is a bit tricky: exec is synchronous, but WebSocket is async.
|
| 75 |
-
# We need to bridge this. A simple way is to run the async part
|
| 76 |
-
# in the event loop, but it's usually better to refactor the executor
|
| 77 |
-
# to be fully async if input() is truly interactive.
|
| 78 |
-
# For this example, we'll use a blocking call to asyncio.run
|
| 79 |
-
# which is generally discouraged in a running event loop,
|
| 80 |
-
# but works for simple cases or if the exec is in a separate thread.
|
| 81 |
-
# A more robust solution involves a custom Future/Event loop integration.
|
| 82 |
try:
|
| 83 |
-
|
| 84 |
return asyncio.run(self.readline())
|
| 85 |
except Exception as e:
|
| 86 |
-
print(f"Synchronous input wrapper error: {e}")
|
| 87 |
raise
|
| 88 |
|
| 89 |
def read(self, n=-1):
|
| 90 |
"""Reads n characters. For simplicity, we'll treat it as readline."""
|
|
|
|
| 91 |
return self._get_input_line() # Or implement more complex buffering
|
| 92 |
|
| 93 |
def __getattr__(self, name):
|
| 94 |
"""Delegate other attributes if needed, or raise AttributeError."""
|
| 95 |
-
# This is a placeholder; real implementation would be more robust
|
| 96 |
-
# For now, we only need write, flush, and readline.
|
| 97 |
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
| 98 |
|
| 99 |
|
|
@@ -314,11 +309,13 @@ class SecurePythonExecutor:
|
|
| 314 |
async def execute_code(self, code: str, websocket_io: WebSocketInputOutput) -> Dict[str, Any]:
|
| 315 |
"""Execute Python code in a secure environment with WebSocket IO."""
|
| 316 |
start_time = time.time()
|
|
|
|
| 317 |
|
| 318 |
# Input validation
|
| 319 |
if not code or not code.strip():
|
| 320 |
error_msg = 'No code provided'
|
| 321 |
await websocket_io.write(f"Error: {error_msg}\n")
|
|
|
|
| 322 |
return {
|
| 323 |
'success': False,
|
| 324 |
'output': '',
|
|
@@ -341,6 +338,7 @@ class SecurePythonExecutor:
|
|
| 341 |
if not restricted_check['allowed']:
|
| 342 |
error_msg = f"Security violation: Restricted module '{restricted_check['module']}' is not allowed"
|
| 343 |
await websocket_io.write(f"Error: {error_msg}\n")
|
|
|
|
| 344 |
return {
|
| 345 |
'success': False,
|
| 346 |
'output': '',
|
|
@@ -352,6 +350,7 @@ class SecurePythonExecutor:
|
|
| 352 |
# Execute code with timeout
|
| 353 |
result = await self.run_with_timeout(code, websocket_io) # Pass websocket_io
|
| 354 |
execution_time = time.time() - start_time
|
|
|
|
| 355 |
|
| 356 |
return {
|
| 357 |
'success': result['success'],
|
|
@@ -374,12 +373,14 @@ class SecurePythonExecutor:
|
|
| 374 |
if node.module and node.module.split('.')[0] in self.restricted_modules:
|
| 375 |
return {'allowed': False, 'module': node.module}
|
| 376 |
return {'allowed': True, 'module': None}
|
| 377 |
-
except Exception:
|
|
|
|
| 378 |
return {'allowed': True, 'module': None} # If parsing fails, let execution handle it
|
| 379 |
|
| 380 |
async def run_with_timeout(self, code: str, websocket_io: WebSocketInputOutput) -> Dict[str, Any]:
|
| 381 |
"""Run code with timeout protection, interacting via WebSocketIO."""
|
| 382 |
result = {'success': False, 'output': '', 'error': ''}
|
|
|
|
| 383 |
|
| 384 |
# Store original stdin/stdout/stderr
|
| 385 |
original_stdin = sys.stdin
|
|
@@ -396,7 +397,8 @@ class SecurePythonExecutor:
|
|
| 396 |
sys.stdin = websocket_io
|
| 397 |
sys.stdout = websocket_io
|
| 398 |
sys.stderr = websocket_io
|
| 399 |
-
|
|
|
|
| 400 |
# Create restricted execution environment
|
| 401 |
restricted_globals = {
|
| 402 |
'__builtins__': self.safe_builtins,
|
|
@@ -404,21 +406,27 @@ class SecurePythonExecutor:
|
|
| 404 |
'__doc__': None,
|
| 405 |
}
|
| 406 |
|
|
|
|
| 407 |
# Execute the code
|
| 408 |
exec(code, restricted_globals)
|
|
|
|
| 409 |
result['success'] = True
|
| 410 |
|
| 411 |
except EOFError as e:
|
| 412 |
# This happens if the WebSocket disconnects during input()
|
| 413 |
result['error'] = f"Input stream closed: {str(e)}"
|
| 414 |
result['success'] = False
|
|
|
|
| 415 |
except Exception as e:
|
| 416 |
error_msg = f"{type(e).__name__}: {str(e)}\n{traceback.format_exc()}"
|
|
|
|
| 417 |
# Write error to WebSocket and internal buffer
|
| 418 |
try:
|
|
|
|
|
|
|
| 419 |
asyncio.run(websocket_io.write(f"Error: {error_msg}\n"))
|
| 420 |
except Exception as write_e:
|
| 421 |
-
print(f"Failed to write error to WebSocket: {write_e}")
|
| 422 |
websocket_io.error_buffer.write(error_msg)
|
| 423 |
result['success'] = False
|
| 424 |
finally:
|
|
@@ -427,16 +435,19 @@ class SecurePythonExecutor:
|
|
| 427 |
sys.stdout = original_stdout
|
| 428 |
sys.stderr = original_stderr
|
| 429 |
execution_finished_event.set() # Signal completion
|
|
|
|
| 430 |
|
| 431 |
# Run the execution in a separate thread to allow for timeout
|
| 432 |
-
thread = threading.Thread(target=target)
|
| 433 |
thread.daemon = True # Allow the program to exit even if thread is running
|
| 434 |
thread.start()
|
|
|
|
| 435 |
|
| 436 |
# Wait for the thread to finish or timeout
|
| 437 |
thread.join(timeout=self.timeout)
|
| 438 |
|
| 439 |
if thread.is_alive():
|
|
|
|
| 440 |
result['error'] = f"Code execution timed out after {self.timeout} seconds"
|
| 441 |
result['success'] = False
|
| 442 |
# Attempt to send timeout message to client
|
|
@@ -445,7 +456,7 @@ class SecurePythonExecutor:
|
|
| 445 |
except Exception:
|
| 446 |
pass # Ignore if websocket is already closed
|
| 447 |
else:
|
| 448 |
-
|
| 449 |
result['output'] = websocket_io.output_buffer.getvalue()
|
| 450 |
if websocket_io.error_buffer.getvalue():
|
| 451 |
result['error'] = websocket_io.error_buffer.getvalue()
|
|
@@ -455,12 +466,16 @@ class SecurePythonExecutor:
|
|
| 455 |
|
| 456 |
# Send a final message to the client indicating execution is complete
|
| 457 |
try:
|
|
|
|
| 458 |
await websocket_io.send_text(json.dumps({"type": "execution_complete", "result": result}))
|
|
|
|
| 459 |
except WebSocketDisconnect:
|
|
|
|
| 460 |
pass # Ignore if websocket is already closed
|
| 461 |
except Exception as e:
|
| 462 |
-
print(f"Error sending execution_complete message: {e}")
|
| 463 |
|
|
|
|
| 464 |
return result
|
| 465 |
|
| 466 |
|
|
@@ -479,6 +494,7 @@ app = FastAPI(
|
|
| 479 |
async def get_index():
|
| 480 |
"""Serves the interactive frontend."""
|
| 481 |
# This assumes index.html is in the same directory as app.py
|
|
|
|
| 482 |
with open("index.html", "r") as f:
|
| 483 |
return HTMLResponse(content=f.read())
|
| 484 |
|
|
@@ -493,6 +509,7 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 493 |
while True:
|
| 494 |
message = await websocket.receive_text()
|
| 495 |
data = json.loads(message)
|
|
|
|
| 496 |
|
| 497 |
if data["type"] == "code":
|
| 498 |
code = data["content"]
|
|
@@ -506,6 +523,7 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 506 |
elif data["type"] == "ping":
|
| 507 |
# Respond to pings to keep connection alive
|
| 508 |
await websocket.send_text(json.dumps({"type": "pong"}))
|
|
|
|
| 509 |
else:
|
| 510 |
await websocket_io.write(f"Unknown message type: {data['type']}\n")
|
| 511 |
|
|
@@ -548,5 +566,4 @@ if __name__ == "__main__":
|
|
| 548 |
)
|
| 549 |
except Exception as e:
|
| 550 |
print(f"Failed to start server: {e}")
|
| 551 |
-
sys.exit(1)
|
| 552 |
-
|
|
|
|
| 35 |
self.output_buffer = io.StringIO()
|
| 36 |
self.error_buffer = io.StringIO()
|
| 37 |
self.closed = False
|
| 38 |
+
print("DEBUG: WebSocketInputOutput initialized.")
|
| 39 |
|
| 40 |
async def write(self, s: str):
|
| 41 |
"""Writes to stdout/stderr and sends to WebSocket."""
|
|
|
|
| 43 |
try:
|
| 44 |
# Send output in chunks or lines to prevent large messages
|
| 45 |
await self.websocket.send_text(json.dumps({"type": "output", "content": s}))
|
| 46 |
+
print(f"DEBUG: Sent output chunk: {s.strip()[:100]}...")
|
| 47 |
except WebSocketDisconnect:
|
| 48 |
self.closed = True
|
| 49 |
+
print("ERROR: WebSocket disconnected during write.")
|
| 50 |
except Exception as e:
|
| 51 |
+
print(f"ERROR: Error sending output over WebSocket: {e}")
|
| 52 |
self.closed = True
|
| 53 |
|
| 54 |
def flush(self):
|
|
|
|
| 58 |
async def readline(self) -> str:
|
| 59 |
"""Reads a line from stdin, waiting for input from WebSocket."""
|
| 60 |
try:
|
| 61 |
+
print("DEBUG: Sending input_request to client.")
|
| 62 |
await self.websocket.send_text(json.dumps({"type": "input_request"}))
|
| 63 |
line = await self.input_queue.get() # Wait for input from client
|
| 64 |
+
print(f"DEBUG: Received input from queue: {line.strip()[:50]}...")
|
| 65 |
return line
|
| 66 |
except WebSocketDisconnect:
|
| 67 |
self.closed = True
|
| 68 |
+
print("ERROR: WebSocket disconnected during readline.")
|
| 69 |
raise EOFError("Input stream closed due to WebSocket disconnect")
|
| 70 |
except Exception as e:
|
| 71 |
+
print(f"ERROR: Error requesting input over WebSocket: {e}")
|
| 72 |
self.closed = True
|
| 73 |
raise EOFError(f"Input stream error: {e}")
|
| 74 |
|
| 75 |
# For compatibility with sys.stdin, which expects a file-like object
|
| 76 |
def _get_input_line(self):
|
| 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}")
|
| 83 |
raise
|
| 84 |
|
| 85 |
def read(self, n=-1):
|
| 86 |
"""Reads n characters. For simplicity, we'll treat it as readline."""
|
| 87 |
+
print(f"DEBUG: Read called with n={n}. Delegating to readline.")
|
| 88 |
return self._get_input_line() # Or implement more complex buffering
|
| 89 |
|
| 90 |
def __getattr__(self, name):
|
| 91 |
"""Delegate other attributes if needed, or raise AttributeError."""
|
|
|
|
|
|
|
| 92 |
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
| 93 |
|
| 94 |
|
|
|
|
| 309 |
async def execute_code(self, code: str, websocket_io: WebSocketInputOutput) -> Dict[str, Any]:
|
| 310 |
"""Execute Python code in a secure environment with WebSocket IO."""
|
| 311 |
start_time = time.time()
|
| 312 |
+
print(f"DEBUG: execute_code called for client: {websocket_io.websocket.client}")
|
| 313 |
|
| 314 |
# Input validation
|
| 315 |
if not code or not code.strip():
|
| 316 |
error_msg = 'No code provided'
|
| 317 |
await websocket_io.write(f"Error: {error_msg}\n")
|
| 318 |
+
print(f"DEBUG: No code provided for execution. Client: {websocket_io.websocket.client}")
|
| 319 |
return {
|
| 320 |
'success': False,
|
| 321 |
'output': '',
|
|
|
|
| 338 |
if not restricted_check['allowed']:
|
| 339 |
error_msg = f"Security violation: Restricted module '{restricted_check['module']}' is not allowed"
|
| 340 |
await websocket_io.write(f"Error: {error_msg}\n")
|
| 341 |
+
print(f"DEBUG: Restricted import detected: {restricted_check['module']}. Client: {websocket_io.websocket.client}")
|
| 342 |
return {
|
| 343 |
'success': False,
|
| 344 |
'output': '',
|
|
|
|
| 350 |
# Execute code with timeout
|
| 351 |
result = await self.run_with_timeout(code, websocket_io) # Pass websocket_io
|
| 352 |
execution_time = time.time() - start_time
|
| 353 |
+
print(f"DEBUG: Code execution finished. Success: {result['success']}, Client: {websocket_io.websocket.client}")
|
| 354 |
|
| 355 |
return {
|
| 356 |
'success': result['success'],
|
|
|
|
| 373 |
if node.module and node.module.split('.')[0] in self.restricted_modules:
|
| 374 |
return {'allowed': False, 'module': node.module}
|
| 375 |
return {'allowed': True, 'module': None}
|
| 376 |
+
except Exception as e:
|
| 377 |
+
print(f"WARNING: AST parsing failed for restricted import check: {e}")
|
| 378 |
return {'allowed': True, 'module': None} # If parsing fails, let execution handle it
|
| 379 |
|
| 380 |
async def run_with_timeout(self, code: str, websocket_io: WebSocketInputOutput) -> Dict[str, Any]:
|
| 381 |
"""Run code with timeout protection, interacting via WebSocketIO."""
|
| 382 |
result = {'success': False, 'output': '', 'error': ''}
|
| 383 |
+
print(f"DEBUG: run_with_timeout called. Client: {websocket_io.websocket.client}")
|
| 384 |
|
| 385 |
# Store original stdin/stdout/stderr
|
| 386 |
original_stdin = sys.stdin
|
|
|
|
| 397 |
sys.stdin = websocket_io
|
| 398 |
sys.stdout = websocket_io
|
| 399 |
sys.stderr = websocket_io
|
| 400 |
+
print(f"DEBUG: Thread '{threading.current_thread().name}' started. IO redirected.")
|
| 401 |
+
|
| 402 |
# Create restricted execution environment
|
| 403 |
restricted_globals = {
|
| 404 |
'__builtins__': self.safe_builtins,
|
|
|
|
| 406 |
'__doc__': None,
|
| 407 |
}
|
| 408 |
|
| 409 |
+
print(f"DEBUG: Entering exec context for code (thread): {code[:100]}...") # NEW
|
| 410 |
# Execute the code
|
| 411 |
exec(code, restricted_globals)
|
| 412 |
+
print("DEBUG: Code execution completed successfully in thread.") # NEW
|
| 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}") # NEW
|
| 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}") # NEW
|
| 423 |
# Write error to WebSocket and internal buffer
|
| 424 |
try:
|
| 425 |
+
# Use asyncio.run for writing within a synchronous thread, if necessary
|
| 426 |
+
# This is generally safe if it's a new event loop per call or in a separate thread.
|
| 427 |
asyncio.run(websocket_io.write(f"Error: {error_msg}\n"))
|
| 428 |
except Exception as write_e:
|
| 429 |
+
print(f"ERROR: Failed to write error to WebSocket from thread: {write_e}")
|
| 430 |
websocket_io.error_buffer.write(error_msg)
|
| 431 |
result['success'] = False
|
| 432 |
finally:
|
|
|
|
| 435 |
sys.stdout = original_stdout
|
| 436 |
sys.stderr = original_stderr
|
| 437 |
execution_finished_event.set() # Signal completion
|
| 438 |
+
print(f"DEBUG: Thread '{threading.current_thread().name}' finished. IO restored.")
|
| 439 |
|
| 440 |
# Run the execution in a separate thread to allow for timeout
|
| 441 |
+
thread = threading.Thread(target=target, name=f"CodeExecutorThread-{id(websocket_io)}")
|
| 442 |
thread.daemon = True # Allow the program to exit even if thread is running
|
| 443 |
thread.start()
|
| 444 |
+
print(f"DEBUG: Code execution thread '{thread.name}' started. Waiting for join.")
|
| 445 |
|
| 446 |
# Wait for the thread to finish or timeout
|
| 447 |
thread.join(timeout=self.timeout)
|
| 448 |
|
| 449 |
if thread.is_alive():
|
| 450 |
+
print(f"DEBUG: Thread '{thread.name}' timed out after {self.timeout} seconds.") # NEW
|
| 451 |
result['error'] = f"Code execution timed out after {self.timeout} seconds"
|
| 452 |
result['success'] = False
|
| 453 |
# Attempt to send timeout message to client
|
|
|
|
| 456 |
except Exception:
|
| 457 |
pass # Ignore if websocket is already closed
|
| 458 |
else:
|
| 459 |
+
print(f"DEBUG: Thread '{thread.name}' finished within timeout.") # NEW
|
| 460 |
result['output'] = websocket_io.output_buffer.getvalue()
|
| 461 |
if websocket_io.error_buffer.getvalue():
|
| 462 |
result['error'] = websocket_io.error_buffer.getvalue()
|
|
|
|
| 466 |
|
| 467 |
# Send a final message to the client indicating execution is complete
|
| 468 |
try:
|
| 469 |
+
print(f"DEBUG: Attempting to send execution_complete message (success={result['success']}).") # NEW
|
| 470 |
await websocket_io.send_text(json.dumps({"type": "execution_complete", "result": result}))
|
| 471 |
+
print("DEBUG: execution_complete message sent.")
|
| 472 |
except WebSocketDisconnect:
|
| 473 |
+
print("DEBUG: WebSocketDisconnect when sending execution_complete.") # NEW
|
| 474 |
pass # Ignore if websocket is already closed
|
| 475 |
except Exception as e:
|
| 476 |
+
print(f"ERROR: Error sending execution_complete message: {e}") # Warning if this fails
|
| 477 |
|
| 478 |
+
print(f"DEBUG: run_with_timeout returning result for client: {websocket_io.websocket.client}")
|
| 479 |
return result
|
| 480 |
|
| 481 |
|
|
|
|
| 494 |
async def get_index():
|
| 495 |
"""Serves the interactive frontend."""
|
| 496 |
# This assumes index.html is in the same directory as app.py
|
| 497 |
+
[cite_start]# [cite: 9] Explicitly copy index.html to /app/index.html to avoid any ambiguity with '.'
|
| 498 |
with open("index.html", "r") as f:
|
| 499 |
return HTMLResponse(content=f.read())
|
| 500 |
|
|
|
|
| 509 |
while True:
|
| 510 |
message = await websocket.receive_text()
|
| 511 |
data = json.loads(message)
|
| 512 |
+
print(f"DEBUG: Received message from {websocket.client}: type={data['type']}")
|
| 513 |
|
| 514 |
if data["type"] == "code":
|
| 515 |
code = data["content"]
|
|
|
|
| 523 |
elif data["type"] == "ping":
|
| 524 |
# Respond to pings to keep connection alive
|
| 525 |
await websocket.send_text(json.dumps({"type": "pong"}))
|
| 526 |
+
print(f"DEBUG: Sent pong to {websocket.client}")
|
| 527 |
else:
|
| 528 |
await websocket_io.write(f"Unknown message type: {data['type']}\n")
|
| 529 |
|
|
|
|
| 566 |
)
|
| 567 |
except Exception as e:
|
| 568 |
print(f"Failed to start server: {e}")
|
| 569 |
+
sys.exit(1)
|
|
|