├── .dockerignore ├── .github └── workflows │ └── build.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── README.en.md ├── README.md ├── crates ├── seele-cgroup │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── systemd.rs │ │ ├── systemd_api.rs │ │ └── utils.rs ├── seele-composer │ ├── Cargo.toml │ └── src │ │ ├── execute.rs │ │ ├── lib.rs │ │ ├── predicate.rs │ │ ├── report.rs │ │ ├── reporter │ │ ├── javascript.rs │ │ ├── mod.rs │ │ └── utils.rs │ │ ├── resolve.rs │ │ ├── signal.rs │ │ └── tests │ │ ├── snapshots │ │ ├── seele_composer__execute__tests__execute_submission@submission_complex_1.yaml.snap │ │ ├── seele_composer__execute__tests__execute_submission@submission_needs_1.yaml.snap │ │ ├── seele_composer__execute__tests__execute_submission@submission_needs_2.yaml.snap │ │ ├── seele_composer__execute__tests__execute_submission@submission_needs_complex.yaml.snap │ │ ├── seele_composer__execute__tests__execute_submission@submission_nested_sequence_1.yaml.snap │ │ ├── seele_composer__execute__tests__execute_submission@submission_nested_sequence_2.yaml.snap │ │ ├── seele_composer__execute__tests__execute_submission@submission_nested_sequence_3.yaml.snap │ │ ├── seele_composer__execute__tests__execute_submission@submission_simple.yaml.snap │ │ ├── seele_composer__resolve__tests__resolve_submission@submission_complex_1.yaml.snap │ │ ├── seele_composer__resolve__tests__resolve_submission@submission_needs_1.yaml.snap │ │ ├── seele_composer__resolve__tests__resolve_submission@submission_needs_2.yaml.snap │ │ ├── seele_composer__resolve__tests__resolve_submission@submission_needs_complex.yaml.snap │ │ ├── seele_composer__resolve__tests__resolve_submission@submission_nested_sequence_1.yaml.snap │ │ ├── seele_composer__resolve__tests__resolve_submission@submission_nested_sequence_2.yaml.snap │ │ ├── seele_composer__resolve__tests__resolve_submission@submission_nested_sequence_3.yaml.snap │ │ └── seele_composer__resolve__tests__resolve_submission@submission_simple.yaml.snap │ │ ├── submission_complex_1.yaml │ │ ├── submission_needs_1.yaml │ │ ├── submission_needs_2.yaml │ │ ├── submission_needs_complex.yaml │ │ ├── submission_nested_sequence_1.yaml │ │ ├── submission_nested_sequence_2.yaml │ │ ├── submission_nested_sequence_3.yaml │ │ └── submission_simple.yaml ├── seele-config │ ├── Cargo.toml │ └── src │ │ ├── action.rs │ │ ├── composer.rs │ │ ├── env.rs │ │ ├── exchange.rs │ │ ├── healthz.rs │ │ ├── http.rs │ │ ├── image.rs │ │ ├── lib.rs │ │ ├── path.rs │ │ ├── telemetry.rs │ │ └── worker.rs ├── seele-exchange │ ├── Cargo.toml │ └── src │ │ ├── amqp.rs │ │ ├── http.rs │ │ └── lib.rs ├── seele-shared │ ├── Cargo.toml │ └── src │ │ ├── cond.rs │ │ ├── entities │ │ ├── action │ │ │ ├── add_file.rs │ │ │ ├── mod.rs │ │ │ ├── noop.rs │ │ │ └── run_container │ │ │ │ ├── mod.rs │ │ │ │ ├── run_judge │ │ │ │ ├── compile.rs │ │ │ │ ├── mod.rs │ │ │ │ └── run.rs │ │ │ │ └── runj.rs │ │ ├── mod.rs │ │ ├── report.rs │ │ └── submission.rs │ │ ├── file.rs │ │ ├── http.rs │ │ ├── lib.rs │ │ ├── metrics.rs │ │ └── runner.rs ├── seele-worker │ ├── Cargo.toml │ └── src │ │ ├── action │ │ ├── add_file.rs │ │ ├── mod.rs │ │ ├── noop.rs │ │ └── run_container │ │ │ ├── cache.rs │ │ │ ├── idmap.rs │ │ │ ├── image.rs │ │ │ ├── mod.rs │ │ │ ├── run_judge │ │ │ ├── compile.rs │ │ │ ├── mod.rs │ │ │ └── run.rs │ │ │ └── utils.rs │ │ └── lib.rs └── seele │ ├── Cargo.toml │ └── src │ ├── cgroup.rs │ ├── healthz.rs │ ├── main.rs │ └── telemetry.rs ├── docs ├── middleware.js ├── next.config.mjs ├── package.json ├── pages │ ├── _app.mdx │ ├── _meta.en.json │ ├── _meta.zh.json │ ├── advanced │ │ ├── _meta.en.json │ │ ├── _meta.zh.json │ │ ├── architecture.en.mdx │ │ ├── architecture.zh.mdx │ │ ├── fairness.en.mdx │ │ ├── fairness.zh.mdx │ │ ├── images.en.mdx │ │ ├── images.zh.mdx │ │ ├── sandbox.en.mdx │ │ ├── sandbox.zh.mdx │ │ ├── telemetry.en.mdx │ │ └── telemetry.zh.mdx │ ├── configurations │ │ ├── _meta.en.json │ │ ├── _meta.zh.json │ │ ├── composer.en.mdx │ │ ├── composer.zh.mdx │ │ ├── exchange.en.mdx │ │ ├── exchange.zh.mdx │ │ ├── file.en.mdx │ │ ├── file.zh.mdx │ │ ├── worker.en.mdx │ │ └── worker.zh.mdx │ ├── getting-started.en.mdx │ ├── getting-started.zh.mdx │ ├── index.en.mdx │ ├── index.zh.mdx │ ├── misc │ │ ├── _meta.en.json │ │ ├── _meta.zh.json │ │ ├── development.en.mdx │ │ ├── development.zh.mdx │ │ ├── naming.en.mdx │ │ ├── naming.zh.mdx │ │ ├── roadmap.en.mdx │ │ └── roadmap.zh.mdx │ └── tasks │ │ ├── _meta.en.json │ │ ├── _meta.zh.json │ │ ├── description.en.mdx │ │ ├── description.zh.mdx │ │ ├── directory.en.mdx │ │ ├── directory.zh.mdx │ │ ├── embed-and-upload.en.mdx │ │ ├── embed-and-upload.zh.mdx │ │ ├── files.en.mdx │ │ ├── files.zh.mdx │ │ ├── judge.en.mdx │ │ ├── judge.zh.mdx │ │ ├── order.en.mdx │ │ ├── order.zh.mdx │ │ ├── report.en.mdx │ │ ├── report.zh.mdx │ │ ├── script.en.mdx │ │ ├── script.zh.mdx │ │ ├── states.en.mdx │ │ ├── states.zh.mdx │ │ ├── tags.en.mdx │ │ ├── tags.zh.mdx │ │ ├── types.en.mdx │ │ └── types.zh.mdx ├── pnpm-lock.yaml ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── architecture.jpg │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── grafana.png │ ├── logo.svg │ ├── order-1.png │ ├── order-2.png │ ├── report.png │ ├── site.webmanifest │ ├── states.png │ └── tempo.png ├── theme.config.jsx └── vercel.json ├── images └── diff-scripts │ ├── Dockerfile │ ├── diff-loose │ └── diff-strict ├── runj ├── .gitignore ├── Makefile ├── README.md ├── cmd │ └── runj │ │ ├── cgroup │ │ ├── cgroupfs.go │ │ ├── dbus.go │ │ ├── systemd.go │ │ └── utils.go │ │ ├── entities │ │ ├── config.go │ │ └── report.go │ │ ├── execute │ │ ├── execute.go │ │ ├── report.go │ │ ├── spec.go │ │ └── utils.go │ │ ├── main.go │ │ └── utils │ │ ├── overlayfs.go │ │ └── utils.go ├── go.mod ├── go.sum └── tests │ ├── go.mod │ ├── package.json │ ├── run.mjs │ └── stubs │ ├── 16g │ ├── main.c │ └── stub.mjs │ ├── bigexe.c │ ├── main.c │ └── stub.mjs │ ├── ctle │ ├── main.cpp │ └── stub.mjs │ ├── ctle2 │ ├── main.cpp │ └── stub.mjs │ ├── dead_loop_printf │ ├── main │ ├── main.go │ └── stub.mjs │ ├── divide_by_zero │ ├── main │ ├── main.c │ └── stub.mjs │ ├── fork │ ├── main │ ├── main.c │ └── stub.mjs │ ├── include_self │ ├── main.c │ └── stub.mjs │ ├── macro │ ├── main.c │ └── stub.mjs │ ├── manyfiles │ ├── main │ ├── main.c │ └── stub.mjs │ ├── memory1 │ ├── main │ ├── main.c │ └── stub.mjs │ ├── memory2 │ ├── main │ ├── main.c │ └── stub.mjs │ ├── memory3 │ ├── main │ ├── main.c │ └── stub.mjs │ ├── network │ ├── main │ ├── main.go │ └── stub.mjs │ ├── out_of_bounds │ ├── main │ ├── main.c │ └── stub.mjs │ ├── rlimit_fsize_1024 │ ├── main │ ├── main.go │ └── stub.mjs │ ├── rlimit_fsize_1025 │ ├── main │ ├── main.go │ └── stub.mjs │ ├── rm_rf │ ├── main │ └── stub.mjs │ ├── stack_overflow_1 │ ├── main │ ├── main.c │ └── stub.mjs │ ├── stack_overflow_2 │ ├── main │ ├── main.c │ └── stub.mjs │ ├── thread │ ├── main │ ├── main.cpp │ └── stub.mjs │ ├── tle_system │ ├── main │ ├── main.c │ └── stub.mjs │ ├── tle_system_child │ ├── main │ ├── main.c │ └── stub.mjs │ ├── tle_user │ ├── main │ ├── main.c │ └── stub.mjs │ └── tle_user_child │ ├── main │ ├── main.c │ └── stub.mjs └── rustfmt.toml /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile 3 | .github/ 4 | .vscode/ 5 | root/ 6 | runj/bin/ 7 | runj/tests/ 8 | target/ 9 | config.toml 10 | docs/ -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Docker Buildx 16 | uses: docker/setup-buildx-action@v3 17 | 18 | - name: Login to GitHub Container Registry 19 | uses: docker/login-action@v3 20 | with: 21 | registry: ghcr.io 22 | username: ${{ github.repository_owner }} 23 | password: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | - name: Build and push 26 | uses: docker/build-push-action@v5 27 | with: 28 | context: . 29 | platforms: linux/amd64 30 | push: true 31 | build-args: | 32 | GIT_SHA=${{ github.sha }} 33 | GIT_NAME=${{ github.ref_name }} 34 | tags: | 35 | ghcr.io/darkyzhou/seele:latest 36 | ghcr.io/darkyzhou/seele:${{ github.sha }} 37 | ghcr.io/darkyzhou/seele:${{ github.ref_name }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | config.toml 9 | 10 | /.vscode 11 | 12 | /root 13 | 14 | **/node_modules 15 | **/pnpm-lock.yaml 16 | **/package-lock.json 17 | **/.next 18 | 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ "crates/*" ] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | edition = "2021" 7 | version = "0.6.0" 8 | 9 | [profile.release] 10 | lto = "thin" 11 | opt-level = 3 12 | strip = true 13 | 14 | [workspace.dependencies] 15 | anyhow = "1.0" 16 | async-recursion = "1.1" 17 | async-trait = "0.1" 18 | axum = "0.8" 19 | base64 = "0.22" 20 | bytes = "1.9" 21 | chrono = { version = "0.4", features = [ "serde" ] } 22 | config = "0.15" 23 | dbus = "0.9" 24 | duct = "0.13" 25 | either = "1.12" 26 | ellipse = "0.2" 27 | futures-util = "0.3" 28 | http = "1.2" 29 | http-cache = { version = "0.20", default-features = false, features = [ 30 | "manager-moka" 31 | ] } 32 | http-cache-reqwest = "0.15" 33 | indexmap = { version = "2.7", features = [ "serde" ] } 34 | insta = { version = "1.42", features = [ "glob", "redactions", "ron" ] } 35 | lapin = "2.5" 36 | libcgroups = "0.5" 37 | map-macro = "0.3" 38 | moka = { version = "0.12", features = [ "future", "sync" ] } 39 | nano-id = { version = "0.4", features = [ "base62" ] } 40 | nix = { version = "0.29", features = [ "hostname", "mount", "signal" ] } 41 | num_cpus = "1" 42 | opentelemetry = "0.27" 43 | opentelemetry-otlp = "0.27" 44 | opentelemetry_sdk = { version = "0.27", features = [ "rt-tokio" ] } 45 | quick-js = { version = "0.4", features = [ "patched" ] } 46 | rand = { version = "0.9" } 47 | regex = "1" 48 | reqwest = { version = "0.12", features = [ "multipart", "stream" ] } 49 | reqwest-middleware = "0.4" 50 | ring-channel = "0.12" 51 | rkyv = "0.8" 52 | serde = { version = "1.0", features = [ "derive", "rc" ] } 53 | serde_json = { version = "1.0", features = [ "preserve_order" ] } 54 | serde_yaml = "0.9" 55 | sha2 = "0.10" 56 | shell-words = "1.1" 57 | systemd = "0.10" 58 | thread_local = "1.1" 59 | tokio = { version = "1", features = [ "full" ] } 60 | tokio-graceful-shutdown = "0.15" 61 | tokio-util = { version = "0.7", features = [ "io" ] } 62 | tracing = "0.1" 63 | tracing-opentelemetry = "0.28" 64 | tracing-subscriber = "0.3" 65 | triggered = "0.1" 66 | url = { version = "2.5", features = [ "serde" ] } 67 | uzers = "0.12" 68 | 69 | # local dependencies 70 | 71 | seele_cgroup = { path = "crates/seele-cgroup" } 72 | seele_composer = { path = "crates/seele-composer" } 73 | seele_config = { path = "crates/seele-config" } 74 | seele_exchange = { path = "crates/seele-exchange" } 75 | seele_shared = { path = "crates/seele-shared" } 76 | seele_worker = { path = "crates/seele-worker" } 77 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GIT_SHA 2 | ARG GIT_NAME 3 | 4 | FROM golang:1.23-bookworm AS runj 5 | WORKDIR /usr/src/app/ 6 | COPY runj/go.mod runj/go.sum ./ 7 | RUN go mod download && go mod verify 8 | COPY runj/ ./ 9 | RUN make build 10 | 11 | FROM rust:1.84-slim-bookworm AS builder 12 | RUN apt update -qq && \ 13 | DEBIAN_FRONTEND=noninteractive apt install -qqy --no-install-recommends pkg-config libdbus-1-dev libsystemd-dev protobuf-compiler libssl-dev patch 14 | ENV COMMIT_TAG=$GIT_NAME 15 | ENV COMMIT_SHA=$GIT_SHA 16 | WORKDIR /usr/src/seele 17 | COPY . . 18 | RUN cargo install --path crates/seele 19 | 20 | FROM bitnami/minideb:bookworm AS runtime 21 | WORKDIR /etc/seele 22 | RUN install_packages ca-certificates curl gpg gpg-agent umoci uidmap pkg-config libdbus-1-dev libsystemd-dev protobuf-compiler libssl-dev skopeo 23 | ENV TINI_VERSION=v0.19.0 24 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-amd64 /tini 25 | RUN chmod +x /tini 26 | ENTRYPOINT ["/tini", "--"] 27 | COPY --from=runj /usr/src/app/bin/runj /usr/local/bin 28 | COPY --from=builder /usr/local/cargo/bin/seele /usr/local/bin 29 | CMD ["/usr/local/bin/seele"] 30 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-bookworm AS runj 2 | WORKDIR /usr/src/app/ 3 | COPY runj/go.mod runj/go.sum ./ 4 | RUN go mod download && go mod verify 5 | COPY runj/ ./ 6 | RUN make build 7 | 8 | FROM lukemathwalker/cargo-chef:latest-rust-1.84-slim-bookworm AS chef 9 | RUN apt update -qq && \ 10 | DEBIAN_FRONTEND=noninteractive apt install -qqy --no-install-recommends pkg-config libdbus-1-dev libsystemd-dev protobuf-compiler libssl-dev patch 11 | WORKDIR /app 12 | 13 | FROM chef AS planner 14 | COPY . . 15 | RUN cargo chef prepare --recipe-path recipe.json 16 | 17 | FROM chef AS builder 18 | COPY --from=planner /app/recipe.json recipe.json 19 | RUN cargo chef cook --recipe-path recipe.json 20 | COPY . . 21 | RUN cargo build --bin seele 22 | 23 | FROM bitnami/minideb:bookworm AS runtime 24 | WORKDIR /etc/seele 25 | RUN install_packages ca-certificates curl gpg gpg-agent umoci uidmap pkg-config libdbus-1-dev libsystemd-dev protobuf-compiler libssl-dev skopeo 26 | ENV TINI_VERSION=v0.19.0 27 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-amd64 /tini 28 | RUN chmod +x /tini 29 | ENTRYPOINT ["/tini", "--"] 30 | COPY --from=runj /usr/src/app/bin/runj /usr/local/bin 31 | COPY --from=builder /app/target/debug/seele /usr/local/bin 32 | CMD ["/usr/local/bin/seele"] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 darkyzhou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/seele-cgroup/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition.workspace = true 3 | name = "seele_cgroup" 4 | version.workspace = true 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | dbus = { workspace = true } 9 | libcgroups = { workspace = true } 10 | tracing = { workspace = true } 11 | 12 | # local dependencies 13 | 14 | seele_config = { workspace = true } 15 | seele_shared = { workspace = true } 16 | -------------------------------------------------------------------------------- /crates/seele-cgroup/src/systemd.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, process, time::Duration}; 2 | 3 | use anyhow::{Context, Result, bail}; 4 | use dbus::{ 5 | arg::{RefArg, Variant}, 6 | blocking::{Connection, Proxy}, 7 | }; 8 | use libcgroups::common::DEFAULT_CGROUP_ROOT; 9 | 10 | use super::systemd_api::OrgFreedesktopSystemd1Manager; 11 | 12 | const PARENT_SLICE: &str = "user.slice"; 13 | const SEELE_SCOPE: &str = "seele.scope"; 14 | 15 | pub fn create_and_enter_cgroup() -> Result { 16 | let connection = Connection::new_session().context("Error connecting systemd session bus")?; 17 | let proxy = create_proxy(&connection); 18 | 19 | let version = systemd_version(&proxy)?; 20 | if version <= 243 { 21 | bail!("Seele requires systemd version being greater than 243"); 22 | } 23 | 24 | start_transient_unit(&proxy)?; 25 | 26 | let cgroup_path = proxy.control_group().context("Error getting systemd cgroup path")?; 27 | Ok([DEFAULT_CGROUP_ROOT, cgroup_path.trim_start_matches('/'), PARENT_SLICE, SEELE_SCOPE] 28 | .into_iter() 29 | .collect()) 30 | } 31 | 32 | fn create_proxy(connection: &Connection) -> Proxy<&Connection> { 33 | connection.with_proxy( 34 | "org.freedesktop.systemd1", 35 | "/org/freedesktop/systemd1", 36 | Duration::from_millis(5000), 37 | ) 38 | } 39 | 40 | fn start_transient_unit(proxy: &Proxy<&Connection>) -> Result<()> { 41 | let properties: Vec<(&str, Variant>)> = vec![ 42 | ( 43 | "Description", 44 | Variant(Box::new("Seele, a modern cloud-native online judge backend".to_string())), 45 | ), 46 | ("Delegate", Variant(Box::new(true))), 47 | ("Slice", Variant(Box::new(PARENT_SLICE.to_string()))), 48 | ("DefaultDependencies", Variant(Box::new(false))), 49 | ("PIDs", Variant(Box::new(vec![process::id()]))), 50 | ]; 51 | proxy 52 | .start_transient_unit(SEELE_SCOPE, "replace", properties, vec![]) 53 | .context("Error starting transient unit")?; 54 | Ok(()) 55 | } 56 | 57 | fn systemd_version(proxy: &Proxy<&Connection>) -> Result { 58 | proxy 59 | .version() 60 | .context("Error requesting systemd dbus")? 61 | .chars() 62 | .skip_while(|c| c.is_alphabetic()) 63 | .take_while(|c| c.is_numeric()) 64 | .collect::() 65 | .parse::() 66 | .context("Error parsing systemd version") 67 | } 68 | -------------------------------------------------------------------------------- /crates/seele-cgroup/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | use anyhow::{Result, bail}; 4 | use libcgroups::common::{DEFAULT_CGROUP_ROOT, read_cgroup_file}; 5 | 6 | pub fn check_and_get_self_cgroup() -> Result { 7 | let content = fs::read_to_string("/proc/thread-self/cgroup")?; 8 | let content = content.trim(); 9 | 10 | if content.is_empty() { 11 | bail!("Unexpected blank /proc/thread-self/cgroup content"); 12 | } 13 | 14 | // Refer to https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#processes 15 | if !content.starts_with("0::") { 16 | bail!("Unexpected /proc/thread-self/cgroup content: {}", content); 17 | } 18 | 19 | if content.ends_with("(deleted)") { 20 | bail!("Unexpected /proc/thread-self/cgroup content, the cgroup is deleted: {}", content); 21 | } 22 | 23 | if content == "0::/" { 24 | return Ok(DEFAULT_CGROUP_ROOT.into()); 25 | } 26 | 27 | let cgroup_path = content.trim_start_matches("0::/"); 28 | Ok([DEFAULT_CGROUP_ROOT, cgroup_path].into_iter().collect()) 29 | } 30 | 31 | pub fn get_self_cpuset_cpu() -> Result { 32 | let path = check_and_get_self_cgroup()?; 33 | let content = read_cgroup_file(path.join("cpuset.cpus"))?; 34 | tracing::debug!("cpuset.cpus path: {:?}", path.join("cpuset.cpus")); 35 | match content.trim().parse() { 36 | Ok(cpu) => Ok(cpu), 37 | Err(_err) => bail!("Error parsing cpuset.cpus content: {:?}", content), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/seele-composer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition.workspace = true 3 | name = "seele_composer" 4 | version.workspace = true 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | async-recursion = { workspace = true } 9 | chrono = { workspace = true } 10 | either = { workspace = true } 11 | ellipse = { workspace = true } 12 | futures-util = { workspace = true } 13 | opentelemetry = { workspace = true } 14 | quick-js = { workspace = true } 15 | reqwest = { workspace = true } 16 | ring-channel = { workspace = true } 17 | serde = { workspace = true } 18 | serde_json = { workspace = true } 19 | serde_yaml = { workspace = true } 20 | tokio = { workspace = true } 21 | tokio-graceful-shutdown = { workspace = true } 22 | tracing = { workspace = true } 23 | 24 | # local dependencies 25 | 26 | seele_config = { workspace = true } 27 | seele_shared = { workspace = true } 28 | seele_worker = { workspace = true } 29 | 30 | [dev-dependencies] 31 | insta = { workspace = true } 32 | map-macro = { workspace = true } 33 | rand = { workspace = true } 34 | -------------------------------------------------------------------------------- /crates/seele-composer/src/predicate.rs: -------------------------------------------------------------------------------- 1 | use crate::entities::{TaskNode, TaskStatus}; 2 | 3 | const TRUE: &str = "true"; 4 | const PREVIOUS_OK: &str = "previous.ok"; 5 | 6 | pub fn check_node_predicate(parent_node: &TaskNode, node: &TaskNode) -> bool { 7 | let predicate = match &node.config.when { 8 | Some(when) => when.as_str(), 9 | None => PREVIOUS_OK, 10 | }; 11 | 12 | match predicate { 13 | TRUE => true, 14 | PREVIOUS_OK => { 15 | matches!(*parent_node.config.status.read().unwrap(), TaskStatus::Success { .. }) 16 | } 17 | _ => false, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/seele-composer/src/reporter/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::{Context, Result}; 4 | use serde_json::Value; 5 | use tracing::instrument; 6 | 7 | use super::report::apply_embeds_config; 8 | use crate::entities::{SubmissionReportUploadConfig, SubmissionReporter}; 9 | 10 | mod javascript; 11 | mod utils; 12 | 13 | #[instrument(skip_all)] 14 | pub async fn execute_reporter( 15 | root_directory: &Path, 16 | reporter: &SubmissionReporter, 17 | data: Value, 18 | ) -> Result<(Value, Vec)> { 19 | let mut config = match reporter { 20 | SubmissionReporter::JavaScript { javascript } => { 21 | javascript::execute_javascript_reporter(data, javascript.to_string()).await? 22 | } 23 | }; 24 | 25 | let embeds = apply_embeds_config(root_directory, &config.embeds) 26 | .await 27 | .context("Error applying the embeds config")?; 28 | for (field, content) in embeds { 29 | config.report.insert(field, content.into()); 30 | } 31 | 32 | let report = serde_json::to_value(config.report) 33 | .context("Error serializing the report from the reporter")?; 34 | Ok((report, config.uploads)) 35 | } 36 | -------------------------------------------------------------------------------- /crates/seele-composer/src/reporter/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::entities::run_container::{ExecutionReport, ExecutionStatus}; 2 | 3 | pub enum OjStatus { 4 | Accepted, 5 | TimeLimitExceeded, 6 | MemoryLimitExceeded, 7 | WrongAnswer, 8 | RuntimeError, 9 | OutputLimitExceeded, 10 | InternalError, 11 | } 12 | 13 | impl From for &'static str { 14 | fn from(status: OjStatus) -> Self { 15 | match status { 16 | OjStatus::Accepted => "ACC", 17 | OjStatus::TimeLimitExceeded => "TLE", 18 | OjStatus::MemoryLimitExceeded => "MLE", 19 | OjStatus::WrongAnswer => "WA", 20 | OjStatus::RuntimeError => "RE", 21 | OjStatus::OutputLimitExceeded => "OLE", 22 | OjStatus::InternalError => "INTERNAL", 23 | } 24 | } 25 | } 26 | 27 | pub fn get_oj_status(run_report: ExecutionReport, compare_report: ExecutionReport) -> OjStatus { 28 | if matches!(run_report.status, ExecutionStatus::Normal) 29 | && matches!(compare_report.status, ExecutionStatus::RuntimeError) 30 | { 31 | return OjStatus::WrongAnswer; 32 | } 33 | 34 | if matches!( 35 | (&run_report.status, &compare_report.status), 36 | (&ExecutionStatus::Normal, &ExecutionStatus::Normal) 37 | ) { 38 | return OjStatus::Accepted; 39 | } 40 | 41 | if matches!( 42 | run_report.status, 43 | ExecutionStatus::UserTimeLimitExceeded | ExecutionStatus::WallTimeLimitExceeded 44 | ) { 45 | return OjStatus::TimeLimitExceeded; 46 | } 47 | 48 | if matches!(run_report.status, ExecutionStatus::OutputLimitExceeded) { 49 | return OjStatus::OutputLimitExceeded; 50 | } 51 | 52 | if matches!(run_report.status, ExecutionStatus::MemoryLimitExceeded) { 53 | return OjStatus::MemoryLimitExceeded; 54 | } 55 | 56 | if !matches!(run_report.status, ExecutionStatus::Normal) { 57 | return OjStatus::RuntimeError; 58 | } 59 | 60 | OjStatus::InternalError 61 | } 62 | -------------------------------------------------------------------------------- /crates/seele-composer/src/signal.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use serde_json::Value; 3 | 4 | use crate::entities::UtcTimestamp; 5 | 6 | #[derive(Debug, Serialize)] 7 | pub struct SubmissionSignal { 8 | #[serde(skip_serializing_if = "Option::is_none")] 9 | pub id: Option, 10 | 11 | #[serde(flatten)] 12 | pub ext: SubmissionSignalExt, 13 | } 14 | 15 | #[derive(Debug, Serialize)] 16 | #[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] 17 | pub enum SubmissionSignalExt { 18 | Progress(SubmissionReportSignal), 19 | Error(SubmissionErrorSignal), 20 | Completed(SubmissionReportSignal), 21 | } 22 | 23 | #[derive(Debug, Serialize)] 24 | pub struct SubmissionErrorSignal { 25 | pub error: String, 26 | } 27 | 28 | #[derive(Debug, Serialize)] 29 | pub struct SubmissionReportSignal { 30 | pub report_at: UtcTimestamp, 31 | 32 | #[serde(skip_serializing_if = "Option::is_none")] 33 | pub report: Option, 34 | 35 | #[serde(skip_serializing_if = "Option::is_none")] 36 | pub report_error: Option, 37 | 38 | pub status: Value, 39 | } 40 | 41 | impl SubmissionSignalExt { 42 | pub fn get_type(&self) -> &'static str { 43 | match self { 44 | Self::Progress { .. } => "PROGRESS", 45 | Self::Error { .. } => "ERROR", 46 | Self::Completed { .. } => "COMPLETED", 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/snapshots/seele_composer__execute__tests__execute_submission@submission_complex_1.yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/seele-composer/src/execute.rs 3 | expression: results 4 | input_file: crates/seele-composer/src/tests/submission_complex_1.yaml 5 | --- 6 | [ 7 | Config( 8 | action: "seele/noop@1", 9 | test: 1, 10 | ), 11 | Config( 12 | action: "seele/noop@1", 13 | test: 201, 14 | ), 15 | Config( 16 | action: "seele/noop@1", 17 | test: 202, 18 | ), 19 | Config( 20 | action: "seele/noop@1", 21 | test: 301, 22 | ), 23 | Config( 24 | action: "seele/noop@1", 25 | test: 302, 26 | ), 27 | Config( 28 | action: "seele/noop@1", 29 | test: 303, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/snapshots/seele_composer__execute__tests__execute_submission@submission_needs_1.yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/seele-composer/src/execute.rs 3 | expression: results 4 | input_file: crates/seele-composer/src/tests/submission_needs_1.yaml 5 | --- 6 | [ 7 | Config( 8 | action: "seele/noop@1", 9 | test: 1, 10 | ), 11 | Config( 12 | action: "seele/noop@1", 13 | test: 2, 14 | ), 15 | Config( 16 | action: "seele/noop@1", 17 | test: 3, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/snapshots/seele_composer__execute__tests__execute_submission@submission_needs_2.yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/seele-composer/src/execute.rs 3 | expression: results 4 | input_file: crates/seele-composer/src/tests/submission_needs_2.yaml 5 | --- 6 | [ 7 | Config( 8 | action: "seele/noop@1", 9 | test: 1, 10 | ), 11 | Config( 12 | action: "seele/noop@1", 13 | test: 2, 14 | ), 15 | Config( 16 | action: "seele/noop@1", 17 | test: 3, 18 | ), 19 | Config( 20 | action: "seele/noop@1", 21 | test: 4, 22 | ), 23 | Config( 24 | action: "seele/noop@1", 25 | test: 5, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/snapshots/seele_composer__execute__tests__execute_submission@submission_needs_complex.yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/seele-composer/src/execute.rs 3 | expression: results 4 | input_file: crates/seele-composer/src/tests/submission_needs_complex.yaml 5 | --- 6 | [ 7 | Config( 8 | action: "seele/noop@1", 9 | test: 1, 10 | ), 11 | Config( 12 | action: "seele/noop@1", 13 | test: 2111, 14 | ), 15 | Config( 16 | action: "seele/noop@1", 17 | test: 3, 18 | ), 19 | Config( 20 | action: "seele/noop@1", 21 | test: 2112, 22 | ), 23 | Config( 24 | action: "seele/noop@1", 25 | test: 2113, 26 | ), 27 | Config( 28 | action: "seele/noop@1", 29 | test: 2115, 30 | ), 31 | Config( 32 | action: "seele/noop@1", 33 | test: 2114, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/snapshots/seele_composer__execute__tests__execute_submission@submission_nested_sequence_1.yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/seele-composer/src/execute.rs 3 | expression: results 4 | input_file: crates/seele-composer/src/tests/submission_nested_sequence_1.yaml 5 | --- 6 | [ 7 | Config( 8 | action: "seele/noop@1", 9 | test: 1, 10 | ), 11 | Config( 12 | action: "seele/noop@1", 13 | test: 21, 14 | ), 15 | Config( 16 | action: "seele/noop@1", 17 | test: 22, 18 | ), 19 | Config( 20 | action: "seele/noop@1", 21 | test: 3, 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/snapshots/seele_composer__execute__tests__execute_submission@submission_nested_sequence_2.yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/seele-composer/src/execute.rs 3 | expression: results 4 | input_file: crates/seele-composer/src/tests/submission_nested_sequence_2.yaml 5 | --- 6 | [ 7 | Config( 8 | action: "seele/noop@1", 9 | test: 1, 10 | ), 11 | Config( 12 | action: "seele/noop@1", 13 | test: 21, 14 | ), 15 | Config( 16 | action: "seele/noop@1", 17 | test: 22, 18 | ), 19 | Config( 20 | action: "seele/noop@1", 21 | test: 3, 22 | ), 23 | Config( 24 | action: "seele/noop@1", 25 | test: 41, 26 | ), 27 | Config( 28 | action: "seele/noop@1", 29 | test: 421, 30 | ), 31 | Config( 32 | action: "seele/noop@1", 33 | test: 422, 34 | ), 35 | Config( 36 | action: "seele/noop@1", 37 | test: 423, 38 | ), 39 | Config( 40 | action: "seele/noop@1", 41 | test: 431, 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/snapshots/seele_composer__execute__tests__execute_submission@submission_nested_sequence_3.yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/seele-composer/src/execute.rs 3 | expression: results 4 | input_file: crates/seele-composer/src/tests/submission_nested_sequence_3.yaml 5 | --- 6 | [ 7 | Config( 8 | action: "seele/noop@1", 9 | test: 1, 10 | ), 11 | Config( 12 | action: "seele/noop@1", 13 | test: 21, 14 | ), 15 | Config( 16 | action: "seele/noop@1", 17 | test: 22, 18 | ), 19 | Config( 20 | action: "seele/noop@1", 21 | test: 3, 22 | ), 23 | Config( 24 | action: "seele/noop@1", 25 | test: 4111111, 26 | ), 27 | Config( 28 | action: "seele/noop@1", 29 | test: 421, 30 | ), 31 | Config( 32 | action: "seele/noop@1", 33 | test: 422, 34 | ), 35 | Config( 36 | action: "seele/noop@1", 37 | test: 423, 38 | ), 39 | Config( 40 | action: "seele/noop@1", 41 | test: 431, 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/snapshots/seele_composer__execute__tests__execute_submission@submission_simple.yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/seele-composer/src/execute.rs 3 | expression: results 4 | input_file: crates/seele-composer/src/tests/submission_simple.yaml 5 | --- 6 | [ 7 | Config( 8 | action: "seele/noop@1", 9 | test: 1, 10 | ), 11 | Config( 12 | action: "seele/noop@1", 13 | test: 2, 14 | ), 15 | Config( 16 | action: "seele/noop@1", 17 | test: 3, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/snapshots/seele_composer__resolve__tests__resolve_submission@submission_needs_1.yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/seele-composer/src/resolve.rs 3 | expression: submission 4 | input_file: crates/seele-composer/src/tests/submission_needs_1.yaml 5 | --- 6 | Submission( 7 | id: "needs_1", 8 | root_directory: "test", 9 | config: SubmissionConfig( 10 | id: "needs_1", 11 | steps: { 12 | "one": { 13 | "status": "PENDING", 14 | "embeds": {}, 15 | }, 16 | "two": { 17 | "status": "PENDING", 18 | "embeds": {}, 19 | }, 20 | "three": { 21 | "status": "PENDING", 22 | "embeds": {}, 23 | }, 24 | }, 25 | ), 26 | root_node: RootTaskNode( 27 | tasks: [ 28 | TaskNode( 29 | name: ".one", 30 | children: [ 31 | TaskNode( 32 | name: ".two", 33 | children: [], 34 | ext: Config( 35 | action: "seele/noop@1", 36 | test: 2, 37 | ), 38 | ), 39 | TaskNode( 40 | name: ".three", 41 | children: [], 42 | ext: Config( 43 | action: "seele/noop@1", 44 | test: 3, 45 | ), 46 | ), 47 | ], 48 | ext: Config( 49 | action: "seele/noop@1", 50 | test: 1, 51 | ), 52 | ), 53 | ], 54 | ), 55 | ) 56 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/snapshots/seele_composer__resolve__tests__resolve_submission@submission_needs_2.yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/seele-composer/src/resolve.rs 3 | expression: submission 4 | input_file: crates/seele-composer/src/tests/submission_needs_2.yaml 5 | --- 6 | Submission( 7 | id: "needs_2", 8 | root_directory: "test", 9 | config: SubmissionConfig( 10 | id: "needs_2", 11 | steps: { 12 | "one": { 13 | "status": "PENDING", 14 | "embeds": {}, 15 | }, 16 | "two": { 17 | "status": "PENDING", 18 | "embeds": {}, 19 | }, 20 | "three": { 21 | "status": "PENDING", 22 | "embeds": {}, 23 | }, 24 | "four": { 25 | "status": "PENDING", 26 | "embeds": {}, 27 | }, 28 | "five": { 29 | "status": "PENDING", 30 | "embeds": {}, 31 | }, 32 | }, 33 | ), 34 | root_node: RootTaskNode( 35 | tasks: [ 36 | TaskNode( 37 | name: ".one", 38 | children: [ 39 | TaskNode( 40 | name: ".two", 41 | children: [], 42 | ext: Config( 43 | action: "seele/noop@1", 44 | test: 2, 45 | ), 46 | ), 47 | TaskNode( 48 | name: ".three", 49 | children: [ 50 | TaskNode( 51 | name: ".four", 52 | children: [], 53 | ext: Config( 54 | action: "seele/noop@1", 55 | test: 4, 56 | ), 57 | ), 58 | TaskNode( 59 | name: ".five", 60 | children: [], 61 | ext: Config( 62 | action: "seele/noop@1", 63 | test: 5, 64 | ), 65 | ), 66 | ], 67 | ext: Config( 68 | action: "seele/noop@1", 69 | test: 3, 70 | ), 71 | ), 72 | ], 73 | ext: Config( 74 | action: "seele/noop@1", 75 | test: 1, 76 | ), 77 | ), 78 | ], 79 | ), 80 | ) 81 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/snapshots/seele_composer__resolve__tests__resolve_submission@submission_nested_sequence_1.yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/seele-composer/src/resolve.rs 3 | expression: submission 4 | input_file: crates/seele-composer/src/tests/submission_nested_sequence_1.yaml 5 | --- 6 | Submission( 7 | id: "nested_sequence_1", 8 | root_directory: "test", 9 | config: SubmissionConfig( 10 | id: "nested_sequence_1", 11 | steps: { 12 | "one": { 13 | "status": "PENDING", 14 | "embeds": {}, 15 | }, 16 | "two": { 17 | "status": "PENDING", 18 | "embeds": {}, 19 | "steps": { 20 | "two-one": { 21 | "status": "PENDING", 22 | "embeds": {}, 23 | }, 24 | "two-two": { 25 | "status": "PENDING", 26 | "embeds": {}, 27 | }, 28 | }, 29 | }, 30 | "three": { 31 | "status": "PENDING", 32 | "embeds": {}, 33 | }, 34 | }, 35 | ), 36 | root_node: RootTaskNode( 37 | tasks: [ 38 | TaskNode( 39 | name: ".one", 40 | children: [ 41 | TaskNode( 42 | name: ".two", 43 | children: [ 44 | TaskNode( 45 | name: ".three", 46 | children: [], 47 | ext: Config( 48 | action: "seele/noop@1", 49 | test: 3, 50 | ), 51 | ), 52 | ], 53 | ext: [ 54 | TaskNode( 55 | name: ".two.two-one", 56 | children: [ 57 | TaskNode( 58 | name: ".two.two-two", 59 | children: [], 60 | ext: Config( 61 | action: "seele/noop@1", 62 | test: 22, 63 | ), 64 | ), 65 | ], 66 | ext: Config( 67 | action: "seele/noop@1", 68 | test: 21, 69 | ), 70 | ), 71 | ], 72 | ), 73 | ], 74 | ext: Config( 75 | action: "seele/noop@1", 76 | test: 1, 77 | ), 78 | ), 79 | ], 80 | ), 81 | ) 82 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/snapshots/seele_composer__resolve__tests__resolve_submission@submission_simple.yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/seele-composer/src/resolve.rs 3 | expression: submission 4 | input_file: crates/seele-composer/src/tests/submission_simple.yaml 5 | --- 6 | Submission( 7 | id: "simple", 8 | root_directory: "test", 9 | config: SubmissionConfig( 10 | id: "simple", 11 | steps: { 12 | "one": { 13 | "status": "PENDING", 14 | "embeds": {}, 15 | }, 16 | "two": { 17 | "status": "PENDING", 18 | "embeds": {}, 19 | }, 20 | "three": { 21 | "status": "PENDING", 22 | "embeds": {}, 23 | }, 24 | }, 25 | ), 26 | root_node: RootTaskNode( 27 | tasks: [ 28 | TaskNode( 29 | name: ".one", 30 | children: [ 31 | TaskNode( 32 | name: ".two", 33 | children: [ 34 | TaskNode( 35 | name: ".three", 36 | children: [], 37 | ext: Config( 38 | action: "seele/noop@1", 39 | test: 3, 40 | ), 41 | ), 42 | ], 43 | ext: Config( 44 | action: "seele/noop@1", 45 | test: 2, 46 | ), 47 | ), 48 | ], 49 | ext: Config( 50 | action: "seele/noop@1", 51 | test: 1, 52 | ), 53 | ), 54 | ], 55 | ), 56 | ) 57 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/submission_complex_1.yaml: -------------------------------------------------------------------------------- 1 | id: complex_1 2 | steps: 3 | prepare_sources: 4 | action: seele/noop@1 5 | test: 1 6 | 7 | compile: 8 | parallel: 9 | - action: seele/noop@1 10 | test: 201 11 | - action: seele/noop@1 12 | test: 202 13 | 14 | standard: 15 | parallel: 16 | - steps: 17 | prepare: 18 | action: seele/noop@1 19 | test: 301 20 | run: 21 | action: seele/noop@1 22 | test: 302 23 | compare: 24 | action: seele/noop@1 25 | test: 303 26 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/submission_needs_1.yaml: -------------------------------------------------------------------------------- 1 | id: needs_1 2 | steps: 3 | one: 4 | action: seele/noop@1 5 | test: 1 6 | two: 7 | needs: one 8 | action: seele/noop@1 9 | test: 2 10 | three: 11 | needs: one 12 | action: seele/noop@1 13 | test: 3 14 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/submission_needs_2.yaml: -------------------------------------------------------------------------------- 1 | id: needs_2 2 | steps: 3 | one: 4 | action: seele/noop@1 5 | test: 1 6 | two: 7 | needs: one 8 | action: seele/noop@1 9 | test: 2 10 | three: 11 | action: seele/noop@1 12 | test: 3 13 | four: 14 | action: seele/noop@1 15 | test: 4 16 | five: 17 | needs: three 18 | action: seele/noop@1 19 | test: 5 20 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/submission_needs_complex.yaml: -------------------------------------------------------------------------------- 1 | id: needs_complex 2 | steps: 3 | one: 4 | action: seele/noop@1 5 | test: 1 6 | two: 7 | steps: 8 | two-one: 9 | parallel: 10 | - steps: 11 | two-one-one: 12 | action: seele/noop@1 13 | test: 2111 14 | two-one-two: 15 | action: seele/noop@1 16 | test: 2112 17 | two-one-three: 18 | needs: two-one-one 19 | action: seele/noop@1 20 | test: 2113 21 | two-one-four: 22 | needs: two-one-three 23 | action: seele/noop@1 24 | test: 2114 25 | two-one-five: 26 | action: seele/noop@1 27 | test: 2115 28 | three: 29 | needs: one 30 | action: seele/noop@1 31 | test: 3 32 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/submission_nested_sequence_1.yaml: -------------------------------------------------------------------------------- 1 | id: nested_sequence_1 2 | steps: 3 | one: 4 | action: seele/noop@1 5 | test: 1 6 | two: 7 | steps: 8 | two-one: 9 | action: seele/noop@1 10 | test: 21 11 | two-two: 12 | action: seele/noop@1 13 | test: 22 14 | three: 15 | action: seele/noop@1 16 | test: 3 -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/submission_nested_sequence_2.yaml: -------------------------------------------------------------------------------- 1 | id: nested_sequence_2 2 | steps: 3 | one: 4 | action: seele/noop@1 5 | test: 1 6 | two: 7 | steps: 8 | two-one: 9 | action: seele/noop@1 10 | test: 21 11 | two-two: 12 | action: seele/noop@1 13 | test: 22 14 | three: 15 | action: seele/noop@1 16 | test: 3 17 | four: 18 | steps: 19 | four-one: 20 | action: seele/noop@1 21 | test: 41 22 | four-two: 23 | parallel: 24 | - action: seele/noop@1 25 | test: 421 26 | - action: seele/noop@1 27 | test: 422 28 | - action: seele/noop@1 29 | test: 423 30 | four-three: 31 | steps: 32 | four-three-one: 33 | action: seele/noop@1 34 | test: 431 -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/submission_nested_sequence_3.yaml: -------------------------------------------------------------------------------- 1 | id: nested_sequence_3 2 | steps: 3 | one: 4 | action: seele/noop@1 5 | test: 1 6 | two: 7 | steps: 8 | two-one: 9 | action: seele/noop@1 10 | test: 21 11 | two-two: 12 | action: seele/noop@1 13 | test: 22 14 | three: 15 | action: seele/noop@1 16 | test: 3 17 | four: 18 | steps: 19 | four-one: 20 | steps: 21 | four-one-one: 22 | steps: 23 | four-one-one-one: 24 | steps: 25 | four-one-one-one-one: 26 | steps: 27 | four-one-one-one-one-one: 28 | action: seele/noop@1 29 | test: 4111111 30 | four-two: 31 | parallel: 32 | - action: seele/noop@1 33 | test: 421 34 | - parallel: 35 | - parallel: 36 | - parallel: 37 | - parallel: 38 | - action: seele/noop@1 39 | test: 422 40 | - action: seele/noop@1 41 | test: 423 42 | four-three: 43 | steps: 44 | four-three-one: 45 | action: seele/noop@1 46 | test: 431 47 | -------------------------------------------------------------------------------- /crates/seele-composer/src/tests/submission_simple.yaml: -------------------------------------------------------------------------------- 1 | id: simple 2 | steps: 3 | one: 4 | action: seele/noop@1 5 | test: 1 6 | two: 7 | action: seele/noop@1 8 | test: 2 9 | three: 10 | action: seele/noop@1 11 | test: 3 -------------------------------------------------------------------------------- /crates/seele-config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition.workspace = true 3 | name = "seele_config" 4 | version.workspace = true 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | config = { workspace = true } 9 | http-cache = { workspace = true } 10 | indexmap = { workspace = true } 11 | lapin = { workspace = true } 12 | nano-id = { workspace = true } 13 | nix = { workspace = true } 14 | num_cpus = { workspace = true } 15 | serde = { workspace = true } 16 | tokio = { workspace = true } 17 | tracing = { workspace = true } 18 | tracing-subscriber = { workspace = true } 19 | url = { workspace = true } 20 | uzers = { workspace = true } 21 | -------------------------------------------------------------------------------- /crates/seele-config/src/composer.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize, Default)] 4 | pub struct ComposerConfig; 5 | -------------------------------------------------------------------------------- /crates/seele-config/src/env.rs: -------------------------------------------------------------------------------- 1 | use std::{env, sync::LazyLock}; 2 | 3 | pub static HOSTNAME: LazyLock = 4 | LazyLock::new(|| env::var("HOSTNAME").unwrap_or(get_hostname())); 5 | pub static CONTAINER_NAME: LazyLock> = 6 | LazyLock::new(|| env::var("CONTAINER_NAME").ok()); 7 | pub static CONTAINER_IMAGE_NAME: LazyLock> = 8 | LazyLock::new(|| env::var("CONTAINER_IMAGE_NAME").ok()); 9 | 10 | pub static COMMIT_TAG: LazyLock> = LazyLock::new(|| option_env!("COMMIT_TAG")); 11 | pub static COMMIT_SHA: LazyLock> = LazyLock::new(|| option_env!("COMMIT_SHA")); 12 | 13 | #[inline] 14 | fn get_hostname() -> String { 15 | nix::unistd::gethostname() 16 | .expect("Failed to get hostname") 17 | .into_string() 18 | .expect("Error converting hostname from OsString") 19 | } 20 | -------------------------------------------------------------------------------- /crates/seele-config/src/exchange.rs: -------------------------------------------------------------------------------- 1 | use std::net::{IpAddr, Ipv4Addr}; 2 | 3 | use serde::Deserialize; 4 | use url::Url; 5 | 6 | #[derive(Debug, Deserialize)] 7 | #[serde(tag = "type")] 8 | #[allow(clippy::large_enum_variant)] 9 | pub enum ExchangeConfig { 10 | #[serde(rename = "http")] 11 | Http(HttpExchangeConfig), 12 | 13 | #[serde(rename = "amqp")] 14 | Amqp(AmqpExchangeConfig), 15 | } 16 | 17 | #[derive(Debug, Deserialize)] 18 | pub struct HttpExchangeConfig { 19 | #[serde(default = "default_http_address")] 20 | pub address: IpAddr, 21 | 22 | pub port: u16, 23 | 24 | #[serde(default = "default_max_body_size")] 25 | pub max_body_size_bytes: u64, 26 | } 27 | 28 | #[derive(Debug, Deserialize)] 29 | pub struct AmqpExchangeConfig { 30 | pub url: Url, 31 | pub submission: AmqpExchangeSubmissionConfig, 32 | pub report: AmqpExchangeReportConfig, 33 | } 34 | 35 | #[derive(Debug, Deserialize)] 36 | pub struct AmqpExchangeSubmissionConfig { 37 | pub exchange: LapinExchangeConfig, 38 | pub routing_key: String, 39 | pub queue: String, 40 | 41 | #[serde(default)] 42 | pub queue_options: lapin::options::QueueDeclareOptions, 43 | } 44 | 45 | #[derive(Debug, Clone, Deserialize)] 46 | pub struct AmqpExchangeReportConfig { 47 | pub exchange: LapinExchangeConfig, 48 | pub report_routing_key: String, 49 | 50 | #[serde(default)] 51 | pub progress_routing_key: String, 52 | } 53 | 54 | #[derive(Debug, Clone, Deserialize)] 55 | pub struct LapinExchangeConfig { 56 | pub name: String, 57 | 58 | #[serde(default)] 59 | pub kind: lapin::ExchangeKind, 60 | 61 | #[serde(default)] 62 | pub options: lapin::options::ExchangeDeclareOptions, 63 | } 64 | 65 | #[inline] 66 | const fn default_http_address() -> IpAddr { 67 | IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)) 68 | } 69 | 70 | #[inline] 71 | const fn default_max_body_size() -> u64 { 72 | 8 * 1024 * 1024 73 | } 74 | -------------------------------------------------------------------------------- /crates/seele-config/src/healthz.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | pub struct HealthzConfig { 5 | #[serde(default = "default_enabled")] 6 | pub enabled: bool, 7 | 8 | #[serde(default = "default_port")] 9 | pub port: u16, 10 | } 11 | 12 | impl Default for HealthzConfig { 13 | fn default() -> Self { 14 | Self { enabled: default_enabled(), port: default_port() } 15 | } 16 | } 17 | 18 | #[inline] 19 | const fn default_enabled() -> bool { 20 | true 21 | } 22 | 23 | #[inline] 24 | const fn default_port() -> u16 { 25 | 50000 26 | } 27 | -------------------------------------------------------------------------------- /crates/seele-config/src/http.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::env; 4 | 5 | #[derive(Debug, Deserialize)] 6 | pub struct HttpConfig { 7 | #[serde(default = "default_user_agent")] 8 | pub user_agent: String, 9 | 10 | #[serde(default = "default_connect_timeout_seconds")] 11 | pub connect_timeout_seconds: u64, 12 | 13 | #[serde(default = "default_timeout_seconds")] 14 | pub timeout_seconds: u64, 15 | 16 | #[serde(default = "default_pool_idle_timeout_seconds")] 17 | pub pool_idle_timeout_seconds: u64, 18 | 19 | #[serde(default = "default_pool_max_idle_per_host")] 20 | pub pool_max_idle_per_host: usize, 21 | } 22 | 23 | impl Default for HttpConfig { 24 | #[inline] 25 | fn default() -> Self { 26 | Self { 27 | user_agent: default_user_agent(), 28 | connect_timeout_seconds: default_connect_timeout_seconds(), 29 | timeout_seconds: default_timeout_seconds(), 30 | pool_idle_timeout_seconds: default_pool_idle_timeout_seconds(), 31 | pool_max_idle_per_host: default_pool_max_idle_per_host(), 32 | } 33 | } 34 | } 35 | 36 | #[inline] 37 | fn default_user_agent() -> String { 38 | format!("seele/{}", env::COMMIT_TAG.or(*env::COMMIT_SHA).unwrap_or("unknown")) 39 | } 40 | 41 | #[inline] 42 | const fn default_connect_timeout_seconds() -> u64 { 43 | 8 44 | } 45 | 46 | #[inline] 47 | const fn default_timeout_seconds() -> u64 { 48 | 60 49 | } 50 | 51 | #[inline] 52 | const fn default_pool_idle_timeout_seconds() -> u64 { 53 | 600 54 | } 55 | 56 | #[inline] 57 | const fn default_pool_max_idle_per_host() -> usize { 58 | 8 59 | } 60 | -------------------------------------------------------------------------------- /crates/seele-config/src/image.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(PartialEq, Eq, Hash, Clone, Debug)] 6 | pub struct OciImage { 7 | pub registry: String, 8 | pub name: String, 9 | pub tag: String, 10 | } 11 | 12 | impl Display for OciImage { 13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 14 | write!(f, "{}/{}:{}", self.registry, self.name, self.tag) 15 | } 16 | } 17 | 18 | impl From<&str> for OciImage { 19 | fn from(value: &str) -> Self { 20 | const DEFAULT_TAG: &str = "latest"; 21 | const DEFAULT_REGISTRY: &str = "docker.io"; 22 | 23 | // FIXME: Check validity according to the specification 24 | let (rest, tag) = match value.rfind(':').map(|i| value.split_at(i)) { 25 | None => (value, DEFAULT_TAG), 26 | Some((rest, tag)) => (rest, tag.trim_start_matches(':')), 27 | }; 28 | 29 | let (registry, name) = match rest.split_once('/') { 30 | None => (DEFAULT_REGISTRY, rest), 31 | Some((registry, name)) => { 32 | if registry != "localhost" && !registry.contains('.') { 33 | (DEFAULT_REGISTRY, rest) 34 | } else { 35 | (registry, name) 36 | } 37 | } 38 | }; 39 | 40 | Self { registry: registry.to_string(), name: name.to_string(), tag: tag.to_string() } 41 | } 42 | } 43 | 44 | impl Serialize for OciImage { 45 | fn serialize(&self, serializer: S) -> Result 46 | where 47 | S: serde::Serializer, 48 | { 49 | let str = format!("{}/{}:{}", self.registry, self.name, self.tag); 50 | serializer.serialize_str(&str) 51 | } 52 | } 53 | 54 | impl<'de> Deserialize<'de> for OciImage { 55 | fn deserialize(deserializer: D) -> Result 56 | where 57 | D: serde::Deserializer<'de>, 58 | { 59 | let str = String::deserialize(deserializer)?; 60 | Ok(str.as_str().into()) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::OciImage; 67 | 68 | macro_rules! image { 69 | ($registry:expr, $name:expr, $tag:expr) => { 70 | OciImage { 71 | registry: $registry.to_string(), 72 | name: $name.to_string(), 73 | tag: $tag.to_string(), 74 | } 75 | }; 76 | } 77 | 78 | #[test] 79 | fn test_from_str() { 80 | let cases = vec![ 81 | ( 82 | "docker.io/rancher/system-upgrade-controller:v0.8.0", 83 | image!("docker.io", "rancher/system-upgrade-controller", "v0.8.0"), 84 | ), 85 | ("busybox:1.34.1-glibc", image!("docker.io", "busybox", "1.34.1-glibc")), 86 | ( 87 | "rancher/system-upgrade-controller:v0.8.0", 88 | image!("docker.io", "rancher/system-upgrade-controller", "v0.8.0"), 89 | ), 90 | ("127.0.0.1:5000/helloworld:latest", image!("127.0.0.1:5000", "helloworld", "latest")), 91 | ("quay.io/go/go/gadget:arms", image!("quay.io", "go/go/gadget", "arms")), 92 | ("busybox", image!("docker.io", "busybox", "latest")), 93 | ("docker.io/alpine", image!("docker.io", "alpine", "latest")), 94 | ("library/alpine", image!("docker.io", "library/alpine", "latest")), 95 | ]; 96 | 97 | for (str, image) in cases { 98 | assert_eq!(OciImage::from(str), image, "case {str}"); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /crates/seele-config/src/path.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, sync::LazyLock}; 2 | 3 | use anyhow::{Context, Result}; 4 | use tokio::fs; 5 | 6 | use super::CONFIG; 7 | 8 | #[derive(Debug)] 9 | pub struct SeelePaths { 10 | pub root: PathBuf, 11 | pub images: PathBuf, 12 | pub temp: PathBuf, 13 | pub submissions: PathBuf, 14 | } 15 | 16 | impl SeelePaths { 17 | pub async fn new_temp_directory(&self) -> Result { 18 | let path = self.temp.join(nano_id::base62::<16>()); 19 | fs::create_dir(&path) 20 | .await 21 | .with_context(|| format!("Error creating temp directory {}", path.display()))?; 22 | Ok(path) 23 | } 24 | } 25 | 26 | pub static PATHS: LazyLock = LazyLock::new(|| SeelePaths { 27 | root: CONFIG.paths.root.clone(), 28 | images: CONFIG.paths.root.join("images"), 29 | temp: CONFIG.paths.root.join("temp"), 30 | submissions: CONFIG.paths.tmp.join("seele").join("submissions"), 31 | }); 32 | -------------------------------------------------------------------------------- /crates/seele-config/src/telemetry.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | pub struct TelemetryConfig { 5 | #[serde(default = "default_histogram_boundaries")] 6 | pub histogram_boundaries: Vec, 7 | 8 | pub collector_url: String, 9 | } 10 | 11 | #[inline] 12 | fn default_histogram_boundaries() -> Vec { 13 | vec![0.05, 0.3, 1.8, 4.0, 8.0, 12.0, 16.0, 30.0, 60.0] 14 | } 15 | -------------------------------------------------------------------------------- /crates/seele-config/src/worker.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use super::ActionConfig; 4 | 5 | #[derive(Debug, Default, Deserialize)] 6 | pub struct WorkerConfig { 7 | #[serde(default)] 8 | pub action: ActionConfig, 9 | } 10 | -------------------------------------------------------------------------------- /crates/seele-exchange/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition.workspace = true 3 | name = "seele_exchange" 4 | version.workspace = true 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | axum = { workspace = true } 9 | futures-util = { workspace = true } 10 | lapin = { workspace = true } 11 | nano-id = { workspace = true } 12 | ring-channel = { workspace = true } 13 | serde_json = { workspace = true } 14 | tokio = { workspace = true } 15 | tokio-graceful-shutdown = { workspace = true } 16 | tracing = { workspace = true } 17 | triggered = { workspace = true } 18 | 19 | # local dependencies 20 | 21 | seele_composer = { workspace = true } 22 | seele_config = { workspace = true } 23 | seele_shared = { workspace = true } 24 | -------------------------------------------------------------------------------- /crates/seele-exchange/src/http.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, net::SocketAddr, num::NonZeroUsize, time::Duration}; 2 | 3 | use anyhow::{Result, bail}; 4 | use axum::{ 5 | Router, 6 | body::{Body, HttpBody, to_bytes}, 7 | extract::Request, 8 | http::StatusCode, 9 | response::{IntoResponse, Response}, 10 | routing::any, 11 | }; 12 | use futures_util::StreamExt; 13 | use ring_channel::ring_channel; 14 | use tokio::{net::TcpListener, time::sleep}; 15 | use tokio_graceful_shutdown::SubsystemHandle; 16 | use tracing::{error, info}; 17 | 18 | use crate::{ 19 | composer::{ComposerQueueItem, ComposerQueueTx, SubmissionSignal, SubmissionSignalExt}, 20 | conf::HttpExchangeConfig, 21 | }; 22 | 23 | pub async fn run( 24 | name: &str, 25 | handle: SubsystemHandle, 26 | tx: ComposerQueueTx, 27 | config: &HttpExchangeConfig, 28 | ) -> Result<()> { 29 | let app = Router::new().route( 30 | "/", 31 | any({ 32 | let tx = tx.clone(); 33 | let max_body_size_bytes = config.max_body_size_bytes; 34 | move |request: Request| handle_submission_request(request, tx, max_body_size_bytes) 35 | }), 36 | ); 37 | 38 | let addr = SocketAddr::from((config.address, config.port)); 39 | let listener = TcpListener::bind(addr).await?; 40 | 41 | info!("Starting http exchange {} on {}:{}", name, config.address, config.port); 42 | 43 | axum::serve(listener, app) 44 | .with_graceful_shutdown(async move { 45 | handle.on_shutdown_requested().await; 46 | 47 | info!("Http exchange is shutting down, waiting for unfinished submissions"); 48 | sleep(Duration::from_secs(5)).await; 49 | }) 50 | .await?; 51 | 52 | Ok(()) 53 | } 54 | 55 | fn serialize(debug: bool, signal: &SubmissionSignal) -> String { 56 | let result = 57 | if debug { serde_json::to_string_pretty(signal) } else { serde_json::to_string(signal) }; 58 | match result { 59 | Err(err) => { 60 | error!("Error serializing the value: {:#}", err); 61 | "".to_string() 62 | } 63 | Ok(json) => format!("{}\n", json), 64 | } 65 | } 66 | 67 | async fn handle_submission_request( 68 | request: Request, 69 | tx: ComposerQueueTx, 70 | max_body_size_bytes: u64, 71 | ) -> impl IntoResponse { 72 | match handle_submission_request_inner(request, tx, max_body_size_bytes).await { 73 | Ok(response) => (StatusCode::OK, response), 74 | Err(err) => { 75 | error!("Error handling the submission request: {:#}", err); 76 | (StatusCode::INTERNAL_SERVER_ERROR, Response::new(Body::from(err.to_string()))) 77 | } 78 | } 79 | } 80 | 81 | async fn handle_submission_request_inner( 82 | request: Request, 83 | tx: ComposerQueueTx, 84 | max_body_size_bytes: u64, 85 | ) -> Result { 86 | { 87 | let body_size = request.body().size_hint().upper().unwrap_or(max_body_size_bytes + 1); 88 | if body_size > max_body_size_bytes { 89 | bail!("The size of the request body exceeds the limit: {}", body_size); 90 | } 91 | } 92 | 93 | let show_progress = matches!(request.uri().query(), Some(query) if query.contains("progress")); 94 | let debug = matches!(request.uri().query(), Some(query) if query.contains("debug")); 95 | let config_yaml = 96 | { String::from_utf8(to_bytes(request.into_body(), usize::MAX).await?.to_vec())? }; 97 | let (status_tx, status_rx) = ring_channel(NonZeroUsize::try_from(1).unwrap()); 98 | tx.send(ComposerQueueItem { config_yaml, status_tx }).await?; 99 | 100 | let stream = status_rx.map(move |signal| { 101 | type CallbackResult = Result; 102 | 103 | if !show_progress && matches!(signal.ext, SubmissionSignalExt::Progress { .. }) { 104 | return CallbackResult::Ok("".to_string()); 105 | } 106 | 107 | CallbackResult::Ok(serialize(debug, &signal)) 108 | }); 109 | 110 | Ok(Response::new(Body::from_stream(stream))) 111 | } 112 | -------------------------------------------------------------------------------- /crates/seele-exchange/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use seele_composer::{self as composer, ComposerQueueItem}; 3 | use seele_config::{self as conf, ExchangeConfig}; 4 | use tokio::sync::mpsc; 5 | use tokio_graceful_shutdown::{SubsystemBuilder, SubsystemHandle}; 6 | use tracing::info; 7 | 8 | pub use self::amqp::is_amqp_healthy; 9 | 10 | mod amqp; 11 | mod http; 12 | 13 | pub async fn exchange_main( 14 | handle: SubsystemHandle, 15 | composer_queue_tx: mpsc::Sender, 16 | ) -> Result<()> { 17 | info!("Initializing exchanges based on the configuration"); 18 | 19 | for (name, exchange) in &conf::CONFIG.exchange { 20 | match exchange { 21 | ExchangeConfig::Http(config) => { 22 | let tx = composer_queue_tx.clone(); 23 | handle.start(SubsystemBuilder::new(format!("{name}-http"), move |handle| { 24 | http::run(name, handle, tx, config) 25 | })); 26 | } 27 | ExchangeConfig::Amqp(config) => { 28 | let tx = composer_queue_tx.clone(); 29 | handle.start(SubsystemBuilder::new(format!("{name}-amqp"), move |handle| { 30 | amqp::run(name, handle, tx, config) 31 | })); 32 | } 33 | } 34 | } 35 | 36 | handle.on_shutdown_requested().await; 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /crates/seele-shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition.workspace = true 3 | name = "seele_shared" 4 | version.workspace = true 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | chrono = { workspace = true } 9 | either = { workspace = true } 10 | ellipse = { workspace = true } 11 | futures-util = { workspace = true } 12 | indexmap = { workspace = true } 13 | nano-id = { workspace = true } 14 | opentelemetry = { workspace = true } 15 | opentelemetry_sdk = { workspace = true } 16 | rand = { workspace = true } 17 | reqwest = { workspace = true } 18 | serde = { workspace = true } 19 | serde_yaml = { workspace = true } 20 | shell-words = { workspace = true } 21 | tokio = { workspace = true } 22 | triggered = { workspace = true } 23 | url = { workspace = true } 24 | 25 | # local dependencies 26 | 27 | seele_config = { workspace = true } 28 | -------------------------------------------------------------------------------- /crates/seele-shared/src/entities/action/add_file.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, path::PathBuf}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Clone, Deserialize, Serialize)] 6 | pub struct Config { 7 | pub files: Vec, 8 | } 9 | 10 | #[derive(Debug, Clone, Deserialize, Serialize)] 11 | pub struct FileItem { 12 | pub path: PathBuf, 13 | 14 | #[serde(flatten)] 15 | pub ext: FileItemExt, 16 | } 17 | 18 | #[derive(Debug, Clone, Deserialize, Serialize)] 19 | #[serde(untagged)] 20 | pub enum FileItemExt { 21 | Http { url: String }, 22 | PlainText { plain: String }, 23 | Base64 { base64: String }, 24 | LocalPath { local: PathBuf }, 25 | } 26 | 27 | impl Display for FileItem { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | use ellipse::Ellipse; 30 | 31 | write!(f, "{}({})", self.path.display(), match &self.ext { 32 | FileItemExt::Http { url } => url.to_string(), 33 | FileItemExt::PlainText { plain } => 34 | format!("{}...", plain.as_str().truncate_ellipse(30)), 35 | FileItemExt::Base64 { base64 } => 36 | format!("{}...", base64.as_str().truncate_ellipse(30)), 37 | FileItemExt::LocalPath { local } => format!("{}", local.display()), 38 | }) 39 | } 40 | } 41 | 42 | #[derive(Debug, Clone, Deserialize, Serialize)] 43 | pub struct FailedReport { 44 | pub files: Vec, 45 | } 46 | -------------------------------------------------------------------------------- /crates/seele-shared/src/entities/action/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | pub mod add_file; 4 | pub mod noop; 5 | pub mod run_container; 6 | 7 | #[derive(Debug, Clone, Deserialize, Serialize)] 8 | #[serde(tag = "action")] 9 | pub enum ActionTaskConfig { 10 | #[serde(rename = "seele/noop@1")] 11 | Noop(noop::Config), 12 | 13 | #[serde(rename = "seele/add-file@1")] 14 | AddFile(add_file::Config), 15 | 16 | #[serde(rename = "seele/run-container@1")] 17 | RunContainer(run_container::Config), 18 | 19 | #[serde(rename = "seele/run-judge/compile@1")] 20 | RunJudgeCompile(run_container::run_judge::compile::Config), 21 | 22 | #[serde(rename = "seele/run-judge/run@1")] 23 | RunJudgeRun(run_container::run_judge::run::Config), 24 | } 25 | 26 | #[derive(Debug, Clone)] 27 | pub enum ActionReportExt { 28 | Success(ActionSuccessReportExt), 29 | Failure(ActionFailureReportExt), 30 | } 31 | 32 | #[derive(Debug, Clone, Deserialize, Serialize)] 33 | #[serde(tag = "type", rename_all = "snake_case")] 34 | pub enum ActionSuccessReportExt { 35 | Noop(noop::ExecutionReport), 36 | AddFile, 37 | RunCompile(run_container::run_judge::compile::ExecutionReport), 38 | RunContainer(run_container::ExecutionReport), 39 | } 40 | 41 | #[derive(Debug, Clone, Deserialize, Serialize)] 42 | #[serde(tag = "type", rename_all = "snake_case")] 43 | pub enum ActionFailureReportExt { 44 | Noop(noop::ExecutionReport), 45 | AddFile(add_file::FailedReport), 46 | RunContainer(run_container::ExecutionReport), 47 | } 48 | -------------------------------------------------------------------------------- /crates/seele-shared/src/entities/action/noop.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Deserialize, Serialize)] 4 | pub struct Config { 5 | #[serde(default)] 6 | pub test: u64, 7 | } 8 | 9 | #[derive(Debug, Clone, Deserialize, Serialize)] 10 | pub struct ExecutionReport { 11 | pub test: u64, 12 | } 13 | -------------------------------------------------------------------------------- /crates/seele-shared/src/entities/action/run_container/run_judge/compile.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::MountFile; 4 | use crate::entities::run_container; 5 | 6 | #[derive(Debug, Clone, Deserialize, Serialize)] 7 | pub struct Config { 8 | #[serde(flatten)] 9 | pub run_container_config: run_container::Config, 10 | 11 | #[serde(default)] 12 | pub sources: Vec, 13 | 14 | #[serde(default)] 15 | pub saves: Vec, 16 | 17 | #[serde(default)] 18 | pub cache: CacheConfig, 19 | } 20 | 21 | #[derive(Debug, Clone, Deserialize, Serialize)] 22 | pub struct CacheConfig { 23 | pub enabled: bool, 24 | 25 | #[serde(default = "default_max_allowed_size_mib")] 26 | pub max_allowed_size_mib: u64, 27 | 28 | #[serde(default)] 29 | pub extra: Vec, 30 | } 31 | 32 | impl Default for CacheConfig { 33 | fn default() -> Self { 34 | Self { 35 | enabled: false, 36 | max_allowed_size_mib: default_max_allowed_size_mib(), 37 | extra: Default::default(), 38 | } 39 | } 40 | } 41 | 42 | #[inline] 43 | fn default_max_allowed_size_mib() -> u64 { 44 | seele_config::CONFIG.worker.action.run_container.cache_size_mib / 16 45 | } 46 | 47 | #[derive(Debug, Clone, Deserialize, Serialize)] 48 | #[serde(untagged)] 49 | pub enum ExecutionReport { 50 | CacheHit { cache_hit: bool }, 51 | CacheMiss(run_container::ExecutionReport), 52 | } 53 | -------------------------------------------------------------------------------- /crates/seele-shared/src/entities/action/run_container/run_judge/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use anyhow::bail; 4 | use serde::{Deserialize, Serialize, de}; 5 | 6 | pub mod compile; 7 | pub mod run; 8 | 9 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 10 | pub struct MountFile { 11 | pub from_path: String, 12 | pub to_path: String, 13 | pub exec: bool, 14 | } 15 | 16 | impl<'de> Deserialize<'de> for MountFile { 17 | fn deserialize(deserializer: D) -> Result 18 | where 19 | D: serde::Deserializer<'de>, 20 | { 21 | let str = String::deserialize(deserializer)?; 22 | str.as_str().try_into().map_err(|err| de::Error::custom(format!("{err:#}"))) 23 | } 24 | } 25 | 26 | impl Serialize for MountFile { 27 | fn serialize(&self, serializer: S) -> Result 28 | where 29 | S: serde::Serializer, 30 | { 31 | serializer.serialize_str(&format!("{self}")) 32 | } 33 | } 34 | 35 | impl TryFrom<&str> for MountFile { 36 | type Error = anyhow::Error; 37 | 38 | fn try_from(value: &str) -> Result { 39 | Ok(match value.split(':').collect::>()[..] { 40 | [from_path] => { 41 | Self { from_path: from_path.into(), to_path: from_path.into(), exec: false } 42 | } 43 | [from_path, "exec"] => { 44 | Self { from_path: from_path.into(), to_path: from_path.into(), exec: true } 45 | } 46 | [from_path, to_path] => { 47 | Self { from_path: from_path.into(), to_path: to_path.into(), exec: false } 48 | } 49 | [from_path, to_path, "exec"] => { 50 | Self { from_path: from_path.into(), to_path: to_path.into(), exec: true } 51 | } 52 | _ => bail!("Unexpected file item: {value}"), 53 | }) 54 | } 55 | } 56 | 57 | impl Display for MountFile { 58 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 59 | write!(f, "{}:{}{}", self.from_path, self.to_path, if self.exec { ":exec" } else { "" }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/seele-shared/src/entities/action/run_container/run_judge/run.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::MountFile; 4 | use crate::entities::run_container; 5 | 6 | #[derive(Debug, Clone, Deserialize, Serialize)] 7 | pub struct Config { 8 | #[serde(flatten)] 9 | pub run_container_config: run_container::Config, 10 | 11 | #[serde(default)] 12 | pub files: Vec, 13 | } 14 | -------------------------------------------------------------------------------- /crates/seele-shared/src/entities/mod.rs: -------------------------------------------------------------------------------- 1 | pub use action::*; 2 | pub use report::*; 3 | pub use submission::*; 4 | 5 | mod action; 6 | mod report; 7 | mod submission; 8 | -------------------------------------------------------------------------------- /crates/seele-shared/src/entities/report.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use indexmap::IndexMap; 4 | use serde::{Deserialize, Serialize}; 5 | use url::Url; 6 | 7 | pub type SubmissionReport = IndexMap; 8 | 9 | #[derive(Debug, Clone, Deserialize, Serialize)] 10 | #[serde(untagged)] 11 | pub enum SubmissionReporter { 12 | JavaScript { javascript: String }, 13 | } 14 | 15 | #[derive(Debug, Clone, Deserialize, Serialize)] 16 | #[serde(deny_unknown_fields)] 17 | pub struct SubmissionReportConfig { 18 | pub report: SubmissionReport, 19 | 20 | #[serde(default)] 21 | pub embeds: Vec, 22 | 23 | #[serde(default)] 24 | pub uploads: Vec, 25 | } 26 | 27 | #[derive(Debug, Clone, Deserialize, Serialize)] 28 | #[serde(deny_unknown_fields)] 29 | pub struct SubmissionReportEmbedConfig { 30 | pub path: PathBuf, 31 | pub field: String, 32 | pub truncate_kib: usize, 33 | 34 | #[serde(default = "default_ignore_if_missing")] 35 | pub ignore_if_missing: bool, 36 | } 37 | 38 | #[derive(Debug, Clone, Deserialize, Serialize)] 39 | #[serde(deny_unknown_fields)] 40 | pub struct SubmissionReportUploadConfig { 41 | pub path: PathBuf, 42 | pub target: Url, 43 | 44 | #[serde(default = "default_upload_method")] 45 | pub method: SubmissionReportUploadMethod, 46 | 47 | #[serde(default = "default_form_field")] 48 | pub form_field: String, 49 | 50 | #[serde(default = "default_ignore_if_missing")] 51 | pub ignore_if_missing: bool, 52 | } 53 | 54 | #[derive(Debug, Clone, Deserialize, Serialize)] 55 | #[serde(rename_all = "UPPERCASE")] 56 | pub enum SubmissionReportUploadMethod { 57 | Post, 58 | Put, 59 | } 60 | 61 | #[inline] 62 | fn default_upload_method() -> SubmissionReportUploadMethod { 63 | SubmissionReportUploadMethod::Post 64 | } 65 | 66 | #[inline] 67 | fn default_ignore_if_missing() -> bool { 68 | true 69 | } 70 | 71 | #[inline] 72 | fn default_form_field() -> String { 73 | "file".to_owned() 74 | } 75 | -------------------------------------------------------------------------------- /crates/seele-shared/src/file.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::{Context, Result}; 4 | use tokio::fs; 5 | 6 | pub async fn create_parent_directories(path: &Path) -> Result<()> { 7 | if let Some(parent) = path.parent() { 8 | fs::create_dir_all(parent).await.context("Error creating the directories")?; 9 | } 10 | 11 | Ok(()) 12 | } 13 | 14 | pub async fn create_file(path: &Path) -> Result { 15 | create_parent_directories(path).await?; 16 | fs::File::create(path).await.context("Error creating the file") 17 | } 18 | -------------------------------------------------------------------------------- /crates/seele-shared/src/http.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use reqwest::Client; 4 | 5 | use crate::conf; 6 | 7 | pub fn build_http_client() -> Client { 8 | Client::builder() 9 | .user_agent(&conf::CONFIG.http.user_agent) 10 | .connect_timeout(Duration::from_secs(conf::CONFIG.http.connect_timeout_seconds)) 11 | .timeout(Duration::from_secs(conf::CONFIG.http.connect_timeout_seconds)) 12 | .pool_idle_timeout(Duration::from_secs(conf::CONFIG.http.pool_idle_timeout_seconds)) 13 | .pool_max_idle_per_host(conf::CONFIG.http.pool_max_idle_per_host) 14 | .build() 15 | .unwrap() 16 | } 17 | -------------------------------------------------------------------------------- /crates/seele-shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{env, io::SeekFrom, sync::LazyLock}; 2 | 3 | use anyhow::Result; 4 | use seele_config as conf; 5 | use tokio::{ 6 | fs::File, 7 | io::{AsyncReadExt, AsyncSeekExt, BufReader}, 8 | }; 9 | 10 | pub mod cond; 11 | pub mod entities; 12 | pub mod file; 13 | pub mod http; 14 | pub mod metrics; 15 | pub mod runner; 16 | 17 | pub static TINI_PRESENTS: LazyLock = LazyLock::new(|| env::var_os("TINI_VERSION").is_some()); 18 | 19 | pub static ABORTED_MESSAGE: &str = "Aborted due to shutting down"; 20 | 21 | pub async fn tail(file: File, count: u64) -> Result> { 22 | let metadata = file.metadata().await?; 23 | let mut reader = BufReader::new(file); 24 | if metadata.len() > count { 25 | reader.seek(SeekFrom::End((count as i64).wrapping_neg())).await?; 26 | } 27 | 28 | let mut buffer = vec![]; 29 | reader.read_to_end(&mut buffer).await?; 30 | Ok(buffer) 31 | } 32 | -------------------------------------------------------------------------------- /crates/seele-shared/src/runner.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{ 2 | LazyLock, 3 | atomic::{AtomicU64, Ordering}, 4 | }; 5 | 6 | use tokio::{ 7 | sync::Semaphore, 8 | task::{self, JoinError}, 9 | }; 10 | 11 | use crate::conf; 12 | 13 | pub static PENDING_TASKS: LazyLock = LazyLock::new(|| AtomicU64::new(0)); 14 | 15 | static RUNNERS: LazyLock = 16 | LazyLock::new(|| Semaphore::new(conf::CONFIG.thread_counts.runner)); 17 | 18 | pub async fn spawn_blocking(f: F) -> Result 19 | where 20 | F: FnOnce() -> R + Send + 'static, 21 | R: Send + 'static, 22 | { 23 | PENDING_TASKS.fetch_add(1, Ordering::SeqCst); 24 | let _permit = RUNNERS.acquire().await.unwrap(); 25 | PENDING_TASKS.fetch_sub(1, Ordering::SeqCst); 26 | 27 | task::spawn_blocking(f).await 28 | } 29 | -------------------------------------------------------------------------------- /crates/seele-worker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition.workspace = true 3 | name = "seele_worker" 4 | version.workspace = true 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | base64 = { workspace = true } 9 | bytes = { workspace = true } 10 | chrono = { workspace = true } 11 | duct = { workspace = true } 12 | futures-util = { workspace = true } 13 | http-cache = { workspace = true } 14 | http-cache-reqwest = { workspace = true } 15 | moka = { workspace = true } 16 | nano-id = { workspace = true } 17 | nix = { workspace = true } 18 | reqwest = { workspace = true } 19 | reqwest-middleware = { workspace = true } 20 | rkyv = { workspace = true } 21 | serde = { workspace = true } 22 | serde_json = { workspace = true } 23 | sha2 = { workspace = true } 24 | thread_local = { workspace = true } 25 | tokio = { workspace = true } 26 | tokio-graceful-shutdown = { workspace = true } 27 | tracing = { workspace = true } 28 | triggered = { workspace = true } 29 | 30 | # local dependencies 31 | 32 | seele_cgroup = { workspace = true } 33 | seele_config = { workspace = true } 34 | seele_shared = { workspace = true } 35 | -------------------------------------------------------------------------------- /crates/seele-worker/src/action/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | pub mod add_file; 4 | pub mod noop; 5 | pub mod run_container; 6 | 7 | #[derive(Debug)] 8 | pub struct ActionContext { 9 | pub submission_root: PathBuf, 10 | } 11 | -------------------------------------------------------------------------------- /crates/seele-worker/src/action/noop.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use seele_shared::entities::noop::*; 3 | 4 | use crate::entities::{ActionReportExt, ActionSuccessReportExt}; 5 | 6 | pub async fn execute(config: &Config) -> Result { 7 | Ok(ActionReportExt::Success(ActionSuccessReportExt::Noop(ExecutionReport { 8 | test: config.test, 9 | }))) 10 | } 11 | -------------------------------------------------------------------------------- /crates/seele-worker/src/action/run_container/cache.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::{Arc, LazyLock}, 3 | time::Duration, 4 | }; 5 | 6 | use moka::sync::Cache; 7 | 8 | use crate::conf; 9 | 10 | #[allow(clippy::type_complexity)] 11 | static CACHE: LazyLock, Arc<[u8]>>> = LazyLock::new(|| { 12 | let config = &conf::CONFIG.worker.action.run_container; 13 | Cache::builder() 14 | .name("seele-run-container") 15 | .weigher(|_, value: &Arc<[u8]>| -> u32 { value.len().try_into().unwrap_or(u32::MAX) }) 16 | .max_capacity(1024 * 1024 * config.cache_size_mib) 17 | .time_to_idle(Duration::from_secs(60 * 60 * config.cache_ttl_hour)) 18 | .build() 19 | }); 20 | 21 | pub fn init() { 22 | LazyLock::force(&CACHE); 23 | } 24 | 25 | pub fn get(key: &[u8]) -> Option> { 26 | CACHE.get(key) 27 | } 28 | 29 | pub fn write(key: Box<[u8]>, value: Arc<[u8]>) { 30 | CACHE.insert(key, value) 31 | } 32 | -------------------------------------------------------------------------------- /crates/seele-worker/src/action/run_container/idmap.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{BufRead, BufReader}, 4 | sync::LazyLock, 5 | }; 6 | 7 | use anyhow::{Context, Result, bail}; 8 | 9 | use crate::conf; 10 | 11 | const SUBUID_PATH: &str = "/etc/subuid"; 12 | const SUBGID_PATH: &str = "/etc/subgid"; 13 | 14 | pub struct SubIds { 15 | pub begin: u32, 16 | pub count: u32, 17 | } 18 | 19 | pub static SUBUIDS: LazyLock = 20 | LazyLock::new(|| get_subids(SUBUID_PATH).expect("Error getting subuids")); 21 | 22 | pub static SUBGIDS: LazyLock = 23 | LazyLock::new(|| get_subids(SUBGID_PATH).expect("Error getting subgids")); 24 | 25 | fn get_subids(path: &str) -> Result { 26 | let username = &conf::CONFIG.worker.action.run_container.userns_user; 27 | let reader = BufReader::new(File::open(path)?); 28 | get_subids_impl(username, reader.lines().map_while(Result::ok)) 29 | .with_context(|| format!("Error getting subids from {path}")) 30 | } 31 | 32 | fn get_subids_impl(username: &str, lines: impl Iterator) -> Result { 33 | for line in lines { 34 | match line.split(':').collect::>()[..] { 35 | [name, _, _] if name != username => continue, 36 | [_, begin, count] => { 37 | return Ok(SubIds { begin: begin.parse()?, count: count.parse()? }); 38 | } 39 | _ => bail!("Unexpected line: {line}"), 40 | } 41 | } 42 | 43 | bail!("Cannot find the entry for username {username}"); 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | #[test] 49 | fn test_get_subids() { 50 | let bad_cases = vec![ 51 | ("seele", ""), 52 | ("seele", "seele:"), 53 | ("seele", "bronya:114514:233"), 54 | ("seele", "bronya:114514:233\nvollerei:123123:123"), 55 | ]; 56 | 57 | let good_cases = vec![ 58 | ("seele", "seele:100000:65536", (100000, 65536)), 59 | ("seele", "yzy1:100000:65536\nseele:165536:65536", (165536, 65536)), 60 | ( 61 | "bronya", 62 | "yzy1:100000:65536\nseele:165536:65536\nbronya:1145141919:233", 63 | (1145141919, 233), 64 | ), 65 | ( 66 | "seele", 67 | "yzy1:100000:65536\nseele:165536:65536\nbronya:1145141919:233\nseele:233:123", 68 | (165536, 65536), 69 | ), 70 | ]; 71 | 72 | for (username, content) in bad_cases { 73 | let result = 74 | super::get_subids_impl(username, content.split('\n').map(|item| item.to_owned())); 75 | assert!(result.is_err()); 76 | } 77 | 78 | for (username, content, (begin, count)) in good_cases { 79 | let result = 80 | super::get_subids_impl(username, content.split('\n').map(|item| item.to_owned())); 81 | assert!(result.is_ok()); 82 | 83 | let ids = result.unwrap(); 84 | assert_eq!((ids.begin, ids.count), (begin, count)); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /crates/seele-worker/src/action/run_container/run_judge/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, path::Path, sync::LazyLock}; 2 | 3 | use anyhow::bail; 4 | use serde::{Deserialize, Serialize, de}; 5 | 6 | pub mod compile; 7 | pub mod run; 8 | 9 | static DEFAULT_MOUNT_DIRECTORY: LazyLock<&'static Path> = LazyLock::new(|| Path::new("/seele")); 10 | 11 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 12 | pub struct MountFile { 13 | pub from_path: String, 14 | pub to_path: String, 15 | pub exec: bool, 16 | } 17 | 18 | impl<'de> Deserialize<'de> for MountFile { 19 | fn deserialize(deserializer: D) -> Result 20 | where 21 | D: serde::Deserializer<'de>, 22 | { 23 | let str = String::deserialize(deserializer)?; 24 | str.as_str().try_into().map_err(|err| de::Error::custom(format!("{err:#}"))) 25 | } 26 | } 27 | 28 | impl Serialize for MountFile { 29 | fn serialize(&self, serializer: S) -> Result 30 | where 31 | S: serde::Serializer, 32 | { 33 | serializer.serialize_str(&format!("{self}")) 34 | } 35 | } 36 | 37 | impl TryFrom<&str> for MountFile { 38 | type Error = anyhow::Error; 39 | 40 | fn try_from(value: &str) -> Result { 41 | Ok(match value.split(':').collect::>()[..] { 42 | [from_path] => { 43 | Self { from_path: from_path.into(), to_path: from_path.into(), exec: false } 44 | } 45 | [from_path, "exec"] => { 46 | Self { from_path: from_path.into(), to_path: from_path.into(), exec: true } 47 | } 48 | [from_path, to_path] => { 49 | Self { from_path: from_path.into(), to_path: to_path.into(), exec: false } 50 | } 51 | [from_path, to_path, "exec"] => { 52 | Self { from_path: from_path.into(), to_path: to_path.into(), exec: true } 53 | } 54 | _ => bail!("Unexpected file item: {value}"), 55 | }) 56 | } 57 | } 58 | 59 | impl Display for MountFile { 60 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 61 | write!(f, "{}:{}{}", self.from_path, self.to_path, if self.exec { ":exec" } else { "" }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/seele-worker/src/action/run_container/run_judge/run.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::Permissions, os::unix::prelude::PermissionsExt}; 2 | 3 | use anyhow::{Context, Result, bail}; 4 | use seele_shared::entities::{ 5 | ActionReportExt, 6 | run_container::{self, run_judge::run::Config, runj}, 7 | }; 8 | use tokio::fs; 9 | use tracing::{instrument, warn}; 10 | use triggered::Listener; 11 | 12 | use super::DEFAULT_MOUNT_DIRECTORY; 13 | use crate::ActionContext; 14 | 15 | #[instrument(skip_all, name = "action_run_judge_run_execute")] 16 | pub async fn execute( 17 | handle: Listener, 18 | ctx: &ActionContext, 19 | config: &Config, 20 | ) -> Result { 21 | let mount_directory = crate::conf::PATHS.new_temp_directory().await?; 22 | // XXX: 0o777 is mandatory. The group bit is for rootless case and the others 23 | // bit is for rootful case. 24 | fs::set_permissions(&mount_directory, Permissions::from_mode(0o777)).await?; 25 | 26 | let result = async { 27 | let mut run_container_config = config.run_container_config.clone(); 28 | 29 | run_container_config.cwd = DEFAULT_MOUNT_DIRECTORY.to_owned(); 30 | 31 | run_container_config.mounts.push(run_container::MountConfig::Full(runj::MountConfig { 32 | from: mount_directory.clone(), 33 | to: DEFAULT_MOUNT_DIRECTORY.to_owned(), 34 | options: None, 35 | })); 36 | 37 | if let Some(paths) = run_container_config.paths.as_mut() { 38 | paths.push(DEFAULT_MOUNT_DIRECTORY.to_owned()); 39 | } else { 40 | run_container_config.paths = Some(vec![DEFAULT_MOUNT_DIRECTORY.to_owned()]); 41 | } 42 | 43 | for file in &config.files { 44 | let from_path = ctx.submission_root.join(&file.from_path); 45 | 46 | if let Err(err) = fs::metadata(&from_path).await { 47 | bail!("The file {file} does not exist: {err:#}"); 48 | } 49 | 50 | run_container_config.mounts.push(run_container::MountConfig::Full({ 51 | if file.exec { 52 | fs::set_permissions(&from_path, Permissions::from_mode(0o777)) 53 | .await 54 | .with_context(|| { 55 | format!("Error setting the permission of the executable {file}") 56 | })?; 57 | } 58 | 59 | let to_path = DEFAULT_MOUNT_DIRECTORY.join(&file.to_path); 60 | 61 | let options = if file.exec { Some(vec!["exec".to_owned()]) } else { None }; 62 | 63 | runj::MountConfig { from: from_path, to: to_path, options } 64 | })); 65 | } 66 | 67 | crate::run_container::execute(handle, ctx, &run_container_config).await 68 | } 69 | .await; 70 | 71 | if let Err(err) = fs::remove_dir_all(&mount_directory).await { 72 | warn!(directory = %mount_directory.display(), "Error removing mount directory: {err:#}") 73 | } 74 | 75 | result 76 | } 77 | -------------------------------------------------------------------------------- /crates/seele/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition.workspace = true 3 | name = "seele" 4 | version.workspace = true 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | axum = { workspace = true } 9 | num_cpus = { workspace = true } 10 | opentelemetry = { workspace = true } 11 | opentelemetry-otlp = { workspace = true } 12 | opentelemetry_sdk = { workspace = true } 13 | tokio = { workspace = true } 14 | tokio-graceful-shutdown = { workspace = true } 15 | tracing = { workspace = true } 16 | tracing-opentelemetry = { workspace = true } 17 | tracing-subscriber = { workspace = true } 18 | 19 | # local dependencies 20 | 21 | seele_cgroup = { workspace = true } 22 | seele_composer = { workspace = true } 23 | seele_config = { workspace = true } 24 | seele_exchange = { workspace = true } 25 | seele_shared = { workspace = true } 26 | seele_worker = { workspace = true } 27 | -------------------------------------------------------------------------------- /crates/seele/src/cgroup.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Barrier}; 2 | 3 | use anyhow::{Context, Result}; 4 | use seele_cgroup as cgroup; 5 | use tokio::task::spawn_blocking; 6 | 7 | use crate::conf; 8 | 9 | pub async fn setup_cgroup() -> Result<()> { 10 | spawn_blocking(|| -> Result<()> { 11 | cgroup::check_cgroup_setup().context("Error checking cgroup setup")?; 12 | cgroup::initialize_cgroup_subtrees().context("Error initializing cgroup subtrees") 13 | }) 14 | .await??; 15 | 16 | let count = conf::CONFIG.thread_counts.worker + conf::CONFIG.thread_counts.runner; 17 | let begin_barrier = Arc::new(Barrier::new(count)); 18 | let end_barrier = Arc::new(Barrier::new(count)); 19 | 20 | for _ in 0..(count - 1) { 21 | let begin_barrier = begin_barrier.clone(); 22 | let end_barrier = end_barrier.clone(); 23 | spawn_blocking(move || { 24 | begin_barrier.wait(); 25 | end_barrier.wait(); 26 | }); 27 | } 28 | 29 | spawn_blocking(move || { 30 | begin_barrier.wait(); 31 | let result = cgroup::bind_application_threads(); 32 | end_barrier.wait(); 33 | result 34 | }) 35 | .await? 36 | .context("Error binding application threads") 37 | } 38 | -------------------------------------------------------------------------------- /crates/seele/src/healthz.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | 3 | use anyhow::Result; 4 | use axum::{Router, http::StatusCode, response::IntoResponse, routing::any}; 5 | use tokio::net::TcpListener; 6 | use tokio_graceful_shutdown::SubsystemHandle; 7 | use tracing::info; 8 | 9 | use crate::{conf, exchange}; 10 | 11 | pub async fn healthz_main(handle: SubsystemHandle) -> Result<()> { 12 | if !conf::CONFIG.healthz.enabled { 13 | info!("Healthz is disabled"); 14 | return Ok(()); 15 | } 16 | 17 | let app = Router::new().route("/", any(healthz_handler)); 18 | 19 | let addr = SocketAddr::from(([0, 0, 0, 0], conf::CONFIG.healthz.port)); 20 | let listener = TcpListener::bind(addr).await?; 21 | 22 | info!("Running healthz endpoint at port: {}", conf::CONFIG.healthz.port); 23 | 24 | axum::serve(listener, app) 25 | .with_graceful_shutdown(async move { 26 | handle.on_shutdown_requested().await; 27 | }) 28 | .await?; 29 | 30 | Ok(()) 31 | } 32 | 33 | async fn healthz_handler() -> impl IntoResponse { 34 | if check_healthz().await { 35 | (StatusCode::OK, "ok") 36 | } else { 37 | (StatusCode::INTERNAL_SERVER_ERROR, "error") 38 | } 39 | } 40 | 41 | async fn check_healthz() -> bool { 42 | exchange::is_amqp_healthy().await 43 | } 44 | -------------------------------------------------------------------------------- /crates/seele/src/telemetry.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::{Context, Result}; 4 | use opentelemetry::trace::TracerProvider as _; 5 | use opentelemetry_otlp::{ExportConfig, MetricExporter, Protocol, SpanExporter, WithExportConfig}; 6 | use opentelemetry_sdk::{ 7 | metrics::{PeriodicReader, SdkMeterProvider, Temporality}, 8 | trace::TracerProvider, 9 | }; 10 | use tracing::*; 11 | use tracing_subscriber::{Layer, filter::LevelFilter, prelude::*}; 12 | 13 | use crate::{conf, shared}; 14 | 15 | pub async fn setup_telemetry() -> Result<()> { 16 | if conf::CONFIG.telemetry.is_none() { 17 | tracing::subscriber::set_global_default( 18 | tracing_subscriber::fmt() 19 | .compact() 20 | .with_line_number(true) 21 | .with_max_level(conf::CONFIG.log_level) 22 | .finish(), 23 | ) 24 | .context("Failed to initialize the tracing subscriber")?; 25 | } 26 | 27 | let telemetry = conf::CONFIG.telemetry.as_ref().unwrap(); 28 | 29 | info!("Initializing telemetry"); 30 | 31 | let span_exporter = SpanExporter::builder() 32 | .with_tonic() 33 | .with_export_config(ExportConfig { 34 | endpoint: Some(telemetry.collector_url.clone()), 35 | timeout: Duration::from_secs(5), 36 | protocol: Protocol::Grpc, 37 | }) 38 | .build() 39 | .context("Failed to initialize the tracer")?; 40 | 41 | let tracer_provider = TracerProvider::builder() 42 | .with_batch_exporter(span_exporter, opentelemetry_sdk::runtime::Tokio) 43 | .with_resource(shared::metrics::metrics_resource()) 44 | .build(); 45 | 46 | let tracer = tracer_provider.tracer("seele"); 47 | 48 | let metric_exporter = MetricExporter::builder() 49 | .with_temporality(Temporality::Cumulative) 50 | .with_tonic() 51 | .with_export_config(ExportConfig { 52 | endpoint: Some(telemetry.collector_url.clone()), 53 | timeout: Duration::from_secs(5), 54 | protocol: Protocol::Grpc, 55 | }) 56 | .build() 57 | .context("Failed to initialize the metrics")?; 58 | 59 | let meter_provider = SdkMeterProvider::builder() 60 | .with_reader( 61 | PeriodicReader::builder(metric_exporter, opentelemetry_sdk::runtime::Tokio) 62 | .with_interval(Duration::from_secs(3)) 63 | .build(), 64 | ) 65 | .with_resource(shared::metrics::metrics_resource()) 66 | .build(); 67 | 68 | shared::metrics::init_with_meter_provider(meter_provider); 69 | 70 | tracing::subscriber::set_global_default( 71 | tracing_subscriber::registry() 72 | .with( 73 | tracing_subscriber::fmt::layer() 74 | .compact() 75 | .with_line_number(true) 76 | .with_filter::(conf::CONFIG.log_level.into()), 77 | ) 78 | .with( 79 | tracing_opentelemetry::layer().with_tracer(tracer).with_filter(LevelFilter::INFO), 80 | ), 81 | ) 82 | .context("Failed to initialize the tracing subscriber") 83 | } 84 | -------------------------------------------------------------------------------- /docs/middleware.js: -------------------------------------------------------------------------------- 1 | export { locales as middleware } from "nextra/locales"; 2 | -------------------------------------------------------------------------------- /docs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import nextra from 'nextra' 2 | 3 | const withNextra = nextra({ 4 | theme: "nextra-theme-docs", 5 | themeConfig: "./theme.config.jsx", 6 | }) 7 | 8 | export default withNextra({ 9 | i18n: { 10 | locales: ["zh", "en"], 11 | defaultLocale: "zh", 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": {}, 6 | "dependencies": { 7 | "next": "13.5.8", 8 | "nextra": "2.13.4", 9 | "nextra-theme-docs": "2.13.4", 10 | "react": "18.3.1", 11 | "react-dom": "18.3.1" 12 | }, 13 | "devDependencies": { 14 | "@opentelemetry/api": "^1.4.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/pages/_app.mdx: -------------------------------------------------------------------------------- 1 | export default function App({ Component, pageProps }) { 2 | return 3 | } -------------------------------------------------------------------------------- /docs/pages/_meta.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "Introduction", 3 | "getting-started": "Getting Started", 4 | "tasks": "Judge Tasks", 5 | "configurations": "Configurations", 6 | "advanced": "Advanced", 7 | "misc": "Miscellaneous" 8 | } -------------------------------------------------------------------------------- /docs/pages/_meta.zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "简介", 3 | "getting-started": "开始使用", 4 | "tasks": "评测任务", 5 | "configurations": "配置项", 6 | "advanced": "高级", 7 | "misc": "其它" 8 | } -------------------------------------------------------------------------------- /docs/pages/advanced/_meta.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "architecture": "Architecture", 3 | "sandbox": "Sandbox", 4 | "fairness": "Fairness", 5 | "telemetry": "Observability", 6 | "images": "Utility Images" 7 | } -------------------------------------------------------------------------------- /docs/pages/advanced/_meta.zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "architecture": "整体架构", 3 | "sandbox": "安全沙箱", 4 | "fairness": "公平性", 5 | "telemetry": "观测性", 6 | "images": "工具镜像" 7 | } -------------------------------------------------------------------------------- /docs/pages/advanced/architecture.zh.mdx: -------------------------------------------------------------------------------- 1 | # 整体架构 2 | 3 | Seele 评测系统分为两个组成部分:评测服务和安全沙箱。评测服务作为服务进程,接收用户提交的请求并产生安全沙箱子进程来运行评测程序。 4 | 评测系统的整体架构如下图所示: 5 | 6 | ![Seele 架构图](/architecture.jpg) 7 | 8 | 其中,评测服务使用 [Rust](https://www.rust-lang.org/) 语言编写,基于流行的高性能异步编程框架 [Tokio](https://tokio.rs/)。 9 | Rust 语言通过其独特的所有权系统和内存安全特性,让人们在使用它编写高性能应用程序时能够更加安全、高效地管理内存,避免应用程序因为内存问题而崩溃。 10 | 此外,Rust 也拥有出色的并发性能和可伸缩性,使得它成为编写高吞吐量和高并发性能应用程序的理想选择。 11 | Tokio 框架为 Rust 语言带来了高性能的异步运行时实现,提供了基于事件驱动的非阻塞 I/O 模型,能够让评测服务高效地处理大量 I/O 任务。 12 | 13 | 对于安全沙箱,它使用 [Go](https://go.dev/) 语言编写,基于著名的容器运行时 [runc](https://github.com/opencontainers/runc/)。参见[安全沙箱](/advanced/sandbox)。 14 | 15 | ## Exchange 16 | 17 | Exchange 接收用户提交的评测任务,将其交给 Composer,然后将 Composer 发送的评测报告反馈给用户。为了提升灵活性,评测系统允许用户配置多个 Exchange 共同工作。 18 | 目前,评测服务提供了两种 Exchange 的实现,为用户提供了多种与评测系统进行数据交互的方式。 19 | 20 | ## Composer 21 | 22 | Composer 接收来自 Exchange 的评测任务,解析评测任务并生成一棵由步骤构成的多叉树,从沿着这棵树从根部开始向 Worker 发送步骤,追踪步骤的执行。 23 | 当无法再沿着多叉树执行更多步骤时,评测任务执行完毕,Composer 此时汇总数据产生评测报告并发送给 Exchange。 24 | 25 | ## Worker 26 | 27 | Worker 接收 Composer 发来的执行步骤,并根据其中的配置执行对应的任务,最后将执行报告发送给 Composer。目前,Worker 中提供了两种任务:添加文件和运行容器。 28 | 在运行容器任务中,Worker 通过 [skopeo](https://github.com/containers/skopeo) 从用户指定的镜像源中拉取镜像到本地,然后通过 [umoci](https://github.com/opencontainers/umoci) 将镜像解压。 29 | Worker 会通过调用安全沙箱程序来启动容器、运行评测程序并收集报告。 30 | 31 | ## 线程池 32 | 33 | Seele 将 Exchange、Composer 和 Worker 运行在主线程中,确保系统的其它 CPU 核心带来的并发能力能够更多地分配给安全沙箱的运行,提高评测服务并发执行评测任务的能力。同时,为了确保运行 Tokio 框架事件循环的主线程不被持续运行时间较长的同步任务影响导致事件响应时间变高,拖累系统的整体性能,需要将一些阻塞型 34 | I/O 任务和 CPU 密集型任务分发给线程池中的辅助线程运行。在运行每个安全沙箱时,由于需要确保评测程序能够尽可能地独占一个 CPU 核心以确保公平性,我们也需要将运行安全沙箱的任务发给线程池中的辅助线程运行。 35 | 36 | 此外,由于安全沙箱的运行时间相比其它同步任务要长的多,为了避免 Tokio 线程池被安全沙箱运行任务占满导致其他关键的同步任务无法即时执行,Seele 在 Tokio 线程池的基础上构建了一个仅限 Worker 使用的安全沙箱线程池。此线程池仅包含 Tokio 线程池中的部分线程,避免了安全沙箱运行任务占满 Tokio 线程池,而安全沙箱线程池中的线程仍然能够被其它同步任务使用,提高了线程的利用率。 37 | 38 | 在评测服务启动时,主线程和 Tokio 线程池中的线程都会被评测服务通过 cgroup 的 cpu controller 绑定到系统不同的 CPU 核心上。这在为安全沙箱的运行提供了公平性(降低了运行时间的波动性)的同时,也在一定程度上通过更好的 CPU 缓存局部性提升了各个线程的运行性能。参见[公平性](/advanced/fairness)。 39 | -------------------------------------------------------------------------------- /docs/pages/advanced/fairness.zh.mdx: -------------------------------------------------------------------------------- 1 | # 公平性 2 | 3 | Seele 允许用户指定评测程序的运行时间限制和内存占用限制,并在程序运行结束后提供运行时间以及内存占用量等数据,而用户会依据这些数据对学生的提交进行评分。因此,Seele 需要尽可能确保提供稳定的运行时间和内存占用量,确保公平性。对于同一个评测程序,如果在评测系统中多次运行得到的运行时间和内存占用量报告存在较大波动,我们认为这样的评测系统难以满足公平性的需求。 4 | 5 | ## CPU 性能 6 | 7 | Linux 作为抢占式调度的操作系统,依据优先级为每个进程分配 CPU 时间片运行。并且在多核心系统中,进程可能会被先后分配到多个 CPU 核心上运行。当某个 CPU 核心上运行的进程被切换为另一个进程时,CPU 中的缓存很可能会被另一个进程使用的内存所占用。导致原有的进程重新运行或者被调度到另一个核心时,容易出现缓存不命中的问题,最终需要花费额外的时间从内存中重新读取数据。 8 | 9 | 为了缓解缓存未命中的问题,我们需要使用 cgroup 提供的 cpu controller,将评测进程限制为只能在某个 CPU 核心上运行,从而阻止 Linux 内核的进程调度器将其调离原来的 CPU 核心。此外,也需要控制系统上其它线程能够使用的 CPU 核心,确保它们不使用评测进程正在使用的 CPU 核心。[B. Merry 的实验](https://www.semanticscholar.org/paper/Performance-Analysis-of-Sandboxes-for-Reactive-Merry/8d9051d833ad39b33d061221893139ce1f325705)表明,当评测进程被固定到同一个 CPU 核心上时,其运行时间的稳定性表现要显著优于未被固定的情况。 10 | 11 | Seele 在启动时会通过 cgroup 将进程的所有线程(包括[主线程和辅助线程](/advanced/architecture))绑定到不同的 CPU 核心上。并且,Seele 还会在每次启动安全沙箱时,配置安全沙箱运行在辅助线程所在的 CPU 核心上并让辅助线程陷入休眠态来实现 CPU 核心的让渡,确保评测进程能够独占一个 CPU 核心运行。 12 | 13 | ## 内存访问性能 14 | 15 | 许多专业服务器会在主板上会配备 2 个或以上的 CPU,并在操作系统中启用 NUMA 来让每个处理器能够访问归属于另一个处理器的内存。然而,每个处理器访问非本地内存的速度往往慢于本地内存。因此,如果评测进程的内存访问出现了上述的非本地内存访问,那么其运行时间会出现明显波动。同时,内存交换也可能造成内存访问性能的波动。当进程的内存被交换到磁盘上时,进程访问对部分内存的访问速度会严重下降,拖慢其运行时间。 16 | 17 | 为了解决跨 NUMA 内存访问的问题,Seele 使用 cgroup 提供的 cpu controller,将评测进程限制为只能在某个内存节点上运行,从而防止进程发生非本地内存访问。同时,Seele 的安全沙箱通过 cgroup 的 memory controller 阻止 Linux 内核将评测进程的内存交换到磁盘上。 18 | 19 | ## 磁盘访问性能 20 | 21 | Linux 内核为磁盘读写配备了页缓存来加速读写速度。如果用户提供的文件恰好处于页缓存中,那么当评测进程读取这些文件时,其速度会明显快于文件没有处于页缓存的情况。同理,页缓存也会对评测进程向文件输出数据的速度造成影响。在页缓存之下的层面,磁盘 IO 的调度算法和磁盘本身的行为都同样会对评测进程读写文件的速度造成影响。 22 | 23 | 为了解决上述问题,Seele 利用 Linux 内核提供的 tmpfs 文件系统来存储需要评测进程访问的文件。tmpfs 文件系统使用内存来作为存储介质,对它的读写不需要页缓存、调度算法的参与,这能够在一定程度上避免页缓存等层面对公平性带来的负面影响。 24 | 25 | ## 内存占用计算方式 26 | 27 | 在线评测系统使用的内存占用计算方式主要包括通过 `ru_maxrss` 和通过 28 | cgroup v1 提供的 `mem.max_usage_in_bytes`。本安全沙盒使用 cgroup v2 提供的 `memory.peak` 来获得这项数据。在 Linux 内核底层,cgroup v1 和 v2 的 29 | 这两项数据均取自相同的进程内存数据结构。 30 | 31 | Linux 内核的内存管理机制中存在 swap 机制,在系统内存压力变高时会将进程使用的内存页写入到磁盘中,并从内存中将其驱逐。RSS 并不会将这部分换入磁盘的内存计算在内。而 32 | cgroup 可能会通过 SwapCache 的方式仍然将其计算在内。为了确保内存访问性能的稳定,以及避免上述计算方式的偏差,我们需要阻止评测进程的 swap 机制,让 cgroup 计算的 SwapCache 始终为 0。在 cgroup v2 中,可通过将 `memory.swap.max` 设为 0 来实现这一点。 33 | 34 | 除了上述的 SwapCache,cgroup v1 的 `mem.max_usage_in_bytes` 和 cgroup v2 的 `memory.peak` 均会将进程占用的 RSS、页缓存和共享页纳入计算,并且对于共享页的计算机制是:[如果进程积极地使用共享页,那么它所使用的共享页就会被纳入计算](https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt)。一个进程使用的共享页主要包括它所使用的动态链接库,例如 35 | libc。而在 RSS 的计算中并不包括这部分动态链接库的计算。因此,cgroup 相比 `ru_maxrss` 是一种更加准确的内存占用计算方式。cgroup v1 的文档中还提到任意时刻读取 `mem.usage_in_bytes` 得到的内存占用值[有可能不准确](https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt)。由此推断 `memory.peak` 也可能存在波动性。因此,Seele 的安全沙箱会结合 `ru_maxrss` 和 `memory.peak` 来共同决定进程的最大内存占用。 36 | -------------------------------------------------------------------------------- /docs/pages/advanced/images.en.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from "nextra-theme-docs"; 2 | 3 | # Utility Images 4 | 5 | We provide some utility container images to make it easier for users to use Seele. 6 | 7 | ## `diff-scripts` image 8 | 9 | The source files for the image are located at [images/diff-scripts](https://github.com/darkyzhou/seele/tree/main/images/diff-scripts). You can reference this image in the judge task using `image: darkyzhou/diff-scripts`. 10 | 11 | `diff-scripts` provides two script files, `diff-strict` and `diff-loose`, for comparing the differences between the output content of the judge program and the standard answer content. They are based on GNU diffutils. 12 | 13 | ### `diff-strict` 14 | 15 | Checks if the contents of the `user.out` and `standard.out` files in the current directory are exactly the same. 16 | 17 | - If there are differences other than trailing whitespace characters and differing numbers of whitespace characters, the exit code is `8`. 18 | - If there are _only_ differences in trailing whitespace characters or differing numbers of whitespace characters, the exit code is `9`. 19 | - If there are no differences, the exit code is `0`. 20 | - If the file does not exist, the exit code is `1`. 21 | 22 | ### `diff-loose` 23 | 24 | Checks if the contents of the `user.out` and `standard.out` files in the current directory are _basically_ the same. 25 | 26 | - If there are differences other than trailing whitespace characters, differing numbers of whitespace characters, and differing numbers of empty lines, the exit code is `8`. 27 | - If there are no such differences, the exit code is `0`. 28 | - If the file does not exist, the exit code is `1`. 29 | 30 | ### Usage 31 | 32 | The script files always use the `user.out` file in the current directory as the output content of the judge program and the `standard.out` as the standard answer content. When using the [execution task](/tasks/judge), we can modify the mounted file names using the `files` attribute. 33 | 34 | The following example demonstrates the usage of `diff-loose`, and the usage of `diff-strict` is similar. In this example, we redirect the output stream of the judge program to the file `__user_output`, and then mount it along with the answer file `__answer` in the utility image for comparison. 35 | 36 | ```yaml 37 | steps: 38 | run: 39 | # ... 40 | fd: 41 | stdout: "__user_output" 42 | 43 | compare: 44 | action: "seele/run-judge/run@1" 45 | image: "harbor.matrix.moe/docker.io/darkyzhou/diff-scripts" 46 | command: "diff-loose" 47 | files: 48 | - "__user_output:user.out" 49 | - "__answer:standard.out" 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/pages/advanced/images.zh.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from "nextra-theme-docs"; 2 | 3 | # 工具镜像 4 | 5 | 我们提供了一些工具类的容器镜像,为用户使用 Seele 提供了方便。 6 | 7 | ## `diff-scripts` 镜像 8 | 9 | 镜像的源文件位于 [images/diff-scripts](https://github.com/darkyzhou/seele/tree/main/images/diff-scripts)。可在评测任务中使用 `image: darkyzhou/diff-scripts` 引用此镜像。 10 | 11 | `diff-scripts` 提供了两个脚本文件 `diff-strict` 和 `diff-loose`,用于对比评测程序的输出内容和标准答案内容之间是否存在差异,它们基于 GNU 的 diffutils。 12 | 13 | ### `diff-strict` 14 | 15 | 检查当前目录的 `user.out` 和 `standard.out` 文件内容是否完全一致。 16 | 17 | - 若存在除了末尾空白字符和数量不同的空白字符外的差异,退出码为 `8`。 18 | - 若**仅存在**末尾空白字符或数量不同的空白字符上的差异,退出码为 `9`。 19 | - 若不存在差异,退出码为 `0`。 20 | - 若文件不存在,退出码为 `1`。 21 | 22 | ### `diff-loose` 23 | 24 | 检查当前目录的 `user.out` 和 `standard.out` 文件内容是否*基本*一致。 25 | 26 | - 若存在除了末尾空白字符、数量不同的空白字符以及数量不同的空行外的差异,退出码为 `8`。 27 | - 若不存在上述差异,退出码为 `0`。 28 | - 若文件不存在,退出码为 `1`。 29 | 30 | ### 使用方法 31 | 32 | 脚本文件总是固定使用当前目录的 `user.out` 文件作为评测程序的输出内容,使用 `standard.out` 作为标准答案内容。我们在使用[执行任务](/tasks/judge)时可以通过 `files` 属性来修改挂载文件的名称。 33 | 34 | 下面的例子展示了 `diff-loose` 的使用方法,`diff-strict` 的使用同理。在这个例子中,我们将评测程序的输出流重定向到文件 `__user_output` 中,然后结合答案文件 `__answer` 挂载到工具镜像中进行比较。 35 | 36 | ```yaml 37 | steps: 38 | run: 39 | # ... 40 | fd: 41 | stdout: "__user_output" 42 | 43 | compare: 44 | action: "seele/run-judge/run@1" 45 | image: "harbor.matrix.moe/docker.io/darkyzhou/diff-scripts" 46 | command: "diff-loose" 47 | files: 48 | - "__user_output:user.out" 49 | - "__answer:standard.out" 50 | ``` -------------------------------------------------------------------------------- /docs/pages/advanced/sandbox.zh.mdx: -------------------------------------------------------------------------------- 1 | # 安全沙箱 2 | 3 | Seele 评测系统的安全沙箱是一个被称为 Runj 的独立程序,使用 [Go](https://go.dev/) 语言编写。评测系统在运行[编译任务和执行任务](/tasks/judge)时会启动 Runj,由它创建一个隔离环境,并在隔离环境中启动需要评测的程序。 4 | 5 | Runj 确保程序运行在一个独立的环境中,仅具备有限的权限以完成执行,进而防止不同评测任务之间相互影响,以及恶意代码实施数据窃取、破坏系统的行为。为了进一步的安全性,Runj 6 | 具有尽可能低的权限需求,不使用 root 权限。此外,它不会对评测程序的执行效率造成显著的负面影响。 7 | 8 | 常见的在线评测系统使用的技术主要包括:`ptrace`、`chroot`、`seccomp` 和容器技术等。Seele 选择基于 [runc](https://github.com/opencontainers/runc) 使用容器技术构建安全沙箱。 9 | 10 | ## `ptrace` 与 `chroot` 11 | 12 | `ptrace` 是 Linux 操作系统提供的一个系统调用,原本用于调试程序,可以控制程序的执行、监视资源占用等,因而被评测系统用来构建安全沙箱,防止程序执行不安全的系统调用。`chroot` 也是 Linux 内核提供的一个系统调用,可以修改一个进程的根目录。评测系统可以使用 `chroot` 来将程序的根目录设置到一个独立的文件夹中,使得程序无法访问系统的其它文件,进而实现环境隔离。 13 | 14 | [virusdefender 通过实验发现](https://github.com/virusdefender/UndergraduateThesis):`ptrace` 会对程序的运行速度造成显著的负面影响:一个使用 C++ 语言编写的程序运行 900 万次 `cin` 和 `cout`,相比不使用 `ptrace` 沙箱的情况下,消耗的 CPU 时间增加了近 2 倍、实际运行时间增加了近 5 倍。因此,`ptrace` 技术可能导致评测系统错误地判断学生提交的代码超过用户设置的时间限制,并给出错误的评分,对课程和考试的公正性造成恶劣影响。 15 | 16 | 安全沙箱使用 `chroot` 时需要具备 root 权限,这带来了一定的安全风险。同时,`chroot` 的本质是修改内核中进程结构体的相关字段,并非为了安全沙箱而设计,不能完全阻止攻击。 恶意代码可能会利用这些潜在的攻击面取得 root 权限或者访问外部文件,实施数据窃取等行为。 17 | 18 | ## `seccomp` 19 | 20 | 近年来,一些新的评测系统开始使用 Linux 内核提供的 `seccomp` 技术取代 `ptrace` 技术以限制程序进行的系统调用。`seccomp` 能够给程序设置一个由系统调用及其相关参数构成的规则列表,在这个列表中用户可以指定可以执行和不可执行的系统调用。相比 `ptrace`,它对程序的运行性能几乎没有负面影响。 21 | 22 | 不同程序的运行会产生不同的系统调用以及参数,因此这些评测系统一般都会给每种编程语言的运行环境专门设置一个 `seccomp` 的规则列表。维护这些规则列表本身需要耗费时间和努力,并且一旦规则出现纰漏可能会导致攻击面的出现,给评测系统带来安全风险。同时,若规则限制过当,学生提交的程序可能无法正常运行。同时,一些语言例如 Haskell 产生的程序在运行时产生的系统调用是难以完全列举的。`seccomp` 技术较差的泛用性和较高的配置复杂度,使得它难以应对本评测系统的需求。 23 | 24 | ## 容器技术 25 | 26 | 传统的虚拟化技术通过在物理服务器上使用虚拟机创建多个操作系统来实现环境隔离。以 Docker 为代表的软件提出了一种新的虚拟化形式:容器虚拟化。它们使用 Linux 容器相关技术为程序提供一个隔离的环境。容器虚拟化带来的功能非常切合安全沙箱的需求。以 Docker 为例,除了前文介绍的 `seccomp` 技术之外,它还主要使用了 Linux 内核提供的下列技术来构建容器: 27 | 28 | ### 命名空间 29 | 30 | Linux 命名空间可以将系统的各种资源进行分隔,使得指定的程序只能看到访问的一部分资源,而无法访问其他资源。从这项技术被引入的 2006 年至今,人们已经向 Linux 内核中添加了许多种类的命名空间,对系统的不同资源进行分隔: 31 | 32 | - 用户命名空间(User namespaces)。隔离用户 ID 和相关权限。用户命名空间具有特殊的功能,下文会单独介绍。 33 | - Cgroup 命名空间(Cgroup namespaces)。隔离进程能在 cgroup 目录访问和配置的进程。 34 | - IPC 命名空间(IPC namespaces)。隔离进程对 SystemV IPC 和 POSIX 消息队列的访问。 35 | - 网络命名空间(Network namespaces)。隔离进程使用的网络栈、网络设备、端口等。 36 | - 挂载命名空间(Mount namespaces)。隔离进程访问的各个挂载点。 37 | - PID 命名空间(PID namespaces)。隔离进程的进程 ID 空间,这可以让位于不同的进程命名空间的进程拥有相同的 PID。 38 | - 时间命名空间(Time namespaces)。隔离进程得到的系统时钟时间。 39 | - UTS 命名空间(UTS namespaces)。隔离进程得到的 hostname。 40 | 41 | ### cgroup 42 | 43 | cgroup 技术能够限制程序的各种资源占用并在必要时终止超过限制的程序,以及收集程序的资源占用情况。Cgroup 目前分为 `v1` 和 `v2` 两个版本,后者进行了较大规模的改进,并且目前大多数新版的 Linux 发行版已经默认使用 `v2` 版本,因此 Seele 使用了 `v2` 版本。Cgroup 通过不同的 controller 来实现不同种类资源的占用: 44 | 45 | - Cpu controller。控制进程使用的 Cpu 时间、优先级等,也可以收集进程运行消耗的 Cpu 时间,包括内核态时间与用户态时间。 46 | - Cpuset controller。控制进程处于哪些 Cpu 核心或节点,以及哪些 NUMA 节点。 47 | - Memory controller。控制进程在用户态、内核态以及 TCP 套接字中使用的内存,也可以收集进程运行消耗的内存量。 48 | - Pid controller。控制进程在达到数量限制之后无法再通过 `fork()` 或 `clone()` 系统调用来产生新进程。 49 | 50 | ### rlimit 51 | 52 | Linux 提供的 rlimit 技术可以限制程序的某些资源占用,例如可以通过 `RLIMIT_FSIZE` 限制程序向文件描述符写入的数据总量、通过 `RLIMIT_CORE` 可以开启或关闭在程序宕机时系统收集 Core dump 信息的行为。 53 | 54 | ### overlayfs 55 | 56 | 每个使用容器技术运行的进程都拥有一个隔离的文件系统,这个文件系统中含有进程需要的 Linux 系统中常见的各种文件和文件夹,例如 `/proc`、`/var` 等文件夹、 `bash`、`ls` 等工具,以及 `glibc` 链接库。 57 | 58 | ### 总结 59 | 60 | 为了节约硬盘空间以及实现镜像分层存储,容器技术使用 Linux 提供的 `overlayfs` 为容器中的进程构建隔离的文件系统。`overlayfs` 可以将多个文件系统或文件夹合并为单个文件系统,让不同的容器共享同一个系统镜像。它们能够访问磁盘中相同的 `glibc` 链接库文件,也能够对各自的文件系统进行写入。`overlayfs` 会自动隔离这些写入,使得容器之间无法互相干扰,保证文件系统的隔离性。 61 | 62 | 容器技术相比前文介绍的 `ptrace`、`chroot` 和 `seccomp` 技术在理论上具有更好的安全性。在命名空间技术的帮助下,我们可以对操作系统的各种关键资源进行隔离,让不同的程序处于隔离的环境中。例如,通过 IPC 命名空间的隔离,进程无法访问宿主系统上的其它进程的共享内存。又如,通过挂载命名空间的隔离,进程无法访问宿主系统的文件系统,也无法访问其他容器进程的文件系统。 63 | 64 | 容器技术相比前文介绍的技术也具有更好的泛用性。用户不需要再像使用 `seccomp` 技术那样,为每一种评测场景准备一套规则列表。容器技术为容器中的程序构建了一份独立的环境,即使程序包含恶意代码,它也只能访问到独立环境中的资源,难以突破到外部的评测系统的环境。 65 | -------------------------------------------------------------------------------- /docs/pages/advanced/telemetry.en.mdx: -------------------------------------------------------------------------------- 1 | # Observability 2 | 3 | Seele exports Tracing and Metrics data using the SDK provided by OpenTelemetry. 4 | 5 | ## Tracing 6 | 7 | Seele's Tracing data starts from when Composer receives the judge task from Exchange and ends when Composer sends the judge report to Exchange. Each Tracing contains multiple Spans, which include sufficient Log Events for tracking the execution status of the judge task. 8 | 9 | The various Spans in Tracing mainly come from: 10 | 11 | - Composer tracks the execution of each subtask. 12 | - Worker receives the action task sent by Composer. 13 | - Worker performs the [add file task](/tasks/files). 14 | - Worker performs the [compilation task and execution task](/tasks/judge). 15 | 16 | Composer adds the following attributes to the root Span reported before and after processing the judge task: 17 | 18 | - `seele.submission.id`, taken from the ID of the judge task. 19 | - `seele.submission.attribute`, taken from the `tracing_attributes` of the judge task. 20 | - `seele.submission.status`, indicating the type of judge report, with values `COMPLETED` or `ERROR`. 21 | 22 | When the Worker performs the compilation task or execution task, it adds additional attributes to the Log Events in the Span, which come from the corresponding judge report](/tasks/judge#judge-report). 23 | 24 | - `seele.container.status` 25 | - `seele.container.code` 26 | - `seele.container.signal` 27 | - `seele.container.cpu_user_time` 28 | - `seele.container.cpu_kernel_time` 29 | - `seele.container.memory_usage` 30 | 31 | Users can use the above attributes to assist in querying specific Tracing records, thus quickly locating problems. 32 | 33 | ## Metrics 34 | 35 | ### `seele.submission.duration` 36 | 37 | A `float64` Histogram with units of `s`. It records the overall execution time of each judge task received by the judge system. This Histogram can also be used to count the total number of judge tasks performed by the judge system and the number of requests in a period. 38 | 39 | Seele adds a `submission.status` attribute to each reported record, indicating the type of judge report, with values `COMPLETED` or `ERROR`. Users can monitor the occurrence of failed judge tasks based on the value of this attribute and promptly alert and handle them. 40 | 41 | ### `seele.runner.count` 42 | 43 | A `uint64` Gauge, indicating the number of online [Runner threads](/advanced/architecture) in the current instance. 44 | 45 | ### `seele.action.container.pending.count` 46 | 47 | A `uint64` Gauge, indicating the number of [compilation tasks or execution tasks](/tasks/judge) waiting to be executed in the secure sandbox thread pool task queue in the current instance. If this data remains at a consistently high value and continues to rise, it often indicates that the number of CPU cores allocated by the user for the judge system is insufficient to support the large volume of requests. 48 | -------------------------------------------------------------------------------- /docs/pages/advanced/telemetry.zh.mdx: -------------------------------------------------------------------------------- 1 | # 观测性 2 | 3 | Seele 通过 OpenTelemetry 提供的 SDK 导出 Tracing 和 Metrics 数据。 4 | 5 | ## Tracing 6 | 7 | Seele 的 Tracing 数据从 Composer 接收到 Exchange 发来的评测任务开始,直到 Composer 向 Exchange 8 | 发送评测报告结束。每个 Tracing 包含多个 Span,其中包含充足的 Log Event 用于追踪评测任务的执行状况。 9 | 10 | Tracing 的各个 Span 主要来自于: 11 | 12 | - Composer 追踪每个子任务的执行。 13 | - Worker 收到 Composer 发来的动作任务。 14 | - Worker 执行[添加文件任务](/tasks/files)。 15 | - Worker 执行[编译任务和执行任务](/tasks/judge)。 16 | 17 | Composer 在处理评测任务的前后会为上报的根 Span 附加下列的属性: 18 | 19 | - `seele.submission.id`,取自评测任务的 ID。 20 | - `seele.submission.attribute`,取自评测任务的 `tracing_attributes`。 21 | - `seele.submission.status`,表示评测报告的类型,取值为 `COMPLETED` 或 `ERROR`。 22 | 23 | 当 Worker 执行编译任务或执行任务时,它会为 Span 中的 Log Event 附加额外的属性,它们来自对应的[评测报告](/tasks/judge#%E8%AF%84%E6%B5%8B%E6%8A%A5%E5%91%8A)。 24 | 25 | - `seele.container.status` 26 | - `seele.container.code` 27 | - `seele.container.signal` 28 | - `seele.container.cpu_user_time` 29 | - `seele.container.cpu_kernel_time` 30 | - `seele.container.memory_usage` 31 | 32 | 用户可根据上述的属性辅助查询特定的 Tracing 记录,从而快速定位问题。 33 | 34 | ## Metrics 35 | 36 | ### `seele.submission.duration` 37 | 38 | 类型为 `float64` 的 Histogram,单位为 `s`。统计了评测系统收到的每个评测任务整体的执行时间。此 Histogram 亦可用于统计评测系统执行的评测任务总数,以及在一段时间内的请求数。 39 | 40 | Seele 为上报的每一条记录附加了 `submission.status` 属性,表示评测报告的类型,取值为 `COMPLETED` 或 `ERROR`。用户可根据此属性的取值监控是否出现执行失败的评测任务,并及时告警处理。 41 | 42 | ### `seele.runner.count` 43 | 44 | 类型为 `uint64` 的 Gauge,指示了当前实例在线的 [Runner 线程](/advanced/architecture)数量。 45 | 46 | ### `seele.action.container.pending.count` 47 | 48 | 类型为 `uint64` 的 Gauge,指示了当前实例中,在安全沙箱线程池任务队列中等待执行的[编译任务或执行任务](/tasks/judge)数量。若此数据持续保持较高的数值且不断升高,往往说明用户为评测系统分配的 CPU 核心数量不足以支撑庞大的请求量。 49 | -------------------------------------------------------------------------------- /docs/pages/configurations/_meta.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "file": "Configuration File", 3 | "exchange": "Exchange", 4 | "composer": "Composer", 5 | "worker": "Worker" 6 | } -------------------------------------------------------------------------------- /docs/pages/configurations/_meta.zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "file": "配置文件", 3 | "exchange": "Exchange 配置", 4 | "composer": "Composer 配置", 5 | "worker": "Worker 配置" 6 | } -------------------------------------------------------------------------------- /docs/pages/configurations/composer.en.mdx: -------------------------------------------------------------------------------- 1 | # Composer Configuration 2 | 3 | Composer is responsible for receiving judge tasks from Exchange, parsing the tasks, and generating a multi-branch tree composed of steps. It then sends the steps from the root of the tree to the Worker and tracks the execution of the steps. 4 | 5 | Composer currently does not have any configuration options. 6 | -------------------------------------------------------------------------------- /docs/pages/configurations/composer.zh.mdx: -------------------------------------------------------------------------------- 1 | # Composer 配置 2 | 3 | Composer 接收来自 Exchange 的评测任务,解析评测任务并生成一棵由步骤构成的多叉树,从沿着这棵树从根部开始向 Worker 发送步骤,追踪步骤的执行。 4 | 5 | 奇怪的是,Composer 目前没有任何配置项。 6 | -------------------------------------------------------------------------------- /docs/pages/configurations/worker.en.mdx: -------------------------------------------------------------------------------- 1 | # Worker Configuration 2 | 3 | Worker receives action tasks from Composer and executes the corresponding action tasks according to the parameters in them. Finally, it sends the execution report to Composer. The configuration items of Worker are shown in the table below: 4 | 5 | | Name | Type | Description | 6 | | :------- | :------: | :------------------------------ | 7 | | `action` | `object` | Action task configuration items | 8 | 9 | The properties of the `action` configuration item are shown in the table below: 10 | 11 | | Name | Type | Description | 12 | | :-------------- | :------: | :------------------------------------------- | 13 | | `add_file` | `object` | [Add File](/tasks/files) configuration items | 14 | | `run_container` | `object` | [Sandbox](/tasks/judge) configuration items | 15 | 16 | ## `add_file` Configuration 17 | 18 | | Name | Type | Default Value | Description | 19 | | :--------------- | :------: | :-----------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 20 | | `cache_strategy` | `string` | `'default'` | Cache strategy for HTTP URL downloads, see [lib.rs](https://github.com/06chaynes/http-cache/blob/427438ce590aeba88ab2de6936a85bb5db4d7193/http-cache/src/lib.rs#L248).
Note that the value uses Snake case | 21 | | `cache_size_mib` | `number` | `512` | Maximum cache size. This cache is stored in memory | 22 | | `cache_ttl_hour` | `number` | `72` | TTL time for each cache item, in hours | 23 | 24 | ## `run_container` Configuration 25 | 26 | | Name | Type | Default Value | Description | 27 | | :----------------------------- | :--------: | :--------------: | :-------------------------------------------------------------------------------------------------------- | 28 | | `pull_image_timeout_seconds` | `number` | `600` | Timeout for skopeo to pull the image, in seconds | 29 | | `unpack_image_timeout_seconds` | `number` | `600` | Timeout for umoci to unpack the image, in seconds | 30 | | `userns_uid` | `number` | Current user ID | User ID used by the secure sandbox | 31 | | `userns_user` | `string` | Current username | Username used by the secure sandbox | 32 | | `userns_gid` | `number` | Current group ID | Group ID used by the secure sandbox | 33 | | `cache_size_mib` | `number` | `512` | Maximum size of the [compilation task cache](/tasks/judge#cache-property). This cache is stored in memory | 34 | | `cache_ttl_hour` | `number` | `72` | TTL time for each cache item, in hours | 35 | | `preload_images` | `string[]` | `[]` | List of image names to be downloaded and unpacked before Seele starts receiving requests | 36 | -------------------------------------------------------------------------------- /docs/pages/configurations/worker.zh.mdx: -------------------------------------------------------------------------------- 1 | # Worker 配置 2 | 3 | Worker 接收 Composer 发来的动作任务,并根据其中的参数执行对应的动作任务,最后将执行报告发送给 Composer。它的配置项如下表所示: 4 | 5 | | 名称 | 类型 | 简介 | 6 | | :------- | :------: | :--------------- | 7 | | `action` | `object` | 动作任务的配置项 | 8 | 9 | 其中,`action` 配置项的属性如下表所示: 10 | 11 | | 名称 | 类型 | 简介 | 12 | | :-------------- | :------: | :------------------------------- | 13 | | `add_file` | `object` | [添加文件](/tasks/files)的配置项 | 14 | | `run_container` | `object` | [安全沙箱](/tasks/judge)的配置项 | 15 | 16 | ## `add_file` 配置 17 | 18 | | 名称 | 类型 | 默认值 | 简介 | 19 | | :--------------- | :------: | :---------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 20 | | `cache_strategy` | `string` | `'default'` | HTTP URL 下载的缓存策略,参见 [lib.rs](https://github.com/06chaynes/http-cache/blob/427438ce590aeba88ab2de6936a85bb5db4d7193/http-cache/src/lib.rs#L248)。
注意取值使用 Snake case | 21 | | `cache_size_mib` | `number` | `512` | 缓存的大小上限。此缓存存在于内存中 | 22 | | `cache_ttl_hour` | `number` | `72` | 每个缓存项的 TTL 时间。单位为小时 | 23 | 24 | ## `run_container` 配置 25 | 26 | | 名称 | 类型 | 默认值 | 简介 | 27 | | :----------------------------- | :--------: | :-----------: | :------------------------------------------------------------------------------------ | 28 | | `pull_image_timeout_seconds` | `number` | `600` | skopeo 拉取镜像的超时时间。单位为 s | 29 | | `unpack_image_timeout_seconds` | `number` | `600` | umoci 解压镜像的超时时间。单位为 s | 30 | | `userns_uid` | `number` | 当前用户 ID | 安全沙箱使用的用户 ID | 31 | | `userns_user` | `string` | 当前用户名 | 安全沙箱使用的用户名 | 32 | | `userns_gid` | `number` | 当前用户组 ID | 安全沙箱使用的用户组 ID | 33 | | `cache_size_mib` | `number` | `512` | [编译任务的缓存](/tasks/judge#cache-%E5%B1%9E%E6%80%A7)的大小上限。此缓存存在于内存中 | 34 | | `cache_ttl_hour` | `number` | `72` | 每个缓存项的 TTL 时间。单位为小时 | 35 | | `preload_images` | `string[]` | `[]` | Seele 启动后,在开始接收请求前需要下载并解压的镜像名称列表 | 36 | -------------------------------------------------------------------------------- /docs/pages/index.zh.mdx: -------------------------------------------------------------------------------- 1 | # Seele 2 | 3 | Seele 是一款面向云原生的在线评测(Online Judge)系统,主要面向计算机相关的在线课程系统、程序设计竞赛等场景。 4 | 它作为评测服务接收用户提交的代码,在安全沙箱中运行并返回评测报告。 5 | 6 | Seele 的诞生是为了解决当前一些流行的开源在线评测系统在伸缩性、扩展性和观测性上存在的不足。 7 | 同时,它的安全沙箱基于著名的容器运行时 [runc](https://github.com/opencontainers/runc/),并使用 [Rootless Containers](https://rootlesscontaine.rs/) 带来额外的安全性。 8 | 目前,Seele 服务于华南某高校的在线课程系统,承接各类实验课程和机试的需求,覆盖来自不同学院的数以千计的师生。 9 | 10 | 本项目是作者的本科毕业设计,并且处于早期阶段,在功能性和稳定性上可能存在许多不足之处,敬请谅解。 11 | 如果你有好的建议或发现了 bug,欢迎前往本项目的 [GitHub 仓库](https://github.com/darkyzhou/seele)发表 issue 并顺便点一下 star。 12 | 13 | ## 伸缩性 14 | 15 | Seele 自设计之初就充分考虑了利用系统提供的多个 CPU 核心提高并行处理请求能力的重要性。 16 | 17 | 此外,Seele 还可以在多种环境下运行。我们既可以作为系统上的普通用户直接运行 Seele,也可以通过 Docker、Kubernetes 等平台调度多个实例来处理用户请求。 18 | 在 Kubernetes 平台上运行时,用户可以根据系统负载自动进行横向扩容,充分利用多台服务器带来的计算资源。 19 | 20 | ## 扩展性 21 | 22 | Seele 允许用户通过 YAML 语言,使用类似 GitHub Actions 风格的结构描述评测任务每个步骤的具体内容,并决定每个步骤之间的依赖关系、并发关系等。 23 | 要运行任意程序,用户只需要提供对应的容器镜像名称。Seele 会像 Docker 那样自动安装这些镜像,然后启动容器来运行用户指定的程序。 24 | 25 | 下面是一个简单的评测任务,它分为三个步骤:添加源文件、编译源文件并执行程序。此评测任务也提供了一段 JavaScript 脚本代码,当评测任务执行时 Seele 26 | 会运行这段脚本来为返回的评测报告附加额外的内容。 27 | 28 | 通过这种方式,Seele 将定义评测流程的职责交给用户,让用户可以自由定制评测流程以应对复杂多变的课程需求。 29 | 30 | ```yaml 31 | reporter: 32 | # Seele 提供了全局变量 DATA 表示任务执行状态 33 | javascript: | 34 | const date = new Date(); 35 | return { 36 | report: { 37 | message: "Hello at " + date, 38 | type: DATA.steps.prepare.status 39 | } 40 | } 41 | 42 | steps: 43 | prepare: 44 | action: "seele/add-file@1" 45 | files: 46 | # 将下列内容添加为 `main.c` 文件,也可以通过 url 提供文件内容 47 | - path: "main.c" 48 | plain: | 49 | #include 50 | int main(void) { 51 | printf("Hello, world!\n"); 52 | return 0; 53 | } 54 | 55 | compile: 56 | action: "seele/run-judge/compile@1" 57 | # 在 gcc 11-bullseye 镜像中执行编译命令 58 | # Seele 默认会从 https://hub.docker.com 中下载容器镜像 59 | image: "gcc:11-bullseye" 60 | command: "gcc -O2 -Wall main.c -o main" 61 | sources: ["main.c"] 62 | saves: ["main"] 63 | 64 | run: 65 | action: "seele/run-judge/run@1" 66 | image: "gcc:11-bullseye" 67 | command: "main" 68 | files: ["main"] 69 | ``` 70 | 71 | ## 观测性 72 | 73 | Seele 基于 [OpenTelemetry](https://opentelemetry.io/) 提供了良好的观测性,方便维护人员了解评测系统目前的负载状况,以及进行相关的预警设置。 74 | 它主要提供了 Tracing 和 Metrics 两项指标。Tracing 能够为每个输入的评测任务进行追踪,收集它在评测系统各个组件中的执行流程。Metrics 能够提供评测系统的负载情况、处理请求速度等信息。 75 | 76 | 下图展示了 Metrics 指标通过 [Grafana](https://grafana.com/) 进行监控的示例。 77 | 78 | ![示例 Grafana 面板](/grafana.png) 79 | 80 | 下图展示了一次评测任务的 Tracing 数据,通过 [Tempo](https://grafana.com/oss/tempo/) 收集并展示。 81 | 82 | ![示例 Tracing 数据](/tempo.png) 83 | 84 | ## 安全性 85 | 86 | Seele 的安全沙箱基于 Linux 内核提供的容器技术,包括 [Control Group v2](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html)、[Namespaces](https://www.kernel.org/doc/html/latest/admin-guide/namespaces/index.html) 等技术。 87 | 它还使用了 [Rootless Containers](https://rootlesscontaine.rs/) 技术,使得它的运行**不需要** `root` 权限。 88 | 相比于许多基于 `ptrace` 技术的安全沙箱来说,它具有更好的安全性、可扩展性,并且不会对程序的运行效率造成显著影响。相比于许多基于 `seccomp` 技术的安全沙箱,它则具备更好的灵活性,不需要为每一种评测场景准备系统调用白名单。 89 | 90 | 安全沙箱的底层基于著名的容器运行时 [runc](https://github.com/opencontainers/runc/),使得安全沙箱能够伴随后者的更新对 Linux 内核中出现的安全漏洞持续地提供修复, 91 | 并且也能够确保容器技术在使用上的正确性。我们为 Seele 安全沙箱的集成测试准备了来自[青岛大学评测系统](https://github.com/QingdaoU/Judger)、 92 | [Vijos](https://github.com/vijos/malicious-code) 和 [Matrix 课程系统](https://matrix.sysu.edu.cn/about)的数十个测例,这些测例的内容涵盖了恶意消耗计算资源、输出大量数据等恶意行为,结果显示安全沙箱均能通过这些测例,保护系统的安全。 93 | 94 | ## 不具备的功能 95 | 96 | Seele 是一个功能较为纯粹的评测系统,它的唯一功能是:接收外部输入的评测任务、执行评测任务并返回评测报告。 97 | 它**并不具备**其它评测系统常常具备的功能,包括用户管理、课程管理、竞赛功能、排行榜、网页管理前端等。 98 | 99 | Seele 不具备保存评测任务的功能,当系统关闭或在执行评测任务过程中崩溃时,它并不会重新执行评测任务。因此,用户需要自行维护一套机制保存和追踪提交的评测任务。 100 | -------------------------------------------------------------------------------- /docs/pages/misc/_meta.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": "Local Development", 3 | "roadmap": "Roadmap", 4 | "naming": "About Naming" 5 | } -------------------------------------------------------------------------------- /docs/pages/misc/_meta.zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": "本地开发", 3 | "roadmap": "路线图", 4 | "naming": "关于命名" 5 | } -------------------------------------------------------------------------------- /docs/pages/misc/development.en.mdx: -------------------------------------------------------------------------------- 1 | # Local Development 2 | 3 | It is recommended to use a distribution with a newer kernel, such as Archlinux, for local development. Local development only requires a regular user, not `root` privileges. 4 | 5 | ## Environment Requirements 6 | 7 | - Golang 1.19 8 | - Cargo 1.69.0+ 9 | - rustc 1.69.0+ 10 | 11 | ## Debugging the Security Sandbox 12 | 13 | There is a `Makefile` in the `runj` folder. You can build the sandbox and save the executable file to `runj/bin` by running `make build`. 14 | 15 | For specific methods to run unit tests and integration tests, please refer to the `README.md` file in the `runj` directory. 16 | 17 | ## Debugging the Judge Service 18 | 19 | The Seele repository provides a `Dockerfile.dev` for local development packaging. You can build Seele, including the judge service program and the sandbox program, by running `docker build -f Dockerfile.dev .`. After the build is complete, you can start Seele following the [Docker Deployment](/getting-started) method. 20 | 21 | If you want to use a debugger or start the judge service directly with `cargo run`, follow the steps below: 22 | 23 | - On Archlinux, you need to install additional packages `systemd-libs` and `protobuf`. On Ubuntu, you need to install additional packages `libsystemd-dev` and `protobuf-compiler`. 24 | - Refer to the configuration file below: 25 | 26 | ```toml copy filename="config.toml" 27 | log_level = "info" 28 | work_mode = "bare" 29 | 30 | [paths] 31 | root = '/home/user/seele/root' 32 | runj = '/home/user/seele/runj/bin/runj' 33 | 34 | [worker.action.run_container] 35 | userns_user = "user" 36 | userns_uid = 1000 37 | userns_gid = 1000 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/pages/misc/development.zh.mdx: -------------------------------------------------------------------------------- 1 | # 本地开发 2 | 3 | 建议使用 Archlinux 等具有新版内核的发行版进行本地开发。进行本地开发仅需使用普通用户,不需要 `root` 权限。 4 | 5 | ## 环境要求 6 | 7 | * Golang 1.19 8 | * Cargo 1.69.0+ 9 | * rustc 1.69.0+ 10 | 11 | ## 调试安全沙箱 12 | 13 | `runj` 文件夹下存在 `Makefile`,通过 `make build` 即可构建安全沙箱并将可执行文件保存到 `runj/bin` 中。 14 | 15 | 对于运行单元测试和集成测试的具体方法,请参考 `runj` 文件夹下的 `README.md`。 16 | 17 | ## 调试评测服务 18 | 19 | Seele 的仓库中提供了 `Dockerfile.dev` 用于本地开发的打包。可以通过 `docker build -f Dockerfile.dev .` 构建 Seele 20 | 评测系统,包括评测服务程序和安全沙箱程序。构建完成后可按照 [Docker 部署](/getting-started)的方法启动评测系统。 21 | 22 | 如果要使用调试器或 `cargo run` 直接启动评测服务,请参照下面的步骤: 23 | 24 | * 在 Archlinux 上需要额外安装 `systemd-libs` 和 `protobuf`,对于 Ubuntu 则需要额外安装 `libsystemd-dev` 和 `protobuf-compiler` 25 | * 参考下面的配置文件 26 | 27 | ```toml copy filename="config.toml" 28 | log_level = "info" 29 | work_mode = "bare" 30 | 31 | [paths] 32 | root = '/home/user/seele/root' 33 | runj = '/home/user/seele/runj/bin/runj' 34 | 35 | [worker.action.run_container] 36 | userns_user = "user" 37 | userns_uid = 1000 38 | userns_gid = 1000 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/pages/misc/naming.en.mdx: -------------------------------------------------------------------------------- 1 | # About Naming 2 | 3 | With humble words and heart sincere, 4 | 5 | I dedicate this work so dear, 6 | 7 | To the one I cherish, pure and true, 8 | 9 | My beloved Seele Vollerei, I offer you. 10 | -------------------------------------------------------------------------------- /docs/pages/misc/naming.zh.mdx: -------------------------------------------------------------------------------- 1 | # 关于命名 2 | 3 | *谨以此微小的工作,献给我最爱的她——希儿·芙乐艾(Seele Vollerei)* 4 | -------------------------------------------------------------------------------- /docs/pages/misc/roadmap.en.mdx: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | Due to time constraints, many important features of Seele have not yet been implemented. 4 | 5 | - [ ] Implement automatic detection of NUMA nodes and automatic binding of memory nodes 6 | 7 | - [ ] Run Seele in Rootless Docker 8 | 9 | - [ ] Provide a standalone Seele executable for bare-metal deployment 10 | 11 | - [ ] Provide a Kubernetes deployment guide 12 | -------------------------------------------------------------------------------- /docs/pages/misc/roadmap.zh.mdx: -------------------------------------------------------------------------------- 1 | # 路线图 2 | 3 | 由于时间仓促,Seele 的许多重要功能暂未实现。 4 | 5 |
6 | 7 | - [ ] 实现 NUMA 节点的自动检测,并自动绑定内存节点 8 | 9 | - [ ] 在 Rootless docker 中运行 Seele 10 | 11 | - [ ] 提供裸机部署的 Seele 可执行文件 12 | 13 | - [ ] 提供 Kubernetes 部署指南 14 | -------------------------------------------------------------------------------- /docs/pages/tasks/_meta.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Describing Tasks", 3 | "types": "Task Types", 4 | "states": "Task States", 5 | "report": "Judge Report", 6 | "order": "Control Execution", 7 | "tags": "Tags", 8 | "directory": "Root Folder", 9 | "files": "Adding Files", 10 | "judge": "Running Judge Programs", 11 | "embed-and-upload": "Embedding and Uploading Files", 12 | "script": "Report Scripts" 13 | } -------------------------------------------------------------------------------- /docs/pages/tasks/_meta.zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "描述评测任务", 3 | "types": "任务类型", 4 | "states": "任务状态", 5 | "report": "评测报告", 6 | "order": "控制执行", 7 | "tags": "标签", 8 | "directory": "根文件夹", 9 | "files": "添加文件", 10 | "judge": "运行评测程序", 11 | "embed-and-upload": "嵌入和上传文件", 12 | "script": "报告脚本" 13 | } -------------------------------------------------------------------------------- /docs/pages/tasks/description.en.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from "nextra-theme-docs"; 2 | 3 | # Describing Judge Tasks 4 | 5 | Seele requires users to describe judge tasks using the YAML language. Each judge task consists of the properties listed in the table below: 6 | 7 | | Name | Type | Description | 8 | | :------------------ | :------: | :---------------------------------------------------------------------------- | 9 | | `id` | `string` | The ID of the judge task | 10 | | `tracing_attribute` | `string` | Optional. Attach [observability](/advanced/telemetry) attributes to this task | 11 | | `reporter` | `object` | Optional. [Report scripts](/tasks/script) configuration | 12 | | `steps` | `object` | The parameters for the root sequential task, see [Task Types](/tasks/types) | 13 | 14 | 15 | For beginner users, only `id` and `steps` are necessary from the properties 16 | listed above. 17 | 18 | -------------------------------------------------------------------------------- /docs/pages/tasks/description.zh.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from 'nextra-theme-docs' 2 | 3 | # 描述评测任务 4 | 5 | Seele 要求用户使用 YAML 语言描述评测任务。每个评测任务由下表列出的属性构成: 6 | 7 | | 名称 | 类型 | 简介 | 8 | | :------------------ | :------: | :-------------------------------------------------- | 9 | | `id` | `string` | 评测任务的 ID | 10 | | `tracing_attribute` | `string` | 可选。为此任务附加[观测性](/advanced/telemetry)属性 | 11 | | `reporter` | `object` | 可选。[报告脚本](/tasks/script)配置 | 12 | | `steps` | `object` | 根顺序任务的参数,参见[任务类型](/tasks/types) | 13 | 14 | 15 | 对于入门用户,上面的属性中只有 `id` 和 `steps` 是必要的。 16 | 17 | -------------------------------------------------------------------------------- /docs/pages/tasks/directory.en.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from "nextra-theme-docs"; 2 | 3 | # Root Folder 4 | 5 | For each judge task, Seele creates a folder on tmpfs for all subtasks to access, which we refer to as the "root folder." For example, when Seele performs an [Add File](/tasks/files) action, the file is actually saved to the root folder. When [running the judge program](/tasks/judge), the files mounted in the container also come from the root folder. 6 | 7 | Placing the root folder on tmpfs is mainly to reduce the impact of IO factors on the efficiency of running judge programs. For more information, see [Fairness](/advanced/fairness). 8 | 9 | 10 | It is the user's responsibility to ensure that no duplicate files appear in 11 | the root folder. Seele does not check whether it is overwriting an existing 12 | file when writing a new one. 13 | 14 | -------------------------------------------------------------------------------- /docs/pages/tasks/directory.zh.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from "nextra-theme-docs"; 2 | 3 | # 根文件夹 4 | 5 | 对于每个评测任务,Seele 会在 tmpfs 上创建一个文件夹供各个子任务访问,我们将这个文件夹称为“根文件夹”。 6 | 例如,当 Seele 执行[添加文件](/tasks/files)的动作任务时,文件实际上会被保存到根文件夹中。当[运行评测程序](/tasks/judge)时,向容器中挂载的文件也来自于根文件夹。 7 | 8 | 将根文件夹放置于 tmpfs 上主要是为了减少 IO 因素对评测程序的运行效率的影响,参见[公平性](/advanced/fairness)。 9 | 10 | 11 | 确保根文件夹中不出现重名文件是用户的责任。 Seele 12 | 在写入新文件时并不会检查是否正在覆盖一个已经存在的文件。 13 | 14 | -------------------------------------------------------------------------------- /docs/pages/tasks/embed-and-upload.en.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from "nextra-theme-docs"; 2 | 3 | # Embedding and Uploading Files 4 | 5 | Sequential tasks, concurrent tasks, and action tasks configuration provide a `report` property. Its parameters are shown in the table below: 6 | 7 | | Name | Type | Description | 8 | | :-------- | :--------: | :--------------------------------------------------------------------------------------------------------------------------- | 9 | | `embeds` | `object[]` | At the end of task execution, embed the contents of specified files in the [root folder](/tasks/files) into the judge report | 10 | | `uploads` | `object[]` | At the end of the entire judge task, upload specified files in the root folder through an HTTP request | 11 | 12 | ## `embeds` Property 13 | 14 | The parameters for the `embeds` property are shown in the table below: 15 | 16 | | Name | Type | Description | 17 | | :------------------ | :-------: | :----------------------------------------------------------------------------------------------------- | 18 | | `path` | `string` | The path of the file to embed content from | 19 | | `field` | `string` | The attribute name used when embedding file content into the judge report | 20 | | `truncate_kib` | `number` | Embed only the specified size of file content, in KiB | 21 | | `ignore_if_missing` | `boolean` | If the file does not exist, whether to ignore and not report an error. **The default value is `true`** | 22 | 23 | ## `uploads` Property 24 | 25 | The parameters for the `uploads` property are shown in the table below: 26 | 27 | | Name | Type | Description | 28 | | :------------------ | :-----------------: | :----------------------------------------------------------------------------------------------------- | 29 | | `path` | `string` | The path of the file to upload | 30 | | `target` | `string` | The URL of the HTTP request for uploading the file | 31 | | `method` | `'POST'` or `'PUT'` | The method of the HTTP request for uploading the file | 32 | | `ignore_if_missing` | `boolean` | If the file does not exist, whether to ignore and not report an error. **The default value is `true`** | 33 | 34 | 35 | Seele will begin asynchronously uploading files after the judge task is 36 | completed and the completion report or error report has been sent. 37 | 38 | -------------------------------------------------------------------------------- /docs/pages/tasks/embed-and-upload.zh.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from 'nextra-theme-docs' 2 | 3 | # 嵌入和上传文件 4 | 5 | 顺序任务、并发任务和动作任务的配置提供了一个 `report` 属性。它的参数如下表所示: 6 | 7 | | 名称 | 类型 | 简介 | 8 | | :-------- | :--------: | :----------------------------------------------------------------------------- | 9 | | `embeds` | `object[]` | 在任务执行结束时,将[根文件夹](/tasks/files)中的指定文件的内容嵌入到评测报告中 | 10 | | `uploads` | `object[]` | 在整个评测任务执行结束时,通过 HTTP 请求上传根文件夹中指定的文件 | 11 | 12 | ## `embeds` 属性 13 | 14 | `embeds` 属性的参数如下表所示: 15 | 16 | | 名称 | 类型 | 简介 | 17 | | :------------------ | :-------: | :---------------------------------------------------- | 18 | | `path` | `string` | 要嵌入内容的文件的路径 | 19 | | `field` | `string` | 将文件内容嵌入评测报告时使用的属性名 | 20 | | `truncate_kib` | `number` | 仅嵌入此属性指定大小的文件内容,单位为 KiB | 21 | | `ignore_if_missing` | `boolean` | 如果文件不存在,是否忽略且不报错。**默认值为 `true`** | 22 | 23 | ## `uploads` 属性 24 | 25 | `uploads` 属性的参数如下表所示: 26 | 27 | | 名称 | 类型 | 简介 | 28 | | :------------------ | :-----------------: | :---------------------------------------------------- | 29 | | `path` | `string` | 要上传的文件的路径 | 30 | | `target` | `string` | 上传文件的 HTTP 请求的 URL | 31 | | `method` | `'POST'` 或 `'PUT'` | 上传文件的 HTTP 请求的方法 | 32 | | `ignore_if_missing` | `boolean` | 如果文件不存在,是否忽略且不报错。**默认值为 `true`** | 33 | 34 | 35 | Seele 会在评测任务执行完毕,并且发送完成报告或错误报告后,开始异步地上传文件。 36 | 37 | -------------------------------------------------------------------------------- /docs/pages/tasks/files.en.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from "nextra-theme-docs"; 2 | 3 | # Adding Files 4 | 5 | Adding files is an action task that allows users to save necessary files in the [root folder](/tasks/directory) according to their parameters. Currently, this action task offers several methods to add files. 6 | 7 | ## Inline Plain Text 8 | 9 | Use the `plain` property to pass in a plain text string. Seele will write the contents of the string to the specified file. 10 | 11 | The example below adds two source files `main.h` and `main.c` using inline plain text. 12 | 13 | ```yaml 14 | steps: 15 | prepare: 16 | action: "seele/add-files@1" 17 | files: 18 | - path: "main.h" 19 | plain: | 20 | extern int power; 21 | 22 | - path: "main.c" 23 | plain: | 24 | #include 25 | #include "main.h" 26 | 27 | int power = 114514; 28 | 29 | int main(void) { 30 | printf("Power: %d\n", power); 31 | return 0; 32 | } 33 | ``` 34 | 35 | 36 | We do not recommend using inline plain text to pass user-submitted source code 37 | and other files, as YAML does not support carrying some special characters. In 38 | addition, Seele will also refuse to process non-UTF-8 encoded plain text. In 39 | these cases, using inline Base64 text or HTTP URLs is a better choice. 40 | 41 | 42 | ## Inline Base64 Text 43 | 44 | Use the `base64` property to pass in a Base64 text string. Seele will decode the Base64 text and write the resulting data to the specified file. 45 | 46 | 47 | Seele accepts only Base64 text **without** padding. Passing Base64 text with 48 | padding will cause an error. 49 | 50 | 51 | The example below adds two source files `main.h` and `main.c` using inline Base64 text. Seele will decode the Base64 text and store the binary data directly in the file. 52 | 53 | ```yaml 54 | steps: 55 | prepare: 56 | action: "seele/add-files@1" 57 | files: 58 | - path: "main.h" 59 | base64: "ZXh0ZXJuIGludCBwb3dlcjs" 60 | - path: "main.c" 61 | base64: "I2luY2x1ZGUgPHN0ZGlvLmg+CiNpbmNsdWRlICJtYWluLmgiCgppbnQgcG93ZXIgPSAxMTQ1MTQ7CgppbnQgbWFpbih2b2lkKSB7CiAgcHJpbnRmKCJQb3dlcjogJWRcbiIsIHBvd2VyKTsKICByZXR1cm4gMDsKfQ" 62 | ``` 63 | 64 | ## HTTP URL 65 | 66 | Use the `url` property to pass in an HTTP URL string. Seele will use the built-in HTTP client to send GET requests to the two URLs and write the obtained responses to the specified files. 67 | 68 | The example below adds two source files `main.h` and `main.c` using HTTP URLs. 69 | 70 | ```yaml 71 | steps: 72 | prepare: 73 | action: "seele/add-files@1" 74 | files: 75 | - path: "main.h" 76 | url: "http://darkyzhou.net/main.h" 77 | - path: "main.c" 78 | url: "http://darkyzhou.net/main.c" 79 | ``` 80 | 81 | 82 | Authentication, downloading HTTPS URLs are not supported yet. 83 | 84 | 85 | By default, the Add File task will attempt to use the negotiated cache headers in the HTTP response headers to cache downloaded files in memory, speeding up subsequent judge tasks' downloads of the same files. For more information, see [Adding File Configurations](/configurations/files). 86 | 87 | ## Local Files 88 | 89 | Use the `local` property to pass in an **absolute path** string pointing to a local file. Seele will copy the specified file. 90 | 91 | 92 | When running Seele in a Docker or Kubernetes container, make sure to mount the 93 | relevant files into the container's filesystem. 94 | 95 | 96 | The example below adds a local file `/tmp/foo.txt` to the submission root folder. 97 | 98 | ```yaml 99 | steps: 100 | prepare: 101 | action: "seele/add-files@1" 102 | files: 103 | - path: "foo.txt" 104 | local: "/tmp/foo.txt" 105 | ``` 106 | -------------------------------------------------------------------------------- /docs/pages/tasks/files.zh.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from "nextra-theme-docs"; 2 | 3 | # 添加文件 4 | 5 | 添加文件是一种动作任务,能够按照用户的参数向[根文件夹](/tasks/directory)中保存一些必要的文件。目前,此动作任务为用户提供了多种添加文件的方法。 6 | 7 | ## 内联纯文本 8 | 9 | 使用 `plain` 属性来传入纯文本字符串。Seele 会将字符串的内容写入指定的文件中。 10 | 11 | 下面的例子通过内联纯文本添加两个源文件 `main.h` 和 `main.c`。 12 | 13 | ```yaml 14 | steps: 15 | prepare: 16 | action: "seele/add-files@1" 17 | files: 18 | - path: "main.h" 19 | plain: | 20 | extern int power; 21 | 22 | - path: "main.c" 23 | plain: | 24 | #include 25 | #include "main.h" 26 | 27 | int power = 114514; 28 | 29 | int main(void) { 30 | printf("Power: %d\n", power); 31 | return 0; 32 | } 33 | ``` 34 | 35 | 36 | 不建议使用内联纯文本传递用户提交的源代码等文件,因为 YAML 37 | 并不支持承载一些特殊字符。此外,Seele 也会拒绝处理非 UTF-8 38 | 编码的纯文本。在这些情况下,使用内联 Base64 文本或 HTTP URL 是更好的选择。 39 | 40 | 41 | ## 内联 Base64 文本 42 | 43 | 使用 `base64` 属性来传入 Base64 文本字符串。Seele 会对 Base64 文本进行解码,将得到的数据写入指定的文件中。 44 | 45 | 46 | Seele 仅接受**不含** Padding 的 Base64 文本。如果传入含 Padding 的 Base64 47 | 文本会导致报错。 48 | 49 | 50 | 下面的例子通过内联 Base64 文本添加两个源文件 `main.h` 和 `main.c`。Seele 会对 Base64 文本进行解码,将二进制数据直接存入文件中。 51 | 52 | ```yaml 53 | steps: 54 | prepare: 55 | action: "seele/add-files@1" 56 | files: 57 | - path: "main.h" 58 | base64: "ZXh0ZXJuIGludCBwb3dlcjs" 59 | - path: "main.c" 60 | base64: "I2luY2x1ZGUgPHN0ZGlvLmg+CiNpbmNsdWRlICJtYWluLmgiCgppbnQgcG93ZXIgPSAxMTQ1MTQ7CgppbnQgbWFpbih2b2lkKSB7CiAgcHJpbnRmKCJQb3dlcjogJWRcbiIsIHBvd2VyKTsKICByZXR1cm4gMDsKfQ" 61 | ``` 62 | 63 | ## HTTP URL 64 | 65 | 使用 `url` 属性来传入 HTTP URL 字符串。Seele 会使用内置的 HTTP 客户端向两个 URL 发送 GET 请求,将得到的响应写入指定的文件中。 66 | 67 | 下面的例子通过 HTTP URL 添加两个源文件 `main.h` 和 `main.c`。 68 | 69 | ```yaml 70 | steps: 71 | prepare: 72 | action: "seele/add-files@1" 73 | files: 74 | - path: "main.h" 75 | url: "http://darkyzhou.net/main.h" 76 | - path: "main.c" 77 | url: "http://darkyzhou.net/main.c" 78 | ``` 79 | 80 | 暂不支持身份验证、下载 HTTPS URL。 81 | 82 | 在默认情况下,添加文件任务会尝试使用 HTTP 响应头中的协商缓存头来将下载的文件缓存到内存中,这样能够加速后续评测任务对相同文件的下载。详见[添加文件的配置项](/configurations/files)。 83 | 84 | ## 本地文件 85 | 86 | 使用 `local` 属性来传入一个指向本地文件的**绝对路径**字符串。Seele 会将指定文件进行拷贝。 87 | 88 | 89 | 当在 Docker 或 Kubernetes 的容器中运行 Seele 时,请确保将相关文件挂载到了容器文件系统中 90 | 91 | 92 | 下面的例子将本地文件 `/tmp/foo.txt` 添加到提交根文件夹中。 93 | 94 | ```yaml 95 | steps: 96 | prepare: 97 | action: "seele/add-files@1" 98 | files: 99 | - path: "foo.txt" 100 | local: "/tmp/foo.txt" 101 | ``` 102 | -------------------------------------------------------------------------------- /docs/pages/tasks/order.en.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from "nextra-theme-docs"; 2 | 3 | # Control Execution 4 | 5 | Sequential tasks can execute their subtasks in a certain order, defined by `steps`. By default, sequential tasks execute each subtask in the order they appear. When a subtask fails and enters the [`FAILED`](/tasks/states) state, the sequential task stops executing and sets the remaining subtask states to `SKIPPED`. To allow users to model more complex task execution flows, Seele provides two attributes to change the default behavior: `when` and `needs`. 6 | 7 | ## `when` 8 | 9 | Accepts a string, with possible values: `true` or `previous.ok`. The default value is `previous.ok`. 10 | 11 | When a subtask in a sequential task fails, Seele checks the `when` value of its successor nodes. By default, `previous.ok` means that the current node will only continue to run if the predecessor node executes successfully. Setting it to `true` allows Seele to execute the current node even if the predecessor node fails. 12 | 13 | 14 | If you have better suggestions for this attribute, feel free to open an issue 15 | in this project's [GitHub repository](https://github.com/darkyzhou/seele). 16 | 17 | 18 | ## `needs` 19 | 20 | Accepts a string specifying the name of the predecessor task as a subtask in the sequential task. 21 | 22 | By default, sequential tasks execute subtasks in the order they are declared. Each subtask's predecessor node is the task immediately preceding it. For example, in the following case, the `one`, `two`, and `three` subtasks will be executed in order, as shown in the diagram below. 23 | 24 | ```yaml 25 | steps: 26 | one: # ... 27 | two: # ... 28 | three: # ... 29 | ``` 30 | 31 | ![Order of the three tasks](/order-1.png) 32 | 33 | By adding `when: one` to the `three` configuration, we change the predecessor node of `three` from `two` to `one`. Now, the execution order of the sequential task changes. It still starts with `one`, but then **concurrently executes** `two` and `three`. Their relationship is shown in the diagram below. 34 | 35 | ![Order of the three tasks when using needs](/order-2.png) 36 | 37 | When used with `when`, `previous.ok` points to the state of its predecessor task, i.e., the task specified by `when`. `when` can only specify the names of tasks declared before it. For example, in the above case, we **cannot** use `when: three` in `two`. 38 | -------------------------------------------------------------------------------- /docs/pages/tasks/order.zh.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from 'nextra-theme-docs' 2 | 3 | # 控制执行 4 | 5 | 顺序任务能够按一定的顺序执行它的子任务,通过 `steps` 定义。默认情况下,顺序任务会按照子任务的前后顺序先后执行每个子任务,当某个子任务执行失败进入 6 | [`FAILED`](/tasks/states) 态时,顺序任务停止执行,并将剩余的子节点的状态设置为 `SKIPPED` 态。为了让用户能够建模一些更复杂的任务执行流程,Seele 提供了两种属性改变上述的行为,它们分别是:`when` 和 `needs`。 7 | 8 | ## `when` 配置 9 | 10 | 接收一个字符串,可选值:`true` 或 `previous.ok`。默认值为 `previous.ok`。 11 | 12 | 当顺序任务中的某个子任务执行失败时,Seele 会检查它的后继节点的 `when` 值。默认情况下,`previous.ok` 表示仅在前驱节点执行成功时继续运行本节点。设置为 `true` 可以让 Seele 即使在前驱节点执行失败时依然执行本节点。 13 | 14 | 15 | 如果你对这个属性有更好的建议,欢迎在本项目的 [GitHub 仓库](https://github.com/darkyzhou/seele)中发表 issue。 16 | 17 | 18 | ## `needs` 配置 19 | 20 | 接收一个字符串,指定在顺序任务中它作为子任务的前驱任务的名称。 21 | 22 | 默认情况下,顺序会按照声明顺序先后执行子任务。每个子任务的前驱节点就是它相邻的前一个任务。 23 | 例如下面的例子中,`one`、`two` 和 `three` 三个子任务会被按顺序执行。它们的关系如下图所示。 24 | 25 | ```yaml 26 | steps: 27 | one: # ... 28 | two: # ... 29 | three: # ... 30 | ``` 31 | 32 | ![Order of the three tasks](/order-1.png) 33 | 34 | 通过在 `three` 的配置中添加 `when: one`,我们将 `three` 的前驱节点从 `two` 改变为 `one`。此时,顺序任务的执行顺序发生了变化,它仍然会先执行 35 | `one`,但之后会**并发执行** `two` 和 `three`。此时它们的关系如下图所示。 36 | 37 | ![Order of the three tasks when using needs](/order-2.png) 38 | 39 | 当搭配 `when` 使用时,`previous.ok` 指向它的前驱任务的状态,也就是 `when` 指定的前驱任务。`when` 40 | 只能指定声明在它之前的任务名称,例如在上面的例子中我们**不能**在 `two` 中使用 `when: three`。 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/pages/tasks/script.zh.mdx: -------------------------------------------------------------------------------- 1 | # 报告脚本 2 | 3 | Seele 允许为评测任务通过 `reporter` 属性指定一个报告脚本。当返回[进度报告和完成报告](/tasks/types)时,评测系统将评测报告作为输入运行报告脚本,并根据脚本的返回值执行相应的操作。目前,Seele 仅支持 JavaScript 语言作为报告脚本。 4 | 5 | `reporter` 属性的参数如下表所示: 6 | 7 | | 属性 | 类型 | 简介 | 8 | | :----------- | :------: | :--------------------------- | 9 | | `javascript` | `string` | 使用 JavaScript 作为报告脚本 | 10 | 11 | 脚本的返回值**必须**符合下表所示的结构: 12 | 13 | | 属性 | 类型 | 可选 | 简介 | 14 | | :-------- | :------: | :----------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------- | 15 | | `report` | `object` | 否 | 额外的报告内容,将被作为评测报告的 `report` 属性 | 16 | | `embeds` | `object` | 是 | 嵌入文件内容的配置,参见[嵌入和上传文件](/tasks/embed-and-upload) | 17 | | `uploads` | `object` | 是 | 上传文件的配置,参见[嵌入和上传文件](/tasks/embed-and-upload)。对于进度报告,脚本返回的 `uploads` 会被忽略,只有当输入完成报告时此属性才有效。 | 18 | 19 | ## JavaScript 报告脚本 20 | 21 | 当报告脚本被运行时,Seele 提供了一个全局变量 `DATA` 指向输入的评测报告对象,为用户提供了各个子任务的状态。用户可以根据 `DATA` 的内容决定脚本的返回值。 22 | 23 | 在下面的脚本例子中,我们检查子任务 `check` 的完成状况来额外返回一个 `grade` 属性代表本次评测任务的分数。 24 | 25 | ```javascript 26 | function getGrade() { 27 | const status = DATA.steps.check.status; 28 | 29 | switch (status) { 30 | case "PENDING": 31 | case "RUNNING": 32 | return null; 33 | case "SKIPPED": 34 | return -1; 35 | case "FAILED": 36 | return 0; 37 | case "SUCCESS": 38 | return 100; 39 | } 40 | } 41 | 42 | return { 43 | report: { 44 | grade: getGrade(), 45 | }, 46 | }; 47 | ``` 48 | 49 | Seele 返回的完成报告如下所示: 50 | 51 | ```jsonc 52 | { 53 | "id": "F7UAO37LMPYQRqLo", 54 | "type": "COMPLETED", 55 | "report_at": "2023-03-26T13:33:10.934345832Z", 56 | "report": { 57 | "grade": 100 58 | }, 59 | "status": { 60 | // ... 61 | } 62 | } 63 | ``` 64 | 65 | Seele 使用 [QuickJS](https://bellard.org/quickjs/) 引擎执行 JavaScript 报告脚本。此引擎基本支持较新的 ES2020 规范,对 `Date`、`Math`、`JSON` 等基础 API 提供了完整支持。JavaScript 66 | 报告脚本存在包括但不局限于以下列出的限制: 67 | 68 | - Seele 并没有接入异步功能,像 `Promise` 这样的异步 API 不会起作用。 69 | - QuickJS 引擎没有提供 Web API,例如 `fetch()`。 70 | - 无法访问文件系统、网络。 71 | -------------------------------------------------------------------------------- /docs/pages/tasks/states.en.mdx: -------------------------------------------------------------------------------- 1 | # Task States 2 | 3 | The execution states of sequential tasks, concurrent tasks, and action tasks are shown in the diagram below. 4 | 5 | - Each task is in the `PENDING` state after it is created, indicating that it is waiting for execution. 6 | - When a task cannot be executed due to the failure of its predecessor node or the relationship specified by [`when`](/tasks/order) is not satisfied, it will be set to the `SKIPPED` state. 7 | - When the task is completed, it will be set to the `SUCCESS` state if the execution is successful, otherwise, it will be set to the `FAILED` state. 8 | 9 | ![States](/states.png) 10 | -------------------------------------------------------------------------------- /docs/pages/tasks/states.zh.mdx: -------------------------------------------------------------------------------- 1 | # 任务状态 2 | 3 | 顺序任务、并发任务和动作任务的执行状态如下图所示。 4 | 5 | * 每个任务被创建后处于 `PENDING` 状态表示等待执行。 6 | * 当任务因为前驱节点失败或通过 [`when`](/tasks/order) 指定的关系不满足,导致无法执行时,它会被设置为 `SKIPPED` 态。 7 | * 当任务执行完毕后,若执行成功则会被设置为 `SUCCESS` 态,否则设置为 `FAILED` 态。 8 | 9 | ![States](/states.png) 10 | -------------------------------------------------------------------------------- /docs/pages/tasks/tags.en.mdx: -------------------------------------------------------------------------------- 1 | # Tags 2 | 3 | To meet the needs of some scenarios, we can attach tags to subtasks in the judge tasks using `tags`. These tags will be preserved as-is in the corresponding judge reports. `tags` accepts a dictionary with string key-value pairs. 4 | 5 | In the example below, we run a task to add a file and use `tags` to attach some tags. 6 | 7 | ```yaml 8 | steps: 9 | prepare: 10 | tags: 11 | foo: "bar" 12 | message: "hello" 13 | action: "seele/add-file@1" 14 | files: 15 | - path: "main.py" 16 | plain: | 17 | print(f"{1/0}") 18 | ``` 19 | 20 | Seele will preserve the tags we provided in the corresponding judge reports (including progress reports and completed reports), as shown below: 21 | 22 | ```json 23 | { 24 | "id": "QqmPeQrUFsrLAULA", 25 | "type": "COMPLETED", 26 | "report_at": "2023-03-26T03:29:15.388936921Z", 27 | "status": { 28 | "submitted_at": "2023-03-26T03:29:15.387603442Z", 29 | "id": "QqmPeQrUFsrLAULA", 30 | "steps": { 31 | "prepare": { 32 | "tags": { 33 | "foo": "bar", 34 | "message": "hello" 35 | }, 36 | "status": "SUCCESS", 37 | "report": { 38 | "run_at": "2023-03-26T03:29:15.388262860Z", 39 | "time_elapsed_ms": 0, 40 | "type": "add_file" 41 | }, 42 | "embeds": {} 43 | } 44 | } 45 | } 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/pages/tasks/tags.zh.mdx: -------------------------------------------------------------------------------- 1 | # 标签 2 | 3 | 为了满足一些场景的需要,我们可以在评测任务中通过 `tags` 向子任务附加标签。这些标签会被原样保留在对应的评测报告中。`tags` 接受一个字典,键值为字符串。 4 | 5 | 在下面的例子中,我们运行了一个添加文件的任务并使用 `tags` 附加了一些标签。 6 | 7 | ```yaml 8 | steps: 9 | prepare: 10 | tags: 11 | foo: "bar" 12 | message: "hello" 13 | action: "seele/add-file@1" 14 | files: 15 | - path: "main.py" 16 | plain: | 17 | print(f"{1/0}") 18 | ``` 19 | 20 | Seele 会在对应的评测报告(包括进度报告和完成报告)中保留我们提供的标签,如下所示: 21 | 22 | ```json 23 | { 24 | "id": "QqmPeQrUFsrLAULA", 25 | "type": "COMPLETED", 26 | "report_at": "2023-03-26T03:29:15.388936921Z", 27 | "status": { 28 | "submitted_at": "2023-03-26T03:29:15.387603442Z", 29 | "id": "QqmPeQrUFsrLAULA", 30 | "steps": { 31 | "prepare": { 32 | "tags": { 33 | "foo": "bar", 34 | "message": "hello" 35 | }, 36 | "status": "SUCCESS", 37 | "report": { 38 | "run_at": "2023-03-26T03:29:15.388262860Z", 39 | "time_elapsed_ms": 0, 40 | "type": "add_file" 41 | }, 42 | "embeds": {} 43 | } 44 | } 45 | } 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/pages/tasks/types.en.mdx: -------------------------------------------------------------------------------- 1 | # Task Types 2 | 3 | A judge task consists of several subtasks, which are divided into three types: sequential tasks, concurrent tasks, and action tasks. Sequential tasks and concurrent tasks can nest other subtasks, expressing complex judge processes. Action tasks perform actual operations, such as adding files, running containers, etc. If we compare a judge task to a multi-branch tree, then sequential tasks and concurrent tasks are the internal nodes, and action tasks are the leaf nodes. 4 | 5 | ## Sequential Tasks 6 | 7 | The purpose of a sequential task is to execute its subtasks in a certain order, defined by `steps`. The root node of each judge task is a sequential task. In the example below, the sequential task will execute the `one`, `two`, and `three` subtasks in order. 8 | 9 | ```yaml 10 | steps: 11 | one: # ... 12 | two: # ... 13 | three: #... 14 | ``` 15 | 16 | ## Concurrent Tasks 17 | 18 | The purpose of a concurrent task is to execute its subtasks concurrently, defined by `parallel`. A concurrent task always waits for all subtasks to complete, regardless of whether any subtasks fail. 19 | 20 | In the example below, the concurrent task will execute the `one`, `two`, and `three` subtasks concurrently. 21 | 22 | ```yaml 23 | parallel: 24 | - # ... 25 | - # ... 26 | - #... 27 | ``` 28 | 29 | The parameters of a concurrent task can also be a dictionary. In this case, the role of the key is similar to that of a sequential task, assigning a name to each subtask: 30 | 31 | ```yaml 32 | parallel: 33 | one: # ... 34 | two: # ... 35 | three: # ... 36 | ``` 37 | 38 | ## Action Tasks 39 | 40 | Action tasks can perform operations such as adding files, running judge programs, etc., defined by `action`. In the example below, the action task will perform the operation of adding a file to the root folder of the judge task. 41 | 42 | ```yaml 43 | action: "seele/add-file@1" 44 | # ... 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/pages/tasks/types.zh.mdx: -------------------------------------------------------------------------------- 1 | # 任务类型 2 | 3 | 一个评测任务由若干个子任务构成,这些子任务被分为三种类型:顺序任务、并发任务和动作任务。顺序任务和并发任务能够嵌套其它子任务,表达复杂的评测流程。动作任务执行实际的操作,如添加文件、运行容器等。如果将评测任务比作一棵多叉树,那么顺序任务和并发就是其中的内部节点,动作任务就是其中的叶子节点。 4 | 5 | ## 顺序任务 6 | 7 | 顺序任务的作用是按一定的顺序执行它的子任务,通过 `steps` 定义。每个评测任务的根节点就是一个顺序任务。在下面的例子中,顺序任务会按顺序执行 `one`、`two` 和 `three` 三个子任务。 8 | 9 | ```yaml 10 | steps: 11 | one: # ... 12 | two: # ... 13 | three: #... 14 | ``` 15 | 16 | ## 并发任务 17 | 18 | 并发任务的作用是并发执行它的子任务,通过 `parallel` 定义。并发任务总是会等待所有子任务执行完成,无论是否有子任务执行失败。 19 | 20 | 在下面的例子中,并发任务会并发执行 `one`、`two` 和 `three` 三个子任务。 21 | 22 | ```yaml 23 | parallel: 24 | - # ... 25 | - # ... 26 | - #... 27 | ``` 28 | 29 | 并发任务的参数也可以是一个字典,此时键的作用和顺序任务类似,同样是给每个子任务赋予名称: 30 | 31 | ```yaml 32 | parallel: 33 | one: # ... 34 | two: # ... 35 | three: # ... 36 | ``` 37 | 38 | ## 动作任务 39 | 40 | 动作任务能够执行添加文件、运行评测程序等操作,通过 `action` 定义。在下面的例子中,动作任务会执行添加文件的操作,向评测任务的根文件夹中添加文件。 41 | 42 | ```yaml 43 | action: "seele/add-file@1" 44 | # ... 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/architecture.jpg -------------------------------------------------------------------------------- /docs/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/favicon-32x32.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/grafana.png -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/public/order-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/order-1.png -------------------------------------------------------------------------------- /docs/public/order-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/order-2.png -------------------------------------------------------------------------------- /docs/public/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/report.png -------------------------------------------------------------------------------- /docs/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Seele Docs", 3 | "short_name": "Seele Docs", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#171717", 17 | "background_color": "#171717", 18 | "display": "standalone" 19 | } -------------------------------------------------------------------------------- /docs/public/states.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/states.png -------------------------------------------------------------------------------- /docs/public/tempo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/docs/public/tempo.png -------------------------------------------------------------------------------- /docs/theme.config.jsx: -------------------------------------------------------------------------------- 1 | export default { 2 | docsRepositoryBase: "https://github.com/darkyzhou/seele/tree/main/docs", 3 | useNextSeoProps() { 4 | return { 5 | titleTemplate: "%s - Seele Docs", 6 | }; 7 | }, 8 | head: ( 9 | <> 10 | 15 | 21 | 27 | 28 | 29 | ), 30 | logo: ( 31 |
32 | 33 | Seele 34 |
35 | ), 36 | project: { 37 | link: "https://github.com/darkyzhou/seele", 38 | }, 39 | search: { 40 | component: null, 41 | }, 42 | i18n: [ 43 | { locale: "en", text: "English" }, 44 | { locale: "zh", text: "中文" }, 45 | ], 46 | footer: { 47 | text: ( 48 |
49 | Made with ❤️ by{" "} 50 | 56 | darkyzhou 57 | 58 |
59 | ), 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /docs/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true, 4 | "deploymentEnabled": { 5 | "main": false 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /images/diff-scripts/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-slim 2 | 3 | LABEL org.opencontainers.image.source="https://github.com/darkyzhou/seele" \ 4 | org.opencontainers.image.url="https://github.com/darkyzhou/seele" \ 5 | org.opencontainers.image.title="A utility image for seele" 6 | 7 | ADD . /usr/local/bin 8 | RUN chmod +x /usr/local/bin/diff-* -------------------------------------------------------------------------------- /images/diff-scripts/diff-loose: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Loose diff 4 | # 5 | # Ignores extra spaces and empty lines at the end 6 | # 7 | # Conventions: 8 | # - the output from user's program should be mounted as file `./user.out` 9 | # - the standard output (aka. correct answer) should be mounted as `./standard.out` 10 | 11 | diff --strip-trailing-cr --ignore-trailing-space --ignore-space-change --ignore-blank-lines user.out standard.out >/dev/null 2>&1 12 | 13 | EXITCODE=$? 14 | [[ $EXITCODE -gt 1 ]] && exit 1 # Unexpected internal error 15 | [[ $EXITCODE -ne 0 ]] && exit 8 # Wrong answer 16 | 17 | exit 0 # Accepted 18 | -------------------------------------------------------------------------------- /images/diff-scripts/diff-strict: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Strict diff 4 | # 5 | # Returns `representation error` if the differences only includes extra spaces or empty lines at the end 6 | # 7 | # Conventions: 8 | # - the output from user's program should be mounted as file `./user.out` 9 | # - the standard output (aka. correct answer) should be mounted as `./standard.out` 10 | 11 | diff --strip-trailing-cr --ignore-trailing-space --ignore-space-change --ignore-blank-lines user.out standard.out >/dev/null 2>&1 12 | EXITCODE=$? 13 | [[ $EXITCODE -gt 1 ]] && exit 1 # Unexpected internal error 14 | [[ $EXITCODE -ne 0 ]] && exit 8 # Wrong answer 15 | 16 | diff --strip-trailing-cr user.out standard.out >/dev/null 2>&1 17 | EXITCODE=$? 18 | [[ $EXITCODE -gt 1 ]] && exit 1 # Unexpected internal error 19 | [[ $EXITCODE -ne 0 ]] && exit 9 # Representation error 20 | 21 | exit 0 # Accepted 22 | -------------------------------------------------------------------------------- /runj/.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | tests/image 3 | tests/runj-* 4 | -------------------------------------------------------------------------------- /runj/Makefile: -------------------------------------------------------------------------------- 1 | PKG := runj 2 | BIN_DIR := ./bin 3 | 4 | BIN_NAME := runj 5 | BIN_PATH := "$(BIN_DIR)/$(BIN_NAME)" 6 | MAIN_ENTRYPOINT := cmd/runj/main.go 7 | 8 | GO_VERSION := $(shell go version | cut -d ' ' -f 3) 9 | GO_LDFLAGS += -X "$(PKG)/seele.GoVersion=$(GO_VERSION)" 10 | GO_LDFLAGS += -X "$(PKG)/seele.BuildTime=$(shell date -u '+%Y-%m-%d_%H:%M:%S%Z')" 11 | 12 | lint: 13 | @echo "Linting..." 14 | @golangci-lint run 15 | 16 | test-integration: build-debug 17 | @echo "Running integration tests..." 18 | @cd tests && npm install && npm run test 19 | 20 | test-unit: build-debug 21 | @echo "Running unit tests..." 22 | @go test -v ./... 23 | 24 | build: clean 25 | @echo "Building runj..." 26 | @CGO_ENABLED=1 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -ldflags '$(GO_LDFLAGS)' -o $(BIN_PATH) $(MAIN_ENTRYPOINT) 27 | @echo "Successfully built into $(BIN_PATH)" 28 | 29 | build-debug: clean 30 | @echo "Building runj (debug)..." 31 | @CGO_ENABLED=1 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -tags dev -ldflags '$(GO_LDFLAGS)' -o $(BIN_PATH) $(MAIN_ENTRYPOINT) 32 | @go mod tidy 33 | @echo "Successfully built into $(BIN_PATH)" 34 | 35 | clean: 36 | @echo "Cleaning..." 37 | @go clean 38 | @rm -f $(BIN_PATH) 39 | -------------------------------------------------------------------------------- /runj/README.md: -------------------------------------------------------------------------------- 1 | ## Runj 2 | 3 | ### Prerequisites 4 | 5 | > Using ArchLinux is recommended :) 6 | 7 | * Make sure your distribution provides both `newuidmap` and `newgidmap` binaries. 8 | * Install [skopeo](https://github.com/containers/skopeo) and [umoci](https://github.com/opencontainers/umoci). 9 | * Install Node.js 20 and Go 1.21. 10 | * Refer to [https://github.com/opencontainers/runc/blob/main/docs/cgroup-v2.md](https://github.com/opencontainers/runc/blob/main/docs/cgroup-v2.md). 11 | * Make sure you have a normal user with UID `1000` and GID `1000` as well as `/etc/subuid` like `seele:100000:65536` and `/etc/subgid` like `seele:100000:65536`. 12 | * Reboot your system if you have installed some new packages or changed systemd settings. 13 | * Run `skopeo copy docker://gcc:11-bullseye oci:/tmp/_tmp_gcc:11-bullseye` 14 | * Run `umoci unpack --rootless --image /tmp/_tmp_gcc:11-bullseye ./tests/image` 15 | 16 | ### Run Unit Tests 17 | 18 | `make test-unit` 19 | 20 | ### Run Integration Tests 21 | 22 | `make test-integration` 23 | -------------------------------------------------------------------------------- /runj/cmd/runj/cgroup/cgroupfs.go: -------------------------------------------------------------------------------- 1 | package cgroup 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "github.com/darkyzhou/seele/runj/cmd/runj/utils" 9 | ) 10 | 11 | // Initialize a new cgroup v2 directory via cgroupfs. 12 | // Mainly used for containerized environments with the help of sysbox. 13 | func GetCgroupPathViaFs(parentCgroupPath string) (string, error) { 14 | cgroupPath := path.Join(parentCgroupPath, fmt.Sprintf("runj-container-%s", utils.RunjInstanceId)) 15 | 16 | if err := os.Mkdir(cgroupPath, 0775); err != nil { 17 | return "", fmt.Errorf("Failed to create cgroup directory: %w", err) 18 | } 19 | 20 | return cgroupPath, nil 21 | } 22 | -------------------------------------------------------------------------------- /runj/cmd/runj/cgroup/utils.go: -------------------------------------------------------------------------------- 1 | package cgroup 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/opencontainers/runc/libcontainer/cgroups" 8 | "github.com/opencontainers/runc/libcontainer/cgroups/fs2" 9 | "github.com/samber/lo" 10 | ) 11 | 12 | var mandatoryControllers = []string{"cpu", "cpuset", "io", "memory", "pids"} 13 | 14 | func checkSupportedControllers() error { 15 | controllers, err := cgroups.ReadFile(fs2.UnifiedMountpoint, "/cgroup.controllers") 16 | if err != nil { 17 | return fmt.Errorf("Error reading cgroup.controllers: %w", err) 18 | } 19 | 20 | if supported := lo.Intersect(strings.Fields(controllers), mandatoryControllers); len(supported) < len(mandatoryControllers) { 21 | return fmt.Errorf("Missing some cgroup controllers, available controllers: %s", controllers) 22 | } 23 | 24 | return nil 25 | } 26 | 27 | func enableMandatoryControllers(path string) error { 28 | for _, controller := range mandatoryControllers { 29 | if err := cgroups.WriteFile(path, "cgroup.subtree_control", "+"+controller); err != nil { 30 | return fmt.Errorf("Failed to enable %s controller via cgroup.subtree_control: %w", controller, err) 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /runj/cmd/runj/entities/config.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | type RunjConfig struct { 4 | UserNamespace *UserNamespaceConfig `mapstructure:"user_namespace"` 5 | Overlayfs *OverlayfsConfig `mapstructure:"overlayfs" validate:"required"` 6 | CgroupPath string `mapstructure:"cgroup_path"` 7 | Cwd string `mapstructure:"cwd" validate:"required"` 8 | Command []string `mapstructure:"command" validate:"required,dive,required"` 9 | Paths []string `mapstructure:"paths" validate:"dive,required"` 10 | Fd *FdConfig `mapstructure:"fd"` 11 | Mounts []*MountConfig `mapstructure:"mounts"` 12 | Limits *LimitsConfig `mapstructure:"limits" validate:"required"` 13 | } 14 | 15 | type UserNamespaceConfig struct { 16 | Enabled bool `mapstructure:"enabled"` 17 | RootUid uint32 `mapstructure:"root_uid" validate:"required"` 18 | UidMapBegin uint32 `mapstructure:"uid_map_begin" validate:"required"` 19 | UidMapCount uint32 `mapstructure:"uid_map_count" validate:"required"` 20 | RootGid uint32 `mapstructure:"root_gid" validate:"required"` 21 | GidMapBegin uint32 `mapstructure:"gid_map_begin" validate:"required"` 22 | GidMapCount uint32 `mapstructure:"gid_map_count" validate:"required"` 23 | } 24 | 25 | type OverlayfsConfig struct { 26 | LowerDirectory string `mapstructure:"lower_dir" validate:"required"` 27 | UpperDirectory string `mapstructure:"upper_dir" validate:"required"` 28 | WorkDirectory string `mapstructure:"work_dir" validate:"required"` 29 | MergedDirectory string `mapstructure:"merged_dir" validate:"required"` 30 | } 31 | 32 | type FdConfig struct { 33 | StdIn string `mapstructure:"stdin"` 34 | StdOut string `mapstructure:"stdout"` 35 | StdErr string `mapstructure:"stderr"` 36 | StdOutToStdErr bool `mapstructure:"stdout_to_stderr"` 37 | StdErrToStdOut bool `mapstructure:"stderr_to_stdout"` 38 | } 39 | 40 | type MountConfig struct { 41 | From string `mapstructure:"from" validate:"required"` 42 | To string `mapstructure:"to" validate:"required"` 43 | Options []string `mapstructure:"options"` 44 | } 45 | 46 | type LimitsConfig struct { 47 | TimeMs uint64 `mapstructure:"time_ms" validate:"required"` 48 | Cgroup *CgroupConfig `mapstructure:"cgroup" validate:"required"` 49 | Rlimit *RlimitConfig `mapstructure:"rlimit" validate:"required"` 50 | } 51 | 52 | type CgroupConfig struct { 53 | CpuShares uint64 `mapstructure:"cpu_shares"` 54 | CpuQuota int64 `mapstructure:"cpu_quota"` 55 | CpusetCpus string `mapstructure:"cpuset_cpus"` 56 | CpusetMems string `mapstructure:"cpuset_mems"` 57 | Memory int64 `mapstructure:"memory" validate:"required"` 58 | PidsLimit int64 `mapstructure:"pids_limit" validate:"required"` 59 | } 60 | 61 | type RlimitConfig struct { 62 | Core *RlimitItem `mapstructure:"core" validate:"required"` 63 | Fsize *RlimitItem `mapstructure:"fsize" validate:"required"` 64 | NoFile *RlimitItem `mapstructure:"no_file" validate:"required"` 65 | } 66 | 67 | type RlimitItem struct { 68 | Hard uint64 `mapstructure:"hard"` 69 | Soft uint64 `mapstructure:"soft"` 70 | } 71 | -------------------------------------------------------------------------------- /runj/cmd/runj/entities/report.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | type ExecutionReport struct { 4 | Status string `json:"status"` 5 | ExitCode int `json:"exit_code"` 6 | Signal string `json:"signal,omitempty"` 7 | WallTimeMs uint64 `json:"wall_time_ms"` 8 | CpuUserTimeMs uint64 `json:"cpu_user_time_ms"` 9 | CpuKernelTimeMs uint64 `json:"cpu_kernel_time_ms"` 10 | MemoryUsageKiB uint64 `json:"memory_usage_kib"` 11 | } 12 | -------------------------------------------------------------------------------- /runj/cmd/runj/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "os/signal" 10 | "runtime" 11 | "strings" 12 | "syscall" 13 | 14 | "github.com/darkyzhou/seele/runj/cmd/runj/entities" 15 | "github.com/darkyzhou/seele/runj/cmd/runj/execute" 16 | "github.com/darkyzhou/seele/runj/cmd/runj/utils" 17 | "github.com/go-playground/validator/v10" 18 | "github.com/mitchellh/mapstructure" 19 | "github.com/opencontainers/runc/libcontainer" 20 | _ "github.com/opencontainers/runc/libcontainer/nsenter" 21 | "github.com/sirupsen/logrus" 22 | ) 23 | 24 | func init() { 25 | runtime.GOMAXPROCS(1) 26 | 27 | if len(os.Args) > 2 && os.Args[1] == "init" { 28 | runtime.LockOSThread() 29 | 30 | if err := utils.SetupOverlayfs(); err != nil { 31 | // FIXME: Find a way to pass the error message 32 | os.Exit(1) 33 | } 34 | 35 | factory, _ := libcontainer.New("") 36 | if err := factory.StartInitialization(); err != nil { 37 | os.Exit(1) 38 | } 39 | 40 | panic("Libcontainer failed to init") 41 | } 42 | 43 | if os.Getenv("RUNJ_DEBUG") != "" { 44 | logrus.SetLevel(logrus.DebugLevel) 45 | logrus.SetOutput(os.Stdout) 46 | } else { 47 | logrus.SetLevel(logrus.FatalLevel) 48 | logrus.SetOutput(os.Stderr) 49 | } 50 | } 51 | 52 | func main() { 53 | var input string 54 | 55 | inputFile := os.Getenv("RUNJ_FILE") 56 | if inputFile == "" { 57 | scanner := bufio.NewScanner(os.Stdin) 58 | 59 | builder := strings.Builder{} 60 | for scanner.Scan() { 61 | builder.WriteString(scanner.Text()) 62 | } 63 | if err := scanner.Err(); err != nil { 64 | logrus.WithError(err).Fatal("Error reading from stdin") 65 | } 66 | 67 | input = builder.String() 68 | } else { 69 | data, err := os.ReadFile(inputFile) 70 | if err != nil { 71 | logrus.WithError(err).Fatalf("Error reading input file: %s", inputFile) 72 | } 73 | input = string(data) 74 | } 75 | 76 | var payload map[string]interface{} 77 | if err := json.Unmarshal([]byte(input), &payload); err != nil { 78 | logrus.WithError(err).Fatal("Error unmarshalling the input") 79 | } 80 | 81 | var config entities.RunjConfig 82 | if err := mapstructure.Decode(payload, &config); err != nil { 83 | logrus.WithError(err).Fatal("Error unmarshalling the input") 84 | } 85 | 86 | validate := validator.New() 87 | if err := validate.Struct(config); err != nil { 88 | logrus.WithError(err).Fatal("Invalid config") 89 | } 90 | 91 | ctx, cancel := context.WithCancel(context.Background()) 92 | defer cancel() 93 | 94 | sigs := make(chan os.Signal, 1) 95 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 96 | go func() { 97 | <-sigs 98 | cancel() 99 | }() 100 | 101 | report, err := execute.Execute(ctx, &config) 102 | if err != nil { 103 | logrus.WithError(err).Fatal("Error executing the container") 104 | } 105 | 106 | output, err := json.Marshal(report) 107 | if err != nil { 108 | logrus.WithError(err).Fatal("Error marshalling the report") 109 | } 110 | fmt.Println(string(output)) 111 | } 112 | -------------------------------------------------------------------------------- /runj/cmd/runj/utils/overlayfs.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/darkyzhou/seele/runj/cmd/runj/entities" 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func SetupOverlayfs() error { 13 | configJson := os.Args[2] 14 | if configJson == "" { 15 | return fmt.Errorf("Unexpected empty overlayfs config") 16 | } 17 | 18 | var config entities.OverlayfsConfig 19 | if err := json.Unmarshal([]byte(configJson), &config); err != nil { 20 | return fmt.Errorf("Error deserializing the overlayfs config: %w", err) 21 | } 22 | 23 | // * `userxattr` is mandatory when using overlayfs with user namespace. 24 | // Note that when using tmpfs directories as upperdirs, overlayfs will complain 'failed to set xattr on upper' 25 | // because tmpfs does NOT support user extended attributes(user xattrs). 26 | // * `xino=off` is used to prevent overlayfs from complaining that some filesystem 'does not support file handles' 27 | // which seems to because runj lacks CAP_DAC_READ_SEARCH. 28 | // * `index=off` is used as Moby also sets it. 29 | options := fmt.Sprintf("userxattr,xino=off,index=off,lowerdir=%s,upperdir=%s,workdir=%s", config.LowerDirectory, config.UpperDirectory, config.WorkDirectory) 30 | if err := unix.Mount("overlay", config.MergedDirectory, "overlay", 0, options); err != nil { 31 | return fmt.Errorf("Error creating overlayfs mount: %w", err) 32 | } 33 | 34 | _ = os.Setenv("GOMAXPROCS", "1") 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /runj/cmd/runj/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/fs" 7 | "os" 8 | 9 | gonanoid "github.com/matoous/go-nanoid/v2" 10 | ) 11 | 12 | var RunjInstanceId = gonanoid.MustGenerate("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 12) 13 | 14 | // Returns true if the specified file exists and is actually a file (not a directory) 15 | func FileExists(path string) bool { 16 | info, err := os.Stat(path) 17 | if os.IsNotExist(err) { 18 | return false 19 | } 20 | 21 | return !info.IsDir() 22 | } 23 | 24 | // Returns true if the specified directory exists and is actually a directory (not a file) 25 | func DirectoryExists(path string) bool { 26 | info, err := os.Stat(path) 27 | if os.IsNotExist(err) { 28 | return false 29 | } 30 | 31 | return info.IsDir() 32 | } 33 | 34 | func CheckPermission(path string, bits fs.FileMode) error { 35 | info, err := os.Stat(path) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | mode := info.Mode() 41 | if mode&bits != bits { 42 | return fmt.Errorf("Insufficient permissions, want at least %b", bits) 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func DirectoryEmpty(path string) (bool, error) { 49 | dir, err := os.Open(path) 50 | if err != nil { 51 | return false, err 52 | } 53 | defer dir.Close() 54 | 55 | _, err = dir.Readdirnames(1) 56 | if err == io.EOF { 57 | return true, nil 58 | } 59 | return false, err 60 | } 61 | -------------------------------------------------------------------------------- /runj/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/darkyzhou/seele/runj 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/coreos/go-systemd/v22 v22.5.0 7 | github.com/cyphar/filepath-securejoin v0.4.1 8 | github.com/go-playground/validator/v10 v10.24.0 9 | github.com/godbus/dbus/v5 v5.1.0 10 | github.com/matoous/go-nanoid/v2 v2.1.0 11 | github.com/mitchellh/mapstructure v1.5.0 12 | 13 | // can not upgrade to runc v1.2+ as LinuxFactory is removed 14 | // https://github.com/opencontainers/runc/commit/6a3fe1618f5166e5c44f21714736049bac9c02cb 15 | github.com/opencontainers/runc v1.1.15 16 | github.com/opencontainers/runtime-spec v1.2.0 17 | github.com/samber/lo v1.49.1 18 | github.com/sirupsen/logrus v1.9.3 19 | golang.org/x/sys v0.29.0 20 | ) 21 | 22 | require ( 23 | github.com/checkpoint-restore/go-criu/v5 v5.3.0 // indirect 24 | github.com/cilium/ebpf v0.12.3 // indirect 25 | github.com/containerd/console v1.0.4 // indirect 26 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 27 | github.com/go-playground/locales v0.14.1 // indirect 28 | github.com/go-playground/universal-translator v0.18.1 // indirect 29 | github.com/leodido/go-urn v1.4.0 // indirect 30 | github.com/moby/sys/mountinfo v0.7.2 // indirect 31 | github.com/mrunalp/fileutils v0.5.1 // indirect 32 | github.com/opencontainers/selinux v1.11.1 // indirect 33 | github.com/seccomp/libseccomp-golang v0.10.0 // indirect 34 | github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect 35 | github.com/vishvananda/netlink v1.3.0 // indirect 36 | github.com/vishvananda/netns v0.0.5 // indirect 37 | golang.org/x/crypto v0.32.0 // indirect 38 | golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect 39 | golang.org/x/net v0.34.0 // indirect 40 | golang.org/x/text v0.21.0 // indirect 41 | google.golang.org/protobuf v1.36.4 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /runj/tests/go.mod: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /runj/tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runj-tests", 3 | "scripts": { 4 | "test": "node run.mjs" 5 | }, 6 | "dependencies": { 7 | "lodash-es": "^4.17.21" 8 | } 9 | } -------------------------------------------------------------------------------- /runj/tests/stubs/16g/main.c: -------------------------------------------------------------------------------- 1 | // https://codeexplainer.wordpress.com/2018/01/20/how-dismantle-compiler-bomb/ 2 | main[-1u]={1}; -------------------------------------------------------------------------------- /runj/tests/stubs/16g/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/seele", 8 | command: ["gcc", "main.c"], 9 | mounts: [ 10 | { 11 | from: "$TEMP_PATH", 12 | to: "/seele", 13 | options: ["rw"], 14 | }, 15 | { 16 | from: `${resolve(fileURLToPath(import.meta.url), "../main.c")}`, 17 | to: "/seele/main.c", 18 | }, 19 | ], 20 | limits: { 21 | time_ms: 10000, 22 | rlimit: { 23 | fsize: { 24 | hard: 102400, 25 | soft: 102400, 26 | }, 27 | }, 28 | }, 29 | }, 30 | check: (report) => { 31 | // Gcc will handle SIGXFSZ and exits with code 4 32 | assert.strictEqual(report.status, "RUNTIME_ERROR"); 33 | assert.strictEqual(report.exit_code, 4); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /runj/tests/stubs/bigexe.c/main.c: -------------------------------------------------------------------------------- 1 | // https://github.com/vijos/malicious-code 2 | #include 3 | 4 | char magic[1024 * 1024 * 1024] = { '\n' }; 5 | 6 | int main() 7 | { 8 | printf("hello, world"); 9 | printf(magic); 10 | return 0; 11 | } -------------------------------------------------------------------------------- /runj/tests/stubs/bigexe.c/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/seele", 8 | command: ["gcc", "main.c"], 9 | mounts: [ 10 | { 11 | from: "$TEMP_PATH", 12 | to: "/seele", 13 | options: ["rw"], 14 | }, 15 | { 16 | from: `${resolve(fileURLToPath(import.meta.url), "../main.c")}`, 17 | to: "/seele/main.c", 18 | }, 19 | ], 20 | limits: { 21 | time_ms: 10000, 22 | rlimit: { 23 | fsize: { 24 | hard: 102400, 25 | soft: 102400, 26 | }, 27 | }, 28 | }, 29 | }, 30 | check: (report) => { 31 | // Gcc will handle SIGXFSZ and exits with code 4 32 | assert.strictEqual(report.status, "RUNTIME_ERROR"); 33 | assert.strictEqual(report.exit_code, 4); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /runj/tests/stubs/ctle/main.cpp: -------------------------------------------------------------------------------- 1 | // https://github.com/vijos/malicious-code 2 | #ifdef _WIN32 3 | #include 4 | #else 5 | #include 6 | #include 7 | #endif 8 | -------------------------------------------------------------------------------- /runj/tests/stubs/ctle/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/seele", 8 | command: ["g++", "main.cpp"], 9 | mounts: [ 10 | { 11 | from: "$TEMP_PATH", 12 | to: "/seele", 13 | options: ["rw"], 14 | }, 15 | { 16 | from: `${resolve(fileURLToPath(import.meta.url), "../main.cpp")}`, 17 | to: "/seele/main.cpp", 18 | }, 19 | ], 20 | limits: { 21 | time_ms: 10000, 22 | cgroup: { 23 | memory: 32 * 1024 * 1024, // 32 MiB 24 | }, 25 | }, 26 | }, 27 | check: (report) => { 28 | assert.strictEqual(report.status, "MEMORY_LIMIT_EXCEEDED"); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /runj/tests/stubs/ctle2/main.cpp: -------------------------------------------------------------------------------- 1 | // https://github.com/vijos/malicious-code 2 | struct x struct zv { 25 | assert.strictEqual(report.status, "USER_TIME_LIMIT_EXCEEDED"); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /runj/tests/stubs/dead_loop_printf/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/dead_loop_printf/main -------------------------------------------------------------------------------- /runj/tests/stubs/dead_loop_printf/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | for { 7 | fmt.Print("希儿世界第一可爱") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /runj/tests/stubs/dead_loop_printf/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | time_ms: 300, 18 | rlimit: { 19 | fsize: { 20 | hard: 1024, 21 | soft: 1024, 22 | }, 23 | }, 24 | }, 25 | fd: { 26 | stdout: "$TEMP_PATH/stdout.txt", 27 | }, 28 | }, 29 | check: (report) => { 30 | assert.strictEqual(report.status, "OUTPUT_LIMIT_EXCEEDED"); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /runj/tests/stubs/divide_by_zero/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/divide_by_zero/main -------------------------------------------------------------------------------- /runj/tests/stubs/divide_by_zero/main.c: -------------------------------------------------------------------------------- 1 | int main(void) { 2 | return 1 / 0; 3 | } -------------------------------------------------------------------------------- /runj/tests/stubs/divide_by_zero/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | }, 17 | check: (report) => { 18 | assert.strictEqual(report.status, "SIGNAL_TERMINATE"); 19 | assert.strictEqual(report.exit_code, 136); 20 | assert.strictEqual(report.signal, "SIGFPE"); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /runj/tests/stubs/fork/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/fork/main -------------------------------------------------------------------------------- /runj/tests/stubs/fork/main.c: -------------------------------------------------------------------------------- 1 | // https://www.geeksforgeeks.org/fork-bomb/ 2 | 3 | // C program Sample for FORK BOMB 4 | // It is not recommended to run the program as 5 | // it may make a system non-responsive. 6 | #include 7 | #include 8 | 9 | int main() 10 | { 11 | while(1) 12 | fork(); 13 | return 0; 14 | } 15 | -------------------------------------------------------------------------------- /runj/tests/stubs/fork/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | time_ms: 2000, 18 | cgroup: { 19 | pids_limit: 8, 20 | }, 21 | }, 22 | }, 23 | check: (report) => { 24 | assert.strictEqual(report.status, "MEMORY_LIMIT_EXCEEDED"); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /runj/tests/stubs/include_self/main.c: -------------------------------------------------------------------------------- 1 | // https://github.com/vijos/malicious-code 2 | #include __FILE__ 3 | #include __FILE__ -------------------------------------------------------------------------------- /runj/tests/stubs/include_self/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/seele", 8 | command: ["gcc", "main.c"], 9 | mounts: [ 10 | { 11 | from: "$TEMP_PATH", 12 | to: "/seele", 13 | options: ["rw"], 14 | }, 15 | { 16 | from: `${resolve(fileURLToPath(import.meta.url), "../main.c")}`, 17 | to: "/seele/main.c", 18 | }, 19 | ], 20 | limits: { 21 | time_ms: 1000, 22 | cgroup: { 23 | memory: 32 * 1024 * 1024, 24 | }, 25 | }, 26 | }, 27 | check: (report) => { 28 | assert.strictEqual(report.status, "USER_TIME_LIMIT_EXCEEDED"); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /runj/tests/stubs/macro/main.c: -------------------------------------------------------------------------------- 1 | // https://github.com/vijos/malicious-code 2 | 3 | // 使用 gcc / g++ 编译会吃掉大量内存 且会超时 4 | int f(int num) { 5 | return num; 6 | } 7 | #define TEN(i, t) (t[++i] = f(i), t[++i] = f(i), t[++i] = f(i), t[++i] = f(i), t[++i] = f(i), t[++i] = f(i), t[++i] = f(i), t[++i] = f(i), t[++i] = f(i)) 8 | #define HUN(i, t) (TEN(i, t),TEN(i, t),TEN(i, t),TEN(i, t),TEN(i, t),TEN(i, t),TEN(i, t),TEN(i, t),TEN(i, t),TEN(i, t)) 9 | #define THO(i, t) (HUN(i, t),HUN(i, t),HUN(i, t),HUN(i, t),HUN(i, t),HUN(i, t),HUN(i, t),HUN(i, t),HUN(i, t),HUN(i, t)) 10 | #define TTT(i, t) (THO(i, t),THO(i, t),THO(i, t),THO(i, t),THO(i, t),THO(i, t),THO(i, t),THO(i, t),THO(i, t),THO(i, t)) 11 | #define YYY(i, t) (TTT(i, t),TTT(i, t),TTT(i, t),TTT(i, t),TTT(i, t),TTT(i, t),TTT(i, t),TTT(i, t),TTT(i, t),TTT(i, t)) 12 | #define UUU(i, t) (YYY(i, t),YYY(i, t),YYY(i, t),YYY(i, t),YYY(i, t),YYY(i, t),YYY(i, t),YYY(i, t),YYY(i, t),YYY(i, t)) 13 | #define III(i, t) (UUU(i, t),UUU(i, t),UUU(i, t),UUU(i, t),UUU(i, t),UUU(i, t),UUU(i, t),UUU(i, t),UUU(i, t),UUU(i, t)) 14 | #define OOO(i, t) (III(i, t),III(i, t),III(i, t),III(i, t),III(i, t),III(i, t),III(i, t),III(i, t),III(i, t),III(i, t)) 15 | #define PPP(i, t) (OOO(i, t),OOO(i, t),OOO(i, t),OOO(i, t),OOO(i, t),OOO(i, t),OOO(i, t),OOO(i, t),OOO(i, t),OOO(i, t)) 16 | int arr[99999999999]; 17 | int main(int argc, char const *argv[]) 18 | { 19 | int i = 0; 20 | PPP(i, arr); 21 | return 0; 22 | } 23 | -------------------------------------------------------------------------------- /runj/tests/stubs/macro/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/seele", 8 | command: ["gcc", "main.c"], 9 | mounts: [ 10 | { 11 | from: "$TEMP_PATH", 12 | to: "/seele", 13 | options: ["rw"], 14 | }, 15 | { 16 | from: `${resolve(fileURLToPath(import.meta.url), "../main.c")}`, 17 | to: "/seele/main.c", 18 | }, 19 | ], 20 | limits: { 21 | time_ms: 1000, 22 | cgroup: { 23 | memory: 32 * 1024 * 1024, 24 | }, 25 | }, 26 | }, 27 | check: (report) => { 28 | assert.strictEqual(report.status, "MEMORY_LIMIT_EXCEEDED"); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /runj/tests/stubs/manyfiles/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/manyfiles/main -------------------------------------------------------------------------------- /runj/tests/stubs/manyfiles/main.c: -------------------------------------------------------------------------------- 1 | // https://github.com/vijos/malicious-code 2 | 3 | #include 4 | #include 5 | int main(void) 6 | { 7 | int i = 0; 8 | while (1) { 9 | i++; 10 | char s[10000000]; 11 | sprintf(s, "file%d.txt", i); 12 | FILE *tp = fopen(s, "w"); 13 | fprintf(tp, "hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\nhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\n"); 14 | fclose(tp); 15 | if (i < 0) return 0; 16 | } 17 | return 0; 18 | } -------------------------------------------------------------------------------- /runj/tests/stubs/manyfiles/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | time_ms: 1000, 18 | rlimit: { 19 | no_file: { 20 | hard: 32, 21 | soft: 32, 22 | }, 23 | }, 24 | }, 25 | }, 26 | check: (report) => { 27 | assert.strictEqual(report.status, "SIGNAL_TERMINATE"); 28 | // The `fopen()` call returns `null` when exceeds the limit 29 | assert.strictEqual(report.exit_code, 139); 30 | assert.strictEqual(report.signal, "SIGSEGV"); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /runj/tests/stubs/memory1/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/memory1/main -------------------------------------------------------------------------------- /runj/tests/stubs/memory1/main.c: -------------------------------------------------------------------------------- 1 | // https://github.com/QingdaoU/Judger/blob/newnew/tests/test_src/integration/memory1.c 2 | #include 3 | #include 4 | #include 5 | 6 | int main() 7 | { 8 | int size = 80 * 1024 * 1024; 9 | int *a = NULL; 10 | a = (int *)malloc(size); 11 | memset(a, 1, size); 12 | free(a); 13 | return 0; 14 | } -------------------------------------------------------------------------------- /runj/tests/stubs/memory1/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | cgroup: { 18 | memory: 32 * 1024 * 1024, 19 | }, 20 | }, 21 | }, 22 | check: (report) => { 23 | assert.strictEqual(report.status, "MEMORY_LIMIT_EXCEEDED"); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /runj/tests/stubs/memory2/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/memory2/main -------------------------------------------------------------------------------- /runj/tests/stubs/memory2/main.c: -------------------------------------------------------------------------------- 1 | // https://github.com/QingdaoU/Judger/blob/newnew/tests/test_src/integration/memory2.c 2 | #include 3 | #include 4 | #include 5 | 6 | int main() 7 | { 8 | int size = 256 * 1024 * 1024; 9 | int *a = NULL; 10 | a = (int *)malloc(size); 11 | if (a == NULL) { 12 | return 1; 13 | } 14 | else { 15 | memset(a, 1, size); 16 | free(a); 17 | return 0; 18 | } 19 | } -------------------------------------------------------------------------------- /runj/tests/stubs/memory2/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | cgroup: { 18 | memory: 32 * 1024 * 1024, 19 | }, 20 | }, 21 | }, 22 | check: (report) => { 23 | assert.strictEqual(report.status, "MEMORY_LIMIT_EXCEEDED"); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /runj/tests/stubs/memory3/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/memory3/main -------------------------------------------------------------------------------- /runj/tests/stubs/memory3/main.c: -------------------------------------------------------------------------------- 1 | // https://github.com/QingdaoU/Judger/blob/newnew/tests/test_src/integration/memory3.c 2 | #include 3 | #include 4 | #include 5 | 6 | int arr[102400000]; 7 | 8 | 9 | int main() 10 | { 11 | memset(arr, 1, sizeof(arr)); 12 | return 0; 13 | } -------------------------------------------------------------------------------- /runj/tests/stubs/memory3/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | cgroup: { 18 | memory: 512 * 1024 * 1024, 19 | }, 20 | }, 21 | }, 22 | check: (report) => { 23 | assert.strictEqual(report.status, "NORMAL"); 24 | assert.strictEqual(report.memory_usage_kib > 100000, true); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /runj/tests/stubs/network/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/network/main -------------------------------------------------------------------------------- /runj/tests/stubs/network/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | resp, err := http.Get("http://baidu.com") 12 | if err != nil { 13 | fmt.Printf("%s", err) 14 | os.Exit(1) 15 | } 16 | defer resp.Body.Close() 17 | 18 | body, err := ioutil.ReadAll(resp.Body) 19 | if err != nil { 20 | fmt.Printf("%s", err) 21 | os.Exit(2) 22 | } 23 | 24 | fmt.Printf("%s\n", body) 25 | } 26 | -------------------------------------------------------------------------------- /runj/tests/stubs/network/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | time_ms: 1000, 18 | }, 19 | }, 20 | check: (report) => { 21 | assert.strictEqual(report.status, "RUNTIME_ERROR"); 22 | assert.strictEqual(report.exit_code, 1); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /runj/tests/stubs/out_of_bounds/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/out_of_bounds/main -------------------------------------------------------------------------------- /runj/tests/stubs/out_of_bounds/main.c: -------------------------------------------------------------------------------- 1 | 2 | int main(void) { 3 | int a[1]; 4 | return a[114514]; 5 | } -------------------------------------------------------------------------------- /runj/tests/stubs/out_of_bounds/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | cgroup: { 18 | memory: 32 * 1024 * 1024, 19 | }, 20 | }, 21 | }, 22 | check: (report) => { 23 | assert.strictEqual(report.status, "SIGNAL_TERMINATE"); 24 | assert.strictEqual(report.exit_code, 139); 25 | assert.strictEqual(report.signal, "SIGSEGV"); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /runj/tests/stubs/rlimit_fsize_1024/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/rlimit_fsize_1024/main -------------------------------------------------------------------------------- /runj/tests/stubs/rlimit_fsize_1024/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func main() { 8 | data := [1024]byte{} 9 | os.Stdout.Write(data[:]) 10 | } 11 | -------------------------------------------------------------------------------- /runj/tests/stubs/rlimit_fsize_1024/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | rlimit: { 18 | fsize: { 19 | hard: 1024, 20 | soft: 1024, 21 | }, 22 | }, 23 | }, 24 | fd: { 25 | stdout: "$TEMP_PATH/stdout.txt", 26 | }, 27 | }, 28 | check: (report) => { 29 | assert.strictEqual(report.status, "NORMAL"); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /runj/tests/stubs/rlimit_fsize_1025/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/rlimit_fsize_1025/main -------------------------------------------------------------------------------- /runj/tests/stubs/rlimit_fsize_1025/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func main() { 8 | data := [1025]byte{} 9 | os.Stdout.Write(data[:]) 10 | } 11 | -------------------------------------------------------------------------------- /runj/tests/stubs/rlimit_fsize_1025/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | rlimit: { 18 | fsize: { 19 | hard: 1024, 20 | soft: 1024, 21 | }, 22 | }, 23 | }, 24 | fd: { 25 | stdout: "$TEMP_PATH/stdout.txt", 26 | }, 27 | }, 28 | check: (report) => { 29 | assert.strictEqual(report.status, "OUTPUT_LIMIT_EXCEEDED"); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /runj/tests/stubs/rm_rf/main: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf / -------------------------------------------------------------------------------- /runj/tests/stubs/rm_rf/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | time_ms: 1000, 18 | }, 19 | }, 20 | check: (report) => { 21 | assert.strictEqual(report.status, "RUNTIME_ERROR"); 22 | assert.strictEqual(report.exit_code, 1); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /runj/tests/stubs/stack_overflow_1/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/stack_overflow_1/main -------------------------------------------------------------------------------- /runj/tests/stubs/stack_overflow_1/main.c: -------------------------------------------------------------------------------- 1 | int main(void) { 2 | int mat[1000000][1000000] = {0}; 3 | return mat[114514][114514]; 4 | } -------------------------------------------------------------------------------- /runj/tests/stubs/stack_overflow_1/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | cgroup: { 18 | memory: 32 * 1024 * 1024, 19 | }, 20 | }, 21 | }, 22 | check: (report) => { 23 | assert.strictEqual(report.status, "SIGNAL_TERMINATE"); 24 | assert.strictEqual(report.exit_code, 139); 25 | assert.strictEqual(report.signal, "SIGSEGV"); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /runj/tests/stubs/stack_overflow_2/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/stack_overflow_2/main -------------------------------------------------------------------------------- /runj/tests/stubs/stack_overflow_2/main.c: -------------------------------------------------------------------------------- 1 | void fun(int x) 2 | { 3 | fun(x + 1); 4 | } 5 | 6 | int main() 7 | { 8 | fun(0); 9 | } -------------------------------------------------------------------------------- /runj/tests/stubs/stack_overflow_2/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | cgroup: { 18 | memory: 32 * 1024 * 1024, 19 | }, 20 | }, 21 | }, 22 | check: (report) => { 23 | assert.strictEqual(report.status, "SIGNAL_TERMINATE"); 24 | assert.strictEqual(report.exit_code, 139); 25 | assert.strictEqual(report.signal, "SIGSEGV"); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /runj/tests/stubs/thread/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/thread/main -------------------------------------------------------------------------------- /runj/tests/stubs/thread/main.cpp: -------------------------------------------------------------------------------- 1 | // https://github.com/vijos/malicious-code 2 | 3 | #include 4 | 5 | void new_thread() 6 | { 7 | while (true) 8 | { 9 | std::thread *th = new std::thread(new_thread); 10 | } 11 | } 12 | 13 | int main() 14 | { 15 | new_thread(); 16 | return 0; 17 | } -------------------------------------------------------------------------------- /runj/tests/stubs/thread/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | time_ms: 3000, 18 | cgroup: { 19 | pids_limit: 8, 20 | }, 21 | }, 22 | }, 23 | check: (report) => { 24 | assert.strictEqual(report.status, "SIGNAL_TERMINATE"); 25 | assert.strictEqual(report.exit_code, 139); 26 | assert.strictEqual(report.signal, "SIGSEGV"); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /runj/tests/stubs/tle_system/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/tle_system/main -------------------------------------------------------------------------------- /runj/tests/stubs/tle_system/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(void) { 4 | sleep(1145141919); 5 | } -------------------------------------------------------------------------------- /runj/tests/stubs/tle_system/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | time_ms: 1000, 18 | }, 19 | }, 20 | check: (report) => { 21 | assert.strictEqual(report.status, "WALL_TIME_LIMIT_EXCEEDED"); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /runj/tests/stubs/tle_system_child/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/tle_system_child/main -------------------------------------------------------------------------------- /runj/tests/stubs/tle_system_child/main.c: -------------------------------------------------------------------------------- 1 | // https://github.com/QingdaoU/Judger/blob/newnew/tests/test_src/integration/child_proc_real_time_limit.c 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | int main() 10 | { 11 | int status; 12 | int pid; 13 | 14 | if ((pid = fork()) < 0) { 15 | perror("fork error"); 16 | return 0; 17 | } 18 | 19 | if (pid == 0) { 20 | sleep(10); 21 | } 22 | else { 23 | struct rusage resource_usage; 24 | if (wait4(pid, &status, 0, &resource_usage) == -1) { 25 | perror("wait4 error!"); 26 | } 27 | } 28 | 29 | return 0; 30 | } 31 | -------------------------------------------------------------------------------- /runj/tests/stubs/tle_system_child/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | time_ms: 1000, 18 | }, 19 | }, 20 | check: (report) => { 21 | assert.strictEqual(report.status, "WALL_TIME_LIMIT_EXCEEDED"); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /runj/tests/stubs/tle_user/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/tle_user/main -------------------------------------------------------------------------------- /runj/tests/stubs/tle_user/main.c: -------------------------------------------------------------------------------- 1 | int main(void) { 2 | while (1); 3 | } -------------------------------------------------------------------------------- /runj/tests/stubs/tle_user/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | time_ms: 1000, 18 | }, 19 | }, 20 | check: (report) => { 21 | assert.strictEqual(report.status, "USER_TIME_LIMIT_EXCEEDED"); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /runj/tests/stubs/tle_user_child/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkyzhou/seele/080c81ec03380a63c26dcba037be6aff787b2cac/runj/tests/stubs/tle_user_child/main -------------------------------------------------------------------------------- /runj/tests/stubs/tle_user_child/main.c: -------------------------------------------------------------------------------- 1 | // https://github.com/QingdaoU/Judger/blob/newnew/tests/test_src/integration/child_proc_cpu_time_limit.c 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | int main() 10 | { 11 | int status; 12 | int pid; 13 | 14 | if ((pid = fork()) < 0) { 15 | perror("fork error"); 16 | return 0; 17 | } 18 | 19 | if (pid == 0) { 20 | while (1) {}; 21 | } 22 | else { 23 | struct rusage resource_usage; 24 | if (wait4(pid, &status, 0, &resource_usage) == -1) { 25 | perror("wait4 error!"); 26 | } 27 | } 28 | 29 | return 0; 30 | } -------------------------------------------------------------------------------- /runj/tests/stubs/tle_user_child/stub.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { resolve } from "node:path"; 3 | import assert from "node:assert"; 4 | 5 | export default { 6 | config: { 7 | cwd: "/", 8 | command: ["main"], 9 | mounts: [ 10 | { 11 | from: `${resolve(fileURLToPath(import.meta.url), "../main")}`, 12 | to: "/usr/local/bin/main", 13 | options: ["exec"], 14 | }, 15 | ], 16 | limits: { 17 | time_ms: 1000, 18 | }, 19 | }, 20 | check: (report) => { 21 | assert.strictEqual(report.status, "USER_TIME_LIMIT_EXCEEDED"); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Format with "cargo +nightly fmt --all" 2 | 3 | style_edition = "2024" 4 | unstable_features = true 5 | 6 | newline_style = "Unix" 7 | imports_granularity = "Crate" 8 | group_imports = "StdExternalCrate" 9 | use_small_heuristics = "Max" 10 | wrap_comments = true 11 | format_code_in_doc_comments = true 12 | format_macro_bodies = true 13 | format_macro_matchers = true 14 | format_strings = true 15 | --------------------------------------------------------------------------------