diff --git a/README.md b/README.md index 7e97af05a8819a0475cadd30de68e58dbf9f9da9..94e9f785bcce651621e3b26e9b4ce420cdce2562 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ Work in progress competitive programming judge that is compatible with Polygon p ## How to Run * Make sure the `isolate/` submodule has been fetched -* Just use docker compose (`docker-compose up`), there is a development environment +* Copy `isolate/systemd/*` files to `/etc/systemd/system/` and start isolate's +daemon that provides the base cgroups v2 root directory. +* Use docker compose (`docker compose up`), there is a development environment and a production environment. ## Features diff --git a/alvokanto/Dockerfile b/alvokanto/Dockerfile index 40ecdc60db164d856aa07ceba6465d5f08f09262..1ed1d967d1f1ddc80e106c6cd6e8a5d3edb3fa76 100644 --- a/alvokanto/Dockerfile +++ b/alvokanto/Dockerfile @@ -46,8 +46,7 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* COPY --from=isolate_builder /usr/local/bin/isolate /usr/local/bin/ COPY --from=isolate_builder /usr/local/bin/isolate-check-environment /usr/local/bin/ -COPY --from=isolate_builder /usr/local/sbin/isolate-cg-keeper /usr/local/sbin/ -COPY --from=isolate_builder /usr/local/etc/isolate /usr/local/etc/isolate +ADD alvokanto/isolate.cf /usr/local/etc/isolate COPY --from=builder /usr/local/bin/alvokanto /usr/local/bin/ RUN mkdir -p /var/local/lib/isolate RUN mkdir -p /usr/local/alvokanto diff --git a/alvokanto/dev.Dockerfile b/alvokanto/dev.Dockerfile index 5231a9545a921998d783d17b071051b0f011268b..9f2252d24c4f260c07b973bf0557b41d683976ec 100644 --- a/alvokanto/dev.Dockerfile +++ b/alvokanto/dev.Dockerfile @@ -31,8 +31,7 @@ RUN apt-get update && \ RUN rustup component add rustfmt COPY --from=isolate_builder /usr/local/bin/isolate /usr/local/bin/ COPY --from=isolate_builder /usr/local/bin/isolate-check-environment /usr/local/bin/ -COPY --from=isolate_builder /usr/local/sbin/isolate-cg-keeper /usr/local/sbin/ -COPY --from=isolate_builder /usr/local/etc/isolate /usr/local/etc/isolate +ADD alvokanto/isolate.cf /usr/local/etc/isolate RUN mkdir -p /var/local/lib/isolate RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ diff --git a/alvokanto/isolate.cf b/alvokanto/isolate.cf new file mode 100644 index 0000000000000000000000000000000000000000..6556fa9b1f76e69b9dcf9cfc88958d50204ebc1c --- /dev/null +++ b/alvokanto/isolate.cf @@ -0,0 +1,29 @@ +# This is a configuration file for Isolate + +# All sandboxes are created under this directory. +# To avoid symlink attacks, this directory and all its ancestors +# must be writeable only to root. +box_root = /var/local/lib/isolate + +# Directory where lock files are created. +lock_root = /run/isolate/locks + +# Control group under which we place our subgroups +# Either an explicit path to a subdirectory in cgroupfs, or "auto:file" to read +# the path from "file", where it is put by isolate-cg-helper. +cg_root = /sys/fs/cgroup/isolate.slice/isolate.service +# cg_root = auto:/run/isolate/cgroup + +# Block of UIDs and GIDs reserved for sandboxes +first_uid = 60000 +first_gid = 60000 +num_boxes = 1000 + +# Only root can create new sandboxes (default: 0=everybody can) +#restricted_init = 1 + +# Per-box settings of the set of allowed CPUs and NUMA nodes +# (see linux/Documentation/cgroups/cpusets.txt for precise syntax) + +#box0.cpus = 4-7 +#box0.mems = 1 diff --git a/alvokanto/src/isolate.rs b/alvokanto/src/isolate.rs index c600a2075f34d3593dcc291242737f4fe39b7185..2cfe869cce94698d4d8cbd146f9cf02bfc4db3a1 100644 --- a/alvokanto/src/isolate.rs +++ b/alvokanto/src/isolate.rs @@ -28,13 +28,8 @@ pub struct IsolateBox { pub fn new_isolate_box( isolate_executable_path: &PathBuf, - isolate_cg_keeper_path: &PathBuf, id: i32, ) -> Result<IsolateBox, CommandError> { - Command::new(isolate_cg_keeper_path) - .spawn() - .map_err(CommandError::CommandIo)?; - let output = reset(isolate_executable_path, id)?; Ok(IsolateBox { id, @@ -46,8 +41,6 @@ pub fn reset( isolate_executable_path: &PathBuf, id: i32, ) -> Result<std::process::Output, CommandError> { - cleanup_box(isolate_executable_path, id)?; - let output = Command::new(isolate_executable_path) .arg("--init") .arg("--cg") @@ -61,6 +54,8 @@ pub fn reset( )); } + info!("Resetting box {}: {}", id, str::from_utf8(&output.stdout)?); + Ok(output) } @@ -319,14 +314,3 @@ pub fn compile( }, ) } - -pub fn cleanup_box(isolate_executable_path: &PathBuf, box_id: i32) -> Result<bool, CommandError> { - Ok(Command::new(isolate_executable_path) - .arg("--cleanup") - .arg("--cg") - .arg(format!("--box-id={}", box_id)) - .output() - .map_err(CommandError::CommandIo)? - .status - .success()) -} diff --git a/alvokanto/src/main.rs b/alvokanto/src/main.rs index 5dbf737478216c8f189fe797aba31455257cd459..c58e50d0e1deb7180f0eaaef51dafd5e59c0206a 100644 --- a/alvokanto/src/main.rs +++ b/alvokanto/src/main.rs @@ -14,6 +14,7 @@ mod language; use tokio::time::{sleep, Duration}; use isolate::{IsolateBox, new_isolate_box, RunStatus, RunStats, CommandTuple, CompileParams}; use std::fs::read_to_string; +use std::os::unix::fs::PermissionsExt; use std::fs; use log::info; use language::Compile; @@ -25,10 +26,6 @@ pub fn get_isolate_executable_path() -> PathBuf { which("isolate").expect("isolate binary not installed") } -pub fn get_isolate_cg_keeper_executable_path() -> PathBuf { - which("isolate-cg-keeper").expect("isolate-cg-keeper binary not installed") -} - fn run_cached( isolate_executable_path: &PathBuf, isolate_box: &IsolateBox, @@ -61,7 +58,7 @@ fn run_cached( .replace("{}", path_without_suffix.to_str().unwrap()); if !root_data.join(&output_path).exists() { isolate::reset(&isolate_executable_path, isolate_box.id) - .expect("reset to work"); + .expect("Reset failed"); fs_extra::dir::copy( path_with_suffix.parent().unwrap(), &isolate_box.path, @@ -148,6 +145,8 @@ fn run_cached( .collect(), }; + isolate::reset(&isolate_executable_path, isolate_box.id) + .expect("Reset failed"); let run_stats = isolate::execute( &isolate_executable_path, &isolate_box, @@ -167,9 +166,6 @@ fn run_cached( let error_output = read_to_string(run_stats.stderr_path).unwrap_or("".into()); - isolate::reset(&isolate_executable_path, isolate_box.id) - .expect("reset to work"); - JobResult { uuid, code: job_result::Code::Ok.into(), @@ -194,41 +190,27 @@ fn run_cached( } } -fn judge( +fn compile_checker_if_necessary( isolate_executable_path: &PathBuf, isolate_box: &IsolateBox, supported_languages: &HashMap<String, language::LanguageParams>, - uuid: String, - language: String, - time_limit_ms: i32, - memory_limit_kib: i32, - request: job::Judgement -) -> JobResult { + uuid: &String, + request: &job::Judgement +) -> Option<JobResult> { let root_data = PathBuf::from("/data/"); - let language = supported_languages.get(&language); - if let None = language { - return JobResult { - uuid, - code: job_result::Code::InvalidLanguage.into(), - which: None, - }; - } - let language = language.unwrap(); - let checker_language = supported_languages.get(&request.checker_language); if let None = checker_language { - return JobResult { - uuid, + return Some(JobResult { + uuid: uuid.to_string(), code: job_result::Code::InvalidLanguage.into(), which: None, - }; + }); } let checker_language = checker_language.unwrap(); let path_with_suffix = PathBuf::from("/data/").join(&request.checker_source_path); let path_without_suffix = path_with_suffix.with_extension(""); - if let Compile::Command(_, command, output) = &checker_language.compile { let output_path = output.replace("{.}", path_with_suffix.to_str().unwrap()) @@ -284,8 +266,8 @@ fn judge( } => true, } { fs_extra::dir::create(&isolate_box.path, true).unwrap(); - return JobResult { - uuid, + return Some(JobResult { + uuid: uuid.to_string(), code: job_result::Code::Ok.into(), which: Some(job_result::Which::Judgement(job_result::Judgement { failed_test: 0, @@ -299,7 +281,7 @@ fn judge( judge_start_instant: Local::now().naive_utc().format("%Y-%m-%dT%H:%M:%S%.f").to_string(), judge_end_instant: Local::now().naive_utc().format("%Y-%m-%dT%H:%M:%S%.f").to_string(), })) - } + }) } fs::copy(&isolate_box.path.join( @@ -308,15 +290,51 @@ fn judge( } } + return None +} + +fn judge( + isolate_executable_path: &PathBuf, + isolate_box: &IsolateBox, + supported_languages: &HashMap<String, language::LanguageParams>, + uuid: String, + language: String, + time_limit_ms: i32, + memory_limit_kib: i32, + request: job::Judgement +) -> JobResult { + let language = supported_languages.get(&language); + if let None = language { + return JobResult { + uuid, + code: job_result::Code::InvalidLanguage.into(), + which: None, + }; + } + let language = language.unwrap(); + + if let Some(res) = compile_checker_if_necessary( + isolate_executable_path, + isolate_box, + supported_languages, + &uuid, + &request + ) { + return res + } + let judge_start_instant = Local::now().naive_utc(); + let mut binary_file = request.source_text.as_bytes().to_vec(); + if let Compile::Command(transform, command, _) = &language.compile { isolate::reset(&isolate_executable_path, isolate_box.id) - .expect("reset to work"); + .expect("Reset failed"); - let mut file = File::create(isolate_box.path.join(format!("x{}", language.suffix))).unwrap(); - file.write_all(transform(request.source_text, "x".into()).as_bytes()).unwrap(); - file.sync_data().unwrap(); + { + let mut file = File::create(isolate_box.path.join(format!("x{}", language.suffix))).unwrap(); + file.write_all(transform(request.source_text, "x".into()).as_bytes()).unwrap(); + } let command = CommandTuple { binary_path: command.binary_path.clone(), @@ -371,10 +389,10 @@ fn judge( })) } } - } else { - let mut file = File::create(isolate_box.path.join(format!("x{}", language.suffix))).unwrap(); - file.write_all(request.source_text.as_bytes()).unwrap(); - file.sync_data().unwrap(); + + binary_file = fs::read(isolate_box.path.join(language.run.binary_path.to_str().unwrap() + .replace("{.}", &format!("x{}", language.suffix)) + .replace("{}", "x".into()))).unwrap(); } let mut last_execute_stats: Option<RunStats> = None; @@ -399,6 +417,8 @@ fn judge( let mut time_wall_ms = 0; for i in 1..request.test_count + 1 { + isolate::reset(&isolate_executable_path, isolate_box.id) + .expect("Reset failed"); let stdin_path = format_width(&request.test_pattern, i.try_into().unwrap()); let answer_path = format!("{}.a", stdin_path); @@ -406,6 +426,13 @@ fn judge( "Starting run {}/{} with test {:?}", i, request.test_count, stdin_path ); + { + let mut file = File::create(isolate_box.path.join(&command.binary_path)).unwrap(); + let mut perms = file.metadata().unwrap().permissions(); + perms.set_mode(0o755); + file.set_permissions(perms).unwrap(); + file.write_all(&binary_file).unwrap(); + } let execute_stats = isolate::execute( &isolate_executable_path, &isolate_box, @@ -492,9 +519,6 @@ fn judge( } } - isolate::reset(&isolate_executable_path, isolate_box.id) - .expect("reset to work"); - let judge_end_instant = Local::now().naive_utc(); let last_execute_stats = last_execute_stats.unwrap(); @@ -584,10 +608,8 @@ async fn main() { env_logger::init(); let isolate_executable_path = get_isolate_executable_path(); - let isolate_cg_keeper_executable_path = get_isolate_cg_keeper_executable_path(); log::info!("Found isolate at {:?}", isolate_executable_path); - log::info!("Found isolate-cg-keeper at {:?}", isolate_cg_keeper_executable_path); - let isolate_box = new_isolate_box(&isolate_executable_path, &isolate_cg_keeper_executable_path, 0).expect("Couldn't create an isolate box"); + let isolate_box = new_isolate_box(&isolate_executable_path, 0).expect("Couldn't create an isolate box"); log::info!("Created an isolate box at {:?}", isolate_box.path); let supported_languages = language::get_supported_languages(); diff --git a/alvokanto/src/queue.rs b/alvokanto/src/queue.rs index 50a8ecffc2c1e52496962d8a96c909cf852e6776..d9e1343529567770738e5ca7bcf237cf8acc168e 100644 --- a/alvokanto/src/queue.rs +++ b/alvokanto/src/queue.rs @@ -46,6 +46,7 @@ fn run_loop( info!("Starting to compile"); let language = languages.get(&submission.language).unwrap(); + isolate::reset(isolate_executable_path, 0).expect("Reset failed"); let compile_source_result = language::compile_source( &isolate_executable_path, &isolate_box, @@ -107,6 +108,7 @@ fn run_loop( "Starting run {}/{} with test {:?}", i, submission.test_count, stdin_path ); + isolate::reset(isolate_executable_path, 0).expect("Reset failed"); let execute_stats = language::run( &isolate_executable_path, &isolate_box, @@ -143,6 +145,7 @@ fn run_loop( fs::copy(&execute_stats.stdout_path, isolate_box.path.join("stdin")).expect("Copy"); + isolate::reset(isolate_executable_path, 0).expect("Reset failed"); let checker_stats = isolate::execute( &isolate_executable_path, &isolate_box, @@ -205,8 +208,6 @@ fn run_loop( let last_execute_stats = last_execute_stats.unwrap(); - isolate::reset(isolate_executable_path, 0).expect("Reset failed"); - if let(last_execute_stats.status) = RunStatus::TimeLimitExceeded { if last_execute_stats.time_ms == 0 { continue; } } @@ -231,7 +232,9 @@ fn run_loop( time_wall_ms: last_execute_stats.time_wall_ms, error_output: stderr, }) - .expect("Coudln't send back submission completion"); + .expect("Couldn't send back submission completion"); + + break; } } diff --git a/docker-compose.yml b/docker-compose.yml index 4d2a4cc08f06753dd076e024f24a652c631ff9e2..9a4c9eed71f74b04bfd8c01ef23fb04a560266c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,4 +13,8 @@ services: build: context: . dockerfile: alvokanto/Dockerfile + volumes: + - type: bind + source: /sys/fs/cgroup/isolate.slice + target: /sys/fs/cgroup/isolate.slice privileged: true diff --git a/src/import_contest.rs b/src/import_contest.rs index ddc42ff281de239c4798dbdff98ccca47845577c..cde9b7de5a0c535f36623ae3d4b4ba9c30ffd568 100644 --- a/src/import_contest.rs +++ b/src/import_contest.rs @@ -51,6 +51,7 @@ mod xml { #[derive(Deserialize, Debug)] pub struct Contest { + #[serde(rename = "@url")] pub url: String, pub names: Names, pub problems: Problems, @@ -63,7 +64,9 @@ mod xml { #[derive(Deserialize, Debug)] pub struct Name { + #[serde(rename = "@language")] pub language: String, + #[serde(rename = "@value")] pub value: String, } @@ -74,7 +77,9 @@ mod xml { #[derive(Deserialize, Debug)] pub struct Problem { + #[serde(rename = "@index")] pub index: String, + #[serde(rename = "@url")] pub url: String, } @@ -90,9 +95,11 @@ mod xml { #[derive(Deserialize, Debug)] pub struct Problem { + #[serde(rename = "@url")] pub url: String, + #[serde(rename = "@revision")] pub revision: String, - #[serde(rename = "short-name")] + #[serde(rename = "@short-name")] pub short_name: String, pub names: Names, pub statements: Statements, @@ -110,7 +117,9 @@ mod xml { #[derive(Deserialize, Debug)] pub struct Name { + #[serde(rename = "@language")] pub language: String, + #[serde(rename = "@value")] pub value: String, } @@ -121,72 +130,48 @@ mod xml { #[derive(Deserialize, Debug)] pub struct Statement { + #[serde(rename = "@charset")] pub charset: Option<String>, + #[serde(rename = "@language")] pub language: String, + #[serde(rename = "@mathjax")] pub mathjax: Option<bool>, + #[serde(rename = "@path")] pub path: String, + #[serde(rename = "@type")] pub r#type: String, } #[derive(Deserialize, Debug)] pub struct Judging { - #[serde(rename = "cpu-name")] + #[serde(rename = "@cpu-name")] pub cpu_name: String, - #[serde(rename = "cpu-speed")] + #[serde(rename = "@cpu-speed")] pub cpu_speed: String, - #[serde(rename = "input-file")] + #[serde(rename = "@input-file")] pub input_file: String, - #[serde(rename = "output-file")] + #[serde(rename = "@output-file")] pub output_file: String, pub testset: Vec<Testset>, } #[derive(Deserialize, Debug)] pub struct Testset { + #[serde(rename = "@name")] pub name: String, #[serde(rename = "time-limit")] - pub time_limit: TimeLimit, + pub time_limit: String, #[serde(rename = "memory-limit")] - pub memory_limit: MemoryLimit, + pub memory_limit: String, #[serde(rename = "test-count")] - pub test_count: TestCount, + pub test_count: String, #[serde(rename = "input-path-pattern")] - pub input_path_pattern: InputPathPattern, + pub input_path_pattern: String, #[serde(rename = "answer-path-pattern")] - pub answer_path_pattern: AnswerPathPattern, + pub answer_path_pattern: String, pub tests: Tests, } - #[derive(Deserialize, Debug)] - pub struct TimeLimit { - #[serde(rename = "$value")] - pub value: String, - } - - #[derive(Deserialize, Debug)] - pub struct MemoryLimit { - #[serde(rename = "$value")] - pub value: String, - } - - #[derive(Deserialize, Debug)] - pub struct TestCount { - #[serde(rename = "$value")] - pub value: String, - } - - #[derive(Deserialize, Debug)] - pub struct InputPathPattern { - #[serde(rename = "$value")] - pub value: String, - } - - #[derive(Deserialize, Debug)] - pub struct AnswerPathPattern { - #[serde(rename = "$value")] - pub value: String, - } - #[derive(Deserialize, Debug)] pub struct Tests { pub test: Vec<Test>, @@ -194,9 +179,13 @@ mod xml { #[derive(Deserialize, Debug)] pub struct Test { + #[serde(rename = "@method")] pub method: Option<String>, + #[serde(rename = "@sample")] pub sample: Option<bool>, + #[serde(rename = "@description")] pub description: Option<String>, + #[serde(rename = "@cmd")] pub cmd: Option<String>, } @@ -213,6 +202,7 @@ mod xml { #[derive(Deserialize, Debug)] pub struct File { + #[serde(rename = "@path")] pub path: String, } @@ -229,13 +219,17 @@ mod xml { #[derive(Deserialize, Debug)] pub struct Source { + #[serde(rename = "@path")] pub path: String, + #[serde(rename = "@type")] pub r#type: String, } #[derive(Deserialize, Debug)] pub struct Binary { + #[serde(rename = "@path")] pub path: String, + #[serde(rename = "@type")] pub r#type: String, } @@ -248,6 +242,7 @@ mod xml { #[derive(Deserialize, Debug)] pub struct Checker { + #[serde(rename = "@type")] pub r#type: String, pub source: Source, pub binary: Binary, @@ -258,16 +253,17 @@ mod xml { #[derive(Deserialize, Debug)] pub struct CheckerTestset { #[serde(rename = "test-count")] - pub test_count: TestCount, + pub test_count: String, #[serde(rename = "input-path-pattern")] - pub input_path_pattern: InputPathPattern, + pub input_path_pattern: String, #[serde(rename = "answer-path-pattern")] - pub answer_path_pattern: AnswerPathPattern, + pub answer_path_pattern: String, pub tests: VerdictTests, } #[derive(Deserialize, Debug)] pub struct Copy { + #[serde(rename = "@path")] pub path: String, } @@ -286,9 +282,9 @@ mod xml { #[derive(Deserialize, Debug)] pub struct ValidatorTestset { #[serde(rename = "test-count")] - pub test_count: TestCount, + pub test_count: String, #[serde(rename = "input-path-pattern")] - pub input_path_pattern: InputPathPattern, + pub input_path_pattern: String, pub tests: VerdictTests, } @@ -299,6 +295,7 @@ mod xml { #[derive(Deserialize, Debug)] pub struct VerdictTest { + #[serde(rename = "@verdict")] pub verdict: String, } @@ -309,6 +306,7 @@ mod xml { #[derive(Deserialize, Debug)] pub struct Solution { + #[serde(rename = "@tag")] pub tag: String, pub source: Source, pub binary: Binary, @@ -321,7 +319,9 @@ mod xml { #[derive(Deserialize, Debug)] pub struct Property { + #[serde(rename = "@name")] pub name: String, + #[serde(rename = "@value")] pub value: String, } @@ -344,6 +344,7 @@ mod xml { #[derive(Deserialize, Debug)] pub struct Tag { + #[serde(rename = "@value")] pub value: String, } diff --git a/src/pages/create_contest.rs b/src/pages/create_contest.rs index 8d79f0e3f15888ad18af8dd4f1fa12a96c238a18..ef82d8fc0c5d6d171f3afca37709a82c316fd778 100644 --- a/src/pages/create_contest.rs +++ b/src/pages/create_contest.rs @@ -145,6 +145,7 @@ async fn create_contest( m.insert("cpp.g++17".into(), "cpp.17.g++".into()); m.insert("cpp.g++20".into(), "cpp.20.g++".into()); m.insert("cpp.msys2-mingw64-9-g++17".into(), "cpp.17.g++".into()); + m.insert("cpp.msys2-mingw64-9-g++20".into(), "cpp.20.g++".into()); m.insert("java.8".into(), "java.8".into()); m.insert("testlib".into(), "cpp.20.g++".into()); m @@ -224,12 +225,10 @@ async fn create_contest( name: metadata.names.name[0].value.clone(), memory_limit_bytes: metadata.judging.testset[0] .memory_limit - .value .parse() .unwrap(), time_limit_ms: metadata.judging.testset[0] .time_limit - .value .parse() .unwrap(), checker_path: metadata.assets.checker.source.path.clone(), @@ -240,10 +239,9 @@ async fn create_contest( )?, main_solution_path: main_solution.path.clone(), main_solution_language: map_codeforces_language(&main_solution.r#type)?, - test_pattern: metadata.judging.testset[0].input_path_pattern.value.clone(), + test_pattern: metadata.judging.testset[0].input_path_pattern.clone(), test_count: metadata.judging.testset[0] .test_count - .value .parse() .unwrap(), status: "compiled".into(),