| |
| """ |
| HTTP server that runs inside the Docker container to execute Python code. |
| This server creates temporary directories for each execution and cleans them up. |
| """ |
|
|
| import asyncio |
| import os |
| import subprocess |
| import tempfile |
| import shutil |
| import logging |
| from concurrent.futures import ThreadPoolExecutor |
| from typing import Dict, Any |
| from aiohttp import web |
|
|
| |
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
|
|
| |
| BASE_TEMP_DIR = os.environ.get("SANDBOX_TEMP_DIR", "/tmp/sandbox_executions") |
|
|
| |
| _executor = ThreadPoolExecutor(max_workers=10) |
|
|
|
|
| def _run_subprocess_sync(_code_file: str, temp_dir: str, timeout: int) -> Dict[str, Any]: |
| """ |
| Synchronous helper function to run subprocess. |
| This runs in a thread pool to avoid blocking the event loop. |
| """ |
| try: |
| with subprocess.Popen( |
| ["python", "-u", "code.py"], |
| cwd=temp_dir, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| text=True, |
| bufsize=1, |
| universal_newlines=True, |
| ) as process: |
| try: |
| |
| stdout, _ = process.communicate(timeout=timeout) |
| exit_code = process.returncode |
|
|
| return { |
| "success": exit_code == 0, |
| "exit_code": exit_code, |
| "output": stdout or "", |
| "error": None if exit_code == 0 else stdout |
| } |
| except subprocess.TimeoutExpired: |
| process.kill() |
| process.wait() |
| return { |
| "success": False, |
| "exit_code": -1, |
| "output": "", |
| "error": f"Execution timeout after {timeout} seconds" |
| } |
| except Exception as e: |
| logger.error("Error in subprocess execution: %s", e) |
| return { |
| "success": False, |
| "exit_code": -1, |
| "output": "", |
| "error": f"Execution error: {str(e)}" |
| } |
|
|
|
|
| async def execute_python_code(code: str, timeout: int = 300) -> Dict[str, Any]: |
| """ |
| Execute Python code in a temporary directory. |
| Each execution runs in its own process and thread, allowing concurrent requests. |
| |
| Args: |
| code: Python code to execute |
| timeout: Maximum execution time in seconds |
| |
| Returns: |
| Dictionary with success, exit_code, output, and error fields |
| """ |
| temp_dir = None |
| try: |
| |
| os.makedirs(BASE_TEMP_DIR, exist_ok=True) |
| temp_dir = tempfile.mkdtemp(prefix="exec_", dir=BASE_TEMP_DIR) |
| logger.info("Created temporary directory: %s", temp_dir) |
|
|
| |
| code_file = os.path.join(temp_dir, "code.py") |
| with open(code_file, "w", encoding="utf-8") as f: |
| f.write(code) |
|
|
| |
| |
| loop = asyncio.get_event_loop() |
| result = await loop.run_in_executor( |
| _executor, |
| _run_subprocess_sync, |
| code_file, |
| temp_dir, |
| timeout |
| ) |
|
|
| return result |
|
|
| except Exception as e: |
| logger.error("Error executing code: %s", e) |
| return { |
| "success": False, |
| "exit_code": -1, |
| "output": "", |
| "error": f"Execution error: {str(e)}" |
| } |
| finally: |
| |
| if temp_dir and os.path.exists(temp_dir): |
| try: |
| |
| loop = asyncio.get_event_loop() |
| await loop.run_in_executor(_executor, shutil.rmtree, temp_dir) |
| logger.info("Cleaned up temporary directory: %s", temp_dir) |
| except Exception as e: |
| logger.warning("Failed to clean up %s: %s", temp_dir, e) |
|
|
|
|
| async def handle_execute(request: web.Request) -> web.Response: |
| """Handle code execution requests""" |
| try: |
| data = await request.json() |
| code = data.get("code", "") |
| timeout = data.get("timeout", 300) |
|
|
| if not code.strip(): |
| return web.json_response({ |
| "success": False, |
| "exit_code": -1, |
| "output": "", |
| "error": "No code provided" |
| }, status=400) |
|
|
| result = await execute_python_code(code, timeout) |
| return web.json_response(result) |
|
|
| except Exception as e: |
| logger.error("Error handling request: %s", e) |
| return web.json_response({ |
| "success": False, |
| "exit_code": -1, |
| "output": "", |
| "error": f"Server error: {str(e)}" |
| }, status=500) |
|
|
|
|
| async def handle_health(_request: web.Request) -> web.Response: |
| """Health check endpoint""" |
| return web.json_response({"status": "ok"}) |
|
|
|
|
| def create_app(): |
| """Create the aiohttp application""" |
| app = web.Application() |
| app.router.add_post("/execute", handle_execute) |
| app.router.add_get("/health", handle_health) |
| return app |
|
|
|
|
| async def main(): |
| """Main entry point""" |
| port = int(os.environ.get("SANDBOX_PORT", "8080")) |
| logger.info("Starting sandbox HTTP server on port %s", port) |
|
|
| app = create_app() |
| runner = web.AppRunner(app) |
| await runner.setup() |
|
|
| site = web.TCPSite(runner, "0.0.0.0", port) |
| await site.start() |
|
|
| logger.info("Sandbox server ready on port %s", port) |
|
|
| |
| try: |
| await asyncio.Event().wait() |
| except KeyboardInterrupt: |
| logger.info("Shutting down server...") |
| finally: |
| await runner.cleanup() |
| _executor.shutdown(wait=True) |
| logger.info("Thread pool executor shut down") |
|
|
|
|
| if __name__ == "__main__": |
| asyncio.run(main()) |
|
|