From 476481089a4879c024a5a98988fb6dc9b7dfc6f3 Mon Sep 17 00:00:00 2001 From: w8385 Date: Tue, 27 Jan 2026 12:55:39 +0900 Subject: [PATCH 01/22] feat: /judge/init --- Cargo.toml | 1 + Dockerfile | 22 ++++++ docker-compose.yml | 10 +++ src/docker/cp.rs | 40 +++++++++++ src/docker/exec.rs | 38 ++++++++++ src/docker/mod.rs | 2 + src/errors/docker.rs | 45 ++++++++++++ src/errors/isolate.rs | 62 ++++++++++++++++ src/errors/judge.rs | 53 ++++++++++++++ src/errors/mod.rs | 6 ++ src/grader/docker_isolate.rs | 52 ++++++++++++++ src/grader/mod.rs | 1 + src/isolate/box_id.rs | 8 +++ src/isolate/cleanup.rs | 12 ++++ src/isolate/compile.rs | 53 ++++++++++++++ src/isolate/copy.rs | 27 +++++++ src/isolate/execute.rs | 26 +++++++ src/isolate/init.rs | 12 ++++ src/isolate/mod.rs | 65 +++++++++++++++++ src/isolate/path.rs | 6 ++ src/judge/config.rs | 27 +++++++ src/judge/grader.rs | 12 ++++ src/judge/mod.rs | 7 ++ src/judge/result.rs | 15 ++++ src/judge_manager/handlers.rs | 131 ++++++++++++++++++++++++++++++++++ src/judge_manager/mod.rs | 3 + src/lib.rs | 15 ++++ 27 files changed, 751 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 src/docker/cp.rs create mode 100644 src/docker/exec.rs create mode 100644 src/docker/mod.rs create mode 100644 src/errors/docker.rs create mode 100644 src/errors/isolate.rs create mode 100644 src/errors/judge.rs create mode 100644 src/grader/docker_isolate.rs create mode 100644 src/grader/mod.rs create mode 100644 src/isolate/box_id.rs create mode 100644 src/isolate/cleanup.rs create mode 100644 src/isolate/compile.rs create mode 100644 src/isolate/copy.rs create mode 100644 src/isolate/execute.rs create mode 100644 src/isolate/init.rs create mode 100644 src/isolate/mod.rs create mode 100644 src/isolate/path.rs create mode 100644 src/judge/config.rs create mode 100644 src/judge/grader.rs create mode 100644 src/judge/mod.rs create mode 100644 src/judge/result.rs create mode 100644 src/judge_manager/handlers.rs create mode 100644 src/judge_manager/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 46e41a9..7f12758 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.133" tokio = { version = "1.45.1", features = ["full"] } uuid = { version = "1.17.0", features = ["v4"] } +async-trait = "0.1.89" [dev-dependencies] rstest = "0.25.0" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..971e2f8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM ubuntu:22.04 + +RUN apt-get update +RUN apt-get install -y --no-install-recommends \ + gcc g++ \ + python3 pypy3 \ + openjdk-8-jdk-headless \ + git make pkg-config libcap-dev libsystemd-dev asciidoc \ + curl jq + +# config isolate +RUN git clone https://github.com/ioi/isolate.git +RUN cd isolate && make install +RUN rm -rf isolate + +# install testlib +RUN curl -L https://raw.githubusercontent.com/MikeMirzayanov/testlib/master/testlib.h \ + -o /usr/include/testlib.h + +# create judge user +RUN useradd -m judge +WORKDIR /home/judge diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..043bfb4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + grader: + build: + context: . + privileged: true + container_name: coduck-grader + entrypoint: [ "sleep", "infinity" ] + restart: unless-stopped + volumes: + - ./uploads:/home/judge/uploads diff --git a/src/docker/cp.rs b/src/docker/cp.rs new file mode 100644 index 0000000..eb19915 --- /dev/null +++ b/src/docker/cp.rs @@ -0,0 +1,40 @@ +use std::path::Path; +use tokio::process::Command; + +use crate::errors::DockerError; + +pub async fn to_container( + container: &str, + local_src: &Path, + container_dst: &Path, +) -> Result<(), DockerError> { + tokio::fs::metadata(local_src) + .await + .map_err(|_| DockerError::FileNotFound(local_src.display().to_string()))?; + + Command::new("docker") + .arg("cp") + .arg(local_src) + .arg(format!("{}:{}", container, container_dst.display())) + .output() + .await + .map_err(|_| DockerError::Spawn)?; + + Ok(()) +} + +pub async fn from_container( + container: &str, + container_src: &Path, + local_dst: &Path, +) -> Result<(), DockerError> { + Command::new("docker") + .arg("cp") + .arg(format!("{}:{}", container, container_src.display())) + .arg(local_dst) + .output() + .await + .map_err(|_| DockerError::Spawn)?; + + Ok(()) +} diff --git a/src/docker/exec.rs b/src/docker/exec.rs new file mode 100644 index 0000000..fd99654 --- /dev/null +++ b/src/docker/exec.rs @@ -0,0 +1,38 @@ +use crate::errors::DockerError; +use std::process::Stdio; +use tokio::process::Command; + +pub struct CommandOutput { + pub status: std::process::ExitStatus, + pub stdout: String, + pub stderr: String, +} + +impl CommandOutput { + pub fn ensure_success(self, map_err: impl FnOnce(&CommandOutput) -> E) -> Result { + if self.status.success() { + Ok(self) + } else { + Err(map_err(&self)) + } + } +} + +pub async fn exec(container: &str, args: &[&str]) -> Result { + let output = Command::new("docker") + .arg("exec") + .args(["-u", "judge"]) + .arg(container) + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .unwrap(); + + Ok(CommandOutput { + status: output.status, + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + }) +} diff --git a/src/docker/mod.rs b/src/docker/mod.rs new file mode 100644 index 0000000..3bc01b0 --- /dev/null +++ b/src/docker/mod.rs @@ -0,0 +1,2 @@ +pub mod cp; +pub mod exec; diff --git a/src/errors/docker.rs b/src/errors/docker.rs new file mode 100644 index 0000000..c6cb8a5 --- /dev/null +++ b/src/errors/docker.rs @@ -0,0 +1,45 @@ +use axum::response::{IntoResponse, Response}; +use axum::Json; +use reqwest::StatusCode; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum DockerError { + UnsupportedExtension(String), + InvalidFilename, + Spawn, + FileNotFound(String), +} + +impl Display for DockerError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match self { + DockerError::UnsupportedExtension(extension) => { + write!(f, "Unsupported file extension: {extension}") + } + DockerError::InvalidFilename => write!(f, "Invalid filename"), + DockerError::Spawn => write!(f, "Failed to spawn docker process"), + DockerError::FileNotFound(filename) => write!(f, "File not found: {filename}"), + } + } +} + +impl IntoResponse for DockerError { + fn into_response(self) -> Response { + let status = match self { + DockerError::FileNotFound(_) => StatusCode::NOT_FOUND, + DockerError::UnsupportedExtension(_) => StatusCode::BAD_REQUEST, + DockerError::InvalidFilename => StatusCode::BAD_REQUEST, + DockerError::Spawn => StatusCode::INTERNAL_SERVER_ERROR, + }; + + ( + status, + Json(serde_json::json!({ + "error": self.to_string() + })), + ) + .into_response() + } +} diff --git a/src/errors/isolate.rs b/src/errors/isolate.rs new file mode 100644 index 0000000..a1e1725 --- /dev/null +++ b/src/errors/isolate.rs @@ -0,0 +1,62 @@ +use crate::errors::DockerError; +use axum::response::IntoResponse; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum IsolateError { + Docker(DockerError), + UnsupportedExtension(String), + InvalidFilename, + InvalidBoxId(i32), + InitFailed(String), + CompileFailed(String), + CleanupFailed(String), + ExecuteFailed(String), +} + +impl std::fmt::Display for IsolateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IsolateError::Docker(err) => write!(f, "Docker error: {}", err), + IsolateError::UnsupportedExtension(extension) => { + write!(f, "Unsupported file extension: {extension}") + } + IsolateError::InvalidFilename => write!(f, "Invalid filename"), + IsolateError::InitFailed(msg) => write!(f, "Isolate initialization failed: {msg}"), + IsolateError::InvalidBoxId(id) => write!(f, "Invalid box ID returned by isolate: {id}"), + IsolateError::CompileFailed(msg) => write!(f, "Isolate compile error: {msg}"), + IsolateError::ExecuteFailed(msg) => write!(f, "Isolate execution failed: {msg}"), + IsolateError::CleanupFailed(msg) => write!(f, "Isolate cleanup failed: {msg}"), + } + } +} + +impl From for IsolateError { + fn from(e: DockerError) -> Self { + IsolateError::Docker(e) + } +} + +impl IntoResponse for IsolateError { + fn into_response(self) -> axum::response::Response { + let status = match self { + IsolateError::Docker(ref e) => match e { + DockerError::FileNotFound(_) => reqwest::StatusCode::NOT_FOUND, + DockerError::UnsupportedExtension(_) => reqwest::StatusCode::BAD_REQUEST, + DockerError::InvalidFilename => reqwest::StatusCode::BAD_REQUEST, + DockerError::Spawn => reqwest::StatusCode::INTERNAL_SERVER_ERROR, + }, + IsolateError::UnsupportedExtension(_) => reqwest::StatusCode::BAD_REQUEST, + IsolateError::InvalidFilename => reqwest::StatusCode::BAD_REQUEST, + _ => reqwest::StatusCode::INTERNAL_SERVER_ERROR, + }; + + ( + status, + axum::Json(serde_json::json!({ + "error": self.to_string() + })), + ) + .into_response() + } +} diff --git a/src/errors/judge.rs b/src/errors/judge.rs new file mode 100644 index 0000000..9d63f5c --- /dev/null +++ b/src/errors/judge.rs @@ -0,0 +1,53 @@ +use crate::errors::IsolateError; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use reqwest::StatusCode; + +#[derive(Debug)] +pub enum JudgeError { + Isolate(IsolateError), + BadRequest(String), + NotFound(String), + Internal(String), +} + +impl std::fmt::Display for JudgeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + JudgeError::Isolate(err) => write!(f, "Isolate error: {}", err), + JudgeError::BadRequest(msg) => write!(f, "Bad request: {}", msg), + JudgeError::NotFound(msg) => write!(f, "Not found: {}", msg), + JudgeError::Internal(msg) => write!(f, "Internal server error: {}", msg), + } + } +} + +impl From for JudgeError { + fn from(e: IsolateError) -> Self { + JudgeError::Isolate(e) + } +} + +impl IntoResponse for JudgeError { + fn into_response(self) -> Response { + match self { + JudgeError::Isolate(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error" : e.to_string() })), + ), + JudgeError::BadRequest(msg) => ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error" : msg })), + ), + JudgeError::NotFound(msg) => ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error" : msg })), + ), + JudgeError::Internal(msg) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error" : msg })), + ), + } + .into_response() + } +} diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 2ff9686..274d34a 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -1,3 +1,9 @@ +mod docker; +mod isolate; +mod judge; mod language; +pub(crate) use docker::*; +pub(crate) use isolate::*; +pub(crate) use judge::*; pub(crate) use language::*; diff --git a/src/grader/docker_isolate.rs b/src/grader/docker_isolate.rs new file mode 100644 index 0000000..463793f --- /dev/null +++ b/src/grader/docker_isolate.rs @@ -0,0 +1,52 @@ +use crate::errors::JudgeError; +use crate::isolate::{BoxId, Isolate}; +use crate::judge::{CompileConfig, CompileResult, ExecuteConfig, ExecuteResult, Grader}; +use async_trait::async_trait; + +pub struct DockerIsolateGrader { + isolate: Isolate, +} + +impl DockerIsolateGrader { + pub fn new(container: impl Into) -> Self { + Self { + isolate: Isolate::new(container), + } + } +} + +const UPLOAD_DIR: &str = "uploads"; + +#[async_trait] +impl Grader for DockerIsolateGrader { + /// Isolate 박스를 초기화하고, UPLOAD_DIR/{box_id} 내부 파일을 복사합니다. + /// # Arguments + /// * `box_id` - The ID of the isolate box + /// # Returns + /// * `Result<(), JudgeError>` - Ok if successful, Err otherwise + async fn init(&self, box_id: u32) -> Result<(), JudgeError> { + self.isolate.init(BoxId(box_id)).await?; + self.isolate + .copy_to_box( + &BoxId(box_id), + &format!("{}/{}/.", UPLOAD_DIR, box_id).as_ref(), + ".", + ) + .await?; + Ok(()) + } + + async fn compile(&self, box_id: u32, cfg: CompileConfig) -> Result { + self.isolate + .compile(&BoxId(box_id), &cfg) + .await + .map_err(JudgeError::from) + } + + async fn execute(&self, box_id: u32, cfg: ExecuteConfig) -> Result { + self.isolate + .execute(&BoxId(box_id), &cfg) + .await + .map_err(JudgeError::from) + } +} diff --git a/src/grader/mod.rs b/src/grader/mod.rs new file mode 100644 index 0000000..8dc977a --- /dev/null +++ b/src/grader/mod.rs @@ -0,0 +1 @@ +pub mod docker_isolate; diff --git a/src/isolate/box_id.rs b/src/isolate/box_id.rs new file mode 100644 index 0000000..6984853 --- /dev/null +++ b/src/isolate/box_id.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BoxId(pub u32); + +impl std::fmt::Display for BoxId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/src/isolate/cleanup.rs b/src/isolate/cleanup.rs new file mode 100644 index 0000000..a65900d --- /dev/null +++ b/src/isolate/cleanup.rs @@ -0,0 +1,12 @@ +use crate::{docker::exec::exec, errors::IsolateError, isolate::BoxId}; + +pub async fn cleanup(container: &str, box_id: &BoxId) -> Result<(), IsolateError> { + exec( + container, + &["isolate", "--cleanup", &format!("--box-id={}", box_id)], + ) + .await? + .ensure_success(|out| IsolateError::CleanupFailed(out.stderr.clone()))?; + + Ok(()) +} diff --git a/src/isolate/compile.rs b/src/isolate/compile.rs new file mode 100644 index 0000000..cb999e4 --- /dev/null +++ b/src/isolate/compile.rs @@ -0,0 +1,53 @@ +use crate::file_manager::Language; +use crate::judge::{CompileConfig, CompileResult}; +use crate::{docker::exec::exec, errors::IsolateError, isolate::BoxId}; +use std::path::PathBuf; + +pub async fn compile( + container: &str, + box_id: &BoxId, + cfg: &CompileConfig, +) -> Result { + let box_id = format!("--box-id={}", box_id); + let processes = format!("--processes={}", cfg.processes); + let mut args = vec!["isolate", "--run", "--full-env", &box_id, &processes, "--"]; + + let argv: Vec<&str> = cfg.argv.iter().map(|s| s.as_str()).collect(); + args.extend(argv); + + let out = exec(container, &args) + .await? + .ensure_success(|out| IsolateError::CompileFailed(out.stderr.clone()))?; + + Ok(CompileResult { + success: out.status.success(), + stdout: out.stdout, + stderr: out.stderr, + }) +} + +pub struct CompilePlan { + pub problem_id: u32, + pub language: Language, + pub source_path: PathBuf, + pub source_file: String, + pub executable: Option, +} + +impl CompilePlan { + pub fn new(problem_id: u32, category: &str, filename: &str, language: Language) -> Self { + let source_file = format!("{}/{}", category, filename); + let executable = match language { + Language::Cpp => Some(filename.split('.').next().unwrap().to_string()), + _ => None, + }; + + Self { + problem_id, + language, + source_path: PathBuf::from(source_file.clone()), + source_file, + executable, + } + } +} diff --git a/src/isolate/copy.rs b/src/isolate/copy.rs new file mode 100644 index 0000000..5568bf3 --- /dev/null +++ b/src/isolate/copy.rs @@ -0,0 +1,27 @@ +use std::path::Path; + +use crate::{docker, errors::IsolateError, isolate::BoxId}; + +use super::path::box_root; + +pub async fn to_box( + container: &str, + box_id: &BoxId, + local: &Path, + box_relative: &str, +) -> Result<(), IsolateError> { + let remote = box_root(box_id).join(box_relative); + docker::cp::to_container(container, local, &remote).await?; + Ok(()) +} + +pub async fn from_box( + container: &str, + box_id: &BoxId, + box_relative: &str, + local: &Path, +) -> Result<(), IsolateError> { + let remote = box_root(box_id).join(box_relative); + docker::cp::from_container(container, &remote, local).await?; + Ok(()) +} diff --git a/src/isolate/execute.rs b/src/isolate/execute.rs new file mode 100644 index 0000000..df77f46 --- /dev/null +++ b/src/isolate/execute.rs @@ -0,0 +1,26 @@ +use crate::judge::{ExecuteConfig, ExecuteResult}; +use crate::{docker::exec::exec, errors::IsolateError, isolate::BoxId}; + +pub async fn execute( + container: &str, + box_id: &BoxId, + cfg: &ExecuteConfig, +) -> Result { + let box_id = format!("--box-id={}", box_id); + let mut args = vec!["isolate", "--run", &box_id, "--"]; + + let argv: Vec<&str> = cfg.argv.iter().map(|s| s.as_str()).collect(); + args.extend(argv); + + let out = exec(container, &args) + .await? + .ensure_success(|out| IsolateError::ExecuteFailed(out.stderr.clone()))?; + + Ok(ExecuteResult { + exit_code: out.status.code().unwrap_or(-1), + stdout: out.stdout, + stderr: out.stderr, + time_ms: 0, + memory_kb: 0, + }) +} diff --git a/src/isolate/init.rs b/src/isolate/init.rs new file mode 100644 index 0000000..01970da --- /dev/null +++ b/src/isolate/init.rs @@ -0,0 +1,12 @@ +use crate::{docker::exec::exec, errors::IsolateError, isolate::BoxId}; + +pub async fn init(container: &str, box_id: BoxId) -> Result<(), IsolateError> { + exec( + container, + &["isolate", "--init", format!("--box-id={}", box_id).as_str()], + ) + .await? + .ensure_success(|out| IsolateError::InitFailed(out.stderr.clone()))?; + + Ok(()) +} diff --git a/src/isolate/mod.rs b/src/isolate/mod.rs new file mode 100644 index 0000000..960b3fc --- /dev/null +++ b/src/isolate/mod.rs @@ -0,0 +1,65 @@ +mod box_id; +mod cleanup; +mod compile; +mod copy; +mod execute; +mod init; +mod path; + +use crate::errors::IsolateError; +use crate::judge::{CompileConfig, CompileResult, ExecuteConfig, ExecuteResult}; +pub use box_id::BoxId; + +pub struct Isolate { + container: String, +} + +impl Isolate { + pub fn new(container: impl Into) -> Self { + Self { + container: container.into(), + } + } + + pub async fn init(&self, box_id: BoxId) -> Result<(), IsolateError> { + init::init(&self.container, box_id).await + } + + pub async fn compile( + &self, + box_id: &BoxId, + cfg: &CompileConfig, + ) -> Result { + compile::compile(&self.container, box_id, cfg).await + } + + pub async fn execute( + &self, + box_id: &BoxId, + cfg: &ExecuteConfig, + ) -> Result { + execute::execute(&self.container, box_id, cfg).await + } + + pub async fn copy_to_box( + &self, + box_id: &BoxId, + local: &std::path::Path, + box_path: &str, + ) -> Result<(), IsolateError> { + copy::to_box(&self.container, box_id, local, box_path).await + } + + pub async fn copy_from_box( + &self, + box_id: &BoxId, + box_path: &str, + local: &std::path::Path, + ) -> Result<(), IsolateError> { + copy::from_box(&self.container, box_id, box_path, local).await + } + + pub async fn cleanup(&self, box_id: &BoxId) -> Result<(), IsolateError> { + cleanup::cleanup(&self.container, box_id).await + } +} diff --git a/src/isolate/path.rs b/src/isolate/path.rs new file mode 100644 index 0000000..44305cc --- /dev/null +++ b/src/isolate/path.rs @@ -0,0 +1,6 @@ +use super::BoxId; +use std::path::PathBuf; + +pub fn box_root(box_id: &BoxId) -> PathBuf { + PathBuf::from(format!("/var/local/lib/isolate/{}/box", box_id.0)) +} diff --git a/src/judge/config.rs b/src/judge/config.rs new file mode 100644 index 0000000..25068d5 --- /dev/null +++ b/src/judge/config.rs @@ -0,0 +1,27 @@ +use std::path::PathBuf; + +#[derive(Debug)] +pub struct CompileConfig { + pub language: Language, + pub source_path: PathBuf, + pub workdir: PathBuf, + pub argv: Vec, + pub processes: u32, +} + +#[derive(Debug)] +pub struct ExecuteConfig { + pub executable: String, + pub argv: Vec, + pub time_limit_ms: u64, + pub memory_limit_kb: u64, + pub processes: u32, +} + +#[derive(Debug, Clone, Copy)] +pub enum Language { + C, + Cpp, + Python, + Java, +} diff --git a/src/judge/grader.rs b/src/judge/grader.rs new file mode 100644 index 0000000..396e693 --- /dev/null +++ b/src/judge/grader.rs @@ -0,0 +1,12 @@ +use super::{CompileConfig, CompileResult, ExecuteConfig, ExecuteResult}; +use crate::errors::JudgeError; +use async_trait::async_trait; + +#[async_trait] +pub trait Grader: Send + Sync { + async fn init(&self, box_id: u32) -> Result<(), JudgeError>; + + async fn compile(&self, box_id: u32, cfg: CompileConfig) -> Result; + + async fn execute(&self, box_id: u32, cfg: ExecuteConfig) -> Result; +} diff --git a/src/judge/mod.rs b/src/judge/mod.rs new file mode 100644 index 0000000..a85ba3e --- /dev/null +++ b/src/judge/mod.rs @@ -0,0 +1,7 @@ +pub mod config; +pub mod grader; +pub mod result; + +pub use config::*; +pub use grader::Grader; +pub use result::*; diff --git a/src/judge/result.rs b/src/judge/result.rs new file mode 100644 index 0000000..83950d0 --- /dev/null +++ b/src/judge/result.rs @@ -0,0 +1,15 @@ +#[derive(Debug)] +pub struct CompileResult { + pub success: bool, + pub stdout: String, + pub stderr: String, +} + +#[derive(Debug)] +pub struct ExecuteResult { + pub exit_code: i32, + pub stdout: String, + pub stderr: String, + pub time_ms: u64, + pub memory_kb: u64, +} diff --git a/src/judge_manager/handlers.rs b/src/judge_manager/handlers.rs new file mode 100644 index 0000000..034be62 --- /dev/null +++ b/src/judge_manager/handlers.rs @@ -0,0 +1,131 @@ +use crate::errors::JudgeError; +use crate::grader::docker_isolate::DockerIsolateGrader; +use crate::judge::Grader; +use axum::{extract::Path, http::StatusCode, response::IntoResponse, Json}; + +pub async fn initialize_isolate( + Path(problem_id): Path, +) -> Result { + let grader = DockerIsolateGrader::new("coduck-grader"); + grader.init(problem_id).await?; + + Ok(Json(serde_json::json!({ + "message": format!("Sandbox initialized for problem ID {}", problem_id) + }))) +} + +pub async fn compile_file( + Path((problem_id, category, filename)): Path<(u32, String, String)>, +) -> impl IntoResponse { + ( + StatusCode::OK, + Json(serde_json::json!({ + "message": format!("File '{filename}' compiled successfully") + })), + ) + .into_response() +} +// let file_path = PathBuf::from(UPLOAD_DIR) +// .join(problem_id.to_string()) +// .join(&category) +// .join(&filename); +// +// if !file_exists(&file_path).await { +// return ( +// StatusCode::NOT_FOUND, +// Json(serde_json::json!({ +// "error": format!("File '{filename}' not found in category '{category}'") +// })), +// ) +// .into_response(); +// } +// +// let box_id = format!("--box-id={}", problem_id); +// let args = vec![ +// "exec", +// "-u", +// "judge", +// "coduck-grader", +// "isolate", +// "--run", +// "--full-env", +// box_id.as_str(), +// ]; +// +// let source_file = format!("{}/{}", category, filename); +// let executable = source_file +// .split('.') +// .next() +// .ok_or(StatusCode::NOT_FOUND) +// .expect("source file contains invalid extension"); +// +// let language = Language::from_filename(&filename).unwrap(); +// let python_temp = format!( +// "\"import py_compile; py_compile.compile(r'{}')\"", +// source_file +// ); +// let command = match language { +// Language::Cpp => vec![ +// "--processes=4", +// "--", +// "/usr/bin/g++", +// &source_file, +// "-o", +// executable, +// "-O2", +// "-Wall", +// "-lm", +// "-static", +// "-std=gnu++20", +// ], +// Language::Python => vec!["--", "/usr/bin/python3", "-W", "ignore", "-c", &python_temp], +// Language::Java => vec![ +// "--processes=32", +// "--", +// "/usr/lib/jvm/java-8-openjdk-amd64/bin/javac", +// "-J-Xms1024m", +// "-J-Xmx1920m", +// "-J-Xss512m", +// "-encoding", +// "UTF-8", +// &source_file, +// ], +// _ => { +// return ( +// StatusCode::INTERNAL_SERVER_ERROR, +// Json(serde_json::json!({ +// "error": format!("Unsupported file type for '{filename}'") +// })), +// ) +// .into_response(); +// } +// }; +// +// Command::new("docker") +// .args(args) +// .args(command) +// .output() +// .map_err(|_| { +// ( +// StatusCode::INTERNAL_SERVER_ERROR, +// Json(serde_json::json!({ +// "error": "Failed to execute compilation command" +// })), +// ) +// }) +// .expect("TODO: panic message"); +// +// docker_cp_from_container( +// &format!("/var/local/lib/isolate/0/box/{}", executable), +// &format!("{}/{}/{}", UPLOAD_DIR, problem_id, executable), +// ) +// .unwrap(); +// +// ( +// StatusCode::OK, +// Json(serde_json::json!({ +// "message": format!("File '{filename}' compiled successfully") +// })), +// ) +// .into_response() +// } diff --git a/src/judge_manager/mod.rs b/src/judge_manager/mod.rs new file mode 100644 index 0000000..91ae163 --- /dev/null +++ b/src/judge_manager/mod.rs @@ -0,0 +1,3 @@ +mod handlers; + +pub(crate) use handlers::*; diff --git a/src/lib.rs b/src/lib.rs index 2c3e91d..993fe04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,10 @@ +mod docker; mod errors; pub mod file_manager; +mod grader; +mod isolate; +mod judge; +pub mod judge_manager; use axum::{ routing::{delete, get, post, put}, @@ -10,6 +15,8 @@ use crate::file_manager::{ delete_file, get_file, get_files_by_category, update_file_content, update_filename, upload_file, }; +use crate::judge_manager::{compile_file, initialize_isolate}; + async fn health_check() -> &'static str { "OK" } @@ -26,7 +33,15 @@ pub fn build_router() -> Router { ) .route("/{problem_id}/{category}", put(update_filename)); + let judge_router = Router::new() + .route("/init/{problem_id}", post(initialize_isolate)) + .route( + "/compile/{problem_id}/{category}/{filename}", + post(compile_file), + ); + Router::new() .route("/health", get(health_check)) .nest("/problems", problems_router) + .nest("/judge", judge_router) } From cfc6d353a158fbd52b5cf5eb084797f61ace428d Mon Sep 17 00:00:00 2001 From: w8385 Date: Tue, 27 Jan 2026 13:18:39 +0900 Subject: [PATCH 02/22] refactor: remove unnecessary IntoResponse implementations --- src/errors/docker.rs | 22 ---------------------- src/errors/isolate.rs | 25 ------------------------- 2 files changed, 47 deletions(-) diff --git a/src/errors/docker.rs b/src/errors/docker.rs index c6cb8a5..9e75547 100644 --- a/src/errors/docker.rs +++ b/src/errors/docker.rs @@ -1,6 +1,3 @@ -use axum::response::{IntoResponse, Response}; -use axum::Json; -use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter, Result}; @@ -24,22 +21,3 @@ impl Display for DockerError { } } } - -impl IntoResponse for DockerError { - fn into_response(self) -> Response { - let status = match self { - DockerError::FileNotFound(_) => StatusCode::NOT_FOUND, - DockerError::UnsupportedExtension(_) => StatusCode::BAD_REQUEST, - DockerError::InvalidFilename => StatusCode::BAD_REQUEST, - DockerError::Spawn => StatusCode::INTERNAL_SERVER_ERROR, - }; - - ( - status, - Json(serde_json::json!({ - "error": self.to_string() - })), - ) - .into_response() - } -} diff --git a/src/errors/isolate.rs b/src/errors/isolate.rs index a1e1725..5cd4ca2 100644 --- a/src/errors/isolate.rs +++ b/src/errors/isolate.rs @@ -1,5 +1,4 @@ use crate::errors::DockerError; -use axum::response::IntoResponse; use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Serialize, Deserialize)] @@ -36,27 +35,3 @@ impl From for IsolateError { IsolateError::Docker(e) } } - -impl IntoResponse for IsolateError { - fn into_response(self) -> axum::response::Response { - let status = match self { - IsolateError::Docker(ref e) => match e { - DockerError::FileNotFound(_) => reqwest::StatusCode::NOT_FOUND, - DockerError::UnsupportedExtension(_) => reqwest::StatusCode::BAD_REQUEST, - DockerError::InvalidFilename => reqwest::StatusCode::BAD_REQUEST, - DockerError::Spawn => reqwest::StatusCode::INTERNAL_SERVER_ERROR, - }, - IsolateError::UnsupportedExtension(_) => reqwest::StatusCode::BAD_REQUEST, - IsolateError::InvalidFilename => reqwest::StatusCode::BAD_REQUEST, - _ => reqwest::StatusCode::INTERNAL_SERVER_ERROR, - }; - - ( - status, - axum::Json(serde_json::json!({ - "error": self.to_string() - })), - ) - .into_response() - } -} From 2b84dcd4ebe2bca3204a3b29b86ccb3bddbcc19b Mon Sep 17 00:00:00 2001 From: w8385 Date: Tue, 27 Jan 2026 14:23:28 +0900 Subject: [PATCH 03/22] refactor: migrate judge components to grader module --- src/{judge => grader}/config.rs | 0 src/grader/docker_isolate.rs | 4 +++- src/{judge => grader}/grader.rs | 3 ++- src/grader/mod.rs | 3 +++ src/{judge => grader}/result.rs | 0 src/isolate/compile.rs | 3 ++- src/isolate/execute.rs | 3 ++- src/isolate/mod.rs | 3 ++- src/judge/mod.rs | 7 ------- src/judge_manager/handlers.rs | 25 ++++++++++++++++--------- src/lib.rs | 1 - 11 files changed, 30 insertions(+), 22 deletions(-) rename src/{judge => grader}/config.rs (100%) rename src/{judge => grader}/grader.rs (76%) rename src/{judge => grader}/result.rs (100%) delete mode 100644 src/judge/mod.rs diff --git a/src/judge/config.rs b/src/grader/config.rs similarity index 100% rename from src/judge/config.rs rename to src/grader/config.rs diff --git a/src/grader/docker_isolate.rs b/src/grader/docker_isolate.rs index 463793f..3903b44 100644 --- a/src/grader/docker_isolate.rs +++ b/src/grader/docker_isolate.rs @@ -1,6 +1,8 @@ use crate::errors::JudgeError; +use crate::grader::config::{CompileConfig, ExecuteConfig}; +use crate::grader::grader::Grader; +use crate::grader::result::{CompileResult, ExecuteResult}; use crate::isolate::{BoxId, Isolate}; -use crate::judge::{CompileConfig, CompileResult, ExecuteConfig, ExecuteResult, Grader}; use async_trait::async_trait; pub struct DockerIsolateGrader { diff --git a/src/judge/grader.rs b/src/grader/grader.rs similarity index 76% rename from src/judge/grader.rs rename to src/grader/grader.rs index 396e693..08a87dc 100644 --- a/src/judge/grader.rs +++ b/src/grader/grader.rs @@ -1,5 +1,6 @@ -use super::{CompileConfig, CompileResult, ExecuteConfig, ExecuteResult}; use crate::errors::JudgeError; +use crate::grader::config::{CompileConfig, ExecuteConfig}; +use crate::grader::result::{CompileResult, ExecuteResult}; use async_trait::async_trait; #[async_trait] diff --git a/src/grader/mod.rs b/src/grader/mod.rs index 8dc977a..01696fc 100644 --- a/src/grader/mod.rs +++ b/src/grader/mod.rs @@ -1 +1,4 @@ +pub mod config; pub mod docker_isolate; +pub mod grader; +pub mod result; diff --git a/src/judge/result.rs b/src/grader/result.rs similarity index 100% rename from src/judge/result.rs rename to src/grader/result.rs diff --git a/src/isolate/compile.rs b/src/isolate/compile.rs index cb999e4..55d1a33 100644 --- a/src/isolate/compile.rs +++ b/src/isolate/compile.rs @@ -1,5 +1,6 @@ use crate::file_manager::Language; -use crate::judge::{CompileConfig, CompileResult}; +use crate::grader::config::CompileConfig; +use crate::grader::result::CompileResult; use crate::{docker::exec::exec, errors::IsolateError, isolate::BoxId}; use std::path::PathBuf; diff --git a/src/isolate/execute.rs b/src/isolate/execute.rs index df77f46..d2971b5 100644 --- a/src/isolate/execute.rs +++ b/src/isolate/execute.rs @@ -1,4 +1,5 @@ -use crate::judge::{ExecuteConfig, ExecuteResult}; +use crate::grader::config::ExecuteConfig; +use crate::grader::result::ExecuteResult; use crate::{docker::exec::exec, errors::IsolateError, isolate::BoxId}; pub async fn execute( diff --git a/src/isolate/mod.rs b/src/isolate/mod.rs index 960b3fc..fcb91d8 100644 --- a/src/isolate/mod.rs +++ b/src/isolate/mod.rs @@ -7,7 +7,8 @@ mod init; mod path; use crate::errors::IsolateError; -use crate::judge::{CompileConfig, CompileResult, ExecuteConfig, ExecuteResult}; +use crate::grader::config::{CompileConfig, ExecuteConfig}; +use crate::grader::result::{CompileResult, ExecuteResult}; pub use box_id::BoxId; pub struct Isolate { diff --git a/src/judge/mod.rs b/src/judge/mod.rs deleted file mode 100644 index a85ba3e..0000000 --- a/src/judge/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod config; -pub mod grader; -pub mod result; - -pub use config::*; -pub use grader::Grader; -pub use result::*; diff --git a/src/judge_manager/handlers.rs b/src/judge_manager/handlers.rs index 034be62..ab10211 100644 --- a/src/judge_manager/handlers.rs +++ b/src/judge_manager/handlers.rs @@ -1,7 +1,7 @@ use crate::errors::JudgeError; use crate::grader::docker_isolate::DockerIsolateGrader; -use crate::judge::Grader; -use axum::{extract::Path, http::StatusCode, response::IntoResponse, Json}; +use crate::grader::grader::Grader; +use axum::{extract::Path, response::IntoResponse, Json}; pub async fn initialize_isolate( Path(problem_id): Path, @@ -17,14 +17,21 @@ pub async fn initialize_isolate( pub async fn compile_file( Path((problem_id, category, filename)): Path<(u32, String, String)>, ) -> impl IntoResponse { - ( - StatusCode::OK, - Json(serde_json::json!({ - "message": format!("File '{filename}' compiled successfully") - })), - ) - .into_response() } +// let grader = DockerIsolateGrader::new("coduck-grader"); +// grader +// .compile( +// problem_id, +// crate::judge::CompileConfig::from_filename(&filename, &category), +// ) +// .await +// .unwrap(); +// +// Ok(Json(serde_json::json!({ +// "message": format!("File '{filename}' compiled successfully") +// }))) +// } + // let file_path = PathBuf::from(UPLOAD_DIR) // .join(problem_id.to_string()) // .join(&category) diff --git a/src/lib.rs b/src/lib.rs index 993fe04..d6133db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ mod errors; pub mod file_manager; mod grader; mod isolate; -mod judge; pub mod judge_manager; use axum::{ From 7ce815f54d58cfd819d6b5a5227900550245be8a Mon Sep 17 00:00:00 2001 From: w8385 Date: Tue, 27 Jan 2026 22:18:52 +0900 Subject: [PATCH 04/22] feat: /judge/compile --- src/grader/config.rs | 70 +++++++++++++---- src/grader/docker_isolate.rs | 18 +++-- src/grader/result.rs | 2 - src/isolate/compile.rs | 39 +-------- src/judge_manager/handlers.rs | 144 ++++++---------------------------- 5 files changed, 97 insertions(+), 176 deletions(-) diff --git a/src/grader/config.rs b/src/grader/config.rs index 25068d5..c0a1eb7 100644 --- a/src/grader/config.rs +++ b/src/grader/config.rs @@ -1,27 +1,69 @@ -use std::path::PathBuf; +use crate::file_manager::Language; #[derive(Debug)] pub struct CompileConfig { - pub language: Language, - pub source_path: PathBuf, - pub workdir: PathBuf, + pub source_path: String, + pub output_path: String, pub argv: Vec, - pub processes: u32, } #[derive(Debug)] pub struct ExecuteConfig { pub executable: String, pub argv: Vec, - pub time_limit_ms: u64, - pub memory_limit_kb: u64, - pub processes: u32, + // pub time_limit_ms: u64, + // pub memory_limit_kb: u64, } -#[derive(Debug, Clone, Copy)] -pub enum Language { - C, - Cpp, - Python, - Java, +const UPLOAD_DIR: &str = "uploads"; + +impl CompileConfig { + pub fn new(category: &str, filename: &str, language: Language) -> CompileConfig { + let source_path = format!("{}/{}", category, filename); + let output_path = format!("{}/{}", category, filename.split('.').next().unwrap()); + + let python_temp = format!( + "\"import py_compile; py_compile.compile(r'{}')\"", + source_path + ); + let argv = match language { + Language::Cpp => vec![ + "--processes=4", + "--", + "/usr/bin/g++", + &source_path, + "-o", + &output_path, + "-O2", + "-Wall", + "-lm", + "-static", + "-std=gnu++17", + ], + Language::Python => { + vec!["--", "/usr/bin/pypy3", "-W", "ignore", "-c", &python_temp] + } + Language::Java => vec![ + "--processes=32", + "--", + "/usr/lib/jvm/java-8-openjdk-amd64/bin/javac", + "-J-Xms1024m", + "-J-Xmx1920m", + "-J-Xss512m", + "-encoding", + "UTF-8", + &source_path, + ], + _ => vec![], + } + .iter() + .map(|s| s.to_string()) + .collect(); + + CompileConfig { + source_path, + output_path, + argv, + } + } } diff --git a/src/grader/docker_isolate.rs b/src/grader/docker_isolate.rs index 3903b44..d94a3df 100644 --- a/src/grader/docker_isolate.rs +++ b/src/grader/docker_isolate.rs @@ -24,8 +24,6 @@ impl Grader for DockerIsolateGrader { /// Isolate 박스를 초기화하고, UPLOAD_DIR/{box_id} 내부 파일을 복사합니다. /// # Arguments /// * `box_id` - The ID of the isolate box - /// # Returns - /// * `Result<(), JudgeError>` - Ok if successful, Err otherwise async fn init(&self, box_id: u32) -> Result<(), JudgeError> { self.isolate.init(BoxId(box_id)).await?; self.isolate @@ -38,11 +36,21 @@ impl Grader for DockerIsolateGrader { Ok(()) } + /// 컴파일을 수행하고, 결과물을 UPLOAD_DIR/{box_id}/{output_path}에 복사합니다. + /// # Arguments + /// * `box_id` - The ID of the isolate box + /// * `cfg` - The compiler configuration async fn compile(&self, box_id: u32, cfg: CompileConfig) -> Result { + let result = self.isolate.compile(&BoxId(box_id), &cfg).await?; self.isolate - .compile(&BoxId(box_id), &cfg) - .await - .map_err(JudgeError::from) + .copy_from_box( + &BoxId(box_id), + &cfg.output_path, + &format!("{}/{}/{}", UPLOAD_DIR, box_id, cfg.output_path).as_ref(), + ) + .await?; + + Ok(result) } async fn execute(&self, box_id: u32, cfg: ExecuteConfig) -> Result { diff --git a/src/grader/result.rs b/src/grader/result.rs index 83950d0..a38561e 100644 --- a/src/grader/result.rs +++ b/src/grader/result.rs @@ -1,8 +1,6 @@ #[derive(Debug)] pub struct CompileResult { - pub success: bool, pub stdout: String, - pub stderr: String, } #[derive(Debug)] diff --git a/src/isolate/compile.rs b/src/isolate/compile.rs index 55d1a33..f58318b 100644 --- a/src/isolate/compile.rs +++ b/src/isolate/compile.rs @@ -1,8 +1,6 @@ -use crate::file_manager::Language; use crate::grader::config::CompileConfig; use crate::grader::result::CompileResult; use crate::{docker::exec::exec, errors::IsolateError, isolate::BoxId}; -use std::path::PathBuf; pub async fn compile( container: &str, @@ -10,45 +8,14 @@ pub async fn compile( cfg: &CompileConfig, ) -> Result { let box_id = format!("--box-id={}", box_id); - let processes = format!("--processes={}", cfg.processes); - let mut args = vec!["isolate", "--run", "--full-env", &box_id, &processes, "--"]; + let mut args = vec!["isolate", "--run", "--full-env", &box_id]; - let argv: Vec<&str> = cfg.argv.iter().map(|s| s.as_str()).collect(); + let argv: Vec<&str> = cfg.argv.iter().map(String::as_str).collect(); args.extend(argv); let out = exec(container, &args) .await? .ensure_success(|out| IsolateError::CompileFailed(out.stderr.clone()))?; - Ok(CompileResult { - success: out.status.success(), - stdout: out.stdout, - stderr: out.stderr, - }) -} - -pub struct CompilePlan { - pub problem_id: u32, - pub language: Language, - pub source_path: PathBuf, - pub source_file: String, - pub executable: Option, -} - -impl CompilePlan { - pub fn new(problem_id: u32, category: &str, filename: &str, language: Language) -> Self { - let source_file = format!("{}/{}", category, filename); - let executable = match language { - Language::Cpp => Some(filename.split('.').next().unwrap().to_string()), - _ => None, - }; - - Self { - problem_id, - language, - source_path: PathBuf::from(source_file.clone()), - source_file, - executable, - } - } + Ok(CompileResult { stdout: out.stdout }) } diff --git a/src/judge_manager/handlers.rs b/src/judge_manager/handlers.rs index ab10211..9a8bdd2 100644 --- a/src/judge_manager/handlers.rs +++ b/src/judge_manager/handlers.rs @@ -1,7 +1,11 @@ use crate::errors::JudgeError; +use crate::file_manager::Language; +use crate::grader::config::CompileConfig; use crate::grader::docker_isolate::DockerIsolateGrader; use crate::grader::grader::Grader; +use axum::extract::Query; use axum::{extract::Path, response::IntoResponse, Json}; +use serde::Deserialize; pub async fn initialize_isolate( Path(problem_id): Path, @@ -14,125 +18,27 @@ pub async fn initialize_isolate( }))) } +#[derive(Deserialize)] +pub struct Params { + language: Option, +} + pub async fn compile_file( Path((problem_id, category, filename)): Path<(u32, String, String)>, -) -> impl IntoResponse { -} -// let grader = DockerIsolateGrader::new("coduck-grader"); -// grader -// .compile( -// problem_id, -// crate::judge::CompileConfig::from_filename(&filename, &category), -// ) -// .await -// .unwrap(); -// -// Ok(Json(serde_json::json!({ -// "message": format!("File '{filename}' compiled successfully") -// }))) -// } + params: Query, +) -> Result { + let language = params.language.clone().unwrap(); -// let file_path = PathBuf::from(UPLOAD_DIR) -// .join(problem_id.to_string()) -// .join(&category) -// .join(&filename); -// -// if !file_exists(&file_path).await { -// return ( -// StatusCode::NOT_FOUND, -// Json(serde_json::json!({ -// "error": format!("File '{filename}' not found in category '{category}'") -// })), -// ) -// .into_response(); -// } -// -// let box_id = format!("--box-id={}", problem_id); -// let args = vec![ -// "exec", -// "-u", -// "judge", -// "coduck-grader", -// "isolate", -// "--run", -// "--full-env", -// box_id.as_str(), -// ]; -// -// let source_file = format!("{}/{}", category, filename); -// let executable = source_file -// .split('.') -// .next() -// .ok_or(StatusCode::NOT_FOUND) -// .expect("source file contains invalid extension"); -// -// let language = Language::from_filename(&filename).unwrap(); -// let python_temp = format!( -// "\"import py_compile; py_compile.compile(r'{}')\"", -// source_file -// ); -// let command = match language { -// Language::Cpp => vec![ -// "--processes=4", -// "--", -// "/usr/bin/g++", -// &source_file, -// "-o", -// executable, -// "-O2", -// "-Wall", -// "-lm", -// "-static", -// "-std=gnu++20", -// ], -// Language::Python => vec!["--", "/usr/bin/python3", "-W", "ignore", "-c", &python_temp], -// Language::Java => vec![ -// "--processes=32", -// "--", -// "/usr/lib/jvm/java-8-openjdk-amd64/bin/javac", -// "-J-Xms1024m", -// "-J-Xmx1920m", -// "-J-Xss512m", -// "-encoding", -// "UTF-8", -// &source_file, -// ], -// _ => { -// return ( -// StatusCode::INTERNAL_SERVER_ERROR, -// Json(serde_json::json!({ -// "error": format!("Unsupported file type for '{filename}'") -// })), -// ) -// .into_response(); -// } -// }; -// -// Command::new("docker") -// .args(args) -// .args(command) -// .output() -// .map_err(|_| { -// ( -// StatusCode::INTERNAL_SERVER_ERROR, -// Json(serde_json::json!({ -// "error": "Failed to execute compilation command" -// })), -// ) -// }) -// .expect("TODO: panic message"); -// -// docker_cp_from_container( -// &format!("/var/local/lib/isolate/0/box/{}", executable), -// &format!("{}/{}/{}", UPLOAD_DIR, problem_id, executable), -// ) -// .unwrap(); -// -// ( -// StatusCode::OK, -// Json(serde_json::json!({ -// "message": format!("File '{filename}' compiled successfully") -// })), -// ) -// .into_response() -// } + let grader = DockerIsolateGrader::new("coduck-grader"); + let result = grader + .compile( + problem_id, + CompileConfig::new(&category, &filename, language), + ) + .await?; + + Ok(Json(serde_json::json!({ + "message": format!("File {} compiled successfully in category {} for problem ID {}", filename, category, problem_id), + "stdout": result.stdout, + }))) +} From 3f3a929338f79e52b450623e7e4a3b7225b5c6aa Mon Sep 17 00:00:00 2001 From: w8385 Date: Tue, 27 Jan 2026 22:24:46 +0900 Subject: [PATCH 05/22] feat: /judge/cleanup --- src/grader/docker_isolate.rs | 5 +++++ src/grader/grader.rs | 2 ++ src/judge_manager/handlers.rs | 9 +++++++++ src/lib.rs | 5 +++-- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/grader/docker_isolate.rs b/src/grader/docker_isolate.rs index d94a3df..4551672 100644 --- a/src/grader/docker_isolate.rs +++ b/src/grader/docker_isolate.rs @@ -59,4 +59,9 @@ impl Grader for DockerIsolateGrader { .await .map_err(JudgeError::from) } + + async fn cleanup(&self, box_id: &BoxId) -> Result<(), JudgeError> { + self.isolate.cleanup(box_id).await?; + Ok(()) + } } diff --git a/src/grader/grader.rs b/src/grader/grader.rs index 08a87dc..1b52eef 100644 --- a/src/grader/grader.rs +++ b/src/grader/grader.rs @@ -10,4 +10,6 @@ pub trait Grader: Send + Sync { async fn compile(&self, box_id: u32, cfg: CompileConfig) -> Result; async fn execute(&self, box_id: u32, cfg: ExecuteConfig) -> Result; + + async fn cleanup(&self, box_id: &crate::isolate::BoxId) -> Result<(), JudgeError>; } diff --git a/src/judge_manager/handlers.rs b/src/judge_manager/handlers.rs index 9a8bdd2..3f50959 100644 --- a/src/judge_manager/handlers.rs +++ b/src/judge_manager/handlers.rs @@ -42,3 +42,12 @@ pub async fn compile_file( "stdout": result.stdout, }))) } + +pub async fn cleanup_isolate(Path(problem_id): Path) -> Result { + let grader = DockerIsolateGrader::new("coduck-grader"); + grader.cleanup(&crate::isolate::BoxId(problem_id)).await?; + + Ok(Json(serde_json::json!({ + "message": format!("Sandbox cleaned up for problem ID {}", problem_id) + }))) +} diff --git a/src/lib.rs b/src/lib.rs index d6133db..28d9c68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ use crate::file_manager::{ delete_file, get_file, get_files_by_category, update_file_content, update_filename, upload_file, }; -use crate::judge_manager::{compile_file, initialize_isolate}; +use crate::judge_manager::{cleanup_isolate, compile_file, initialize_isolate}; async fn health_check() -> &'static str { "OK" @@ -37,7 +37,8 @@ pub fn build_router() -> Router { .route( "/compile/{problem_id}/{category}/{filename}", post(compile_file), - ); + ) + .route("/cleanup/{problem_id}", delete(cleanup_isolate)); Router::new() .route("/health", get(health_check)) From 72e60036229157eea383d004c659783b48f35d9d Mon Sep 17 00:00:00 2001 From: w8385 Date: Tue, 27 Jan 2026 22:50:13 +0900 Subject: [PATCH 06/22] feat: /judge/execute --- src/grader/config.rs | 25 +++++++++++++++++++++++++ src/grader/docker_isolate.rs | 7 +++---- src/isolate/execute.rs | 2 +- src/judge_manager/handlers.rs | 24 +++++++++++++++++++++++- src/lib.rs | 6 +++++- 5 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/grader/config.rs b/src/grader/config.rs index c0a1eb7..31cb48c 100644 --- a/src/grader/config.rs +++ b/src/grader/config.rs @@ -67,3 +67,28 @@ impl CompileConfig { } } } + +impl ExecuteConfig { + pub fn new(category: &str, filename: &str, language: Language) -> ExecuteConfig { + let executable = format!("{}/{}", category, filename); + let argv = match language { + Language::Cpp => vec!["--", &executable], + Language::Python => vec!["--", "/usr/bin/python3", &executable], + Language::Java => vec![ + "--processes=32", + "--", + "/usr/lib/jvm/java-8-openjdk-amd64/bin/java", + "-Xms1024m", + "-Xmx1920m", + "-Xss512m", + "Main", + ], + _ => vec![], + } + .iter() + .map(|s| s.to_string()) + .collect(); + + ExecuteConfig { executable, argv } + } +} diff --git a/src/grader/docker_isolate.rs b/src/grader/docker_isolate.rs index 4551672..ad6f857 100644 --- a/src/grader/docker_isolate.rs +++ b/src/grader/docker_isolate.rs @@ -54,10 +54,9 @@ impl Grader for DockerIsolateGrader { } async fn execute(&self, box_id: u32, cfg: ExecuteConfig) -> Result { - self.isolate - .execute(&BoxId(box_id), &cfg) - .await - .map_err(JudgeError::from) + let result = self.isolate.execute(&BoxId(box_id), &cfg).await?; + + Ok(result) } async fn cleanup(&self, box_id: &BoxId) -> Result<(), JudgeError> { diff --git a/src/isolate/execute.rs b/src/isolate/execute.rs index d2971b5..d6a09c5 100644 --- a/src/isolate/execute.rs +++ b/src/isolate/execute.rs @@ -8,7 +8,7 @@ pub async fn execute( cfg: &ExecuteConfig, ) -> Result { let box_id = format!("--box-id={}", box_id); - let mut args = vec!["isolate", "--run", &box_id, "--"]; + let mut args = vec!["isolate", "--run", "--full-env", &box_id]; let argv: Vec<&str> = cfg.argv.iter().map(|s| s.as_str()).collect(); args.extend(argv); diff --git a/src/judge_manager/handlers.rs b/src/judge_manager/handlers.rs index 3f50959..e6b2848 100644 --- a/src/judge_manager/handlers.rs +++ b/src/judge_manager/handlers.rs @@ -1,6 +1,6 @@ use crate::errors::JudgeError; use crate::file_manager::Language; -use crate::grader::config::CompileConfig; +use crate::grader::config::{CompileConfig, ExecuteConfig}; use crate::grader::docker_isolate::DockerIsolateGrader; use crate::grader::grader::Grader; use axum::extract::Query; @@ -43,6 +43,28 @@ pub async fn compile_file( }))) } +pub async fn execute_file( + Path((problem_id, category, filename)): Path<(u32, String, String)>, + params: Query, +) -> Result { + let language = params.language.clone().unwrap(); + + let grader = DockerIsolateGrader::new("coduck-grader"); + let result = grader + .execute( + problem_id, + ExecuteConfig::new(&category, &filename, language), + ) + .await?; + + Ok(Json(serde_json::json!({ + "message": format!("File {} executed successfully in category {} for problem ID {}", filename, category, problem_id), + "stdout": result.stdout, + "stderr": result.stderr, + "exit_code": result.exit_code, + }))) +} + pub async fn cleanup_isolate(Path(problem_id): Path) -> Result { let grader = DockerIsolateGrader::new("coduck-grader"); grader.cleanup(&crate::isolate::BoxId(problem_id)).await?; diff --git a/src/lib.rs b/src/lib.rs index 28d9c68..4c8e60b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ use crate::file_manager::{ delete_file, get_file, get_files_by_category, update_file_content, update_filename, upload_file, }; -use crate::judge_manager::{cleanup_isolate, compile_file, initialize_isolate}; +use crate::judge_manager::{cleanup_isolate, compile_file, execute_file, initialize_isolate}; async fn health_check() -> &'static str { "OK" @@ -38,6 +38,10 @@ pub fn build_router() -> Router { "/compile/{problem_id}/{category}/{filename}", post(compile_file), ) + .route( + "/execute/{problem_id}/{category}/{filename}", + post(execute_file), + ) .route("/cleanup/{problem_id}", delete(cleanup_isolate)); Router::new() From efdcad0ca4a2f6c019520bead1c86f19a5e5de0e Mon Sep 17 00:00:00 2001 From: w8385 Date: Tue, 27 Jan 2026 22:58:37 +0900 Subject: [PATCH 07/22] fix: add classpath option to Java execution configuration --- src/grader/config.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/grader/config.rs b/src/grader/config.rs index 31cb48c..1f8f0d0 100644 --- a/src/grader/config.rs +++ b/src/grader/config.rs @@ -78,6 +78,8 @@ impl ExecuteConfig { "--processes=32", "--", "/usr/lib/jvm/java-8-openjdk-amd64/bin/java", + "-cp", + &category, "-Xms1024m", "-Xmx1920m", "-Xss512m", From 4a08eade09c034c763ad3def78981ecde7305e0b Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 16:39:42 +0900 Subject: [PATCH 08/22] refactor: add ExecuteConfig builders arg, stdin, stdout --- src/grader/config.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/grader/config.rs b/src/grader/config.rs index 1f8f0d0..c946817 100644 --- a/src/grader/config.rs +++ b/src/grader/config.rs @@ -15,8 +15,6 @@ pub struct ExecuteConfig { // pub memory_limit_kb: u64, } -const UPLOAD_DIR: &str = "uploads"; - impl CompileConfig { pub fn new(category: &str, filename: &str, language: Language) -> CompileConfig { let source_path = format!("{}/{}", category, filename); @@ -69,7 +67,7 @@ impl CompileConfig { } impl ExecuteConfig { - pub fn new(category: &str, filename: &str, language: Language) -> ExecuteConfig { + pub fn new(category: &str, filename: &str, language: Language) -> Self { let executable = format!("{}/{}", category, filename); let argv = match language { Language::Cpp => vec!["--", &executable], @@ -91,6 +89,29 @@ impl ExecuteConfig { .map(|s| s.to_string()) .collect(); - ExecuteConfig { executable, argv } + Self { executable, argv } + } + + pub fn arg(mut self, arg: String) -> Self { + self.argv.push(arg); + self + } + + pub fn stdin(mut self, input_path: &str) -> Self { + self.argv = vec!["-i", input_path] + .iter() + .map(|s| s.to_string()) + .chain(self.argv.iter().cloned()) + .collect(); + self + } + + pub fn stdout(mut self, output_path: &str) -> Self { + self.argv = vec!["-o", output_path] + .iter() + .map(|s| s.to_string()) + .chain(self.argv.iter().cloned()) + .collect(); + self } } From 423b69613a149eda8d10795400b4e08240a35bad Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 16:40:13 +0900 Subject: [PATCH 09/22] feat: include output path in CompileResult --- src/grader/result.rs | 1 + src/isolate/compile.rs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/grader/result.rs b/src/grader/result.rs index a38561e..106f8d6 100644 --- a/src/grader/result.rs +++ b/src/grader/result.rs @@ -1,6 +1,7 @@ #[derive(Debug)] pub struct CompileResult { pub stdout: String, + pub output: String, } #[derive(Debug)] diff --git a/src/isolate/compile.rs b/src/isolate/compile.rs index f58318b..860c0d1 100644 --- a/src/isolate/compile.rs +++ b/src/isolate/compile.rs @@ -17,5 +17,8 @@ pub async fn compile( .await? .ensure_success(|out| IsolateError::CompileFailed(out.stderr.clone()))?; - Ok(CompileResult { stdout: out.stdout }) + Ok(CompileResult { + stdout: out.stdout, + output: cfg.output_path.clone(), + }) } From 863686484b9f68ba4f8b4cc98c68be9435e6d188 Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 16:45:44 +0900 Subject: [PATCH 10/22] refactor: update copy functions to use string paths instead of Path --- src/docker/cp.rs | 35 +++++++++++++++++++++---------- src/grader/docker_isolate.rs | 40 +++++++++++++++++------------------- src/grader/grader.rs | 6 +++++- src/isolate/copy.rs | 10 ++++----- src/isolate/mod.rs | 4 ++-- 5 files changed, 54 insertions(+), 41 deletions(-) diff --git a/src/docker/cp.rs b/src/docker/cp.rs index eb19915..128a3ea 100644 --- a/src/docker/cp.rs +++ b/src/docker/cp.rs @@ -1,36 +1,49 @@ -use std::path::Path; use tokio::process::Command; use crate::errors::DockerError; pub async fn to_container( container: &str, - local_src: &Path, - container_dst: &Path, + local_src: &str, + container_dst: &str, ) -> Result<(), DockerError> { - tokio::fs::metadata(local_src) - .await - .map_err(|_| DockerError::FileNotFound(local_src.display().to_string()))?; - Command::new("docker") .arg("cp") .arg(local_src) - .arg(format!("{}:{}", container, container_dst.display())) + .arg(format!("{}:{}", container, container_dst)) .output() .await .map_err(|_| DockerError::Spawn)?; + let status = Command::new("docker") + .args(["exec", container, "test", "-e", container_dst]) + .status() + .await; + + if !status.unwrap().success() { + return Err(DockerError::FileNotFound(container_dst.to_string())); + } + Ok(()) } pub async fn from_container( container: &str, - container_src: &Path, - local_dst: &Path, + container_src: &str, + local_dst: &str, ) -> Result<(), DockerError> { + let status = Command::new("docker") + .args(["exec", container, "test", "-e", container_src]) + .status() + .await; + + if !status.unwrap().success() { + return Err(DockerError::FileNotFound(container_src.to_string())); + } + Command::new("docker") .arg("cp") - .arg(format!("{}:{}", container, container_src.display())) + .arg(format!("{}:{}", container, container_src)) .arg(local_dst) .output() .await diff --git a/src/grader/docker_isolate.rs b/src/grader/docker_isolate.rs index ad6f857..a59135a 100644 --- a/src/grader/docker_isolate.rs +++ b/src/grader/docker_isolate.rs @@ -17,8 +17,6 @@ impl DockerIsolateGrader { } } -const UPLOAD_DIR: &str = "uploads"; - #[async_trait] impl Grader for DockerIsolateGrader { /// Isolate 박스를 초기화하고, UPLOAD_DIR/{box_id} 내부 파일을 복사합니다. @@ -26,41 +24,41 @@ impl Grader for DockerIsolateGrader { /// * `box_id` - The ID of the isolate box async fn init(&self, box_id: u32) -> Result<(), JudgeError> { self.isolate.init(BoxId(box_id)).await?; - self.isolate - .copy_to_box( - &BoxId(box_id), - &format!("{}/{}/.", UPLOAD_DIR, box_id).as_ref(), - ".", - ) - .await?; Ok(()) } - /// 컴파일을 수행하고, 결과물을 UPLOAD_DIR/{box_id}/{output_path}에 복사합니다. + async fn copy_to_box(&self, box_id: u32, src: &str, dst: &str) -> Result<(), JudgeError> { + tokio::fs::metadata(src) + .await + .map_err(|_| JudgeError::NotFound(src.to_string()))?; + self.isolate.copy_to_box(&BoxId(box_id), src, dst).await?; + Ok(()) + } + + async fn copy_from_box(&self, box_id: u32, src: &str, dst: &str) -> Result<(), JudgeError> { + self.isolate.copy_from_box(&BoxId(box_id), src, dst).await?; + tokio::fs::metadata(dst) + .await + .map_err(|_| JudgeError::NotFound(dst.to_string()))?; + Ok(()) + } + + /// 컴파일을 수행하고, 결과물을 UPLOAD_DIR/{box_id}에 복사합니다. /// # Arguments /// * `box_id` - The ID of the isolate box /// * `cfg` - The compiler configuration async fn compile(&self, box_id: u32, cfg: CompileConfig) -> Result { let result = self.isolate.compile(&BoxId(box_id), &cfg).await?; - self.isolate - .copy_from_box( - &BoxId(box_id), - &cfg.output_path, - &format!("{}/{}/{}", UPLOAD_DIR, box_id, cfg.output_path).as_ref(), - ) - .await?; - Ok(result) } async fn execute(&self, box_id: u32, cfg: ExecuteConfig) -> Result { let result = self.isolate.execute(&BoxId(box_id), &cfg).await?; - Ok(result) } - async fn cleanup(&self, box_id: &BoxId) -> Result<(), JudgeError> { - self.isolate.cleanup(box_id).await?; + async fn cleanup(&self, box_id: u32) -> Result<(), JudgeError> { + self.isolate.cleanup(&BoxId(box_id)).await?; Ok(()) } } diff --git a/src/grader/grader.rs b/src/grader/grader.rs index 1b52eef..7c253d3 100644 --- a/src/grader/grader.rs +++ b/src/grader/grader.rs @@ -7,9 +7,13 @@ use async_trait::async_trait; pub trait Grader: Send + Sync { async fn init(&self, box_id: u32) -> Result<(), JudgeError>; + async fn copy_to_box(&self, box_id: u32, src: &str, dst: &str) -> Result<(), JudgeError>; + + async fn copy_from_box(&self, box_id: u32, src: &str, dst: &str) -> Result<(), JudgeError>; + async fn compile(&self, box_id: u32, cfg: CompileConfig) -> Result; async fn execute(&self, box_id: u32, cfg: ExecuteConfig) -> Result; - async fn cleanup(&self, box_id: &crate::isolate::BoxId) -> Result<(), JudgeError>; + async fn cleanup(&self, box_id: u32) -> Result<(), JudgeError>; } diff --git a/src/isolate/copy.rs b/src/isolate/copy.rs index 5568bf3..7c8a820 100644 --- a/src/isolate/copy.rs +++ b/src/isolate/copy.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use crate::{docker, errors::IsolateError, isolate::BoxId}; use super::path::box_root; @@ -7,11 +5,11 @@ use super::path::box_root; pub async fn to_box( container: &str, box_id: &BoxId, - local: &Path, + local: &str, box_relative: &str, ) -> Result<(), IsolateError> { let remote = box_root(box_id).join(box_relative); - docker::cp::to_container(container, local, &remote).await?; + docker::cp::to_container(container, local, remote.to_str().unwrap()).await?; Ok(()) } @@ -19,9 +17,9 @@ pub async fn from_box( container: &str, box_id: &BoxId, box_relative: &str, - local: &Path, + local: &str, ) -> Result<(), IsolateError> { let remote = box_root(box_id).join(box_relative); - docker::cp::from_container(container, &remote, local).await?; + docker::cp::from_container(container, remote.to_str().unwrap(), local).await?; Ok(()) } diff --git a/src/isolate/mod.rs b/src/isolate/mod.rs index fcb91d8..fb0201a 100644 --- a/src/isolate/mod.rs +++ b/src/isolate/mod.rs @@ -45,7 +45,7 @@ impl Isolate { pub async fn copy_to_box( &self, box_id: &BoxId, - local: &std::path::Path, + local: &str, box_path: &str, ) -> Result<(), IsolateError> { copy::to_box(&self.container, box_id, local, box_path).await @@ -55,7 +55,7 @@ impl Isolate { &self, box_id: &BoxId, box_path: &str, - local: &std::path::Path, + local: &str, ) -> Result<(), IsolateError> { copy::from_box(&self.container, box_id, box_path, local).await } From b048d58af868bdf4bcab7046b2d6312529e45a52 Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 16:51:43 +0900 Subject: [PATCH 11/22] refactor: replace PathBuf with String for box_root function and update copy functions --- src/isolate/copy.rs | 8 ++++---- src/isolate/path.rs | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/isolate/copy.rs b/src/isolate/copy.rs index 7c8a820..2e65f7a 100644 --- a/src/isolate/copy.rs +++ b/src/isolate/copy.rs @@ -8,8 +8,8 @@ pub async fn to_box( local: &str, box_relative: &str, ) -> Result<(), IsolateError> { - let remote = box_root(box_id).join(box_relative); - docker::cp::to_container(container, local, remote.to_str().unwrap()).await?; + let remote = box_root(box_id) + "/" + box_relative; + docker::cp::to_container(container, local, &remote).await?; Ok(()) } @@ -19,7 +19,7 @@ pub async fn from_box( box_relative: &str, local: &str, ) -> Result<(), IsolateError> { - let remote = box_root(box_id).join(box_relative); - docker::cp::from_container(container, remote.to_str().unwrap(), local).await?; + let remote = box_root(box_id) + "/" + box_relative; + docker::cp::from_container(container, &remote, local).await?; Ok(()) } diff --git a/src/isolate/path.rs b/src/isolate/path.rs index 44305cc..4cfd5e8 100644 --- a/src/isolate/path.rs +++ b/src/isolate/path.rs @@ -1,6 +1,5 @@ use super::BoxId; -use std::path::PathBuf; -pub fn box_root(box_id: &BoxId) -> PathBuf { - PathBuf::from(format!("/var/local/lib/isolate/{}/box", box_id.0)) +pub fn box_root(box_id: &BoxId) -> String { + format!("/var/local/lib/isolate/{}/box", box_id.0) } From c938c503d608428797d00589143562d172f18d48 Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 17:00:49 +0900 Subject: [PATCH 12/22] feat: /judge/generator --- src/judge_manager/handlers.rs | 65 ++++++++++++++++++++++++++++++++--- src/lib.rs | 5 ++- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/src/judge_manager/handlers.rs b/src/judge_manager/handlers.rs index e6b2848..18b3853 100644 --- a/src/judge_manager/handlers.rs +++ b/src/judge_manager/handlers.rs @@ -7,11 +7,16 @@ use axum::extract::Query; use axum::{extract::Path, response::IntoResponse, Json}; use serde::Deserialize; +const UPLOAD_DIR: &str = "uploads"; + pub async fn initialize_isolate( Path(problem_id): Path, ) -> Result { let grader = DockerIsolateGrader::new("coduck-grader"); grader.init(problem_id).await?; + grader + .copy_to_box(problem_id, &format!("{}/{}/.", UPLOAD_DIR, problem_id), ".") + .await?; Ok(Json(serde_json::json!({ "message": format!("Sandbox initialized for problem ID {}", problem_id) @@ -19,13 +24,13 @@ pub async fn initialize_isolate( } #[derive(Deserialize)] -pub struct Params { +pub struct OptionLanguage { language: Option, } pub async fn compile_file( Path((problem_id, category, filename)): Path<(u32, String, String)>, - params: Query, + params: Query, ) -> Result { let language = params.language.clone().unwrap(); @@ -37,6 +42,15 @@ pub async fn compile_file( ) .await?; + let output_path = result.output.clone(); + grader + .copy_from_box( + problem_id, + &output_path, + &format!("{}/{}/{}", UPLOAD_DIR, problem_id, output_path), + ) + .await?; + Ok(Json(serde_json::json!({ "message": format!("File {} compiled successfully in category {} for problem ID {}", filename, category, problem_id), "stdout": result.stdout, @@ -45,7 +59,7 @@ pub async fn compile_file( pub async fn execute_file( Path((problem_id, category, filename)): Path<(u32, String, String)>, - params: Query, + params: Query, ) -> Result { let language = params.language.clone().unwrap(); @@ -65,9 +79,52 @@ pub async fn execute_file( }))) } +#[derive(Deserialize)] +pub struct OptionGenerateCount { + count: Option, +} + +pub async fn generator( + Path(problem_id): Path, + params: Query, +) -> Result { + let count = params.count.unwrap_or(10); + + let grader = DockerIsolateGrader::new("coduck-grader"); + grader + .compile( + problem_id, + CompileConfig::new("files", "gen.cpp", Language::Cpp), + ) + .await?; + + for i in 0..count { + grader + .execute( + problem_id, + ExecuteConfig::new("files", "gen", Language::Cpp) + .stdout(&format!("tests/input/{:02}.in", i)) + .arg(i.to_string()), + ) + .await?; + + grader + .copy_from_box( + problem_id, + &format!("tests/input/{:02}.in", i), + &format!("{}/{}/tests/input/{:02}.in", UPLOAD_DIR, problem_id, i), + ) + .await?; + } + + Ok(Json(serde_json::json!({ + "message": format!("Generated {} test cases using {} for problem ID {}", count, "gen", problem_id) + }))) +} + pub async fn cleanup_isolate(Path(problem_id): Path) -> Result { let grader = DockerIsolateGrader::new("coduck-grader"); - grader.cleanup(&crate::isolate::BoxId(problem_id)).await?; + grader.cleanup(problem_id).await?; Ok(Json(serde_json::json!({ "message": format!("Sandbox cleaned up for problem ID {}", problem_id) diff --git a/src/lib.rs b/src/lib.rs index 4c8e60b..9293970 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,9 @@ use crate::file_manager::{ delete_file, get_file, get_files_by_category, update_file_content, update_filename, upload_file, }; -use crate::judge_manager::{cleanup_isolate, compile_file, execute_file, initialize_isolate}; +use crate::judge_manager::{ + cleanup_isolate, compile_file, execute_file, generator, initialize_isolate, +}; async fn health_check() -> &'static str { "OK" @@ -42,6 +44,7 @@ pub fn build_router() -> Router { "/execute/{problem_id}/{category}/{filename}", post(execute_file), ) + .route("/generator/{problem_id}/files/gen", post(generator)) .route("/cleanup/{problem_id}", delete(cleanup_isolate)); Router::new() From 75dde35fee6678b11f57a2fbe3301bbf1b4dc833 Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 21:45:38 +0900 Subject: [PATCH 13/22] refactor: change arg method to &str instead of String --- src/grader/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/grader/config.rs b/src/grader/config.rs index c946817..7162d89 100644 --- a/src/grader/config.rs +++ b/src/grader/config.rs @@ -92,8 +92,8 @@ impl ExecuteConfig { Self { executable, argv } } - pub fn arg(mut self, arg: String) -> Self { - self.argv.push(arg); + pub fn arg(mut self, arg: &str) -> Self { + self.argv.push(arg.to_string()); self } From 7b590e8aa178162324292c5e9f2a1d319c65e717 Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 21:45:55 +0900 Subject: [PATCH 14/22] feat: /judge/checker --- src/judge_manager/handlers.rs | 76 ++++++++++++++++++++++++++++++++++- src/lib.rs | 3 +- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/judge_manager/handlers.rs b/src/judge_manager/handlers.rs index 18b3853..9ff8177 100644 --- a/src/judge_manager/handlers.rs +++ b/src/judge_manager/handlers.rs @@ -104,7 +104,7 @@ pub async fn generator( problem_id, ExecuteConfig::new("files", "gen", Language::Cpp) .stdout(&format!("tests/input/{:02}.in", i)) - .arg(i.to_string()), + .arg(&i.to_string()), ) .await?; @@ -115,6 +115,23 @@ pub async fn generator( &format!("{}/{}/tests/input/{:02}.in", UPLOAD_DIR, problem_id, i), ) .await?; + + grader + .execute( + problem_id, + ExecuteConfig::new("solutions", "mcs", Language::Cpp) + .stdin(&format!("tests/input/{:02}.in", i)) + .stdout(&format!("tests/answer/{:02}.a", i)), + ) + .await?; + + grader + .copy_from_box( + problem_id, + &format!("tests/answer/{:02}.a", i), + &format!("{}/{}/tests/answer/{:02}.a", UPLOAD_DIR, problem_id, i), + ) + .await?; } Ok(Json(serde_json::json!({ @@ -122,6 +139,63 @@ pub async fn generator( }))) } +pub async fn checker( + Path((problem_id, category, filename)): Path<(u32, String, String)>, + params: Query, +) -> Result { + let language = params.language.clone().unwrap(); + + let grader = DockerIsolateGrader::new("coduck-grader"); + let submission_result = grader + .compile( + problem_id, + CompileConfig::new(&category, &filename, language), + ) + .await? + .output + .clone(); + let executable = submission_result.split('/').last().unwrap(); + println!("submission_result: {}", submission_result); + println!("executable: {}", executable); + + grader + .compile( + problem_id, + CompileConfig::new("files", "wcmp.cpp", Language::Cpp), + ) + .await?; + println!("check compiled"); + + let mut json_result = Json(serde_json::json!({})); + for i in 0..10 { + grader + .execute( + problem_id, + ExecuteConfig::new(&category, &executable, Language::Cpp) + .stdin(&format!("tests/input/{:02}.in", i)) + .stdout(&format!("tests/output/{:02}.out", i)), + ) + .await?; + println!("executed test {}", i); + + let result = grader + .execute( + problem_id, + ExecuteConfig::new("files", "wcmp", Language::Cpp) + .arg(&format!("tests/input/{:02}.in", i)) + .arg(&format!("tests/output/{:02}.out", i)) + .arg(&format!("tests/answer/{:02}.a", i)), + ) + .await?; + + json_result.0[format!("test_{}", i)] = serde_json::json!({ + "verdict": result.stderr, + }); + } + + Ok(json_result) +} + pub async fn cleanup_isolate(Path(problem_id): Path) -> Result { let grader = DockerIsolateGrader::new("coduck-grader"); grader.cleanup(problem_id).await?; diff --git a/src/lib.rs b/src/lib.rs index 9293970..7aada5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,7 @@ use crate::file_manager::{ }; use crate::judge_manager::{ - cleanup_isolate, compile_file, execute_file, generator, initialize_isolate, + checker, cleanup_isolate, compile_file, execute_file, generator, initialize_isolate, }; async fn health_check() -> &'static str { @@ -45,6 +45,7 @@ pub fn build_router() -> Router { post(execute_file), ) .route("/generator/{problem_id}/files/gen", post(generator)) + .route("/checker/{problem_id}/{category}/{filename}", post(checker)) .route("/cleanup/{problem_id}", delete(cleanup_isolate)); Router::new() From 61cdecf64a861b5e72227c86fe65fa85b4300ded Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 21:47:48 +0900 Subject: [PATCH 15/22] refactor: remove unused error variants from JudgeError enum --- src/errors/judge.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/errors/judge.rs b/src/errors/judge.rs index 9d63f5c..a0b5370 100644 --- a/src/errors/judge.rs +++ b/src/errors/judge.rs @@ -6,18 +6,14 @@ use reqwest::StatusCode; #[derive(Debug)] pub enum JudgeError { Isolate(IsolateError), - BadRequest(String), NotFound(String), - Internal(String), } impl std::fmt::Display for JudgeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { JudgeError::Isolate(err) => write!(f, "Isolate error: {}", err), - JudgeError::BadRequest(msg) => write!(f, "Bad request: {}", msg), JudgeError::NotFound(msg) => write!(f, "Not found: {}", msg), - JudgeError::Internal(msg) => write!(f, "Internal server error: {}", msg), } } } @@ -35,18 +31,10 @@ impl IntoResponse for JudgeError { StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error" : e.to_string() })), ), - JudgeError::BadRequest(msg) => ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error" : msg })), - ), JudgeError::NotFound(msg) => ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error" : msg })), ), - JudgeError::Internal(msg) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error" : msg })), - ), } .into_response() } From 47c17be07e0406886eacaf8f990c56f2dd8a5a6f Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 21:49:42 +0900 Subject: [PATCH 16/22] refactor: remove unused fields from CompileConfig and ExecuteConfig structs --- src/grader/config.rs | 12 ++---------- src/grader/result.rs | 2 -- src/isolate/execute.rs | 2 -- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/grader/config.rs b/src/grader/config.rs index 7162d89..b67d52f 100644 --- a/src/grader/config.rs +++ b/src/grader/config.rs @@ -2,17 +2,13 @@ use crate::file_manager::Language; #[derive(Debug)] pub struct CompileConfig { - pub source_path: String, pub output_path: String, pub argv: Vec, } #[derive(Debug)] pub struct ExecuteConfig { - pub executable: String, pub argv: Vec, - // pub time_limit_ms: u64, - // pub memory_limit_kb: u64, } impl CompileConfig { @@ -58,11 +54,7 @@ impl CompileConfig { .map(|s| s.to_string()) .collect(); - CompileConfig { - source_path, - output_path, - argv, - } + CompileConfig { output_path, argv } } } @@ -89,7 +81,7 @@ impl ExecuteConfig { .map(|s| s.to_string()) .collect(); - Self { executable, argv } + Self { argv } } pub fn arg(mut self, arg: &str) -> Self { diff --git a/src/grader/result.rs b/src/grader/result.rs index 106f8d6..ec177d5 100644 --- a/src/grader/result.rs +++ b/src/grader/result.rs @@ -9,6 +9,4 @@ pub struct ExecuteResult { pub exit_code: i32, pub stdout: String, pub stderr: String, - pub time_ms: u64, - pub memory_kb: u64, } diff --git a/src/isolate/execute.rs b/src/isolate/execute.rs index d6a09c5..6894922 100644 --- a/src/isolate/execute.rs +++ b/src/isolate/execute.rs @@ -21,7 +21,5 @@ pub async fn execute( exit_code: out.status.code().unwrap_or(-1), stdout: out.stdout, stderr: out.stderr, - time_ms: 0, - memory_kb: 0, }) } From bd0387e6533f53a42157ea082a8e6f027563eaa8 Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 21:56:33 +0900 Subject: [PATCH 17/22] refactor: remove unused stdout field from compilation response --- src/judge_manager/handlers.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/judge_manager/handlers.rs b/src/judge_manager/handlers.rs index 9ff8177..7f0045c 100644 --- a/src/judge_manager/handlers.rs +++ b/src/judge_manager/handlers.rs @@ -53,7 +53,6 @@ pub async fn compile_file( Ok(Json(serde_json::json!({ "message": format!("File {} compiled successfully in category {} for problem ID {}", filename, category, problem_id), - "stdout": result.stdout, }))) } From 2a88bdc45df46676a339310a24b8e1ea34ba569c Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 22:15:48 +0900 Subject: [PATCH 18/22] refactor: remove unused stdout field from CompileResult struct --- src/grader/result.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/grader/result.rs b/src/grader/result.rs index ec177d5..e2bc2c1 100644 --- a/src/grader/result.rs +++ b/src/grader/result.rs @@ -1,6 +1,5 @@ #[derive(Debug)] pub struct CompileResult { - pub stdout: String, pub output: String, } From f30bff87b3d3a5b2b88a989b4d8395ae48e66b4d Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 22:16:34 +0900 Subject: [PATCH 19/22] refactor: remove unused stdout field from CompileResult in compile.rs --- src/isolate/compile.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/isolate/compile.rs b/src/isolate/compile.rs index 860c0d1..7cc0958 100644 --- a/src/isolate/compile.rs +++ b/src/isolate/compile.rs @@ -13,12 +13,11 @@ pub async fn compile( let argv: Vec<&str> = cfg.argv.iter().map(String::as_str).collect(); args.extend(argv); - let out = exec(container, &args) + exec(container, &args) .await? .ensure_success(|out| IsolateError::CompileFailed(out.stderr.clone()))?; Ok(CompileResult { - stdout: out.stdout, output: cfg.output_path.clone(), }) } From 577c92cf82dcc9bce3a5e5cdc295e97d863e5725 Mon Sep 17 00:00:00 2001 From: w8385 Date: Mon, 2 Feb 2026 22:56:52 +0900 Subject: [PATCH 20/22] refactor: simplify docker cp operations by extracting helper functions --- src/docker/cp.rs | 60 ++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/src/docker/cp.rs b/src/docker/cp.rs index 128a3ea..1c2457a 100644 --- a/src/docker/cp.rs +++ b/src/docker/cp.rs @@ -2,28 +2,39 @@ use tokio::process::Command; use crate::errors::DockerError; -pub async fn to_container( - container: &str, - local_src: &str, - container_dst: &str, -) -> Result<(), DockerError> { +async fn ensure_exists_in_container(container: &str, path: &str) -> Result<(), DockerError> { + let status = Command::new("docker") + .args(["exec", container, "test", "-e", path]) + .status() + .await + .map_err(|_| DockerError::Spawn)?; + + if !status.success() { + return Err(DockerError::FileNotFound(path.to_string())); + } + + Ok(()) +} + +async fn cp_container(src: &str, dst: &str) -> Result<(), DockerError> { Command::new("docker") .arg("cp") - .arg(local_src) - .arg(format!("{}:{}", container, container_dst)) + .arg(src) + .arg(dst) .output() .await .map_err(|_| DockerError::Spawn)?; - let status = Command::new("docker") - .args(["exec", container, "test", "-e", container_dst]) - .status() - .await; - - if !status.unwrap().success() { - return Err(DockerError::FileNotFound(container_dst.to_string())); - } + Ok(()) +} +pub async fn to_container( + container: &str, + local_src: &str, + container_dst: &str, +) -> Result<(), DockerError> { + cp_container(local_src, &format!("{}:{}", container, container_dst)).await?; + ensure_exists_in_container(container, container_dst).await?; Ok(()) } @@ -32,22 +43,7 @@ pub async fn from_container( container_src: &str, local_dst: &str, ) -> Result<(), DockerError> { - let status = Command::new("docker") - .args(["exec", container, "test", "-e", container_src]) - .status() - .await; - - if !status.unwrap().success() { - return Err(DockerError::FileNotFound(container_src.to_string())); - } - - Command::new("docker") - .arg("cp") - .arg(format!("{}:{}", container, container_src)) - .arg(local_dst) - .output() - .await - .map_err(|_| DockerError::Spawn)?; - + ensure_exists_in_container(container, container_src).await?; + cp_container(&format!("{}:{}", container, container_src), local_dst).await?; Ok(()) } From c57342942f408e7003406f082644d6d953cb3af3 Mon Sep 17 00:00:00 2001 From: w8385 Date: Tue, 3 Feb 2026 15:04:20 +0900 Subject: [PATCH 21/22] refactor: clean up Dockerfile by removing apt cache after installation --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 971e2f8..0ddd1fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,8 @@ RUN apt-get install -y --no-install-recommends \ python3 pypy3 \ openjdk-8-jdk-headless \ git make pkg-config libcap-dev libsystemd-dev asciidoc \ - curl jq + curl jq \ + && rm -rf /var/lib/apt/lists/* # config isolate RUN git clone https://github.com/ioi/isolate.git From 75ba7b4adde869cc60fc9c48e794b0e7e0e03c1c Mon Sep 17 00:00:00 2001 From: w8385 Date: Tue, 3 Feb 2026 15:09:57 +0900 Subject: [PATCH 22/22] refactor: handle Docker command spawn errors with proper error mapping --- src/docker/exec.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/docker/exec.rs b/src/docker/exec.rs index fd99654..983f6a5 100644 --- a/src/docker/exec.rs +++ b/src/docker/exec.rs @@ -28,8 +28,7 @@ pub async fn exec(container: &str, args: &[&str]) -> Result