From 0c26a966191bedefcc6b33c8bbbfc8d807210e89 Mon Sep 17 00:00:00 2001 From: Mikail Bagishov Date: Thu, 13 May 2021 21:20:59 +0300 Subject: [PATCH] Finish initial judge implementation --- .github/workflows/ci.yaml | 44 +--- .gitignore | 2 + Cargo.lock | 76 +++--- bors.toml | 2 +- ci/config.yaml | 3 +- ci/publish-build.sh | 3 + docker-compose.yml | 42 +++- invoker-client/src/lib.rs | 1 + problem-loader/Cargo.toml | 2 +- problem-loader/src/lib.rs | 21 +- processor/src/compile.rs | 88 +++++-- processor/src/exec_test.rs | 401 +++++++++++++++++++++---------- processor/src/lib.rs | 76 +++--- processor/src/request_builder.rs | 1 + rustfmt.toml | 16 ++ setup/main.sh | 33 ++- src/main.rs | 26 +- src/rest.rs | 26 +- valuer-client/Cargo.toml | 1 - valuer-client/src/child.rs | 16 -- valuer-client/src/lib.rs | 7 +- 21 files changed, 579 insertions(+), 308 deletions(-) create mode 100644 ci/publish-build.sh create mode 100755 rustfmt.toml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5b59b41..4f52d1c 100755 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,44 +8,6 @@ name: ci - trying - master jobs: - e2e-build: - env: - DOCKER_BUILDKIT: "1" - runs-on: ubuntu-20.04 - timeout-minutes: 15 - steps: - - name: Fetch sources - uses: actions/checkout@v2 - - name: Setup cache - uses: Swatinem/rust-cache@v1 - - name: Build e2e artifacts - run: bash ci/e2e-build.sh - - name: Upload e2e artifacts - uses: actions/upload-artifact@v2 - with: - name: e2e-artifacts - path: e2e-artifacts - retention-days: "2" - e2e-run: - needs: e2e-build - runs-on: ubuntu-20.04 - timeout-minutes: 15 - steps: - - name: Fetch sources - uses: actions/checkout@v2 - - name: Download e2e artifacts - uses: actions/download-artifact@v2 - with: - name: e2e-artifacts - path: e2e-artifacts - - name: Execute tests - run: bash ci/e2e-run.sh - - name: Upload logs - uses: actions/upload-artifact@v2 - with: - name: e2e-logs - path: e2e-logs - retention-days: "2" misspell: runs-on: ubuntu-20.04 timeout-minutes: 2 @@ -100,12 +62,12 @@ jobs: steps: - name: Fetch sources uses: actions/checkout@v2 - - name: Install nightly toolchain + - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: components: clippy,rustfmt override: "true" - toolchain: nightly + toolchain: stable - name: Setup cache uses: Swatinem/rust-cache@v1 - id: cargo_udeps @@ -122,7 +84,7 @@ jobs: mkdir -p ~/udeps cp $( which cargo-udeps ) ~/udeps - name: Run cargo-udeps - run: "\nexport PATH=~/udeps:$PATH \ncargo udeps \n" + run: "\nexport PATH=~/udeps:$PATH\nexport RUSTC_BOOTSTRAP=1\ncargo udeps \n" rustfmt: name: rustfmt runs-on: ubuntu-20.04 diff --git a/.gitignore b/.gitignore index ea8c4bf..c46c367 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +log-contestant.json +log-full.json \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 5e1fdec..ffdcdb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "adler" version = "1.0.2" @@ -35,7 +37,7 @@ checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" [[package]] name = "api-util" version = "0.1.0" -source = "git+https://github.com/jjs-dev/commons#f269b3440d6dd149b145bb5a7c119710fe0ce384" +source = "git+https://github.com/jjs-dev/commons#ef88e3370a8b9ca3534475088f35dd72dacbd6c8" dependencies = [ "anyhow", "http", @@ -96,8 +98,9 @@ dependencies = [ [[package]] name = "bson" -version = "1.2.0" -source = "git+https://github.com/mongodb/bson-rust#982b15e0066ed81650827ab85257c73448d5b7c6" +version = "2.0.0-beta" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "441bd9b15699ed6fd29a3546ed614a943a9dbab851b799e2344f3f11b3230bdc" dependencies = [ "ahash", "base64", @@ -214,9 +217,9 @@ checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" [[package]] name = "cpufeatures" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec1028182c380cc45a2e2c5ec841134f2dfd0f8f5f0a5bcd68004f81b5efdf4" +checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" dependencies = [ "libc", ] @@ -744,7 +747,7 @@ dependencies = [ [[package]] name = "invoker-api" version = "0.1.0" -source = "git+https://github.com/jjs-dev/invoker#9b49072aa3310e2e45ee6e483f8507990664596e" +source = "git+https://github.com/jjs-dev/invoker#f60ff25a6837bcd23fe0260e66f932d21391a354" dependencies = [ "serde", "serde_json", @@ -971,8 +974,8 @@ dependencies = [ [[package]] name = "mongodb" -version = "2.0.0-alpha.1" -source = "git+https://github.com/mongodb/mongo-rust-driver#79d27827f8b4f84be66750b486173eb1a9531e23" +version = "2.0.0-beta" +source = "git+https://github.com/mongodb/mongo-rust-driver#dfb484ba96e409f5e880a82fd88a6a066966f8a2" dependencies = [ "async-trait", "base64", @@ -1052,18 +1055,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nix" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" -dependencies = [ - "bitflags", - "cc", - "cfg-if", - "libc", -] - [[package]] name = "ntapi" version = "0.3.6" @@ -1149,9 +1140,9 @@ dependencies = [ [[package]] name = "os_info" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afaa687e89d04fcd3573987b14965578e54434a120c9477500e24b95b6fe79ee" +checksum = "1fa6218eb01b752b12726590e9321b4ecebd4eb4f3b53b27a80fdbdcd65d7afd" dependencies = [ "log", "winapi", @@ -1244,7 +1235,7 @@ checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" [[package]] name = "pom" version = "0.1.0" -source = "git+https://github.com/jjs-dev/pps?branch=master#f1992eb358a73987582067dc812945e55cba057e" +source = "git+https://github.com/jjs-dev/pps?branch=master#f2eebed8c8928c796cb3127ccce938141cebf90e" dependencies = [ "serde", ] @@ -1559,9 +1550,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" [[package]] name = "ryu" @@ -1632,9 +1623,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.125" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" dependencies = [ "serde_derive", ] @@ -1650,9 +1641,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.125" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" dependencies = [ "proc-macro2", "quote", @@ -1685,9 +1676,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d14cb8c1b03d86e97ecbb3128d3e2f81fd8f02805680537b8d9ccb7dd8960b" +checksum = "edeeaecd5445109b937a3a335dc52780ca7779c4b4b7374cc6340dedfe44cfca" dependencies = [ "rustversion", "serde", @@ -1960,9 +1951,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5" +checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37" dependencies = [ "autocfg", "bytes", @@ -1979,9 +1970,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" +checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" dependencies = [ "proc-macro2", "quote", @@ -2011,9 +2002,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e177a5d8c3bf36de9ebe6d58537d8879e964332f93fb3339e43f618c81361af0" +checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066" dependencies = [ "futures-core", "pin-project-lite", @@ -2035,9 +2026,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940a12c99365c31ea8dd9ba04ec1be183ffe4920102bb7122c2f515437601e8e" +checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" dependencies = [ "bytes", "futures-core", @@ -2143,9 +2134,9 @@ dependencies = [ [[package]] name = "trust-dns-proto" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "952a078337565ba39007de99b151770f41039253a31846f0a3d5cd5a4ac8eedf" +checksum = "ad0d7f5db438199a6e2609debe3f69f808d074e0a2888ee0bccb45fe234d03f4" dependencies = [ "async-trait", "cfg-if", @@ -2319,7 +2310,7 @@ dependencies = [ [[package]] name = "valuer-api" version = "0.1.0" -source = "git+https://github.com/jjs-dev/pps?branch=master#f1992eb358a73987582067dc812945e55cba057e" +source = "git+https://github.com/jjs-dev/pps?branch=master#f2eebed8c8928c796cb3127ccce938141cebf90e" dependencies = [ "bitflags", "pom", @@ -2332,7 +2323,6 @@ name = "valuer-client" version = "0.1.0" dependencies = [ "anyhow", - "nix", "serde", "serde_json", "tokio", diff --git a/bors.toml b/bors.toml index f4a7ae7..e55bfd5 100755 --- a/bors.toml +++ b/bors.toml @@ -1,3 +1,3 @@ delete-merged-branches = true -status = ["check-ci-config", "e2e-build", "e2e-run", "rustfmt", "rust-unit-tests", "rust-unused-deps", "rust-cargo-deny", "rust-lint", "publish"] +status = ["check-ci-config", "rustfmt", "rust-unit-tests", "rust-unused-deps", "rust-cargo-deny", "rust-lint", "publish"] timeout-sec = 900 diff --git a/ci/config.yaml b/ci/config.yaml index 0d043ff..a72484b 100644 --- a/ci/config.yaml +++ b/ci/config.yaml @@ -1,3 +1,4 @@ dockerImages: - judge -buildTimeoutMinutes: 15 \ No newline at end of file +buildTimeoutMinutes: 15 +noE2e: true diff --git a/ci/publish-build.sh b/ci/publish-build.sh new file mode 100644 index 0000000..4050fca --- /dev/null +++ b/ci/publish-build.sh @@ -0,0 +1,3 @@ +set -euxo pipefail + +docker build -t judge --build-arg 'RELEASE=--release' . diff --git a/docker-compose.yml b/docker-compose.yml index 752be38..5547d38 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,8 +12,11 @@ services: volumes: - problems:/etc/jjs/problems:ro - toolchains:/etc/jjs/toolchains:ro + - judge_logs:/var/log/judges ports: - '1789:1789' + environment: + RUST_LOG: info,warp::filters::trace=warn,judge=debug,processor=debug setup: build: setup volumes: @@ -25,16 +28,33 @@ services: image: ghcr.io/jjs-dev/jjs-invoker:latest # TODO: investigate why this is required privileged: true + # only needed for interactive debugging + # pid: host environment: - RUST_LOG: info,invoker=debug + RUST_LOG: info,invoker=trace,minion=debug,minion::linux::sandbox::watchdog=info command: - --shim=http://shim:8001 - --listen-address=tcp://0.0.0.0:8000 - --work-dir=/var/invoker/work + # Debugging flags; uncomment if needed + # - --debug-leak-sandboxes + # - --interactive-debug-url=http://invoker-strace-debug:8000/debug volumes: - pulled:/var/shim-share + - invoker_work:/var/invoker/work + - invoker_debug:/var/invoker/debug expose: - 8000 + invoker-strace-debug: + image: ghcr.io/jjs-dev/jjs-invoker-strace-debugger:latest + privileged: true + pid: host + environment: + RUST_LOG: info,strace_debug=debug + volumes: + - strace:/var/jjs/debug/strace + expose: + - 8000 shim: image: ghcr.io/jjs-dev/jjs-invoker-shim:latest environment: @@ -50,7 +70,27 @@ services: - 8001 healthcheck: disable: true + debug: + image: ubuntu:focal + command: + - tail + - -f + - /dev/null + volumes: + - pulled:/mnt/pulled-toolchains + - problems:/mnt/problems + - toolchains:/mnt/toolchains + - invoker_work:/mnt/invoker + - invoker_debug:/mnt/invoker-debug + - judge_logs:/mnt/judge-logs + - strace:/mnt/strace + # only needed for interactive debugging (TODO: is it actually needed?) + # pid: host volumes: pulled: {} problems: {} toolchains: {} + invoker_work: {} + invoker_debug: {} + judge_logs: {} + strace: {} diff --git a/invoker-client/src/lib.rs b/invoker-client/src/lib.rs index 3b5ac6c..6a9b039 100644 --- a/invoker-client/src/lib.rs +++ b/invoker-client/src/lib.rs @@ -43,6 +43,7 @@ impl ClientBuilder { pub fn add(&mut self, pool: Pool) { self.pools.push(pool.0); } + /// Builds a client pub fn build(self) -> Client { Client { diff --git a/problem-loader/Cargo.toml b/problem-loader/Cargo.toml index cbfd511..ee548e1 100644 --- a/problem-loader/Cargo.toml +++ b/problem-loader/Cargo.toml @@ -14,7 +14,7 @@ tokio = { version = "1.5.0", features = ["fs", "sync"] } fs_extra = "1.2.0" mongodb = { git = "https://github.com/mongodb/mongo-rust-driver" } url = "2.2.1" -bson = { git = "https://github.com/mongodb/bson-rust" } +bson = "2.0.0-beta" flate2 = "1.0.20" tar = "0.4.33" serde = "1.0.125" diff --git a/problem-loader/src/lib.rs b/problem-loader/src/lib.rs index 2b07396..2a3a968 100644 --- a/problem-loader/src/lib.rs +++ b/problem-loader/src/lib.rs @@ -73,17 +73,19 @@ impl Loader { } tracing::info!("cache miss"); // cache for this problem not found, let's load it. - let assets_path = self.cache_dir.join(problem_name); - tokio::fs::remove_dir_all(&assets_path).await.ok(); - tokio::fs::create_dir(&assets_path).await.with_context(|| { - format!( - "failed to prepare problem assets directory at {}", - assets_path.display() - ) - })?; + let problem_path = self.cache_dir.join(problem_name); + tokio::fs::remove_dir_all(&problem_path).await.ok(); + tokio::fs::create_dir(&problem_path) + .await + .with_context(|| { + format!( + "failed to prepare problem assets directory at {}", + problem_path.display() + ) + })?; for registry in &self.registries { let res = registry - .get_problem(problem_name, &assets_path) + .get_problem(problem_name, &problem_path) .await .with_context(|| { format!( @@ -98,6 +100,7 @@ impl Loader { registry_name = registry.name(), "successfully resolved problem" ); + let assets_path = problem_path.join("assets"); cache.items.insert( problem_name.to_string(), ProblemCacheItem { diff --git a/processor/src/compile.rs b/processor/src/compile.rs index f62f85f..dc2079b 100644 --- a/processor/src/compile.rs +++ b/processor/src/compile.rs @@ -1,24 +1,31 @@ use crate::CommandStatus; +use anyhow::Context; use invoker_api::{ invoke::{ Action, ActionResult, Command, EnvVarValue, EnvironmentVariable, Extensions, FileId, - InvokeRequest, Limits, OutputRequest, OutputRequestTarget, SandboxSettings, SharedDir, - SharedDirectoryMode, Stdio, Step, + InvokeRequest, Limits, OutputRequest, OutputRequestTarget, PathPrefix, PrefixedPath, + SandboxSettings, SharedDir, SharedDirectoryMode, Stdio, Step, VolumeSettings, }, - shim::{SandboxSettingsExtensions, EXTRA_FILES_DIR_NAME}, + shim::{ExtraFile, SandboxSettingsExtensions, EXTRA_FILES_DIR_NAME}, }; use std::{collections::HashMap, path::PathBuf}; use uuid::Uuid; use valuer_api::{status_codes, Status, StatusKind}; +pub(crate) struct BuiltRun { + pub(crate) binary: Vec, +} + pub(crate) struct BuildOutcome { - pub(crate) result: Result<(), Status>, + // Wrapped in option to allow stealing + pub(crate) result: Result, Status>, pub(crate) log: String, } //const FILE_ID_SOURCE: &str = "run-source"; const FILE_ID_EMPTY: &str = "empty"; const SANDBOX_NAME: &str = "compile-sandbox"; +const VOLUME_NAME: &str = "work"; pub(crate) async fn compile( req: &crate::Request, @@ -33,10 +40,16 @@ pub(crate) async fn compile( let mut ef = HashMap::new(); ef.insert( toolchain.spec.filename.clone(), - req_builder.intern(&req.run_source).await?, + ExtraFile { + contents: req_builder.intern(&req.run_source).await?, + executable: false, + }, ); s.insert("Run.SourceFilePath".to_string(), source_file_path.clone()); - s.insert("Run.BinaryFilePath".to_string(), "/jjs/bin".to_string()); + s.insert( + "Run.BinaryFilePath".to_string(), + "/compile-output/bin".to_string(), + ); (s, ef) }; let mut invoke_request = InvokeRequest { @@ -58,11 +71,20 @@ pub(crate) async fn compile( ext: Extensions::default(), }); + invoke_request.steps.push(Step { + stage: 0, + action: Action::CreateVolume(VolumeSettings { + name: VOLUME_NAME.to_string(), + limit: toolchain.spec.limits.work_dir_size, + ext: Extensions::default(), + }), + ext: Extensions::default(), + }); + let limits = Limits { memory: toolchain.spec.limits.memory(), time: toolchain.spec.limits.time(), process_count: toolchain.spec.limits.process_count, - work_dir_size: toolchain.spec.limits.work_dir_size, ext: Extensions::default(), }; invoke_request.steps.push(Step { @@ -71,14 +93,32 @@ pub(crate) async fn compile( limits: limits.clone(), name: SANDBOX_NAME.to_string(), base_image: PathBuf::new(), - expose: vec![SharedDir { - host_path: EXTRA_FILES_DIR_NAME.into(), - sandbox_path: "/compile-input".into(), - mode: SharedDirectoryMode::ReadOnly, - create: false, - ext: Extensions::default(), - }], - work_dir: PathBuf::new(), + expose: vec![ + SharedDir { + host_path: PrefixedPath { + prefix: PathPrefix::Extension(Extensions::make( + invoker_api::shim::SharedDirExtensionSource { + name: EXTRA_FILES_DIR_NAME.to_string(), + }, + )?), + path: PathBuf::new(), + }, + sandbox_path: "/compile-input".into(), + mode: SharedDirectoryMode::ReadOnly, + create: false, + ext: Extensions::default(), + }, + SharedDir { + host_path: PrefixedPath { + prefix: PathPrefix::Volume(VOLUME_NAME.to_string()), + path: PathBuf::new(), + }, + sandbox_path: "/compile-output".into(), + mode: SharedDirectoryMode::ReadWrite, + create: false, + ext: Extensions::default(), + }, + ], ext: Extensions::make(SandboxSettingsExtensions { image: toolchain.image.clone(), })?, @@ -121,7 +161,7 @@ pub(crate) async fn compile( ext: Extensions::default(), }) .collect(), - cwd: "/jjs".to_string(), + cwd: "/".to_string(), stdio: Stdio { stdin: FileId(FILE_ID_EMPTY.to_string()), stdout: FileId(stdout_file_id.clone()), @@ -149,6 +189,16 @@ pub(crate) async fn compile( ext: Extensions::default(), }); } + + invoke_request.outputs.push(OutputRequest { + name: "artifact".to_string(), + target: OutputRequestTarget::Path(PrefixedPath { + prefix: PathPrefix::Volume(VOLUME_NAME.to_string()), + path: "bin".into(), + }), + ext: Extensions::default(), + }); + let response = client.instance()?.call(invoke_request).await?; let mut compile_log = String::new(); for (step_no, pos) in command_steps.into_iter().enumerate() { @@ -185,8 +235,12 @@ pub(crate) async fn compile( log: compile_log, }); } + let binary = req_builder + .read_output(&response, "artifact") + .await + .context("failed to export compiled binary")?; Ok(BuildOutcome { - result: Ok(()), + result: Ok(Some(BuiltRun { binary })), log: compile_log, }) } diff --git a/processor/src/exec_test.rs b/processor/src/exec_test.rs index d2a76d9..36d7bd8 100644 --- a/processor/src/exec_test.rs +++ b/processor/src/exec_test.rs @@ -1,16 +1,23 @@ mod checker_proto; -use std::path::PathBuf; - use anyhow::Context; -use invoker_api::invoke::{ - Action, ActionResult, Command, EnvVarValue, EnvironmentVariable, Extensions, FileId, Input, - InvokeRequest, Limits, SandboxSettings, Stdio, Step, +use invoker_api::{ + invoke::{ + Action, ActionResult, Command, EnvVarValue, EnvironmentVariable, Extensions, FileId, Input, + InvokeRequest, Limits, OutputRequest, OutputRequestTarget, PathPrefix, PrefixedPath, + SandboxSettings, SharedDir, SharedDirectoryMode, Stdio, Step, + }, + shim::{ + ExtraFile, RequestExtensions, SandboxSettingsExtensions, SharedDirExtensionSource, + EXTRA_FILES_DIR_NAME, + }, }; -use tokio::io::AsyncWriteExt; +use std::{collections::HashMap, path::PathBuf}; use uuid::Uuid; use valuer_api::{status_codes, Status, StatusKind}; +use crate::compile::BuiltRun; + #[derive(Debug, Clone, Copy, Default)] pub(crate) struct ResourceUsage { pub(crate) memory: Option, @@ -46,42 +53,79 @@ fn map_checker_outcome_to_status(out: checker_proto::Output) -> Status { } } -/// Runs Artifact on one test and produces output -pub(crate) async fn exec( +const PREPARE_STAGE: u32 = 0; +const EXEC_SOLUTION_STAGE: u32 = 1; +const TEST_DATA_INPUT_FILE: &str = "test-data"; +const EXEC_SOLUTION_OUTPUT_FILE: &str = "solution-output"; +const EXEC_SOLUTION_ERROR_FILE: &str = "solution-error"; +const CORRECT_ANSWER_FILE: &str = "correct"; +const EMPTY_FILE: &str = "empty"; + +const SOLUTION_SANDBOX_NAME: &str = "exec-sandbox"; +const CHECKER_SANDBOX_NAME: &str = "checker-sandbox"; + +const EXEC_CHECKER_STAGE: u32 = 2; + +const CHECKER_DECISION: &str = "checker-decision"; +const CHECKER_LOG: &str = "checker-logs"; + +struct StepIds { + exec_solution: usize, + exec_checker: usize, +} + +async fn create_request( toolchain: &toolchain_loader::Toolchain, problem: &pom::Problem, - client: invoker_client::Client, file_ref_resolver: &crate::FileRefResolver, - test_id: pom::TestId, - debug: &crate::DebugDumps, -) -> anyhow::Result { - let test = problem - .tests - .get(test_id.to_idx()) - .context("unknown test")?; + test: &pom::Test, + req_builder: &crate::request_builder::RequestBuilder, + built: &BuiltRun, +) -> anyhow::Result<(InvokeRequest, StepIds)> { + let (substitutions, extra_files) = { + let mut s = HashMap::new(); + let mut ef = HashMap::new(); + let test_path = file_ref_resolver.resolve_asset(&test.path); + ef.insert( + "exec/test".to_string(), + ExtraFile { + contents: req_builder.intern_file(&test_path).await?, + executable: false, + }, + ); + ef.insert( + "compile-out/bin".to_string(), + ExtraFile { + contents: req_builder.intern(&built.binary).await?, + executable: true, + }, + ); + let checker = file_ref_resolver.resolve_asset(&problem.checker_exe); + ef.insert( + "check/checker".to_string(), + ExtraFile { + contents: req_builder.intern_file(&checker).await?, + executable: true, + }, + ); + s.insert( + "Run.BinaryFilePath".to_string(), + "/compile-out/bin".to_string(), + ); + (s, ef) + }; let mut invoke_request = InvokeRequest { steps: vec![], inputs: vec![], outputs: vec![], id: Uuid::nil(), - ext: Extensions::default(), + ext: Extensions::make(RequestExtensions { + extra_files, + substitutions, + })?, }; - let req_builder = crate::request_builder::RequestBuilder::new(); let test_file = file_ref_resolver.resolve_asset(&test.path); - //let test_data = tokio::fs::read(input_file).await.context("failed to read test")?; - const PREPARE_STAGE: u32 = 0; - const EXEC_SOLUTION_STAGE: u32 = 1; - const TEST_DATA_INPUT_FILE: &str = "test-data"; - const EXEC_SOLUTION_OUTPUT_FILE: &str = "solution-output"; - const EXEC_SOLUTION_ERROR_FILE: &str = "solution-error"; - const CORRECT_ANSWER_FILE: &str = "correct"; - const EMPTY_FILE: &str = "empty"; - - const SOLUTION_SANDBOX_NAME: &str = "exec-sandbox"; - const CHECKER_SANDBOX_NAME: &str = "checker-sandbox"; - - const EXEC_CHECKER_STAGE: u32 = 2; // create an input with the test data let test_data_input = Input { @@ -107,7 +151,7 @@ pub(crate) async fn exec( stage: EXEC_SOLUTION_STAGE, action: Action::CreateFile { id: FileId(EXEC_SOLUTION_OUTPUT_FILE.to_string()), - readable: false, + readable: true, writeable: true, }, ext: Extensions::default(), @@ -116,15 +160,13 @@ pub(crate) async fn exec( stage: EXEC_SOLUTION_STAGE, action: Action::CreateFile { id: FileId(EXEC_SOLUTION_ERROR_FILE.to_string()), - readable: false, + readable: true, writeable: true, }, ext: Extensions::default(), }); - let exec_solution_step_id = invoke_request.steps.len(); - - // create sandbox + // create solution sandbox invoke_request.steps.push(Step { stage: EXEC_SOLUTION_STAGE, action: Action::CreateSandbox(SandboxSettings { @@ -132,74 +174,85 @@ pub(crate) async fn exec( memory: test.limits.memory(), time: test.limits.time(), process_count: Some(test.limits.process_count()), - work_dir_size: Some(test.limits.work_dir_size()), ext: Extensions::default(), }, name: SOLUTION_SANDBOX_NAME.to_string(), base_image: PathBuf::new(), - expose: Vec::new(), - work_dir: PathBuf::new(), - ext: Extensions::default(), + expose: vec![SharedDir { + host_path: PrefixedPath { + prefix: PathPrefix::Extension(Extensions::make(SharedDirExtensionSource { + name: EXTRA_FILES_DIR_NAME.to_string(), + })?), + path: "compile-out".into(), + }, + sandbox_path: "/compile-out".into(), + mode: SharedDirectoryMode::ReadOnly, + create: false, + ext: Extensions::default(), + }], + ext: Extensions::make(SandboxSettingsExtensions { + image: toolchain.image.clone(), + })?, }), ext: Extensions::default(), }); + // produce a step for executing solution - { - let exec_solution_step = Step { - stage: EXEC_SOLUTION_STAGE, - action: Action::ExecuteCommand(Command { - sandbox_name: SOLUTION_SANDBOX_NAME.to_string(), - argv: toolchain.spec.run_command.argv.clone(), - env: toolchain - .spec - .run_command - .env - .iter() - .map(|(k, v)| EnvironmentVariable { - name: k.clone(), - value: EnvVarValue::Plain(v.clone()), - ext: Extensions::default(), - }) - .collect(), - cwd: toolchain.spec.run_command.cwd.clone(), - stdio: Stdio { - stdin: FileId(TEST_DATA_INPUT_FILE.to_string()), - stdout: FileId(EXEC_SOLUTION_OUTPUT_FILE.to_string()), - stderr: FileId(EXEC_SOLUTION_ERROR_FILE.to_string()), + let exec_solution_step_id = invoke_request.steps.len(); + + invoke_request.steps.push(Step { + stage: EXEC_SOLUTION_STAGE, + action: Action::ExecuteCommand(Command { + sandbox_name: SOLUTION_SANDBOX_NAME.to_string(), + argv: toolchain.spec.run_command.argv.clone(), + env: toolchain + .spec + .run_command + .env + .iter() + .map(|(k, v)| EnvironmentVariable { + name: k.clone(), + value: EnvVarValue::Plain(v.clone()), ext: Extensions::default(), - }, + }) + .collect(), + cwd: toolchain.spec.run_command.cwd.clone(), + stdio: Stdio { + stdin: FileId(TEST_DATA_INPUT_FILE.to_string()), + stdout: FileId(EXEC_SOLUTION_OUTPUT_FILE.to_string()), + stderr: FileId(EXEC_SOLUTION_ERROR_FILE.to_string()), ext: Extensions::default(), - }), + }, ext: Extensions::default(), - }; - invoke_request.steps.push(exec_solution_step); - } + }), + ext: Extensions::default(), + }); + // provide a correct answer if requested + let has_correct_answer; { - let source = if let Some(corr_path) = &test.correct { + if let Some(corr_path) = &test.correct { let full_path = file_ref_resolver.resolve_asset(corr_path); - let data = tokio::fs::read(full_path) - .await - .context("failed to read correct answer")?; - req_builder.intern(&data).await? + let source = req_builder.intern_file(&full_path).await?; + + has_correct_answer = true; + + invoke_request.inputs.push(Input { + file_id: FileId(CORRECT_ANSWER_FILE.to_string()), + source, + ext: Extensions::default(), + }) } else { - req_builder.intern(&[]).await? - }; - invoke_request.inputs.push(Input { - file_id: FileId(CORRECT_ANSWER_FILE.to_string()), - source, - ext: Extensions::default(), - }) + has_correct_answer = false; + } } // generate checker feedback files - const CHECKER_DECISION: &str = "checker-decision"; - const CHECKER_LOG: &str = "checker-logs"; invoke_request.steps.push(Step { stage: EXEC_CHECKER_STAGE, action: Action::CreateFile { id: FileId(CHECKER_DECISION.to_string()), - readable: false, + readable: true, writeable: true, }, ext: Extensions::default(), @@ -208,52 +261,90 @@ pub(crate) async fn exec( stage: EXEC_CHECKER_STAGE, action: Action::CreateFile { id: FileId(CHECKER_LOG.to_string()), - readable: false, + readable: true, writeable: true, }, ext: Extensions::default(), }); + // create a checker sandbox + invoke_request.steps.push(Step { + stage: EXEC_CHECKER_STAGE, + action: Action::CreateSandbox(SandboxSettings { + limits: Limits { + memory: test.limits.memory(), + time: test.limits.time(), + process_count: Some(test.limits.process_count()), + ext: Extensions::default(), + }, + name: CHECKER_SANDBOX_NAME.to_string(), + base_image: PathBuf::new(), + expose: vec![SharedDir { + host_path: PrefixedPath { + prefix: PathPrefix::Extension(Extensions::make(SharedDirExtensionSource { + name: EXTRA_FILES_DIR_NAME.to_string(), + })?), + path: "check".into(), + }, + sandbox_path: "/check".into(), + mode: SharedDirectoryMode::ReadOnly, + create: false, + ext: Extensions::default(), + }], + ext: Extensions::make(SandboxSettingsExtensions { + // TODO: allow overriding + image: "gcr.io/distroless/cc:latest".to_string(), + })?, + }), + ext: Extensions::default(), + }); + // produce a step for executing checker let exec_checker_test_id = invoke_request.steps.len(); + + let mut checker_cmd = vec!["/check/checker".to_string()]; + checker_cmd.extend_from_slice(&problem.checker_cmd); + let mut checker_env = vec![ + EnvironmentVariable { + name: "JJS_SOL".to_string(), + value: EnvVarValue::File(FileId(EXEC_SOLUTION_OUTPUT_FILE.to_string())), + ext: Extensions::default(), + }, + EnvironmentVariable { + name: "JJS_TEST".to_string(), + value: EnvVarValue::File(FileId(TEST_DATA_INPUT_FILE.to_string())), + ext: Extensions::default(), + }, + EnvironmentVariable { + name: "JJS_CHECKER_OUT".to_string(), + value: EnvVarValue::File(FileId(CHECKER_DECISION.to_string())), + ext: Extensions::default(), + }, + EnvironmentVariable { + name: "JJS_CHECKER_COMMENT".to_string(), + value: EnvVarValue::File(FileId(CHECKER_LOG.to_string())), + ext: Extensions::default(), + }, + ]; + + if has_correct_answer { + checker_env.push(EnvironmentVariable { + name: "JJS_CORR".to_string(), + value: EnvVarValue::File(FileId(CORRECT_ANSWER_FILE.to_string())), + ext: Extensions::default(), + }); + } + invoke_request.steps.push(Step { stage: EXEC_CHECKER_STAGE, action: Action::ExecuteCommand(Command { - argv: problem.checker_cmd.clone(), - env: vec![ - EnvironmentVariable { - name: "JJS_CORR".to_string(), - value: EnvVarValue::File(FileId(CORRECT_ANSWER_FILE.to_string())), - ext: Extensions::default(), - }, - EnvironmentVariable { - name: "JJS_SOL".to_string(), - value: EnvVarValue::File(FileId(EXEC_SOLUTION_OUTPUT_FILE.to_string())), - ext: Extensions::default(), - }, - EnvironmentVariable { - name: "JJS_TEST".to_string(), - value: EnvVarValue::File(FileId(TEST_DATA_INPUT_FILE.to_string())), - ext: Extensions::default(), - }, - EnvironmentVariable { - name: "JJS_CHECKER_OUT".to_string(), - value: EnvVarValue::File(FileId(CHECKER_DECISION.to_string())), - ext: Extensions::default(), - }, - EnvironmentVariable { - name: "JJS_CHECKER_COMMENT".to_string(), - value: EnvVarValue::File(FileId(CHECKER_LOG.to_string())), - ext: Extensions::default(), - }, - ] - .into_iter() - .collect(), + argv: checker_cmd, + env: checker_env, cwd: "/".to_string(), stdio: Stdio { stdin: FileId(EMPTY_FILE.to_string()), - stdout: FileId(EMPTY_FILE.to_string()), - stderr: FileId(EMPTY_FILE.to_string()), + stdout: FileId(CHECKER_LOG.to_string()), + stderr: FileId(CHECKER_LOG.to_string()), ext: Extensions::default(), }, ext: Extensions::default(), @@ -262,13 +353,77 @@ pub(crate) async fn exec( ext: Extensions::default(), }); + // add output requests + invoke_request.outputs.push(OutputRequest { + name: CHECKER_LOG.to_string(), + target: OutputRequestTarget::File(FileId(CHECKER_LOG.to_string())), + ext: Extensions::default(), + }); + invoke_request.outputs.push(OutputRequest { + name: CHECKER_DECISION.to_string(), + target: OutputRequestTarget::File(FileId(CHECKER_DECISION.to_string())), + ext: Extensions::default(), + }); + invoke_request.outputs.push(OutputRequest { + name: EXEC_SOLUTION_OUTPUT_FILE.to_string(), + target: OutputRequestTarget::File(FileId(EXEC_SOLUTION_OUTPUT_FILE.to_string())), + ext: Extensions::default(), + }); + invoke_request.outputs.push(OutputRequest { + name: EXEC_SOLUTION_ERROR_FILE.to_string(), + target: OutputRequestTarget::File(FileId(EXEC_SOLUTION_ERROR_FILE.to_string())), + ext: Extensions::default(), + }); + + Ok(( + invoke_request, + StepIds { + exec_checker: exec_checker_test_id, + exec_solution: exec_solution_step_id, + }, + )) +} + +/// Runs Artifact on one test and produces output +pub(crate) async fn exec( + toolchain: &toolchain_loader::Toolchain, + problem: &pom::Problem, + client: invoker_client::Client, + file_ref_resolver: &crate::FileRefResolver, + test_id: pom::TestId, + settings: &crate::Settings, + built: &BuiltRun, +) -> anyhow::Result { + let req_builder = crate::request_builder::RequestBuilder::new(); + + let test = problem + .tests + .get(test_id.to_idx()) + .context("unknown test")?; + + let (invoke_request, step_ids) = create_request( + toolchain, + problem, + file_ref_resolver, + test, + &req_builder, + built, + ) + .await + .context("failed to prepare invoke request")?; + let response = client.instance()?.call(invoke_request).await?; - if let Some(dir) = &debug.checker_logs { - let checker_out_file = tokio::fs::File::create(dir.join(test_id.to_string())).await?; - let mut checker_out_file = tokio::io::BufWriter::new(checker_out_file); + tracing::debug!("parsing invoker response"); + + if let Some(dir) = &settings.checker_logs { + tracing::debug!("saving checker log"); + tokio::fs::create_dir_all(&dir) + .await + .context("failed to create checker logs directory")?; + let checker_out_file = dir.join(test_id.get().to_string()); let checker_logs = req_builder.read_output(&response, CHECKER_LOG).await?; - checker_out_file.write_all(&checker_logs).await?; + tokio::fs::write(checker_out_file, checker_logs).await?; } let make_return_value_for_judge_fault = || { @@ -286,11 +441,11 @@ pub(crate) async fn exec( let solution_command_result = { let res = response .actions - .get(exec_solution_step_id) + .get(step_ids.exec_solution) .context("bug: invalid index")?; match res { ActionResult::ExecuteCommand(cmd) => cmd, - _ => anyhow::bail!("bug: unexpected action result"), + _ => anyhow::bail!("bug: unexpected action result for exec solution step"), } }; @@ -304,11 +459,11 @@ pub(crate) async fn exec( let checker_command_result = { let res = response .actions - .get(exec_checker_test_id) + .get(step_ids.exec_checker) .context("bug: invalid index")?; match res { ActionResult::ExecuteCommand(cmd) => cmd, - _ => anyhow::bail!("bug: unexpected action result"), + _ => anyhow::bail!("bug: unexpected action result for exec checker step"), } }; diff --git a/processor/src/lib.rs b/processor/src/lib.rs index 0e86a72..4b0105b 100644 --- a/processor/src/lib.rs +++ b/processor/src/lib.rs @@ -64,16 +64,17 @@ pub struct Clients { pub invokers: invoker_client::Client, } -/// Debugging settings -pub struct DebugDumps { - /// ${checker_logs}/${test_id} will contain checker log +/// Settings are global rather then come from a request. +#[derive(Clone)] +pub struct Settings { + /// ${checker_logs}/${job_id}/${test_id} will contain checker log /// for a test test_id. pub checker_logs: Option, } /// The main function, which responds to a single request. -#[tracing::instrument(skip(req, clients))] -pub fn judge(req: Request, clients: Clients) -> JobProgress { +#[tracing::instrument(skip(req, clients, settings))] +pub fn judge(req: Request, clients: Clients, settings: Settings) -> JobProgress { let (done_tx, done_rx) = oneshot::channel(); let (events_tx, events_rx) = mpsc::channel(1); tokio::task::spawn( @@ -85,7 +86,7 @@ pub fn judge(req: Request, clients: Clients) -> JobProgress { debug_dump_dir: None, }; - let res = do_judge(req, events_tx, clients, &mut protocol_sender).await; + let res = do_judge(req, events_tx, clients, &mut protocol_sender, settings).await; if let Err(err) = &res { tracing::warn!(err = %format_args!("{:#}", err),"judging failed, responding with judge fault"); protocol_sender @@ -123,6 +124,7 @@ impl JobProgress { Err(error) => JudgeOutcome::Fault { error }, } } + /// Returns next event. pub async fn event(&mut self) -> Option { self.events_rx.recv().await @@ -134,6 +136,7 @@ async fn do_judge( tx: mpsc::Sender, clients: Clients, protocol_sender: &mut ProtocolSender, + settings: Settings, ) -> anyhow::Result<()> { tracing::info!("loading problem"); let (problem, problem_assets) = clients @@ -144,7 +147,7 @@ async fn do_judge( .context("problem not found")?; let file_ref_resolver = FileRefResolver { - problem_dir: problem_assets.clone(), + problem_assets_dir: problem_assets.clone(), }; tracing::info!("loading toolchain"); @@ -155,23 +158,37 @@ async fn do_judge( .context("failed to find toolchain")?; tracing::info!("compiling"); - let compile_res = compile::compile(&req, &toolchain, clients.invokers.clone()).await?; - if let Err(status) = compile_res.result { - tracing::info!("compilation failed"); - protocol_sender - .send_fake_logs(status, &compile_res.log) - .await; - return Ok(()); - } + let mut compile_res = compile::compile(&req, &toolchain, clients.invokers.clone()).await?; + let built = match &mut compile_res.result { + Ok(b) => b.take().expect("compile does not return none"), + Err(status) => { + tracing::info!("compilation failed"); + protocol_sender + .send_fake_logs(status.clone(), &compile_res.log) + .await; + return Ok(()); + } + }; + let compile_res = compile_res; tracing::info!("running tests"); + let valuer_config = match &problem.valuer { - Valuer::Child(child) => ClientConfig::Child(ChildClientConfig { - exe: file_ref_resolver.resolve_asset(&child.exe), - args: child.extra_args.clone(), - // TODO: support overriding - log_file: "/dev/null".into(), - current_dir: problem_assets.clone(), - }), + Valuer::Child(child) => { + let current_dir = match &child.current_dir { + Some(p) => file_ref_resolver.resolve_asset(p), + None => { + tracing::debug!( + "valuer current_directory unset in problem manifest, defaulting to problem assets directory" + ); + problem_assets.clone() + } + }; + ClientConfig::Child(ChildClientConfig { + exe: file_ref_resolver.resolve_asset(&child.exe), + args: child.extra_args.clone(), + current_dir, + }) + } }; let mut valuer = valuer_client::ValuerClient::new(&valuer_config) .await @@ -194,23 +211,22 @@ async fn do_judge( tx.send(Event::LiveTest(tid.get())).await.ok(); } - let dbg_dumps = DebugDumps { checker_logs: None }; - - let judge_response = exec_test::exec( + let test_result = exec_test::exec( &toolchain, &problem, clients.invokers.clone(), &file_ref_resolver, tid, - &dbg_dumps, + &settings, + &built, ) .await .with_context(|| format!("failed to judge solution on test {}", tid))?; - test_results.push((tid, judge_response.clone())); + test_results.push((tid, test_result.clone())); valuer .notify_test_done(TestDoneNotification { test_id: tid, - test_status: judge_response.status, + test_status: test_result.status, }) .await .with_context(|| { @@ -276,13 +292,13 @@ fn describe_command_result(limits: &Limits, data: &CommandResult) -> CommandStat } struct FileRefResolver { - problem_dir: PathBuf, + problem_assets_dir: PathBuf, } impl FileRefResolver { fn resolve_asset(&self, short_path: &pom::FileRef) -> PathBuf { let root: Cow = match short_path.root { - pom::FileRefRoot::Problem => self.problem_dir.clone().into(), + pom::FileRefRoot::Problem => self.problem_assets_dir.clone().into(), pom::FileRefRoot::Root => Path::new("/").into(), }; diff --git a/processor/src/request_builder.rs b/processor/src/request_builder.rs index 640b4f6..94036a8 100644 --- a/processor/src/request_builder.rs +++ b/processor/src/request_builder.rs @@ -26,6 +26,7 @@ impl RequestBuilder { pub async fn read_output_data(&self, out: &OutputData) -> anyhow::Result> { match out { OutputData::InlineBase64(b) => base64::decode(b).context("invalid base64"), + OutputData::None => anyhow::bail!("output is None"), } } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100755 index 0000000..18a6cac --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,16 @@ + +# GENERATED FILE + +imports_granularity = "Crate" +force_explicit_abi = true +reorder_imports = true +reorder_modules = true +reorder_impl_items = true +use_field_init_shorthand = true +format_code_in_doc_comments = true +edition = "2018" +merge_derives = true +newline_style = "Unix" +report_fixme = "Unnumbered" +unstable_features = true +version = "Two" diff --git a/setup/main.sh b/setup/main.sh index 6c20da3..981a853 100644 --- a/setup/main.sh +++ b/setup/main.sh @@ -1,17 +1,24 @@ #!/usr/bin/env bash set -euxo pipefail -echo "Setting up problems" -cd /etc/problems -for i in *; do - mkdir -p /var/problems/$i - pps-cli compile --pkg /etc/problems/$i --out /var/problems/$i --force -done +if [ ! -f /var/problems/setup-done ]; then + echo "Setting up problems" + cd /etc/problems + for i in *; do + mkdir -p /var/problems/$i + pps-cli compile --pkg /etc/problems/$i --out /var/problems/$i --force + done + touch /var/problems/setup-done +fi -echo "Setting up toolchains" -cd /etc/toolchains -for i in *; do - mkdir -p /var/toolchains/$i - cp $i/manifest.yaml /var/toolchains/$i/manifest.yaml - echo "ghcr.io/jjs-dev/toolchain-$i:latest" > /var/toolchains/$i/image.txt -done \ No newline at end of file +if [ ! -f /var/toolchains/setup-done ]; then + echo "Setting up toolchains" + cd /etc/toolchains + for i in *; do + mkdir -p /var/toolchains/$i + cp $i/manifest.yaml /var/toolchains/$i/manifest.yaml + echo "ghcr.io/jjs-dev/toolchain-$i:latest" > /var/toolchains/$i/image.txt + done + + touch /var/toolchains/setup-done +fi \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 86f8487..6858041 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,10 @@ mod rest; use anyhow::Context; use clap::Clap; -use std::{path::PathBuf, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; #[derive(Clap)] struct Args { @@ -24,6 +27,9 @@ struct Args { /// URL identifying MongoDB database containing problems #[clap(long)] problems_source_mongodb: Option, + /// Directory containing judging logs. Set to `/dev/null` to disable logging + #[clap(long, default_value = "/var/log/judges")] + logs: PathBuf, } async fn create_clients(args: &Args) -> anyhow::Result { @@ -59,6 +65,22 @@ async fn main() -> anyhow::Result<()> { .context("failed to initialize dependency clients")?; tracing::info!("Running REST API"); let cfg = rest::RestConfig { port: args.port }; - rest::serve(cfg, clients).await?; + + let settings = { + let checker_logs = match &args.logs { + p if p == Path::new("/dev/null") => (None), + p => Some(p.join("checkers")), + }; + if let Some(p) = &checker_logs { + tokio::fs::create_dir_all(&p).await.with_context(|| { + format!( + "failed to create directory for checker logs {}", + p.display() + ) + })?; + } + processor::Settings { checker_logs } + }; + rest::serve(cfg, clients, settings).await?; Ok(()) } diff --git a/src/rest.rs b/src/rest.rs index 7471840..94433de 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -45,6 +45,7 @@ impl JudgeJob { struct State { judge: RwLock>>>, clients: processor::Clients, + settings: processor::Settings, } async fn start_job( @@ -56,8 +57,16 @@ async fn start_job( problem_id: req.problem_id, run_source: req.run_source.0, }; - let mut progress = processor::judge(proc_request, state.clients.clone()); let job_id = Uuid::new_v4(); + let mut settings = state.settings.clone(); + { + let mut job_id_s = Uuid::encode_buffer(); + let job_id_s = job_id.to_hyphenated().encode_lower(&mut job_id_s); + if let Some(p) = &mut settings.checker_logs { + p.push(&*job_id_s); + } + } + let mut progress = processor::judge(proc_request, state.clients.clone(), settings); let job = JudgeJob { id: job_id, live_test: None, @@ -106,7 +115,7 @@ async fn get_job(state: Arc, id: Uuid) -> anyhow::Result anyhow::Result<()> { +#[tracing::instrument(skip(cfg, clients, settings))] +pub async fn serve( + cfg: RestConfig, + clients: processor::Clients, + settings: processor::Settings, +) -> anyhow::Result<()> { let state = Arc::new(State { judge: RwLock::new(HashMap::new()), clients, + settings, }); let state2 = state.clone(); let route_create_job = warp::post() diff --git a/valuer-client/Cargo.toml b/valuer-client/Cargo.toml index 3a78f8a..f1b3736 100644 --- a/valuer-client/Cargo.toml +++ b/valuer-client/Cargo.toml @@ -6,7 +6,6 @@ edition = "2018" [dependencies] anyhow = "1.0.40" -nix = "0.20.0" serde = "1.0.125" serde_json = "1.0.64" tokio = { version = "1.5.0", features = ["process", "io-util", "time"] } diff --git a/valuer-client/src/child.rs b/valuer-client/src/child.rs index 3dacb8a..29b3fca 100644 --- a/valuer-client/src/child.rs +++ b/valuer-client/src/child.rs @@ -1,6 +1,5 @@ use crate::ChildClientConfig; use anyhow::Context; -use std::os::unix::io::IntoRawFd; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}; pub(crate) struct ChildClient { @@ -10,8 +9,6 @@ pub(crate) struct ChildClient { _child: tokio::process::Child, } -const STDERR_FD: i32 = 2; - impl ChildClient { pub(crate) async fn new(cfg: &ChildClientConfig) -> anyhow::Result { let mut cmd = tokio::process::Command::new(&cfg.exe); @@ -32,19 +29,6 @@ impl ChildClient { cfg.current_dir.display() ); } - let log = tokio::fs::File::create(&cfg.log_file) - .await - .context("failed to create valuer log file")? - .into_std() - .await - .into_raw_fd(); - unsafe { - cmd.pre_exec(move || { - nix::unistd::dup3(log, STDERR_FD, nix::fcntl::OFlag::empty()) - .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; - Ok(()) - }); - } let mut child = cmd.spawn().with_context(|| { format!( "failed to spawn valuer {} (requested current dir {})", diff --git a/valuer-client/src/lib.rs b/valuer-client/src/lib.rs index 9d526c2..ce31c13 100644 --- a/valuer-client/src/lib.rs +++ b/valuer-client/src/lib.rs @@ -1,19 +1,19 @@ -use std::path::PathBuf; - use child::ChildClient; +use std::path::PathBuf; mod child; /// Data, required to create a valuer client. /// This is a bit lowered version of `pom::Valuer`. +#[derive(Debug)] pub enum ClientConfig { Child(ChildClientConfig), } +#[derive(Debug)] pub struct ChildClientConfig { pub exe: PathBuf, pub args: Vec, - pub log_file: PathBuf, pub current_dir: PathBuf, } @@ -26,6 +26,7 @@ pub struct ValuerClient(Inner); impl ValuerClient { pub async fn new(config: &ClientConfig) -> anyhow::Result { + tracing::info!(config = ?config, "connecting to valuer"); let inner = match config { ClientConfig::Child(cfg) => Inner::Child(ChildClient::new(cfg).await?), };