Skip to content

Commit 0217a02

Browse files
authored
Update app.py
1 parent c3929f2 commit 0217a02

File tree

1 file changed

+174
-156
lines changed

1 file changed

+174
-156
lines changed

server/app.py

Lines changed: 174 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,99 @@
11
import os
22
import subprocess
3-
import httpx
43
import sys
4+
import httpx
55
from flask import Flask, request, jsonify
66
from openai import OpenAI
77
from dotenv import load_dotenv
8+
from pathlib import Path
9+
import shlex
810

9-
# .env 파일 로드
11+
# .env 로드
1012
load_dotenv()
1113

12-
# 환경변수 확인
14+
# ==== OpenAI 클라이언트 (키 없으면 비활성) ====
1315
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
14-
print(f"✅ Loaded OPENAI_API_KEY: {OPENAI_API_KEY[:5]}...") # Key 일부만 출력
16+
if OPENAI_API_KEY:
17+
print("🔐 OPENAI_API_KEY loaded")
18+
else:
19+
print("⚠️ OPENAI_API_KEY not set; OpenAI features disabled")
1520

16-
# httpx Client 설정
1721
http_client = httpx.Client(timeout=30.0)
18-
19-
# OpenAI Client 생성
20-
client = OpenAI(
21-
api_key=OPENAI_API_KEY,
22-
http_client=http_client
23-
)
22+
client = OpenAI(api_key=OPENAI_API_KEY, http_client=http_client) if OPENAI_API_KEY else None
2423

2524
app = Flask(__name__)
2625

27-
@app.route('/')
28-
def index():
29-
return 'DebugVisual Python Compiler Server is running!'
26+
# 실행 작업 디렉터리
27+
TMP_DIR = Path("/usr/src/app/code")
28+
TMP_DIR.mkdir(parents=True, exist_ok=True)
3029

31-
@app.route('/test', methods=['POST'])
32-
def test():
33-
print("✅ /test 진입됨")
34-
return "pong"
30+
MAX_TIME = int(os.getenv("EXEC_TIMEOUT_SEC", "10")) # 실행 타임아웃
31+
PY_BIN = ["python", "-X", "utf8"] # 파이썬 실행 커맨드
3532

33+
# ========== 공통 유틸 ==========
34+
def run_proc(cmd, *, cwd=None, stdin_data="", timeout=MAX_TIME):
35+
"""명령 실행 유틸. stdout/stderr/returncode 반환."""
36+
try:
37+
print(f"▶️ run: {shlex.join(cmd)} (cwd={cwd})")
38+
p = subprocess.run(
39+
cmd,
40+
input=(stdin_data or "").encode("utf-8"),
41+
cwd=cwd,
42+
stdout=subprocess.PIPE,
43+
stderr=subprocess.PIPE,
44+
timeout=timeout,
45+
)
46+
return p.stdout.decode("utf-8", "ignore"), p.stderr.decode("utf-8", "ignore"), p.returncode, None
47+
except subprocess.TimeoutExpired:
48+
return "", "", -1, "⏰ 실행 시간이 초과되었습니다."
49+
50+
def safe_print(msg):
51+
try:
52+
sys.stdout.buffer.write((str(msg) + "\n").encode("utf-8"))
53+
sys.stdout.flush()
54+
except Exception:
55+
fallback_msg = str(msg).encode("utf-8", "backslashreplace").decode("ascii", "ignore")
56+
print(f"[safe_print fallback] {fallback_msg}", flush=True)
57+
58+
def normalize_lang(lang: str) -> str:
59+
if not lang:
60+
return "python"
61+
l = lang.lower()
62+
if l in {"py", "python3"}:
63+
return "python"
64+
if l.startswith("java"):
65+
return "java"
66+
if l in {"c"}:
67+
return "c"
68+
return l
69+
70+
# ========== 웹 훅/헬스/CORS ==========
3671
@app.before_request
3772
def log_request_info():
3873
print(f"📡 요청 URL: {request.url}")
3974
print(f"📡 요청 메서드: {request.method}")
4075

4176
@app.after_request
42-
def cors_headers(response):
43-
response.headers['Access-Control-Allow-Origin'] = '*'
44-
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
45-
response.headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
46-
return response
77+
def cors_headers(resp):
78+
resp.headers["Access-Control-Allow-Origin"] = "*"
79+
resp.headers["Access-Control-Allow-Headers"] = "Content-Type"
80+
resp.headers["Access-Control-Allow-Methods"] = "POST, OPTIONS"
81+
return resp
82+
83+
@app.get("/")
84+
def index():
85+
return "DebugVisual Python Compiler Server is running!"
86+
87+
@app.get("/healthz")
88+
def healthz():
89+
return "OK", 200
4790

48-
@app.route('/echo', methods=['POST'])
91+
@app.post("/test")
92+
def test():
93+
print("✅ /test 진입됨")
94+
return "pong"
95+
96+
@app.post("/echo")
4997
def echo():
5098
try:
5199
data = request.get_json(force=True)
@@ -55,111 +103,84 @@ def echo():
55103
print("❌ JSON 파싱 실패:", e)
56104
return "Invalid JSON", 400
57105

58-
# 공통 코드 실행 함수
59-
def execute_code(code, input_data, lang):
60-
base_dir = '/home/ec2-user/DebugVisual_Spike/server/code'
61-
flask_dir = '/usr/src/app/code'
62-
os.makedirs(base_dir, exist_ok=True)
63-
64-
file_map = {
65-
'c': ('main.c', 'c-compiler'),
66-
'python': ('main.py', 'python-compiler'),
67-
'java': ('Main.java', 'java-compiler'),
68-
}
69-
70-
if lang not in file_map:
71-
return None, f"❌ 지원하지 않는 언어입니다: {lang}"
72-
73-
filename, image = file_map[lang]
74-
code_path = os.path.join(flask_dir, filename)
75-
input_path = os.path.join(flask_dir, 'input.txt')
76-
77-
with open(code_path, 'w') as f:
78-
f.write(code)
79-
80-
with open(input_path, 'w') as f:
81-
f.write(input_data)
82-
83-
if lang == 'python':
84-
docker_cmd = [
85-
'docker', 'run', '--rm',
86-
'-v', f'{base_dir}:/code',
87-
'-w', '/code', image,
88-
'python', filename
89-
]
90-
elif lang == 'java':
91-
docker_cmd = [
92-
'docker', 'run', '--rm',
93-
'-v', f'{base_dir}:/code',
94-
'-w', '/code', image,
95-
'sh', '-c', 'javac Main.java && java Main'
96-
]
97-
elif lang == 'c':
98-
docker_cmd = [
99-
'docker', 'run', '--rm',
100-
'-v', f'{base_dir}:/code',
101-
'-w', '/code', image,
102-
'sh', '-c', 'gcc main.c -o program && ./program'
103-
]
104-
105-
print("🐳 Docker 실행 명령어:", ' '.join(docker_cmd))
106-
107-
try:
108-
result = subprocess.run(
109-
docker_cmd,
110-
stdout=subprocess.PIPE,
111-
stderr=subprocess.PIPE,
112-
text=True,
113-
timeout=10
114-
)
115-
except subprocess.TimeoutExpired:
116-
return {
117-
"stdout": "",
118-
"stderr": "",
119-
"exitCode": -1,
120-
"success": False,
121-
"error": "⏰ 실행 시간이 초과되었습니다."
122-
}, None
123-
124-
return result, None
125-
126-
@app.route('/run', methods=['POST'])
106+
# ========== 언어별 실행 ==========
107+
def exec_python(code: str, stdin_data: str):
108+
src = TMP_DIR / "main.py"
109+
src.write_text(code or "", encoding="utf-8")
110+
return run_proc([*PY_BIN, str(src)], cwd=TMP_DIR, stdin_data=stdin_data)
111+
112+
def exec_c(code: str, stdin_data: str):
113+
src = TMP_DIR / "main.c"
114+
bin_path = TMP_DIR / "program"
115+
src.write_text(code or "", encoding="utf-8")
116+
117+
# 컴파일
118+
out, err, rc, to = run_proc(["gcc", "-O2", "-std=c11", str(src), "-o", str(bin_path)], cwd=TMP_DIR)
119+
if to: # 타임아웃
120+
return "", to, -1, to
121+
if rc != 0:
122+
return out, err, rc, None
123+
124+
# 실행
125+
return run_proc([str(bin_path)], cwd=TMP_DIR, stdin_data=stdin_data)
126+
127+
def exec_java(code: str, stdin_data: str):
128+
# 클래스명은 Main으로 고정
129+
src = TMP_DIR / "Main.java"
130+
src.write_text(code or "", encoding="utf-8")
131+
132+
# 컴파일
133+
out, err, rc, to = run_proc(["javac", str(src)], cwd=TMP_DIR)
134+
if to:
135+
return "", to, -1, to
136+
if rc != 0:
137+
return out, err, rc, None
138+
139+
# 실행 (CLASSPATH = TMP_DIR)
140+
return run_proc(["java", "-cp", str(TMP_DIR), "Main"], cwd=TMP_DIR, stdin_data=stdin_data)
141+
142+
def execute_code(code: str, input_data: str, lang: str):
143+
lang = normalize_lang(lang)
144+
if lang == "python":
145+
return exec_python(code, input_data)
146+
if lang == "c":
147+
return exec_c(code, input_data)
148+
if lang == "java":
149+
return exec_java(code, input_data)
150+
return "", f"❌ 지원하지 않는 언어입니다: {lang}", 1, None
151+
152+
# ========== /run ==========
153+
@app.route("/run", methods=["POST", "OPTIONS"])
127154
def run_code():
128-
print("📥 /run 요청 수신됨")
155+
if request.method == "OPTIONS":
156+
return jsonify({"ok": True}), 200
129157

158+
print("📥 /run 요청 수신됨")
130159
try:
131160
data = request.get_json(force=True)
132161
print("✅ JSON 파싱 성공:", data)
133162
except Exception as e:
134163
print(f"❌ JSON 파싱 실패: {e}")
135164
return jsonify({"error": "JSON 파싱 오류", "message": str(e)}), 400
136165

137-
code = data.get('code', '')
138-
input_data = data.get('input', '')
139-
lang = data.get('lang', '')
140-
141-
result, error = execute_code(code, input_data, lang)
166+
# lang / language 둘 다 지원, 기본값 python
167+
lang = data.get("lang") or data.get("language") or "python"
168+
code = data.get("code", "")
169+
stdin_data = data.get("input", "") or data.get("stdin", "")
142170

143-
if error:
144-
return jsonify({"error": error}), 400
171+
out, err, rc, timeout_msg = execute_code(code, stdin_data, lang)
172+
if timeout_msg:
173+
return jsonify({"stdout": out, "stderr": timeout_msg, "exitCode": -1, "success": False}), 400
145174

146175
return jsonify({
147-
"stdout": result.stdout,
148-
"stderr": result.stderr,
149-
"exitCode": result.returncode,
150-
"success": result.returncode == 0
151-
}), 200 if result.returncode == 0 else 400
152-
153-
154-
def safe_print(msg):
155-
try:
156-
sys.stdout.buffer.write((str(msg) + '\n').encode('utf-8'))
157-
sys.stdout.flush()
158-
except Exception as e:
159-
fallback_msg = str(msg).encode('utf-8', 'backslashreplace').decode('ascii', 'ignore')
160-
print(f"[safe_print fallback] {fallback_msg}", flush=True)
161-
162-
@app.route('/visualize', methods=['POST'])
176+
"stdout": out,
177+
"stderr": err,
178+
"exitCode": rc,
179+
"success": rc == 0
180+
}), 200 if rc == 0 else 400
181+
182+
# ========== /visualize ==========
183+
@app.post("/visualize")
163184
def visualize_code():
164185
print("📥 /visualize 요청 수신됨", flush=True)
165186

@@ -170,46 +191,43 @@ def visualize_code():
170191
print(f"❌ JSON 파싱 실패: {e}", flush=True)
171192
return jsonify({"error": "JSON 파싱 오류", "message": str(e)}), 400
172193

173-
code = data.get('code', '')
174-
input_data = data.get('input', '')
175-
lang = data.get('lang', '')
176-
177-
result, error = execute_code(code, input_data, lang)
194+
lang = data.get("lang") or data.get("language") or "python"
195+
code = data.get("code", "")
196+
stdin_data = data.get("input", "") or data.get("stdin", "")
197+
198+
out, err, rc, timeout_msg = execute_code(code, stdin_data, lang)
199+
if timeout_msg:
200+
return jsonify({"error": timeout_msg}), 400
201+
# GPT 호출
202+
gpt_response = None
203+
if client is None:
204+
print("⚠️ OpenAI 비활성: OPENAI_API_KEY 미설정", flush=True)
205+
gpt_response = "OpenAI disabled"
206+
else:
207+
gpt_prompt = f"{code}"
208+
try:
209+
completion = client.chat.completions.create(
210+
model="gpt-4o",
211+
messages=[
212+
{"role": "system",
213+
"content": "사용자 코드에 대한 단계별 시각화 JSON만 출력하세요."},
214+
{"role": "user", "content": gpt_prompt},
215+
],
216+
)
217+
gpt_response = completion.choices[0].message.content
218+
safe_print(f"✅ 최종 프론트 전달용 GPT 응답: {repr(gpt_response)}")
219+
except Exception as e:
220+
print(f"❌ GPT 응답 호출 실패: {e}", flush=True)
221+
gpt_response = "GPT 응답 호출 실패"
178222

179-
if error:
180-
return jsonify({"error": error}), 400
181-
182-
# GPT 호출용 프롬프트 구성
183-
gpt_prompt = f"{code}"
184-
185-
try:
186-
completion = client.chat.completions.create(
187-
model="gpt-4o",
188-
messages=[
189-
{
190-
"role": "system",
191-
"content": "당신은 **코드 실행 시각화 JSON 생성기**입니다. 사용자가 언어(lang), 코드(code), 입력값(input)을 주면 아래 스키마에 맞춰서 **모든 연산, 비교, 대입, 스왑, 함수 호출, 반복문, 조건문, 재귀 호출, 자료구조 변화 등을 단계별로** 시각화 가능한 JSON을 **정확하고 완전하게** 생성하세요.\n\n**출력 JSON 스키마**:\n{\n \"lang\": \"언어\",\n \"TimeComplexity\": \"시간복잡도(Big O)\",\n \"SpaceComplexity\": \"공간복잡도(Big O)\",\n \"input\": \"입력값(없으면 빈 문자열)\",\n \"variables\": [\n { \"name\": \"변수명\", \"type\": \"자료형|array|graph|heap|linkedList|bst\", \"initialValue\": 값, \"currentValue\": 값 },\n\n ],\n \"functions\": [\n { \"name\": \"함수명\", \"params\": [\"param1\", …], \"called\": 호출횟수 },\n\n ],\n \"steps\": [\n {\n \"line\": 소스코드_행번호,\n \"description\": \"이 단계에서 일어난 일\",\n \"changes\": [ { \"variable\": \"변수명\", \"before\": 이전값, \"after\": 이후값 }, … ],\n \"stack\": [ { \"function\": \"함수명\", \"params\": [값들] }, … ], (선택)\n \"loop\": { \"type\": \"for|while|do-while\", \"index\": 현재반복인덱스, \"total\": 총반복횟수 }, (선택)\n \"condition\": { \"expression\": \"조건식\", \"result\": true|false }, (선택)\n \"dataStructure\": { (선택)\n \"type\": \"array|linkedList|bst|heap|graph\",\n \"nodes\": [ \"0\", \"1\", … ] | [ { \"id\": \"0\", \"value\": 값, \"links\": [\"1\", …] }, … ],\n \"edges\": [[\"0\",\"1\"],[\"1\",\"2\"],…], // graph 전용\n \"adjacencyMatrix\": [[0,1],[1,0],…] // graph 전용\n }\n },\n\n ]\n}\n\n✅ 단계별로 dataStructure 객체를 **반드시** 포함하여 자료구조 상태를 **정확히** 기록하세요.\n✅ graph의 경우 반드시 nodes, edges, adjacencyMatrix를 **모두 포함**해야 합니다.\n✅ 생략이나 … 없이 **모든 단계의 흐름과 변수/자료구조 변화**를 상세히 출력하세요.\n✅ HTML 시각화 코드와 호환되도록 구조를 유지하며, 다른 설명이나 텍스트는 **절대로 추가하지 마세요**.\n✅ **올바른 JSON만** 출력하세요."
192-
},
193-
{"role": "user", "content": gpt_prompt}
194-
]
195-
)
196-
gpt_response = completion.choices[0].message.content
197-
safe_print(f"✅ 최종 프론트 전달용 GPT 응답: {repr(gpt_response)}")
198-
199-
except Exception as e:
200-
print(f"❌ GPT 응답 호출 실패: {e}", flush=True)
201-
gpt_response = "GPT 응답 호출 실패"
202-
203-
# 프론트로 응답 전송
204223
return jsonify({
205-
"stdout": result.stdout,
206-
"stderr": result.stderr,
224+
"stdout": out,
225+
"stderr": err,
207226
"ast": gpt_response,
208-
"exitCode": result.returncode,
209-
"success": result.returncode == 0
210-
}), 200 if result.returncode == 0 else 400
211-
212-
227+
"exitCode": rc,
228+
"success": rc == 0
229+
}), 200 if rc == 0 else 400
213230

214-
if __name__ == '__main__':
215-
app.run(host="0.0.0.0", port=5050)
231+
if __name__ == "__main__":
232+
port = int(os.getenv("PORT", "5050"))
233+
app.run(host="0.0.0.0", port=port)

0 commit comments

Comments
 (0)