backend.py (18206B)
1 import asyncio 2 import aiohttp 3 from aiohttp import web 4 import json 5 import subprocess 6 import os 7 import sys 8 import threading 9 import re 10 import importlib.util 11 import platform 12 import uuid 13 import shutil 14 15 # ================================================================================== 16 # CLOUD COMPILER SERVER (AIOHTTP) - RENDER COMPATIBLE - SECURE VERSION 17 # ================================================================================== 18 19 # Get API Key from Environment 20 GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") 21 22 def install_package(package_name, ws=None, loop=None): 23 """Attempt to install a python package via pip.""" 24 try: 25 if importlib.util.find_spec(package_name) is not None: 26 return 27 28 msg = f"Installing missing package: {package_name}..." 29 print(msg) 30 if ws and loop: 31 asyncio.run_coroutine_threadsafe( 32 ws.send_json({'type': 'status', 'msg': msg}), 33 loop 34 ) 35 36 subprocess.check_call([sys.executable, "-m", "pip", "install", package_name]) 37 except Exception as e: 38 error_msg = f"Failed to install {package_name}: {e}" 39 print(error_msg) 40 if ws and loop: 41 asyncio.run_coroutine_threadsafe( 42 ws.send_json({'type': 'stdout', 'data': f"\n[Error] {error_msg}\n"}), 43 loop 44 ) 45 46 def check_and_install_packages(code, ws=None, loop=None): 47 """Scan Python code for imports and install them if missing.""" 48 imports = re.findall(r'^\s*import\s+(\w+)', code, re.MULTILINE) 49 from_imports = re.findall(r'^\s*from\s+(\w+)', code, re.MULTILINE) 50 unique_packages = set(imports + from_imports) 51 for pkg in unique_packages: 52 if pkg in ['os', 'sys', 'time', 'random', 'math', 'json', 'asyncio', 'threading', 'platform', 'subprocess', 're', 'aiohttp']: 53 continue 54 install_package(pkg, ws, loop) 55 56 async def handle_client(request): 57 # --- HEALTH CHECK HANDLING --- 58 if request.headers.get("Upgrade", "").lower() != "websocket": 59 return web.Response(text="OK") 60 61 # --- WEBSOCKET HANDLING --- 62 ws = web.WebSocketResponse() 63 await ws.prepare(request) 64 65 print(f"Client connected: {request.remote}") 66 67 # Create unique session directory for this client 68 session_id = str(uuid.uuid4()) 69 session_dir = os.path.join("temp_sessions", session_id) 70 os.makedirs(session_dir, exist_ok=True) 71 print(f"Created session directory: {session_dir}") 72 73 process = None 74 75 # Helper to read output stream in a separate thread 76 def read_stream(stream, loop): 77 try: 78 while True: 79 char = stream.read(1) 80 if not char: 81 break 82 asyncio.run_coroutine_threadsafe( 83 ws.send_json({'type': 'stdout', 'data': char}), 84 loop 85 ) 86 87 asyncio.run_coroutine_threadsafe( 88 ws.send_json({'type': 'status', 'msg': 'Program finished'}), 89 loop 90 ) 91 except Exception: 92 pass 93 94 try: 95 async for msg in ws: 96 if msg.type == web.WSMsgType.TEXT: 97 data = json.loads(msg.data) 98 99 if data.get('type') == 'run': 100 code = data.get('code') 101 language = data.get('language', 'python') 102 103 # --- PYTHON HANDLING --- 104 if language == 'python': 105 await ws.send_json({'type': 'status', 'msg': 'Checking dependencies...'}) 106 loop = asyncio.get_running_loop() 107 await loop.run_in_executor(None, check_and_install_packages, code, ws, loop) 108 109 filename = os.path.join(session_dir, "temp_script.py") 110 with open(filename, "w", encoding="utf-8") as f: 111 f.write(code) 112 113 # 🔒 SECURITY FIX: Sanitized environment (no API keys) 114 safe_env = { 115 "PYTHONIOENCODING": "utf-8", 116 "PATH": os.environ.get("PATH", ""), 117 "PYTHONPATH": os.environ.get("PYTHONPATH", ""), 118 "HOME": os.environ.get("HOME", ""), 119 "USER": os.environ.get("USER", ""), 120 "LANG": os.environ.get("LANG", "en_US.UTF-8"), 121 } 122 123 process = subprocess.Popen( 124 [sys.executable, "-u", filename], 125 stdin=subprocess.PIPE, 126 stdout=subprocess.PIPE, 127 stderr=subprocess.STDOUT, 128 text=True, 129 bufsize=0, 130 encoding='utf-8', 131 env=safe_env # ✅ Use sanitized environment 132 ) 133 await ws.send_json({'type': 'status', 'msg': 'Running Python...'}) 134 135 # --- C HANDLING --- 136 elif language == 'c': 137 filename = os.path.join(session_dir, "temp_code.c") 138 executable = os.path.join(session_dir, "a.out") if platform.system() != "Windows" else os.path.join(session_dir, "a.exe") 139 140 with open(filename, "w", encoding="utf-8") as f: 141 f.write(code) 142 143 await ws.send_json({'type': 'status', 'msg': 'Compiling C...'}) 144 145 # 🔒 SECURITY: Sanitized environment 146 safe_env = { 147 "PATH": os.environ.get("PATH", ""), 148 "HOME": os.environ.get("HOME", ""), 149 "USER": os.environ.get("USER", ""), 150 "LANG": os.environ.get("LANG", "en_US.UTF-8"), 151 "TMPDIR": os.environ.get("TMPDIR", "/tmp"), 152 } 153 154 compile_process = subprocess.run( 155 ["gcc", filename, "-o", executable, "-lm", "-pthread"], 156 capture_output=True, 157 text=True, 158 env=safe_env 159 ) 160 161 if compile_process.returncode != 0: 162 await ws.send_json({'type': 'stdout', 'data': f"Compilation Error:\n{compile_process.stderr}"}) 163 await ws.send_json({'type': 'status', 'msg': 'Compilation Failed'}) 164 continue 165 166 await ws.send_json({'type': 'status', 'msg': 'Running C Binary...'}) 167 168 process = subprocess.Popen( 169 [executable], 170 stdin=subprocess.PIPE, 171 stdout=subprocess.PIPE, 172 stderr=subprocess.STDOUT, 173 text=True, 174 bufsize=0, 175 encoding='utf-8', 176 env=safe_env # ✅ Sanitized environment 177 ) 178 179 # --- C++ HANDLING --- 180 elif language == 'cpp': 181 # FIXED: Removed the space in "temp_code.cpp" 182 filename = os.path.join(session_dir, "temp_code.cpp") 183 executable = os.path.join(session_dir, "a.out") if platform.system() != "Windows" else os.path.join(session_dir, "a.exe") 184 185 with open(filename, "w", encoding="utf-8") as f: 186 f.write(code) 187 188 await ws.send_json({'type': 'status', 'msg': 'Compiling C++...'}) 189 190 # 🔒 SECURITY: Sanitized environment 191 safe_env = { 192 "PATH": os.environ.get("PATH", ""), 193 "HOME": os.environ.get("HOME", ""), 194 "USER": os.environ.get("USER", ""), 195 "LANG": os.environ.get("LANG", "en_US.UTF-8"), 196 "TMPDIR": os.environ.get("TMPDIR", "/tmp"), 197 } 198 199 compile_process = subprocess.run( 200 ["g++", filename, "-o", executable, "-lm", "-pthread"], 201 capture_output=True, 202 text=True, 203 env=safe_env 204 ) 205 206 if compile_process.returncode != 0: 207 await ws.send_json({'type': 'stdout', 'data': f"Compilation Error:\n{compile_process.stderr}"}) 208 await ws.send_json({'type': 'status', 'msg': 'Compilation Failed'}) 209 continue 210 211 await ws.send_json({'type': 'status', 'msg': 'Running C++ Binary...'}) 212 213 process = subprocess.Popen( 214 [executable], 215 stdin=subprocess.PIPE, 216 stdout=subprocess.PIPE, 217 stderr=subprocess.STDOUT, 218 text=True, 219 bufsize=0, 220 encoding='utf-8', 221 env=safe_env # ✅ Sanitized environment 222 ) 223 224 if process: 225 loop = asyncio.get_running_loop() 226 thread = threading.Thread(target=read_stream, args=(process.stdout, loop)) 227 thread.daemon = True 228 thread.start() 229 230 elif data.get('type') == 'input': 231 if process and process.poll() is None: 232 user_input = data.get('data') 233 try: 234 process.stdin.write(user_input) 235 process.stdin.flush() 236 except Exception: 237 pass 238 239 # --- AI FIX HANDLER --- 240 elif data.get('type') == 'ai_fix': 241 code = data.get('code') 242 error_log = data.get('error') 243 language = data.get('language', 'c') 244 245 if not GEMINI_API_KEY: 246 await ws.send_json({'type': 'ai_error', 'msg': 'Server Error: GEMINI_API_KEY not configured.'}) 247 continue 248 249 # Construct Prompt - Ask for JSON in plain text 250 prompt = f"""You are an expert {language.upper()} debugger. Analyze this code and error, then respond with ONLY a valid JSON object (no markdown, no backticks). 251 252 CODE: 253 {code} 254 255 ERROR: 256 {error_log} 257 258 Respond in this exact JSON format: 259 {{ 260 "explanation": "Brief 1-2 sentence explanation of the bug", 261 "fixed_code": "Complete corrected code here" 262 }}""" 263 264 try: 265 api_url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-lite-preview:generateContent?key={GEMINI_API_KEY}" 266 267 print(f"[AI] Calling Gemini API...") 268 269 async with aiohttp.ClientSession() as session: 270 async with session.post( 271 api_url, 272 headers={"Content-Type": "application/json"}, 273 json={ 274 "contents": [{"parts": [{"text": prompt}]}], 275 "generationConfig": { 276 "temperature": 0.1, 277 "maxOutputTokens": 2048 278 } 279 } 280 ) as resp: 281 response_text = await resp.text() 282 283 print(f"[AI] Status: {resp.status}") 284 print(f"[AI] Response preview: {response_text[: 200]}") 285 286 if resp.status != 200: 287 await ws.send_json({ 288 'type': 'ai_error', 289 'msg': f"AI API Error (Status {resp.status})" 290 }) 291 else: 292 try: 293 result = json.loads(response_text) 294 295 # Extract AI response 296 ai_text = result['candidates'][0]['content']['parts'][0]['text'] 297 298 # Clean markdown if present 299 ai_text = ai_text.strip() 300 if ai_text.startswith('```json'): 301 ai_text = ai_text.split('```json')[1].split('```')[0].strip() 302 elif ai_text.startswith('```'): 303 ai_text = ai_text.split('```')[1].split('```')[0].strip() 304 305 # Parse JSON 306 parsed_response = json.loads(ai_text) 307 308 # Validate structure 309 if 'explanation' in parsed_response and 'fixed_code' in parsed_response: 310 print(f"[AI] Success! Sending response to client") 311 await ws.send_json({ 312 'type': 'ai_response', 313 'data': parsed_response 314 }) 315 else: 316 print(f"[AI] Invalid structure: {parsed_response}") 317 await ws.send_json({ 318 'type': 'ai_error', 319 'msg': 'Invalid AI response format' 320 }) 321 322 except (KeyError, IndexError) as e: 323 print(f"[AI] API structure error: {e}") 324 await ws.send_json({ 325 'type': 'ai_error', 326 'msg': 'Unexpected API response' 327 }) 328 except json.JSONDecodeError as e: 329 print(f"[AI] JSON parse error: {e}") 330 print(f"[AI] Attempted to parse: {ai_text[: 300]}") 331 await ws.send_json({ 332 'type': 'ai_error', 333 'msg': 'AI returned invalid JSON' 334 }) 335 336 except aiohttp.ClientError as e: 337 print(f"[AI] Network error: {e}") 338 await ws.send_json({ 339 'type': 'ai_error', 340 'msg': 'Network error' 341 }) 342 except Exception as e: 343 print(f"[AI] Unexpected error: {e}") 344 import traceback 345 traceback.print_exc() 346 await ws.send_json({ 347 'type': 'ai_error', 348 'msg': f'Server error: {type(e).__name__}' 349 }) 350 351 elif msg.type == web.WSMsgType.ERROR: 352 print(f'ws connection closed with exception {ws.exception()}') 353 354 finally: 355 if process: 356 try: 357 process.kill() 358 except: 359 pass 360 # Clean up session directory 361 try: 362 shutil.rmtree(session_dir, ignore_errors=True) 363 print(f"Cleaned up session directory: {session_dir}") 364 except: 365 pass 366 print("Client disconnected") 367 368 return ws 369 370 async def main(): 371 # Create temp_sessions directory if it doesn't exist 372 os.makedirs("temp_sessions", exist_ok=True) 373 print("temp_sessions directory ready") 374 375 port = int(os.environ.get("PORT", 8765)) 376 app = web.Application() 377 app.add_routes([web.get('/', handle_client)]) 378 379 runner = web.AppRunner(app) 380 await runner.setup() 381 site = web.TCPSite(runner, '0.0.0.0', port) 382 print(f"AIOHTTP Server started on port {port}...") 383 await site.start() 384 385 # Keep the server running 386 await asyncio.Event().wait() 387 388 if __name__ == "__main__": 389 asyncio.run(main()) 390 391