-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #32 from edu-pi/feature/issue29
[#29] subprocess로 코드 실행
- Loading branch information
Showing
8 changed files
with
64 additions
and
144 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,92 +1,42 @@ | ||
import os | ||
import re | ||
import subprocess | ||
import tempfile | ||
import textwrap | ||
|
||
from RestrictedPython import compile_restricted, PrintCollector | ||
|
||
from app.config.restricted_python_config import RestrictedPythonConfig | ||
from app.route.execute.exception.code_execute_error import CodeExecuteError | ||
from app.route.execute.exception.code_syntax_error import CodeSyntaxError | ||
from app.route.execute.exception.input_size_matching_error import InputSizeMatchingError | ||
from app.web.exception.enum.error_enum import ErrorEnum | ||
from app.web.exception.task_fail_exception import TaskFailException | ||
|
||
FORBIDDEN_IMPORTS = ["os", "sys", "subprocess", "shutil"] | ||
|
||
|
||
def execute_code(source_code: str, user_input: str): | ||
code = textwrap.dedent(source_code) | ||
restricted_config = RestrictedPythonConfig(user_input) | ||
if _contains_forbidden_imports(source_code): | ||
# 보안상 실행 안함 | ||
raise CodeExecuteError(ErrorEnum.CODE_EXEC_SECURITY_ERROR) | ||
|
||
try: | ||
byte_code = compile_restricted(code, filename="<string>", mode="exec") | ||
restricted_locals = restricted_config.get_limited_locals() | ||
restricted_globals = restricted_config.get_limited_globals() | ||
|
||
exec(byte_code, restricted_globals, restricted_locals) | ||
|
||
return _get_print_result(restricted_locals) | ||
|
||
except InputSizeMatchingError as e: | ||
raise CodeExecuteError(e.error_enum) | ||
|
||
except SyntaxError as e: | ||
error = _get_error_message(source_code) | ||
raise CodeSyntaxError(ErrorEnum.CODE_SYNTAX_ERROR, {"error": error} if e.args else {}) | ||
process = subprocess.run( | ||
args=["python3", "-c", source_code], | ||
input=user_input, | ||
capture_output=True, # stdout, stderr 별도의 Pipe에서 처리 | ||
timeout=3, # limit child process execute time | ||
check=True, # CalledProcessError exception if return_code is 0 | ||
text=True | ||
) | ||
return process.stdout | ||
|
||
# 프로세스 실행 중 비정상 종료 | ||
except subprocess.CalledProcessError as e: | ||
raise CodeSyntaxError(ErrorEnum.CODE_SYNTAX_ERROR, {"error": e.stderr}) | ||
|
||
except Exception as e: | ||
error = _get_error_message(source_code) | ||
raise CodeExecuteError(ErrorEnum.CODE_EXEC_ERROR, {"error": error} if e.args else {}) | ||
|
||
|
||
def _get_print_result(restricted_locals): | ||
"""_print 변수가 존재할 경우, 그 결과 반환.""" | ||
if "_print" in restricted_locals: | ||
if isinstance(restricted_locals["_print"], PrintCollector): | ||
return "".join(restricted_locals["_print"].txt) # 리스트 요소를 합쳐 하나의 문자열로 | ||
|
||
return "" | ||
|
||
|
||
def _get_error_message(code): | ||
""" 잘못된 코드의 에러 메시지를 추출 ex)'1:26: E999 SyntaxError: '(' was never closed' """ | ||
code = textwrap.dedent(code) | ||
temp_file_path = _create_temp_file_with_code(code) | ||
|
||
result = _run_flake8(temp_file_path) | ||
os.remove(temp_file_path) | ||
|
||
if result.returncode != 0: | ||
return _extract_error_message(result.stdout) | ||
|
||
return None | ||
|
||
|
||
def _create_temp_file_with_code(code) -> str: | ||
""" 임시 파일에 코드 저장후, 파일 경로 반환 """ | ||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file: | ||
temp_file.write(code) | ||
return temp_file.name | ||
|
||
|
||
def _run_flake8(temp_file_path: str) -> subprocess.CompletedProcess: | ||
""" Flake8을 실행하여 코드 검사 """ | ||
result = subprocess.run( | ||
['flake8', temp_file_path], | ||
stdout=subprocess.PIPE, | ||
stderr=subprocess.PIPE, | ||
text=True | ||
) | ||
return result | ||
|
||
raise TaskFailException(ErrorEnum.CODE_EXEC_SERVER_ERROR, dict(e.args)) | ||
|
||
def _extract_error_message(error_string) -> str: | ||
# [행:열: error message] 형태로 추출 | ||
pattern = r"(\d+:\d+: [^\n]+)" | ||
|
||
# 정규 표현식을 사용하여 매칭된 부분 추출 | ||
match = re.search(pattern, error_string) | ||
def _contains_forbidden_imports(code: str) -> bool: | ||
# 금지된 모듈이 코드에 있는지 확인 | ||
for module in FORBIDDEN_IMPORTS: | ||
if re.search(rf'\bimport\s+{module}\b', code): | ||
return True | ||
return False | ||
|
||
if match: | ||
return match.group(1) # 매칭된 부분을 반환 | ||
else: | ||
return "No match found." |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from starlette import status | ||
|
||
from app.web.exception.base_exception import BaseCustomException | ||
from app.web.exception.enum.error_enum import ErrorEnum | ||
|
||
|
||
class TaskFailException(BaseCustomException): | ||
def __init__(self, error_enum: ErrorEnum, result: dict = None): | ||
super().__init__( | ||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | ||
error_enum=error_enum, | ||
result={} if result is None else result | ||
) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
def test_placeholder(): | ||
assert True |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from app.route.execute.service import execute_service | ||
|
||
|
||
def test__contains_forbidden_imports(): | ||
code = "import os\n" | ||
|
||
assert execute_service._contains_forbidden_imports(code) is True |