ABAO77 commited on
Commit
a2049a0
·
verified ·
1 Parent(s): a0a4d87

Upload app_3.py

Browse files
Files changed (1) hide show
  1. app_3.py +1047 -0
app_3.py ADDED
@@ -0,0 +1,1047 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import shutil
4
+ import asyncio
5
+ from pathlib import Path
6
+ from typing import List, Optional, Dict, Any
7
+ import uuid
8
+ import re
9
+ from fastapi import (
10
+ UploadFile,
11
+ File,
12
+ Form,
13
+ HTTPException,
14
+ FastAPI,
15
+ )
16
+ from fastapi.responses import JSONResponse
17
+ from pydantic import BaseModel
18
+ import json
19
+ import resource
20
+ import platform
21
+ from loguru import logger
22
+
23
+ app = FastAPI(title="Code Execution and Input Analysis API", docs_url="/")
24
+
25
+ # Configuration
26
+ MAX_EXECUTION_TIME = 5 # seconds
27
+ MAX_MEMORY = 256 * 1024 * 1024 # 256MB
28
+ MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
29
+ MAX_TOTAL_SIZE = 2 * 1024 * 1024 # 2MB total size limit for uploads and repos
30
+
31
+
32
+ class ExecutionResult(BaseModel):
33
+ success: bool
34
+ stdout: str
35
+ stderr: str
36
+ execution_time: float
37
+ exit_code: int
38
+ error: Optional[str] = None
39
+
40
+
41
+ class InputPattern(BaseModel):
42
+ type: str # "input", "scanf", "cin", etc.
43
+ line_number: int
44
+ variable_name: Optional[str] = None
45
+ prompt_message: Optional[str] = None
46
+ data_type: Optional[str] = None # "int", "str", "float", etc.
47
+ raw_code: str
48
+
49
+
50
+ class InputAnalysisResult(BaseModel):
51
+ language: str
52
+ total_inputs: int
53
+ input_patterns: List[InputPattern]
54
+ suggestions: List[str] # UI suggestions for user
55
+
56
+
57
+ class CodeExecutor:
58
+ def __init__(self):
59
+ self.compilers = {
60
+ "c": "clang" if platform.system() == "Darwin" else "gcc",
61
+ "cpp": "clang++" if platform.system() == "Darwin" else "g++",
62
+ "java": "javac",
63
+ "python": "python3",
64
+ }
65
+
66
+ def set_resource_limits(self):
67
+ """Set resource limits for subprocess (Unix only)"""
68
+ if platform.system() == "Linux":
69
+ resource.setrlimit(
70
+ resource.RLIMIT_CPU, (MAX_EXECUTION_TIME, MAX_EXECUTION_TIME)
71
+ )
72
+ resource.setrlimit(resource.RLIMIT_AS, (MAX_MEMORY, MAX_MEMORY))
73
+
74
+ def analyze_input_patterns(
75
+ self, language: str, file_contents: Dict[str, str]
76
+ ) -> InputAnalysisResult:
77
+ """Analyze code files to detect input patterns"""
78
+ patterns = []
79
+
80
+ if language == "python":
81
+ patterns = self._analyze_python_inputs(file_contents)
82
+ elif language == "java":
83
+ patterns = self._analyze_java_inputs(file_contents)
84
+ elif language in ["c", "cpp"]:
85
+ patterns = self._analyze_c_cpp_inputs(file_contents)
86
+
87
+ suggestions = self._generate_input_suggestions(patterns, language)
88
+
89
+ return InputAnalysisResult(
90
+ language=language,
91
+ total_inputs=len(patterns),
92
+ input_patterns=patterns,
93
+ suggestions=suggestions,
94
+ )
95
+
96
+ def _analyze_python_inputs(
97
+ self, file_contents: Dict[str, str]
98
+ ) -> List[InputPattern]:
99
+ """Analyze Python files for input() patterns"""
100
+ patterns = []
101
+
102
+ for filename, content in file_contents.items():
103
+ lines = content.split("\n")
104
+
105
+ for i, line in enumerate(lines, 1):
106
+ # Pattern 1: variable = input("prompt")
107
+ match = re.search(
108
+ r'(\w+)\s*=\s*input\s*\(\s*["\']([^"\']*)["\']?\s*\)', line
109
+ )
110
+ if match:
111
+ var_name, prompt = match.groups()
112
+ patterns.append(
113
+ InputPattern(
114
+ type="input",
115
+ line_number=i,
116
+ variable_name=var_name,
117
+ prompt_message=prompt or f"Enter value for {var_name}",
118
+ data_type="str",
119
+ raw_code=line.strip(),
120
+ )
121
+ )
122
+ continue
123
+
124
+ # Pattern 2: variable = int(input("prompt"))
125
+ match = re.search(
126
+ r'(\w+)\s*=\s*(int|float|str)\s*\(\s*input\s*\(\s*["\']([^"\']*)["\']?\s*\)\s*\)',
127
+ line,
128
+ )
129
+ if match:
130
+ var_name, data_type, prompt = match.groups()
131
+ patterns.append(
132
+ InputPattern(
133
+ type="input",
134
+ line_number=i,
135
+ variable_name=var_name,
136
+ prompt_message=prompt
137
+ or f"Enter {data_type} value for {var_name}",
138
+ data_type=data_type,
139
+ raw_code=line.strip(),
140
+ )
141
+ )
142
+ continue
143
+
144
+ # Pattern 3: Simple input() without assignment
145
+ if "input(" in line and "=" not in line:
146
+ patterns.append(
147
+ InputPattern(
148
+ type="input",
149
+ line_number=i,
150
+ variable_name=None,
151
+ prompt_message="Enter input",
152
+ data_type="str",
153
+ raw_code=line.strip(),
154
+ )
155
+ )
156
+
157
+ return patterns
158
+
159
+ def _analyze_java_inputs(self, file_contents: Dict[str, str]) -> List[InputPattern]:
160
+ """Analyze Java files for Scanner input patterns"""
161
+ patterns = []
162
+
163
+ for filename, content in file_contents.items():
164
+ lines = content.split("\n")
165
+
166
+ for i, line in enumerate(lines, 1):
167
+ # Pattern 1: scanner.nextInt(), scanner.nextLine(), etc.
168
+ match = re.search(r"(\w+)\s*=\s*(\w+)\.next(\w+)\s*\(\s*\)", line)
169
+ if match:
170
+ var_name, scanner_name, method = match.groups()
171
+ data_type = self._java_method_to_type(method)
172
+ patterns.append(
173
+ InputPattern(
174
+ type="scanner",
175
+ line_number=i,
176
+ variable_name=var_name,
177
+ prompt_message=f"Enter {data_type} value for {var_name}",
178
+ data_type=data_type,
179
+ raw_code=line.strip(),
180
+ )
181
+ )
182
+ continue
183
+
184
+ # Pattern 2: Direct scanner calls without assignment
185
+ match = re.search(r"(\w+)\.next(\w+)\s*\(\s*\)", line)
186
+ if match and "=" not in line:
187
+ scanner_name, method = match.groups()
188
+ data_type = self._java_method_to_type(method)
189
+ patterns.append(
190
+ InputPattern(
191
+ type="scanner",
192
+ line_number=i,
193
+ variable_name=None,
194
+ prompt_message=f"Enter {data_type} input",
195
+ data_type=data_type,
196
+ raw_code=line.strip(),
197
+ )
198
+ )
199
+
200
+ return patterns
201
+
202
+ def _analyze_c_cpp_inputs(
203
+ self, file_contents: Dict[str, str]
204
+ ) -> List[InputPattern]:
205
+ """Analyze C/C++ files for input patterns"""
206
+ patterns = []
207
+
208
+ for filename, content in file_contents.items():
209
+ lines = content.split("\n")
210
+
211
+ for i, line in enumerate(lines, 1):
212
+ # Pattern 1: scanf("%d", &variable)
213
+ match = re.search(
214
+ r'scanf\s*\(\s*["\']([^"\']*)["\'],\s*&(\w+)\s*\)', line
215
+ )
216
+ if match:
217
+ format_spec, var_name = match.groups()
218
+ data_type = self._c_format_to_type(format_spec)
219
+ patterns.append(
220
+ InputPattern(
221
+ type="scanf",
222
+ line_number=i,
223
+ variable_name=var_name,
224
+ prompt_message=f"Enter {data_type} value for {var_name}",
225
+ data_type=data_type,
226
+ raw_code=line.strip(),
227
+ )
228
+ )
229
+ continue
230
+
231
+ # Pattern 2: cin >> variable (C++)
232
+ match = re.search(r"cin\s*>>\s*(\w+)", line)
233
+ if match:
234
+ var_name = match.group(1)
235
+ patterns.append(
236
+ InputPattern(
237
+ type="cin",
238
+ line_number=i,
239
+ variable_name=var_name,
240
+ prompt_message=f"Enter value for {var_name}",
241
+ data_type="unknown",
242
+ raw_code=line.strip(),
243
+ )
244
+ )
245
+ continue
246
+
247
+ # Pattern 3: getline(cin, variable) for strings
248
+ match = re.search(r"getline\s*\(\s*cin\s*,\s*(\w+)\s*\)", line)
249
+ if match:
250
+ var_name = match.group(1)
251
+ patterns.append(
252
+ InputPattern(
253
+ type="getline",
254
+ line_number=i,
255
+ variable_name=var_name,
256
+ prompt_message=f"Enter string for {var_name}",
257
+ data_type="string",
258
+ raw_code=line.strip(),
259
+ )
260
+ )
261
+
262
+ return patterns
263
+
264
+ def _java_method_to_type(self, method: str) -> str:
265
+ """Convert Java Scanner method to data type"""
266
+ type_mapping = {
267
+ "Int": "int",
268
+ "Double": "double",
269
+ "Float": "float",
270
+ "Long": "long",
271
+ "Line": "string",
272
+ "": "string",
273
+ }
274
+ return type_mapping.get(method, "string")
275
+
276
+ def _c_format_to_type(self, format_spec: str) -> str:
277
+ """Convert C format specifier to data type"""
278
+ if "%d" in format_spec or "%i" in format_spec:
279
+ return "int"
280
+ elif "%f" in format_spec:
281
+ return "float"
282
+ elif "%lf" in format_spec:
283
+ return "double"
284
+ elif "%c" in format_spec:
285
+ return "char"
286
+ elif "%s" in format_spec:
287
+ return "string"
288
+ return "unknown"
289
+
290
+ def _generate_input_suggestions(
291
+ self, patterns: List[InputPattern], language: str
292
+ ) -> List[str]:
293
+ """Generate UI suggestions based on detected patterns"""
294
+ suggestions = []
295
+
296
+ if not patterns:
297
+ suggestions.append(
298
+ "No input patterns detected. Code will run without user input."
299
+ )
300
+ return suggestions
301
+
302
+ suggestions.append(
303
+ f"Detected {len(patterns)} input requirement(s) in {language} code:"
304
+ )
305
+
306
+ for i, pattern in enumerate(patterns, 1):
307
+ if pattern.variable_name:
308
+ suggestions.append(
309
+ f"{i}. Line {pattern.line_number}: {pattern.prompt_message} "
310
+ f"(Variable: {pattern.variable_name}, Type: {pattern.data_type})"
311
+ )
312
+ else:
313
+ suggestions.append(
314
+ f"{i}. Line {pattern.line_number}: {pattern.prompt_message} "
315
+ f"(Type: {pattern.data_type})"
316
+ )
317
+
318
+ suggestions.append(
319
+ "Please provide input values in the order they appear in the code."
320
+ )
321
+
322
+ return suggestions
323
+
324
+ async def execute_code(
325
+ self,
326
+ language: str,
327
+ main_files: List[str],
328
+ workspace: str,
329
+ input_data: Optional[List[str]] = None,
330
+ ) -> ExecutionResult:
331
+ """Execute code based on language with optional input data"""
332
+ try:
333
+ if language == "python":
334
+ return await self._execute_python(main_files, workspace, input_data)
335
+ elif language == "java":
336
+ return await self._execute_java(main_files, workspace, input_data)
337
+ elif language in ["c", "cpp"]:
338
+ return await self._execute_c_cpp(
339
+ main_files, workspace, language, input_data
340
+ )
341
+ else:
342
+ raise ValueError(f"Unsupported language: {language}")
343
+ except Exception as e:
344
+ return ExecutionResult(
345
+ success=False,
346
+ stdout="",
347
+ stderr=str(e),
348
+ execution_time=0,
349
+ exit_code=-1,
350
+ error=str(e),
351
+ )
352
+
353
+ async def _execute_with_input(
354
+ self, command: List[str], workspace: str, input_data: Optional[List[str]] = None
355
+ ) -> tuple:
356
+ """Execute process with input data"""
357
+ process = await asyncio.create_subprocess_exec(
358
+ *command,
359
+ cwd=workspace,
360
+ stdout=asyncio.subprocess.PIPE,
361
+ stderr=asyncio.subprocess.PIPE,
362
+ stdin=asyncio.subprocess.PIPE,
363
+ preexec_fn=(
364
+ self.set_resource_limits if platform.system() == "Linux" else None
365
+ ),
366
+ )
367
+
368
+ # Prepare input string
369
+ stdin_input = None
370
+ if input_data:
371
+ stdin_input = "\n".join(input_data) + "\n"
372
+ stdin_input = stdin_input.encode("utf-8")
373
+
374
+ try:
375
+ stdout, stderr = await asyncio.wait_for(
376
+ process.communicate(input=stdin_input), timeout=MAX_EXECUTION_TIME
377
+ )
378
+ return stdout, stderr, process.returncode
379
+ except asyncio.TimeoutError:
380
+ process.kill()
381
+ await process.wait()
382
+ raise asyncio.TimeoutError()
383
+
384
+ async def _execute_python(
385
+ self,
386
+ main_files: List[str],
387
+ workspace: str,
388
+ input_data: Optional[List[str]] = None,
389
+ ) -> ExecutionResult:
390
+ """Execute Python code with input support"""
391
+ results = []
392
+
393
+ for main_file in main_files:
394
+ file_path = os.path.join(workspace, main_file)
395
+ if not os.path.exists(file_path):
396
+ results.append(
397
+ ExecutionResult(
398
+ success=False,
399
+ stdout="",
400
+ stderr=f"File not found: {main_file}",
401
+ execution_time=0,
402
+ exit_code=-1,
403
+ )
404
+ )
405
+ continue
406
+
407
+ try:
408
+ start_time = asyncio.get_event_loop().time()
409
+
410
+ stdout, stderr, returncode = await self._execute_with_input(
411
+ ["python3", main_file], workspace, input_data
412
+ )
413
+
414
+ execution_time = asyncio.get_event_loop().time() - start_time
415
+
416
+ results.append(
417
+ ExecutionResult(
418
+ success=returncode == 0,
419
+ stdout=stdout.decode("utf-8", errors="replace"),
420
+ stderr=stderr.decode("utf-8", errors="replace"),
421
+ execution_time=execution_time,
422
+ exit_code=returncode,
423
+ )
424
+ )
425
+
426
+ except asyncio.TimeoutError:
427
+ results.append(
428
+ ExecutionResult(
429
+ success=False,
430
+ stdout="",
431
+ stderr="Execution timeout exceeded",
432
+ execution_time=MAX_EXECUTION_TIME,
433
+ exit_code=-1,
434
+ )
435
+ )
436
+ except Exception as e:
437
+ results.append(
438
+ ExecutionResult(
439
+ success=False,
440
+ stdout="",
441
+ stderr=str(e),
442
+ execution_time=0,
443
+ exit_code=-1,
444
+ error=str(e),
445
+ )
446
+ )
447
+
448
+ return self._combine_results(results, main_files)
449
+
450
+ async def _execute_java(
451
+ self,
452
+ main_files: List[str],
453
+ workspace: str,
454
+ input_data: Optional[List[str]] = None,
455
+ ) -> ExecutionResult:
456
+ """Compile and execute Java code with input support"""
457
+
458
+ # Check if we have .java files to compile
459
+ java_files = list(Path(workspace).glob("*.java"))
460
+ needs_compilation = len(java_files) > 0
461
+
462
+ # If we have .java files, compile them
463
+ if needs_compilation:
464
+ logger.info(f"Found {len(java_files)} Java source files, compiling...")
465
+
466
+ compile_process = await asyncio.create_subprocess_exec(
467
+ "javac",
468
+ *[str(f) for f in java_files],
469
+ cwd=workspace,
470
+ stdout=asyncio.subprocess.PIPE,
471
+ stderr=asyncio.subprocess.PIPE,
472
+ )
473
+
474
+ stdout, stderr = await compile_process.communicate()
475
+
476
+ if compile_process.returncode != 0:
477
+ return ExecutionResult(
478
+ success=False,
479
+ stdout="",
480
+ stderr=f"Compilation failed:\n{stderr.decode('utf-8', errors='replace')}",
481
+ execution_time=0,
482
+ exit_code=compile_process.returncode,
483
+ )
484
+
485
+ logger.info("Java compilation successful")
486
+ else:
487
+ # Check if we have .class files for the main files
488
+ class_files_missing = []
489
+ for main_file in main_files:
490
+ if main_file.endswith(".class"):
491
+ class_file_path = os.path.join(workspace, main_file)
492
+ else:
493
+ class_file_path = os.path.join(workspace, f"{main_file}.class")
494
+
495
+ if not os.path.exists(class_file_path):
496
+ class_files_missing.append(main_file)
497
+
498
+ if class_files_missing:
499
+ return ExecutionResult(
500
+ success=False,
501
+ stdout="",
502
+ stderr=f"No Java source files found and missing .class files for: {', '.join(class_files_missing)}",
503
+ execution_time=0,
504
+ exit_code=-1,
505
+ )
506
+
507
+ logger.info("Using existing .class files, skipping compilation")
508
+
509
+ # Execute main files
510
+ results = []
511
+ for main_file in main_files:
512
+ # Determine class name
513
+ if main_file.endswith(".class"):
514
+ class_name = main_file.replace(".class", "")
515
+ elif main_file.endswith(".java"):
516
+ class_name = main_file.replace(".java", "")
517
+ else:
518
+ class_name = main_file
519
+
520
+ # Verify the .class file exists
521
+ class_file_path = os.path.join(workspace, f"{class_name}.class")
522
+ if not os.path.exists(class_file_path):
523
+ results.append(
524
+ ExecutionResult(
525
+ success=False,
526
+ stdout="",
527
+ stderr=f"Class file not found: {class_name}.class",
528
+ execution_time=0,
529
+ exit_code=-1,
530
+ )
531
+ )
532
+ continue
533
+
534
+ try:
535
+ start_time = asyncio.get_event_loop().time()
536
+
537
+ stdout, stderr, returncode = await self._execute_with_input(
538
+ ["java", class_name], workspace, input_data
539
+ )
540
+
541
+ execution_time = asyncio.get_event_loop().time() - start_time
542
+
543
+ results.append(
544
+ ExecutionResult(
545
+ success=returncode == 0,
546
+ stdout=stdout.decode("utf-8", errors="replace"),
547
+ stderr=stderr.decode("utf-8", errors="replace"),
548
+ execution_time=execution_time,
549
+ exit_code=returncode,
550
+ )
551
+ )
552
+
553
+ except asyncio.TimeoutError:
554
+ results.append(
555
+ ExecutionResult(
556
+ success=False,
557
+ stdout="",
558
+ stderr="Execution timeout exceeded",
559
+ execution_time=MAX_EXECUTION_TIME,
560
+ exit_code=-1,
561
+ )
562
+ )
563
+ except Exception as e:
564
+ results.append(
565
+ ExecutionResult(
566
+ success=False,
567
+ stdout="",
568
+ stderr=str(e),
569
+ execution_time=0,
570
+ exit_code=-1,
571
+ error=str(e),
572
+ )
573
+ )
574
+
575
+ return self._combine_results(results, main_files)
576
+
577
+ async def _execute_c_cpp(
578
+ self,
579
+ main_files: List[str],
580
+ workspace: str,
581
+ language: str,
582
+ input_data: Optional[List[str]] = None,
583
+ ) -> ExecutionResult:
584
+ """Compile and execute C/C++ code with input support"""
585
+ compiler = self.compilers[language]
586
+ results = []
587
+
588
+ for main_file in main_files:
589
+ file_path = os.path.join(workspace, main_file)
590
+ if not os.path.exists(file_path):
591
+ results.append(
592
+ ExecutionResult(
593
+ success=False,
594
+ stdout="",
595
+ stderr=f"File not found: {main_file}",
596
+ execution_time=0,
597
+ exit_code=-1,
598
+ )
599
+ )
600
+ continue
601
+
602
+ # Output binary name
603
+ output_name = main_file.replace(".c", "").replace(".cpp", "")
604
+
605
+ # Compile
606
+ compile_process = await asyncio.create_subprocess_exec(
607
+ compiler,
608
+ main_file,
609
+ "-o",
610
+ output_name,
611
+ cwd=workspace,
612
+ stdout=asyncio.subprocess.PIPE,
613
+ stderr=asyncio.subprocess.PIPE,
614
+ )
615
+
616
+ stdout, stderr = await compile_process.communicate()
617
+
618
+ if compile_process.returncode != 0:
619
+ results.append(
620
+ ExecutionResult(
621
+ success=False,
622
+ stdout="",
623
+ stderr=f"Compilation failed:\n{stderr.decode('utf-8', errors='replace')}",
624
+ execution_time=0,
625
+ exit_code=compile_process.returncode,
626
+ )
627
+ )
628
+ continue
629
+
630
+ # Execute
631
+ try:
632
+ start_time = asyncio.get_event_loop().time()
633
+
634
+ stdout, stderr, returncode = await self._execute_with_input(
635
+ [f"./{output_name}"], workspace, input_data
636
+ )
637
+
638
+ execution_time = asyncio.get_event_loop().time() - start_time
639
+
640
+ results.append(
641
+ ExecutionResult(
642
+ success=returncode == 0,
643
+ stdout=stdout.decode("utf-8", errors="replace"),
644
+ stderr=stderr.decode("utf-8", errors="replace"),
645
+ execution_time=execution_time,
646
+ exit_code=returncode,
647
+ )
648
+ )
649
+
650
+ except asyncio.TimeoutError:
651
+ results.append(
652
+ ExecutionResult(
653
+ success=False,
654
+ stdout="",
655
+ stderr="Execution timeout exceeded",
656
+ execution_time=MAX_EXECUTION_TIME,
657
+ exit_code=-1,
658
+ )
659
+ )
660
+ except Exception as e:
661
+ results.append(
662
+ ExecutionResult(
663
+ success=False,
664
+ stdout="",
665
+ stderr=str(e),
666
+ execution_time=0,
667
+ exit_code=-1,
668
+ error=str(e),
669
+ )
670
+ )
671
+
672
+ return self._combine_results(results, main_files)
673
+
674
+ def _combine_results(
675
+ self, results: List[ExecutionResult], main_files: List[str]
676
+ ) -> ExecutionResult:
677
+ """Combine multiple execution results"""
678
+ if len(results) == 1:
679
+ return results[0]
680
+ else:
681
+ combined_stdout = "\n".join(
682
+ [f"=== {main_files[i]} ===\n{r.stdout}" for i, r in enumerate(results)]
683
+ )
684
+ combined_stderr = "\n".join(
685
+ [
686
+ f"=== {main_files[i]} ===\n{r.stderr}"
687
+ for i, r in enumerate(results)
688
+ if r.stderr
689
+ ]
690
+ )
691
+ total_time = sum(r.execution_time for r in results)
692
+ all_success = all(r.success for r in results)
693
+
694
+ return ExecutionResult(
695
+ success=all_success,
696
+ stdout=combined_stdout,
697
+ stderr=combined_stderr,
698
+ execution_time=total_time,
699
+ exit_code=0 if all_success else -1,
700
+ )
701
+
702
+
703
+ def get_directory_size(directory_path: str) -> int:
704
+ """Calculate total size of directory in bytes"""
705
+ total_size = 0
706
+ try:
707
+ for dirpath, dirnames, filenames in os.walk(directory_path):
708
+ for filename in filenames:
709
+ filepath = os.path.join(dirpath, filename)
710
+ if os.path.exists(filepath):
711
+ total_size += os.path.getsize(filepath)
712
+ except Exception as e:
713
+ print(f"Error calculating directory size: {e}")
714
+ return float("inf")
715
+ return total_size
716
+
717
+
718
+ def validate_upload_size(files: List[UploadFile]) -> tuple[bool, int]:
719
+ """Validate total size of uploaded files"""
720
+ total_size = 0
721
+ for file in files:
722
+ if hasattr(file, "size") and file.size:
723
+ total_size += file.size
724
+ else:
725
+ return True, 0
726
+ return total_size <= MAX_TOTAL_SIZE, total_size
727
+
728
+
729
+ # Create executor instance
730
+ executor = CodeExecutor()
731
+
732
+
733
+ async def clone_repo(repo_url: str, workspace: str):
734
+ """Clone a git repository into the workspace."""
735
+ try:
736
+ process = await asyncio.create_subprocess_exec(
737
+ "git",
738
+ "clone",
739
+ repo_url,
740
+ ".",
741
+ cwd=workspace,
742
+ stdout=asyncio.subprocess.PIPE,
743
+ stderr=asyncio.subprocess.PIPE,
744
+ )
745
+ stdout, stderr = await process.communicate()
746
+ if process.returncode != 0:
747
+ raise HTTPException(
748
+ status_code=400, detail=f"Failed to clone repository: {stderr.decode()}"
749
+ )
750
+
751
+ repo_size = get_directory_size(workspace)
752
+ if repo_size > MAX_TOTAL_SIZE:
753
+ raise HTTPException(
754
+ status_code=400,
755
+ detail=f"Repository size ({repo_size / 1024 / 1024:.2f}MB) exceeds limit ({MAX_TOTAL_SIZE / 1024 / 1024:.2f}MB)",
756
+ )
757
+
758
+ except HTTPException:
759
+ raise
760
+ except Exception as e:
761
+ raise HTTPException(
762
+ status_code=500, detail=f"Error cloning repository: {str(e)}"
763
+ )
764
+
765
+
766
+ def detect_language_from_files(main_files: List[str]) -> str:
767
+ """Detect programming language from file extensions"""
768
+ if not main_files:
769
+ raise HTTPException(status_code=400, detail="No main files provided")
770
+
771
+ first_file = main_files[0]
772
+ if "." not in first_file:
773
+ java_extensions = [".java", ".class"]
774
+ for file in main_files:
775
+ if any(file.endswith(ext) for ext in java_extensions):
776
+ return "java"
777
+
778
+ raise HTTPException(
779
+ status_code=400,
780
+ detail=f"Cannot detect language: file '{first_file}' has no extension",
781
+ )
782
+
783
+ extension = first_file.split(".")[-1].lower()
784
+
785
+ extension_to_language = {
786
+ "py": "python",
787
+ "java": "java",
788
+ "class": "java",
789
+ "c": "c",
790
+ "cpp": "cpp",
791
+ "cc": "cpp",
792
+ "cxx": "cpp",
793
+ "c++": "cpp",
794
+ }
795
+
796
+ if extension not in extension_to_language:
797
+ supported_extensions = ", ".join(extension_to_language.keys())
798
+ raise HTTPException(
799
+ status_code=400,
800
+ detail=f"Unsupported file extension '.{extension}'. Supported extensions: {supported_extensions}",
801
+ )
802
+
803
+ detected_language = extension_to_language[extension]
804
+
805
+ for file in main_files:
806
+ if "." in file:
807
+ file_ext = file.split(".")[-1].lower()
808
+ file_language = extension_to_language.get(file_ext)
809
+ if file_language != detected_language:
810
+ raise HTTPException(
811
+ status_code=400,
812
+ detail=f"Mixed languages detected: '{first_file}' ({detected_language}) and '{file}' ({file_language})",
813
+ )
814
+
815
+ return detected_language
816
+
817
+
818
+ @app.post("/analyze-inputs")
819
+ async def analyze_inputs(code_content: str = Form(...), language: str = Form(...)):
820
+ """
821
+ Analyze code content to detect input patterns with caching
822
+
823
+ Simple API that takes code content and language, returns input patterns
824
+
825
+ Args:
826
+ code_content: The source code content to analyze
827
+ language: Programming language (python, java, c, cpp)
828
+
829
+ Returns:
830
+ InputAnalysisResult with detected input patterns
831
+ """
832
+ try:
833
+ # Validate language
834
+ supported_languages = ["python", "java", "c", "cpp"]
835
+ if language.lower() not in supported_languages:
836
+ raise HTTPException(
837
+ status_code=400,
838
+ detail=f"Unsupported language: {language}. Supported: {supported_languages}",
839
+ )
840
+
841
+ # Create a simple file contents dict for analysis
842
+ file_contents_dict = {"main": code_content}
843
+
844
+ analysis_result = executor.analyze_input_patterns(
845
+ language.lower(), file_contents_dict
846
+ )
847
+ result_dict = analysis_result.model_dump()
848
+
849
+ # Cache the result
850
+
851
+ logger.info(f"Input analysis completed for {language} code")
852
+ return JSONResponse(content=result_dict)
853
+
854
+ except HTTPException:
855
+ raise
856
+ except Exception as e:
857
+ logger.error(f"Error analyzing inputs: {str(e)}")
858
+ raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
859
+
860
+
861
+ @app.post("/judge")
862
+ async def judge_code(
863
+ main_files: str = Form(...),
864
+ files: List[UploadFile] = File(None),
865
+ repo_url: Optional[str] = Form(None),
866
+ input_data: Optional[str] = Form(None), # JSON array of input strings
867
+ ):
868
+ """
869
+ Judge code submission with optional input data
870
+
871
+ - main_files: JSON array of main files to execute
872
+ - files: Multiple files maintaining folder structure
873
+ - repo_url: Git repository URL (alternative to files)
874
+ - input_data: JSON array of input strings for programs that require user input
875
+ """
876
+ # Parse main_files
877
+ try:
878
+ main_files_list = json.loads(main_files)
879
+ except json.JSONDecodeError:
880
+ raise HTTPException(status_code=400, detail="Invalid main_files format")
881
+
882
+ # Parse input_data if provided
883
+ input_list = None
884
+ logger.info(f"Received input_data: {input_data}")
885
+ if input_data:
886
+ try:
887
+ input_list = json.loads(input_data)
888
+ if not isinstance(input_list, list):
889
+ raise ValueError("Input data must be an array")
890
+ except (json.JSONDecodeError, ValueError):
891
+ raise HTTPException(
892
+ status_code=400, detail="Invalid input_data format - must be JSON array"
893
+ )
894
+
895
+ # Auto-detect language from file extensions
896
+ language = detect_language_from_files(main_files_list)
897
+
898
+ # Validate input: either files or repo_url must be provided
899
+ if not files and not repo_url:
900
+ raise HTTPException(
901
+ status_code=400, detail="Either files or repo_url must be provided"
902
+ )
903
+ if files and repo_url:
904
+ raise HTTPException(
905
+ status_code=400, detail="Provide either files or repo_url, not both"
906
+ )
907
+
908
+ # Create temporary workspace
909
+ workspace = None
910
+ try:
911
+ # Create unique temporary directory
912
+ workspace = tempfile.mkdtemp(prefix=f"judge_{uuid.uuid4().hex}_")
913
+
914
+ if repo_url:
915
+ # Clone repository
916
+ await clone_repo(repo_url, workspace)
917
+ else:
918
+ # Validate total upload size
919
+ total_upload_size = 0
920
+ file_contents = []
921
+
922
+ # Pre-read all files to check total size
923
+ for file in files:
924
+ content = await file.read()
925
+ if len(content) > MAX_FILE_SIZE:
926
+ raise HTTPException(
927
+ status_code=400,
928
+ detail=f"File {file.filename} exceeds individual size limit",
929
+ )
930
+
931
+ total_upload_size += len(content)
932
+ file_contents.append((file.filename, content))
933
+
934
+ # Check total size limit
935
+ if total_upload_size > MAX_TOTAL_SIZE:
936
+ raise HTTPException(
937
+ status_code=400,
938
+ detail=f"Total upload size ({total_upload_size / 1024 / 1024:.2f}MB) exceeds limit ({MAX_TOTAL_SIZE / 1024 / 1024:.2f}MB)",
939
+ )
940
+
941
+ # Save uploaded files maintaining structure
942
+ for filename, content in file_contents:
943
+ # Create file path
944
+ file_path = os.path.join(workspace, filename)
945
+
946
+ # Create directories if needed
947
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
948
+
949
+ # Write file
950
+ with open(file_path, "wb") as f:
951
+ f.write(content)
952
+
953
+ # Execute code with input data
954
+ result = await executor.execute_code(
955
+ language, main_files_list, workspace, input_list
956
+ )
957
+ logger.info(f"Execution result: {result}")
958
+ return JSONResponse(content=result.model_dump())
959
+
960
+ except Exception as e:
961
+ raise HTTPException(status_code=500, detail=str(e))
962
+
963
+ finally:
964
+ # Clean up temporary files
965
+ if workspace and os.path.exists(workspace):
966
+ try:
967
+ shutil.rmtree(workspace)
968
+ except Exception as e:
969
+ print(f"Error cleaning up workspace: {e}")
970
+
971
+
972
+ @app.get("/languages")
973
+ async def get_supported_languages():
974
+ """Get supported programming languages with auto-detection info"""
975
+ return {
976
+ "languages": [
977
+ {"id": "python", "name": "Python 3", "extensions": [".py"]},
978
+ {"id": "java", "name": "Java", "extensions": [".java", ".class"]},
979
+ {"id": "c", "name": "C", "extensions": [".c"]},
980
+ {"id": "cpp", "name": "C++", "extensions": [".cpp", ".cc", ".cxx", ".c++"]},
981
+ ],
982
+ "note": "Language is automatically detected from file extensions. For Java, both source (.java) and compiled (.class) files are supported.",
983
+ "input_support": "All languages support automatic input detection and handling for interactive programs.",
984
+ }
985
+
986
+
987
+ # Example usage endpoints for testing
988
+ @app.get("/examples/input-patterns")
989
+ async def get_input_pattern_examples():
990
+ """Get examples of supported input patterns for each language"""
991
+ return {
992
+ "python": [
993
+ 'name = input("Enter your name: ")',
994
+ 'age = int(input("Enter your age: "))',
995
+ 'score = float(input("Enter score: "))',
996
+ "input() # Simple input without assignment",
997
+ ],
998
+ "java": [
999
+ "String name = scanner.nextLine();",
1000
+ "int age = scanner.nextInt();",
1001
+ "double score = scanner.nextDouble();",
1002
+ "scanner.next(); # Direct call",
1003
+ ],
1004
+ "c": ['scanf("%s", name);', 'scanf("%d", &age);', 'scanf("%f", &score);'],
1005
+ "cpp": ["cin >> name;", "cin >> age;", "getline(cin, fullName);"],
1006
+ }
1007
+
1008
+
1009
+ @app.post("/test-input-analysis")
1010
+ async def test_input_analysis():
1011
+ """Test endpoint with sample code for input analysis"""
1012
+
1013
+ # Sample Python code with inputs
1014
+ sample_code = {
1015
+ "main.py": """
1016
+ name = input("Enter your name: ")
1017
+ age = int(input("Enter your age: "))
1018
+ score = float(input("Enter your score: "))
1019
+
1020
+ print(f"Hello {name}")
1021
+ print(f"You are {age} years old")
1022
+ print(f"Your score is {score}")
1023
+
1024
+ # Simple input without assignment
1025
+ input("Press enter to continue...")
1026
+ """
1027
+ }
1028
+
1029
+ # Test the analysis
1030
+ analysis_result = executor.analyze_input_patterns("python", sample_code)
1031
+
1032
+ return {
1033
+ "sample_code": sample_code,
1034
+ "analysis": analysis_result.model_dump(),
1035
+ "suggested_inputs": [
1036
+ "John Doe", # for name
1037
+ "25", # for age
1038
+ "95.5", # for score
1039
+ "", # for press enter
1040
+ ],
1041
+ }
1042
+
1043
+
1044
+ if __name__ == "__main__":
1045
+ import uvicorn
1046
+
1047
+ uvicorn.run("app_3:app", host="0.0.0.0", port=8000, reload=True)