From 63b1734f5ea4049dd59a529053b6921608b14d9d Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 24 Dec 2025 01:58:40 +0700 Subject: [PATCH 01/81] feat(language): add templating --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/language/cpp.rs | 6 ++---- src/language/java.rs | 5 +---- src/language/javascript.rs | 5 +---- src/language/mod.rs | 39 ++++++++++++++++++++++++++++++++++++-- src/language/python.rs | 5 +---- src/language/rust.rs | 4 ++-- src/language/typescript.rs | 5 +---- 9 files changed, 53 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a03ea60..380d6e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -511,6 +511,7 @@ dependencies = [ "anyhow", "byte-unit", "cgroups-rs", + "strfmt", "uuid", ] @@ -959,6 +960,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strfmt" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fdc163db75f7b5ffa3daf0c5a7136fb0d4b2f35523cd1769da05e034159feb" + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index 7870cf0..d14d73a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,5 @@ A code runner library for online judge system anyhow = "1.0.100" byte-unit = "5.2.0" cgroups-rs = "0.5.0" +strfmt = "0.2.5" uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } diff --git a/src/language/cpp.rs b/src/language/cpp.rs index 8f69b4e..d945113 100644 --- a/src/language/cpp.rs +++ b/src/language/cpp.rs @@ -1,6 +1,4 @@ use super::Language; -pub const CPP: Language = Language { - compile_args: Some(&["g++", "-o", "main", "main.cpp"]), - run_args: &["./main"], -}; +pub const CPP: Language = + Language::new(Some(&["g++", "-o", "{main}", "{main}.cpp"]), &["./{main}"]); diff --git a/src/language/java.rs b/src/language/java.rs index 215024b..87b4a05 100644 --- a/src/language/java.rs +++ b/src/language/java.rs @@ -1,6 +1,3 @@ use super::Language; -pub const JAVA: Language = Language { - compile_args: Some(&["javac", "Main.java"]), - run_args: &["java", "Main"], -}; +pub const JAVA: Language = Language::new(Some(&["javac", "{main}.java"]), &["java", "{main}"]); diff --git a/src/language/javascript.rs b/src/language/javascript.rs index 995c3e4..dd2afcb 100644 --- a/src/language/javascript.rs +++ b/src/language/javascript.rs @@ -1,6 +1,3 @@ use super::Language; -pub const JAVASCRIPT: Language = Language { - compile_args: None, - run_args: &["bun", "run", "main.js"], -}; +pub const JAVASCRIPT: Language = Language::new(None, &["bun", "run", "{main}.js"]); diff --git a/src/language/mod.rs b/src/language/mod.rs index 87f8adf..e568c49 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -5,6 +5,11 @@ mod python; mod rust; mod typescript; +use std::collections::HashMap; + +use anyhow::Result; +use strfmt::Format; + pub use cpp::CPP; pub use java::JAVA; pub use javascript::JAVASCRIPT; @@ -13,6 +18,36 @@ pub use rust::RUST; pub use typescript::TYPESCRIPT; pub struct Language<'a> { - pub compile_args: Option<&'a [&'a str]>, - pub run_args: &'a [&'a str], + compile_args: Option<&'a [&'a str]>, + run_args: &'a [&'a str], +} + +impl Language<'_> { + #[inline] + pub const fn new<'a>( + compile_args: Option<&'a [&'a str]>, + run_args: &'a [&'a str], + ) -> Language<'a> { + Language { + compile_args, + run_args, + } + } + + pub fn compile_args(&self, main: &str) -> Option>> { + self.compile_args.map(|raw| format(raw, main)) + } + + pub fn run_args(&self, main: &str) -> Result> { + format(self.run_args, main) + } +} + +fn format(args: &[&str], main: &str) -> Result> { + const MAIN: &str = "main"; + let vars = HashMap::from_iter([(MAIN.to_string(), main)]); + + args.iter() + .map(|&x| x.format(&vars).map_err(anyhow::Error::from)) + .collect() } diff --git a/src/language/python.rs b/src/language/python.rs index 9d9e181..03bd628 100644 --- a/src/language/python.rs +++ b/src/language/python.rs @@ -1,6 +1,3 @@ use super::Language; -pub const PYTHON: Language = Language { - compile_args: None, - run_args: &["python", "main.py"], -}; +pub const PYTHON: Language = Language::new(None, &["python", "{main}.py"]); diff --git a/src/language/rust.rs b/src/language/rust.rs index 32a20a6..19bf06c 100644 --- a/src/language/rust.rs +++ b/src/language/rust.rs @@ -1,6 +1,6 @@ use super::Language; pub const RUST: Language = Language { - compile_args: Some(&["rustc", "-O", "main.rs"]), - run_args: &["./main"], + compile_args: Some(&["rustc", "-O", "{main}.rs"]), + run_args: &["./{main}"], }; diff --git a/src/language/typescript.rs b/src/language/typescript.rs index b2c1fbd..ebc1f1d 100644 --- a/src/language/typescript.rs +++ b/src/language/typescript.rs @@ -1,6 +1,3 @@ use super::Language; -pub const TYPESCRIPT: Language = Language { - compile_args: None, - run_args: &["bun", "run", "main.ts"], -}; +pub const TYPESCRIPT: Language = Language::new(None, &["bun", "run", "{main}.ts"]); From ec740c90e54e5df133c486a09c801cda17744c4c Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 24 Dec 2025 02:23:25 +0700 Subject: [PATCH 02/81] feat(language): add extension --- src/language/cpp.rs | 7 +++++-- src/language/java.rs | 3 ++- src/language/javascript.rs | 2 +- src/language/mod.rs | 3 +++ src/language/python.rs | 2 +- src/language/rust.rs | 5 +---- src/language/typescript.rs | 2 +- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/language/cpp.rs b/src/language/cpp.rs index d945113..49f552b 100644 --- a/src/language/cpp.rs +++ b/src/language/cpp.rs @@ -1,4 +1,7 @@ use super::Language; -pub const CPP: Language = - Language::new(Some(&["g++", "-o", "{main}", "{main}.cpp"]), &["./{main}"]); +pub const CPP: Language = Language::new( + Some(&["g++", "-o", "{main}", "{main}.cpp"]), + &["./{main}"], + "cpp", +); diff --git a/src/language/java.rs b/src/language/java.rs index 87b4a05..205fc35 100644 --- a/src/language/java.rs +++ b/src/language/java.rs @@ -1,3 +1,4 @@ use super::Language; -pub const JAVA: Language = Language::new(Some(&["javac", "{main}.java"]), &["java", "{main}"]); +pub const JAVA: Language = + Language::new(Some(&["javac", "{main}.java"]), &["java", "{main}"], "java"); diff --git a/src/language/javascript.rs b/src/language/javascript.rs index dd2afcb..d8cfe9e 100644 --- a/src/language/javascript.rs +++ b/src/language/javascript.rs @@ -1,3 +1,3 @@ use super::Language; -pub const JAVASCRIPT: Language = Language::new(None, &["bun", "run", "{main}.js"]); +pub const JAVASCRIPT: Language = Language::new(None, &["bun", "run", "{main}.js"], "js"); diff --git a/src/language/mod.rs b/src/language/mod.rs index e568c49..a366b03 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -20,6 +20,7 @@ pub use typescript::TYPESCRIPT; pub struct Language<'a> { compile_args: Option<&'a [&'a str]>, run_args: &'a [&'a str], + pub extension: &'a str, } impl Language<'_> { @@ -27,10 +28,12 @@ impl Language<'_> { pub const fn new<'a>( compile_args: Option<&'a [&'a str]>, run_args: &'a [&'a str], + extension: &'a str, ) -> Language<'a> { Language { compile_args, run_args, + extension, } } diff --git a/src/language/python.rs b/src/language/python.rs index 03bd628..ea319f0 100644 --- a/src/language/python.rs +++ b/src/language/python.rs @@ -1,3 +1,3 @@ use super::Language; -pub const PYTHON: Language = Language::new(None, &["python", "{main}.py"]); +pub const PYTHON: Language = Language::new(None, &["python", "{main}.py"], "py"); diff --git a/src/language/rust.rs b/src/language/rust.rs index 19bf06c..8d737a9 100644 --- a/src/language/rust.rs +++ b/src/language/rust.rs @@ -1,6 +1,3 @@ use super::Language; -pub const RUST: Language = Language { - compile_args: Some(&["rustc", "-O", "{main}.rs"]), - run_args: &["./{main}"], -}; +pub const RUST: Language = Language::new(Some(&["rustc", "-O", "{main}.rs"]), &["./{main}"], "rs"); diff --git a/src/language/typescript.rs b/src/language/typescript.rs index ebc1f1d..83c6d6b 100644 --- a/src/language/typescript.rs +++ b/src/language/typescript.rs @@ -1,3 +1,3 @@ use super::Language; -pub const TYPESCRIPT: Language = Language::new(None, &["bun", "run", "{main}.ts"]); +pub const TYPESCRIPT: Language = Language::new(None, &["bun", "run", "{main}.ts"], "ts"); From e73458ade3c1e587108db871a26ba4d0f6ae05dc Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 24 Dec 2025 03:02:11 +0700 Subject: [PATCH 03/81] feat(language): define macro rule for language --- Cargo.lock | 47 ++++++++++++++++++-- Cargo.toml | 3 +- src/language/cpp.rs | 10 ++--- src/language/java.rs | 5 +-- src/language/javascript.rs | 4 +- src/language/mod.rs | 88 ++++++++++++++++++++++++-------------- src/language/python.rs | 4 +- src/language/rust.rs | 4 +- src/language/typescript.rs | 4 +- src/lib.rs | 1 + 10 files changed, 116 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 380d6e7..4a3b889 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -315,6 +315,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -511,7 +531,8 @@ dependencies = [ "anyhow", "byte-unit", "cgroups-rs", - "strfmt", + "const_format", + "state-shift", "uuid", ] @@ -954,6 +975,18 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "state-shift" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b84d514d82ae2456c0ecc1fdf84e77368ff158e7a9dcdfc135669ba3ec57fdf" +dependencies = [ + "proc-macro2", + "quote", + "stringcase", + "syn 2.0.111", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -961,10 +994,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "strfmt" -version = "0.2.5" +name = "stringcase" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29fdc163db75f7b5ffa3daf0c5a7136fb0d4b2f35523cd1769da05e034159feb" +checksum = "72abeda133c49d7bddece6c154728f83eec8172380c80ab7096da9487e20d27c" [[package]] name = "syn" @@ -1120,6 +1153,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8-width" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index d14d73a..325a8f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,5 +14,6 @@ A code runner library for online judge system anyhow = "1.0.100" byte-unit = "5.2.0" cgroups-rs = "0.5.0" -strfmt = "0.2.5" +const_format = "0.2.35" +state-shift = "2.1.1" uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } diff --git a/src/language/cpp.rs b/src/language/cpp.rs index 49f552b..8cef992 100644 --- a/src/language/cpp.rs +++ b/src/language/cpp.rs @@ -1,7 +1,7 @@ -use super::Language; +use crate::{Language, language}; -pub const CPP: Language = Language::new( - Some(&["g++", "-o", "{main}", "{main}.cpp"]), - &["./{main}"], - "cpp", +pub const CPP: Language = language!( + ["g++", "-o", "{main}", "{main}.cpp"], + ["./{main}"], + "cpp" ); diff --git a/src/language/java.rs b/src/language/java.rs index 205fc35..57abca5 100644 --- a/src/language/java.rs +++ b/src/language/java.rs @@ -1,4 +1,3 @@ -use super::Language; +use crate::{Language, language}; -pub const JAVA: Language = - Language::new(Some(&["javac", "{main}.java"]), &["java", "{main}"], "java"); +pub const JAVA: Language = language!(["javac", "{main}.java"], ["java", "{main}"], "java"); diff --git a/src/language/javascript.rs b/src/language/javascript.rs index d8cfe9e..7d8165f 100644 --- a/src/language/javascript.rs +++ b/src/language/javascript.rs @@ -1,3 +1,3 @@ -use super::Language; +use crate::{Language, language}; -pub const JAVASCRIPT: Language = Language::new(None, &["bun", "run", "{main}.js"], "js"); +pub const JAVASCRIPT: Language = language!(["bun", "run", "{main}.js"], "js"); diff --git a/src/language/mod.rs b/src/language/mod.rs index a366b03..a5e76f1 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -5,11 +5,6 @@ mod python; mod rust; mod typescript; -use std::collections::HashMap; - -use anyhow::Result; -use strfmt::Format; - pub use cpp::CPP; pub use java::JAVA; pub use javascript::JAVASCRIPT; @@ -18,39 +13,66 @@ pub use rust::RUST; pub use typescript::TYPESCRIPT; pub struct Language<'a> { - compile_args: Option<&'a [&'a str]>, - run_args: &'a [&'a str], + pub checker_compile_args: Option<&'a [&'a str]>, + pub checker_run_args: &'a [&'a str], + + pub submission_compile_args: Option<&'a [&'a str]>, + pub submission_run_args: &'a [&'a str], + pub extension: &'a str, } -impl Language<'_> { - #[inline] - pub const fn new<'a>( - compile_args: Option<&'a [&'a str]>, - run_args: &'a [&'a str], - extension: &'a str, - ) -> Language<'a> { +#[macro_export] +macro_rules! language { + ([ $( $r:expr ),* $(,)? ], $e:expr) => {{ + let checker_run_args: &[&str] = &[ + $( + const_format::str_replace!($r, "{main}", "checker") + ),* + ]; + let submission_run_args: &[&str] = &[ + $( + const_format::str_replace!($r, "{main}", "main") + ),* + ]; + Language { - compile_args, - run_args, - extension, + checker_compile_args: None, + checker_run_args, + submission_compile_args: None, + submission_run_args, + extension: $e, } - } + }}; - pub fn compile_args(&self, main: &str) -> Option>> { - self.compile_args.map(|raw| format(raw, main)) - } + ([ $( $c:expr ),* $(,)? ], [ $( $r:expr ),* $(,)? ], $e:expr) => {{ + let checker_compile_args: &[&str] = &[ + $( + const_format::str_replace!($c, "{main}", "checker") + ),* + ]; + let checker_run_args: &[&str] = &[ + $( + const_format::str_replace!($r, "{main}", "checker") + ),* + ]; + let submission_compile_args: &[&str] = &[ + $( + const_format::str_replace!($c, "{main}", "main") + ),* + ]; + let submission_run_args: &[&str] = &[ + $( + const_format::str_replace!($r, "{main}", "main") + ),* + ]; - pub fn run_args(&self, main: &str) -> Result> { - format(self.run_args, main) - } -} - -fn format(args: &[&str], main: &str) -> Result> { - const MAIN: &str = "main"; - let vars = HashMap::from_iter([(MAIN.to_string(), main)]); - - args.iter() - .map(|&x| x.format(&vars).map_err(anyhow::Error::from)) - .collect() + Language { + checker_compile_args: Some(checker_compile_args), + checker_run_args, + submission_compile_args: Some(submission_compile_args), + submission_run_args, + extension: $e, + } + }}; } diff --git a/src/language/python.rs b/src/language/python.rs index ea319f0..8fe5caf 100644 --- a/src/language/python.rs +++ b/src/language/python.rs @@ -1,3 +1,3 @@ -use super::Language; +use crate::{Language, language}; -pub const PYTHON: Language = Language::new(None, &["python", "{main}.py"], "py"); +pub const PYTHON: Language = language!(["python", "{main}.py"], "py"); diff --git a/src/language/rust.rs b/src/language/rust.rs index 8d737a9..6936ec1 100644 --- a/src/language/rust.rs +++ b/src/language/rust.rs @@ -1,3 +1,3 @@ -use super::Language; +use crate::{Language, language}; -pub const RUST: Language = Language::new(Some(&["rustc", "-O", "{main}.rs"]), &["./{main}"], "rs"); +pub const RUST: Language = language!(["rustc", "-O", "{main}.rs"], ["./{main}"], "rs"); diff --git a/src/language/typescript.rs b/src/language/typescript.rs index 83c6d6b..6ed0531 100644 --- a/src/language/typescript.rs +++ b/src/language/typescript.rs @@ -1,3 +1,3 @@ -use super::Language; +use crate::{Language, language}; -pub const TYPESCRIPT: Language = Language::new(None, &["bun", "run", "{main}.ts"], "ts"); +pub const TYPESCRIPT: Language = language!(["bun", "run", "{main}.ts"], "ts"); diff --git a/src/lib.rs b/src/lib.rs index 7ee60ac..fd40615 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ mod verdict; pub use sandbox::*; pub use verdict::*; +pub use language::Language; #[cfg(test)] mod test { From 5bf16897035454822d982cc7ceb4863d1e1675d0 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 24 Dec 2025 15:11:12 +0700 Subject: [PATCH 04/81] feat(language): use plain replace at runtime for replacing main --- Cargo.lock | 27 ----------- Cargo.toml | 1 - src/language/cpp.rs | 12 ++--- src/language/java.rs | 8 +++- src/language/javascript.rs | 8 +++- src/language/mod.rs | 91 +++++++++++++------------------------- src/language/python.rs | 8 +++- src/language/rust.rs | 8 +++- src/language/typescript.rs | 8 +++- 9 files changed, 66 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a3b889..f750e65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -315,26 +315,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "const_format" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -531,7 +511,6 @@ dependencies = [ "anyhow", "byte-unit", "cgroups-rs", - "const_format", "state-shift", "uuid", ] @@ -1153,12 +1132,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "utf8-width" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 325a8f4..1550558 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,5 @@ A code runner library for online judge system anyhow = "1.0.100" byte-unit = "5.2.0" cgroups-rs = "0.5.0" -const_format = "0.2.35" state-shift = "2.1.1" uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } diff --git a/src/language/cpp.rs b/src/language/cpp.rs index 8cef992..c08d72f 100644 --- a/src/language/cpp.rs +++ b/src/language/cpp.rs @@ -1,7 +1,7 @@ -use crate::{Language, language}; +use crate::Language; -pub const CPP: Language = language!( - ["g++", "-o", "{main}", "{main}.cpp"], - ["./{main}"], - "cpp" -); +pub const CPP: Language = Language { + compile_args: Some("g++ -o {main} {main}.cpp"), + run_args: "./{main}", + extension: "cpp", +}; diff --git a/src/language/java.rs b/src/language/java.rs index 57abca5..6571b43 100644 --- a/src/language/java.rs +++ b/src/language/java.rs @@ -1,3 +1,7 @@ -use crate::{Language, language}; +use crate::Language; -pub const JAVA: Language = language!(["javac", "{main}.java"], ["java", "{main}"], "java"); +pub const JAVA: Language = Language { + compile_args: Some("javac {main}.java"), + run_args: "java {main}", + extension: "java", +}; diff --git a/src/language/javascript.rs b/src/language/javascript.rs index 7d8165f..b12b4bd 100644 --- a/src/language/javascript.rs +++ b/src/language/javascript.rs @@ -1,3 +1,7 @@ -use crate::{Language, language}; +use crate::Language; -pub const JAVASCRIPT: Language = language!(["bun", "run", "{main}.js"], "js"); +pub const JAVASCRIPT: Language = Language { + compile_args: None, + run_args: "bun run {main}.js", + extension: "js", +}; diff --git a/src/language/mod.rs b/src/language/mod.rs index a5e76f1..e713bf2 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -5,6 +5,8 @@ mod python; mod rust; mod typescript; +use std::process::Command; + pub use cpp::CPP; pub use java::JAVA; pub use javascript::JAVASCRIPT; @@ -12,67 +14,34 @@ pub use python::PYTHON; pub use rust::RUST; pub use typescript::TYPESCRIPT; -pub struct Language<'a> { - pub checker_compile_args: Option<&'a [&'a str]>, - pub checker_run_args: &'a [&'a str], - - pub submission_compile_args: Option<&'a [&'a str]>, - pub submission_run_args: &'a [&'a str], - - pub extension: &'a str, +pub struct Language { + pub compile_args: Option<&'static str>, + pub run_args: &'static str, + pub extension: &'static str, } -#[macro_export] -macro_rules! language { - ([ $( $r:expr ),* $(,)? ], $e:expr) => {{ - let checker_run_args: &[&str] = &[ - $( - const_format::str_replace!($r, "{main}", "checker") - ),* - ]; - let submission_run_args: &[&str] = &[ - $( - const_format::str_replace!($r, "{main}", "main") - ),* - ]; - - Language { - checker_compile_args: None, - checker_run_args, - submission_compile_args: None, - submission_run_args, - extension: $e, - } - }}; - - ([ $( $c:expr ),* $(,)? ], [ $( $r:expr ),* $(,)? ], $e:expr) => {{ - let checker_compile_args: &[&str] = &[ - $( - const_format::str_replace!($c, "{main}", "checker") - ),* - ]; - let checker_run_args: &[&str] = &[ - $( - const_format::str_replace!($r, "{main}", "checker") - ),* - ]; - let submission_compile_args: &[&str] = &[ - $( - const_format::str_replace!($c, "{main}", "main") - ),* - ]; - let submission_run_args: &[&str] = &[ - $( - const_format::str_replace!($r, "{main}", "main") - ),* - ]; - - Language { - checker_compile_args: Some(checker_compile_args), - checker_run_args, - submission_compile_args: Some(submission_compile_args), - submission_run_args, - extension: $e, - } - }}; +impl Language { + pub fn get_compile_command(&self, main: &str) -> Option { + let args = self.compile_args?; + let args = args.replace("{main}", main); + let mut args = args.split_whitespace(); + // SAFETY: there is always at least 1 element + let binary = args.next().unwrap(); + + let mut command = Command::new(binary); + command.args(args); + + Some(command) + } + pub fn get_run_command(&self, main: &str) -> Command { + let args = self.run_args.replace("{main}", main); + let mut args = args.split_whitespace(); + // SAFETY: there is always at least 1 element + let binary = args.next().unwrap(); + + let mut command = Command::new(binary); + command.args(args); + + command + } } diff --git a/src/language/python.rs b/src/language/python.rs index 8fe5caf..a253437 100644 --- a/src/language/python.rs +++ b/src/language/python.rs @@ -1,3 +1,7 @@ -use crate::{Language, language}; +use crate::Language; -pub const PYTHON: Language = language!(["python", "{main}.py"], "py"); +pub const PYTHON: Language = Language { + compile_args: None, + run_args: "python {main}.py", + extension: "py", +}; diff --git a/src/language/rust.rs b/src/language/rust.rs index 6936ec1..ae8a2f8 100644 --- a/src/language/rust.rs +++ b/src/language/rust.rs @@ -1,3 +1,7 @@ -use crate::{Language, language}; +use crate::Language; -pub const RUST: Language = language!(["rustc", "-O", "{main}.rs"], ["./{main}"], "rs"); +pub const RUST: Language = Language { + compile_args: Some("rustc -O {main}.rs"), + run_args: "./{main}", + extension: "rs", +}; diff --git a/src/language/typescript.rs b/src/language/typescript.rs index 6ed0531..f42633a 100644 --- a/src/language/typescript.rs +++ b/src/language/typescript.rs @@ -1,3 +1,7 @@ -use crate::{Language, language}; +use crate::Language; -pub const TYPESCRIPT: Language = language!(["bun", "run", "{main}.ts"], "ts"); +pub const TYPESCRIPT: Language = Language { + compile_args: None, + run_args: "bun run {main}.ts", + extension: "ts", +}; From 6fed85423e767706906071234eebf1fb19acd95c Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 03:10:59 +0700 Subject: [PATCH 05/81] feat: add checker compiling --- src/checker.rs | 26 ++++++++++++++++++++++++++ src/language/mod.rs | 1 + src/lib.rs | 3 ++- 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/checker.rs diff --git a/src/checker.rs b/src/checker.rs new file mode 100644 index 0000000..34d6c78 --- /dev/null +++ b/src/checker.rs @@ -0,0 +1,26 @@ +use std::{env, fs}; + +use anyhow::Result; +use uuid::Uuid; + +use crate::Language; + +const MAIN: &str = "checker"; + +pub fn compile(code: &[u8], language: Language) -> Result> { + let Some(mut command) = language.get_compile_command(MAIN) else { + return Ok(code.to_vec()); + }; + let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); + fs::create_dir(&project_path)?; + let main = project_path + .with_file_name(MAIN) + .with_extension(language.extension); + fs::write(&main, code)?; + + let mut process = command.spawn()?; + let _ = process.wait()?; + let binary = fs::read(main.with_extension(""))?; + + Ok(binary) +} diff --git a/src/language/mod.rs b/src/language/mod.rs index e713bf2..21c55d2 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -14,6 +14,7 @@ pub use python::PYTHON; pub use rust::RUST; pub use typescript::TYPESCRIPT; +#[derive(Clone, Copy)] pub struct Language { pub compile_args: Option<&'static str>, pub run_args: &'static str, diff --git a/src/lib.rs b/src/lib.rs index fd40615..f38c054 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,11 @@ +pub mod checker; pub mod language; mod sandbox; mod verdict; +pub use language::Language; pub use sandbox::*; pub use verdict::*; -pub use language::Language; #[cfg(test)] mod test { From 14e6224946a8d0ee28982c3b7fa74f05e20dd0aa Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 03:13:45 +0700 Subject: [PATCH 06/81] fix(checker): change current dir --- src/checker.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/checker.rs b/src/checker.rs index 34d6c78..a1f3cb6 100644 --- a/src/checker.rs +++ b/src/checker.rs @@ -18,7 +18,7 @@ pub fn compile(code: &[u8], language: Language) -> Result> { .with_extension(language.extension); fs::write(&main, code)?; - let mut process = command.spawn()?; + let mut process = command.current_dir(&project_path).spawn()?; let _ = process.wait()?; let binary = fs::read(main.with_extension(""))?; From f91cd8585c958df64169c6269172c41b59aa0a6d Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 03:17:16 +0700 Subject: [PATCH 07/81] feat: use io::Error instead of anyhow --- Cargo.lock | 7 ------- Cargo.toml | 1 - src/checker.rs | 9 +++------ src/sandbox/mod.rs | 9 +++++---- src/sandbox/resource.rs | 8 +++++--- 5 files changed, 13 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f750e65..199a961 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,12 +13,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - [[package]] name = "arrayvec" version = "0.7.6" @@ -508,7 +502,6 @@ dependencies = [ name = "judge-runner" version = "0.1.0" dependencies = [ - "anyhow", "byte-unit", "cgroups-rs", "state-shift", diff --git a/Cargo.toml b/Cargo.toml index 1550558..b25dc94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ A code runner library for online judge system """ [dependencies] -anyhow = "1.0.100" byte-unit = "5.2.0" cgroups-rs = "0.5.0" state-shift = "2.1.1" diff --git a/src/checker.rs b/src/checker.rs index a1f3cb6..71b3a18 100644 --- a/src/checker.rs +++ b/src/checker.rs @@ -1,13 +1,12 @@ -use std::{env, fs}; +use std::{env, fs, io}; -use anyhow::Result; use uuid::Uuid; use crate::Language; const MAIN: &str = "checker"; -pub fn compile(code: &[u8], language: Language) -> Result> { +pub fn compile(code: &[u8], language: Language) -> io::Result> { let Some(mut command) = language.get_compile_command(MAIN) else { return Ok(code.to_vec()); }; @@ -20,7 +19,5 @@ pub fn compile(code: &[u8], language: Language) -> Result> { let mut process = command.current_dir(&project_path).spawn()?; let _ = process.wait()?; - let binary = fs::read(main.with_extension(""))?; - - Ok(binary) + fs::read(main.with_extension("")) } diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index a88b305..99afbc7 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -2,12 +2,12 @@ mod cgroup; mod resource; use std::{ + io, process::Child, thread::sleep, time::{Duration, Instant}, }; -use anyhow::Result; use cgroups_rs::{CgroupPid, fs::Cgroup}; pub use resource::Resource; @@ -25,7 +25,7 @@ pub struct Sandbox { } impl Sandbox { - pub fn new(resource: Resource, time_limit: Duration) -> Result { + pub fn new(resource: Resource, time_limit: Duration) -> io::Result { Ok(Sandbox { cgroup: resource.try_into()?, cpu_time_limit: time_limit, @@ -33,9 +33,10 @@ impl Sandbox { }) } - pub fn monitor(self, mut child: Child) -> Result { + pub fn monitor(self, mut child: Child) -> io::Result { self.cgroup - .add_task_by_tgid(CgroupPid::from(child.id() as u64))?; + .add_task_by_tgid(CgroupPid::from(child.id() as u64)) + .map_err(io::Error::other)?; let start = Instant::now(); let mut prev_cpu_time = self.cgroup.get_cpu_time(); diff --git a/src/sandbox/resource.rs b/src/sandbox/resource.rs index 2543e86..5072a26 100644 --- a/src/sandbox/resource.rs +++ b/src/sandbox/resource.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{io, time::Duration}; use byte_unit::Byte; use cgroups_rs::fs::{Cgroup, cgroup_builder::CgroupBuilder, hierarchies}; @@ -24,7 +24,7 @@ impl Default for Resource { } impl TryFrom for Cgroup { - type Error = anyhow::Error; + type Error = io::Error; fn try_from(resource: Resource) -> Result { let builder = CgroupBuilder::new(&format!("{}/{}", PREFIX, Uuid::new_v4())); @@ -41,7 +41,9 @@ impl TryFrom for Cgroup { let period = resource.cpu_period.as_micros() as u64; let builder = builder.cpu().quota(quota).period(period).done(); - let cgroup = builder.build(hierarchies::auto())?; + let cgroup = builder + .build(hierarchies::auto()) + .map_err(io::Error::other)?; Ok(cgroup) } } From f4cec6c7bc396ac86ddf6218a7a402ffea3b0e2e Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 03:25:29 +0700 Subject: [PATCH 08/81] feat: add judge --- src/judge.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 55 insertions(+) create mode 100644 src/judge.rs diff --git a/src/judge.rs b/src/judge.rs new file mode 100644 index 0000000..8d956fc --- /dev/null +++ b/src/judge.rs @@ -0,0 +1,54 @@ +use std::{env, fs, io, path::PathBuf}; + +use state_shift::{impl_state, type_state}; +use uuid::Uuid; + +use crate::{Language, Verdict}; + +const MAIN: &str = "main"; +const CHECKER: &str = "checker"; + +#[type_state( + states = (Created, Compiled), + slots = (Created) +)] +pub struct Runner { + pub project_path: PathBuf, + pub language: Language, +} + +#[impl_state] +impl Runner { + #[require(Created)] + pub fn new(code: &[u8], checker: &[u8], language: Language) -> io::Result { + let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); + fs::create_dir(&project_path)?; + let main_path = project_path + .with_file_name(MAIN) + .with_extension(language.extension); + let checker_path = project_path.with_file_name(CHECKER); + + fs::write(&main_path, code)?; + fs::write(&checker_path, checker)?; + + Ok(Runner { + project_path, + language, + }) + } + + #[require(Created)] + #[switch_to(Compiled)] + pub fn compile(&self) -> io::Result> { + let Some(mut command) = self.language.get_compile_command(MAIN) else { + return Ok(None); + }; + let mut process = command.current_dir(&self.project_path).spawn()?; + let status = process.wait()?; + if !status.success() { + return Ok(Some(Verdict::CompilationError)); + } + + Ok(None) + } +} diff --git a/src/lib.rs b/src/lib.rs index f38c054..2b2a97b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod checker; +pub mod judge; pub mod language; mod sandbox; mod verdict; From 5628f7a603a30a854b6ab53d707222b5f747a439 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 03:32:14 +0700 Subject: [PATCH 09/81] feat(judge): add sandbox to field --- src/judge.rs | 27 +++++++++++++++++++++------ src/lib.rs | 3 ++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index 8d956fc..6c29fb2 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -1,9 +1,9 @@ -use std::{env, fs, io, path::PathBuf}; +use std::{env, fs, io, path::PathBuf, time::Duration}; use state_shift::{impl_state, type_state}; use uuid::Uuid; -use crate::{Language, Verdict}; +use crate::{Language, Resource, Sandbox, Verdict}; const MAIN: &str = "main"; const CHECKER: &str = "checker"; @@ -12,15 +12,22 @@ const CHECKER: &str = "checker"; states = (Created, Compiled), slots = (Created) )] -pub struct Runner { +pub struct Judge { pub project_path: PathBuf, + pub sandbox: Sandbox, pub language: Language, } #[impl_state] -impl Runner { +impl Judge { #[require(Created)] - pub fn new(code: &[u8], checker: &[u8], language: Language) -> io::Result { + pub fn new( + code: &[u8], + checker: &[u8], + resource: Resource, + time_limit: Duration, + language: Language, + ) -> io::Result { let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); fs::create_dir(&project_path)?; let main_path = project_path @@ -31,8 +38,11 @@ impl Runner { fs::write(&main_path, code)?; fs::write(&checker_path, checker)?; - Ok(Runner { + let sandbox = Sandbox::new(resource, time_limit)?; + + Ok(Judge { project_path, + sandbox, language, }) } @@ -51,4 +61,9 @@ impl Runner { Ok(None) } + + #[require(Compiled)] + pub fn run(&self) -> io::Result { + todo!() + } } diff --git a/src/lib.rs b/src/lib.rs index 2b2a97b..2584cc4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,10 @@ pub mod checker; -pub mod judge; +mod judge; pub mod language; mod sandbox; mod verdict; +pub use judge::*; pub use language::Language; pub use sandbox::*; pub use verdict::*; From 33ed571891da7c972910a6b0555e241ce2bf2147 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 03:54:53 +0700 Subject: [PATCH 10/81] feat(judge): spawn checker and user submission and wire input and output --- Cargo.lock | 1 + Cargo.toml | 1 + src/judge.rs | 72 ++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 199a961..3b961eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -504,6 +504,7 @@ version = "0.1.0" dependencies = [ "byte-unit", "cgroups-rs", + "nix 0.30.1", "state-shift", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index b25dc94..d24da2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,6 @@ A code runner library for online judge system [dependencies] byte-unit = "5.2.0" cgroups-rs = "0.5.0" +nix = { version = "0.30.1", default-features = false, features = ["fs"] } state-shift = "2.1.1" uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } diff --git a/src/judge.rs b/src/judge.rs index 6c29fb2..5f22324 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -1,12 +1,15 @@ use std::{env, fs, io, path::PathBuf, time::Duration}; +use nix::{sys::stat, unistd}; use state_shift::{impl_state, type_state}; use uuid::Uuid; use crate::{Language, Resource, Sandbox, Verdict}; -const MAIN: &str = "main"; +const SUBMISSION: &str = "main"; const CHECKER: &str = "checker"; +const INPUT: &str = "input"; +const OUTPUT: &str = "output"; #[type_state( states = (Created, Compiled), @@ -14,35 +17,25 @@ const CHECKER: &str = "checker"; )] pub struct Judge { pub project_path: PathBuf, - pub sandbox: Sandbox, pub language: Language, } #[impl_state] impl Judge { #[require(Created)] - pub fn new( - code: &[u8], - checker: &[u8], - resource: Resource, - time_limit: Duration, - language: Language, - ) -> io::Result { + pub fn new(code: &[u8], checker: &[u8], language: Language) -> io::Result { let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); fs::create_dir(&project_path)?; let main_path = project_path - .with_file_name(MAIN) + .with_file_name(SUBMISSION) .with_extension(language.extension); let checker_path = project_path.with_file_name(CHECKER); fs::write(&main_path, code)?; fs::write(&checker_path, checker)?; - let sandbox = Sandbox::new(resource, time_limit)?; - Ok(Judge { project_path, - sandbox, language, }) } @@ -50,7 +43,7 @@ impl Judge { #[require(Created)] #[switch_to(Compiled)] pub fn compile(&self) -> io::Result> { - let Some(mut command) = self.language.get_compile_command(MAIN) else { + let Some(mut command) = self.language.get_compile_command(SUBMISSION) else { return Ok(None); }; let mut process = command.current_dir(&self.project_path).spawn()?; @@ -63,7 +56,54 @@ impl Judge { } #[require(Compiled)] - pub fn run(&self) -> io::Result { - todo!() + pub fn run( + self, + input: &[u8], + is_interactive: bool, + resource: Resource, + time_limit: Duration, + ) -> io::Result { + let checker_to_submission = self.project_path.join(INPUT); + unistd::mkfifo(&checker_to_submission, stat::Mode::S_IRWXU)?; + if !is_interactive { + fs::write(&checker_to_submission, input)?; + } + + let submission_to_checker = self.project_path.join(OUTPUT); + unistd::mkfifo(&submission_to_checker, stat::Mode::S_IRWXU)?; + fs::write(&submission_to_checker, input)?; + + let sandbox = Sandbox::new(resource, time_limit)?; + + self.language + .get_run_command(CHECKER) + .current_dir(&self.project_path) + .stdin( + fs::OpenOptions::new() + .read(true) + .open(&submission_to_checker)?, + ) + .stdout( + fs::OpenOptions::new() + .write(true) + .open(&checker_to_submission)?, + ) + .spawn()?; + let submission = self + .language + .get_run_command(SUBMISSION) + .current_dir(&self.project_path) + .stdin( + fs::OpenOptions::new() + .read(true) + .open(&checker_to_submission)?, + ) + .stdout( + fs::OpenOptions::new() + .write(true) + .open(&submission_to_checker)?, + ) + .spawn()?; + sandbox.monitor(submission) } } From 27a196c4eb41621a953806b1b985ad42b1688219 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 13:33:26 +0700 Subject: [PATCH 11/81] feat(sandbox): return None on run successfully --- src/sandbox/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index 99afbc7..6a14f91 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -33,7 +33,7 @@ impl Sandbox { }) } - pub fn monitor(self, mut child: Child) -> io::Result { + pub fn monitor(self, mut child: Child) -> io::Result> { self.cgroup .add_task_by_tgid(CgroupPid::from(child.id() as u64)) .map_err(io::Error::other)?; @@ -48,7 +48,7 @@ impl Sandbox { match idle_start { Some(idle_start) => { if idle_start.elapsed() >= IDLE_TIME_LIMIT { - return Ok(Verdict::IdleTimeLimitExceeded); + return Ok(Some(Verdict::IdleTimeLimitExceeded)); } } None => idle_start = Some(Instant::now()), @@ -58,7 +58,7 @@ impl Sandbox { } if cpu_time >= self.cpu_time_limit || start.elapsed() >= self.wall_time_limit { - return Ok(Verdict::TimeLimitExceeded); + return Ok(Some(Verdict::TimeLimitExceeded)); } prev_cpu_time = cpu_time; @@ -70,12 +70,12 @@ impl Sandbox { let status = child.try_wait()?.unwrap(); if status.success() { // temporarily return AC - return Ok(Verdict::Accepted); + return Ok(None); } if self.cgroup.is_out_of_memory() { - return Ok(Verdict::MemoryLimitExceeded); + return Ok(Some(Verdict::MemoryLimitExceeded)); } - Ok(Verdict::RuntimeError) + Ok(Some(Verdict::RuntimeError)) } } From de7ba2e6b3c4c083676d8bec5a9cf6ca2cd424b6 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 13:36:15 +0700 Subject: [PATCH 12/81] feat(judge): return verdict base on checker exit status --- src/judge.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index 5f22324..e0b66cb 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -75,7 +75,8 @@ impl Judge { let sandbox = Sandbox::new(resource, time_limit)?; - self.language + let mut checker = self + .language .get_run_command(CHECKER) .current_dir(&self.project_path) .stdin( @@ -104,6 +105,17 @@ impl Judge { .open(&submission_to_checker)?, ) .spawn()?; - sandbox.monitor(submission) + if let Some(verdict) = sandbox.monitor(submission)? { + return Ok(verdict); + } + + let status = checker.wait()?; + let verdict = if status.success() { + Verdict::Accepted + } else { + Verdict::WrongAnswer + }; + + Ok(verdict) } } From 2c30bbf09904c34b99347052d5c060d1ff70ff10 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 13:49:38 +0700 Subject: [PATCH 13/81] feat(judge): remove state shift --- Cargo.lock | 19 ------------------- Cargo.toml | 1 - src/judge.rs | 16 +++------------- src/verdict.rs | 1 + 4 files changed, 4 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b961eb..1323599 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -505,7 +505,6 @@ dependencies = [ "byte-unit", "cgroups-rs", "nix 0.30.1", - "state-shift", "uuid", ] @@ -948,30 +947,12 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" -[[package]] -name = "state-shift" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b84d514d82ae2456c0ecc1fdf84e77368ff158e7a9dcdfc135669ba3ec57fdf" -dependencies = [ - "proc-macro2", - "quote", - "stringcase", - "syn 2.0.111", -] - [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stringcase" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72abeda133c49d7bddece6c154728f83eec8172380c80ab7096da9487e20d27c" - [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index d24da2a..c9c56b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,5 +14,4 @@ A code runner library for online judge system byte-unit = "5.2.0" cgroups-rs = "0.5.0" nix = { version = "0.30.1", default-features = false, features = ["fs"] } -state-shift = "2.1.1" uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } diff --git a/src/judge.rs b/src/judge.rs index e0b66cb..bd459a7 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -1,7 +1,6 @@ use std::{env, fs, io, path::PathBuf, time::Duration}; use nix::{sys::stat, unistd}; -use state_shift::{impl_state, type_state}; use uuid::Uuid; use crate::{Language, Resource, Sandbox, Verdict}; @@ -11,25 +10,19 @@ const CHECKER: &str = "checker"; const INPUT: &str = "input"; const OUTPUT: &str = "output"; -#[type_state( - states = (Created, Compiled), - slots = (Created) -)] pub struct Judge { pub project_path: PathBuf, pub language: Language, } -#[impl_state] impl Judge { - #[require(Created)] pub fn new(code: &[u8], checker: &[u8], language: Language) -> io::Result { let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); fs::create_dir(&project_path)?; let main_path = project_path - .with_file_name(SUBMISSION) + .join(SUBMISSION) .with_extension(language.extension); - let checker_path = project_path.with_file_name(CHECKER); + let checker_path = project_path.join(CHECKER); fs::write(&main_path, code)?; fs::write(&checker_path, checker)?; @@ -40,9 +33,7 @@ impl Judge { }) } - #[require(Created)] - #[switch_to(Compiled)] - pub fn compile(&self) -> io::Result> { + pub fn compile(self) -> io::Result> { let Some(mut command) = self.language.get_compile_command(SUBMISSION) else { return Ok(None); }; @@ -55,7 +46,6 @@ impl Judge { Ok(None) } - #[require(Compiled)] pub fn run( self, input: &[u8], diff --git a/src/verdict.rs b/src/verdict.rs index ed1fa29..efde8b2 100644 --- a/src/verdict.rs +++ b/src/verdict.rs @@ -1,3 +1,4 @@ +#[derive(Debug)] pub enum Verdict { Accepted, WrongAnswer, From 278da0fa584d663e83da395fa353dcdf9f09c3ad Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 13:49:51 +0700 Subject: [PATCH 14/81] fix(checker): use join instead of with file name --- src/checker.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/checker.rs b/src/checker.rs index 71b3a18..7abcb7f 100644 --- a/src/checker.rs +++ b/src/checker.rs @@ -12,9 +12,7 @@ pub fn compile(code: &[u8], language: Language) -> io::Result> { }; let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); fs::create_dir(&project_path)?; - let main = project_path - .with_file_name(MAIN) - .with_extension(language.extension); + let main = project_path.join(MAIN).with_extension(language.extension); fs::write(&main, code)?; let mut process = command.current_dir(&project_path).spawn()?; From f9c2ca6f34d5e84c196e33ec07da9ccd945b8169 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 19:13:07 +0700 Subject: [PATCH 15/81] fix: add sudo for cgroup --- .cargo/config.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..ca6c72f --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-unknown-linux-gnu] +runner = 'sudo -E' From 16e030f79ce0183156ea950a32d68c201d15affa Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 19:13:44 +0700 Subject: [PATCH 16/81] fix(judge): write \n so checker and submission can read input --- src/judge.rs | 107 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 40 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index bd459a7..c0b2613 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -1,14 +1,20 @@ -use std::{env, fs, io, path::PathBuf, time::Duration}; +use std::{ + env, fs, + io::{self, Read, Write}, + os::unix::fs::OpenOptionsExt, + path::PathBuf, + process::Stdio, + thread, + time::Duration, +}; -use nix::{sys::stat, unistd}; use uuid::Uuid; use crate::{Language, Resource, Sandbox, Verdict}; const SUBMISSION: &str = "main"; const CHECKER: &str = "checker"; -const INPUT: &str = "input"; -const OUTPUT: &str = "output"; +const BUFFER_SIZE: usize = 512; pub struct Judge { pub project_path: PathBuf, @@ -25,7 +31,13 @@ impl Judge { let checker_path = project_path.join(CHECKER); fs::write(&main_path, code)?; - fs::write(&checker_path, checker)?; + let mut checker_file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .mode(0o755) + .open(&checker_path)?; + checker_file.write_all(checker)?; Ok(Judge { project_path, @@ -33,7 +45,7 @@ impl Judge { }) } - pub fn compile(self) -> io::Result> { + pub fn compile(&self) -> io::Result> { let Some(mut command) = self.language.get_compile_command(SUBMISSION) else { return Ok(None); }; @@ -53,49 +65,44 @@ impl Judge { resource: Resource, time_limit: Duration, ) -> io::Result { - let checker_to_submission = self.project_path.join(INPUT); - unistd::mkfifo(&checker_to_submission, stat::Mode::S_IRWXU)?; - if !is_interactive { - fs::write(&checker_to_submission, input)?; - } - - let submission_to_checker = self.project_path.join(OUTPUT); - unistd::mkfifo(&submission_to_checker, stat::Mode::S_IRWXU)?; - fs::write(&submission_to_checker, input)?; - - let sandbox = Sandbox::new(resource, time_limit)?; - let mut checker = self .language .get_run_command(CHECKER) .current_dir(&self.project_path) - .stdin( - fs::OpenOptions::new() - .read(true) - .open(&submission_to_checker)?, - ) - .stdout( - fs::OpenOptions::new() - .write(true) - .open(&checker_to_submission)?, - ) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) .spawn()?; - let submission = self + let mut cin = checker.stdin.take().unwrap(); + let cout = checker.stdout.take().unwrap(); + + let mut submission = self .language .get_run_command(SUBMISSION) .current_dir(&self.project_path) - .stdin( - fs::OpenOptions::new() - .read(true) - .open(&checker_to_submission)?, - ) - .stdout( - fs::OpenOptions::new() - .write(true) - .open(&submission_to_checker)?, - ) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) .spawn()?; - if let Some(verdict) = sandbox.monitor(submission)? { + let mut sin = submission.stdin.take().unwrap(); + let sout = submission.stdout.take().unwrap(); + + let monitor_thread = thread::spawn(move || { + let sandbox = Sandbox::new(resource, time_limit)?; + sandbox.monitor(submission) + }); + + if !is_interactive { + sin.write_all(input)?; + sin.write_all(b"\n")?; + sin.flush()?; + } + cin.write_all(input)?; + cin.write_all(b"\n")?; + cin.flush()?; + + forward(cout, sin); + forward(sout, cin); + + if let Some(verdict) = monitor_thread.join().unwrap()? { return Ok(verdict); } @@ -109,3 +116,23 @@ impl Judge { Ok(verdict) } } + +fn forward( + mut reader: R, + mut writer: W, +) -> thread::JoinHandle> { + thread::spawn(move || { + let mut buffer = [0u8; BUFFER_SIZE]; + loop { + let n = reader.read(&mut buffer)?; + if n == 0 { + drop(writer); + break; + } + writer.write_all(&buffer[..n])?; + writer.flush()?; + } + + Ok(()) + }) +} From b618744a5df0d16bd0cc9a6da8b368d100b6b3d0 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 20:06:29 +0700 Subject: [PATCH 17/81] fix(sandbox): add to cgroup in pre exec --- src/judge.rs | 15 ++++++--------- src/sandbox/mod.rs | 24 +++++++++++++++++++++--- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index c0b2613..d7ba5c8 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -75,20 +75,17 @@ impl Judge { let mut cin = checker.stdin.take().unwrap(); let cout = checker.stdout.take().unwrap(); - let mut submission = self - .language - .get_run_command(SUBMISSION) + let sandbox = Sandbox::new(resource, time_limit)?; + let mut submimission_command = self.language.get_run_command(SUBMISSION); + submimission_command .current_dir(&self.project_path) .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn()?; + .stdout(Stdio::piped()); + let mut submission = sandbox.spawn(submimission_command)?; let mut sin = submission.stdin.take().unwrap(); let sout = submission.stdout.take().unwrap(); - let monitor_thread = thread::spawn(move || { - let sandbox = Sandbox::new(resource, time_limit)?; - sandbox.monitor(submission) - }); + let monitor_thread = thread::spawn(move || sandbox.monitor(submission)); if !is_interactive { sin.write_all(input)?; diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index 6a14f91..66a7980 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -3,12 +3,14 @@ mod resource; use std::{ io, - process::Child, + os::unix::process::CommandExt, + process::{Child, Command}, thread::sleep, time::{Duration, Instant}, }; use cgroups_rs::{CgroupPid, fs::Cgroup}; +use nix::libc::getpid; pub use resource::Resource; use crate::{Verdict, sandbox::cgroup::CgroupExt}; @@ -33,6 +35,22 @@ impl Sandbox { }) } + pub fn spawn(&self, mut command: Command) -> io::Result { + let cgroup = self.cgroup.clone().clone(); + + unsafe { + command + .pre_exec(move || { + let id = getpid(); + + cgroup + .add_task_by_tgid(CgroupPid::from(id as u64)) + .map_err(io::Error::other) + }) + .spawn() + } + } + pub fn monitor(self, mut child: Child) -> io::Result> { self.cgroup .add_task_by_tgid(CgroupPid::from(child.id() as u64)) @@ -82,9 +100,9 @@ impl Sandbox { impl Drop for Sandbox { fn drop(&mut self) { // SAFETY: always be used with stable version of linux kernel - self.cgroup.kill().unwrap(); + let _ = self.cgroup.kill(); // SAFETY: no descendant is created previously by judge - self.cgroup.delete().unwrap(); + let _ = self.cgroup.delete(); } } From 959ccb0ab900419912acb2c958f1464382ec7f7b Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 21:01:49 +0700 Subject: [PATCH 18/81] fix: use correct language for checker --- src/judge.rs | 32 +++++++++++++++++++++----------- src/language/mod.rs | 3 +++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index d7ba5c8..3f53977 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -18,19 +18,28 @@ const BUFFER_SIZE: usize = 512; pub struct Judge { pub project_path: PathBuf, - pub language: Language, + pub submission_language: Language, + pub checker_language: Language, } impl Judge { - pub fn new(code: &[u8], checker: &[u8], language: Language) -> io::Result { + pub fn new( + submission: &[u8], + submission_language: Language, + checker: &[u8], + checker_language: Language, + ) -> io::Result { let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); fs::create_dir(&project_path)?; let main_path = project_path .join(SUBMISSION) - .with_extension(language.extension); - let checker_path = project_path.join(CHECKER); + .with_extension(submission_language.extension); + let mut checker_path = project_path.join(CHECKER); + if checker_language.is_interpreted() { + checker_path.set_extension(checker_language.extension); + } - fs::write(&main_path, code)?; + fs::write(&main_path, submission)?; let mut checker_file = fs::OpenOptions::new() .create(true) .write(true) @@ -41,12 +50,13 @@ impl Judge { Ok(Judge { project_path, - language, + submission_language, + checker_language, }) } pub fn compile(&self) -> io::Result> { - let Some(mut command) = self.language.get_compile_command(SUBMISSION) else { + let Some(mut command) = self.submission_language.get_compile_command(SUBMISSION) else { return Ok(None); }; let mut process = command.current_dir(&self.project_path).spawn()?; @@ -66,7 +76,7 @@ impl Judge { time_limit: Duration, ) -> io::Result { let mut checker = self - .language + .checker_language .get_run_command(CHECKER) .current_dir(&self.project_path) .stdin(Stdio::piped()) @@ -76,12 +86,12 @@ impl Judge { let cout = checker.stdout.take().unwrap(); let sandbox = Sandbox::new(resource, time_limit)?; - let mut submimission_command = self.language.get_run_command(SUBMISSION); - submimission_command + let mut submission_command = self.submission_language.get_run_command(SUBMISSION); + submission_command .current_dir(&self.project_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()); - let mut submission = sandbox.spawn(submimission_command)?; + let mut submission = sandbox.spawn(submission_command)?; let mut sin = submission.stdin.take().unwrap(); let sout = submission.stdout.take().unwrap(); diff --git a/src/language/mod.rs b/src/language/mod.rs index 21c55d2..1bc0764 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -45,4 +45,7 @@ impl Language { command } + pub fn is_interpreted(&self) -> bool { + self.compile_args.is_none() + } } From 6ff516e5293531a19ee6588b56fd188e71df18ec Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 21:05:50 +0700 Subject: [PATCH 19/81] refactor: remove nix --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c9c56b7..0c7bb29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,5 @@ A code runner library for online judge system [dependencies] byte-unit = "5.2.0" cgroups-rs = "0.5.0" -nix = { version = "0.30.1", default-features = false, features = ["fs"] } +nix = { version = "0.30.1", default-features = false } uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } From 61961ecf81d87ec7343d0c8dade62df4fe278bc1 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 21:14:06 +0700 Subject: [PATCH 20/81] fix(sandbox): remove double clone --- src/sandbox/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index 66a7980..ae0a918 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -36,7 +36,7 @@ impl Sandbox { } pub fn spawn(&self, mut command: Command) -> io::Result { - let cgroup = self.cgroup.clone().clone(); + let cgroup = self.cgroup.clone(); unsafe { command From e48428ff8f2eafdfb582153978756e5f87ff95c7 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 21:59:54 +0700 Subject: [PATCH 21/81] fix(cgroup): check memory limit by oom kill OR limit exceed --- src/sandbox/cgroup.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sandbox/cgroup.rs b/src/sandbox/cgroup.rs index 3fa334d..0485c92 100644 --- a/src/sandbox/cgroup.rs +++ b/src/sandbox/cgroup.rs @@ -29,6 +29,6 @@ impl CgroupExt for Cgroup { let memory_controller: &MemController = self.controller_of().unwrap(); let stats = memory_controller.memory_stat(); - stats.oom_control.oom_kill > 0 && stats.usage_in_bytes as i64 > stats.limit_in_bytes + stats.oom_control.oom_kill > 0 || stats.usage_in_bytes as i64 > stats.limit_in_bytes } } From d2b2d2d58bbf06e269582df932f1f99d329dd8e3 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Thu, 25 Dec 2025 22:59:00 +0700 Subject: [PATCH 22/81] fix(sandbox): use exit status to detect MLE instead --- Cargo.toml | 2 +- src/sandbox/cgroup.rs | 11 +---------- src/sandbox/mod.rs | 11 +++++------ 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0c7bb29..d250add 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,5 @@ A code runner library for online judge system [dependencies] byte-unit = "5.2.0" cgroups-rs = "0.5.0" -nix = { version = "0.30.1", default-features = false } +nix = { version = "0.30.1", default-features = false, features = ["signal"] } uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } diff --git a/src/sandbox/cgroup.rs b/src/sandbox/cgroup.rs index 0485c92..47b32f8 100644 --- a/src/sandbox/cgroup.rs +++ b/src/sandbox/cgroup.rs @@ -1,12 +1,11 @@ use std::time::Duration; -use cgroups_rs::fs::{Cgroup, cpu::CpuController, memory::MemController}; +use cgroups_rs::fs::{Cgroup, cpu::CpuController}; const CPU_USAGE_PREFIX: &str = "usage_usec "; pub trait CgroupExt { fn get_cpu_time(&self) -> Duration; - fn is_out_of_memory(&self) -> bool; } impl CgroupExt for Cgroup { @@ -23,12 +22,4 @@ impl CgroupExt for Cgroup { let usage = usage.parse().unwrap(); Duration::from_micros(usage) } - - fn is_out_of_memory(&self) -> bool { - // SAFETY: there must be memory controller for cgroup v2 - let memory_controller: &MemController = self.controller_of().unwrap(); - let stats = memory_controller.memory_stat(); - - stats.oom_control.oom_kill > 0 || stats.usage_in_bytes as i64 > stats.limit_in_bytes - } } diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index ae0a918..5efe4d2 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -3,14 +3,14 @@ mod resource; use std::{ io, - os::unix::process::CommandExt, + os::unix::process::{CommandExt, ExitStatusExt}, process::{Child, Command}, thread::sleep, time::{Duration, Instant}, }; use cgroups_rs::{CgroupPid, fs::Cgroup}; -use nix::libc::getpid; +use nix::{libc::getpid, sys::signal::Signal}; pub use resource::Resource; use crate::{Verdict, sandbox::cgroup::CgroupExt}; @@ -87,13 +87,12 @@ impl Sandbox { // SAFETY: child must be finished at this point to exit the previous loop let status = child.try_wait()?.unwrap(); if status.success() { - // temporarily return AC return Ok(None); } - if self.cgroup.is_out_of_memory() { - return Ok(Some(Verdict::MemoryLimitExceeded)); + match status.signal().and_then(|x| Signal::try_from(x).ok()) { + Some(Signal::SIGKILL) => Ok(Some(Verdict::MemoryLimitExceeded)), + _ => Ok(Some(Verdict::RuntimeError)), } - Ok(Some(Verdict::RuntimeError)) } } From 5efa7b73ff72b8a3f6e83426529650939864a1b2 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 01:19:00 +0700 Subject: [PATCH 23/81] feat(judge): use typed state to init judge --- Cargo.lock | 19 +++++++ Cargo.toml | 3 +- src/judge.rs | 132 +++++++++++++++----------------------------- src/language/mod.rs | 2 +- 4 files changed, 66 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1323599..3b961eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -505,6 +505,7 @@ dependencies = [ "byte-unit", "cgroups-rs", "nix 0.30.1", + "state-shift", "uuid", ] @@ -947,12 +948,30 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "state-shift" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b84d514d82ae2456c0ecc1fdf84e77368ff158e7a9dcdfc135669ba3ec57fdf" +dependencies = [ + "proc-macro2", + "quote", + "stringcase", + "syn 2.0.111", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringcase" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72abeda133c49d7bddece6c154728f83eec8172380c80ab7096da9487e20d27c" + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index d250add..2ea6c4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,6 @@ A code runner library for online judge system [dependencies] byte-unit = "5.2.0" cgroups-rs = "0.5.0" -nix = { version = "0.30.1", default-features = false, features = ["signal"] } +nix = { version = "0.30.1", default-features = false } +state-shift = "2.1.1" uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } diff --git a/src/judge.rs b/src/judge.rs index 3f53977..ff7448a 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -8,119 +8,75 @@ use std::{ time::Duration, }; +use state_shift::{impl_state, type_state}; use uuid::Uuid; use crate::{Language, Resource, Sandbox, Verdict}; -const SUBMISSION: &str = "main"; +const MAIN: &str = "main"; const CHECKER: &str = "checker"; const BUFFER_SIZE: usize = 512; +#[type_state( + states = (Builder), + slots = (Builder) +)] +#[derive(Default)] pub struct Judge { pub project_path: PathBuf, - pub submission_language: Language, - pub checker_language: Language, + pub language: Language, + pub checker_language: Option, } +#[impl_state] impl Judge { - pub fn new( - submission: &[u8], - submission_language: Language, - checker: &[u8], - checker_language: Language, - ) -> io::Result { + #[require(Builder)] + pub fn new() -> io::Result { let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); fs::create_dir(&project_path)?; - let main_path = project_path - .join(SUBMISSION) - .with_extension(submission_language.extension); - let mut checker_path = project_path.join(CHECKER); - if checker_language.is_interpreted() { - checker_path.set_extension(checker_language.extension); - } - - fs::write(&main_path, submission)?; - let mut checker_file = fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .mode(0o755) - .open(&checker_path)?; - checker_file.write_all(checker)?; Ok(Judge { project_path, - submission_language, - checker_language, + language: Default::default(), + checker_language: Default::default(), }) } - pub fn compile(&self) -> io::Result> { - let Some(mut command) = self.submission_language.get_compile_command(SUBMISSION) else { - return Ok(None); - }; - let mut process = command.current_dir(&self.project_path).spawn()?; - let status = process.wait()?; - if !status.success() { - return Ok(Some(Verdict::CompilationError)); + #[require(Builder)] + pub fn with_checker(self, code: &[u8], language: Language) -> io::Result { + let mut path = self.project_path.join(CHECKER); + if language.is_interpreted() { + path.set_extension(language.extension); } + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .mode(0o755) + .open(&path)?; + file.write_all(code)?; - Ok(None) + Ok(Judge { + project_path: self.project_path, + language: self.language, + checker_language: Some(language), + }) } - pub fn run( - self, - input: &[u8], - is_interactive: bool, - resource: Resource, - time_limit: Duration, - ) -> io::Result { - let mut checker = self - .checker_language - .get_run_command(CHECKER) - .current_dir(&self.project_path) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn()?; - let mut cin = checker.stdin.take().unwrap(); - let cout = checker.stdout.take().unwrap(); - - let sandbox = Sandbox::new(resource, time_limit)?; - let mut submission_command = self.submission_language.get_run_command(SUBMISSION); - submission_command - .current_dir(&self.project_path) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()); - let mut submission = sandbox.spawn(submission_command)?; - let mut sin = submission.stdin.take().unwrap(); - let sout = submission.stdout.take().unwrap(); - - let monitor_thread = thread::spawn(move || sandbox.monitor(submission)); - - if !is_interactive { - sin.write_all(input)?; - sin.write_all(b"\n")?; - sin.flush()?; - } - cin.write_all(input)?; - cin.write_all(b"\n")?; - cin.flush()?; - - forward(cout, sin); - forward(sout, cin); + #[require(Builder)] + #[switch_to(Created)] + pub fn with_main(self, code: &[u8], language: Language) -> io::Result { + let main_path = self + .project_path + .join(MAIN) + .with_extension(language.extension); + fs::write(&main_path, code)?; - if let Some(verdict) = monitor_thread.join().unwrap()? { - return Ok(verdict); - } - - let status = checker.wait()?; - let verdict = if status.success() { - Verdict::Accepted - } else { - Verdict::WrongAnswer - }; - - Ok(verdict) + Ok(Judge { + project_path: self.project_path, + language: self.language, + checker_language: self.checker_language, + }) } } diff --git a/src/language/mod.rs b/src/language/mod.rs index 1bc0764..76b3195 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -14,7 +14,7 @@ pub use python::PYTHON; pub use rust::RUST; pub use typescript::TYPESCRIPT; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] pub struct Language { pub compile_args: Option<&'static str>, pub run_args: &'static str, From 5e0fe44e007503b4d67cdc5d8df3fed0e8044b4b Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 01:27:53 +0700 Subject: [PATCH 24/81] feat(judge): add compile --- Cargo.toml | 2 +- src/judge.rs | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2ea6c4a..8c7e775 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,6 @@ A code runner library for online judge system [dependencies] byte-unit = "5.2.0" cgroups-rs = "0.5.0" -nix = { version = "0.30.1", default-features = false } +nix = { version = "0.30.1", default-features = false, features = ["signal"] } state-shift = "2.1.1" uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } diff --git a/src/judge.rs b/src/judge.rs index ff7448a..7ebc043 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -18,7 +18,7 @@ const CHECKER: &str = "checker"; const BUFFER_SIZE: usize = 512; #[type_state( - states = (Builder), + states = (Builder, Created, Compiled), slots = (Builder) )] #[derive(Default)] @@ -78,6 +78,24 @@ impl Judge { checker_language: self.checker_language, }) } + + #[require(Created)] + #[switch_to(Compiled)] + pub fn compile(self) -> io::Result> { + if let Some(mut cmd) = self.language.get_compile_command(MAIN) { + let mut process = cmd.current_dir(&self.project_path).spawn()?; + let status = process.wait()?; + if !status.success() { + return Ok(Err(Verdict::CompilationError)); + } + } + + Ok(Ok(Judge { + project_path: self.project_path, + language: self.language, + checker_language: self.checker_language, + })) + } } fn forward( From ddc38c83df382ee13ac7cffc5fc595dbdfb03ce7 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 01:44:02 +0700 Subject: [PATCH 25/81] feat(judge): add interactive, resource and time limit to builder --- src/judge.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index 7ebc043..369309c 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -24,8 +24,13 @@ const BUFFER_SIZE: usize = 512; #[derive(Default)] pub struct Judge { pub project_path: PathBuf, + pub language: Language, pub checker_language: Option, + + pub is_interactive: bool, + pub resource: Resource, + pub time_limit: Duration, } #[impl_state] @@ -39,9 +44,44 @@ impl Judge { project_path, language: Default::default(), checker_language: Default::default(), + is_interactive: false, + resource: Default::default(), + time_limit: Duration::from_secs(1), }) } - + #[require(Builder)] + pub fn interactive(self) -> Judge { + Judge { + project_path: self.project_path, + language: self.language, + checker_language: self.checker_language, + is_interactive: true, + resource: self.resource, + time_limit: self.time_limit, + } + } + #[require(Builder)] + pub fn with_resource(self, resource: Resource) -> Judge { + Judge { + project_path: self.project_path, + language: self.language, + checker_language: self.checker_language, + is_interactive: self.is_interactive, + resource, + time_limit: self.time_limit, + } + } + #[require(Builder)] + pub fn with_time_limit(self, time_limit: Duration) -> Judge { + Judge { + project_path: self.project_path, + language: self.language, + checker_language: self.checker_language, + is_interactive: self.is_interactive, + resource: self.resource, + time_limit, + } + } #[require(Builder)] pub fn with_checker(self, code: &[u8], language: Language) -> io::Result { let mut path = self.project_path.join(CHECKER); @@ -60,9 +100,11 @@ impl Judge { project_path: self.project_path, language: self.language, checker_language: Some(language), + is_interactive: self.is_interactive, + resource: self.resource, + time_limit: self.time_limit, }) } - #[require(Builder)] #[switch_to(Created)] pub fn with_main(self, code: &[u8], language: Language) -> io::Result { @@ -76,6 +118,9 @@ impl Judge { project_path: self.project_path, language: self.language, checker_language: self.checker_language, + is_interactive: self.is_interactive, + resource: self.resource, + time_limit: self.time_limit, }) } @@ -94,6 +139,9 @@ impl Judge { project_path: self.project_path, language: self.language, checker_language: self.checker_language, + is_interactive: self.is_interactive, + resource: self.resource, + time_limit: self.time_limit, })) } } From 899b6bacaab6717575ba818d416b962f4a5da1b8 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 01:54:41 +0700 Subject: [PATCH 26/81] feat(judge): add read binary --- src/judge.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/judge.rs b/src/judge.rs index 369309c..2ec37d7 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -144,6 +144,16 @@ impl Judge { time_limit: self.time_limit, })) } + + #[require(Compiled)] + pub fn read_binary(&self) -> io::Result> { + let mut path = self.project_path.join(MAIN); + if self.language.is_interpreted() { + path.set_extension(self.language.extension); + } + + fs::read(path) + } } fn forward( From 92ddf5f86d1bdbb18124089ff7bf31f98a2d7a39 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 01:55:29 +0700 Subject: [PATCH 27/81] refactor(judge): rename to read executable --- src/judge.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/judge.rs b/src/judge.rs index 2ec37d7..14249dd 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -146,7 +146,7 @@ impl Judge { } #[require(Compiled)] - pub fn read_binary(&self) -> io::Result> { + pub fn read_executable(&self) -> io::Result> { let mut path = self.project_path.join(MAIN); if self.language.is_interpreted() { path.set_extension(self.language.extension); From 5db3ec4d7d855bee604cf7663110985a70bef1ba Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 02:44:57 +0700 Subject: [PATCH 28/81] refactor: remove unused checker helper --- src/checker.rs | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 src/checker.rs diff --git a/src/checker.rs b/src/checker.rs deleted file mode 100644 index 7abcb7f..0000000 --- a/src/checker.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::{env, fs, io}; - -use uuid::Uuid; - -use crate::Language; - -const MAIN: &str = "checker"; - -pub fn compile(code: &[u8], language: Language) -> io::Result> { - let Some(mut command) = language.get_compile_command(MAIN) else { - return Ok(code.to_vec()); - }; - let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); - fs::create_dir(&project_path)?; - let main = project_path.join(MAIN).with_extension(language.extension); - fs::write(&main, code)?; - - let mut process = command.current_dir(&project_path).spawn()?; - let _ = process.wait()?; - fs::read(main.with_extension("")) -} From ecad5ba50b51ed5edd14cab08dcd1d4ff4eb7694 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 02:45:22 +0700 Subject: [PATCH 29/81] refactor(judge): create judge from checker with main --- src/judge.rs | 182 ++++++++++++++++++++++---------------------- src/language/mod.rs | 2 +- src/lib.rs | 1 - 3 files changed, 92 insertions(+), 93 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index 14249dd..d20e59f 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -18,115 +18,55 @@ const CHECKER: &str = "checker"; const BUFFER_SIZE: usize = 512; #[type_state( - states = (Builder, Created, Compiled), - slots = (Builder) + states = (Created, Compiled), + slots = (Created) )] #[derive(Default)] pub struct Judge { pub project_path: PathBuf, - pub language: Language, pub checker_language: Option, - - pub is_interactive: bool, - pub resource: Resource, - pub time_limit: Duration, } #[impl_state] impl Judge { - #[require(Builder)] - pub fn new() -> io::Result { + #[require(Created)] + pub fn new(main: (&[u8], Language), checker: Option<(&[u8], Language)>) -> io::Result { let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); fs::create_dir(&project_path)?; - Ok(Judge { - project_path, - language: Default::default(), - checker_language: Default::default(), - is_interactive: false, - resource: Default::default(), - time_limit: Duration::from_secs(1), - }) - } - #[require(Builder)] - pub fn interactive(self) -> Judge { - Judge { - project_path: self.project_path, - language: self.language, - checker_language: self.checker_language, - is_interactive: true, - resource: self.resource, - time_limit: self.time_limit, - } - } - #[require(Builder)] - pub fn with_resource(self, resource: Resource) -> Judge { - Judge { - project_path: self.project_path, - language: self.language, - checker_language: self.checker_language, - is_interactive: self.is_interactive, - resource, - time_limit: self.time_limit, - } - } - #[require(Builder)] - pub fn with_time_limit(self, time_limit: Duration) -> Judge { - Judge { - project_path: self.project_path, - language: self.language, - checker_language: self.checker_language, - is_interactive: self.is_interactive, - resource: self.resource, - time_limit, - } - } - #[require(Builder)] - pub fn with_checker(self, code: &[u8], language: Language) -> io::Result { - let mut path = self.project_path.join(CHECKER); - if language.is_interpreted() { - path.set_extension(language.extension); - } - let mut file = fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .mode(0o755) - .open(&path)?; - file.write_all(code)?; - - Ok(Judge { - project_path: self.project_path, - language: self.language, - checker_language: Some(language), - is_interactive: self.is_interactive, - resource: self.resource, - time_limit: self.time_limit, - }) - } - #[require(Builder)] - #[switch_to(Created)] - pub fn with_main(self, code: &[u8], language: Language) -> io::Result { - let main_path = self - .project_path - .join(MAIN) - .with_extension(language.extension); + let (code, language) = main; + let main_path = project_path.join(MAIN).with_extension(language.extension); fs::write(&main_path, code)?; + let checker_language = if let Some((code, language)) = checker { + let mut checker_path = project_path.join(CHECKER); + if language.is_interpreted() { + checker_path.set_extension(language.extension); + } + let mut checker_file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .mode(0o755) + .open(&checker_path)?; + checker_file.write_all(code)?; + + Some(language) + } else { + None + }; + Ok(Judge { - project_path: self.project_path, - language: self.language, - checker_language: self.checker_language, - is_interactive: self.is_interactive, - resource: self.resource, - time_limit: self.time_limit, + project_path, + language, + checker_language, }) } #[require(Created)] #[switch_to(Compiled)] - pub fn compile(self) -> io::Result> { + pub fn compile(self) -> io::Result, Verdict>> { if let Some(mut cmd) = self.language.get_compile_command(MAIN) { let mut process = cmd.current_dir(&self.project_path).spawn()?; let status = process.wait()?; @@ -139,9 +79,6 @@ impl Judge { project_path: self.project_path, language: self.language, checker_language: self.checker_language, - is_interactive: self.is_interactive, - resource: self.resource, - time_limit: self.time_limit, })) } @@ -154,6 +91,69 @@ impl Judge { fs::read(path) } + + #[require(Compiled)] + pub fn run( + self, + input: &[u8], + is_interactive: bool, + resource: Resource, + time_limit: Duration, + ) -> io::Result { + let Judge { + project_path, + language, + checker_language, + .. + } = self; + + let checker_language = checker_language.ok_or(io::Error::other("Missing checker"))?; + let mut checker = checker_language + .get_run_command(CHECKER) + .current_dir(&project_path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + let mut cin = checker.stdin.take().unwrap(); + let cout = checker.stdout.take().unwrap(); + + let sandbox = Sandbox::new(resource, time_limit)?; + let mut submission_command = language.get_run_command(MAIN); + submission_command + .current_dir(&project_path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()); + let mut submission = sandbox.spawn(submission_command)?; + let mut sin = submission.stdin.take().unwrap(); + let sout = submission.stdout.take().unwrap(); + + let monitor_thread = thread::spawn(move || sandbox.monitor(submission)); + + if !is_interactive { + sin.write_all(input)?; + sin.write_all(b"\n")?; + sin.flush()?; + } + cin.write_all(input)?; + cin.write_all(b"\n")?; + cin.flush()?; + + forward(cout, sin); + forward(sout, cin); + + if let Some(verdict) = monitor_thread.join().unwrap()? { + return Ok(verdict); + } + + let status = checker.wait()?; + let verdict = if status.success() { + Verdict::Accepted + } else { + Verdict::WrongAnswer + }; + + Ok(verdict) + } } fn forward( diff --git a/src/language/mod.rs b/src/language/mod.rs index 76b3195..f33fe1c 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -14,7 +14,7 @@ pub use python::PYTHON; pub use rust::RUST; pub use typescript::TYPESCRIPT; -#[derive(Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, Default)] pub struct Language { pub compile_args: Option<&'static str>, pub run_args: &'static str, diff --git a/src/lib.rs b/src/lib.rs index 2584cc4..2dd7445 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -pub mod checker; mod judge; pub mod language; mod sandbox; From 3aff592b5e0ba7f724ec79aeede58b775c5d1d40 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 14:37:58 +0700 Subject: [PATCH 30/81] refactor(judge): use Code struct --- src/judge.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index d20e59f..7bd8969 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -28,21 +28,27 @@ pub struct Judge { pub checker_language: Option, } +pub struct Code<'a> { + pub content: &'a [u8], + pub language: Language, +} + #[impl_state] impl Judge { #[require(Created)] - pub fn new(main: (&[u8], Language), checker: Option<(&[u8], Language)>) -> io::Result { + pub fn new(main: Code<'_>, checker: Option>) -> io::Result { let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); fs::create_dir(&project_path)?; - let (code, language) = main; - let main_path = project_path.join(MAIN).with_extension(language.extension); - fs::write(&main_path, code)?; + let main_path = project_path + .join(MAIN) + .with_extension(main.language.extension); + fs::write(&main_path, main.content)?; - let checker_language = if let Some((code, language)) = checker { + let checker_language = if let Some(checker) = checker { let mut checker_path = project_path.join(CHECKER); - if language.is_interpreted() { - checker_path.set_extension(language.extension); + if checker.language.is_interpreted() { + checker_path.set_extension(checker.language.extension); } let mut checker_file = fs::OpenOptions::new() .create(true) @@ -50,16 +56,16 @@ impl Judge { .truncate(true) .mode(0o755) .open(&checker_path)?; - checker_file.write_all(code)?; + checker_file.write_all(checker.content)?; - Some(language) + Some(checker.language) } else { None }; Ok(Judge { project_path, - language, + language: main.language, checker_language, }) } From 2a8e28d893c2e290f28278f646a2213dab777efe Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 14:55:30 +0700 Subject: [PATCH 31/81] feat(judge): use bon builder for better constructor --- Cargo.lock | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/judge.rs | 46 ++++++++++++++++----------- 3 files changed, 117 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b961eb..863e11f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,31 @@ dependencies = [ "piper", ] +[[package]] +name = "bon" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.111", +] + [[package]] name = "borsh" version = "1.6.0" @@ -315,6 +340,41 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.111", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.111", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -391,6 +451,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "funty" version = "2.0.0" @@ -472,6 +538,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "2.12.1" @@ -502,6 +574,7 @@ dependencies = [ name = "judge-runner" version = "0.1.0" dependencies = [ + "bon", "byte-unit", "cgroups-rs", "nix 0.30.1", @@ -638,6 +711,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -972,6 +1055,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72abeda133c49d7bddece6c154728f83eec8172380c80ab7096da9487e20d27c" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index 8c7e775..84a9e8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ A code runner library for online judge system """ [dependencies] +bon = "3.8.1" byte-unit = "5.2.0" cgroups-rs = "0.5.0" nix = { version = "0.30.1", default-features = false, features = ["signal"] } diff --git a/src/judge.rs b/src/judge.rs index 7bd8969..b05b908 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -1,6 +1,7 @@ use std::{ env, fs, io::{self, Read, Write}, + marker::PhantomData, os::unix::fs::OpenOptionsExt, path::PathBuf, process::Stdio, @@ -8,6 +9,7 @@ use std::{ time::Duration, }; +use bon::bon; use state_shift::{impl_state, type_state}; use uuid::Uuid; @@ -28,27 +30,29 @@ pub struct Judge { pub checker_language: Option, } -pub struct Code<'a> { - pub content: &'a [u8], - pub language: Language, -} - -#[impl_state] -impl Judge { - #[require(Created)] - pub fn new(main: Code<'_>, checker: Option>) -> io::Result { +#[bon] +impl Judge { + #[builder] + pub fn new<'a>( + #[rustfmt::skip] + #[builder(with = |code: &'a [u8], language: Language| (code, language))] + main: (&'a [u8], Language), + + #[rustfmt::skip] + #[builder(with = |code: &'a [u8], language: Language| (code, language))] + checker: Option<(&'a [u8], Language)>, + ) -> io::Result> { let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); fs::create_dir(&project_path)?; - let main_path = project_path - .join(MAIN) - .with_extension(main.language.extension); - fs::write(&main_path, main.content)?; + let (code, language) = main; + let main_path = project_path.join(MAIN).with_extension(language.extension); + fs::write(&main_path, code)?; - let checker_language = if let Some(checker) = checker { + let checker_language = if let Some((code, language)) = checker { let mut checker_path = project_path.join(CHECKER); - if checker.language.is_interpreted() { - checker_path.set_extension(checker.language.extension); + if language.is_interpreted() { + checker_path.set_extension(language.extension); } let mut checker_file = fs::OpenOptions::new() .create(true) @@ -56,20 +60,24 @@ impl Judge { .truncate(true) .mode(0o755) .open(&checker_path)?; - checker_file.write_all(checker.content)?; + checker_file.write_all(code)?; - Some(checker.language) + Some(language) } else { None }; Ok(Judge { project_path, - language: main.language, + language, checker_language, + _state: PhantomData, }) } +} +#[impl_state] +impl Judge { #[require(Created)] #[switch_to(Compiled)] pub fn compile(self) -> io::Result, Verdict>> { From a2da992813d59896fc4218d9cea5560da919df69 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 14:58:45 +0700 Subject: [PATCH 32/81] doc: add basic example usage --- Cargo.lock | 1 + Cargo.toml | 3 +++ examples/basic.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 examples/basic.rs diff --git a/Cargo.lock b/Cargo.lock index 863e11f..e499de7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -578,6 +578,7 @@ dependencies = [ "byte-unit", "cgroups-rs", "nix 0.30.1", + "rand 0.9.2", "state-shift", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index 84a9e8a..0d53d2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,6 @@ cgroups-rs = "0.5.0" nix = { version = "0.30.1", default-features = false, features = ["signal"] } state-shift = "2.1.1" uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } + +[dev-dependencies] +rand = "0.9.2" diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..b7a04d2 --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,56 @@ +use std::time::Duration; + +use byte_unit::Byte; +use judge_runner::{Judge, Resource, language}; + +fn main() { + let checker_code = r#" +import sys +n = int(input()) +res = int(input()) + +if n == res: + sys.exit(0) +else: + sys.exit(1) + "#; + let code = r#" + #include + + using namespace std; + + int main() { + int n; + cin >> n; + cout << n << endl; + } + "#; + + let checker = Judge::builder() + .main(checker_code.as_bytes(), language::PYTHON) + .build() + .unwrap() + .compile() + .unwrap() + .unwrap() + .read_executable() + .unwrap(); + + let judge = Judge::builder() + .checker(&checker, language::PYTHON) + .main(code.as_bytes(), language::CPP) + .build() + .unwrap(); + let judge = judge.compile().unwrap().unwrap(); + + let input = rand::random::().to_string(); + let resource = Resource { + memory: Byte::MEGABYTE, + ..Default::default() + }; + let time_limit = Duration::from_secs(1); + let verdict = judge + .run(input.as_bytes(), false, resource, time_limit) + .unwrap(); + println!("{:?}", verdict); +} From 6505e74da73b613d6fa1920f9b933201a2f3304e Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 15:19:35 +0700 Subject: [PATCH 33/81] feat: add metrics --- src/lib.rs | 4 ++-- src/metrics.rs | 22 ++++++++++++++++++++++ src/verdict.rs | 10 ---------- 3 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 src/metrics.rs delete mode 100644 src/verdict.rs diff --git a/src/lib.rs b/src/lib.rs index 2dd7445..9d87791 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,12 @@ mod judge; pub mod language; mod sandbox; -mod verdict; +mod metrics; pub use judge::*; pub use language::Language; pub use sandbox::*; -pub use verdict::*; +pub use metrics::*; #[cfg(test)] mod test { diff --git a/src/metrics.rs b/src/metrics.rs new file mode 100644 index 0000000..1180dcb --- /dev/null +++ b/src/metrics.rs @@ -0,0 +1,22 @@ +use std::time::Duration; + +use byte_unit::Byte; + +#[derive(Debug)] +pub enum Verdict { + Accepted, + WrongAnswer, + TimeLimitExceeded, + CompilationError, + MemoryLimitExceeded, + RuntimeError, + IdleTimeLimitExceeded, +} + +pub struct Metrics { + pub verdict: Verdict, + pub run_time: Duration, + pub stdout: String, + pub stderr: String, + pub memory: Byte, +} diff --git a/src/verdict.rs b/src/verdict.rs deleted file mode 100644 index efde8b2..0000000 --- a/src/verdict.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[derive(Debug)] -pub enum Verdict { - Accepted, - WrongAnswer, - TimeLimitExceeded, - CompilationError, - MemoryLimitExceeded, - RuntimeError, - IdleTimeLimitExceeded, -} From 9fe33e419992e91c4c787113761b050bf179d103 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 16:05:22 +0700 Subject: [PATCH 34/81] feat(cgroup): add memory extension methods --- src/metrics.rs | 3 ++- src/sandbox/cgroup.rs | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/metrics.rs b/src/metrics.rs index 1180dcb..296fa72 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -13,10 +13,11 @@ pub enum Verdict { IdleTimeLimitExceeded, } +#[derive(Debug)] pub struct Metrics { pub verdict: Verdict, pub run_time: Duration, + pub memory_usage: Byte, pub stdout: String, pub stderr: String, - pub memory: Byte, } diff --git a/src/sandbox/cgroup.rs b/src/sandbox/cgroup.rs index 47b32f8..d4628f4 100644 --- a/src/sandbox/cgroup.rs +++ b/src/sandbox/cgroup.rs @@ -1,11 +1,14 @@ use std::time::Duration; -use cgroups_rs::fs::{Cgroup, cpu::CpuController}; +use byte_unit::Byte; +use cgroups_rs::fs::{Cgroup, cpu::CpuController, memory::MemController}; const CPU_USAGE_PREFIX: &str = "usage_usec "; pub trait CgroupExt { fn get_cpu_time(&self) -> Duration; + fn get_memory_usage(&self) -> Byte; + fn get_memory_limit(&self) -> Byte; } impl CgroupExt for Cgroup { @@ -22,4 +25,20 @@ impl CgroupExt for Cgroup { let usage = usage.parse().unwrap(); Duration::from_micros(usage) } + + fn get_memory_usage(&self) -> Byte { + // SAFETY: there must be memory controller for cgroup v2 + let memory_controller: &MemController = self.controller_of().unwrap(); + let stats = memory_controller.memory_stat(); + + Byte::from_u64(stats.usage_in_bytes) + } + + fn get_memory_limit(&self) -> Byte { + // SAFETY: there must be memory controller for cgroup v2 + let memory_controller: &MemController = self.controller_of().unwrap(); + let stats = memory_controller.memory_stat(); + + Byte::from_u64(stats.limit_in_bytes.max(0) as u64) + } } From c4ff34990de0ef578da292df107a0ab21e1e0357 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 16:08:33 +0700 Subject: [PATCH 35/81] feat(sandbox): return extra informations --- src/sandbox/mod.rs | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index 5efe4d2..8a9b842 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -9,6 +9,7 @@ use std::{ time::{Duration, Instant}, }; +use byte_unit::Byte; use cgroups_rs::{CgroupPid, fs::Cgroup}; use nix::{libc::getpid, sys::signal::Signal}; pub use resource::Resource; @@ -51,22 +52,29 @@ impl Sandbox { } } - pub fn monitor(self, mut child: Child) -> io::Result> { + pub fn monitor(&self, mut child: Child) -> io::Result<(Option, Duration, Byte)> { self.cgroup .add_task_by_tgid(CgroupPid::from(child.id() as u64)) .map_err(io::Error::other)?; let start = Instant::now(); + let mut memory_usage = Byte::default(); let mut prev_cpu_time = self.cgroup.get_cpu_time(); let mut idle_start: Option = None; + while child.try_wait()?.is_none() { let cpu_time = self.cgroup.get_cpu_time(); + memory_usage = memory_usage.max(self.cgroup.get_memory_usage()); if cpu_time.abs_diff(prev_cpu_time) <= MIN_CPU_TIME_PER_POLL { match idle_start { Some(idle_start) => { if idle_start.elapsed() >= IDLE_TIME_LIMIT { - return Ok(Some(Verdict::IdleTimeLimitExceeded)); + return Ok(( + Some(Verdict::IdleTimeLimitExceeded), + cpu_time, + memory_usage, + )); } } None => idle_start = Some(Instant::now()), @@ -76,7 +84,11 @@ impl Sandbox { } if cpu_time >= self.cpu_time_limit || start.elapsed() >= self.wall_time_limit { - return Ok(Some(Verdict::TimeLimitExceeded)); + return Ok(( + Some(Verdict::TimeLimitExceeded), + self.cpu_time_limit, + memory_usage, + )); } prev_cpu_time = cpu_time; @@ -87,11 +99,15 @@ impl Sandbox { // SAFETY: child must be finished at this point to exit the previous loop let status = child.try_wait()?.unwrap(); if status.success() { - return Ok(None); + return Ok((None, prev_cpu_time, memory_usage)); } match status.signal().and_then(|x| Signal::try_from(x).ok()) { - Some(Signal::SIGKILL) => Ok(Some(Verdict::MemoryLimitExceeded)), - _ => Ok(Some(Verdict::RuntimeError)), + Some(Signal::SIGKILL) => Ok(( + Some(Verdict::MemoryLimitExceeded), + prev_cpu_time, + self.cgroup.get_memory_limit(), + )), + _ => Ok((Some(Verdict::RuntimeError), prev_cpu_time, memory_usage)), } } } From 8707bd6655b04befe91c7a63fc116cfa98a239d6 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 16:09:10 +0700 Subject: [PATCH 36/81] feat(judge): return metrics, spawn thread in scope --- src/judge.rs | 91 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 31 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index b05b908..78f4653 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -13,7 +13,7 @@ use bon::bon; use state_shift::{impl_state, type_state}; use uuid::Uuid; -use crate::{Language, Resource, Sandbox, Verdict}; +use crate::{Language, Metrics, Resource, Sandbox, Verdict}; const MAIN: &str = "main"; const CHECKER: &str = "checker"; @@ -113,7 +113,7 @@ impl Judge { is_interactive: bool, resource: Resource, time_limit: Duration, - ) -> io::Result { + ) -> io::Result { let Judge { project_path, language, @@ -127,36 +127,57 @@ impl Judge { .current_dir(&project_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) + .stderr(Stdio::null()) .spawn()?; - let mut cin = checker.stdin.take().unwrap(); - let cout = checker.stdout.take().unwrap(); + let mut cstdin = checker.stdin.take().unwrap(); + let cstdout = checker.stdout.take().unwrap(); let sandbox = Sandbox::new(resource, time_limit)?; - let mut submission_command = language.get_run_command(MAIN); - submission_command - .current_dir(&project_path) + let mut cmd = language.get_run_command(MAIN); + cmd.current_dir(&project_path) .stdin(Stdio::piped()) - .stdout(Stdio::piped()); - let mut submission = sandbox.spawn(submission_command)?; - let mut sin = submission.stdin.take().unwrap(); - let sout = submission.stdout.take().unwrap(); - - let monitor_thread = thread::spawn(move || sandbox.monitor(submission)); - - if !is_interactive { - sin.write_all(input)?; - sin.write_all(b"\n")?; - sin.flush()?; - } - cin.write_all(input)?; - cin.write_all(b"\n")?; - cin.flush()?; - - forward(cout, sin); - forward(sout, cin); - - if let Some(verdict) = monitor_thread.join().unwrap()? { - return Ok(verdict); + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let mut main = sandbox.spawn(cmd)?; + let mut stdin = main.stdin.take().unwrap(); + let stdout = main.stdout.take().unwrap(); + let mut stderr = main.stderr.take().unwrap(); + + let ((verdict, run_time, memory_usage), stdout) = thread::scope(|scope| { + let monitor_thread = scope.spawn(|| sandbox.monitor(main)); + + if !is_interactive { + stdin.write_all(input)?; + stdin.write_all(b"\n")?; + stdin.flush()?; + } + cstdin.write_all(input)?; + cstdin.write_all(b"\n")?; + cstdin.flush()?; + + forward(cstdout, stdin); + let main_to_checker = forward(stdout, cstdin); + + let monitor_result = match monitor_thread.join().unwrap() { + Ok(v) => v, + Err(err) => return Err(err), + }; + let output = main_to_checker.join().unwrap()?; + let output = String::from_utf8(output).map_err(io::Error::other)?; + + Ok((monitor_result, output)) + })?; + let mut err = String::new(); + stderr.read_to_string(&mut err)?; + + if let Some(verdict) = verdict { + return Ok(Metrics { + verdict, + run_time, + stdout, + stderr: err, + memory_usage, + }); } let status = checker.wait()?; @@ -166,15 +187,22 @@ impl Judge { Verdict::WrongAnswer }; - Ok(verdict) + Ok(Metrics { + verdict, + run_time, + stdout, + stderr: err, + memory_usage, + }) } } fn forward( mut reader: R, mut writer: W, -) -> thread::JoinHandle> { +) -> thread::JoinHandle>> { thread::spawn(move || { + let mut stdout: Vec = vec![]; let mut buffer = [0u8; BUFFER_SIZE]; loop { let n = reader.read(&mut buffer)?; @@ -182,10 +210,11 @@ fn forward( drop(writer); break; } + stdout.extend_from_slice(&buffer[0..n]); writer.write_all(&buffer[..n])?; writer.flush()?; } - Ok(()) + Ok(stdout) }) } From 687320423347490b26d94ac6011ebc4e6ce7fcea Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sat, 27 Dec 2025 16:18:19 +0700 Subject: [PATCH 37/81] perf(sandbox): ext for controllers instead of getting controller everytime --- src/language/mod.rs | 2 -- src/sandbox/cgroup.rs | 36 ++++++++++++++---------------- src/sandbox/mod.rs | 52 ++++++++++++++++++++++++++----------------- 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/src/language/mod.rs b/src/language/mod.rs index f33fe1c..4e56d6e 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -26,7 +26,6 @@ impl Language { let args = self.compile_args?; let args = args.replace("{main}", main); let mut args = args.split_whitespace(); - // SAFETY: there is always at least 1 element let binary = args.next().unwrap(); let mut command = Command::new(binary); @@ -37,7 +36,6 @@ impl Language { pub fn get_run_command(&self, main: &str) -> Command { let args = self.run_args.replace("{main}", main); let mut args = args.split_whitespace(); - // SAFETY: there is always at least 1 element let binary = args.next().unwrap(); let mut command = Command::new(binary); diff --git a/src/sandbox/cgroup.rs b/src/sandbox/cgroup.rs index d4628f4..afc2200 100644 --- a/src/sandbox/cgroup.rs +++ b/src/sandbox/cgroup.rs @@ -1,43 +1,41 @@ use std::time::Duration; use byte_unit::Byte; -use cgroups_rs::fs::{Cgroup, cpu::CpuController, memory::MemController}; +use cgroups_rs::fs::{cpu::CpuController, memory::MemController}; const CPU_USAGE_PREFIX: &str = "usage_usec "; -pub trait CgroupExt { - fn get_cpu_time(&self) -> Duration; - fn get_memory_usage(&self) -> Byte; - fn get_memory_limit(&self) -> Byte; +pub trait CpuControllerExt { + fn usage(&self) -> Duration; } -impl CgroupExt for Cgroup { - fn get_cpu_time(&self) -> Duration { - let cpu_controller: &CpuController = self.controller_of().unwrap(); - let stats = cpu_controller.cpu().stat; +impl CpuControllerExt for CpuController { + fn usage(&self) -> Duration { + let stats = self.cpu().stat; - // SAFETY: there must be cpu usage for valid cgroup let usage = stats .lines() .find_map(|line| line.strip_prefix(CPU_USAGE_PREFIX)) .unwrap(); - // SAFETY: cpu usage must be duration in microsecond let usage = usage.parse().unwrap(); Duration::from_micros(usage) } +} + +pub trait MemControllerExt { + fn usage(&self) -> Byte; + fn limit(&self) -> Byte; +} - fn get_memory_usage(&self) -> Byte { - // SAFETY: there must be memory controller for cgroup v2 - let memory_controller: &MemController = self.controller_of().unwrap(); - let stats = memory_controller.memory_stat(); +impl MemControllerExt for MemController { + fn usage(&self) -> Byte { + let stats = self.memory_stat(); Byte::from_u64(stats.usage_in_bytes) } - fn get_memory_limit(&self) -> Byte { - // SAFETY: there must be memory controller for cgroup v2 - let memory_controller: &MemController = self.controller_of().unwrap(); - let stats = memory_controller.memory_stat(); + fn limit(&self) -> Byte { + let stats = self.memory_stat(); Byte::from_u64(stats.limit_in_bytes.max(0) as u64) } diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index 8a9b842..7a44f4e 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -10,20 +10,26 @@ use std::{ }; use byte_unit::Byte; -use cgroups_rs::{CgroupPid, fs::Cgroup}; +use cgroups_rs::{ + CgroupPid, + fs::{Cgroup, cpu::CpuController, memory::MemController}, +}; use nix::{libc::getpid, sys::signal::Signal}; pub use resource::Resource; -use crate::{Verdict, sandbox::cgroup::CgroupExt}; +use crate::{ + Verdict, + sandbox::cgroup::{CpuControllerExt, MemControllerExt}, +}; // TODO: need further tuning const POLL: Duration = Duration::from_millis(10); -const MIN_CPU_TIME_PER_POLL: Duration = Duration::from_millis(1); +const MIN_CPU_USAGE_PER_POLL: Duration = Duration::from_millis(1); const IDLE_TIME_LIMIT: Duration = Duration::from_millis(100); pub struct Sandbox { pub cgroup: Cgroup, - pub cpu_time_limit: Duration, + pub cpu_usage_limit: Duration, pub wall_time_limit: Duration, } @@ -31,7 +37,7 @@ impl Sandbox { pub fn new(resource: Resource, time_limit: Duration) -> io::Result { Ok(Sandbox { cgroup: resource.try_into()?, - cpu_time_limit: time_limit, + cpu_usage_limit: time_limit, wall_time_limit: Duration::max(time_limit * 2, time_limit + Duration::from_secs(2)), }) } @@ -56,23 +62,31 @@ impl Sandbox { self.cgroup .add_task_by_tgid(CgroupPid::from(child.id() as u64)) .map_err(io::Error::other)?; + let cpu: &CpuController = self + .cgroup + .controller_of() + .ok_or(io::Error::other("Missing cpu controller"))?; + let memory: &MemController = self + .cgroup + .controller_of() + .ok_or(io::Error::other("Missing memory controller"))?; let start = Instant::now(); let mut memory_usage = Byte::default(); - let mut prev_cpu_time = self.cgroup.get_cpu_time(); + let mut prev_cpu_usage = cpu.usage(); let mut idle_start: Option = None; while child.try_wait()?.is_none() { - let cpu_time = self.cgroup.get_cpu_time(); - memory_usage = memory_usage.max(self.cgroup.get_memory_usage()); + let cpu_usage = cpu.usage(); + memory_usage = memory_usage.max(memory.usage()); - if cpu_time.abs_diff(prev_cpu_time) <= MIN_CPU_TIME_PER_POLL { + if cpu_usage.abs_diff(prev_cpu_usage) <= MIN_CPU_USAGE_PER_POLL { match idle_start { Some(idle_start) => { if idle_start.elapsed() >= IDLE_TIME_LIMIT { return Ok(( Some(Verdict::IdleTimeLimitExceeded), - cpu_time, + cpu_usage, memory_usage, )); } @@ -83,41 +97,37 @@ impl Sandbox { idle_start = None; } - if cpu_time >= self.cpu_time_limit || start.elapsed() >= self.wall_time_limit { + if cpu_usage >= self.cpu_usage_limit || start.elapsed() >= self.wall_time_limit { return Ok(( Some(Verdict::TimeLimitExceeded), - self.cpu_time_limit, + self.cpu_usage_limit, memory_usage, )); } - prev_cpu_time = cpu_time; + prev_cpu_usage = cpu_usage; sleep(POLL); } - // SAFETY: child must be finished at this point to exit the previous loop let status = child.try_wait()?.unwrap(); if status.success() { - return Ok((None, prev_cpu_time, memory_usage)); + return Ok((None, prev_cpu_usage, memory_usage)); } match status.signal().and_then(|x| Signal::try_from(x).ok()) { Some(Signal::SIGKILL) => Ok(( Some(Verdict::MemoryLimitExceeded), - prev_cpu_time, - self.cgroup.get_memory_limit(), + prev_cpu_usage, + memory.limit(), )), - _ => Ok((Some(Verdict::RuntimeError), prev_cpu_time, memory_usage)), + _ => Ok((Some(Verdict::RuntimeError), prev_cpu_usage, memory_usage)), } } } impl Drop for Sandbox { fn drop(&mut self) { - // SAFETY: always be used with stable version of linux kernel let _ = self.cgroup.kill(); - - // SAFETY: no descendant is created previously by judge let _ = self.cgroup.delete(); } } From 83de2311e4f82efe9e0bfb68db81845777e3bcfa Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 01:28:51 +0700 Subject: [PATCH 38/81] fix(judge): break on write error to prevent writing after process exit --- src/judge.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index 78f4653..7c3065f 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -207,13 +207,15 @@ fn forward( loop { let n = reader.read(&mut buffer)?; if n == 0 { - drop(writer); + break; + } + if writer.write_all(&buffer[..n]).is_err() { break; } stdout.extend_from_slice(&buffer[0..n]); - writer.write_all(&buffer[..n])?; writer.flush()?; } + drop(writer); Ok(stdout) }) From 20d3736ec4f1e15baab4d826b75a7ac34659f484 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 01:55:10 +0700 Subject: [PATCH 39/81] doc: add README --- README.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/README.md b/README.md index e69de29..047b267 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,77 @@ +# Judge Runner + +A code runner library for online judge system. + +## Supported Languages + +- C++ +- Java +- JavaScript +- Python +- Rust +- TypeScript + +## Usage + +```rust +use std::time::Duration; + +use byte_unit::Byte; +use judge_runner::{Judge, Resource, language}; + +fn main() { + let checker_code = r#" +import sys +n = int(input()) +res = int(input()) + +if n == res: + sys.exit(0) +else: + sys.exit(1) + "#; + let code = r#" + #include + + using namespace std; + + int main() { + int n; + cin >> n; + cout << n << endl; + } + "#; + + let checker = Judge::builder() + .main(checker_code.as_bytes(), language::PYTHON) + .build() + .unwrap() + .compile() + .unwrap() + .unwrap() + .read_executable() + .unwrap(); + + let judge = Judge::builder() + .checker(&checker, language::PYTHON) + .main(code.as_bytes(), language::CPP) + .build() + .unwrap(); + let judge = judge.compile().unwrap().unwrap(); + + let input = rand::random::().to_string(); + let resource = Resource { + memory: Byte::MEGABYTE, + ..Default::default() + }; + let time_limit = Duration::from_secs(1); + let verdict = judge + .run(input.as_bytes(), false, resource, time_limit) + .unwrap(); + println!("{:#?}", verdict); +} +``` + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. From 5909fac3eb60ccd9d92094f3359776c3b1f33378 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 01:55:52 +0700 Subject: [PATCH 40/81] chore: format --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9d87791..be379f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,12 @@ mod judge; pub mod language; -mod sandbox; mod metrics; +mod sandbox; pub use judge::*; pub use language::Language; -pub use sandbox::*; pub use metrics::*; +pub use sandbox::*; #[cfg(test)] mod test { From 41ac77ed3a22dc7819184cfda37f8941e2b5875e Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 13:27:42 +0700 Subject: [PATCH 41/81] fix(judge): prevent infinite waitting by adding timeout to read --- Cargo.lock | 22 +++++++++++++++++++++ Cargo.toml | 1 + src/judge.rs | 54 +++++++++++++++++++++++++++------------------------- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e499de7..29a4db3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,6 +580,7 @@ dependencies = [ "nix 0.30.1", "rand 0.9.2", "state-shift", + "timeout-readwrite", "uuid", ] @@ -628,6 +629,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nix" version = "0.30.1" @@ -1123,6 +1136,15 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "timeout-readwrite" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f99014ef5f7ad8fb986e0885b612ac6bd74a87c8df9816c27de5dcc49029646" +dependencies = [ + "nix 0.29.0", +] + [[package]] name = "tinyvec" version = "1.10.0" diff --git a/Cargo.toml b/Cargo.toml index 0d53d2b..d470c9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ byte-unit = "5.2.0" cgroups-rs = "0.5.0" nix = { version = "0.30.1", default-features = false, features = ["signal"] } state-shift = "2.1.1" +timeout-readwrite = "0.4.0" uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } [dev-dependencies] diff --git a/src/judge.rs b/src/judge.rs index 7c3065f..db9f6d9 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -2,7 +2,7 @@ use std::{ env, fs, io::{self, Read, Write}, marker::PhantomData, - os::unix::fs::OpenOptionsExt, + os::{fd::AsFd, unix::fs::OpenOptionsExt}, path::PathBuf, process::Stdio, thread, @@ -11,6 +11,7 @@ use std::{ use bon::bon; use state_shift::{impl_state, type_state}; +use timeout_readwrite::{TimeoutReadExt, TimeoutReader}; use uuid::Uuid; use crate::{Language, Metrics, Resource, Sandbox, Verdict}; @@ -18,6 +19,7 @@ use crate::{Language, Metrics, Resource, Sandbox, Verdict}; const MAIN: &str = "main"; const CHECKER: &str = "checker"; const BUFFER_SIZE: usize = 512; +const READ_TIMEOUT: Duration = Duration::from_millis(100); #[type_state( states = (Created, Compiled), @@ -130,7 +132,7 @@ impl Judge { .stderr(Stdio::null()) .spawn()?; let mut cstdin = checker.stdin.take().unwrap(); - let cstdout = checker.stdout.take().unwrap(); + let cstdout = checker.stdout.take().unwrap().with_timeout(READ_TIMEOUT); let sandbox = Sandbox::new(resource, time_limit)?; let mut cmd = language.get_run_command(MAIN); @@ -140,8 +142,8 @@ impl Judge { .stderr(Stdio::piped()); let mut main = sandbox.spawn(cmd)?; let mut stdin = main.stdin.take().unwrap(); - let stdout = main.stdout.take().unwrap(); - let mut stderr = main.stderr.take().unwrap(); + let stdout = main.stdout.take().unwrap().with_timeout(READ_TIMEOUT); + let mut stderr = main.stderr.take().unwrap().with_timeout(READ_TIMEOUT); let ((verdict, run_time, memory_usage), stdout) = thread::scope(|scope| { let monitor_thread = scope.spawn(|| sandbox.monitor(main)); @@ -155,8 +157,8 @@ impl Judge { cstdin.write_all(b"\n")?; cstdin.flush()?; - forward(cstdout, stdin); - let main_to_checker = forward(stdout, cstdin); + scope.spawn(|| forward(cstdout, stdin)); + let main_to_checker = scope.spawn(|| forward(stdout, cstdin)); let monitor_result = match monitor_thread.join().unwrap() { Ok(v) => v, @@ -168,7 +170,8 @@ impl Judge { Ok((monitor_result, output)) })?; let mut err = String::new(); - stderr.read_to_string(&mut err)?; + // TODO: handle error + let _ = stderr.read_to_string(&mut err); if let Some(verdict) = verdict { return Ok(Metrics { @@ -197,26 +200,25 @@ impl Judge { } } -fn forward( - mut reader: R, +fn forward( + mut reader: TimeoutReader, mut writer: W, -) -> thread::JoinHandle>> { - thread::spawn(move || { - let mut stdout: Vec = vec![]; - let mut buffer = [0u8; BUFFER_SIZE]; - loop { - let n = reader.read(&mut buffer)?; - if n == 0 { - break; - } - if writer.write_all(&buffer[..n]).is_err() { - break; - } - stdout.extend_from_slice(&buffer[0..n]); - writer.flush()?; +) -> io::Result> { + let mut stdout: Vec = vec![]; + let mut buffer = [0u8; BUFFER_SIZE]; + loop { + // TODO:handle error + let n = match reader.read(&mut buffer) { + Ok(0) | Err(_) => break, + Ok(n) => n, + }; + if writer.write_all(&buffer[..n]).is_err() { + break; } - drop(writer); + stdout.extend_from_slice(&buffer[0..n]); + writer.flush()?; + } + drop(writer); - Ok(stdout) - }) + Ok(stdout) } From 73045fb18c526f81cb67902dc99a25ecaa45f893 Mon Sep 17 00:00:00 2001 From: TCO46 Date: Sun, 28 Dec 2025 14:10:58 +0700 Subject: [PATCH 42/81] refactor: remove nix --- Cargo.lock | 1 - Cargo.toml | 1 - src/sandbox/mod.rs | 9 +++++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 29a4db3..a9c2092 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -577,7 +577,6 @@ dependencies = [ "bon", "byte-unit", "cgroups-rs", - "nix 0.30.1", "rand 0.9.2", "state-shift", "timeout-readwrite", diff --git a/Cargo.toml b/Cargo.toml index d470c9d..73d2759 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ A code runner library for online judge system bon = "3.8.1" byte-unit = "5.2.0" cgroups-rs = "0.5.0" -nix = { version = "0.30.1", default-features = false, features = ["signal"] } state-shift = "2.1.1" timeout-readwrite = "0.4.0" uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index 7a44f4e..9376e64 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -4,6 +4,7 @@ mod resource; use std::{ io, os::unix::process::{CommandExt, ExitStatusExt}, + process, process::{Child, Command}, thread::sleep, time::{Duration, Instant}, @@ -14,7 +15,6 @@ use cgroups_rs::{ CgroupPid, fs::{Cgroup, cpu::CpuController, memory::MemController}, }; -use nix::{libc::getpid, sys::signal::Signal}; pub use resource::Resource; use crate::{ @@ -48,7 +48,7 @@ impl Sandbox { unsafe { command .pre_exec(move || { - let id = getpid(); + let id = process::id(); cgroup .add_task_by_tgid(CgroupPid::from(id as u64)) @@ -114,8 +114,9 @@ impl Sandbox { if status.success() { return Ok((None, prev_cpu_usage, memory_usage)); } - match status.signal().and_then(|x| Signal::try_from(x).ok()) { - Some(Signal::SIGKILL) => Ok(( + match status.signal() { + // SIGKILL + Some(9) => Ok(( Some(Verdict::MemoryLimitExceeded), prev_cpu_usage, memory.limit(), From 9ca725ba36c004c64ebe3e8975703239f16aa2c2 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 14:41:20 +0700 Subject: [PATCH 43/81] deps: add tokio --- Cargo.lock | 209 ++++++++++++++++++++++++++++++++++++++++++++++------- Cargo.toml | 2 +- 2 files changed, 183 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9c2092..24a62d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,7 +72,7 @@ dependencies = [ "polling", "rustix", "slab", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -130,7 +130,7 @@ dependencies = [ "rustix", "signal-hook-registry", "slab", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -421,7 +421,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -579,7 +579,7 @@ dependencies = [ "cgroups-rs", "rand 0.9.2", "state-shift", - "timeout-readwrite", + "tokio", "uuid", ] @@ -595,6 +595,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -617,26 +626,25 @@ dependencies = [ ] [[package]] -name = "nix" -version = "0.25.1" +name = "mio" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", "libc", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "nix" -version = "0.29.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ - "bitflags 2.10.0", + "autocfg", + "bitflags 1.3.2", "cfg-if", - "cfg_aliases", "libc", ] @@ -684,6 +692,29 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -712,7 +743,7 @@ dependencies = [ "hermit-abi", "pin-project-lite", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -852,6 +883,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -936,7 +976,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -963,6 +1003,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "seahash" version = "4.1.0" @@ -1044,6 +1090,22 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "state-shift" version = "2.1.1" @@ -1112,7 +1174,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1135,15 +1197,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "timeout-readwrite" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f99014ef5f7ad8fb986e0885b612ac6bd74a87c8df9816c27de5dcc49029646" -dependencies = [ - "nix 0.29.0", -] - [[package]] name = "tinyvec" version = "1.10.0" @@ -1159,6 +1212,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -1350,6 +1431,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1359,6 +1449,71 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.14" @@ -1410,7 +1565,7 @@ dependencies = [ "tracing", "uds_windows", "uuid", - "windows-sys", + "windows-sys 0.61.2", "winnow", "zbus_macros", "zbus_names", diff --git a/Cargo.toml b/Cargo.toml index 73d2759..7e554bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ bon = "3.8.1" byte-unit = "5.2.0" cgroups-rs = "0.5.0" state-shift = "2.1.1" -timeout-readwrite = "0.4.0" +tokio = { version = "1.48.0", features = ["full"] } uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } [dev-dependencies] From 96c9117d4a41ff599ac8dcf34f6d8f0a0fd6f481 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 14:41:34 +0700 Subject: [PATCH 44/81] feat(sandbox): add async to functions --- src/sandbox/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index 9376e64..c1a06d1 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -3,13 +3,14 @@ mod resource; use std::{ io, - os::unix::process::{CommandExt, ExitStatusExt}, + os::unix::process::ExitStatusExt, process, - process::{Child, Command}, thread::sleep, time::{Duration, Instant}, }; +use tokio::process::{Child, Command}; + use byte_unit::Byte; use cgroups_rs::{ CgroupPid, @@ -58,9 +59,12 @@ impl Sandbox { } } - pub fn monitor(&self, mut child: Child) -> io::Result<(Option, Duration, Byte)> { + pub async fn monitor(&self, mut child: Child) -> io::Result<(Option, Duration, Byte)> { + let Some(id) = child.id() else { + return Err(io::Error::other("Child exited")); + }; self.cgroup - .add_task_by_tgid(CgroupPid::from(child.id() as u64)) + .add_task_by_tgid(CgroupPid::from(id as u64)) .map_err(io::Error::other)?; let cpu: &CpuController = self .cgroup From 588cf588de8151622b8f77b97d3c5739b2c691a1 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 14:41:53 +0700 Subject: [PATCH 45/81] feat(language): return tokio::process::Command --- src/language/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/mod.rs b/src/language/mod.rs index 4e56d6e..85dd388 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -5,7 +5,7 @@ mod python; mod rust; mod typescript; -use std::process::Command; +use tokio::process::Command; pub use cpp::CPP; pub use java::JAVA; From 262def2dad7a360e212109b145d9979c26e5bea1 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 14:42:11 +0700 Subject: [PATCH 46/81] feat(judge): use tokio join and tokio select --- src/judge.rs | 221 +++++++++++++++++++++++++++++---------------------- 1 file changed, 127 insertions(+), 94 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index db9f6d9..3c77e57 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -1,17 +1,13 @@ -use std::{ - env, fs, - io::{self, Read, Write}, - marker::PhantomData, - os::{fd::AsFd, unix::fs::OpenOptionsExt}, - path::PathBuf, - process::Stdio, - thread, - time::Duration, -}; +use std::{env, io, marker::PhantomData, path::PathBuf, process::Stdio, time::Duration}; use bon::bon; +use byte_unit::Byte; use state_shift::{impl_state, type_state}; -use timeout_readwrite::{TimeoutReadExt, TimeoutReader}; +use tokio::{ + fs, + io::{AsyncReadExt, AsyncWriteExt}, + time::sleep, +}; use uuid::Uuid; use crate::{Language, Metrics, Resource, Sandbox, Verdict}; @@ -19,7 +15,6 @@ use crate::{Language, Metrics, Resource, Sandbox, Verdict}; const MAIN: &str = "main"; const CHECKER: &str = "checker"; const BUFFER_SIZE: usize = 512; -const READ_TIMEOUT: Duration = Duration::from_millis(100); #[type_state( states = (Created, Compiled), @@ -35,7 +30,7 @@ pub struct Judge { #[bon] impl Judge { #[builder] - pub fn new<'a>( + pub async fn new<'a>( #[rustfmt::skip] #[builder(with = |code: &'a [u8], language: Language| (code, language))] main: (&'a [u8], Language), @@ -45,34 +40,40 @@ impl Judge { checker: Option<(&'a [u8], Language)>, ) -> io::Result> { let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); - fs::create_dir(&project_path)?; + fs::create_dir(&project_path).await?; + + tokio::try_join! { + async { + let (code, language) = main; + let main_path = project_path.join(MAIN).with_extension(language.extension); + fs::write(&main_path, code).await?; - let (code, language) = main; - let main_path = project_path.join(MAIN).with_extension(language.extension); - fs::write(&main_path, code)?; + Ok::<_, io::Error>(()) + }, + async { + if let Some((code, language)) = checker { + let mut checker_path = project_path.join(CHECKER); + if language.is_interpreted() { + checker_path.set_extension(language.extension); + } + let mut checker_file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .mode(0o755) + .open(&checker_path) + .await?; + checker_file.write_all(code).await?; + } - let checker_language = if let Some((code, language)) = checker { - let mut checker_path = project_path.join(CHECKER); - if language.is_interpreted() { - checker_path.set_extension(language.extension); + Ok(()) } - let mut checker_file = fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .mode(0o755) - .open(&checker_path)?; - checker_file.write_all(code)?; - - Some(language) - } else { - None - }; + }?; Ok(Judge { project_path, - language, - checker_language, + language: main.1, + checker_language: checker.map(|checker| checker.1), _state: PhantomData, }) } @@ -82,10 +83,10 @@ impl Judge { impl Judge { #[require(Created)] #[switch_to(Compiled)] - pub fn compile(self) -> io::Result, Verdict>> { + pub async fn compile(self) -> io::Result, Verdict>> { if let Some(mut cmd) = self.language.get_compile_command(MAIN) { let mut process = cmd.current_dir(&self.project_path).spawn()?; - let status = process.wait()?; + let status = process.wait().await?; if !status.success() { return Ok(Err(Verdict::CompilationError)); } @@ -99,17 +100,17 @@ impl Judge { } #[require(Compiled)] - pub fn read_executable(&self) -> io::Result> { + pub async fn read_executable(&self) -> io::Result> { let mut path = self.project_path.join(MAIN); if self.language.is_interpreted() { path.set_extension(self.language.extension); } - fs::read(path) + fs::read(path).await } #[require(Compiled)] - pub fn run( + pub async fn run( self, input: &[u8], is_interactive: bool, @@ -132,7 +133,7 @@ impl Judge { .stderr(Stdio::null()) .spawn()?; let mut cstdin = checker.stdin.take().unwrap(); - let cstdout = checker.stdout.take().unwrap().with_timeout(READ_TIMEOUT); + let mut cstdout = checker.stdout.take().unwrap(); let sandbox = Sandbox::new(resource, time_limit)?; let mut cmd = language.get_run_command(MAIN); @@ -142,48 +143,103 @@ impl Judge { .stderr(Stdio::piped()); let mut main = sandbox.spawn(cmd)?; let mut stdin = main.stdin.take().unwrap(); - let stdout = main.stdout.take().unwrap().with_timeout(READ_TIMEOUT); - let mut stderr = main.stderr.take().unwrap().with_timeout(READ_TIMEOUT); + let mut stdout = main.stdout.take().unwrap(); + let mut stderr = main.stderr.take().unwrap(); + + let monitor = tokio::spawn(async move { sandbox.monitor(main).await }); + + tokio::try_join! { + async { + if !is_interactive { + stdin.write_all(input).await?; + stdin.write_all(b"\n").await?; + stdin.flush().await?; + } - let ((verdict, run_time, memory_usage), stdout) = thread::scope(|scope| { - let monitor_thread = scope.spawn(|| sandbox.monitor(main)); + Ok::<_, io::Error>(()) + }, + async { + cstdin.write_all(input).await?; + cstdin.write_all(b"\n").await?; + cstdin.flush().await?; - if !is_interactive { - stdin.write_all(input)?; - stdin.write_all(b"\n")?; - stdin.flush()?; + Ok::<_, io::Error>(()) } - cstdin.write_all(input)?; - cstdin.write_all(b"\n")?; - cstdin.flush()?; - - scope.spawn(|| forward(cstdout, stdin)); - let main_to_checker = scope.spawn(|| forward(stdout, cstdin)); - - let monitor_result = match monitor_thread.join().unwrap() { - Ok(v) => v, - Err(err) => return Err(err), - }; - let output = main_to_checker.join().unwrap()?; - let output = String::from_utf8(output).map_err(io::Error::other)?; - - Ok((monitor_result, output)) - })?; - let mut err = String::new(); - // TODO: handle error - let _ = stderr.read_to_string(&mut err); + }?; + + let mut out: Vec = vec![]; + let mut err: Vec = vec![]; + let mut verdict: Option = None; + let mut run_time: Duration = Duration::default(); + let mut memory_usage: Byte = Byte::default(); + tokio::select! { + monitor_result = monitor => { + let monitor_result = monitor_result.unwrap()?; + (verdict, run_time, memory_usage) = monitor_result; + } + err = async { + let mut buffer = [0u8; BUFFER_SIZE]; + loop { + let n = stdout.read(&mut buffer).await?; + if n == 0 { + break; + } + if cstdin.write_all(&buffer[..n]).await.is_err() { + break; + } + cstdin.flush().await?; + out.extend_from_slice(&buffer[0..n]); + } + + // sleep indefinitely until sandbox return + sleep(Duration::MAX).await; + + Ok::<_, io::Error>(()) + } => { err? } + err = async { + let mut buffer = [0u8; BUFFER_SIZE]; + loop { + let n = cstdout.read(&mut buffer).await?; + if n == 0 { break; } + if stdin.write_all(&buffer[..n]).await.is_err() { + break; + } + stdin.flush().await?; + } + + // sleep indefinitely until sandbox return + sleep(Duration::MAX).await; + + Ok::<_, io::Error>(()) + } => { err? } + err = async { + let mut buffer = [0u8; BUFFER_SIZE]; + loop { + let n = stderr.read(&mut buffer).await?; + if n == 0 { break; } + err.extend_from_slice(&buffer[0..n]); + } + + // sleep indefinitely until sandbox return + sleep(Duration::MAX).await; + + Ok::<_, io::Error>(()) + } => { err? } + }; + let out = String::from_utf8(out).map_err(io::Error::other)?; + let err = String::from_utf8(err).map_err(io::Error::other)?; if let Some(verdict) = verdict { return Ok(Metrics { verdict, run_time, - stdout, + stdout: out, stderr: err, memory_usage, }); } - let status = checker.wait()?; + let status = checker.wait().await?; let verdict = if status.success() { Verdict::Accepted } else { @@ -193,32 +249,9 @@ impl Judge { Ok(Metrics { verdict, run_time, - stdout, + stdout: out, stderr: err, memory_usage, }) } } - -fn forward( - mut reader: TimeoutReader, - mut writer: W, -) -> io::Result> { - let mut stdout: Vec = vec![]; - let mut buffer = [0u8; BUFFER_SIZE]; - loop { - // TODO:handle error - let n = match reader.read(&mut buffer) { - Ok(0) | Err(_) => break, - Ok(n) => n, - }; - if writer.write_all(&buffer[..n]).is_err() { - break; - } - stdout.extend_from_slice(&buffer[0..n]); - writer.flush()?; - } - drop(writer); - - Ok(stdout) -} From 86d02bf6952c9afb83c64c3a006a5721f4e764da Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 14:43:51 +0700 Subject: [PATCH 47/81] feat: adapt basic example --- examples/basic.rs | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index b7a04d2..18f0928 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -3,7 +3,8 @@ use std::time::Duration; use byte_unit::Byte; use judge_runner::{Judge, Resource, language}; -fn main() { +#[tokio::main] +async fn main() { let checker_code = r#" import sys n = int(input()) @@ -15,42 +16,42 @@ else: sys.exit(1) "#; let code = r#" - #include - - using namespace std; - - int main() { - int n; - cin >> n; - cout << n << endl; - } - "#; +#include + +using namespace std; + +int main() { + int n; + cin >> n; + cout << n << endl; +} +"#; let checker = Judge::builder() .main(checker_code.as_bytes(), language::PYTHON) .build() - .unwrap() - .compile() - .unwrap() - .unwrap() - .read_executable() + .await .unwrap(); + let checker = checker.compile().await.unwrap().unwrap(); + let checker = checker.read_executable().await.unwrap(); let judge = Judge::builder() .checker(&checker, language::PYTHON) .main(code.as_bytes(), language::CPP) .build() + .await .unwrap(); - let judge = judge.compile().unwrap().unwrap(); + let judge = judge.compile().await.unwrap().unwrap(); - let input = rand::random::().to_string(); + let input = "4"; let resource = Resource { - memory: Byte::MEGABYTE, + memory: Byte::MEBIBYTE.multiply(1030).unwrap(), ..Default::default() }; let time_limit = Duration::from_secs(1); let verdict = judge .run(input.as_bytes(), false, resource, time_limit) + .await .unwrap(); - println!("{:?}", verdict); + println!("{:#?}", verdict); } From 5f5d9789a315d6f0d76d0d6bd7406aa5cd3fb660 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 14:44:19 +0700 Subject: [PATCH 48/81] doc: update new example code to README --- README.md | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 047b267..5e0cdf1 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ use std::time::Duration; use byte_unit::Byte; use judge_runner::{Judge, Resource, language}; -fn main() { +#[tokio::main] +async fn main() { let checker_code = r#" import sys n = int(input()) @@ -31,42 +32,42 @@ else: sys.exit(1) "#; let code = r#" - #include - - using namespace std; +#include - int main() { - int n; - cin >> n; - cout << n << endl; - } - "#; +using namespace std; + +int main() { + int n; + cin >> n; + cout << n << endl; +} +"#; let checker = Judge::builder() .main(checker_code.as_bytes(), language::PYTHON) .build() - .unwrap() - .compile() - .unwrap() - .unwrap() - .read_executable() + .await .unwrap(); + let checker = checker.compile().await.unwrap().unwrap(); + let checker = checker.read_executable().await.unwrap(); let judge = Judge::builder() .checker(&checker, language::PYTHON) .main(code.as_bytes(), language::CPP) .build() + .await .unwrap(); - let judge = judge.compile().unwrap().unwrap(); + let judge = judge.compile().await.unwrap().unwrap(); - let input = rand::random::().to_string(); + let input = "4"; let resource = Resource { - memory: Byte::MEGABYTE, + memory: Byte::MEBIBYTE.multiply(1030).unwrap(), ..Default::default() }; let time_limit = Duration::from_secs(1); let verdict = judge .run(input.as_bytes(), false, resource, time_limit) + .await .unwrap(); println!("{:#?}", verdict); } From d71c34d85bc18632a089445ade8833a98336da33 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 15:06:57 +0700 Subject: [PATCH 49/81] fix: capitalize file name for java --- src/judge.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index 3c77e57..e71d041 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -12,8 +12,8 @@ use uuid::Uuid; use crate::{Language, Metrics, Resource, Sandbox, Verdict}; -const MAIN: &str = "main"; -const CHECKER: &str = "checker"; +const MAIN: &str = "Main"; +const CHECKER: &str = "Checker"; const BUFFER_SIZE: usize = 512; #[type_state( From 30801114416c78c8f9d8b5adab0c514116760251 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 20:32:47 +0700 Subject: [PATCH 50/81] fix(judge): &self instead of self --- src/judge.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index e71d041..8dd774b 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -12,8 +12,8 @@ use uuid::Uuid; use crate::{Language, Metrics, Resource, Sandbox, Verdict}; -const MAIN: &str = "Main"; -const CHECKER: &str = "Checker"; +const MAIN: &str = "main"; +const CHECKER: &str = "checker"; const BUFFER_SIZE: usize = 512; #[type_state( @@ -111,7 +111,7 @@ impl Judge { #[require(Compiled)] pub async fn run( - self, + &self, input: &[u8], is_interactive: bool, resource: Resource, @@ -127,7 +127,7 @@ impl Judge { let checker_language = checker_language.ok_or(io::Error::other("Missing checker"))?; let mut checker = checker_language .get_run_command(CHECKER) - .current_dir(&project_path) + .current_dir(project_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) @@ -137,7 +137,7 @@ impl Judge { let sandbox = Sandbox::new(resource, time_limit)?; let mut cmd = language.get_run_command(MAIN); - cmd.current_dir(&project_path) + cmd.current_dir(project_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); @@ -145,16 +145,16 @@ impl Judge { let mut stdin = main.stdin.take().unwrap(); let mut stdout = main.stdout.take().unwrap(); let mut stderr = main.stderr.take().unwrap(); + if !is_interactive { + stdin.write_all(input).await?; + stdin.write_all(b"\n").await?; + stdin.flush().await?; + } let monitor = tokio::spawn(async move { sandbox.monitor(main).await }); tokio::try_join! { async { - if !is_interactive { - stdin.write_all(input).await?; - stdin.write_all(b"\n").await?; - stdin.flush().await?; - } Ok::<_, io::Error>(()) }, From 5b1a350bbbd13b9958ec2c9ae76f8f42be3ad65c Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 20:33:48 +0700 Subject: [PATCH 51/81] fix(judge): &self instead of self --- src/judge.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index 8dd774b..6e88e66 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -145,16 +145,16 @@ impl Judge { let mut stdin = main.stdin.take().unwrap(); let mut stdout = main.stdout.take().unwrap(); let mut stderr = main.stderr.take().unwrap(); - if !is_interactive { - stdin.write_all(input).await?; - stdin.write_all(b"\n").await?; - stdin.flush().await?; - } let monitor = tokio::spawn(async move { sandbox.monitor(main).await }); tokio::try_join! { async { + if !is_interactive { + stdin.write_all(input).await?; + stdin.write_all(b"\n").await?; + stdin.flush().await?; + } Ok::<_, io::Error>(()) }, From 261f542ff7d6bd3d83be0cafcadd6707a723e245 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 20:34:15 +0700 Subject: [PATCH 52/81] feat(metrics): add Eq --- src/metrics.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/metrics.rs b/src/metrics.rs index 296fa72..aae224a 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -2,7 +2,7 @@ use std::time::Duration; use byte_unit::Byte; -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum Verdict { Accepted, WrongAnswer, From 9410e6c375b7755ff7cf6ab34d94213ec79db12a Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Sun, 28 Dec 2025 23:41:42 +0700 Subject: [PATCH 53/81] feat(test): add basic test --- Cargo.lock | 138 ++++++++++++++++++++ Cargo.toml | 1 + examples/basic.rs | 31 +++-- tests/problem/hello-world/checker/main.cpp | 13 ++ tests/problem/hello-world/input/1.txt | 1 + tests/problem/hello-world/solution/main.cpp | 9 ++ tests/should_return_accepted.rs | 46 +++++++ tests/util.rs | 46 +++++++ 8 files changed, 272 insertions(+), 13 deletions(-) create mode 100644 tests/problem/hello-world/checker/main.cpp create mode 100644 tests/problem/hello-world/input/1.txt create mode 100644 tests/problem/hello-world/solution/main.cpp create mode 100644 tests/should_return_accepted.rs create mode 100644 tests/util.rs diff --git a/Cargo.lock b/Cargo.lock index 24a62d0..79c044f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -488,6 +497,43 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -511,6 +557,12 @@ dependencies = [ "wasip2", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "hashbrown" version = "0.12.3" @@ -578,6 +630,7 @@ dependencies = [ "byte-unit", "cgroups-rs", "rand 0.9.2", + "rstest", "state-shift", "tokio", "uuid", @@ -721,6 +774,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "piper" version = "0.2.4" @@ -912,6 +971,41 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "rend" version = "0.4.2" @@ -950,6 +1044,35 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.111", + "unicode-ident", +] + [[package]] name = "rust_decimal" version = "1.39.0" @@ -966,6 +1089,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.2" @@ -1015,6 +1147,12 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index 7e554bf..d1f27c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,4 @@ uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } [dev-dependencies] rand = "0.9.2" +rstest = "0.26.1" diff --git a/examples/basic.rs b/examples/basic.rs index 18f0928..eec46ed 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -6,14 +6,19 @@ use judge_runner::{Judge, Resource, language}; #[tokio::main] async fn main() { let checker_code = r#" -import sys -n = int(input()) -res = int(input()) - -if n == res: - sys.exit(0) -else: - sys.exit(1) +#include + +using namespace std; + +int main() { + string s, res; + cin >> s >> res; + if (s == res) { + return 0; + } else { + return 1; + } +} "#; let code = r#" #include @@ -21,14 +26,14 @@ else: using namespace std; int main() { - int n; - cin >> n; - cout << n << endl; + string s; + cin >> s; + cout << s << endl; } "#; let checker = Judge::builder() - .main(checker_code.as_bytes(), language::PYTHON) + .main(checker_code.as_bytes(), language::CPP) .build() .await .unwrap(); @@ -36,7 +41,7 @@ int main() { let checker = checker.read_executable().await.unwrap(); let judge = Judge::builder() - .checker(&checker, language::PYTHON) + .checker(&checker, language::CPP) .main(code.as_bytes(), language::CPP) .build() .await diff --git a/tests/problem/hello-world/checker/main.cpp b/tests/problem/hello-world/checker/main.cpp new file mode 100644 index 0000000..467fc7f --- /dev/null +++ b/tests/problem/hello-world/checker/main.cpp @@ -0,0 +1,13 @@ +#include + +using namespace std; + +int main() { + string s, res; + cin >> s >> res; + if (s == res) { + return 0; + } else { + return 1; + } +} diff --git a/tests/problem/hello-world/input/1.txt b/tests/problem/hello-world/input/1.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/problem/hello-world/input/1.txt @@ -0,0 +1 @@ + diff --git a/tests/problem/hello-world/solution/main.cpp b/tests/problem/hello-world/solution/main.cpp new file mode 100644 index 0000000..b05c2e7 --- /dev/null +++ b/tests/problem/hello-world/solution/main.cpp @@ -0,0 +1,9 @@ +#include + +using namespace std; + +int main() { + string s; + cin >> s; + cout << s << endl; +} diff --git a/tests/should_return_accepted.rs b/tests/should_return_accepted.rs new file mode 100644 index 0000000..a67671f --- /dev/null +++ b/tests/should_return_accepted.rs @@ -0,0 +1,46 @@ +mod util; + +use std::path::Path; +use std::time::Duration; + +use byte_unit::Byte; +use judge_runner::{Judge, Language, Resource, Verdict, language::*}; +use rstest::rstest; + +#[rstest] +#[tokio::test] +pub async fn should_return_accepted( + #[rustfmt::skip] + #[values(CPP)] + language: Language, + + #[dirs] + #[files("tests/problem/*")] + #[by_ref] + problem: &Path, +) { + let inputs = util::read_inputs(problem); + let checker = util::read_checker(problem, CPP).await; + let solution = util::read_solution(problem, language); + let resource = Resource { + memory: Byte::GIGABYTE, + ..Default::default() + }; + let time_limit = Duration::from_secs(1); + + let judge = Judge::builder() + .checker(&checker, CPP) + .main(&solution, CPP) + .build() + .await + .unwrap(); + let judge = judge.compile().await.unwrap().unwrap(); + + for input in inputs { + let metrics = judge + .run(input.as_bytes(), false, resource, time_limit) + .await + .unwrap(); + assert_eq!(metrics.verdict, Verdict::Accepted); + } +} diff --git a/tests/util.rs b/tests/util.rs new file mode 100644 index 0000000..4981955 --- /dev/null +++ b/tests/util.rs @@ -0,0 +1,46 @@ +use std::{fs, path::Path}; + +use judge_runner::{Judge, Language}; + +const INPUT: &str = "input"; +const SOLUTION: &str = "solution"; +const CHECKER: &str = "checker"; +const MAIN: &str = "main"; + +pub fn read_inputs(problem: &Path) -> Vec { + problem + .join(INPUT) + .read_dir() + .unwrap() + .flatten() + .map(|x| fs::read_to_string(x.path()).unwrap()) + .collect() +} + +pub async fn read_checker(problem: &Path, language: Language) -> Vec { + let checker = fs::read( + problem + .join(CHECKER) + .join(MAIN) + .with_extension(language.extension), + ) + .unwrap(); + + let checker = Judge::builder() + .main(&checker, language) + .build() + .await + .unwrap(); + let checker = checker.compile().await.unwrap().unwrap(); + checker.read_executable().await.unwrap() +} + +pub fn read_solution(problem: &Path, language: Language) -> Vec { + fs::read( + problem + .join(SOLUTION) + .join(MAIN) + .with_extension(language.extension), + ) + .unwrap() +} From 20157e7cd2828414d2ae9c1b2b469ad6407d6cb5 Mon Sep 17 00:00:00 2001 From: TCO46 Date: Sun, 28 Dec 2025 23:45:57 +0700 Subject: [PATCH 54/81] chore(tests): add input --- tests/problem/hello-world/input/1.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/problem/hello-world/input/1.txt b/tests/problem/hello-world/input/1.txt index 8b13789..b8626c4 100644 --- a/tests/problem/hello-world/input/1.txt +++ b/tests/problem/hello-world/input/1.txt @@ -1 +1 @@ - +4 From 78cddd135ead0ed553397efd18d15a862825d3c1 Mon Sep 17 00:00:00 2001 From: TCO46 Date: Sun, 28 Dec 2025 23:46:25 +0700 Subject: [PATCH 55/81] fix(tests): add multi_thread for tokio::test --- tests/should_return_accepted.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/should_return_accepted.rs b/tests/should_return_accepted.rs index a67671f..43cf1ee 100644 --- a/tests/should_return_accepted.rs +++ b/tests/should_return_accepted.rs @@ -8,7 +8,7 @@ use judge_runner::{Judge, Language, Resource, Verdict, language::*}; use rstest::rstest; #[rstest] -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] pub async fn should_return_accepted( #[rustfmt::skip] #[values(CPP)] From bc0f1a4a0bafd0f3451080978f6a5ff87148c7fd Mon Sep 17 00:00:00 2001 From: TCO46 Date: Mon, 29 Dec 2025 14:07:45 +0700 Subject: [PATCH 56/81] chore: Delete unused test --- src/lib.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index be379f6..65f1db5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,9 +7,3 @@ pub use judge::*; pub use language::Language; pub use metrics::*; pub use sandbox::*; - -#[cfg(test)] -mod test { - #[test] - fn base() {} -} From 03457ebad0f2f4322b3fffde02384bec2df38e8e Mon Sep 17 00:00:00 2001 From: TCO46 Date: Mon, 29 Dec 2025 14:09:11 +0700 Subject: [PATCH 57/81] feat: Add easy test for all supported language --- .../{ => easy}/hello-world/checker/main.cpp | 0 .../{ => easy}/hello-world/input/1.txt | 0 .../{ => easy}/hello-world/solution/main.cpp | 0 .../easy/hello-world/solution/main.java | 8 ++++ .../problem/easy/hello-world/solution/main.js | 4 ++ .../problem/easy/hello-world/solution/main.py | 2 + .../problem/easy/hello-world/solution/main.rs | 6 +++ .../problem/easy/hello-world/solution/main.ts | 4 ++ .../maximum-sum-of-digits/checker/main.cpp | 40 +++++++++++++++++++ .../easy/maximum-sum-of-digits/input/1.txt | 1 + .../maximum-sum-of-digits/solution/main.cpp | 34 ++++++++++++++++ .../maximum-sum-of-digits/solution/main.java | 34 ++++++++++++++++ .../maximum-sum-of-digits/solution/main.js | 24 +++++++++++ .../maximum-sum-of-digits/solution/main.py | 16 ++++++++ .../maximum-sum-of-digits/solution/main.rs | 34 ++++++++++++++++ .../maximum-sum-of-digits/solution/main.ts | 24 +++++++++++ 16 files changed, 231 insertions(+) rename tests/problem/{ => easy}/hello-world/checker/main.cpp (100%) rename tests/problem/{ => easy}/hello-world/input/1.txt (100%) rename tests/problem/{ => easy}/hello-world/solution/main.cpp (100%) create mode 100644 tests/problem/easy/hello-world/solution/main.java create mode 100644 tests/problem/easy/hello-world/solution/main.js create mode 100644 tests/problem/easy/hello-world/solution/main.py create mode 100644 tests/problem/easy/hello-world/solution/main.rs create mode 100644 tests/problem/easy/hello-world/solution/main.ts create mode 100644 tests/problem/easy/maximum-sum-of-digits/checker/main.cpp create mode 100644 tests/problem/easy/maximum-sum-of-digits/input/1.txt create mode 100644 tests/problem/easy/maximum-sum-of-digits/solution/main.cpp create mode 100644 tests/problem/easy/maximum-sum-of-digits/solution/main.java create mode 100644 tests/problem/easy/maximum-sum-of-digits/solution/main.js create mode 100644 tests/problem/easy/maximum-sum-of-digits/solution/main.py create mode 100644 tests/problem/easy/maximum-sum-of-digits/solution/main.rs create mode 100644 tests/problem/easy/maximum-sum-of-digits/solution/main.ts diff --git a/tests/problem/hello-world/checker/main.cpp b/tests/problem/easy/hello-world/checker/main.cpp similarity index 100% rename from tests/problem/hello-world/checker/main.cpp rename to tests/problem/easy/hello-world/checker/main.cpp diff --git a/tests/problem/hello-world/input/1.txt b/tests/problem/easy/hello-world/input/1.txt similarity index 100% rename from tests/problem/hello-world/input/1.txt rename to tests/problem/easy/hello-world/input/1.txt diff --git a/tests/problem/hello-world/solution/main.cpp b/tests/problem/easy/hello-world/solution/main.cpp similarity index 100% rename from tests/problem/hello-world/solution/main.cpp rename to tests/problem/easy/hello-world/solution/main.cpp diff --git a/tests/problem/easy/hello-world/solution/main.java b/tests/problem/easy/hello-world/solution/main.java new file mode 100644 index 0000000..fc414ab --- /dev/null +++ b/tests/problem/easy/hello-world/solution/main.java @@ -0,0 +1,8 @@ +import java.util.Scanner; +class main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String s = sc.nextLine(); + System.out.println(s); + } +} diff --git a/tests/problem/easy/hello-world/solution/main.js b/tests/problem/easy/hello-world/solution/main.js new file mode 100644 index 0000000..51d6444 --- /dev/null +++ b/tests/problem/easy/hello-world/solution/main.js @@ -0,0 +1,4 @@ +for await (const line of console) { + console.log(line); + break; +} diff --git a/tests/problem/easy/hello-world/solution/main.py b/tests/problem/easy/hello-world/solution/main.py new file mode 100644 index 0000000..5ee4533 --- /dev/null +++ b/tests/problem/easy/hello-world/solution/main.py @@ -0,0 +1,2 @@ +x = input() +print(x) diff --git a/tests/problem/easy/hello-world/solution/main.rs b/tests/problem/easy/hello-world/solution/main.rs new file mode 100644 index 0000000..53f0a20 --- /dev/null +++ b/tests/problem/easy/hello-world/solution/main.rs @@ -0,0 +1,6 @@ +use std::io::stdin; + +fn main() { + let s = stdin().lines().next().unwrap().unwrap(); + println!("{s}"); +} diff --git a/tests/problem/easy/hello-world/solution/main.ts b/tests/problem/easy/hello-world/solution/main.ts new file mode 100644 index 0000000..51d6444 --- /dev/null +++ b/tests/problem/easy/hello-world/solution/main.ts @@ -0,0 +1,4 @@ +for await (const line of console) { + console.log(line); + break; +} diff --git a/tests/problem/easy/maximum-sum-of-digits/checker/main.cpp b/tests/problem/easy/maximum-sum-of-digits/checker/main.cpp new file mode 100644 index 0000000..56bcfed --- /dev/null +++ b/tests/problem/easy/maximum-sum-of-digits/checker/main.cpp @@ -0,0 +1,40 @@ +#include + +using namespace std; + +long long sum_digits(long long x) { + long long sum = 0; + while (x > 0) { + sum += x % 10; + x /= 10; + } + return sum; +} + +long long find_max_digit_sum(long long n) { + if (n < 10) { + return n; + } + + long long temp = n; + long long pow10 = 1; + while (pow10 <= n / 10) { + pow10 *= 10; + } + long long candidate = pow10 - 1; + long long a = candidate; + long long b = n - a; + return sum_digits(a) + sum_digits(b); +} + +int main() { + long long n, res; + cin >> n >> res; + long long a = find_max_digit_sum(n); + + if (a == res) { + return 0; + } else { + return 1; + } +} diff --git a/tests/problem/easy/maximum-sum-of-digits/input/1.txt b/tests/problem/easy/maximum-sum-of-digits/input/1.txt new file mode 100644 index 0000000..8f92bfd --- /dev/null +++ b/tests/problem/easy/maximum-sum-of-digits/input/1.txt @@ -0,0 +1 @@ +35 diff --git a/tests/problem/easy/maximum-sum-of-digits/solution/main.cpp b/tests/problem/easy/maximum-sum-of-digits/solution/main.cpp new file mode 100644 index 0000000..5ed83ef --- /dev/null +++ b/tests/problem/easy/maximum-sum-of-digits/solution/main.cpp @@ -0,0 +1,34 @@ +#include +using namespace std; + +long long sum_digits(long long x) { + long long sum = 0; + while (x > 0) { + sum += x % 10; + x /= 10; + } + return sum; +} + +long long find_max_digit_sum(long long n) { + if (n < 10) { + return n; + } + + long long temp = n; + long long pow10 = 1; + while (pow10 <= n / 10) { + pow10 *= 10; + } + long long candidate = pow10 - 1; + long long a = candidate; + long long b = n - a; + return sum_digits(a) + sum_digits(b); +} + +int main() { + long long n; + cin >> n; + cout << find_max_digit_sum(n) << endl; + return 0; +} diff --git a/tests/problem/easy/maximum-sum-of-digits/solution/main.java b/tests/problem/easy/maximum-sum-of-digits/solution/main.java new file mode 100644 index 0000000..a468490 --- /dev/null +++ b/tests/problem/easy/maximum-sum-of-digits/solution/main.java @@ -0,0 +1,34 @@ +import java.util.Scanner; + +public class main { + public static long sumDigits(long x) { + long sum = 0; + while (x > 0) { + sum += x % 10; + x /= 10; + } + return sum; + } + + public static long findMaxDigitSum(long n) { + if (n < 10) { + return n; + } + long temp = n; + long pow10 = 1; + while (pow10 <= n / 10) { + pow10 *= 10; + } + long candidate = pow10 - 1; + long a = candidate; + long b = n - a; + return sumDigits(a) + sumDigits(b); + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + long n = scanner.nextLong(); + System.out.println(findMaxDigitSum(n)); + scanner.close(); + } +} diff --git a/tests/problem/easy/maximum-sum-of-digits/solution/main.js b/tests/problem/easy/maximum-sum-of-digits/solution/main.js new file mode 100644 index 0000000..9aeae1b --- /dev/null +++ b/tests/problem/easy/maximum-sum-of-digits/solution/main.js @@ -0,0 +1,24 @@ +// Bun-compatible version + +function sumDigitsStr(s) { + return [...s].reduce((sum, ch) => sum + Number(ch), 0); +} + +function findMaxDigitSum(n) { + if (n < 10n) return Number(n); + + let pow10 = 1n; + while (pow10 <= n / 10n) { + pow10 *= 10n; + } + + const a = pow10 - 1n; + const b = n - a; + + return sumDigitsStr(a.toString()) + sumDigitsStr(b.toString()); +} + +for await (const n of console) { + console.log(findMaxDigitSum(BigInt(n))); + break; +} diff --git a/tests/problem/easy/maximum-sum-of-digits/solution/main.py b/tests/problem/easy/maximum-sum-of-digits/solution/main.py new file mode 100644 index 0000000..985713d --- /dev/null +++ b/tests/problem/easy/maximum-sum-of-digits/solution/main.py @@ -0,0 +1,16 @@ +def sum_digits(x): + return sum(int(d) for d in str(x)) + +def find_max_digit_sum(n): + if n < 10: + return n + pow10 = 1 + while pow10 <= n // 10: + pow10 *= 10 + candidate = pow10 - 1 + a = candidate + b = n - a + return sum_digits(a) + sum_digits(b) + +n = int(input()) +print(find_max_digit_sum(n)) diff --git a/tests/problem/easy/maximum-sum-of-digits/solution/main.rs b/tests/problem/easy/maximum-sum-of-digits/solution/main.rs new file mode 100644 index 0000000..acab421 --- /dev/null +++ b/tests/problem/easy/maximum-sum-of-digits/solution/main.rs @@ -0,0 +1,34 @@ +use std::io; + +fn sum_digits(mut x: u64) -> u64 { + let mut sum = 0; + while x > 0 { + sum += x % 10; + x /= 10; + } + sum +} + +fn find_max_digit_sum(n: u64) -> u64 { + if n < 10 { + return n; // because a + b = n, a can be 0 and b n, sum is 0 + S(n) = S(n) + } + // Generate the candidate 999...9 where the number has m digits + let mut pow10 = 1; + while pow10 <= n / 10 { + pow10 *= 10; + } + let candidate = pow10 - 1; + let a = candidate; + let b = n - a; + sum_digits(a) + sum_digits(b) +} + +fn main() { + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .expect("Failed to read line"); + let n: u64 = input.trim().parse().expect("Please enter a valid number"); + println!("{}", find_max_digit_sum(n)); +} diff --git a/tests/problem/easy/maximum-sum-of-digits/solution/main.ts b/tests/problem/easy/maximum-sum-of-digits/solution/main.ts new file mode 100644 index 0000000..9aeae1b --- /dev/null +++ b/tests/problem/easy/maximum-sum-of-digits/solution/main.ts @@ -0,0 +1,24 @@ +// Bun-compatible version + +function sumDigitsStr(s) { + return [...s].reduce((sum, ch) => sum + Number(ch), 0); +} + +function findMaxDigitSum(n) { + if (n < 10n) return Number(n); + + let pow10 = 1n; + while (pow10 <= n / 10n) { + pow10 *= 10n; + } + + const a = pow10 - 1n; + const b = n - a; + + return sumDigitsStr(a.toString()) + sumDigitsStr(b.toString()); +} + +for await (const n of console) { + console.log(findMaxDigitSum(BigInt(n))); + break; +} From fff7fc0a36ac00b8e27b391ccfe3e7b190851ff3 Mon Sep 17 00:00:00 2001 From: TCO46 Date: Mon, 29 Dec 2025 14:09:34 +0700 Subject: [PATCH 58/81] feat(tests): Add test that should return accepted --- tests/should_return_accepted.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/should_return_accepted.rs b/tests/should_return_accepted.rs index 43cf1ee..cb9fa8d 100644 --- a/tests/should_return_accepted.rs +++ b/tests/should_return_accepted.rs @@ -11,11 +11,12 @@ use rstest::rstest; #[tokio::test(flavor = "multi_thread")] pub async fn should_return_accepted( #[rustfmt::skip] - #[values(CPP)] + #[values(RUST, CPP, TYPESCRIPT, JAVASCRIPT, PYTHON, JAVA)] language: Language, #[dirs] - #[files("tests/problem/*")] + #[files("tests/problem/easy/*")] + #[exclude("wrong-answer")] #[by_ref] problem: &Path, ) { @@ -26,11 +27,11 @@ pub async fn should_return_accepted( memory: Byte::GIGABYTE, ..Default::default() }; - let time_limit = Duration::from_secs(1); + let time_limit = Duration::from_secs(5); let judge = Judge::builder() .checker(&checker, CPP) - .main(&solution, CPP) + .main(&solution, language) .build() .await .unwrap(); @@ -41,6 +42,7 @@ pub async fn should_return_accepted( .run(input.as_bytes(), false, resource, time_limit) .await .unwrap(); + println!("{:#?}", metrics); assert_eq!(metrics.verdict, Verdict::Accepted); } } From c81471d8d1882321548e547544c2aab44da59d2d Mon Sep 17 00:00:00 2001 From: TCO46 Date: Mon, 29 Dec 2025 14:55:17 +0700 Subject: [PATCH 59/81] feat(tests): remove js and ts --- .../maximum-sum-of-digits/solution/main.js | 24 ------------------- .../maximum-sum-of-digits/solution/main.ts | 24 ------------------- tests/should_return_accepted.rs | 2 +- 3 files changed, 1 insertion(+), 49 deletions(-) delete mode 100644 tests/problem/easy/maximum-sum-of-digits/solution/main.js delete mode 100644 tests/problem/easy/maximum-sum-of-digits/solution/main.ts diff --git a/tests/problem/easy/maximum-sum-of-digits/solution/main.js b/tests/problem/easy/maximum-sum-of-digits/solution/main.js deleted file mode 100644 index 9aeae1b..0000000 --- a/tests/problem/easy/maximum-sum-of-digits/solution/main.js +++ /dev/null @@ -1,24 +0,0 @@ -// Bun-compatible version - -function sumDigitsStr(s) { - return [...s].reduce((sum, ch) => sum + Number(ch), 0); -} - -function findMaxDigitSum(n) { - if (n < 10n) return Number(n); - - let pow10 = 1n; - while (pow10 <= n / 10n) { - pow10 *= 10n; - } - - const a = pow10 - 1n; - const b = n - a; - - return sumDigitsStr(a.toString()) + sumDigitsStr(b.toString()); -} - -for await (const n of console) { - console.log(findMaxDigitSum(BigInt(n))); - break; -} diff --git a/tests/problem/easy/maximum-sum-of-digits/solution/main.ts b/tests/problem/easy/maximum-sum-of-digits/solution/main.ts deleted file mode 100644 index 9aeae1b..0000000 --- a/tests/problem/easy/maximum-sum-of-digits/solution/main.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Bun-compatible version - -function sumDigitsStr(s) { - return [...s].reduce((sum, ch) => sum + Number(ch), 0); -} - -function findMaxDigitSum(n) { - if (n < 10n) return Number(n); - - let pow10 = 1n; - while (pow10 <= n / 10n) { - pow10 *= 10n; - } - - const a = pow10 - 1n; - const b = n - a; - - return sumDigitsStr(a.toString()) + sumDigitsStr(b.toString()); -} - -for await (const n of console) { - console.log(findMaxDigitSum(BigInt(n))); - break; -} diff --git a/tests/should_return_accepted.rs b/tests/should_return_accepted.rs index cb9fa8d..f9bc720 100644 --- a/tests/should_return_accepted.rs +++ b/tests/should_return_accepted.rs @@ -11,7 +11,7 @@ use rstest::rstest; #[tokio::test(flavor = "multi_thread")] pub async fn should_return_accepted( #[rustfmt::skip] - #[values(RUST, CPP, TYPESCRIPT, JAVASCRIPT, PYTHON, JAVA)] + #[values(RUST, CPP, PYTHON, JAVA)] language: Language, #[dirs] From e439b213d9ef94581027924b7912ad24c0b19360 Mon Sep 17 00:00:00 2001 From: TCO46 Date: Mon, 29 Dec 2025 16:03:03 +0700 Subject: [PATCH 60/81] workflow(test): Test using self-hosted --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fc083a8..bcb194d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,7 +10,7 @@ permissions: jobs: test: - runs-on: ${{ matrix.os }} + runs-on: self-hosted strategy: fail-fast: false matrix: From 0ed158c91e47c2856fb917bac486423f781ed219 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 12:05:42 +0700 Subject: [PATCH 61/81] chore: remove debug print --- tests/should_return_accepted.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/should_return_accepted.rs b/tests/should_return_accepted.rs index f9bc720..269dee6 100644 --- a/tests/should_return_accepted.rs +++ b/tests/should_return_accepted.rs @@ -42,7 +42,6 @@ pub async fn should_return_accepted( .run(input.as_bytes(), false, resource, time_limit) .await .unwrap(); - println!("{:#?}", metrics); assert_eq!(metrics.verdict, Verdict::Accepted); } } From 75ac5e07416620e5fb6f6e0813bfc2479f1f97ec Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 12:08:32 +0700 Subject: [PATCH 62/81] fix: change back to github runner --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bcb194d..fc083a8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,7 +10,7 @@ permissions: jobs: test: - runs-on: self-hosted + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: From e2970f5951936799f0973ecd9a1d25ddd402fcaa Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 12:16:11 +0700 Subject: [PATCH 63/81] fix(test): set AC test time limit to MAX --- tests/problem/easy/hello-world/input/1.txt | 2 +- tests/problem/easy/hello-world/input/2.txt | 1 + .../maximum-sum-of-digits/checker/main.cpp | 40 ------------------- .../easy/maximum-sum-of-digits/input/1.txt | 1 - .../maximum-sum-of-digits/solution/main.cpp | 34 ---------------- .../maximum-sum-of-digits/solution/main.java | 34 ---------------- .../maximum-sum-of-digits/solution/main.py | 16 -------- .../maximum-sum-of-digits/solution/main.rs | 34 ---------------- tests/should_return_accepted.rs | 4 +- 9 files changed, 4 insertions(+), 162 deletions(-) create mode 100644 tests/problem/easy/hello-world/input/2.txt delete mode 100644 tests/problem/easy/maximum-sum-of-digits/checker/main.cpp delete mode 100644 tests/problem/easy/maximum-sum-of-digits/input/1.txt delete mode 100644 tests/problem/easy/maximum-sum-of-digits/solution/main.cpp delete mode 100644 tests/problem/easy/maximum-sum-of-digits/solution/main.java delete mode 100644 tests/problem/easy/maximum-sum-of-digits/solution/main.py delete mode 100644 tests/problem/easy/maximum-sum-of-digits/solution/main.rs diff --git a/tests/problem/easy/hello-world/input/1.txt b/tests/problem/easy/hello-world/input/1.txt index b8626c4..e965047 100644 --- a/tests/problem/easy/hello-world/input/1.txt +++ b/tests/problem/easy/hello-world/input/1.txt @@ -1 +1 @@ -4 +Hello diff --git a/tests/problem/easy/hello-world/input/2.txt b/tests/problem/easy/hello-world/input/2.txt new file mode 100644 index 0000000..b8626c4 --- /dev/null +++ b/tests/problem/easy/hello-world/input/2.txt @@ -0,0 +1 @@ +4 diff --git a/tests/problem/easy/maximum-sum-of-digits/checker/main.cpp b/tests/problem/easy/maximum-sum-of-digits/checker/main.cpp deleted file mode 100644 index 56bcfed..0000000 --- a/tests/problem/easy/maximum-sum-of-digits/checker/main.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include - -using namespace std; - -long long sum_digits(long long x) { - long long sum = 0; - while (x > 0) { - sum += x % 10; - x /= 10; - } - return sum; -} - -long long find_max_digit_sum(long long n) { - if (n < 10) { - return n; - } - - long long temp = n; - long long pow10 = 1; - while (pow10 <= n / 10) { - pow10 *= 10; - } - long long candidate = pow10 - 1; - long long a = candidate; - long long b = n - a; - return sum_digits(a) + sum_digits(b); -} - -int main() { - long long n, res; - cin >> n >> res; - long long a = find_max_digit_sum(n); - - if (a == res) { - return 0; - } else { - return 1; - } -} diff --git a/tests/problem/easy/maximum-sum-of-digits/input/1.txt b/tests/problem/easy/maximum-sum-of-digits/input/1.txt deleted file mode 100644 index 8f92bfd..0000000 --- a/tests/problem/easy/maximum-sum-of-digits/input/1.txt +++ /dev/null @@ -1 +0,0 @@ -35 diff --git a/tests/problem/easy/maximum-sum-of-digits/solution/main.cpp b/tests/problem/easy/maximum-sum-of-digits/solution/main.cpp deleted file mode 100644 index 5ed83ef..0000000 --- a/tests/problem/easy/maximum-sum-of-digits/solution/main.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include -using namespace std; - -long long sum_digits(long long x) { - long long sum = 0; - while (x > 0) { - sum += x % 10; - x /= 10; - } - return sum; -} - -long long find_max_digit_sum(long long n) { - if (n < 10) { - return n; - } - - long long temp = n; - long long pow10 = 1; - while (pow10 <= n / 10) { - pow10 *= 10; - } - long long candidate = pow10 - 1; - long long a = candidate; - long long b = n - a; - return sum_digits(a) + sum_digits(b); -} - -int main() { - long long n; - cin >> n; - cout << find_max_digit_sum(n) << endl; - return 0; -} diff --git a/tests/problem/easy/maximum-sum-of-digits/solution/main.java b/tests/problem/easy/maximum-sum-of-digits/solution/main.java deleted file mode 100644 index a468490..0000000 --- a/tests/problem/easy/maximum-sum-of-digits/solution/main.java +++ /dev/null @@ -1,34 +0,0 @@ -import java.util.Scanner; - -public class main { - public static long sumDigits(long x) { - long sum = 0; - while (x > 0) { - sum += x % 10; - x /= 10; - } - return sum; - } - - public static long findMaxDigitSum(long n) { - if (n < 10) { - return n; - } - long temp = n; - long pow10 = 1; - while (pow10 <= n / 10) { - pow10 *= 10; - } - long candidate = pow10 - 1; - long a = candidate; - long b = n - a; - return sumDigits(a) + sumDigits(b); - } - - public static void main(String[] args) { - Scanner scanner = new Scanner(System.in); - long n = scanner.nextLong(); - System.out.println(findMaxDigitSum(n)); - scanner.close(); - } -} diff --git a/tests/problem/easy/maximum-sum-of-digits/solution/main.py b/tests/problem/easy/maximum-sum-of-digits/solution/main.py deleted file mode 100644 index 985713d..0000000 --- a/tests/problem/easy/maximum-sum-of-digits/solution/main.py +++ /dev/null @@ -1,16 +0,0 @@ -def sum_digits(x): - return sum(int(d) for d in str(x)) - -def find_max_digit_sum(n): - if n < 10: - return n - pow10 = 1 - while pow10 <= n // 10: - pow10 *= 10 - candidate = pow10 - 1 - a = candidate - b = n - a - return sum_digits(a) + sum_digits(b) - -n = int(input()) -print(find_max_digit_sum(n)) diff --git a/tests/problem/easy/maximum-sum-of-digits/solution/main.rs b/tests/problem/easy/maximum-sum-of-digits/solution/main.rs deleted file mode 100644 index acab421..0000000 --- a/tests/problem/easy/maximum-sum-of-digits/solution/main.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::io; - -fn sum_digits(mut x: u64) -> u64 { - let mut sum = 0; - while x > 0 { - sum += x % 10; - x /= 10; - } - sum -} - -fn find_max_digit_sum(n: u64) -> u64 { - if n < 10 { - return n; // because a + b = n, a can be 0 and b n, sum is 0 + S(n) = S(n) - } - // Generate the candidate 999...9 where the number has m digits - let mut pow10 = 1; - while pow10 <= n / 10 { - pow10 *= 10; - } - let candidate = pow10 - 1; - let a = candidate; - let b = n - a; - sum_digits(a) + sum_digits(b) -} - -fn main() { - let mut input = String::new(); - io::stdin() - .read_line(&mut input) - .expect("Failed to read line"); - let n: u64 = input.trim().parse().expect("Please enter a valid number"); - println!("{}", find_max_digit_sum(n)); -} diff --git a/tests/should_return_accepted.rs b/tests/should_return_accepted.rs index 269dee6..1280d11 100644 --- a/tests/should_return_accepted.rs +++ b/tests/should_return_accepted.rs @@ -11,7 +11,7 @@ use rstest::rstest; #[tokio::test(flavor = "multi_thread")] pub async fn should_return_accepted( #[rustfmt::skip] - #[values(RUST, CPP, PYTHON, JAVA)] + #[values(RUST, CPP, PYTHON, JAVASCRIPT, TYPESCRIPT, JAVA)] language: Language, #[dirs] @@ -27,7 +27,7 @@ pub async fn should_return_accepted( memory: Byte::GIGABYTE, ..Default::default() }; - let time_limit = Duration::from_secs(5); + let time_limit = Duration::MAX; let judge = Judge::builder() .checker(&checker, CPP) From c2ca5b51ef4a5101c824baa639d8fd908c29461c Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 12:17:15 +0700 Subject: [PATCH 64/81] fix: default to 10mins intead --- tests/should_return_accepted.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/should_return_accepted.rs b/tests/should_return_accepted.rs index 1280d11..61c02a5 100644 --- a/tests/should_return_accepted.rs +++ b/tests/should_return_accepted.rs @@ -27,7 +27,7 @@ pub async fn should_return_accepted( memory: Byte::GIGABYTE, ..Default::default() }; - let time_limit = Duration::MAX; + let time_limit = Duration::from_mins(10); let judge = Judge::builder() .checker(&checker, CPP) From ace8af4bfc2973506601c6b7c1398a58c15725e0 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 12:27:22 +0700 Subject: [PATCH 65/81] temp: unwrap --- src/judge.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index 6e88e66..bc394a1 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -131,17 +131,17 @@ impl Judge { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) - .spawn()?; + .spawn().unwrap(); let mut cstdin = checker.stdin.take().unwrap(); let mut cstdout = checker.stdout.take().unwrap(); - let sandbox = Sandbox::new(resource, time_limit)?; + let sandbox = Sandbox::new(resource, time_limit).unwrap(); let mut cmd = language.get_run_command(MAIN); cmd.current_dir(project_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); - let mut main = sandbox.spawn(cmd)?; + let mut main = sandbox.spawn(cmd).unwrap(); let mut stdin = main.stdin.take().unwrap(); let mut stdout = main.stdout.take().unwrap(); let mut stderr = main.stderr.take().unwrap(); @@ -174,7 +174,7 @@ impl Judge { let mut memory_usage: Byte = Byte::default(); tokio::select! { monitor_result = monitor => { - let monitor_result = monitor_result.unwrap()?; + let monitor_result = monitor_result.unwrap().unwrap(); (verdict, run_time, memory_usage) = monitor_result; } err = async { From 81aa5a368941ec139e880b368ab22b08eeba7ee8 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 12:31:27 +0700 Subject: [PATCH 66/81] fix: print --- src/judge.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/judge.rs b/src/judge.rs index bc394a1..0541734 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -117,6 +117,10 @@ impl Judge { resource: Resource, time_limit: Duration, ) -> io::Result { + self.project_path + .read_dir() + .unwrap() + .for_each(|x| println!("{:?}", x)); let Judge { project_path, language, @@ -131,7 +135,8 @@ impl Judge { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) - .spawn().unwrap(); + .spawn() + .unwrap(); let mut cstdin = checker.stdin.take().unwrap(); let mut cstdout = checker.stdout.take().unwrap(); From ab6ea1d33b93e5e3a9d496c5944262fbf52eeeaf Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 12:37:55 +0700 Subject: [PATCH 67/81] fix: use hashing --- src/judge.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index 0541734..d15d357 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -1,4 +1,12 @@ -use std::{env, io, marker::PhantomData, path::PathBuf, process::Stdio, time::Duration}; +use std::{ + env, + hash::{DefaultHasher, Hash, Hasher}, + io, + marker::PhantomData, + path::PathBuf, + process::Stdio, + time::Duration, +}; use bon::bon; use byte_unit::Byte; @@ -39,7 +47,11 @@ impl Judge { #[builder(with = |code: &'a [u8], language: Language| (code, language))] checker: Option<(&'a [u8], Language)>, ) -> io::Result> { - let project_path = env::temp_dir().join(Uuid::new_v4().to_string()); + let mut hasher = DefaultHasher::default(); + main.0.hash(&mut hasher); + Uuid::new_v4().hash(&mut hasher); + let id = hasher.finish(); + let project_path = env::temp_dir().join(id.to_string()); fs::create_dir(&project_path).await?; tokio::try_join! { From 203a058436d00127928a5ea962e98032b7d93277 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 12:40:02 +0700 Subject: [PATCH 68/81] fix: add bun --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fc083a8..3fc1fb9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -40,6 +40,7 @@ jobs: - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov + - uses: oven-sh/setup-bun@v2 - name: Install dependencies uses: awalsh128/cache-apt-pkgs-action@latest with: From 5ccd29b2506c373ab13ab7ae39911c4d156bc38c Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 12:43:13 +0700 Subject: [PATCH 69/81] fix: remove js and ts --- .github/workflows/test.yaml | 1 - README.md | 6 ++---- src/language/javascript.rs | 7 ------- src/language/typescript.rs | 7 ------- tests/problem/easy/hello-world/solution/main.js | 4 ---- tests/problem/easy/hello-world/solution/main.ts | 4 ---- 6 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 src/language/javascript.rs delete mode 100644 src/language/typescript.rs delete mode 100644 tests/problem/easy/hello-world/solution/main.js delete mode 100644 tests/problem/easy/hello-world/solution/main.ts diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3fc1fb9..fc083a8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -40,7 +40,6 @@ jobs: - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - - uses: oven-sh/setup-bun@v2 - name: Install dependencies uses: awalsh128/cache-apt-pkgs-action@latest with: diff --git a/README.md b/README.md index 5e0cdf1..ef31ce1 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,10 @@ A code runner library for online judge system. ## Supported Languages +- Rust - C++ -- Java -- JavaScript - Python -- Rust -- TypeScript +- Java ## Usage diff --git a/src/language/javascript.rs b/src/language/javascript.rs deleted file mode 100644 index b12b4bd..0000000 --- a/src/language/javascript.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::Language; - -pub const JAVASCRIPT: Language = Language { - compile_args: None, - run_args: "bun run {main}.js", - extension: "js", -}; diff --git a/src/language/typescript.rs b/src/language/typescript.rs deleted file mode 100644 index f42633a..0000000 --- a/src/language/typescript.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::Language; - -pub const TYPESCRIPT: Language = Language { - compile_args: None, - run_args: "bun run {main}.ts", - extension: "ts", -}; diff --git a/tests/problem/easy/hello-world/solution/main.js b/tests/problem/easy/hello-world/solution/main.js deleted file mode 100644 index 51d6444..0000000 --- a/tests/problem/easy/hello-world/solution/main.js +++ /dev/null @@ -1,4 +0,0 @@ -for await (const line of console) { - console.log(line); - break; -} diff --git a/tests/problem/easy/hello-world/solution/main.ts b/tests/problem/easy/hello-world/solution/main.ts deleted file mode 100644 index 51d6444..0000000 --- a/tests/problem/easy/hello-world/solution/main.ts +++ /dev/null @@ -1,4 +0,0 @@ -for await (const line of console) { - console.log(line); - break; -} From 8d45bfd5b025c90b9f80444bf4260eb6ba9bf92a Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 12:44:57 +0700 Subject: [PATCH 70/81] fix: remove non existance import --- src/language/mod.rs | 4 ---- tests/should_return_accepted.rs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/language/mod.rs b/src/language/mod.rs index 85dd388..6b9fc04 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -1,18 +1,14 @@ mod cpp; mod java; -mod javascript; mod python; mod rust; -mod typescript; use tokio::process::Command; pub use cpp::CPP; pub use java::JAVA; -pub use javascript::JAVASCRIPT; pub use python::PYTHON; pub use rust::RUST; -pub use typescript::TYPESCRIPT; #[derive(Debug, Clone, Copy, Default)] pub struct Language { diff --git a/tests/should_return_accepted.rs b/tests/should_return_accepted.rs index 61c02a5..6007fc2 100644 --- a/tests/should_return_accepted.rs +++ b/tests/should_return_accepted.rs @@ -11,7 +11,7 @@ use rstest::rstest; #[tokio::test(flavor = "multi_thread")] pub async fn should_return_accepted( #[rustfmt::skip] - #[values(RUST, CPP, PYTHON, JAVASCRIPT, TYPESCRIPT, JAVA)] + #[values(RUST, CPP, PYTHON, JAVA)] language: Language, #[dirs] From 08becbf4ba590a682c660d4bb0d943ba899319f7 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 13:02:45 +0700 Subject: [PATCH 71/81] chore: remove debug unwrap --- src/judge.rs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index d15d357..9126a26 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -129,10 +129,6 @@ impl Judge { resource: Resource, time_limit: Duration, ) -> io::Result { - self.project_path - .read_dir() - .unwrap() - .for_each(|x| println!("{:?}", x)); let Judge { project_path, language, @@ -147,21 +143,20 @@ impl Judge { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) - .spawn() - .unwrap(); - let mut cstdin = checker.stdin.take().unwrap(); - let mut cstdout = checker.stdout.take().unwrap(); + .spawn()?; + let mut cstdin = checker.stdin.take()?; + let mut cstdout = checker.stdout.take()?; - let sandbox = Sandbox::new(resource, time_limit).unwrap(); + let sandbox = Sandbox::new(resource, time_limit)?; let mut cmd = language.get_run_command(MAIN); cmd.current_dir(project_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); - let mut main = sandbox.spawn(cmd).unwrap(); - let mut stdin = main.stdin.take().unwrap(); - let mut stdout = main.stdout.take().unwrap(); - let mut stderr = main.stderr.take().unwrap(); + let mut main = sandbox.spawn(cmd)?; + let mut stdin = main.stdin.take()?; + let mut stdout = main.stdout.take()?; + let mut stderr = main.stderr.take()?; let monitor = tokio::spawn(async move { sandbox.monitor(main).await }); @@ -191,7 +186,7 @@ impl Judge { let mut memory_usage: Byte = Byte::default(); tokio::select! { monitor_result = monitor => { - let monitor_result = monitor_result.unwrap().unwrap(); + let monitor_result = monitor_result.unwrap()?; (verdict, run_time, memory_usage) = monitor_result; } err = async { From 94daae22de074ed5d5e0960b41ac059e15ce8847 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 13:11:38 +0700 Subject: [PATCH 72/81] feat(judge): drop stdin if not use --- src/judge.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index 9126a26..f39b614 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -144,8 +144,8 @@ impl Judge { .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn()?; - let mut cstdin = checker.stdin.take()?; - let mut cstdout = checker.stdout.take()?; + let mut cstdin = checker.stdin.take().unwrap(); + let mut cstdout = checker.stdout.take().unwrap(); let sandbox = Sandbox::new(resource, time_limit)?; let mut cmd = language.get_run_command(MAIN); @@ -154,9 +154,9 @@ impl Judge { .stdout(Stdio::piped()) .stderr(Stdio::piped()); let mut main = sandbox.spawn(cmd)?; - let mut stdin = main.stdin.take()?; - let mut stdout = main.stdout.take()?; - let mut stderr = main.stderr.take()?; + let mut stdin = main.stdin.take().unwrap(); + let mut stdout = main.stdout.take().unwrap(); + let mut stderr = main.stderr.take().unwrap(); let monitor = tokio::spawn(async move { sandbox.monitor(main).await }); @@ -209,16 +209,20 @@ impl Judge { Ok::<_, io::Error>(()) } => { err? } err = async { - let mut buffer = [0u8; BUFFER_SIZE]; - loop { - let n = cstdout.read(&mut buffer).await?; - if n == 0 { break; } - if stdin.write_all(&buffer[..n]).await.is_err() { - break; + if !is_interactive { + drop(stdin); + } else { + let mut buffer = [0u8; BUFFER_SIZE]; + loop { + let n = cstdout.read(&mut buffer).await?; + if n == 0 { break; } + if stdin.write_all(&buffer[..n]).await.is_err() { + break; + } + stdin.flush().await?; } - stdin.flush().await?; - } + } // sleep indefinitely until sandbox return sleep(Duration::MAX).await; From 3aebe87a59f2abdf99cf367c09b832e6969d7ab7 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 13:33:27 +0700 Subject: [PATCH 73/81] fix(sandbox): use tokio sleep instead --- src/sandbox/mod.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index c1a06d1..03c0e58 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -1,15 +1,12 @@ mod cgroup; mod resource; -use std::{ - io, - os::unix::process::ExitStatusExt, - process, - thread::sleep, - time::{Duration, Instant}, -}; +use std::{io, os::unix::process::ExitStatusExt, process, time::Duration}; -use tokio::process::{Child, Command}; +use tokio::{ + process::{Child, Command}, + time::{Instant, sleep}, +}; use byte_unit::Byte; use cgroups_rs::{ @@ -111,7 +108,7 @@ impl Sandbox { prev_cpu_usage = cpu_usage; - sleep(POLL); + sleep(POLL).await; } let status = child.try_wait()?.unwrap(); From aba39b8a6102e076cd50e3b8b6532a884bccaa4d Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 16:01:36 +0700 Subject: [PATCH 74/81] perf(sandbox): use interval for more precise timing --- src/sandbox/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index 03c0e58..0faae07 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -5,7 +5,7 @@ use std::{io, os::unix::process::ExitStatusExt, process, time::Duration}; use tokio::{ process::{Child, Command}, - time::{Instant, sleep}, + time::{Instant, interval}, }; use byte_unit::Byte; @@ -77,6 +77,8 @@ impl Sandbox { let mut prev_cpu_usage = cpu.usage(); let mut idle_start: Option = None; + let mut interval = interval(POLL); + while child.try_wait()?.is_none() { let cpu_usage = cpu.usage(); memory_usage = memory_usage.max(memory.usage()); @@ -108,7 +110,7 @@ impl Sandbox { prev_cpu_usage = cpu_usage; - sleep(POLL).await; + interval.tick().await; } let status = child.try_wait()?.unwrap(); From d788e51584540d175665a9981c73b4127bc733da Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 16:04:10 +0700 Subject: [PATCH 75/81] perf(metrics): return Vec instead --- src/judge.rs | 2 -- src/metrics.rs | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index f39b614..48d5345 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -242,8 +242,6 @@ impl Judge { Ok::<_, io::Error>(()) } => { err? } }; - let out = String::from_utf8(out).map_err(io::Error::other)?; - let err = String::from_utf8(err).map_err(io::Error::other)?; if let Some(verdict) = verdict { return Ok(Metrics { diff --git a/src/metrics.rs b/src/metrics.rs index aae224a..c7fd0f3 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -18,6 +18,6 @@ pub struct Metrics { pub verdict: Verdict, pub run_time: Duration, pub memory_usage: Byte, - pub stdout: String, - pub stderr: String, + pub stdout: Vec, + pub stderr: Vec, } From 339246b1b5616f0b15b1191b4579fec93333639c Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 16:24:34 +0700 Subject: [PATCH 76/81] refactor: prevent reinitializing time limit and duration over time --- examples/basic.rs | 32 +++++++----- src/judge.rs | 89 +++++++++++++++++---------------- tests/should_return_accepted.rs | 27 +++++----- tests/util.rs | 7 ++- 4 files changed, 86 insertions(+), 69 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index eec46ed..62d9d47 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,7 +1,7 @@ use std::time::Duration; use byte_unit::Byte; -use judge_runner::{Judge, Resource, language}; +use judge_runner::{Code, Judge, Resource, language}; #[tokio::main] async fn main() { @@ -33,7 +33,10 @@ int main() { "#; let checker = Judge::builder() - .main(checker_code.as_bytes(), language::CPP) + .main(Code { + content: checker_code.as_bytes(), + language: language::CPP, + }) .build() .await .unwrap(); @@ -41,22 +44,25 @@ int main() { let checker = checker.read_executable().await.unwrap(); let judge = Judge::builder() - .checker(&checker, language::CPP) - .main(code.as_bytes(), language::CPP) + .checker(Code { + content: &checker, + language: language::CPP, + }) + .main(Code { + content: code.as_bytes(), + language: language::CPP, + }) + .time_limit(Duration::from_secs(1)) + .resource(Resource { + memory: Byte::GIGABYTE, + ..Default::default() + }) .build() .await .unwrap(); let judge = judge.compile().await.unwrap().unwrap(); let input = "4"; - let resource = Resource { - memory: Byte::MEBIBYTE.multiply(1030).unwrap(), - ..Default::default() - }; - let time_limit = Duration::from_secs(1); - let verdict = judge - .run(input.as_bytes(), false, resource, time_limit) - .await - .unwrap(); + let verdict = judge.run(input.as_bytes()).await.unwrap(); println!("{:#?}", verdict); } diff --git a/src/judge.rs b/src/judge.rs index 48d5345..4f77dd6 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -24,6 +24,11 @@ const MAIN: &str = "main"; const CHECKER: &str = "checker"; const BUFFER_SIZE: usize = 512; +pub struct Code<'a> { + pub language: Language, + pub content: &'a [u8], +} + #[type_state( states = (Created, Compiled), slots = (Created) @@ -33,40 +38,36 @@ pub struct Judge { pub project_path: PathBuf, pub language: Language, pub checker_language: Option, + pub is_interactive: bool, + pub resource: Resource, + pub time_limit: Duration, } #[bon] impl Judge { #[builder] pub async fn new<'a>( - #[rustfmt::skip] - #[builder(with = |code: &'a [u8], language: Language| (code, language))] - main: (&'a [u8], Language), - - #[rustfmt::skip] - #[builder(with = |code: &'a [u8], language: Language| (code, language))] - checker: Option<(&'a [u8], Language)>, + main: Code<'a>, + checker: Option>, + #[builder(default = false, name = "interactive")] is_interactive: bool, + #[builder(default)] resource: Resource, + #[builder(default)] time_limit: Duration, ) -> io::Result> { - let mut hasher = DefaultHasher::default(); - main.0.hash(&mut hasher); - Uuid::new_v4().hash(&mut hasher); - let id = hasher.finish(); - let project_path = env::temp_dir().join(id.to_string()); + let project_path = generate_project_path(main.content); fs::create_dir(&project_path).await?; tokio::try_join! { async { - let (code, language) = main; - let main_path = project_path.join(MAIN).with_extension(language.extension); - fs::write(&main_path, code).await?; + let main_path = project_path.join(MAIN).with_extension(main.language.extension); + fs::write(&main_path, main.content).await?; Ok::<_, io::Error>(()) }, async { - if let Some((code, language)) = checker { + if let Some(checker) = &checker { let mut checker_path = project_path.join(CHECKER); - if language.is_interpreted() { - checker_path.set_extension(language.extension); + if checker.language.is_interpreted() { + checker_path.set_extension(checker.language.extension); } let mut checker_file = fs::OpenOptions::new() .create(true) @@ -75,7 +76,7 @@ impl Judge { .mode(0o755) .open(&checker_path) .await?; - checker_file.write_all(code).await?; + checker_file.write_all(checker.content).await?; } Ok(()) @@ -84,8 +85,11 @@ impl Judge { Ok(Judge { project_path, - language: main.1, - checker_language: checker.map(|checker| checker.1), + language: main.language, + checker_language: checker.map(|checker| checker.language), + is_interactive, + resource, + time_limit, _state: PhantomData, }) } @@ -108,6 +112,9 @@ impl Judge { project_path: self.project_path, language: self.language, checker_language: self.checker_language, + is_interactive: self.is_interactive, + resource: self.resource, + time_limit: self.time_limit, })) } @@ -122,24 +129,13 @@ impl Judge { } #[require(Compiled)] - pub async fn run( - &self, - input: &[u8], - is_interactive: bool, - resource: Resource, - time_limit: Duration, - ) -> io::Result { - let Judge { - project_path, - language, - checker_language, - .. - } = self; - - let checker_language = checker_language.ok_or(io::Error::other("Missing checker"))?; + pub async fn run(&self, input: &[u8]) -> io::Result { + let checker_language = self + .checker_language + .ok_or(io::Error::other("Missing checker"))?; let mut checker = checker_language .get_run_command(CHECKER) - .current_dir(project_path) + .current_dir(&self.project_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) @@ -147,9 +143,9 @@ impl Judge { let mut cstdin = checker.stdin.take().unwrap(); let mut cstdout = checker.stdout.take().unwrap(); - let sandbox = Sandbox::new(resource, time_limit)?; - let mut cmd = language.get_run_command(MAIN); - cmd.current_dir(project_path) + let sandbox = Sandbox::new(self.resource, self.time_limit)?; + let mut cmd = self.language.get_run_command(MAIN); + cmd.current_dir(&self.project_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); @@ -162,7 +158,7 @@ impl Judge { tokio::try_join! { async { - if !is_interactive { + if !self.is_interactive { stdin.write_all(input).await?; stdin.write_all(b"\n").await?; stdin.flush().await?; @@ -209,7 +205,7 @@ impl Judge { Ok::<_, io::Error>(()) } => { err? } err = async { - if !is_interactive { + if !self.is_interactive { drop(stdin); } else { let mut buffer = [0u8; BUFFER_SIZE]; @@ -269,3 +265,12 @@ impl Judge { }) } } + +fn generate_project_path(content: &[u8]) -> PathBuf { + let mut hasher = DefaultHasher::default(); + content.hash(&mut hasher); + Uuid::new_v4().hash(&mut hasher); + let id = hasher.finish(); + + env::temp_dir().join(id.to_string()) +} diff --git a/tests/should_return_accepted.rs b/tests/should_return_accepted.rs index 6007fc2..e276bce 100644 --- a/tests/should_return_accepted.rs +++ b/tests/should_return_accepted.rs @@ -4,7 +4,7 @@ use std::path::Path; use std::time::Duration; use byte_unit::Byte; -use judge_runner::{Judge, Language, Resource, Verdict, language::*}; +use judge_runner::{Code, Judge, Language, Resource, Verdict, language::*}; use rstest::rstest; #[rstest] @@ -23,25 +23,28 @@ pub async fn should_return_accepted( let inputs = util::read_inputs(problem); let checker = util::read_checker(problem, CPP).await; let solution = util::read_solution(problem, language); - let resource = Resource { - memory: Byte::GIGABYTE, - ..Default::default() - }; - let time_limit = Duration::from_mins(10); let judge = Judge::builder() - .checker(&checker, CPP) - .main(&solution, language) + .checker(Code { + content: &checker, + language: CPP, + }) + .main(Code { + content: &solution, + language, + }) + .resource(Resource { + memory: Byte::GIGABYTE, + ..Default::default() + }) + .time_limit(Duration::from_secs(1)) .build() .await .unwrap(); let judge = judge.compile().await.unwrap().unwrap(); for input in inputs { - let metrics = judge - .run(input.as_bytes(), false, resource, time_limit) - .await - .unwrap(); + let metrics = judge.run(input.as_bytes()).await.unwrap(); assert_eq!(metrics.verdict, Verdict::Accepted); } } diff --git a/tests/util.rs b/tests/util.rs index 4981955..3f39e37 100644 --- a/tests/util.rs +++ b/tests/util.rs @@ -1,6 +1,6 @@ use std::{fs, path::Path}; -use judge_runner::{Judge, Language}; +use judge_runner::{Code, Judge, Language}; const INPUT: &str = "input"; const SOLUTION: &str = "solution"; @@ -27,7 +27,10 @@ pub async fn read_checker(problem: &Path, language: Language) -> Vec { .unwrap(); let checker = Judge::builder() - .main(&checker, language) + .main(Code { + content: &checker, + language, + }) .build() .await .unwrap(); From 32b4a092df16704d13d9449dd4faed03dbf2d46b Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 17:07:17 +0700 Subject: [PATCH 77/81] perf(judge): use tokio::spawn instead of tokio::select --- src/judge.rs | 118 +++++++++++++++------------------------------------ 1 file changed, 34 insertions(+), 84 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index 4f77dd6..3ab1f25 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -9,12 +9,10 @@ use std::{ }; use bon::bon; -use byte_unit::Byte; use state_shift::{impl_state, type_state}; use tokio::{ fs, io::{AsyncReadExt, AsyncWriteExt}, - time::sleep, }; use uuid::Uuid; @@ -22,7 +20,7 @@ use crate::{Language, Metrics, Resource, Sandbox, Verdict}; const MAIN: &str = "main"; const CHECKER: &str = "checker"; -const BUFFER_SIZE: usize = 512; +const BUFFER_SIZE: usize = 8 * 1024; pub struct Code<'a> { pub language: Language, @@ -142,6 +140,9 @@ impl Judge { .spawn()?; let mut cstdin = checker.stdin.take().unwrap(); let mut cstdout = checker.stdout.take().unwrap(); + cstdin.write_all(input).await?; + cstdin.write_all(b"\n").await?; + cstdin.flush().await?; let sandbox = Sandbox::new(self.resource, self.time_limit)?; let mut cmd = self.language.get_run_command(MAIN); @@ -155,102 +156,51 @@ impl Judge { let mut stderr = main.stderr.take().unwrap(); let monitor = tokio::spawn(async move { sandbox.monitor(main).await }); - - tokio::try_join! { - async { - if !self.is_interactive { - stdin.write_all(input).await?; - stdin.write_all(b"\n").await?; - stdin.flush().await?; + if !self.is_interactive { + stdin.write_all(input).await?; + stdin.write_all(b"\n").await?; + stdin.flush().await?; + } + let stdin_thread = + tokio::spawn(async move { tokio::io::copy(&mut cstdout, &mut stdin).await }); + let stdout_thread = tokio::spawn(async move { + let mut out = vec![]; + let mut buffer = [0u8; BUFFER_SIZE]; + loop { + let n = stdout.read(&mut buffer).await?; + if n == 0 { + break; + } + if cstdin.write_all(&buffer[..n]).await.is_err() { + break; } - - Ok::<_, io::Error>(()) - }, - async { - cstdin.write_all(input).await?; - cstdin.write_all(b"\n").await?; cstdin.flush().await?; - - Ok::<_, io::Error>(()) + out.extend_from_slice(&buffer[0..n]); } - }?; - let mut out: Vec = vec![]; - let mut err: Vec = vec![]; - let mut verdict: Option = None; - let mut run_time: Duration = Duration::default(); - let mut memory_usage: Byte = Byte::default(); - tokio::select! { - monitor_result = monitor => { - let monitor_result = monitor_result.unwrap()?; - (verdict, run_time, memory_usage) = monitor_result; - } - err = async { - let mut buffer = [0u8; BUFFER_SIZE]; - loop { - let n = stdout.read(&mut buffer).await?; - if n == 0 { - break; - } - if cstdin.write_all(&buffer[..n]).await.is_err() { - break; - } - cstdin.flush().await?; - out.extend_from_slice(&buffer[0..n]); - } + Ok::<_, io::Error>(out) + }); - // sleep indefinitely until sandbox return - sleep(Duration::MAX).await; - - Ok::<_, io::Error>(()) - } => { err? } - err = async { - if !self.is_interactive { - drop(stdin); - } else { - let mut buffer = [0u8; BUFFER_SIZE]; - loop { - let n = cstdout.read(&mut buffer).await?; - if n == 0 { break; } - if stdin.write_all(&buffer[..n]).await.is_err() { - break; - } - stdin.flush().await?; - } - - } - // sleep indefinitely until sandbox return - sleep(Duration::MAX).await; + let (verdict, run_time, memory_usage) = monitor.await.unwrap()?; + let checker_status = checker.wait().await?; + drop(checker); - Ok::<_, io::Error>(()) - } => { err? } - err = async { - let mut buffer = [0u8; BUFFER_SIZE]; - loop { - let n = stderr.read(&mut buffer).await?; - if n == 0 { break; } - err.extend_from_slice(&buffer[0..n]); - } - - // sleep indefinitely until sandbox return - sleep(Duration::MAX).await; - - Ok::<_, io::Error>(()) - } => { err? } - }; + let _ = stdin_thread.await; + let stdout = stdout_thread.await.unwrap()?; + let mut err = vec![]; + stderr.read_to_end(&mut err).await?; if let Some(verdict) = verdict { return Ok(Metrics { verdict, run_time, - stdout: out, + stdout, stderr: err, memory_usage, }); } - let status = checker.wait().await?; - let verdict = if status.success() { + let verdict = if checker_status.success() { Verdict::Accepted } else { Verdict::WrongAnswer @@ -259,7 +209,7 @@ impl Judge { Ok(Metrics { verdict, run_time, - stdout: out, + stdout, stderr: err, memory_usage, }) From 913600db3eabd5a6645c1c2f1c4d84ef80273119 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 17:10:42 +0700 Subject: [PATCH 78/81] doc: update new example code --- README.md | 62 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index ef31ce1..a5acc1c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A code runner library for online judge system. - C++ - Python - Java +- ... More language can be defined ## Usage @@ -15,19 +16,24 @@ A code runner library for online judge system. use std::time::Duration; use byte_unit::Byte; -use judge_runner::{Judge, Resource, language}; +use judge_runner::{Code, Judge, Resource, language}; #[tokio::main] async fn main() { let checker_code = r#" -import sys -n = int(input()) -res = int(input()) - -if n == res: - sys.exit(0) -else: - sys.exit(1) +#include + +using namespace std; + +int main() { + string s, res; + cin >> s >> res; + if (s == res) { + return 0; + } else { + return 1; + } +} "#; let code = r#" #include @@ -35,14 +41,17 @@ else: using namespace std; int main() { - int n; - cin >> n; - cout << n << endl; + string s; + cin >> s; + cout << s << endl; } "#; let checker = Judge::builder() - .main(checker_code.as_bytes(), language::PYTHON) + .main(Code { + content: checker_code.as_bytes(), + language: language::CPP, + }) .build() .await .unwrap(); @@ -50,24 +59,27 @@ int main() { let checker = checker.read_executable().await.unwrap(); let judge = Judge::builder() - .checker(&checker, language::PYTHON) - .main(code.as_bytes(), language::CPP) + .checker(Code { + content: &checker, + language: language::CPP, + }) + .main(Code { + content: code.as_bytes(), + language: language::CPP, + }) + .time_limit(Duration::from_secs(1)) + .resource(Resource { + memory: Byte::GIGABYTE, + ..Default::default() + }) .build() .await .unwrap(); let judge = judge.compile().await.unwrap().unwrap(); let input = "4"; - let resource = Resource { - memory: Byte::MEBIBYTE.multiply(1030).unwrap(), - ..Default::default() - }; - let time_limit = Duration::from_secs(1); - let verdict = judge - .run(input.as_bytes(), false, resource, time_limit) - .await - .unwrap(); - println!("{:#?}", verdict); + let metrics = judge.run(input.as_bytes()).await.unwrap(); + println!("{:#?}", metrics); } ``` From 0434813b6f49de2bfe1161956f3ae9463a70b3ce Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 17:10:54 +0700 Subject: [PATCH 79/81] doc: update new example code --- examples/basic.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index 62d9d47..558551d 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -63,6 +63,6 @@ int main() { let judge = judge.compile().await.unwrap().unwrap(); let input = "4"; - let verdict = judge.run(input.as_bytes()).await.unwrap(); - println!("{:#?}", verdict); + let metrics = judge.run(input.as_bytes()).await.unwrap(); + println!("{:#?}", metrics); } From f83c22173f6e71b607e91a56ad10e9558423d077 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 17:16:37 +0700 Subject: [PATCH 80/81] feat: use better random number --- src/judge.rs | 24 +++--------------------- src/lib.rs | 1 + src/sandbox/resource.rs | 7 ++++--- src/util.rs | 11 +++++++++++ 4 files changed, 19 insertions(+), 24 deletions(-) create mode 100644 src/util.rs diff --git a/src/judge.rs b/src/judge.rs index 3ab1f25..dd20da5 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -1,12 +1,4 @@ -use std::{ - env, - hash::{DefaultHasher, Hash, Hasher}, - io, - marker::PhantomData, - path::PathBuf, - process::Stdio, - time::Duration, -}; +use std::{env, io, marker::PhantomData, path::PathBuf, process::Stdio, time::Duration}; use bon::bon; use state_shift::{impl_state, type_state}; @@ -14,9 +6,8 @@ use tokio::{ fs, io::{AsyncReadExt, AsyncWriteExt}, }; -use uuid::Uuid; -use crate::{Language, Metrics, Resource, Sandbox, Verdict}; +use crate::{Language, Metrics, Resource, Sandbox, Verdict, util}; const MAIN: &str = "main"; const CHECKER: &str = "checker"; @@ -51,7 +42,7 @@ impl Judge { #[builder(default)] resource: Resource, #[builder(default)] time_limit: Duration, ) -> io::Result> { - let project_path = generate_project_path(main.content); + let project_path = env::temp_dir().join(util::random(main.content).to_string()); fs::create_dir(&project_path).await?; tokio::try_join! { @@ -215,12 +206,3 @@ impl Judge { }) } } - -fn generate_project_path(content: &[u8]) -> PathBuf { - let mut hasher = DefaultHasher::default(); - content.hash(&mut hasher); - Uuid::new_v4().hash(&mut hasher); - let id = hasher.finish(); - - env::temp_dir().join(id.to_string()) -} diff --git a/src/lib.rs b/src/lib.rs index 65f1db5..206dde8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ mod judge; pub mod language; mod metrics; mod sandbox; +mod util; pub use judge::*; pub use language::Language; diff --git a/src/sandbox/resource.rs b/src/sandbox/resource.rs index 5072a26..80dbc05 100644 --- a/src/sandbox/resource.rs +++ b/src/sandbox/resource.rs @@ -2,11 +2,12 @@ use std::{io, time::Duration}; use byte_unit::Byte; use cgroups_rs::fs::{Cgroup, cgroup_builder::CgroupBuilder, hierarchies}; -use uuid::Uuid; + +use crate::util; const PREFIX: &str = "judge"; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Hash)] pub struct Resource { pub memory: Byte, pub cpu_quota: Duration, @@ -27,7 +28,7 @@ impl TryFrom for Cgroup { type Error = io::Error; fn try_from(resource: Resource) -> Result { - let builder = CgroupBuilder::new(&format!("{}/{}", PREFIX, Uuid::new_v4())); + let builder = CgroupBuilder::new(&format!("{}/{}", PREFIX, util::random(resource))); let memory = resource.memory.as_u64() as i64; let builder = builder diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..fae314f --- /dev/null +++ b/src/util.rs @@ -0,0 +1,11 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; + +use uuid::Uuid; + +pub fn random(data: T) -> u64 { + let mut hasher = DefaultHasher::default(); + data.hash(&mut hasher); + Uuid::new_v4().hash(&mut hasher); + + hasher.finish() +} From 1243c0d3e9a84c88c737e86c2371acc384843534 Mon Sep 17 00:00:00 2001 From: akagiyuu Date: Wed, 31 Dec 2025 17:31:02 +0700 Subject: [PATCH 81/81] fix(judge): force sync data for checker --- src/judge.rs | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/judge.rs b/src/judge.rs index dd20da5..64ce80d 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -45,32 +45,25 @@ impl Judge { let project_path = env::temp_dir().join(util::random(main.content).to_string()); fs::create_dir(&project_path).await?; - tokio::try_join! { - async { - let main_path = project_path.join(MAIN).with_extension(main.language.extension); - fs::write(&main_path, main.content).await?; - - Ok::<_, io::Error>(()) - }, - async { - if let Some(checker) = &checker { - let mut checker_path = project_path.join(CHECKER); - if checker.language.is_interpreted() { - checker_path.set_extension(checker.language.extension); - } - let mut checker_file = fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .mode(0o755) - .open(&checker_path) - .await?; - checker_file.write_all(checker.content).await?; - } - - Ok(()) + let main_path = project_path + .join(MAIN) + .with_extension(main.language.extension); + fs::write(&main_path, main.content).await?; + if let Some(checker) = &checker { + let mut checker_path = project_path.join(CHECKER); + if checker.language.is_interpreted() { + checker_path.set_extension(checker.language.extension); } - }?; + let mut checker_file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .mode(0o755) + .open(&checker_path) + .await?; + checker_file.write_all(checker.content).await?; + checker_file.sync_all().await?; + } Ok(Judge { project_path,