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..0ddd1fc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +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 \ + && rm -rf /var/lib/apt/lists/* + +# 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..1c2457a --- /dev/null +++ b/src/docker/cp.rs @@ -0,0 +1,49 @@ +use tokio::process::Command; + +use crate::errors::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(src) + .arg(dst) + .output() + .await + .map_err(|_| DockerError::Spawn)?; + + 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(()) +} + +pub async fn from_container( + container: &str, + container_src: &str, + local_dst: &str, +) -> Result<(), DockerError> { + ensure_exists_in_container(container, container_src).await?; + cp_container(&format!("{}:{}", container, container_src), local_dst).await?; + Ok(()) +} diff --git a/src/docker/exec.rs b/src/docker/exec.rs new file mode 100644 index 0000000..983f6a5 --- /dev/null +++ b/src/docker/exec.rs @@ -0,0 +1,37 @@ +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 + .map_err(|_| DockerError::Spawn)?; + 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..9e75547 --- /dev/null +++ b/src/errors/docker.rs @@ -0,0 +1,23 @@ +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}"), + } + } +} diff --git a/src/errors/isolate.rs b/src/errors/isolate.rs new file mode 100644 index 0000000..5cd4ca2 --- /dev/null +++ b/src/errors/isolate.rs @@ -0,0 +1,37 @@ +use crate::errors::DockerError; +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) + } +} diff --git a/src/errors/judge.rs b/src/errors/judge.rs new file mode 100644 index 0000000..a0b5370 --- /dev/null +++ b/src/errors/judge.rs @@ -0,0 +1,41 @@ +use crate::errors::IsolateError; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use reqwest::StatusCode; + +#[derive(Debug)] +pub enum JudgeError { + Isolate(IsolateError), + NotFound(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::NotFound(msg) => write!(f, "Not found: {}", 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::NotFound(msg) => ( + StatusCode::NOT_FOUND, + 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/config.rs b/src/grader/config.rs new file mode 100644 index 0000000..b67d52f --- /dev/null +++ b/src/grader/config.rs @@ -0,0 +1,109 @@ +use crate::file_manager::Language; + +#[derive(Debug)] +pub struct CompileConfig { + pub output_path: String, + pub argv: Vec, +} + +#[derive(Debug)] +pub struct ExecuteConfig { + pub argv: Vec, +} + +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 { output_path, argv } + } +} + +impl ExecuteConfig { + pub fn new(category: &str, filename: &str, language: Language) -> Self { + 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", + "-cp", + &category, + "-Xms1024m", + "-Xmx1920m", + "-Xss512m", + "Main", + ], + _ => vec![], + } + .iter() + .map(|s| s.to_string()) + .collect(); + + Self { argv } + } + + pub fn arg(mut self, arg: &str) -> Self { + self.argv.push(arg.to_string()); + 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 + } +} diff --git a/src/grader/docker_isolate.rs b/src/grader/docker_isolate.rs new file mode 100644 index 0000000..a59135a --- /dev/null +++ b/src/grader/docker_isolate.rs @@ -0,0 +1,64 @@ +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 async_trait::async_trait; + +pub struct DockerIsolateGrader { + isolate: Isolate, +} + +impl DockerIsolateGrader { + pub fn new(container: impl Into) -> Self { + Self { + isolate: Isolate::new(container), + } + } +} + +#[async_trait] +impl Grader for DockerIsolateGrader { + /// Isolate 박스를 초기화하고, UPLOAD_DIR/{box_id} 내부 파일을 복사합니다. + /// # Arguments + /// * `box_id` - The ID of the isolate box + async fn init(&self, box_id: u32) -> Result<(), JudgeError> { + self.isolate.init(BoxId(box_id)).await?; + Ok(()) + } + + 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?; + 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: u32) -> Result<(), JudgeError> { + self.isolate.cleanup(&BoxId(box_id)).await?; + Ok(()) + } +} diff --git a/src/grader/grader.rs b/src/grader/grader.rs new file mode 100644 index 0000000..7c253d3 --- /dev/null +++ b/src/grader/grader.rs @@ -0,0 +1,19 @@ +use crate::errors::JudgeError; +use crate::grader::config::{CompileConfig, ExecuteConfig}; +use crate::grader::result::{CompileResult, ExecuteResult}; +use async_trait::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: u32) -> Result<(), JudgeError>; +} diff --git a/src/grader/mod.rs b/src/grader/mod.rs new file mode 100644 index 0000000..01696fc --- /dev/null +++ b/src/grader/mod.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod docker_isolate; +pub mod grader; +pub mod result; diff --git a/src/grader/result.rs b/src/grader/result.rs new file mode 100644 index 0000000..e2bc2c1 --- /dev/null +++ b/src/grader/result.rs @@ -0,0 +1,11 @@ +#[derive(Debug)] +pub struct CompileResult { + pub output: String, +} + +#[derive(Debug)] +pub struct ExecuteResult { + pub exit_code: i32, + pub stdout: String, + pub stderr: String, +} 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..7cc0958 --- /dev/null +++ b/src/isolate/compile.rs @@ -0,0 +1,23 @@ +use crate::grader::config::CompileConfig; +use crate::grader::result::CompileResult; +use crate::{docker::exec::exec, errors::IsolateError, isolate::BoxId}; + +pub async fn compile( + container: &str, + box_id: &BoxId, + cfg: &CompileConfig, +) -> Result { + let box_id = format!("--box-id={}", box_id); + let mut args = vec!["isolate", "--run", "--full-env", &box_id]; + + let argv: Vec<&str> = cfg.argv.iter().map(String::as_str).collect(); + args.extend(argv); + + exec(container, &args) + .await? + .ensure_success(|out| IsolateError::CompileFailed(out.stderr.clone()))?; + + Ok(CompileResult { + output: cfg.output_path.clone(), + }) +} diff --git a/src/isolate/copy.rs b/src/isolate/copy.rs new file mode 100644 index 0000000..2e65f7a --- /dev/null +++ b/src/isolate/copy.rs @@ -0,0 +1,25 @@ +use crate::{docker, errors::IsolateError, isolate::BoxId}; + +use super::path::box_root; + +pub async fn to_box( + container: &str, + box_id: &BoxId, + local: &str, + box_relative: &str, +) -> Result<(), IsolateError> { + let remote = box_root(box_id) + "/" + 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: &str, +) -> Result<(), IsolateError> { + let remote = box_root(box_id) + "/" + 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..6894922 --- /dev/null +++ b/src/isolate/execute.rs @@ -0,0 +1,25 @@ +use crate::grader::config::ExecuteConfig; +use crate::grader::result::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", "--full-env", &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, + }) +} 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..fb0201a --- /dev/null +++ b/src/isolate/mod.rs @@ -0,0 +1,66 @@ +mod box_id; +mod cleanup; +mod compile; +mod copy; +mod execute; +mod init; +mod path; + +use crate::errors::IsolateError; +use crate::grader::config::{CompileConfig, ExecuteConfig}; +use crate::grader::result::{CompileResult, 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: &str, + 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: &str, + ) -> 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..4cfd5e8 --- /dev/null +++ b/src/isolate/path.rs @@ -0,0 +1,5 @@ +use super::BoxId; + +pub fn box_root(box_id: &BoxId) -> String { + format!("/var/local/lib/isolate/{}/box", box_id.0) +} diff --git a/src/judge_manager/handlers.rs b/src/judge_manager/handlers.rs new file mode 100644 index 0000000..7f0045c --- /dev/null +++ b/src/judge_manager/handlers.rs @@ -0,0 +1,205 @@ +use crate::errors::JudgeError; +use crate::file_manager::Language; +use crate::grader::config::{CompileConfig, ExecuteConfig}; +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; + +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) + }))) +} + +#[derive(Deserialize)] +pub struct OptionLanguage { + language: Option, +} + +pub async fn compile_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 + .compile( + problem_id, + CompileConfig::new(&category, &filename, language), + ) + .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), + }))) +} + +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, + }))) +} + +#[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?; + + 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!({ + "message": format!("Generated {} test cases using {} for problem ID {}", count, "gen", problem_id) + }))) +} + +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?; + + Ok(Json(serde_json::json!({ + "message": format!("Sandbox cleaned up for problem ID {}", problem_id) + }))) +} 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..7aada5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,9 @@ +mod docker; mod errors; pub mod file_manager; +mod grader; +mod isolate; +pub mod judge_manager; use axum::{ routing::{delete, get, post, put}, @@ -10,6 +14,10 @@ use crate::file_manager::{ delete_file, get_file, get_files_by_category, update_file_content, update_filename, upload_file, }; +use crate::judge_manager::{ + checker, cleanup_isolate, compile_file, execute_file, generator, initialize_isolate, +}; + async fn health_check() -> &'static str { "OK" } @@ -26,7 +34,22 @@ 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), + ) + .route( + "/execute/{problem_id}/{category}/{filename}", + 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() .route("/health", get(health_check)) .nest("/problems", problems_router) + .nest("/judge", judge_router) }