├── .cargo └── config ├── .clang-format ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── deploy.yaml │ └── pr.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Justfile ├── Readme.md ├── Version.txt ├── actions ├── blocks │ ├── pkg-login.yaml │ ├── python.yaml │ ├── rustc-nightly.yaml │ ├── rustc.yaml │ └── sysdeps.yaml ├── deploy.yaml └── pr.yaml ├── ansible ├── jjs.yaml ├── profile-template.yaml.j2 └── roles │ ├── db │ └── tasks │ │ └── main.yaml │ └── jjs_common │ └── tasks │ └── main.yaml ├── basic-setup-profile.yaml ├── bors.toml ├── ci-data ├── env.txt ├── free-space.sh ├── helm-lint-values.yaml ├── mongodb-values.yaml ├── push_images.py ├── test-e2e-profile.yaml └── values.yaml ├── configure ├── deb ├── build.sh ├── env.txt └── manifest.txt ├── deny.toml ├── docker-compose ├── README.md ├── docker-compose.yml └── invoker.yaml ├── example-config ├── apiserver.yaml ├── env.txt ├── invoker.yaml └── objects │ └── contests │ └── trial.yaml ├── example-problems ├── a-plus-b │ ├── generators │ │ ├── main.cpp │ │ └── random.cpp │ ├── problem.toml │ ├── solutions │ │ └── main.cpp │ ├── tests │ │ ├── 1.txt │ │ ├── 2.txt │ │ ├── 6.txt │ │ └── 7.txt │ └── valuer.yaml ├── array-sum │ ├── checkers │ │ └── main.cpp │ ├── generators │ │ └── main.cpp │ ├── problem.toml │ ├── tests │ │ ├── 1.txt │ │ └── 2.txt │ └── valuer.yaml └── sqrt │ ├── problem.toml │ ├── solutions │ └── main.cpp │ ├── tests │ ├── 1.txt │ ├── 2.txt │ └── 3.txt │ └── valuer.yaml ├── k8s └── jjs │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── apiserver.yaml │ ├── config.yaml │ ├── ejui.yaml │ ├── init.yaml │ ├── invoker.yaml │ └── network-policy.yaml │ ├── values.schema.json │ └── values.yaml ├── man ├── book.toml └── src │ ├── SUMMARY.md │ ├── apiserver.md │ ├── authentication.md │ ├── authorization.md │ ├── clients.md │ ├── problems │ ├── README.md │ ├── checker.md │ ├── packages.md │ ├── polygon.md │ ├── ppc.md │ └── testgen.md │ ├── running.md │ ├── sysroot.md │ ├── userlist.md │ └── vm.md ├── rustfmt.toml ├── scripts ├── dev-cgroups.sh ├── process-strace.py ├── strace-parser.py ├── strace │ ├── remove_pid.sh │ └── remove_rt_sigaction.sh └── uml-tests.sh ├── smoke ├── Readme.md ├── requirements.txt └── test_judging.py ├── src ├── apiserver │ ├── Dockerfile │ ├── __init__.py │ ├── api_models.py │ ├── auth.py │ ├── db_connect.py │ ├── db_models.py │ ├── main.py │ ├── openapi.json │ ├── requirements.txt │ ├── routes.py │ └── test_http.py ├── apiserver_old │ ├── Cargo.toml │ ├── Rocket.toml │ ├── engine │ │ ├── Cargo.toml │ │ ├── docs │ │ │ ├── openapi-gen.json │ │ │ └── openapi.yaml │ │ ├── src │ │ │ ├── api.rs │ │ │ ├── api │ │ │ │ ├── auth.rs │ │ │ │ ├── contests.rs │ │ │ │ ├── context.rs │ │ │ │ ├── misc.rs │ │ │ │ ├── monitor.rs │ │ │ │ ├── runs.rs │ │ │ │ ├── schema.rs │ │ │ │ ├── security.rs │ │ │ │ ├── security │ │ │ │ │ ├── access_ck.rs │ │ │ │ │ ├── token.rs │ │ │ │ │ └── token_mgr.rs │ │ │ │ ├── toolchains.rs │ │ │ │ └── users.rs │ │ │ ├── config.rs │ │ │ ├── introspect.rs │ │ │ ├── lib.rs │ │ │ ├── password.rs │ │ │ ├── root_auth.rs │ │ │ ├── secret_key.rs │ │ │ └── test_util.rs │ │ └── tests │ │ │ ├── authn.rs │ │ │ ├── common │ │ │ └── mod.rs │ │ │ └── gql.rs │ └── src │ │ └── main.rs ├── cleanup │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── cli │ ├── Cargo.toml │ ├── Dockerfile │ └── src │ │ ├── api_version.rs │ │ ├── completion.rs │ │ ├── login.rs │ │ ├── main.rs │ │ ├── problems.rs │ │ ├── runs.rs │ │ ├── submit.rs │ │ ├── toolchains.rs │ │ └── wait.rs ├── client │ ├── Cargo.toml │ └── src │ │ ├── auth_data.rs │ │ └── lib.rs ├── configure-toolchains │ ├── Cargo.toml │ └── src │ │ ├── config.rs │ │ ├── debootstrap.rs │ │ ├── main.rs │ │ ├── trace.rs │ │ └── trace │ │ └── dep_collector.rs ├── db │ ├── Cargo.toml │ ├── diesel.toml │ ├── migrations │ │ ├── 00000000000000_diesel_initial_setup │ │ │ ├── down.sql │ │ │ └── up.sql │ │ ├── 2019-02-13-163254_initial │ │ │ ├── down.sql │ │ │ └── up.sql │ │ ├── 2020-04-01-171741_kv │ │ │ ├── down.sql │ │ │ └── up.sql │ │ └── 2020-04-03-210736_contest_registration │ │ │ ├── down.sql │ │ │ └── up.sql │ └── src │ │ ├── conn.rs │ │ ├── connect.rs │ │ ├── lib.rs │ │ ├── repo.rs │ │ ├── repo │ │ ├── memory.rs │ │ ├── pg.rs │ │ └── redis.rs │ │ ├── schema.rs │ │ ├── schema │ │ ├── invocation.rs │ │ ├── participation.rs │ │ ├── run.rs │ │ └── user.rs │ │ └── schema_raw.rs ├── devtool │ ├── Cargo.toml │ ├── scripts │ │ ├── pages-push.sh │ │ └── postgres-start.sh │ └── src │ │ ├── build.rs │ │ ├── check.rs │ │ ├── ci.rs │ │ ├── codegen.rs │ │ ├── glob_util.rs │ │ ├── main.rs │ │ ├── run.rs │ │ └── tests.rs ├── dist-builder │ ├── Cargo.toml │ ├── deps.sh │ ├── make-tpl.sh │ ├── makefile.tpl │ └── src │ │ ├── artifact.rs │ │ ├── builder.rs │ │ ├── cfg.rs │ │ ├── emit.rs │ │ ├── fs_util.rs │ │ ├── main.rs │ │ └── package.rs ├── dist-files-generator │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── e2e │ ├── Cargo.toml │ ├── src │ │ └── lib.rs │ └── tests │ │ ├── judge.rs │ │ └── users.rs ├── entity │ ├── Cargo.toml │ └── src │ │ ├── entities.rs │ │ ├── entities │ │ ├── contest.rs │ │ └── toolchain.rs │ │ ├── lib.rs │ │ ├── loader.rs │ │ └── loader │ │ └── builder.rs ├── envck │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── gen-api-client │ ├── Cargo.toml │ ├── api_version.rs │ ├── body_route_put_problem_problems_problem_id_put.rs │ ├── contest.rs │ ├── generics.rs │ ├── http_validation_error.rs │ ├── lib.rs │ ├── live_status.rs │ ├── miscellaneous.rs │ ├── problem.rs │ ├── run.rs │ ├── run_patch.rs │ ├── run_submit_simple_params.rs │ ├── session_token.rs │ ├── simple_auth_params.rs │ ├── toolchain.rs │ ├── user.rs │ ├── user_creation_params.rs │ ├── util.rs │ └── validation_error.rs ├── invoker-api │ ├── Cargo.toml │ └── src │ │ ├── judge_log.rs │ │ ├── lib.rs │ │ └── valuer_proto.rs ├── invoker │ ├── Cargo.toml │ ├── Dockerfile │ ├── src │ │ ├── api.rs │ │ ├── config.rs │ │ ├── controller.rs │ │ ├── controller │ │ │ ├── notify.rs │ │ │ ├── task_loading.rs │ │ │ └── toolchains.rs │ │ ├── init.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── scheduler.rs │ │ ├── sources.rs │ │ ├── sources │ │ │ ├── api_source.rs │ │ │ └── cli_source.rs │ │ ├── worker.rs │ │ └── worker │ │ │ ├── compiler.rs │ │ │ ├── exec_test.rs │ │ │ ├── exec_test │ │ │ └── checker_proto.rs │ │ │ ├── invoke_util.rs │ │ │ ├── os_util.rs │ │ │ ├── transform_judge_log.rs │ │ │ └── valuer.rs │ └── tests │ │ └── separated_feedback.rs ├── jtl │ ├── CMakeLists.txt │ ├── Dockerfile │ ├── JtlConfig.cmake │ ├── cbindgen.toml │ ├── include │ │ ├── checker.h │ │ ├── jtl.h │ │ └── testgen.h │ └── src │ │ ├── builtin │ │ ├── checker-cmp-tokens.cpp │ │ └── checker-polygon-compat.cpp │ │ ├── checker.cpp │ │ ├── jtl.cpp │ │ ├── proto.cpp │ │ ├── proto.h │ │ ├── testgen.cpp │ │ ├── util.cpp │ │ └── util.h ├── pom │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── pps │ ├── Readme.md │ ├── api │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── compile_problem.rs │ │ │ ├── import_problem.rs │ │ │ └── lib.rs │ ├── cli │ │ ├── Cargo.toml │ │ ├── Dockerfile │ │ └── src │ │ │ ├── client_util.rs │ │ │ ├── compile.rs │ │ │ ├── import.rs │ │ │ ├── main.rs │ │ │ └── progress_notifier.rs │ └── server │ │ ├── Cargo.toml │ │ └── src │ │ ├── command.rs │ │ ├── compile.rs │ │ ├── compile │ │ ├── build.rs │ │ └── builder.rs │ │ ├── import.rs │ │ ├── import │ │ ├── checker_tpl.cmake │ │ ├── contest_import.rs │ │ ├── default_valuer_config.yaml │ │ ├── gen.cmake │ │ ├── problem_importer.rs │ │ ├── solution.cmake │ │ ├── template.rs │ │ ├── valuer_cfg.pest │ │ └── valuer_cfg.rs │ │ ├── lib.rs │ │ └── manifest.rs ├── problem-loader │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── registry.rs ├── ranker │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── tests.rs ├── setup │ ├── Cargo.toml │ └── src │ │ ├── certs.rs │ │ ├── config.rs │ │ ├── data.rs │ │ ├── db.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── problems.rs │ │ ├── profile.rs │ │ └── toolchains.rs ├── svaluer │ ├── Cargo.toml │ ├── Dockerfile │ └── src │ │ ├── cfg.rs │ │ ├── fiber.rs │ │ ├── fiber │ │ └── group.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ └── tests.rs ├── toolkit │ └── Dockerfile ├── userlist │ ├── Cargo.toml │ └── src │ │ ├── create_user.graphql │ │ ├── gram.pest │ │ ├── list_parse.rs │ │ └── main.rs └── util │ ├── Cargo.toml │ └── src │ ├── cfg.rs │ ├── cmd.rs │ ├── lib.rs │ └── log.rs ├── systemd ├── jjs-apiserver.service.tera └── jjs-invoker.service.tera └── vm-sysroot ├── README.txt ├── build.sh ├── env.txt ├── etc-network-interfaces.conf ├── image ├── build-full.sh └── build-image.sh ├── netns-build.sh ├── problems ├── a-plus-b ├── array-sum └── sqrt ├── scripts ├── post-sysroot │ ├── boot.sh │ ├── busybox.sh │ ├── ect-network-interfaces.sh │ ├── hosts.sh │ ├── init.sh │ ├── jjs-prod.sh │ ├── jjs-sysroot.sh │ ├── jjs.sh │ ├── ld-linux.sh │ ├── passwd.sh │ └── postgres.sh └── sysroot │ ├── debug.sh │ ├── haveged.sh │ ├── jjs.sh │ ├── localhost.sh │ ├── postgres.sh │ ├── strace.sh │ ├── tmp.sh │ └── utc.sh ├── strace └── soft.py └── uml-build.sh /.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | jjs-check = "run --package devtool -- check" 3 | jjs-test = "run --package devtool -- test" 4 | jjs-clean = "run --package devtool -- clean" 5 | jjs-build = "run --package dist-builder -- " 6 | jjs-gen = "run --package dist-files-generator -- " 7 | jjs-run = "run --package devtool -- run" 8 | jjs = "run --package devtool --" 9 | 10 | [build] 11 | rustflags = ["--cfg", "tokio_unstable"] 12 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | PointerAlignment: Left 3 | IndentWidth: 4 4 | SpaceAfterCStyleCast: true 5 | SpaceBeforeCpp11BracedList: true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.github/workflows/*.yaml linguist-generated=true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Tell about bug in JJS 4 | title: '' 5 | labels: T-bug, T-new 6 | assignees: '' 7 | --- 8 | 9 | # Bug description 10 | 15 | 16 | # Steps to reproduce 17 | 19 | 20 | # Environment 21 | 23 | **OS**: `uname -a` 24 | **JJS version**: `git rev-parse HEAD` 25 | **Rustc version**: `rustc --version` 26 | **Docker version**: `docker --version`, `podman --version` 27 | **Cgroup version**: `stat /sys/fs/cgroup --file-system` 28 | **Invoker permissions**: (Root | Rootless) 29 | 30 | # Reminder [delete when creating issue] 31 | If bug is related to sandbox, syscall dump can be very helpful. 32 | You can use following command to obtain it: 33 | `strace -f -o /tmp/jjs-syscall-log.txt -s 128` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: T-enhancement, T-new 6 | assignees: '' 7 | --- 8 | 9 | # Request 10 | 12 | # Motivation 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: deploy 3 | "on": 4 | push: 5 | branches: 6 | - master 7 | - fixup-deploy 8 | env: 9 | CI: 1 10 | RUST_BACKTRACE: full 11 | RUSTC_BOOTSTRAP: 1 12 | jobs: 13 | docker: 14 | name: docker 15 | runs-on: ubuntu-18.04 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Install Rust toolchain 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: stable 22 | components: "clippy,rustfmt" 23 | override: true 24 | - name: Install system dependencies 25 | run: sudo apt-get install -y libssl-dev cmake 26 | - name: Build images 27 | run: "mkdir artifacts\nDOCKER_OPT=\"--enable-docker --docker-tag=% --docker-tags-log=/tmp/taglog.txt --with-docker=docker\"\ncargo jjs-build --out artifacts $DOCKER_OPT --enable=tools --enable=daemons\n" 28 | - name: Upload images 29 | run: "docker login --username jjs-dev --password ${{ secrets.GITHUB_TOKEN }} docker.pkg.github.com\necho \"${{ secrets.GCR_UPLOAD_JSON_KEY }}\" | base64 --decode | docker login --username _json_key --password-stdin gcr.io\npython3 ci-data/push_images.py\n" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.rs.bk 3 | /pkg 4 | cmake-build-* 5 | /man/book 6 | /vm-sysroot/sysroot 7 | /vm-sysroot/sysroot/* 8 | /vm-sysroot/image/hdd.img 9 | /vm-sysroot/typescript 10 | /venv 11 | /src/apiserver/__pycache__ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [profile.release] 2 | lto = true 3 | debug = 1 4 | 5 | [profile.dev] 6 | debug = 1 7 | 8 | # even in debug builds we want toolchain pulling to work fast 9 | 10 | [profile.dev.package.libflate] 11 | opt-level=3 12 | 13 | [profile.dev.package.libflate_lz77] 14 | opt-level=3 15 | 16 | [profile.dev.package.adler32] 17 | opt-level=3 18 | 19 | [profile.dev.package.crc32fast] 20 | opt-level=3 21 | 22 | [profile.dev.package.rle-decode-fast] 23 | opt-level=3 24 | 25 | [profile.dev.package.tar] 26 | opt-level=3 27 | 28 | [profile.dev.package.dkregistry] 29 | opt-level=3 30 | 31 | # Never waste time optimizing `syn` & Co 32 | 33 | [profile.dev.build-override] 34 | opt-level = 0 35 | 36 | [workspace] 37 | # TODO: add other as they are fixed 38 | members=[ "src/devtool", "src/gen-api-client", "src/cli", "src/client", 39 | "src/problem-loader", "src/invoker", "src/dist-files-generator", 40 | "src/dist-builder", "src/svaluer", "src/invoker-api", "src/pps/api", 41 | "src/pps/cli", "src/pps/server" ] 42 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | phony: 2 | @just -l 3 | 4 | db: 5 | #! /bin/bash 6 | echo "recreating db" 7 | dropdb jjs 8 | createdb jjs 9 | echo "running migrations" 10 | cd src/db 11 | diesel migration run 12 | echo "re-running migrations" 13 | diesel migration redo 14 | 15 | users: 16 | cargo run --bin userlist -- add --auth dev_root ./example-config/userlist.txt 17 | 18 | install_tools: 19 | #! /bin/bash 20 | cargo install diesel_cli mdbook || true -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [![Bors enabled](https://bors.tech/images/badge_small.svg)](https://app.bors.tech/repositories/20068) 2 | 3 | # JJS 4 | Judging system 5 | 6 | ## Quick start 7 | 8 | ```bash 9 | # Note: while installation process is intentionally made similar to that of most other Linux tools, JJS doesn't use autotools 10 | 11 | # Of course, you can build out-of-tree 12 | # Note for contributors: it is recommended to build in target 13 | mkdir build && cd build 14 | 15 | # This will automatically include most of JJS features. See ../configure --help for possible options 16 | ../configure --prefix /opt/jjs 17 | 18 | # Install dependencies 19 | make deps 20 | 21 | ## Alternatively, you can do it manually: 22 | # sudo apt-get install libpq-dev 23 | # sudo apt-get install libssl-dev 24 | # cargo install cbindgen 25 | # cargo install mdbook 26 | 27 | # Now, start build. This will also install JJS 28 | # If you don't have make installed, run ./make 29 | make 30 | 31 | # Done. JJS is now installed 32 | # Don't forget to include some env vars: 33 | . /opt/jjs/share/env.sh 34 | ``` 35 | 36 | ## License 37 | Licensed under either of 38 | - [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) 39 | - [MIT license](http://opensource.org/licenses/MIT) 40 | 41 | at your option. 42 | 43 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as 44 | defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 45 | -------------------------------------------------------------------------------- /Version.txt: -------------------------------------------------------------------------------- 1 | 0.0.1 -------------------------------------------------------------------------------- /actions/blocks/pkg-login.yaml: -------------------------------------------------------------------------------- 1 | name: Log into Github Package Registry 2 | run: | 3 | echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u jjs-dev --password-stdin -------------------------------------------------------------------------------- /actions/blocks/python.yaml: -------------------------------------------------------------------------------- 1 | name: Setup Python 2 | uses: actions/setup-python@v2 3 | with: 4 | python-version: '3.8' 5 | -------------------------------------------------------------------------------- /actions/blocks/rustc-nightly.yaml: -------------------------------------------------------------------------------- 1 | name: Install Rust toolchain 2 | uses: actions-rs/toolchain@v1 3 | with: 4 | toolchain: nightly-2020-08-28 5 | components: clippy,rustfmt 6 | override: true -------------------------------------------------------------------------------- /actions/blocks/rustc.yaml: -------------------------------------------------------------------------------- 1 | name: Install Rust toolchain 2 | uses: actions-rs/toolchain@v1 3 | with: 4 | toolchain: stable 5 | components: clippy,rustfmt 6 | override: true -------------------------------------------------------------------------------- /actions/blocks/sysdeps.yaml: -------------------------------------------------------------------------------- 1 | name: Install system dependencies 2 | run: | 3 | sudo apt-get install -y libssl-dev cmake -------------------------------------------------------------------------------- /actions/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: "deploy" 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - fixup-deploy 7 | env: 8 | CI: 1 9 | RUST_BACKTRACE: full 10 | RUSTC_BOOTSTRAP: 1 11 | jobs: 12 | docker: 13 | name: docker 14 | runs-on: "ubuntu-18.04" 15 | steps: 16 | - uses: actions/checkout@v2 17 | - $include: rustc 18 | - $include: sysdeps 19 | - name: Build images 20 | run: | 21 | mkdir artifacts 22 | DOCKER_OPT="--enable-docker --docker-tag=% --docker-tags-log=/tmp/taglog.txt --with-docker=docker" 23 | cargo jjs-build --out artifacts $DOCKER_OPT --enable=tools --enable=daemons 24 | - name: Upload images 25 | run: | 26 | docker login --username jjs-dev --password ${{ secrets.GITHUB_TOKEN }} docker.pkg.github.com 27 | echo "${{ secrets.GCR_UPLOAD_JSON_KEY }}" | base64 --decode | docker login --username _json_key --password-stdin gcr.io 28 | python3 ci-data/push_images.py 29 | -------------------------------------------------------------------------------- /ansible/jjs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | vars: 4 | setup_profile: "{{ lookup('template', './profile-template.yaml.j2') }}" 5 | vars_prompt: 6 | - name: pkg_path 7 | prompt: "Where on local host jjs package is located?" 8 | private: false 9 | default: "/opt/jjs/pkg/jjs.deb" 10 | - name: db_addr 11 | prompt: "Database address (must include port; can be urlencoded unix socket path instead)" 12 | private: false 13 | default: "%2Fvar%2Frun%2Fpostgresql" 14 | roles: 15 | - db 16 | - role: jjs_common 17 | tasks: 18 | - name: Install required APT packages 19 | apt: 20 | name: ["g++"] 21 | update_cache: true 22 | - name: Setup JJS 23 | command: 24 | # Note that "-" is passed as profile path. It tells `jjs-setup` to read profile from stdin. 25 | cmd: jjs-setup - upgrade 26 | stdin: "{{ setup_profile }}" 27 | become: true 28 | become_user: jjs 29 | - name: Reload SystemD 30 | systemd: 31 | daemon_reload: true 32 | - name: Start JJS invoker 33 | service: 34 | name: jjs-invoker 35 | state: restarted 36 | - name: Start JJS apiserver 37 | service: 38 | name: jjs-apiserver 39 | state: restarted 40 | -------------------------------------------------------------------------------- /ansible/profile-template.yaml.j2: -------------------------------------------------------------------------------- 1 | install-dir: '/opt/jjs' 2 | data-dir: '/home/jjs' 3 | pg: 4 | db-name: jjs 5 | conn-string: 'postgresql://jjs@{{ db_addr }}/postgres' 6 | toolchains: {} 7 | problems: 8 | tasks: 9 | - source: 10 | path: "/opt/jjs/example-problems/a-plus-b" 11 | - source: 12 | path: "/opt/jjs/example-problems/array-sum" 13 | - source: 14 | path: "/opt/jjs/example-problems/sqrt" 15 | -------------------------------------------------------------------------------- /ansible/roles/db/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | - name: Install APT packages 2 | apt: 3 | name: 4 | - "libpq-dev" 5 | - "python3-setuptools" 6 | - "python3-pip" 7 | update_cache: true 8 | - block: 9 | - name: Install PG 11 APT packages 10 | apt: 11 | name: 12 | - "postgresql-client-11" 13 | - "postgresql-11" 14 | rescue: 15 | - name: Install PG 10 APT packages 16 | apt: 17 | name: 18 | - "postgresql-client-10" 19 | - "postgresql-10" 20 | - name: Create JJS database 21 | postgresql_db: 22 | name: jjs 23 | become: true 24 | become_user: postgres 25 | - name: Setup JJS postgres user 26 | postgresql_user: 27 | db: jjs 28 | name: jjs 29 | # TODO: take variable 30 | password: internal 31 | role_attr_flags: "SUPERUSER" 32 | become: true 33 | become_user: postgres 34 | -------------------------------------------------------------------------------- /ansible/roles/jjs_common/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | - name: Install APT packages 2 | apt: 3 | name: libpq-dev 4 | - name: Copy JJS package 5 | copy: 6 | dest: /tmp/ 7 | src: "{{ pkg_path }}" 8 | - name: "TODO: hack" 9 | apt: 10 | name: jjs 11 | state: absent 12 | - name: Install JJS package 13 | apt: 14 | deb: /tmp/jjs.deb 15 | state: present 16 | - name: Setup JJS user 17 | user: 18 | create_home: true 19 | name: jjs 20 | -------------------------------------------------------------------------------- /basic-setup-profile.yaml: -------------------------------------------------------------------------------- 1 | install-dir: "/opt/jjs" 2 | data-dir: "/tmp/jjs" 3 | pg: 4 | db-name: jjs 5 | conn-string: "postgresql://jjs:internal@localhost:5432/jjs" 6 | problems: 7 | archive: 8 | sources: 9 | - path: "/opt/jjs/pkg/problems.tgz" 10 | pki: 11 | create-ca: true 12 | toolchains: {} -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ 2 | 'hack', 3 | 'style', 4 | 'clippy', 5 | 'cpp', 6 | 'unit-tests', 7 | 'udeps', 8 | 'codegen', 9 | 'py-test', 10 | 'py-fmt', 11 | 'smoke' 12 | ] 13 | delete-merged-branches = true 14 | timeout-sec = 900 15 | -------------------------------------------------------------------------------- /ci-data/env.txt: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql://jjs:internal@localhost:5432/jjs 2 | REDIS_URL=redis://localhost:6379 3 | RUST_BACKTRACE=full 4 | JJS_PATH=/opt/jjs 5 | RUST_LOG=info,invoker=debug,apiserver=debug 6 | JJS_SYSTEMD=1 7 | JJS_SECRET_KEY=will_be_deprecated_anyway -------------------------------------------------------------------------------- /ci-data/free-space.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | df -h 4 | # decomment some lines below if needed 5 | # rm -rf "/usr/local/share/boost" 6 | # rm -rf /usr/share/dotnet 7 | # apt-get remove -y '^ghc-8.*' 8 | # apt-get remove -y '^dotnet-.*' 9 | # apt-get remove -y 'php.*' 10 | # apt-get remove -y azure-cli google-cloud-sdk hhvm google-chrome-stable firefox powershell mono-devel 11 | # apt-get autoremove -y 12 | # rm -rf "$AGENT_TOOLSDIRECTORY" 13 | df -h 14 | -------------------------------------------------------------------------------- /ci-data/helm-lint-values.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | tag: 3 | notlatest 4 | dev: 5 | kubeScore: true -------------------------------------------------------------------------------- /ci-data/mongodb-values.yaml: -------------------------------------------------------------------------------- 1 | persistence: 2 | enabled: false 3 | auth: 4 | enabled: false 5 | -------------------------------------------------------------------------------- /ci-data/push_images.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import subprocess 4 | 5 | TEMPLATES = [ 6 | "docker.pkg.github.com/jjs-dev/jjs/jjs-%:latest", 7 | "gcr.io/jjs-dev/jjs-%:latest" 8 | ] 9 | 10 | tags = open("/tmp/taglog.txt").readlines() 11 | all_images = [] 12 | for comp in tags: 13 | comp = comp.strip() 14 | for tpl in TEMPLATES: 15 | new_tag = tpl.replace("%", comp) 16 | subprocess.check_call(["docker", "tag", comp, new_tag]) 17 | all_images.append(new_tag) 18 | print("will push", all_images) 19 | 20 | for img in all_images: 21 | subprocess.check_call(["docker", "push", img]) 22 | -------------------------------------------------------------------------------- /ci-data/test-e2e-profile.yaml: -------------------------------------------------------------------------------- 1 | install-dir: "/opt/jjs" 2 | data-dir: "/home/jjs" 3 | pg: 4 | db-name: jjs 5 | conn-string: "postgresql://jjs:internal@localhost:5432/jjs" 6 | toolchains: {} 7 | problems: 8 | archive: 9 | sources: 10 | - path: "/opt/jjs/pkg/problems.tgz" -------------------------------------------------------------------------------- /ci-data/values.yaml: -------------------------------------------------------------------------------- 1 | # Helm chart installation settings from `smoke` job 2 | 3 | image: 4 | # use host images 5 | repositoryPrefix: "" 6 | tag: latest 7 | pullPolicy: Never 8 | services: 9 | apiserver: 10 | serviceType: NodePort 11 | extras: 12 | ejui: 13 | enabled: false 14 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Of course, it is NOT autotools 3 | # copy-pasted from https://stackoverflow.com/questions/59895/get-the-source-directory-of-a-bash-script-from-within-the-script-itself 4 | JJS_CFGR_SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 5 | export JJS_CFGR_SOURCE_DIR 6 | 7 | # check rustc version 8 | 9 | if ! rustc --version > /dev/null 10 | then 11 | echo "Error: rustc not found" 12 | exit 1 13 | fi 14 | echo "checking rustc is installed... ok" 15 | 16 | RustcVersionDataRaw=$( rustc --version ) 17 | 18 | IFS=" " read -r -a RustcVersionData <<< "$RustcVersionDataRaw" 19 | 20 | RustcVersion=${RustcVersionData[1]} 21 | 22 | IFS="-." read -r -a RustcVersionElements <<< "$RustcVersion" 23 | 24 | if [[ "37" -gt "${RustcVersionElements[1]}" ]] 25 | then 26 | echo "Rustc is too old: 1.37.0 is required" 27 | exit 1 28 | fi 29 | 30 | cargo run --bin configure --package deploy -- "$@" -------------------------------------------------------------------------------- /deb/env.txt: -------------------------------------------------------------------------------- 1 | JJS_SYSROOT=/var/lib/jjs 2 | JJS_PATH=/opt/jjs 3 | DATABASE_URL=postgres://jjs:internal@localhost:5432/jjs 4 | # TODO: use unix sockets instead of loopback TCP 5 | JJS_HOST=0.0.0.0 6 | JJS_SELF_ADDR=127.0.0.1 7 | RUST_LOG=info,apiserver=debug,invoker=debug 8 | -------------------------------------------------------------------------------- /deb/manifest.txt: -------------------------------------------------------------------------------- 1 | Package: jjs 2 | Version: 3 | Architecture: 4 | Maintainer: Mikail Bagishov 5 | Suggests: lxtrace 6 | Recommends: postgresql, build-essential 7 | Description: JJS testing system 8 | JJS testing system (see https://github.com/MikailBag/jjs) 9 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [bans] 2 | multiple-versions = "warn" 3 | 4 | [licenses] 5 | allow-osi-fsf-free = "both" 6 | allow = ["BSD-2-Clause", "Unlicense", "CC0-1.0"] 7 | unlicensed = "warn" -------------------------------------------------------------------------------- /docker-compose/README.md: -------------------------------------------------------------------------------- 1 | # WARNING 2 | 3 | This file may be out of date, because it isn't checked during CI. 4 | 5 | 6 | PRs are welcome. 7 | -------------------------------------------------------------------------------- /docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | x-volume: 3 | &data-volume 4 | type: 'volume' 5 | source: jjs_data 6 | target: /var/lib/jjs 7 | services: 8 | db: 9 | image: mongo 10 | init-data: 11 | image: debian:stable-slim 12 | entrypoint: /bin/sh -c 13 | command: > 14 | "/bin/mkdir /var/lib/jjs/etc 15 | && /bin/cp /configs/invoker.yaml /var/lib/jjs/etc/" 16 | volumes: 17 | - *data-volume 18 | - ./compose-config:/configs:ro 19 | apiserver: 20 | image: apiserver 21 | environment: 22 | MONGODB_CONNECTION_STRING: mongodb://db:27017/jjs 23 | depends_on: 24 | - db 25 | ports: 26 | - "1779:1779" 27 | invoker: 28 | image: invoker 29 | privileged: true 30 | environment: 31 | JJS_DATA: /var/lib/jjs 32 | JJS_AUTH_DATA_INLINE: "{\"endpoint\": \"http://apiserver:1779/\", \"auth\": {\"byToken\": {\"token\": \"Dev::root\"}}}" 33 | RUST_LOG: info,invoker=debug,util=debug,svaluer=debug 34 | RUST_BACKTRACE: full 35 | RUST_LIB_BACKTRACE: 1 36 | depends_on: 37 | - db 38 | - init-data 39 | ports: 40 | - "1789:1789" 41 | volumes: 42 | - *data-volume 43 | volumes: 44 | jjs_data: 45 | -------------------------------------------------------------------------------- /docker-compose/invoker.yaml: -------------------------------------------------------------------------------- 1 | workers: 4 2 | problems: 3 | mongodb: mongodb://db:27017/jjs 4 | -------------------------------------------------------------------------------- /example-config/apiserver.yaml: -------------------------------------------------------------------------------- 1 | # See share/schema/apiserver-config.json 2 | {} 3 | -------------------------------------------------------------------------------- /example-config/env.txt: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql://jjs:internal@localhost:5432/jjs 2 | RUST_BACKTRACE=full 3 | JJS_PATH=/opt/jjs 4 | RUST_LOG=info,invoker=debug,apiserver=debug 5 | JJS_SYSTEMD=1 6 | JJS_SECRET_KEY=will_be_deprecated_anyway -------------------------------------------------------------------------------- /example-config/invoker.yaml: -------------------------------------------------------------------------------- 1 | workers: 4 2 | sleep: 3 | max-ms: 5000 4 | 5 | # uncomment if you don't want separate toolchains dir. 6 | # host-toolchains: true -------------------------------------------------------------------------------- /example-config/objects/contests/trial.yaml: -------------------------------------------------------------------------------- 1 | id: trial 2 | title: "JJS demo contest" 3 | vis-unreg: true 4 | vis-anon: true 5 | group: ["Participants"] 6 | judges: ["Judges"] 7 | 8 | problems: 9 | - code: A 10 | name: a-plus-b 11 | - code: B 12 | name: array-sum 13 | - code: C 14 | name: sqrt 15 | -------------------------------------------------------------------------------- /example-problems/a-plus-b/generators/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | testgen::TestgenSession args = testgen::init(); 5 | printf("%d %d\n", args.test_id, args.test_id * 2 + 1); 6 | } -------------------------------------------------------------------------------- /example-problems/a-plus-b/generators/random.cpp: -------------------------------------------------------------------------------- 1 | #include "jjs/jtl.h" 2 | long long NUM = 1e18; 3 | 4 | long long gen(testgen::TestgenSession& sess) { 5 | uint64_t t = sess.gen.next_range(0, 2 * NUM); 6 | long long l = static_cast(t); 7 | return l - NUM; 8 | } 9 | 10 | int main() { 11 | auto sess = testgen::init(); 12 | auto a = gen(sess); 13 | auto b = gen(sess); 14 | printf("%lld %lld\n", a, b); 15 | } 16 | -------------------------------------------------------------------------------- /example-problems/a-plus-b/problem.toml: -------------------------------------------------------------------------------- 1 | primary-solution = "main" 2 | check-type = "builtin" 3 | name = "a-plus-b" 4 | title = "P == NP?" # Clickbait :) 5 | random-seed = "09c5d2237e9b0e60" 6 | valuer = "icpc" 7 | valuer-cfg = "valuer.yaml" 8 | 9 | [builtin-check] 10 | name = "cmp-tokens" 11 | 12 | [[tests]] 13 | map = "1..2" 14 | files = "%d.txt" 15 | group = "samples" 16 | 17 | [[tests]] 18 | map = "3..5" 19 | testgen = ["main"] 20 | group = "tests" 21 | 22 | [[tests]] 23 | map = "6..7" 24 | files = "%d.txt" 25 | group = "tests" 26 | 27 | [[tests]] 28 | map = "8..20" 29 | testgen = ["random"] 30 | group = "tests" -------------------------------------------------------------------------------- /example-problems/a-plus-b/solutions/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | long long a, b; 5 | scanf("%lld %lld", &a, &b); 6 | long long res = a + b; 7 | printf("%lld\n", res); 8 | return 0; 9 | } -------------------------------------------------------------------------------- /example-problems/a-plus-b/tests/1.txt: -------------------------------------------------------------------------------- 1 | 2 2 | 2 3 | -------------------------------------------------------------------------------- /example-problems/a-plus-b/tests/2.txt: -------------------------------------------------------------------------------- 1 | 0 2 | 7 3 | -------------------------------------------------------------------------------- /example-problems/a-plus-b/tests/6.txt: -------------------------------------------------------------------------------- 1 | 4 2 | 5 3 | -------------------------------------------------------------------------------- /example-problems/a-plus-b/tests/7.txt: -------------------------------------------------------------------------------- 1 | 2 2 | 2 3 | -------------------------------------------------------------------------------- /example-problems/a-plus-b/valuer.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: samples 3 | feedback: full 4 | score: 0 5 | - name: tests 6 | feedback: brief 7 | score: 100 8 | deps: 9 | - samples 10 | -------------------------------------------------------------------------------- /example-problems/array-sum/checkers/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace checker; 4 | 5 | int main() { 6 | CheckerInput args = init(); 7 | int n; 8 | test_scanf("%d", &n); 9 | long long ans = 0; 10 | for (int i = 0; i < n; i++) { 11 | int cur; 12 | test_scanf("%d", &cur); 13 | ans += cur; 14 | } 15 | long long sol_ans; 16 | sol_scanf("%lld", &sol_ans); 17 | check_test_eof(); 18 | check_sol_eof(); 19 | finish((sol_ans == ans) ? Outcome::OK : Outcome::WRONG_ANSWER); 20 | return 0; 21 | } 22 | -------------------------------------------------------------------------------- /example-problems/array-sum/generators/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() { 5 | testgen::TestgenSession sess = testgen::init(); 6 | int num_cnt = sess.gen.next_range(1, 10001); 7 | printf("%d\n%ld", num_cnt, sess.gen.next_range(1, 10001)); 8 | for (int i = 1; i < num_cnt; i++) { 9 | printf(" %ld", sess.gen.next_range(1, 10001)); 10 | } 11 | printf("\n"); 12 | return 0; 13 | } 14 | -------------------------------------------------------------------------------- /example-problems/array-sum/problem.toml: -------------------------------------------------------------------------------- 1 | check-type = "custom" 2 | name = "array-sum" 3 | title = "Finding Maximal Flow in Highly Parallel Networks" 4 | random-seed = "c4142a74815ae779" 5 | valuer = "icpc" 6 | valuer-cfg = "valuer.yaml" 7 | 8 | [custom-check] 9 | pass-correct = false 10 | 11 | [[tests]] 12 | map = "1..2" 13 | files = "%d.txt" 14 | group = "samples" 15 | 16 | [[tests]] 17 | map = "3..20" 18 | testgen = ["main"] 19 | group = "tests" 20 | -------------------------------------------------------------------------------- /example-problems/array-sum/tests/1.txt: -------------------------------------------------------------------------------- 1 | 3 2 | 1 2 3 3 | -------------------------------------------------------------------------------- /example-problems/array-sum/tests/2.txt: -------------------------------------------------------------------------------- 1 | 5 2 | 1 2 3 4 5 3 | -------------------------------------------------------------------------------- /example-problems/array-sum/valuer.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: samples 3 | feedback: full 4 | score: 0 5 | - name: tests 6 | feedback: brief 7 | score: 100 8 | deps: 9 | - samples 10 | -------------------------------------------------------------------------------- /example-problems/sqrt/problem.toml: -------------------------------------------------------------------------------- 1 | title = "Calc sqrt" 2 | name = "sqrt" 3 | random-seed = "73ef6c014b4b2aa7" 4 | check-type = "builtin" 5 | primary-solution = "main" 6 | valuer = "icpc" 7 | valuer-cfg = "valuer.yaml" 8 | 9 | [builtin-check] 10 | name = "cmp-tokens" 11 | cmd = ["--epsilon", "0.000001"] 12 | 13 | [[tests]] 14 | map = "1..3" 15 | files = "%d.txt" 16 | group = "samples" 17 | -------------------------------------------------------------------------------- /example-problems/sqrt/solutions/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() { 5 | char buf[20]; 6 | scanf("%s", buf); 7 | long double res = strtold(buf, nullptr); 8 | res = std::sqrt(res); 9 | printf("%lf", (double) res); 10 | } -------------------------------------------------------------------------------- /example-problems/sqrt/tests/1.txt: -------------------------------------------------------------------------------- 1 | 4 -------------------------------------------------------------------------------- /example-problems/sqrt/tests/2.txt: -------------------------------------------------------------------------------- 1 | 0 -------------------------------------------------------------------------------- /example-problems/sqrt/tests/3.txt: -------------------------------------------------------------------------------- 1 | 6.25 -------------------------------------------------------------------------------- /example-problems/sqrt/valuer.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: samples 3 | feedback: full 4 | score: 0 5 | -------------------------------------------------------------------------------- /k8s/jjs/.helmignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /k8s/jjs/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: jjs 3 | description: JJS judging system 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | appVersion: 0.0.0 24 | -------------------------------------------------------------------------------- /k8s/jjs/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | JJS successfully installed into namespace {{ .Release.Namespace }}! 2 | See usage docs at jjs-dev.github.io/jjs/man. 3 | 4 | Apiserver DNS name is apiserver.{{ .Release.Namespace }}. 5 | 6 | {{- if has .Values.services.apiserver.serviceType (list "NodePort" "LoadBalancer" ) }} 7 | Additionally, apiserver is available on dedicated port on each cluster node. To get this port, run 8 | $ kubectl get service apiserver -n {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" 9 | (You may need to wait until NodePort is assigned). 10 | {{- end }} 11 | -------------------------------------------------------------------------------- /k8s/jjs/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "jjs.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 7 | {{- end }} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "jjs.fullname" -}} 15 | {{- if .Values.fullnameOverride }} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 17 | {{- else }} 18 | {{- $name := default .Chart.Name .Values.nameOverride }} 19 | {{- if contains $name .Release.Name }} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 21 | {{- else }} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 23 | {{- end }} 24 | {{- end }} 25 | {{- end }} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "jjs.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 32 | {{- end }} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "jjs.labels" -}} 38 | helm.sh/chart: {{ include "jjs.chart" . }} 39 | {{ include "jjs.selectorLabels" . }} 40 | {{- if .Chart.AppVersion }} 41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 42 | {{- end }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | {{- end }} 45 | 46 | {{/* 47 | Selector labels 48 | */}} 49 | {{- define "jjs.selectorLabels" -}} 50 | app.kubernetes.io/name: {{ include "jjs.name" . }} 51 | app.kubernetes.io/instance: {{ .Release.Name }} 52 | {{- end }} 53 | 54 | {{/* 55 | Create the name of the service account to use 56 | */}} 57 | {{- define "jjs.serviceAccountName" -}} 58 | {{- if .Values.serviceAccount.create }} 59 | {{- default (include "jjs.fullname" .) .Values.serviceAccount.name }} 60 | {{- else }} 61 | {{- default "default" .Values.serviceAccount.name }} 62 | {{- end }} 63 | {{- end }} 64 | -------------------------------------------------------------------------------- /k8s/jjs/templates/apiserver.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: apiserver 5 | labels: 6 | app: apiserver 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: apiserver 12 | template: 13 | metadata: 14 | labels: 15 | app: apiserver 16 | spec: 17 | containers: 18 | - name: apiserver 19 | image: "{{ .Values.image.repositoryPrefix }}apiserver:{{ .Values.image.tag }}" 20 | imagePullPolicy: {{ .Values.image.pullPolicy }} 21 | securityContext: 22 | readOnlyRootFilesystem: true 23 | runAsUser: 10779 24 | runAsGroup: 10779 25 | ports: 26 | - name: http 27 | containerPort: 1779 28 | protocol: TCP 29 | env: 30 | - name: MONGODB_CONNECTION_STRING 31 | value: mongodb://mongodb/jjs 32 | readinessProbe: 33 | httpGet: 34 | # TODO: implement ready 35 | # path: /ready 36 | path: /system/api-version 37 | port: http 38 | --- 39 | apiVersion: v1 40 | kind: Service 41 | metadata: 42 | name: apiserver 43 | spec: 44 | type: {{ .Values.services.apiserver.serviceType }} 45 | ports: 46 | - port: 1779 47 | targetPort: http 48 | protocol: TCP 49 | name: http 50 | selector: 51 | app: apiserver -------------------------------------------------------------------------------- /k8s/jjs/templates/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: jjs-config 5 | data: 6 | judge: | 7 | workers: 4 8 | problems: 9 | mongodb: mongodb://mongodb/jjs -------------------------------------------------------------------------------- /k8s/jjs/templates/ejui.yaml: -------------------------------------------------------------------------------- 1 | #{{- if .Values.extras.ejui.enabled }} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: ejui 6 | labels: 7 | app: ejui 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: ejui 13 | template: 14 | metadata: 15 | labels: 16 | app: ejui 17 | spec: 18 | #{{- if .Values.extras.ejui.imagePullSecretName }} 19 | imagePullSecrets: 20 | - name: "{{ .Values.extras.ejui.imagePullSecretName }}" 21 | #{{- end }} 22 | containers: 23 | - name: ejui 24 | image: "{{ .Values.extras.ejui.image }}" 25 | imagePullPolicy: Always 26 | securityContext: 27 | readOnlyRootFilesystem: true 28 | runAsUser: 10779 29 | runAsGroup: 10779 30 | ports: 31 | - name: ui 32 | containerPort: 8000 33 | protocol: TCP 34 | env: 35 | - name: PYTHONUNBUFFERED 36 | value: "true" 37 | args: ["0.0.0.0:8000", "http+jjs://apiserver:1779/?"] 38 | --- 39 | # Allows all ingress traffic 40 | apiVersion: networking.k8s.io/v1 41 | kind: NetworkPolicy 42 | metadata: 43 | name: ejui 44 | spec: 45 | podSelector: 46 | matchLabels: 47 | app: ejui 48 | policyTypes: 49 | - Ingress 50 | ingress: 51 | - from: 52 | - podSelector: {} 53 | #{{- end }} 54 | -------------------------------------------------------------------------------- /k8s/jjs/templates/invoker.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: invoker 5 | rules: 6 | - apiGroups: [""] 7 | resources: ["configmaps"] 8 | verbs: ["get"] 9 | --- 10 | apiVersion: v1 11 | kind: ServiceAccount 12 | metadata: 13 | name: invoker 14 | --- 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: RoleBinding 17 | metadata: 18 | name: invoker 19 | subjects: 20 | - kind: ServiceAccount 21 | name: invoker 22 | apiGroup: "" 23 | roleRef: 24 | kind: Role 25 | name: invoker 26 | apiGroup: rbac.authorization.k8s.io 27 | --- 28 | apiVersion: apps/v1 29 | kind: Deployment 30 | metadata: 31 | name: invoker 32 | labels: 33 | app: invoker 34 | # {{- if .Values.dev.kubeScore }} 35 | annotations: 36 | kube-score/ignore: container-security-context 37 | # {{- end }} 38 | spec: 39 | replicas: 1 40 | selector: 41 | matchLabels: 42 | app: invoker 43 | template: 44 | metadata: 45 | labels: 46 | app: invoker 47 | spec: 48 | serviceAccountName: invoker 49 | containers: 50 | - name: invoker 51 | env: 52 | - name: RUST_LOG 53 | value: info,invoker=trace,problem_loader=trace,puller=trace 54 | - name: JJS_AUTH_DATA_INLINE 55 | value: '{"endpoint": "http://apiserver:1779/", "auth": {"byToken": {"token": "Dev::root"}}}' 56 | image: "{{ .Values.image.repositoryPrefix }}invoker:{{ .Values.image.tag }}" 57 | imagePullPolicy: {{ .Values.image.pullPolicy }} 58 | securityContext: 59 | privileged: true 60 | ports: 61 | - name: http 62 | containerPort: 1789 63 | protocol: TCP 64 | readinessProbe: 65 | httpGet: 66 | path: /ready 67 | port: http 68 | --- 69 | apiVersion: v1 70 | kind: Service 71 | metadata: 72 | name: invoker 73 | spec: 74 | type: ClusterIP 75 | ports: 76 | - port: 1789 77 | targetPort: http 78 | protocol: TCP 79 | name: http 80 | selector: 81 | app: invoker 82 | -------------------------------------------------------------------------------- /k8s/jjs/templates/network-policy.yaml: -------------------------------------------------------------------------------- 1 | #{{- if .Values.networkPolicy.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: NetworkPolicy 4 | metadata: 5 | name: builtin-apiserver 6 | spec: 7 | podSelector: 8 | matchLabels: 9 | app: apiserver 10 | policyTypes: 11 | - Ingress 12 | ingress: 13 | - from: 14 | - podSelector: {} 15 | --- 16 | apiVersion: networking.k8s.io/v1 17 | kind: NetworkPolicy 18 | metadata: 19 | name: builtin-invoker 20 | spec: 21 | podSelector: 22 | matchLabels: 23 | app: invoker 24 | policyTypes: 25 | - Ingress 26 | ingress: [] 27 | 28 | #{{ if .Values.toolchains }} 29 | --- 30 | apiVersion: networking.k8s.io/v1 31 | kind: NetworkPolicy 32 | metadata: 33 | name: builtin-init-deny-ingress 34 | spec: 35 | podSelector: 36 | matchLabels: 37 | app: init 38 | policyTypes: 39 | - Ingress 40 | ingress: [] 41 | 42 | #{{- end }} 43 | 44 | #{{- end }} 45 | -------------------------------------------------------------------------------- /k8s/jjs/values.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | repositoryPrefix: gcr.io/jjs-dev/ 3 | tag: latest 4 | pullPolicy: Always 5 | toolchains: 6 | official: true 7 | extras: 8 | ejui: 9 | enabled: true 10 | image: docker.pkg.github.com/sleirsgoevy/ejui/ejui:master 11 | services: 12 | apiserver: 13 | serviceType: ClusterIP 14 | networkPolicy: 15 | enabled: true 16 | dev: 17 | kubeScore: false 18 | -------------------------------------------------------------------------------- /man/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Mikail Bagishov"] 3 | multilingual = false 4 | src = "src" 5 | title = "JJS Manual" 6 | -------------------------------------------------------------------------------- /man/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Running](running.md) 4 | - [Clients](clients.md) 5 | - [Apiserver usage Introduction](apiserver.md) 6 | - [Installing JJS in Virtual Machine](vm.md) 7 | - [Sysroot Layout](sysroot.md) 8 | - [Userlist](userlist.md) 9 | - [Authentication](authentication.md) 10 | - [Problem Preparation](problems/README.md) 11 | - [Package Types](problems/packages.md) 12 | - [PPC](problems/ppc.md) 13 | - [Writing Test Generators](problems/testgen.md) 14 | - [Writing Checkers](problems/checker.md) 15 | - [Importing from Polygon](problems/polygon.md) 16 | - [Authorization and Access Control](authorization.md) -------------------------------------------------------------------------------- /man/src/apiserver.md: -------------------------------------------------------------------------------- 1 | # Working with Apiserver 2 | 3 | ## Overview 4 | Apiserver exposes HTTP REST API. 5 | Visit [JJS documantaion](https://jjs-dev.github.io/jjs/api) for api details. 6 | 7 | ## Authentication 8 | All API methods from non-`auth` category, and authDrop (TODO: not implemented) require request to be authenticated. 9 | Authentication can be performed in various ways, e.g. see `authAnonymous` and `authSimple`. 10 | These methods return you `AuthToken` object, containing token. 11 | This token should be provided in all subsequent requests: 12 | ` 13 | // when logging in 14 | let token: AuthToken = ... 15 | 16 | // later, when doing request 17 | req.set_header("Authorization", "Token ${token.data}") 18 | ` 19 | 20 | ## Logout 21 | (TODO: not implemented) 22 | Just call `authDrop` with desired token in `Authorization` header, and token will be revoked. -------------------------------------------------------------------------------- /man/src/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | ## Root 3 | JJS has builtin root user, with username "$root" (Note that $ prefix can't be used in non-special user accounts). 4 | You can not log into this user using primary api. 5 | ### Development root user 6 | When JJS is running in development mode, you can authenticate as root, using "dev_root" string as access token 7 | This option is automatically disabled in non-development environment, and shouldn't be turned on forcefully. 8 | ### Local Root login service in Linux 9 | This server is started automatically by apiserver and is bound to `/tmp/jjs-auth-sock`. 10 | You should connect to this socket from a process, uid of which exactly matches uid of apiserver. 11 | Local auth server will write string of form `===S===`, where S is root access token. 12 | ## Normal users 13 | See [`createUser`](https://mikailbag.github.io/jjs/api/mutation.doc.html). -------------------------------------------------------------------------------- /man/src/authorization.md: -------------------------------------------------------------------------------- 1 | # Authorization and Access Control 2 | ## Sudo mode 3 | Sudo mode for some scope (global or contest) disables any further right checking for objects and resources 4 | in this scope, including: 5 | - submitting and managing submissions 6 | - managing contest state (start, stop, remove) 7 | - grant additional rights (except SUDO bit) to other users -------------------------------------------------------------------------------- /man/src/clients.md: -------------------------------------------------------------------------------- 1 | # Clients 2 | 3 | JJS itself (i.e. `apiserver`) doesn't provide any user interface. 4 | Instead, it has HTTP REST API (see [apiserver](apiserver.md) for more details). 5 | Clients are independent apps built on top of this API, which _have_ UI. 6 | 7 | ## Client list 8 | 9 | * [ejui](https://github.com/sleirsgoevy/ejui) - official web-client 10 | * [jjs-cli](https://github.com/mikailbag/jjs/tree/master/cli) - official CLI client -------------------------------------------------------------------------------- /man/src/problems/README.md: -------------------------------------------------------------------------------- 1 | # Problem preparation 2 | This section describes how you can prepare problems for use in JJS -------------------------------------------------------------------------------- /man/src/problems/checker.md: -------------------------------------------------------------------------------- 1 | # Writing Checkers 2 | ## Checker protocol 3 | - `JJS_CORR` - r-handle to file with test correct answer 4 | - `JJS_SOL` - r-handle to file with provided answer 5 | - `JJS_TEST` - r-handle to test input 6 | - `JJS_CHECKER_OUT` - w-handle for checker output file. It is described below 7 | - `JJS_CHECKER_COMMENT` - w-handle for comments file. It's content will be preserved and output into judge log as is. 8 | 9 | ### Output file format 10 | Output file consists of entries. Each entry occupies one line and has format: 11 | 12 | `TAG: VALUE`. 13 | 14 | Currently, following tags are supported: 15 | - `outcome`: must meet exactly once. Corresponding value is checker outcome. 16 | If this entry is not present, or present more than once, or can't be parsed, `JUDGE_FAULT` status is diagnosed 17 | 18 | Outcome list: 19 | - `Ok` 20 | - `WrongAnswer` 21 | - `PresentationError` 22 | - `CheckerLogicError` -------------------------------------------------------------------------------- /man/src/problems/packages.md: -------------------------------------------------------------------------------- 1 | # Package types 2 | There are two package types in JJS: _source packages_, and _compiled packages_. 3 | ## Compiled packages 4 | Compiled packages are read directly by JJS invoker. They are not intended for human-reading. 5 | Compiled package for problem with id FOO is placed in $ROOT/var/problems/FOO. 6 | ## Source packages 7 | These packages are optional (you can compile problems with your own tooling). 8 | See more about them in [PPC](ppc.md) page. -------------------------------------------------------------------------------- /man/src/problems/polygon.md: -------------------------------------------------------------------------------- 1 | # Importing from Polygon 2 | 3 | ## Step 1: Download polygon package 4 | - Go to 'Packages' section 5 | - Request 'Standard' package generation 6 | - Now wait several minutes, until package status is `RUNNING` 7 | - Download generated archive 8 | - Extract it to some location, referred as `$POLYGON_PKG` 9 | 10 | Note: other package types ('Windows' and 'Linux', available for 'Full' packages) 11 | can be used for import too. However, they consume more space, and Full package are generated 12 | slower, so Standard packages are recommended 13 | 14 | 15 | ## Step 2: Import Ppc package from polygon package 16 | - Prepare some directory for ppc package, referred as `$PPC_PKG` 17 | - Run: `ppc import --pkg $POLYGON_PKD --out $PPC_PKG` 18 | 19 | ## Step 3: Compile invoker package as usual 20 | Let `$INVOKER_PKG` be target path. 21 | 22 | Run `ppc compile --pkg $PPC_PKG --out $INVOKER_PKG` -------------------------------------------------------------------------------- /man/src/problems/ppc.md: -------------------------------------------------------------------------------- 1 | # PPC 2 | PPC (problem package compiler) is tool, which allows you to prepare problems without extra work. 3 | It compiles source packages into compiled packages. 4 | ## Source package structure 5 | TODO 6 | ## Features 7 | - Develop problems using easy-to-use workflow (see ppc-compile) 8 | - Import problems from Polygon (see [ppc-import](./polygon.md)) -------------------------------------------------------------------------------- /man/src/problems/testgen.md: -------------------------------------------------------------------------------- 1 | # Writing test generators 2 | Test generator (aka testgen) is a program that generates test, based on test id 3 | They are located in `testgens` subdirectory of source problem package 4 | ## Protocol 5 | Following environment variables will be set: 6 | - `JJS_TEST_ID` - test id, small number 7 | - `JJS_TEST` - writeable handle, where testgen should write generated test. 8 | 9 | Testgen exit code will be analyzed in following way: 10 | - 0 - test generated successfully 11 | - (all other) - test generation failed -------------------------------------------------------------------------------- /man/src/running.md: -------------------------------------------------------------------------------- 1 | # Running JJS 2 | 3 | ## Database 4 | 5 | Some JJS components use database. 6 | How to get it working: 7 | - Install PostgresQL version 11 8 | - Set POSTGRES_URL to connection URI for database (e.g. `postgres://jjs:internal@localhost:5432/jjs` 9 | means: host -> `localhost`, port -> `5432`, username -> `jjs`, password -> `internal`, database name -> `jjs`. 10 | See Postgres docs for more details.) 11 | - (Optional, recommended for serious usage) configure access rights for DB user used in previous step. 12 | 13 | Note: You _can_ use different connection URI for different JJS instances, but they **must** refer to same database -------------------------------------------------------------------------------- /man/src/sysroot.md: -------------------------------------------------------------------------------- 1 | # Sysroot Layout 2 | * /opt is a nested sysroot (i.e. it should contain something like `./bin`, `./lib` etc). It's contents will be 3 | exposed to sandbox at both build and run time. It __must not__ contain any files or symlinks (only directories), 4 | and it __must not__ contain `./jjs` directory 5 | * /etc contains JJS config files: 6 | - /etc/jjs.toml - main config; 7 | - /etc/toolchains/*.toml - toolchain configs 8 | * /var/submissions contains submissions info 9 | * /var/submissions/s- contains some submission data 10 | * /var/submissions/s-/j- contains info for a particular judging of submission 11 | (submission can be judged multiple times e.g. due to rejudge) 12 | 13 | Sysroot path is referred throughout the manual as $ROOT 14 | 15 | ## Creating sysroot 16 | 17 | __Note__: When running JJS on cluster, make sure sysroot is shared (e.g., using NFS) between all instances. 18 | 19 | * Initialize directory structure. You can use `jjs-mkroot` CLI utility for it. 20 | * Configure JJS (TODO: page about it). 21 | * Setup `$ROOT/opt` - toolchain root. 22 | 23 | You can use various strategies to setup toolchain root. 24 | The simplest is to bind-mount / - this is appropriate for testing, but it is unsecure (as a possible attack, hacker can cpp-include /etc/shadow). 25 | Another option is to rebuild all compilers and interpreters from source with appropriate configure options. However, this approach can take much time. 26 | Finally, you can use `ptrace`-based toolchain installing. It is secure, fast and easy to use. check `soft/example-linux.ps1` for details. -------------------------------------------------------------------------------- /man/src/userlist.md: -------------------------------------------------------------------------------- 1 | # Userlist 2 | Userlist is a tool, which helps you managing users. 3 | ## Adding users 4 | 1. Prepare file, containing users data. 5 | 2. Run `jjs-userlist add path/to/file`. For additional options (including authentication), run `jjs-userlist --help`. 6 | 7 | File format: 8 | Each line is handled separately. It can be 9 | - Configuration update entry. 10 | - User entry. It must contain login, password, and groups. 11 | ### User Entry Format 12 | `add [SETTINGS]` 13 | ### Header Entry Format 14 | `cfg SETTINGS` 15 | ### Supported Options and Flags 16 | Setting are comma-separated. 17 | To enable flag, pass it's name. 18 | To set option, use `option_name=option_value` 19 | Currently, following flags are supported: 20 | - `base64` - if enabled, `userlist` decodes all other entities (logins, passwords, groups) in file from base64. 21 | This option is recommended to use if login, password or other piece of information can contain non-ASCII chars. 22 | - `ignore-fail` - if enabled, `userlist` will continue creating users, ignoring possible errors. 23 | Following options are supported: 24 | - `groups` (takes colon-separated list of groups) - adds groups to implicit list. When user is created, 25 | it is added to all groups from this list. 26 | - `set-groups` - same as `groups`, but clears that implicit list instead of appending. -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | merge_imports = true 2 | force_explicit_abi = true 3 | reorder_imports = true 4 | reorder_modules = true 5 | reorder_impl_items = true 6 | use_field_init_shorthand = true 7 | format_code_in_doc_comments = true 8 | edition = "2018" 9 | merge_derives = true 10 | newline_style = "Unix" 11 | report_fixme = "Unnumbered" 12 | unstable_features = true 13 | version = "Two" -------------------------------------------------------------------------------- /scripts/dev-cgroups.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # configures cgroups v2 for use with invoker 4 | set -e 5 | echo "THIS IS INSECURE SETUP" 6 | echo "NEVER USE IN PRODUCTION" 7 | user="$(whoami)" 8 | root="/sys/fs/cgroup" 9 | echo "Delegating ${root} to ${user}" 10 | sudo chown "$user" "$root" 11 | sudo chmod +w "$root/" 12 | sudo chown "$user" "$root/cgroup.procs" 13 | sudo mkdir -p "$root/jjs" 14 | sudo chown -R "$user" "$root/jjs" 15 | echo "+pids +memory +cpu" | sudo tee "$root/jjs/cgroup.subtree_control" 16 | -------------------------------------------------------------------------------- /scripts/process-strace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | if len(sys.argv) != 3: 5 | print(f"Usage: {sys.argv[0]} ") 6 | exit(1) 7 | 8 | total_lines = 0 9 | filter_sigill = 0 10 | accepted_lines = 0 11 | 12 | fin = open(sys.argv[1]) 13 | fout = open(sys.argv[2], 'w') 14 | 15 | sigilled = set() 16 | 17 | for line in fin: 18 | total_lines += 1 19 | pid = line.split()[0] 20 | pid = int(pid) 21 | is_sigill = "--- SIGILL {si_signo=SIGILL, si_code=ILL_ILLOPN," in line or "--- SIGSEGV {si_signo=SIGSEGV" in line 22 | if is_sigill: 23 | if pid in sigilled: 24 | filter_sigill += 1 25 | continue 26 | else: 27 | sigilled.add(pid) 28 | accepted_lines += 1 29 | fout.write(line) 30 | 31 | total_filtered = total_lines - accepted_lines 32 | 33 | print(f"Got {total_lines} lines, {total_filtered} filtered out, {accepted_lines} included in report") 34 | print("Filter summary") 35 | print(f"\t duplicate SIGs: {filter_sigill}") 36 | -------------------------------------------------------------------------------- /scripts/strace/remove_pid.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "x$2" == x ] || [ "x$3" != x ] 4 | then echo "usage: $0 " 5 | else grep -v "^$2[^0-9]" "$1" 6 | fi 7 | -------------------------------------------------------------------------------- /scripts/strace/remove_rt_sigaction.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "x$1" == x ] || [ "x$1" == x--help ] || [ "x$1" == x-h ] 4 | then echo "usage: $0 " 5 | else grep -v '^[^"]*rt_sigaction' "$1" 6 | fi 7 | -------------------------------------------------------------------------------- /scripts/uml-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck shell=sh 4 | # wait for jjs-frontend to start up 5 | while ! wget http://127.0.0.1:1779/ -O - >/dev/null 2>&1 6 | do true 7 | done 8 | 9 | cat > test.cpp << EOF 10 | #include 11 | int main(){int64_t a,b;std::cin>>a>>b;std::cout< 7 | JJS_BEARER= 8 | # if JJS is running in development mode, use "Dev::root" 9 | python -m pytest . 10 | ``` -------------------------------------------------------------------------------- /smoke/requirements.txt: -------------------------------------------------------------------------------- 1 | requests ~= 2.22.0 2 | pytest ~= 5.4.2 3 | -------------------------------------------------------------------------------- /smoke/test_judging.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import base64 3 | import os 4 | import time 5 | 6 | sess = requests.Session() 7 | sess.headers.update( 8 | { 9 | "Authorization": f"Bearer {os.environ['JJS_BEARER']}" 10 | } 11 | ) 12 | 13 | ENDPOINT = os.environ["JJS_API"] 14 | 15 | A_PLUS_B = """ 16 | #include 17 | int main() { 18 | long long a, b; 19 | std::cin >> a >> b; 20 | std::cout << a + b << std::endl; 21 | } 22 | """ 23 | 24 | 25 | def test_basic_judging(): 26 | created_run = sess.post(f"{ENDPOINT}/runs", json={ 27 | 'code': base64.b64encode(A_PLUS_B.encode()).decode(), 28 | 'contest': 'trial', 29 | 'problem': 'a-plus-b', 30 | 'toolchain': 'gcc-cpp' 31 | }) 32 | created_run.raise_for_status() 33 | assert created_run.json()["status"] == {} 34 | run_id = created_run.json()["id"] 35 | ok = False 36 | for _i in range(75): 37 | run_state = sess.get(f"{ENDPOINT}/runs/{run_id}") 38 | if run_state.json()["status"].get("full") is not None: 39 | assert run_state.json()["status"]["full"] == "Accepted:ACCEPTED" 40 | ok = True 41 | break 42 | time.sleep(1) 43 | assert ok 44 | -------------------------------------------------------------------------------- /src/apiserver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.3-slim 2 | COPY requirements.txt /tmp/req.txt 3 | RUN pip3 install -r /tmp/req.txt 4 | COPY . /app 5 | WORKDIR /app 6 | EXPOSE 1779 7 | ENTRYPOINT ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "1779"] 8 | -------------------------------------------------------------------------------- /src/apiserver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjs-dev/jjs/bf00423f70f8a6ed508bbcb8b38840225e43cc73/src/apiserver/__init__.py -------------------------------------------------------------------------------- /src/apiserver/api_models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import uuid 3 | import pydantic 4 | import typing 5 | import db_models 6 | 7 | 8 | class ApiVersion(pydantic.BaseModel): 9 | major: int 10 | minor: int 11 | 12 | 13 | class Run(pydantic.BaseModel): 14 | id: uuid.UUID 15 | """ 16 | Run identifier, unique in the system. 17 | """ 18 | toolchain_name: str 19 | problem_name: str 20 | user_id: uuid.UUID 21 | contest_name: str 22 | status: typing.Mapping[str, str] = pydantic.Field(default_factory=dict) 23 | 24 | @staticmethod 25 | def from_db(doc: db_models.RunMainProj) -> Run: 26 | return Run(id=doc['id'], toolchain_name=doc['toolchain_name'], 27 | user_id=doc['user_id'], contest_name=doc['contest_name'], problem_name=doc['problem_name'], status=doc['status']) 28 | 29 | 30 | class Toolchain(pydantic.BaseModel): 31 | id: str 32 | description: str 33 | image: str 34 | 35 | 36 | class LiveStatus(pydantic.BaseModel): 37 | current_test: typing.Optional[int] 38 | current_score: typing.Optional[int] 39 | finished: bool 40 | 41 | 42 | class Contest(pydantic.BaseModel): 43 | id: str 44 | """ 45 | Configured by human, something readable like 'olymp-2019', or 'test-contest' 46 | """ 47 | title: str 48 | """ 49 | E.g. 'Berlandian Olympiad in Informatics. Finals. Day 3.' 50 | """ 51 | 52 | 53 | class Problem(pydantic.BaseModel): 54 | name: str 55 | """ 56 | Problem name, e.g. a-plus-b 57 | """ 58 | rel_name: str 59 | """ 60 | Problem relative name (aka problem code) as contestants see. This is usually one letter, e.g. 'A' or '3F'. 61 | """ 62 | title: str 63 | """ 64 | Problem title as contestants see, e.g. 'Find max flow'. 65 | """ 66 | 67 | 68 | class SessionToken(pydantic.BaseModel): 69 | data: str 70 | "Session token for the newly-created user session." 71 | -------------------------------------------------------------------------------- /src/apiserver/auth.py: -------------------------------------------------------------------------------- 1 | import fastapi 2 | import pydantic 3 | import typing 4 | import uuid 5 | 6 | 7 | class Session(pydantic.BaseModel): 8 | token: str 9 | "To be passed in the `Authorization` header by the client." 10 | user_id: uuid.UUID 11 | "UUID of the session owner." 12 | roles: typing.List[str] 13 | "List of roles the user is in. Cached here to avoid looking up the user in DB." 14 | 15 | def is_root(self): 16 | return self.user_id == uuid.UUID('000000000000-0000-0000-000000000000') 17 | 18 | def has_role(self, g: str) -> bool: 19 | return self.is_root() or g in self.roles 20 | 21 | def ensure_role(self, g: str): 22 | if not self.has_role(g): 23 | raise fastapi.HTTPException(403, 24 | detail='You do not have role `{}`'.format(g)) 25 | 26 | 27 | Session.FIELDS = ['token', 'user_id', 'roles'] 28 | -------------------------------------------------------------------------------- /src/apiserver/db_connect.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pymongo 3 | import urllib.parse 4 | 5 | 6 | def db_connect_url(db_url: str) -> pymongo.database.Database: 7 | """ 8 | This function connects to MongoDB database using provided URL 9 | 10 | URL must look like mongodb://[used:password@]host:port/database_name 11 | all except database_name is forwarded to mongodb client, database_name 12 | denotes concrete database on mongodb cluster. 13 | Port usually is 27017. 14 | 15 | Example for mongodb instance, running on localhost without authentication: 16 | mongodb://localhost:27017/jjs 17 | Of course, any other database name can be used. 18 | """ 19 | 20 | # strip is used, because urllib returns path in form `/jjs`, 21 | # so we want to strip leading '/' 22 | db_name = urllib.parse.urlparse(db_url).path[1:] 23 | 24 | client = pymongo.MongoClient(db_url) 25 | 26 | return client[db_name] 27 | 28 | 29 | def db_connect_via_env() -> pymongo.database.Database: 30 | """ 31 | Connects to MongoDB database using URL in MONGODB_CONNECTION_STRING 32 | environment variable. See `db_connect_url` function for url format. 33 | """ 34 | db_url = os.environ["MONGODB_CONNECTION_STRING"] 35 | return db_connect_url(db_url) 36 | -------------------------------------------------------------------------------- /src/apiserver/db_models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from enum import Enum 3 | import time 4 | import typing 5 | from pydantic import BaseModel, Field 6 | 7 | 8 | class RunPhase(Enum): 9 | """ 10 | # QUEUED 11 | Run enters this state when received. 12 | In this state run can be returned to invoker for judging. 13 | # LOCKED 14 | Some invoker is judging this run right now. 15 | There are two possibilities: 16 | 1) Judging finished, and run transitions to the FINISHED state. 17 | 2) Timeout is hit, and run transitions to the QUEUED state. (TBD) 18 | # FINISHED 19 | Judging is finished. All files and statuses are uploaded. 20 | """ 21 | QUEUED = "queued" 22 | LOCKED = "locked" 23 | FINISHED = "finished" 24 | 25 | 26 | class RunMainProj(BaseModel): 27 | id: uuid.UUID 28 | toolchain_name: str 29 | problem_name: str 30 | user_id: uuid.UUID 31 | contest_name: str 32 | phase: str # RunPhase 33 | status: typing.Mapping[str, str] = Field(default_factory=dict) 34 | 35 | """ 36 | Each item is (protocol_kind, f"{status_kind}:{status_code}" as in invoker_api::Status). 37 | """ 38 | 39 | 40 | RunMainProj.FIELDS = ['id', 'toolchain_name', 41 | 'problem_name', 'user_id', 'contest_name', 'status'] 42 | 43 | 44 | class RunSourceProj(BaseModel): 45 | source: typing.Optional[bytes] 46 | 47 | 48 | class RunBinaryProj(BaseModel): 49 | binary: typing.Optional[bytes] 50 | 51 | 52 | class RunProtocolsProj(BaseModel): 53 | protocols: typing.Mapping[str, str] = Field(default_factory=dict) 54 | """ 55 | Key: invoker_api::judge_log::JudgeLogKind 56 | Value: json-encoded invoker_api::judge_log::JudgeLog 57 | """ 58 | 59 | 60 | class User(BaseModel): 61 | id: uuid.UUID 62 | "Unique identifier of the user." 63 | login: str 64 | "Username, as specified by the user." 65 | password_hash: str 66 | "Base64-encoded SHA3 hash of the password." 67 | roles: typing.List[str] 68 | "List of roles the user is in." 69 | 70 | 71 | User.FIELDS = ['id', 'login', 'password_hash', 'roles'] 72 | -------------------------------------------------------------------------------- /src/apiserver/main.py: -------------------------------------------------------------------------------- 1 | import fastapi.encoders 2 | import os 3 | import json 4 | import typing 5 | import uuid 6 | import pymongo 7 | import urllib.parse 8 | import functools 9 | import base64 10 | import routes 11 | import db_connect 12 | 13 | app = routes.create_app(db_connect.db_connect_via_env) 14 | 15 | 16 | if os.environ.get("__JJS_SPEC") is not None: 17 | req = os.environ["__JJS_SPEC"] 18 | if req == "openapi": 19 | print(json.dumps(app.openapi())) 20 | else: 21 | raise ValueError(f"unsupported __JJS_SPEC: {req}") 22 | -------------------------------------------------------------------------------- /src/apiserver/requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | fastapi ~= 0.54.2 3 | pydantic ~= 1.5.1 4 | uvicorn ~= 0.11.7 5 | pytest ~= 5.4.2 6 | requests ~= 2.22.0 7 | pymongo ~= 3.10.1 8 | bcrypt ~= 3.1.7 9 | python-multipart ~= 0.0.5 -------------------------------------------------------------------------------- /src/apiserver_old/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "apiserver" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | apiserver-engine = {path = "../apiserver-engine"} 9 | rocket = {git = "https://github.com/SergioBenitez/rocket", branch = "async", features = []} # TODO: features = ["tls"] 10 | entity = {path = "../entity"} 11 | db = {path = "../db"} 12 | dotenv = "0.15.0" 13 | util = {path = "../util"} 14 | problem-loader = {path = "../problem-loader"} 15 | anyhow = "1.0.28" 16 | serde = { version = "1.0.106", features = ["derive"] } 17 | serde_yaml = "0.8.11" 18 | schemars = "0.7.1" 19 | serde_json = "1.0.51" 20 | tokio = { version = "0.2.18", features = ["signal"] } 21 | futures = "0.3.4" 22 | log = "0.4.8" 23 | -------------------------------------------------------------------------------- /src/apiserver_old/Rocket.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | port = 1779 3 | workers = 1 -------------------------------------------------------------------------------- /src/apiserver_old/engine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "apiserver-engine" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | pom = {path = "../pom"} 9 | digest = "0.8.1" 10 | db = {path = "../db"} 11 | rand = "0.7.3" 12 | uuid = "0.8.1" 13 | rocket = {git = "https://github.com/SergioBenitez/rocket", branch = "async"} 14 | serde = "1.0.106" 15 | sha3 = "0.8.2" 16 | rand_chacha = "0.2.2" 17 | entity = {path = "../entity"} 18 | base64 = "0.12.0" 19 | constant_time_eq = "0.1.5" 20 | libc = "0.2.68" 21 | serde_json = "1.0.51" 22 | branca = "0.9.0" 23 | backtrace = "0.3.46" 24 | ranker = {path = "../ranker"} 25 | anyhow = "1.0.28" 26 | invoker-api = {path = "../invoker-api"} 27 | rocket_contrib = {git = "https://github.com/SergioBenitez/rocket", branch = "async"} 28 | problem-loader = {path = "../problem-loader"} 29 | serde_yaml = "0.8.11" 30 | thiserror = "1.0.15" 31 | schemars = "0.7.1" 32 | tokio = { version = "0.2.18", features = ["rt-threaded", "macros", "uds", "stream"] } 33 | futures = "0.3.4" 34 | chrono = "0.4.11" 35 | log = "0.4.8" 36 | 37 | [dev-dependencies] 38 | toml = "0.5.6" 39 | setup = {path = "../setup"} 40 | simple_logger = "1.6.0" 41 | -------------------------------------------------------------------------------- /src/apiserver_old/engine/src/api/auth.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[derive(Serialize, Deserialize, JsonSchema)] 4 | pub(crate) struct SimpleAuthParams { 5 | /// Login 6 | login: String, 7 | /// Password 8 | password: String, 9 | } 10 | 11 | impl ApiObject for SimpleAuthParams { 12 | fn name() -> &'static str { 13 | "SimpleAuthParams" 14 | } 15 | } 16 | 17 | /// Type that represents session 18 | /// You shouldn't do any assumptions about this type representation 19 | #[derive(Serialize, Deserialize, JsonSchema)] 20 | pub(crate) struct SessionToken { 21 | /// Opaque string that represents session data 22 | /// On all subsequent requests, put this string as value of header `X-Jjs-Auth` 23 | pub data: String, 24 | 25 | /// in dev mode, contains session data in unencrypted form 26 | pub raw_data: Option, 27 | } 28 | 29 | impl ApiObject for SessionToken { 30 | fn name() -> &'static str { 31 | "SessionToken" 32 | } 33 | } 34 | 35 | #[post("/auth/simple", data = "

")] 36 | pub(crate) async fn route_simple( 37 | ctx: Context, 38 | p: Json, 39 | ) -> ApiResult> { 40 | let mut success = false; 41 | let mut reject_reason = ""; 42 | if let Some(user) = ctx 43 | .db() 44 | .user_try_load_by_login(&p.login) 45 | .await 46 | .internal(&ctx)? 47 | { 48 | if let Some(password_hash) = user.password_hash { 49 | success = crate::password::check_password_hash(&p.password, &password_hash); 50 | if !success { 51 | reject_reason = "IncorrectPassword"; 52 | } 53 | } else { 54 | reject_reason = "PasswordAuthNotAvailable"; 55 | } 56 | } else { 57 | reject_reason = "UnknownUser"; 58 | } 59 | if success { 60 | let token = ctx.token_mgr.create_token(&p.login).await.internal(&ctx)?; 61 | let buf = ctx.token_mgr.serialize(&token); 62 | let sess = SessionToken { 63 | data: buf, 64 | raw_data: None, //TODO 65 | }; 66 | Ok(Json(sess)) 67 | } else { 68 | let mut ext = ErrorExtension::new(); 69 | ext.set_error_code(reject_reason); 70 | let err = ApiError { 71 | visible: true, 72 | extension: ext, 73 | cause: None, 74 | ctx, 75 | }; 76 | Err(err) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/apiserver_old/engine/src/api/misc.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[derive(Serialize, Deserialize, JsonSchema)] 4 | pub(crate) struct ApiVersion { 5 | /// MAJOR component 6 | major: u16, 7 | /// MINOR component 8 | minor: u16, 9 | } 10 | 11 | impl ApiObject for ApiVersion { 12 | fn name() -> &'static str { 13 | "ApiVersion" 14 | } 15 | } 16 | 17 | #[get("/system/api-version")] 18 | pub(crate) fn route_get_api_version() -> Json { 19 | Json(ApiVersion { major: 0, minor: 0 }) 20 | } 21 | 22 | #[get("/system/is-dev")] 23 | pub(crate) fn route_is_dev(ctx: Context) -> Json { 24 | Json(matches!(ctx.config().env, crate::config::Env::Dev)) 25 | } 26 | -------------------------------------------------------------------------------- /src/apiserver_old/engine/src/api/schema.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | 3 | pub type ToolchainId = String; 4 | pub type RunId = i32; 5 | pub type ProblemId = String; 6 | pub type ContestId = String; 7 | // will be used when TODO is fixed 8 | #[allow(unused)] 9 | pub type UserId = Uuid; 10 | -------------------------------------------------------------------------------- /src/apiserver_old/engine/src/api/security.rs: -------------------------------------------------------------------------------- 1 | mod access_ck; 2 | mod token; 3 | mod token_mgr; 4 | 5 | pub(crate) use access_ck::{AccessChecker, Subjects}; 6 | pub use token::Token; 7 | pub use token_mgr::{TokenMgr, TokenMgrError}; 8 | -------------------------------------------------------------------------------- /src/apiserver_old/engine/src/api/security/token.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct UserInfo { 5 | pub(super) id: uuid::Uuid, 6 | /// TODO: name should have hierarchical type 7 | pub(super) name: String, 8 | pub(super) groups: Vec, 9 | } 10 | 11 | /// Struct representing API session 12 | #[derive(Serialize, Deserialize, Debug, Clone)] 13 | pub struct Token { 14 | pub(super) session_id: uuid::Uuid, 15 | pub(super) user_info: UserInfo, 16 | } 17 | 18 | impl Token { 19 | pub fn issue_for_virtual_user(id: uuid::Uuid, name: String, groups: Vec) -> Token { 20 | Token { 21 | user_info: UserInfo { id, name, groups }, 22 | session_id: uuid::Uuid::new_v4(), 23 | } 24 | } 25 | 26 | pub fn user_id(&self) -> uuid::Uuid { 27 | self.user_info.id 28 | } 29 | 30 | pub fn session_id(&self) -> uuid::Uuid { 31 | self.session_id 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/apiserver_old/engine/src/api/toolchains.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[derive(Serialize, Deserialize, JsonSchema)] 4 | pub(crate) struct Toolchain { 5 | /// Human readable name, e.g. "GCC C++ v9.1 with sanitizers enables" 6 | pub name: String, 7 | /// Internal name, e.g. "cpp.san.9.1" 8 | pub id: schema::ToolchainId, 9 | } 10 | 11 | impl ApiObject for Toolchain { 12 | fn name() -> &'static str { 13 | "Toolchain" 14 | } 15 | } 16 | 17 | impl<'a> From<&'a entity::entities::toolchain::Toolchain> for Toolchain { 18 | fn from(tc: &'a entity::entities::toolchain::Toolchain) -> Self { 19 | Self { 20 | name: tc.title.clone(), 21 | id: tc.name.clone(), 22 | } 23 | } 24 | } 25 | 26 | #[get("/toolchains")] 27 | pub(crate) fn route_list(ctx: Context) -> ApiResult>> { 28 | let res = ctx 29 | .cfg 30 | .list::() 31 | .map(|tc| Toolchain { 32 | name: tc.title.clone(), 33 | id: tc.name.clone(), 34 | }) 35 | .collect(); 36 | Ok(Json(res)) 37 | } 38 | -------------------------------------------------------------------------------- /src/apiserver_old/engine/src/api/users.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use crate::password; 3 | 4 | #[derive(Serialize, Deserialize, JsonSchema)] 5 | pub(crate) struct User { 6 | // TODO use Uuid here when diesel supports uuidv8 7 | /// UUID of this user. 8 | pub id: String, 9 | pub login: String, 10 | } 11 | 12 | impl ApiObject for User { 13 | fn name() -> &'static str { 14 | "User" 15 | } 16 | } 17 | 18 | impl<'a> From<&'a db::schema::User> for User { 19 | fn from(user: &'a db::schema::User) -> User { 20 | User { 21 | id: user.id.to_hyphenated().to_string(), 22 | login: user.username.clone(), 23 | } 24 | } 25 | } 26 | 27 | #[derive(Serialize, Deserialize, JsonSchema)] 28 | pub(crate) struct UserCreateParams { 29 | /// Login (must be unique) 30 | login: String, 31 | /// Password (no strength validation is performed) 32 | password: String, 33 | /// List of groups new user should belong to 34 | #[serde(default)] 35 | groups: Vec, 36 | } 37 | 38 | impl ApiObject for UserCreateParams { 39 | fn name() -> &'static str { 40 | "UserCreateParams" 41 | } 42 | } 43 | 44 | // TODO allow creation without password 45 | #[post("/users", data = "

")] 46 | pub(crate) async fn route_create( 47 | ctx: Context, 48 | mut p: Json, 49 | ) -> ApiResult> { 50 | // TODO transaction 51 | if !p.groups.is_empty() { 52 | let access_checker = ctx.access_generic(); 53 | if !access_checker.is_sudo() { 54 | return Err(ApiError::access_denied(&ctx)); 55 | } 56 | } 57 | let cur_user = ctx 58 | .db() 59 | .user_try_load_by_login(&p.login) 60 | .await 61 | .internal(&ctx)?; 62 | if let Some(..) = cur_user { 63 | return Err(ApiError::new(&ctx, "UserAlreadyExists")); 64 | } 65 | 66 | let provided_password_hash = password::get_password_hash(&p.password); 67 | 68 | let new_user = db::schema::NewUser { 69 | username: std::mem::take(&mut p.login), 70 | password_hash: Some(provided_password_hash), 71 | groups: std::mem::take(&mut p.groups), 72 | }; 73 | 74 | let user = ctx.db().user_new(new_user).await.internal(&ctx)?; 75 | 76 | Ok(Json((&user).into())) 77 | } 78 | -------------------------------------------------------------------------------- /src/apiserver_old/engine/src/password.rs: -------------------------------------------------------------------------------- 1 | pub fn get_password_hash(password: &str) -> String { 2 | use digest::Digest; 3 | let digest = sha3::Sha3_512::digest(password.as_bytes()); 4 | let digest = format!("{:x}", digest); 5 | digest 6 | } 7 | 8 | pub fn check_password_hash(password: &str, expected_hash: &str) -> bool { 9 | let actual_hash = get_password_hash(password); 10 | constant_time_eq::constant_time_eq(expected_hash.as_bytes(), actual_hash.as_bytes()) 11 | } 12 | -------------------------------------------------------------------------------- /src/apiserver_old/engine/src/secret_key.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | #[derive(Clone)] 4 | pub struct SecretKey(pub Arc<[u8]>); 5 | 6 | impl std::ops::Deref for SecretKey { 7 | type Target = [u8]; 8 | 9 | fn deref(&self) -> &Self::Target { 10 | &*(self.0) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/cleanup/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cleanup" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | structopt = "0.3.13" 9 | libc = "0.2.68" 10 | nix = "0.17.0" 11 | -------------------------------------------------------------------------------- /src/cleanup/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::CString, fs}; 2 | 3 | use structopt::StructOpt; 4 | 5 | #[derive(Debug, StructOpt)] 6 | struct Argv { 7 | #[structopt(long = "cgroupfs", short = "c", default_value = "/sys/fs/cgroup")] 8 | cgroupfs: String, 9 | 10 | #[structopt(long = "root", short = "r")] 11 | root: String, 12 | 13 | #[structopt(long = "jail", short = "j")] 14 | jail_id: String, 15 | } 16 | 17 | fn main() { 18 | let argv: Argv = Argv::from_args(); 19 | println!("----> Procfs"); 20 | let procfs_path = format!("{}/proc", &argv.root); 21 | let self_exe = format!("{}/self/exe", &procfs_path); 22 | let should_unmount = fs::File::open(self_exe).is_ok(); 23 | if should_unmount { 24 | println!("Ok: procfs is not mounted"); 25 | } else { 26 | println!("Unmounting"); 27 | let procfs_path = CString::new(procfs_path).unwrap(); 28 | unsafe { 29 | if libc::umount2(procfs_path.as_ptr(), libc::MNT_DETACH) == -1 { 30 | let err = nix::errno::errno(); 31 | let err = nix::errno::from_i32(err); 32 | eprintln!("Error while unmounting procfs: {}", err); 33 | } 34 | } 35 | println!("done"); 36 | } 37 | println!("----> Cgroups"); 38 | for subsys in &["pids", "memory", "cpuacct"] { 39 | let path = format!("{}/{}/jjs/g-{}", &argv.cgroupfs, subsys, &argv.jail_id); 40 | println!("deleting {}", &path); 41 | if let Err(e) = fs::remove_dir(path) { 42 | eprintln!("Error: {}", e); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | description = "JJS CLI client" 7 | 8 | [dependencies] 9 | util = {path = "../util"} 10 | client = {path = "../client"} 11 | pom = { path = "../pom" } 12 | base64 = "0.13.0" 13 | serde_json = "1.0.59" 14 | serde = "1.0.117" 15 | futures = { version = "0.3.7", features = ["compat"] } 16 | tokio = { version = "0.2.22", features = ["macros", "rt-core", "io-std", "process"] } 17 | log = "0.4.11" 18 | anyhow = "1.0.33" 19 | clap = "3.0.0-beta.2" 20 | either = "1.6.1" 21 | clap_generate = "3.0.0-beta.2" 22 | dialoguer = "0.7.1" 23 | xdg = "2.2.0" 24 | serde_yaml = "0.8.14" 25 | tar = "0.4.30" 26 | humansize = "1.1.0" 27 | flate2 = "1.0.18" 28 | -------------------------------------------------------------------------------- /src/cli/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-slim 2 | RUN apt-get update -y && apt-get install -y libssl-dev 3 | COPY jjs-cli /bin/jjs-cli 4 | ENV JJS_AUTH_DATA=/auth/authdata.yaml 5 | VOLUME ["/auth"] 6 | ENTRYPOINT ["jjs-cli"] 7 | -------------------------------------------------------------------------------- /src/cli/src/api_version.rs: -------------------------------------------------------------------------------- 1 | use client::prelude::Sendable; 2 | 3 | pub async fn exec(api: &client::ApiClient) -> anyhow::Result<()> { 4 | let vers = client::models::ApiVersion::api_version().send(api).await?; 5 | println!("JJS API version: {}.{}", vers.major, vers.minor); 6 | Ok(()) 7 | } 8 | -------------------------------------------------------------------------------- /src/cli/src/completion.rs: -------------------------------------------------------------------------------- 1 | /// Prints completion script to stdout. 2 | /// Usually is should be used like this: 3 | /// 4 | /// . completion.sh 5 | #[derive(clap::Clap)] 6 | pub(crate) struct Opt { 7 | /// Shell generate completions for. 8 | /// Supported: bash, fish, zsh, elvish, powershell|pwsh|ps 9 | #[clap(long, default_value = "bash")] 10 | shell: String, 11 | } 12 | 13 | fn generate() { 14 | let mut app: clap::App = ::into_app(); 15 | clap_generate::generate::(&mut app, "jjs-cli", &mut std::io::stdout()); 16 | } 17 | 18 | pub(crate) fn exec(opt: &Opt) -> anyhow::Result<()> { 19 | match opt.shell.as_str() { 20 | "bash" => generate::(), 21 | "fish" => generate::(), 22 | "zsh" => generate::(), 23 | "elvish" => generate::(), 24 | "powershell" | "pwsh" | "ps" => generate::(), 25 | _ => anyhow::bail!("unsupported shell: {}", opt.shell), 26 | } 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /src/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | mod api_version; 2 | mod completion; 3 | mod login; 4 | mod problems; 5 | mod runs; 6 | mod submit; 7 | mod toolchains; 8 | mod wait; 9 | 10 | use clap::Clap; 11 | 12 | /// Command-line client for JJS 13 | #[derive(Clap)] 14 | #[clap(author, about)] 15 | struct Opt { 16 | #[clap(subcommand)] 17 | sub: SubOpt, 18 | } 19 | 20 | #[derive(Clap)] 21 | enum SubOpt { 22 | Submit(submit::Opt), 23 | ManageRuns(runs::Opt), 24 | Login(login::Opt), 25 | Toolchains(toolchains::Opt), 26 | Wait(wait::Opt), 27 | Problems(problems::Opt), 28 | Completion(completion::Opt), 29 | ApiVersion, 30 | } 31 | 32 | #[tokio::main] 33 | async fn main() { 34 | util::log::setup(); 35 | if let Err(err) = real_main().await { 36 | eprintln!("Error: {:#}", err); 37 | std::process::exit(1); 38 | } 39 | } 40 | 41 | async fn real_main() -> anyhow::Result<()> { 42 | let opt: Opt = Opt::parse(); 43 | 44 | if let SubOpt::Login(opt) = &opt.sub { 45 | login::exec(opt).await?; 46 | return Ok(()); 47 | } 48 | 49 | let client = client::infer().await?; 50 | 51 | match opt.sub { 52 | SubOpt::Submit(sopt) => submit::exec(sopt, &client).await?, 53 | SubOpt::ManageRuns(sopt) => runs::exec(sopt, &client).await?, 54 | SubOpt::ApiVersion => api_version::exec(&client).await?, 55 | SubOpt::Toolchains(sopt) => toolchains::exec(&sopt, &client).await?, 56 | SubOpt::Wait(sopt) => wait::exec(&sopt, &client).await?, 57 | SubOpt::Problems(sopt) => problems::exec(&sopt, &client).await?, 58 | SubOpt::Completion(sopt) => completion::exec(&sopt)?, 59 | SubOpt::Login(_) => unreachable!(), 60 | }; 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /src/cli/src/runs.rs: -------------------------------------------------------------------------------- 1 | use client::prelude::Sendable as _; 2 | 3 | #[derive(clap::Clap)] 4 | pub struct Opt { 5 | /// Action: view, remove or rejudge 6 | action: String, 7 | #[clap(long = "filter", short = 'f', default_value = "true")] 8 | _filter: String, 9 | } 10 | 11 | pub async fn exec(opt: Opt, api: &client::ApiClient) -> anyhow::Result<()> { 12 | // TODO optimizations 13 | 14 | let runs = client::models::Run::list_runs().send(api).await?; 15 | match opt.action.as_str() { 16 | "view" => { 17 | println!("runs: {:?}", runs.object); 18 | Ok(()) 19 | } 20 | _ => { 21 | anyhow::bail!("unknown runs subcommand: {}", opt.action); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/cli/src/toolchains.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | use client::prelude::Sendable; 3 | 4 | #[derive(clap::Clap)] 5 | pub(crate) struct Opt { 6 | /// Toolchain image to add 7 | #[clap(long)] 8 | image: String, 9 | /// Toolchain name. If not provided, will be inferred from image labels. 10 | #[clap(long)] 11 | name: Option, 12 | /// Pull image before using 13 | #[clap(long)] 14 | pull: bool, 15 | } 16 | 17 | pub(crate) async fn exec(opt: &Opt, api: &client::ApiClient) -> anyhow::Result<()> { 18 | if opt.pull { 19 | println!("pulling the image"); 20 | let mut cmd = tokio::process::Command::new("docker"); 21 | cmd.arg("pull"); 22 | cmd.arg(&opt.image); 23 | let status = cmd.status().await.context("docker not available")?; 24 | if !status.success() { 25 | anyhow::bail!("docker pull failed"); 26 | } 27 | } 28 | let toolchain_name = match &opt.name { 29 | Some(name) => name.clone(), 30 | None => { 31 | println!("Inspecting for label value"); 32 | let mut cmd = tokio::process::Command::new("docker"); 33 | cmd.arg("inspect"); 34 | cmd.arg(&opt.image); 35 | let out = cmd.output().await?; 36 | eprintln!("{}", String::from_utf8_lossy(&out.stderr)); 37 | if !out.status.success() { 38 | anyhow::bail!("docker inspect failed"); 39 | } 40 | let image_description: serde_json::Value = 41 | serde_json::from_slice(&out.stdout).context("parse error")?; 42 | image_description 43 | .pointer("/Config/Labels/io.jjs.toolchain.name") 44 | .context("label io.jjs.toolchain.name missing")? 45 | .as_str() 46 | .context("malformed docker inspect output: label is not a string")? 47 | .to_string() 48 | } 49 | }; 50 | println!("Toolchain name: {}", &toolchain_name); 51 | 52 | client::models::Toolchain::put_toolchain() 53 | .image(&opt.image) 54 | .id(&toolchain_name) 55 | .description("TODO") 56 | .send(api) 57 | .await?; 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /src/cli/src/wait.rs: -------------------------------------------------------------------------------- 1 | use client::prelude::Sendable; 2 | /// Wait until apiserver responds 3 | #[derive(clap::Clap)] 4 | pub(crate) struct Opt { 5 | /// Max waiting timeout in seconds 6 | #[clap(long, default_value = "45")] 7 | max_timeout: u32, 8 | /// Time between attempts 9 | #[clap(long, default_value = "5")] 10 | interval: u32, 11 | } 12 | 13 | pub(crate) async fn exec(opt: &Opt, api: &client::ApiClient) -> anyhow::Result<()> { 14 | let deadline = 15 | std::time::Instant::now() + std::time::Duration::from_secs(opt.max_timeout.into()); 16 | loop { 17 | if client::models::ApiVersion::api_version() 18 | .send(api) 19 | .await 20 | .is_ok() 21 | { 22 | println!("\nSuccess"); 23 | break; 24 | } 25 | print!("."); 26 | tokio::io::AsyncWriteExt::flush(&mut tokio::io::stdout()).await?; 27 | if std::time::Instant::now() > deadline { 28 | anyhow::bail!("Deadline exceeded"); 29 | } 30 | tokio::time::delay_for(std::time::Duration::from_secs(opt.interval.into())).await; 31 | } 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /src/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "client" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | openapi = {path = "../gen-api-client"} 9 | hyper = "0.13.8" 10 | tokio = "0.2.22" 11 | serde_json = "1.0.59" 12 | http = "0.2.1" 13 | async-trait = "0.1.41" 14 | anyhow = "1.0.33" 15 | serde = "1.0.117" 16 | url = "2.1.1" 17 | serde_yaml = "0.8.14" 18 | xdg = "2.2.0" 19 | tracing = "0.1.21" 20 | tracing-futures = "0.2.4" 21 | -------------------------------------------------------------------------------- /src/configure-toolchains/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "configure-toolchains" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | structopt = "0.3.13" 9 | serde_json = "1.0.51" 10 | serde = { version = "1.0.106", features = ["derive"] } 11 | anyhow = "1.0.28" 12 | util = {path = "../util"} 13 | copy-ln = { git = "https://github.com/mikailbag/copy-ln", branch = "master" } 14 | tempfile = "3.1.0" 15 | tera = "1.2.0" 16 | serde_yaml = "0.8.11" 17 | nix = "0.17.0" 18 | -------------------------------------------------------------------------------- /src/configure-toolchains/src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)] 4 | #[serde(rename_all = "kebab-case")] 5 | pub(crate) enum Strategy { 6 | Trace, 7 | Debootstrap, 8 | } 9 | 10 | #[derive(Debug, Deserialize, Clone)] 11 | pub(crate) struct ToolchainConfig { 12 | pub(crate) strategies: Vec, 13 | #[serde(default)] 14 | pub(crate) depends: Vec, 15 | #[serde(default)] 16 | pub(crate) auto: bool, 17 | } 18 | -------------------------------------------------------------------------------- /src/db/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "db" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serde = { version = "1.0.106", features = ["derive"] } 9 | uuid = { version = "0.8.1", features = ["serde", "v4"] } 10 | invoker-api = {path = "../invoker-api"} 11 | anyhow = "1.0.28" 12 | serde_json = "1.0.51" 13 | redis = {version = "0.15.1", default-features = false, features = []} 14 | async-trait = "0.1.30" 15 | futures = "0.3.4" 16 | tokio = { version = "0.2.18", features = [] } 17 | chrono = { version = "0.4.11", features = ["serde"] } 18 | tokio-postgres = { version = "0.5.3", optional = true } 19 | postgres-types = { version = "0.1.1", optional = true, features = ["derive", "with-uuid-0_8", "with-serde_json-1", "with-chrono-0_4"] } 20 | bb8 = { version = "0.4.1", optional = true } 21 | bb8-postgres = { version = "0.4.0", optional = true } 22 | 23 | [features] 24 | postgres = ["tokio-postgres", "postgres-types", "bb8", "bb8-postgres"] 25 | default = ["postgres"] 26 | -------------------------------------------------------------------------------- /src/db/diesel.toml: -------------------------------------------------------------------------------- 1 | [print_schema] 2 | file = "src/schema_raw.rs" 3 | import_types = ["super::*"] -------------------------------------------------------------------------------- /src/db/migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /src/db/migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /src/db/migrations/2019-02-13-163254_initial/down.sql: -------------------------------------------------------------------------------- 1 | -- indicies 2 | DROP INDEX runs_id_unique_index; 3 | -- tables 4 | DROP TABLE invocations; 5 | DROP TABLE runs; 6 | DROP TABLE users; 7 | DROP TABLE __revision; 8 | -- sequences 9 | DROP SEQUENCE user_id_seq; 10 | DROP SEQUENCE run_id_seq; 11 | DROP SEQUENCE inv_id_seq; 12 | -- types 13 | DROP DOMAIN unsigned_integer; 14 | -------------------------------------------------------------------------------- /src/db/migrations/2019-02-13-163254_initial/up.sql: -------------------------------------------------------------------------------- 1 | -- Users table 2 | 3 | CREATE SEQUENCE user_id_seq START WITH 0 MINVALUE 0; 4 | 5 | CREATE TABLE users 6 | ( 7 | id UUID UNIQUE PRIMARY KEY NOT NULL, 8 | username VARCHAR(100) UNIQUE NOT NULL, 9 | password_hash CHAR(128), -- SHA3-512, in hex encoding 10 | groups TEXT[] NOT NULL 11 | ); 12 | 13 | INSERT INTO users 14 | values ('04eb5beb-bf14-459c-bcf1-57eca87a0055'::uuid, 15 | 'Global/Root', 16 | NULL, 17 | '{}'), 18 | ('56ff846e-81bd-451b-aeea-90afc192bd77'::uuid, 19 | 'Global/Guest', 20 | NULL, 21 | '{}'); 22 | 23 | -- Runs 24 | 25 | CREATE SEQUENCE run_id_seq START WITH 0 MINVALUE 0; 26 | 27 | CREATE TABLE runs 28 | ( 29 | id INTEGER DEFAULT nextval('run_id_seq') PRIMARY KEY NOT NULL, 30 | toolchain_id VARCHAR(100) NOT NULL, 31 | problem_id VARCHAR(100) NOT NULL, 32 | rejudge_id INTEGER NOT NULL, 33 | user_id UUID REFERENCES users (id) NOT NULL, 34 | contest_id VARCHAR(100) NOT NULL 35 | ); 36 | 37 | CREATE UNIQUE INDEX runs_id_unique_index ON runs (id); 38 | 39 | CREATE SEQUENCE inv_id_seq START WITH 0 MINVALUE 0; 40 | 41 | -- Invocations 42 | 43 | CREATE table invocations 44 | ( 45 | id INTEGER DEFAULT nextval('inv_id_seq') UNIQUE PRIMARY KEY NOT NULL, 46 | run_id INTEGER REFERENCES runs(id) NOT NULL, 47 | -- This is serialized `InvokeTask`. See `invoker-api` for its definition 48 | invoke_task bytea NOT NULL, 49 | -- see InvocationStatus 50 | state SMALLINT NOT NULL, 51 | --- This is InvokeOutcomeHeader 52 | -- most important invocation results. They are copied from judge log, so it can be removed from FS without problems 53 | -- contains JSON document which maps judge log name to InvocationOutcome. May be partial if not all logs are emitted yet. 54 | outcome JSONB NOT NULL 55 | ); 56 | 57 | -- Special table 58 | 59 | CREATE TABLE __revision ( 60 | revision VARCHAR NOT NULL 61 | ); 62 | 63 | INSERT INTO __revision VALUES ('2019-02-13-163254_initial'); -------------------------------------------------------------------------------- /src/db/migrations/2020-04-01-171741_kv/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE kv; 2 | UPDATE __revision SET revision = '2019-02-13-163254_initial'; -------------------------------------------------------------------------------- /src/db/migrations/2020-04-01-171741_kv/up.sql: -------------------------------------------------------------------------------- 1 | -- This table contains temporary key-value pairs, when Redis usage is disabled 2 | CREATE TABLE kv ( 3 | name VARCHAR UNIQUE PRIMARY KEY NOT NULL, 4 | value bytea NOT NULL 5 | ); 6 | UPDATE __revision SET revision = '2020-04-01-171741_kv'; -------------------------------------------------------------------------------- /src/db/migrations/2020-04-03-210736_contest_registration/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE participations; 2 | 3 | DROP SEQUENCE participations_id_seq; 4 | 5 | UPDATE __revision SET revision = '2020-04-01-171741_kv'; -------------------------------------------------------------------------------- /src/db/migrations/2020-04-03-210736_contest_registration/up.sql: -------------------------------------------------------------------------------- 1 | CREATE SEQUENCE participations_id_seq START WITH 0 MINVALUE 0; 2 | 3 | CREATE TABLE participations 4 | ( 5 | id INTEGER DEFAULT nextval('participations_id_seq') UNIQUE PRIMARY KEY NOT NULL, 6 | user_id UUID REFERENCES users(id) NOT NULL, 7 | contest_id VARCHAR NOT NULL, 8 | phase SMALLINT NOT NULL, 9 | virtual_contest_start_time TIMESTAMP 10 | ); 11 | 12 | CREATE INDEX participation_lookup_index ON participations (user_id, contest_id); 13 | 14 | UPDATE __revision SET revision = '2020-04-03-210736_contest_registration'; 15 | -------------------------------------------------------------------------------- /src/db/src/connect.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repo::{MemoryRepo, PgRepo, RedisRepo}, 3 | DbConn, 4 | }; 5 | use anyhow::{Context, Result}; 6 | use futures::future::FutureExt; 7 | use std::env; 8 | 9 | pub struct ConnectOptions { 10 | /// Postgres connection string 11 | pg: Option, 12 | /// Redis connection string 13 | redis: Option, 14 | } 15 | 16 | impl ConnectOptions { 17 | fn warn(&self) { 18 | if cfg!(not(test)) && self.pg.is_none() { 19 | eprintln!( 20 | "warning: pg url not provided in DATABASE_URL. \ 21 | JJS is unusable in such configuration." 22 | ); 23 | } 24 | } 25 | } 26 | 27 | pub async fn connect(options: ConnectOptions) -> Result { 28 | let mem = MemoryRepo::new(); 29 | let pg = match options.pg { 30 | Some(pg_conn_str) => { 31 | let conn = PgRepo::new(&pg_conn_str) 32 | .await 33 | .context("cannot connect to postgres")?; 34 | Some(conn) 35 | } 36 | None => None, 37 | }; 38 | let redis = match options.redis { 39 | Some(redis_conn_str) => { 40 | let conn = RedisRepo::new(&redis_conn_str) 41 | .await 42 | .context("cannot connect to redis")?; 43 | Some(conn) 44 | } 45 | None => None, 46 | }; 47 | Ok(DbConn { mem, pg, redis }) 48 | } 49 | 50 | pub async fn connect_env() -> Result { 51 | let opts = ConnectOptions { 52 | pg: env::var("DATABASE_URL").ok(), 53 | redis: env::var("REDIS_URL").ok(), 54 | }; 55 | opts.warn(); 56 | connect(opts).await 57 | } 58 | 59 | pub fn connect_memory() -> Result { 60 | let opts = ConnectOptions { 61 | pg: None, 62 | redis: None, 63 | }; 64 | connect(opts).now_or_never().unwrap() 65 | } 66 | -------------------------------------------------------------------------------- /src/db/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod conn; 2 | pub mod connect; 3 | pub mod repo; 4 | pub mod schema; 5 | 6 | pub use conn::DbConn; 7 | pub use connect::connect_env; 8 | 9 | pub use anyhow::Error; 10 | -------------------------------------------------------------------------------- /src/db/src/repo/redis.rs: -------------------------------------------------------------------------------- 1 | use super::KvRepo; 2 | use anyhow::{Context as _, Result}; 3 | use redis::AsyncCommands as _; 4 | 5 | pub struct RedisRepo { 6 | conn: tokio::sync::Mutex, 7 | } 8 | 9 | impl std::fmt::Debug for RedisRepo { 10 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 11 | f.debug_struct("RedisRepo").field("conn", &"..").finish() 12 | } 13 | } 14 | 15 | impl RedisRepo { 16 | pub(crate) async fn new(conn_url: &str) -> Result { 17 | let client = redis::Client::open(conn_url).context("invalid connection string")?; 18 | let conn = client 19 | .get_async_connection() 20 | .await 21 | .context("unable to connect")?; 22 | let conn = tokio::sync::Mutex::new(conn); 23 | Ok(RedisRepo { conn }) 24 | } 25 | } 26 | 27 | #[async_trait::async_trait] 28 | impl KvRepo for RedisRepo { 29 | async fn kv_get_raw(&self, key: &str) -> Result>> { 30 | self.conn.lock().await.get(key).await.map_err(Into::into) 31 | } 32 | 33 | async fn kv_put_raw(&self, key: &str, value: &[u8]) -> Result<()> { 34 | self.conn 35 | .lock() 36 | .await 37 | .set(key, value) 38 | .await 39 | .map_err(Into::into) 40 | } 41 | 42 | async fn kv_del(&self, key: &str) -> Result<()> { 43 | self.conn.lock().await.del(key).await.map_err(Into::into) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/db/src/schema.rs: -------------------------------------------------------------------------------- 1 | mod invocation; 2 | mod participation; 3 | mod run; 4 | mod user; 5 | 6 | pub use invocation::InvocationState; 7 | pub use participation::ParticipationPhase; 8 | 9 | use serde::{Deserialize, Serialize}; 10 | 11 | pub type RunId = i32; 12 | pub type InvocationId = i32; 13 | pub type UserId = uuid::Uuid; 14 | pub type ProblemId = String; 15 | pub type ContestId = String; 16 | pub type ParticipationId = i32; 17 | 18 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 19 | pub struct Run { 20 | pub id: RunId, 21 | pub toolchain_id: String, 22 | pub problem_id: ProblemId, 23 | pub rejudge_id: i32, 24 | pub user_id: UserId, 25 | pub contest_id: ContestId, 26 | } 27 | 28 | pub struct NewRun { 29 | pub toolchain_id: String, 30 | pub problem_id: ProblemId, 31 | pub rejudge_id: i32, 32 | pub user_id: UserId, 33 | pub contest_id: ContestId, 34 | } 35 | 36 | #[derive(Default)] 37 | pub struct RunPatch { 38 | pub rejudge_id: Option, 39 | } 40 | 41 | #[derive(Debug, Clone, Serialize, Deserialize)] 42 | pub struct Invocation { 43 | pub id: InvocationId, 44 | pub run_id: RunId, 45 | pub(crate) invoke_task: Vec, 46 | pub(crate) state: i16, 47 | pub(crate) outcome: serde_json::Value, 48 | } 49 | 50 | pub struct NewInvocation { 51 | pub run_id: RunId, 52 | pub(crate) invoke_task: Vec, 53 | pub(crate) state: i16, 54 | pub(crate) outcome: serde_json::Value, 55 | } 56 | 57 | #[derive(Default)] 58 | pub struct InvocationPatch { 59 | pub(crate) state: Option, 60 | } 61 | 62 | #[derive(Serialize, Deserialize, Debug, Clone)] 63 | pub struct User { 64 | pub id: UserId, 65 | pub username: String, 66 | pub password_hash: Option, 67 | pub groups: Vec, 68 | } 69 | 70 | pub struct NewUser { 71 | pub username: String, 72 | pub password_hash: Option, 73 | pub groups: Vec, 74 | } 75 | 76 | #[derive(Serialize, Deserialize, Debug, Clone)] 77 | pub struct Participation { 78 | pub id: ParticipationId, 79 | pub user_id: UserId, 80 | pub contest_id: ContestId, 81 | pub(crate) phase: i16, 82 | pub(crate) virtual_contest_start_time: Option, 83 | } 84 | 85 | #[derive(Default)] 86 | pub struct NewParticipation { 87 | pub user_id: UserId, 88 | pub contest_id: ContestId, 89 | pub(crate) phase: i16, 90 | pub(crate) virtual_contest_start_time: Option, 91 | } 92 | -------------------------------------------------------------------------------- /src/db/src/schema/participation.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | #[repr(i16)] 4 | #[derive(Debug, postgres_types::FromSql, postgres_types::ToSql)] 5 | pub enum ParticipationPhase { 6 | Active, 7 | // in future: Requested, Rejected 8 | // in future: Disqualified, 9 | __Last, 10 | } 11 | 12 | impl From for i16 { 13 | fn from(val: ParticipationPhase) -> Self { 14 | val as i16 15 | } 16 | } 17 | 18 | impl std::convert::TryFrom for ParticipationPhase { 19 | type Error = (); 20 | 21 | fn try_from(value: i16) -> Result { 22 | if value < 0 || value >= (ParticipationPhase::__Last as i16) { 23 | return Err(()); 24 | } 25 | Ok(unsafe { std::mem::transmute(value) }) 26 | } 27 | } 28 | 29 | impl crate::schema::Participation { 30 | pub(crate) fn from_pg_row(row: tokio_postgres::Row) -> Self { 31 | Self { 32 | id: row.get("id"), 33 | user_id: row.get("user_id"), 34 | contest_id: row.get("contest_id"), 35 | phase: row.get("phase"), 36 | virtual_contest_start_time: row.get("virtual_contest_start_time"), 37 | } 38 | } 39 | } 40 | 41 | impl crate::schema::Participation { 42 | pub fn phase(&self) -> ParticipationPhase { 43 | self.phase.try_into().expect("invalid phase") 44 | } 45 | 46 | pub fn virtual_contest_start_time(&self) -> Option> { 47 | self.virtual_contest_start_time 48 | .map(|t| chrono::DateTime::from_utc(t, chrono::Utc)) 49 | } 50 | 51 | pub fn mock_new() -> Self { 52 | Self { 53 | phase: 0, 54 | id: 0, 55 | contest_id: "".to_string(), 56 | user_id: uuid::Uuid::nil(), 57 | virtual_contest_start_time: None, 58 | } 59 | } 60 | } 61 | 62 | impl crate::schema::NewParticipation { 63 | pub fn set_phase(&mut self, phase: ParticipationPhase) -> &mut Self { 64 | self.phase = phase.into(); 65 | self 66 | } 67 | 68 | pub fn set_virtual_contest_start_time( 69 | &mut self, 70 | time: Option>, 71 | ) -> &mut Self { 72 | self.virtual_contest_start_time = time.map(|t| t.naive_utc()); 73 | self 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/db/src/schema/run.rs: -------------------------------------------------------------------------------- 1 | impl super::Run { 2 | pub(crate) fn from_pg_row(row: tokio_postgres::Row) -> Self { 3 | Self { 4 | id: row.get("id"), 5 | toolchain_id: row.get("toolchain_id"), 6 | problem_id: row.get("problem_id"), 7 | contest_id: row.get("contest_id"), 8 | user_id: row.get("user_id"), 9 | rejudge_id: row.get("rejudge_id"), 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/db/src/schema/user.rs: -------------------------------------------------------------------------------- 1 | impl super::User { 2 | pub(crate) fn from_pg_row(row: tokio_postgres::Row) -> super::User { 3 | Self { 4 | id: row.get("id"), 5 | username: row.get("username"), 6 | groups: row.get("groups"), 7 | password_hash: row.get("password_hash"), 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/db/src/schema_raw.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | use super::*; 3 | 4 | invocations (id) { 5 | id -> Int4, 6 | run_id -> Int4, 7 | invoke_task -> Bytea, 8 | state -> Int2, 9 | outcome -> Jsonb, 10 | } 11 | } 12 | 13 | table! { 14 | use super::*; 15 | 16 | kv (name) { 17 | name -> Varchar, 18 | value -> Bytea, 19 | } 20 | } 21 | 22 | table! { 23 | use super::*; 24 | 25 | participations (id) { 26 | id -> Int4, 27 | user_id -> Uuid, 28 | contest_id -> Varchar, 29 | phase -> Int2, 30 | virtual_contest_start_time -> Nullable, 31 | } 32 | } 33 | 34 | table! { 35 | use super::*; 36 | 37 | runs (id) { 38 | id -> Int4, 39 | toolchain_id -> Varchar, 40 | problem_id -> Varchar, 41 | rejudge_id -> Int4, 42 | user_id -> Uuid, 43 | contest_id -> Varchar, 44 | } 45 | } 46 | 47 | table! { 48 | use super::*; 49 | 50 | users (id) { 51 | id -> Uuid, 52 | username -> Varchar, 53 | password_hash -> Nullable, 54 | groups -> Array, 55 | } 56 | } 57 | 58 | joinable!(invocations -> runs (run_id)); 59 | joinable!(participations -> users (user_id)); 60 | joinable!(runs -> users (user_id)); 61 | 62 | allow_tables_to_appear_in_same_query!( 63 | invocations, 64 | kv, 65 | participations, 66 | runs, 67 | users, 68 | ); 69 | -------------------------------------------------------------------------------- /src/devtool/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "devtool" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | clap = "3.0.0-beta.2" 9 | serde = { version = "1.0.117", features = ["derive"] } 10 | log = "0.4.11" 11 | env_logger = "0.8.1" 12 | ignore = "0.4.16" 13 | toml = "0.5.7" 14 | atty = "0.2.14" 15 | util = {path = "../util"} 16 | lazy_static = "1.4.0" 17 | anyhow = "1.0.33" 18 | serde_json = "1.0.59" 19 | tempfile = "3.1.0" 20 | -------------------------------------------------------------------------------- /src/devtool/scripts/pages-push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | REPO="https://${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 4 | BRANCH='gh-pages' 5 | git init 6 | git config user.name "${GITHUB_ACTOR}" 7 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 8 | git add . 9 | git commit -m "Triggered by ${GITHUB_SHA}" 10 | git push --force "$REPO" master:$BRANCH 11 | -------------------------------------------------------------------------------- /src/devtool/scripts/postgres-start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #/usr/lib/postgresql/10/bin/pg_ctl -D /var/lib/postgresql/10/main -l /tmp/pg.log start 4 | createdb jjs 5 | #psql -d jjs -a -f /usr/bin/jjs-db-init 6 | psql -d jjs -a -f "$ORIG_CWD/pkg/share/db-setup.sql" 7 | -------------------------------------------------------------------------------- /src/devtool/src/glob_util.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | pub(crate) enum ItemKind { 4 | Bash, 5 | Cpp, 6 | Python, 7 | } 8 | 9 | pub(crate) fn find_items(kind: ItemKind) -> impl Iterator { 10 | let mut types_builder = ignore::types::TypesBuilder::new(); 11 | types_builder.add_defaults(); 12 | types_builder.negate("all"); 13 | match kind { 14 | ItemKind::Bash => { 15 | types_builder.select("sh"); 16 | } 17 | ItemKind::Cpp => { 18 | types_builder.select("c"); 19 | types_builder.select("cpp"); 20 | } 21 | ItemKind::Python => { 22 | types_builder.select("py"); 23 | } 24 | } 25 | let types_matched = types_builder.build().unwrap(); 26 | ignore::WalkBuilder::new(".") 27 | .types(types_matched) 28 | .build() 29 | .map(Result::unwrap) 30 | .filter(|x| { 31 | let ty = x.file_type(); 32 | match ty { 33 | Some(f) => f.is_file(), 34 | None => false, 35 | } 36 | }) 37 | .map(|x| x.path().to_path_buf()) 38 | } 39 | -------------------------------------------------------------------------------- /src/devtool/src/run.rs: -------------------------------------------------------------------------------- 1 | use clap::Clap; 2 | use std::process::Command; 3 | use util::cmd::CommandExt; 4 | #[derive(Clap)] 5 | pub struct Opts { 6 | #[clap(long, short = 'b')] 7 | build: bool, 8 | #[clap(long, short = 't')] 9 | test: bool, 10 | #[clap(long)] 11 | debug: bool, 12 | #[clap(long)] 13 | nocapture: bool, 14 | #[clap(long)] 15 | podman: bool, 16 | } 17 | pub fn task_run(opts: Opts) -> anyhow::Result<()> { 18 | let compose_bin = if opts.podman { 19 | "podman-compose" 20 | } else { 21 | "docker-compose" 22 | }; 23 | println!("dropping existing docker-compose"); 24 | Command::new(compose_bin).arg("down").try_exec()?; 25 | if opts.build { 26 | println!("Building"); 27 | let mut cmd = Command::new("cargo"); 28 | cmd.arg("jjs-build").arg("--docker"); 29 | if opts.debug { 30 | cmd.arg("--configure-opt=--docker-build-opt=--progress=plain"); 31 | } 32 | cmd.try_exec()?; 33 | } 34 | println!("starting jjs"); 35 | Command::new(compose_bin) 36 | .arg("up") 37 | .arg("--detach") 38 | .try_exec()?; 39 | if opts.test { 40 | println!("Waiting for start"); 41 | Command::new("cargo") 42 | .arg("run") 43 | .arg("--package") 44 | .arg("util") 45 | .env("RUST_LOG", "info,util=debug") 46 | .env("JJS_WAIT", "http://localhost:1779/") 47 | .try_exec()?; 48 | println!("Executing tests"); 49 | let mut cmd = Command::new("cargo"); 50 | cmd.arg("jjs-test") 51 | .arg("--integration-tests") 52 | .arg("--skip-unit"); 53 | if opts.nocapture { 54 | cmd.arg("--nocapture"); 55 | } 56 | cmd.try_exec()?; 57 | } 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /src/dist-builder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dist-builder" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | clap = "3.0.0-beta.2" 9 | serde_json = "1.0.59" 10 | serde = { version = "1.0.117", features = ["derive"] } 11 | util = {path = "../util"} 12 | log = "0.4.11" 13 | anyhow = "1.0.33" 14 | -------------------------------------------------------------------------------- /src/dist-builder/deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | packages="libpq-dev libssl-dev cmake libsystemd-dev" 4 | 5 | if command -v sudo >/dev/null 6 | then sudo=sudo 7 | else sudo= 8 | fi 9 | 10 | if command -v apt >/dev/null 11 | then apt='apt' 12 | elif command -v apt-get >/dev/null 13 | then apt='apt-get' 14 | else echo "Warning: apt not found! 15 | You're probably using a non-Debian-based distribution. To build JJS you must install the development packages of libpq and OpenSSL." >&2 16 | fi 17 | 18 | if [ "x$apt" != x ] 19 | then $sudo $apt update 20 | # shellcheck disable=SC2086 21 | $sudo $apt install --no-install-recommends $packages 22 | fi 23 | -------------------------------------------------------------------------------- /src/dist-builder/make-tpl.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | __SUBST__ 4 | export CARGO_TARGET_DIR=$JJS_BUILD_DIR 5 | cd "$JJS_SRC_DIR" || exit 1 6 | cargo run --package deploy --bin make -- "$@" 7 | -------------------------------------------------------------------------------- /src/dist-builder/makefile.tpl: -------------------------------------------------------------------------------- 1 | PHONY: all 2 | all: 3 | +bash ___SCRIPT_PATH___ 4 | deps: 5 | +bash ___DEPS_SCRIPT_PATH___ 6 | -------------------------------------------------------------------------------- /src/dist-builder/src/artifact.rs: -------------------------------------------------------------------------------- 1 | pub(crate) struct RustArtifact { 2 | pub(crate) package_name: String, 3 | pub(crate) install_name: String, 4 | } 5 | 6 | pub(crate) struct CmakeArtifact { 7 | pub(crate) package_name: String, 8 | } 9 | 10 | pub(crate) enum Artifact { 11 | Rust(RustArtifact), 12 | Cmake(CmakeArtifact), 13 | } 14 | -------------------------------------------------------------------------------- /src/dist-builder/src/cfg.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | #[derive(Copy, Clone, Debug)] 4 | pub enum BuildProfile { 5 | Debug, 6 | Release, 7 | RelWithDebInfo, 8 | } 9 | 10 | impl BuildProfile { 11 | pub fn as_str(&self) -> &'static str { 12 | match self { 13 | BuildProfile::Debug => "Debug", 14 | BuildProfile::Release => "Release", 15 | BuildProfile::RelWithDebInfo => "RelWithDebInfo", 16 | } 17 | } 18 | } 19 | 20 | #[derive(Debug, Clone)] 21 | pub struct ToolInfo { 22 | pub cargo: String, 23 | pub cmake: String, 24 | pub docker: String, 25 | } 26 | 27 | #[derive(Debug, Clone)] 28 | pub struct BuildConfig { 29 | pub profile: BuildProfile, 30 | pub target: Option, 31 | pub tool_info: ToolInfo, 32 | pub features: Vec, 33 | } 34 | 35 | #[derive(Debug, Clone)] 36 | pub struct EmitConfig { 37 | /// Some => docker images are built 38 | pub docker: Option, 39 | } 40 | #[derive(Debug, Clone)] 41 | pub struct DockerConfig { 42 | pub build_options: Vec, 43 | /// None => default tag used 44 | pub tag: Option, 45 | pub write_tags_to_file: Option, 46 | } 47 | 48 | /// Describes which components should be build 49 | #[derive(Debug, Clone)] 50 | pub struct ComponentsConfig { 51 | /// Specifies components (e.g. apiserver) to enable 52 | pub components: Vec, 53 | /// Specifies sections (e.g. tool) to enable 54 | pub sections: Vec, 55 | } 56 | 57 | #[derive(Debug, Clone)] 58 | pub struct Config { 59 | pub artifacts_dir: PathBuf, 60 | pub verbose: bool, 61 | pub emit: EmitConfig, 62 | pub build: BuildConfig, 63 | pub components: ComponentsConfig, 64 | } 65 | -------------------------------------------------------------------------------- /src/dist-builder/src/fs_util.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::Path}; 2 | 3 | pub fn ensure_exists(path: impl AsRef) -> anyhow::Result<()> { 4 | use std::io::ErrorKind::*; 5 | match fs::create_dir_all(path) { 6 | Ok(_) => (), 7 | Err(e) => match e.kind() { 8 | AlreadyExists => (), 9 | _ => return Err(e.into()), 10 | }, 11 | }; 12 | 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /src/dist-builder/src/package.rs: -------------------------------------------------------------------------------- 1 | //! Abstractions for package 2 | #[derive(Debug)] 3 | pub(crate) struct RustPackage { 4 | /// As in sources 5 | pub(crate) name: String, 6 | pub(crate) install_name: String, 7 | pub(crate) section: Section, 8 | } 9 | 10 | pub(crate) struct CmakePackage { 11 | pub(crate) name: String, 12 | pub(crate) section: Section, 13 | } 14 | 15 | #[derive(Debug)] 16 | pub(crate) struct OtherPackage { 17 | /// As in sources 18 | pub(crate) name: String, 19 | pub(crate) section: Section, 20 | } 21 | 22 | /// Automatically enabled if user enabled specified section 23 | #[derive(Debug)] 24 | pub(crate) struct MetaPackage { 25 | pub(crate) name: String, 26 | pub(crate) section: Section, 27 | } 28 | 29 | #[derive(Debug, Copy, Clone)] 30 | pub(crate) enum Section { 31 | /// This component will run as part of JJS 32 | Daemon, 33 | /// This component is recommended for common scenario. 34 | Suggested, 35 | /// This component can be used to work with JJS 36 | Tool, 37 | } 38 | 39 | impl Section { 40 | pub(crate) const ALL: &'static [Section] = 41 | &[Section::Daemon, Section::Suggested, Section::Tool]; 42 | 43 | /// Returns section name in plural. 44 | /// used for validating&parsing CLI args 45 | pub(crate) fn plural(self) -> &'static str { 46 | match self { 47 | Section::Daemon => "daemons", 48 | Section::Suggested => "suggested", 49 | Section::Tool => "tools", 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/dist-files-generator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dist-files-generator" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | clap = "3.0.0-beta.2" 9 | serde = "1.0.117" 10 | util = {path = "../util"} 11 | anyhow = "1.0.33" 12 | fs_extra = "1.2.0" 13 | -------------------------------------------------------------------------------- /src/e2e/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e2e" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | apiserver-engine = {path = "../apiserver-engine"} 9 | reqwest = { version = "0.10.4", features = ["blocking"] } 10 | serde_json = "1.0.51" 11 | base64 = "0.12.0" 12 | -------------------------------------------------------------------------------- /src/e2e/tests/users.rs: -------------------------------------------------------------------------------- 1 | fn check_login_and_password(login: &str, password: &str) { 2 | e2e::RequestBuilder::new() 3 | .action("/users") 4 | .var("login", login) 5 | .var("password", password) 6 | .exec() 7 | .unwrap_ok(); 8 | e2e::RequestBuilder::new() 9 | .action("/auth/simple") 10 | .var("login", login) 11 | .var("password", password) 12 | .exec() 13 | .unwrap_ok(); 14 | } 15 | 16 | #[test] 17 | fn test_unicode() { 18 | check_login_and_password("猫鯉", "ありがとうございまので大丈夫"); 19 | check_login_and_password("💻🌐", "🔑"); 20 | } 21 | -------------------------------------------------------------------------------- /src/entity/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "entity" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serde = { version = "1.0.106", features = ["derive"] } 9 | pom = {path = "../pom"} 10 | serde_yaml = "0.8.11" 11 | either = "1.5.3" 12 | anyhow = "1.0.28" 13 | chrono = { version = "0.4.11", features = ["serde"] } 14 | -------------------------------------------------------------------------------- /src/entity/src/entities.rs: -------------------------------------------------------------------------------- 1 | //! Defines entities - types, used throughout all JJS code base 2 | //! Entities are plain dumb structs 3 | pub mod contest; 4 | pub mod toolchain; 5 | 6 | use serde::{de::DeserializeOwned, Serialize}; 7 | use std::{any::Any, fmt::Debug}; 8 | pub(crate) mod seal { 9 | pub trait Seal {} 10 | } 11 | use seal::Seal; 12 | pub trait Entity: Serialize + DeserializeOwned + Send + Sync + Debug + Any + Seal { 13 | fn name(&self) -> &str; 14 | 15 | fn postprocess(&mut self) -> anyhow::Result<()> { 16 | Ok(()) 17 | } 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use super::*; 23 | fn ensure_entity() {} 24 | 25 | #[test] 26 | fn test_contest_is_entity() { 27 | ensure_entity::() 28 | } 29 | #[test] 30 | fn test_toolchain_is_entity() { 31 | ensure_entity::() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/entity/src/entities/contest.rs: -------------------------------------------------------------------------------- 1 | use super::{Entity, Seal}; 2 | use serde::{Deserialize, Serialize}; 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct ProblemBinding { 5 | /// Problem unique ID 6 | pub name: String, 7 | 8 | /// Problem ID in contest 9 | pub code: String, 10 | } 11 | 12 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 13 | #[serde(rename_all = "kebab-case")] 14 | pub struct Contest { 15 | pub title: String, 16 | 17 | pub id: String, 18 | 19 | /// Information about problems, not related to judging 20 | /// process (which is controlled by problem itself) 21 | pub problems: Vec, 22 | 23 | /// List of groups of judges 24 | /// Judges will have full capabilities in this contest 25 | pub judges: Vec, 26 | 27 | /// Which group members are considered registered for contest 28 | pub group: Vec, 29 | 30 | /// Whether contest is visible for users that are not included in contestants 31 | #[serde(rename = "vis-unreg")] 32 | pub unregistered_visible: bool, 33 | 34 | /// Whether contest is visible for anonymous users 35 | #[serde(rename = "vis-anon")] 36 | pub anon_visible: bool, 37 | 38 | /// Contest start time. 39 | /// If not set, it is `-inf`. 40 | #[serde(default)] 41 | pub start_time: Option>, 42 | 43 | /// Contest end time. 44 | /// If not time, it is `+inf`. 45 | #[serde(default)] 46 | pub end_time: Option>, 47 | 48 | /// Contest duration. 49 | /// For non-virtual contest, must be either 50 | /// omitted or be equal to `end_time` - `start_time` 51 | #[serde(default)] 52 | pub duration: Option, 53 | 54 | /// If enabled, contest is virtual 55 | /// Virtual contest is started by user. 56 | /// User will not be able to interact with contest until they `takePart` in it. 57 | /// For virtual contest, `start_time` and `end_time` define a period of time when user can 58 | /// start their participation. 59 | #[serde(rename = "virtual")] 60 | #[serde(default)] 61 | pub is_virtual: bool, 62 | } 63 | 64 | impl Seal for Contest {} 65 | impl Entity for Contest { 66 | fn name(&self) -> &str { 67 | &self.id 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/entity/src/entities/toolchain.rs: -------------------------------------------------------------------------------- 1 | use super::{Entity, Seal}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::HashMap; 4 | #[derive(Serialize, Deserialize, Debug, Clone)] 5 | pub struct Toolchain { 6 | /// Human-readable 7 | pub title: String, 8 | 9 | /// Machine-readable 10 | pub name: String, 11 | 12 | pub filename: String, 13 | 14 | #[serde(rename = "build")] 15 | pub build_commands: Vec, 16 | 17 | #[serde(rename = "run")] 18 | pub run_command: Command, 19 | 20 | #[serde(rename = "build-limits", default)] 21 | pub limits: pom::Limits, 22 | 23 | #[serde(rename = "env", default)] 24 | pub env: HashMap, 25 | 26 | #[serde(rename = "env-passing", default)] 27 | pub env_passing: bool, 28 | 29 | #[serde(rename = "env-blacklist", default)] 30 | pub env_blacklist: Vec, 31 | } 32 | 33 | #[derive(Serialize, Deserialize, Default, Debug, Clone)] 34 | pub struct Command { 35 | #[serde(default = "Command::default_env")] 36 | pub env: HashMap, 37 | pub argv: Vec, 38 | #[serde(default = "Command::default_cwd")] 39 | pub cwd: String, 40 | } 41 | 42 | impl Command { 43 | fn default_env() -> HashMap { 44 | HashMap::new() 45 | } 46 | 47 | fn default_cwd() -> String { 48 | String::from("/jjs") 49 | } 50 | } 51 | fn command_inherit_env(cmd: &mut Command, dfl: &HashMap) { 52 | for (key, val) in dfl.iter() { 53 | cmd.env.entry(key.clone()).or_insert_with(|| val.clone()); 54 | } 55 | } 56 | impl Seal for Toolchain {} 57 | impl Entity for Toolchain { 58 | fn name(&self) -> &str { 59 | &self.name 60 | } 61 | 62 | fn postprocess(&mut self) -> anyhow::Result<()> { 63 | let mut inherit_env = self.env.clone(); 64 | if self.env_passing { 65 | for (key, value) in std::env::vars() { 66 | if self.env_blacklist.contains(&key) { 67 | continue; 68 | } 69 | inherit_env.entry(key).or_insert(value); 70 | } 71 | } 72 | 73 | for mut cmd in &mut self.build_commands { 74 | command_inherit_env(&mut cmd, &inherit_env); 75 | } 76 | command_inherit_env(&mut self.run_command, &inherit_env); 77 | 78 | Ok(()) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/entity/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod entities; 2 | pub mod loader; 3 | pub use entities::{contest::Contest, toolchain::Toolchain}; 4 | pub use loader::Loader; 5 | -------------------------------------------------------------------------------- /src/entity/src/loader.rs: -------------------------------------------------------------------------------- 1 | mod builder; 2 | 3 | pub use builder::LoaderBuilder; 4 | 5 | use std::{ 6 | any::{Any, TypeId}, 7 | collections::HashMap, 8 | }; 9 | 10 | use crate::entities::Entity; 11 | 12 | #[derive(Debug)] 13 | pub struct Loader { 14 | entities: HashMap>>, 15 | } 16 | 17 | impl Loader { 18 | pub fn list(&self) -> impl Iterator { 19 | let key = TypeId::of::(); 20 | match self.entities.get(&key) { 21 | Some(map) => { 22 | let iter = map 23 | .values() 24 | .map(|any_box| any_box.downcast_ref().expect("corrupted typemap in Loader")); 25 | either::Either::Left(iter) 26 | } 27 | None => either::Either::Right(std::iter::empty()), 28 | } 29 | } 30 | 31 | pub fn find(&self, name: &str) -> Option<&T> { 32 | let key = TypeId::of::(); 33 | self.entities 34 | .get(&key) 35 | .and_then(|map| map.get(name)) 36 | .map(|any_box| any_box.downcast_ref().expect("corrupted typemap in Loader")) 37 | } 38 | } 39 | 40 | impl Loader { 41 | pub fn load_from_data_dir(dir: &std::path::Path) -> anyhow::Result { 42 | let mut builder = LoaderBuilder::new(); 43 | builder.load_from_data_dir(dir)?; 44 | Ok(builder.into_inner()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/envck/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "envck" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | libc = "0.2.68" 9 | semver = "0.9.0" 10 | -------------------------------------------------------------------------------- /src/gen-api-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openapi" 3 | version = "0.1.0" 4 | authors = ["user"] 5 | edition = "2018" 6 | 7 | [lib] 8 | path = "lib.rs" 9 | 10 | [dependencies] 11 | async-trait = "0.1" 12 | bytes = "0.5" 13 | thiserror = "1.0.19" 14 | futures = "0.3" 15 | http = "0.2" 16 | lazy_static = "1.4" 17 | log = "0.4" 18 | mime = { git = "https://github.com/hyperium/mime" } 19 | mime_guess = "2.0" 20 | parking_lot = "0.8" 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_json = "1.0" 23 | serde_yaml = "0.8" 24 | tokio-util = { version = "0.3", features = ["codec"] } 25 | url = "2.1" 26 | 27 | tokio = { version = "0.2", features = ["fs", "io-util"] } 28 | reqwest = { version = "0.10", features = ["stream", "json"] } 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/gen-api-client/body_route_put_problem_problems_problem_id_put.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Default, Clone, Serialize, Deserialize)] 2 | pub struct BodyRoutePutProblemProblemsProblemIdPut { 3 | pub problem_assets: String, 4 | pub problem_manifest: String, 5 | } 6 | 7 | impl BodyRoutePutProblemProblemsProblemIdPut { 8 | /// Create a builder for this object. 9 | #[inline] 10 | pub fn builder() -> BodyRoutePutProblemProblemsProblemIdPutBuilder { 11 | BodyRoutePutProblemProblemsProblemIdPutBuilder { 12 | body: Default::default(), 13 | _problem_assets: core::marker::PhantomData, 14 | _problem_manifest: core::marker::PhantomData, 15 | } 16 | } 17 | } 18 | 19 | impl Into for BodyRoutePutProblemProblemsProblemIdPutBuilder { 20 | fn into(self) -> BodyRoutePutProblemProblemsProblemIdPut { 21 | self.body 22 | } 23 | } 24 | 25 | /// Builder for [`BodyRoutePutProblemProblemsProblemIdPut`](./struct.BodyRoutePutProblemProblemsProblemIdPut.html) object. 26 | #[derive(Debug, Clone)] 27 | pub struct BodyRoutePutProblemProblemsProblemIdPutBuilder { 28 | body: self::BodyRoutePutProblemProblemsProblemIdPut, 29 | _problem_assets: core::marker::PhantomData, 30 | _problem_manifest: core::marker::PhantomData, 31 | } 32 | 33 | impl BodyRoutePutProblemProblemsProblemIdPutBuilder { 34 | #[inline] 35 | pub fn problem_assets(mut self, value: impl Into) -> BodyRoutePutProblemProblemsProblemIdPutBuilder { 36 | self.body.problem_assets = value.into(); 37 | unsafe { std::mem::transmute(self) } 38 | } 39 | 40 | #[inline] 41 | pub fn problem_manifest(mut self, value: impl Into) -> BodyRoutePutProblemProblemsProblemIdPutBuilder { 42 | self.body.problem_manifest = value.into(); 43 | unsafe { std::mem::transmute(self) } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/gen-api-client/generics.rs: -------------------------------------------------------------------------------- 1 | 2 | pub struct MissingCode; 3 | pub struct CodeExists; 4 | pub struct MissingContest; 5 | pub struct ContestExists; 6 | pub struct MissingContestName; 7 | pub struct ContestNameExists; 8 | pub struct MissingData; 9 | pub struct DataExists; 10 | pub struct MissingDescription; 11 | pub struct DescriptionExists; 12 | pub struct MissingFinished; 13 | pub struct FinishedExists; 14 | pub struct MissingId; 15 | pub struct IdExists; 16 | pub struct MissingImage; 17 | pub struct ImageExists; 18 | pub struct MissingLimit; 19 | pub struct LimitExists; 20 | pub struct MissingLoc; 21 | pub struct LocExists; 22 | pub struct MissingLogin; 23 | pub struct LoginExists; 24 | pub struct MissingMajor; 25 | pub struct MajorExists; 26 | pub struct MissingMinor; 27 | pub struct MinorExists; 28 | pub struct MissingMsg; 29 | pub struct MsgExists; 30 | pub struct MissingName; 31 | pub struct NameExists; 32 | pub struct MissingPassword; 33 | pub struct PasswordExists; 34 | pub struct MissingPasswordHash; 35 | pub struct PasswordHashExists; 36 | pub struct MissingProblem; 37 | pub struct ProblemExists; 38 | pub struct MissingProblemAssets; 39 | pub struct ProblemAssetsExists; 40 | pub struct MissingProblemId; 41 | pub struct ProblemIdExists; 42 | pub struct MissingProblemManifest; 43 | pub struct ProblemManifestExists; 44 | pub struct MissingProblemName; 45 | pub struct ProblemNameExists; 46 | pub struct MissingRelName; 47 | pub struct RelNameExists; 48 | pub struct MissingRoles; 49 | pub struct RolesExists; 50 | pub struct MissingRunId; 51 | pub struct RunIdExists; 52 | pub struct MissingTitle; 53 | pub struct TitleExists; 54 | pub struct MissingToolchain; 55 | pub struct ToolchainExists; 56 | pub struct MissingToolchainId; 57 | pub struct ToolchainIdExists; 58 | pub struct MissingToolchainName; 59 | pub struct ToolchainNameExists; 60 | pub struct MissingType; 61 | pub struct TypeExists; 62 | pub struct MissingUserId; 63 | pub struct UserIdExists; 64 | -------------------------------------------------------------------------------- /src/gen-api-client/http_validation_error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Default, Clone, Serialize, Deserialize)] 2 | pub struct HttpValidationError { 3 | pub detail: Option>, 4 | } 5 | 6 | impl HttpValidationError { 7 | /// Create a builder for this object. 8 | #[inline] 9 | pub fn builder() -> HttpValidationErrorBuilder { 10 | HttpValidationErrorBuilder { 11 | body: Default::default(), 12 | } 13 | } 14 | } 15 | 16 | impl Into for HttpValidationErrorBuilder { 17 | fn into(self) -> HttpValidationError { 18 | self.body 19 | } 20 | } 21 | 22 | /// Builder for [`HttpValidationError`](./struct.HttpValidationError.html) object. 23 | #[derive(Debug, Clone)] 24 | pub struct HttpValidationErrorBuilder { 25 | body: self::HttpValidationError, 26 | } 27 | 28 | impl HttpValidationErrorBuilder { 29 | #[inline] 30 | pub fn detail(mut self, value: impl Iterator>) -> Self { 31 | self.body.detail = Some(value.map(|value| value.into()).collect::>().into()); 32 | self 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/gen-api-client/session_token.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Default, Clone, Serialize, Deserialize)] 2 | pub struct SessionToken { 3 | pub data: String, 4 | } 5 | 6 | impl SessionToken { 7 | /// Create a builder for this object. 8 | #[inline] 9 | pub fn builder() -> SessionTokenBuilder { 10 | SessionTokenBuilder { 11 | body: Default::default(), 12 | _data: core::marker::PhantomData, 13 | } 14 | } 15 | } 16 | 17 | impl Into for SessionTokenBuilder { 18 | fn into(self) -> SessionToken { 19 | self.body 20 | } 21 | } 22 | 23 | /// Builder for [`SessionToken`](./struct.SessionToken.html) object. 24 | #[derive(Debug, Clone)] 25 | pub struct SessionTokenBuilder { 26 | body: self::SessionToken, 27 | _data: core::marker::PhantomData, 28 | } 29 | 30 | impl SessionTokenBuilder { 31 | #[inline] 32 | pub fn data(mut self, value: impl Into) -> SessionTokenBuilder { 33 | self.body.data = value.into(); 34 | unsafe { std::mem::transmute(self) } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/gen-api-client/validation_error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Default, Clone, Serialize, Deserialize)] 2 | pub struct ValidationError { 3 | pub loc: Vec, 4 | pub msg: String, 5 | #[serde(rename = "type")] 6 | pub type_: String, 7 | } 8 | 9 | impl ValidationError { 10 | /// Create a builder for this object. 11 | #[inline] 12 | pub fn builder() -> ValidationErrorBuilder { 13 | ValidationErrorBuilder { 14 | body: Default::default(), 15 | _loc: core::marker::PhantomData, 16 | _msg: core::marker::PhantomData, 17 | _type: core::marker::PhantomData, 18 | } 19 | } 20 | } 21 | 22 | impl Into for ValidationErrorBuilder { 23 | fn into(self) -> ValidationError { 24 | self.body 25 | } 26 | } 27 | 28 | /// Builder for [`ValidationError`](./struct.ValidationError.html) object. 29 | #[derive(Debug, Clone)] 30 | pub struct ValidationErrorBuilder { 31 | body: self::ValidationError, 32 | _loc: core::marker::PhantomData, 33 | _msg: core::marker::PhantomData, 34 | _type: core::marker::PhantomData, 35 | } 36 | 37 | impl ValidationErrorBuilder { 38 | #[inline] 39 | pub fn loc(mut self, value: impl Iterator>) -> ValidationErrorBuilder { 40 | self.body.loc = value.map(|value| value.into()).collect::>().into(); 41 | unsafe { std::mem::transmute(self) } 42 | } 43 | 44 | #[inline] 45 | pub fn msg(mut self, value: impl Into) -> ValidationErrorBuilder { 46 | self.body.msg = value.into(); 47 | unsafe { std::mem::transmute(self) } 48 | } 49 | 50 | #[inline] 51 | pub fn type_(mut self, value: impl Into) -> ValidationErrorBuilder { 52 | self.body.type_ = value.into(); 53 | unsafe { std::mem::transmute(self) } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/invoker-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "invoker-api" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serde = { version = "1.0.117", features = ["derive"] } 9 | strum = "0.19.5" 10 | strum_macros = "0.19.4" 11 | bitflags = "1.2.1" 12 | pom = {path = "../pom"} 13 | uuid = { version = "0.8.1", features = ["serde"] } 14 | -------------------------------------------------------------------------------- /src/invoker-api/src/judge_log.rs: -------------------------------------------------------------------------------- 1 | //! Judge log stored in FS 2 | pub use crate::valuer_proto::JudgeLogKind; 3 | use crate::{valuer_proto::SubtaskId, Status, StatusKind}; 4 | use serde::{Deserialize, Serialize}; 5 | #[derive(Debug, Serialize, Deserialize, Clone)] 6 | pub struct JudgeLogTestRow { 7 | pub test_id: pom::TestId, 8 | pub status: Option, 9 | pub test_stdin: Option, 10 | pub test_stdout: Option, 11 | pub test_stderr: Option, 12 | pub test_answer: Option, 13 | pub time_usage: Option, 14 | pub memory_usage: Option, 15 | } 16 | 17 | #[derive(Debug, Serialize, Deserialize, Clone)] 18 | pub struct JudgeLogSubtaskRow { 19 | pub subtask_id: SubtaskId, 20 | pub score: Option, 21 | } 22 | 23 | #[derive(Debug, Serialize, Deserialize, Clone)] 24 | pub struct JudgeLog { 25 | pub kind: JudgeLogKind, 26 | pub tests: Vec, 27 | pub subtasks: Vec, 28 | pub compile_stdout: String, 29 | pub compile_stderr: String, 30 | pub score: u32, 31 | pub is_full: bool, 32 | pub status: Status, 33 | } 34 | 35 | impl Default for JudgeLog { 36 | fn default() -> Self { 37 | Self { 38 | kind: JudgeLogKind::Contestant, 39 | tests: vec![], 40 | subtasks: vec![], 41 | compile_stdout: String::new(), 42 | compile_stderr: String::new(), 43 | score: 0, 44 | is_full: false, 45 | status: Status { 46 | code: "".to_string(), 47 | kind: StatusKind::NotSet, 48 | }, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/invoker/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "invoker" 4 | version = "0.1.0" 5 | authors = ["Mikail Bagishov "] 6 | edition = "2018" 7 | 8 | [dependencies] 9 | minion = {git = "https://github.com/jjs-dev/minion"} 10 | serde = { version = "1.0.117", features = ["derive"] } 11 | serde_json = "1.0.59" 12 | dotenv = "0.15.0" 13 | aho-corasick = "0.7.14" 14 | invoker-api = {path = "../invoker-api"} 15 | pom = {path = "../pom"} 16 | libc = "0.2.80" 17 | nix = "0.19.0" 18 | strum = "0.19.5" 19 | strum_macros = "0.19.4" 20 | chrono = "0.4.19" 21 | tempfile = "3.1.0" 22 | fs_extra = "1.2.0" 23 | base64 = "0.13.0" 24 | bitflags = "1.2.1" 25 | util = {path = "../util"} 26 | anyhow = "1.0.33" 27 | thiserror = "1.0.21" 28 | uuid = { version = "0.8.1", features = ["v5"] } 29 | problem-loader = {path = "../problem-loader"} 30 | tokio = { version = "0.2.22", features = ["rt-core", "process", "io-std", "macros", "fs", "sync"] } 31 | async-trait = "0.1.41" 32 | num_cpus = "1.13.0" 33 | serde_yaml = "0.8.14" 34 | openssl = "0.10.30" 35 | actix-web = { version = "3.2.0", features = ["openssl"], default-features = false } 36 | actix-rt = { version = "1.1.1", default-features = false } 37 | once_cell = "1.4.1" 38 | client = {path = "../client"} 39 | kube = { version = "0.43.0", optional = true } 40 | k8s-openapi = { version = "0.9.0", optional = true, features = ["v1_17"], default-features = false } 41 | puller = { git = "https://github.com/jjs-dev/commons" } 42 | tracing = "0.1.21" 43 | tracing-futures = "0.2.4" 44 | async-mpmc = { git = "https://github.com/jjs-dev/commons" } 45 | multiwake = { git = "https://github.com/jjs-dev/commons" } 46 | dkregistry = { git = "https://github.com/mikailbag/dkregistry-rs", branch = "all" } 47 | 48 | [features] 49 | k8s = ["kube", "k8s-openapi"] 50 | -------------------------------------------------------------------------------- /src/invoker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-slim 2 | RUN apt-get update -y && apt-get install -y libssl-dev ca-certificates 3 | ENV JJS_DATA=/data 4 | ENV JJS_AUTH_DATA=/auth/authdata.yaml 5 | ENV RUST_BACKTRACE=1 6 | COPY jjs-invoker /bin/jjs-invoker 7 | ENTRYPOINT ["jjs-invoker"] 8 | -------------------------------------------------------------------------------- /src/invoker/src/api.rs: -------------------------------------------------------------------------------- 1 | //! Defines Invoker API 2 | //! 3 | //! If you just want to use JJS, you should look at apiserver. 4 | //! This API is desired for advanced use cases, such as integrating invoker 5 | //! in custom system. 6 | 7 | use crate::controller::JudgeRequestAndCallbacks; 8 | use actix_web::{web, App, HttpResponse, HttpServer, Responder}; 9 | use anyhow::Context as _; 10 | use tracing::instrument; 11 | 12 | #[derive(Clone)] 13 | struct State { 14 | task_tx: async_mpmc::Sender, 15 | cancel_token: tokio::sync::CancellationToken, 16 | } 17 | 18 | async fn route_ping() -> impl Responder { 19 | HttpResponse::Ok() 20 | .content_type("text/plain") 21 | .body("hello, world!") 22 | } 23 | 24 | async fn route_ready() -> impl Responder { 25 | "" 26 | } 27 | 28 | async fn route_shutdown(state: web::Data) -> impl Responder { 29 | tracing::info!("invoker api: got shutdown request"); 30 | state.cancel_token.cancel(); 31 | "cancellation triggered" 32 | } 33 | 34 | #[actix_rt::main] 35 | #[instrument(skip(task_tx))] 36 | async fn exec( 37 | cancel_token: tokio::sync::CancellationToken, 38 | bind_addr: std::net::SocketAddr, 39 | task_tx: async_mpmc::Sender, 40 | ) -> anyhow::Result<()> { 41 | let state = State { 42 | task_tx, 43 | cancel_token: cancel_token.clone(), 44 | }; 45 | 46 | let srv = HttpServer::new(move || { 47 | App::new() 48 | .app_data(web::Data::new(state.clone())) 49 | .wrap(actix_web::middleware::Logger::default()) 50 | .route("/", web::get().to(route_ping)) 51 | .route("/ready", web::get().to(route_ready)) 52 | .route("/state/shutdown", web::post().to(route_shutdown)) 53 | }) 54 | .workers(1) 55 | .disable_signals() 56 | .bind(bind_addr) 57 | .context("unable to bind")? 58 | .run(); 59 | cancel_token.cancelled().await; 60 | srv.stop(false).await; 61 | 62 | Ok(()) 63 | } 64 | 65 | pub async fn start( 66 | cancel_token: tokio::sync::CancellationToken, 67 | bind_addr: std::net::SocketAddr, 68 | task_tx: async_mpmc::Sender, 69 | ) -> Result<(), anyhow::Error> { 70 | tokio::task::spawn_blocking(move || { 71 | if let Err(err) = exec(cancel_token, bind_addr, task_tx) { 72 | eprintln!("Invoker api service: serve error: {:#}", err); 73 | } 74 | }); 75 | Ok(()) 76 | } 77 | -------------------------------------------------------------------------------- /src/invoker/src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize, Serialize, Debug)] 4 | #[serde(deny_unknown_fields)] 5 | #[serde(rename_all = "kebab-case")] 6 | pub struct InvokerConfig { 7 | /// How many workers should be spawned 8 | /// By default equal to processor count 9 | #[serde(default)] 10 | pub workers: Option, 11 | /// API service config 12 | #[serde(default)] 13 | pub api: ApiSvcConfig, 14 | /// If enabled, invoker will directly mount host filesystem instead of 15 | /// toolchain image. 16 | #[serde(default)] 17 | pub host_toolchains: bool, 18 | /// Override directories that will be mounted into sandbox. 19 | /// E.g. if `expose-host-dirs = ["lib64", "usr/lib"]`, 20 | /// then invoker will mount: 21 | /// - `$SANDBOX_ROOT/lib64` -> `/lib64` 22 | /// - `$SANDBOX_ROOT/usr/lib` -> `/usr/lib` 23 | /// As usual, all mounts will be no-suid and read-only. 24 | #[serde(default)] 25 | pub expose_host_dirs: Option>, 26 | /// Configures how invoker should resolve problems 27 | pub problems: problem_loader::LoaderConfig, 28 | } 29 | 30 | #[derive(Deserialize, Serialize, Debug)] 31 | #[serde(deny_unknown_fields)] 32 | pub struct ApiSvcConfig { 33 | /// Override bind IP 34 | #[serde(default = "ApiSvcConfig::default_address")] 35 | pub address: String, 36 | /// Override bind port 37 | #[serde(default = "ApiSvcConfig::default_port")] 38 | pub port: u16, 39 | } 40 | 41 | impl ApiSvcConfig { 42 | fn default_address() -> String { 43 | "0.0.0.0".to_string() 44 | } 45 | 46 | fn default_port() -> u16 { 47 | 1789 48 | } 49 | } 50 | 51 | impl Default for ApiSvcConfig { 52 | fn default() -> Self { 53 | ApiSvcConfig { 54 | address: ApiSvcConfig::default_address(), 55 | port: ApiSvcConfig::default_port(), 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/invoker/src/init.rs: -------------------------------------------------------------------------------- 1 | //! platform-specific initialization 2 | use anyhow::{bail, Context}; 3 | use nix::sched::CloneFlags; 4 | fn check_system() -> anyhow::Result<()> { 5 | if let Some(err) = minion::check() { 6 | bail!("invoker is not able to test runs: {}", err); 7 | } 8 | Ok(()) 9 | } 10 | 11 | fn unshare_mount_namespace() -> anyhow::Result<()> { 12 | nix::sched::unshare(CloneFlags::CLONE_NEWNS).context("unshare() fail") 13 | } 14 | 15 | fn unshare_user_namespace() -> anyhow::Result<()> { 16 | if nix::unistd::getuid().is_root() { 17 | return Ok(()); 18 | } 19 | let uid = nix::unistd::getuid().as_raw(); 20 | let gid = nix::unistd::getgid().as_raw(); 21 | let uid_mapping = format!("0 {} 1", uid); 22 | let gid_mapping = format!("0 {} 1", gid); 23 | nix::sched::unshare(CloneFlags::CLONE_NEWUSER).context("unshare() fail")?; 24 | std::fs::write("/proc/self/setgroups", "deny").context("failed to deny setgroups()")?; 25 | std::fs::write("/proc/self/uid_map", uid_mapping).context("failed to setup uid mapping")?; 26 | std::fs::write("/proc/self/gid_map", gid_mapping).context("failed to setup gid mapping")?; 27 | nix::unistd::setuid(nix::unistd::Uid::from_raw(0)).expect("failed to become root user"); 28 | nix::unistd::setgid(nix::unistd::Gid::from_raw(0)).expect("failed to join root group"); 29 | Ok(()) 30 | } 31 | 32 | fn unshare() -> anyhow::Result<()> { 33 | unshare_user_namespace().context("failed to unshare user ns")?; 34 | unshare_mount_namespace().context("failed to unshare mount ns")?; 35 | Ok(()) 36 | } 37 | 38 | pub fn init() -> anyhow::Result<()> { 39 | check_system().context("system configuration problem detected")?; 40 | unshare().context("failed to create namespaces")?; 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /src/invoker/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![type_length_limit = "4323264"] 2 | pub mod api; 3 | pub mod config; 4 | pub mod controller; 5 | pub mod init; 6 | mod scheduler; 7 | pub mod sources; 8 | pub mod worker; 9 | -------------------------------------------------------------------------------- /src/invoker/src/sources.rs: -------------------------------------------------------------------------------- 1 | mod api_source; 2 | pub mod cli_source; 3 | 4 | pub use api_source::ApiSource; 5 | -------------------------------------------------------------------------------- /src/invoker/src/worker/exec_test/checker_proto.rs: -------------------------------------------------------------------------------- 1 | //! Checker out file parser 2 | use anyhow::bail; 3 | use strum_macros::EnumString; 4 | 5 | #[derive(EnumString)] 6 | pub enum Outcome { 7 | Ok, 8 | WrongAnswer, 9 | PresentationError, 10 | #[strum(to_string = "CheckerLogicError")] 11 | BadChecker, 12 | } 13 | 14 | pub struct Output { 15 | pub outcome: Outcome, 16 | } 17 | 18 | pub fn parse(data: &str) -> anyhow::Result { 19 | let mut res_outcome = None; 20 | for (line_id, line) in data.lines().enumerate() { 21 | let line_id = (line_id + 1) as u32; 22 | let p = match line.find('=') { 23 | Some(i) => i, 24 | None => { 25 | bail!( 26 | "Line {} doesn't contain '='-separated key and value", 27 | line_id 28 | ); 29 | } 30 | }; 31 | let tag = &data[..p]; 32 | let value = &data[p + 1..]; 33 | match tag { 34 | "outcome" => { 35 | let data = value.trim(); 36 | let outcome: Outcome = match data.parse() { 37 | Ok(o) => o, 38 | Err(e) => { 39 | bail!("Tag outcome: {}", e); 40 | } 41 | }; 42 | if res_outcome.replace(outcome).is_some() { 43 | bail!("Tag outcome redefined"); 44 | } 45 | } 46 | _ => { 47 | bail!("Line {}: unknown tag {}", line_id, tag); 48 | } 49 | } 50 | } 51 | let outcome = match res_outcome { 52 | Some(o) => o, 53 | None => { 54 | bail!("Tag outcome missong"); 55 | } 56 | }; 57 | Ok(Output { outcome }) 58 | } 59 | -------------------------------------------------------------------------------- /src/invoker/src/worker/os_util.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | 3 | pub fn buffer_to_file(buf: &[u8], comment: &str) -> i64 { 4 | use nix::{ 5 | fcntl::{self, FcntlArg}, 6 | sys::memfd::{self, MemFdCreateFlag}, 7 | }; 8 | let fd = memfd::memfd_create( 9 | &CString::new(comment).unwrap(), 10 | MemFdCreateFlag::MFD_ALLOW_SEALING, 11 | ) 12 | .unwrap(); 13 | let mut buf_rem = buf; 14 | loop { 15 | let cnt = nix::unistd::write(fd, buf_rem).unwrap(); 16 | buf_rem = &buf_rem[cnt..]; 17 | if cnt == 0 { 18 | break; 19 | } 20 | } 21 | // now seal memfd 22 | // currently this is not important, but when... 23 | // TODO: cache all this stuff 24 | // ... it is important that file can't be altered by solution 25 | let seals = libc::F_SEAL_GROW | libc::F_SEAL_SEAL | libc::F_SEAL_WRITE | libc::F_SEAL_SHRINK; 26 | fcntl::fcntl( 27 | fd, 28 | FcntlArg::F_ADD_SEALS(fcntl::SealFlag::from_bits(seals).unwrap()), 29 | ) 30 | .unwrap(); 31 | // and seek fd to begin 32 | nix::unistd::lseek64(fd, 0, nix::unistd::Whence::SeekSet).unwrap(); 33 | i64::from(fd) 34 | } 35 | 36 | // this is bug in clippy: https://github.com/rust-lang/rust-clippy/issues/5368 37 | #[allow(clippy::verbose_file_reads)] 38 | pub fn handle_read_all(h: i64) -> Vec { 39 | use std::{io::Read, os::unix::io::FromRawFd}; 40 | let h = h as i32; 41 | let mut file = unsafe { std::fs::File::from_raw_fd(h) }; 42 | let mut out = Vec::new(); 43 | let res = file.read_to_end(&mut out); 44 | res.unwrap(); 45 | out 46 | } 47 | 48 | pub fn make_pipe() -> (i64, i64) { 49 | let (a, b) = nix::unistd::pipe().unwrap(); 50 | (i64::from(a), i64::from(b)) 51 | } 52 | 53 | pub fn handle_inherit(h: i64, close: bool) -> i64 { 54 | let out = i64::from(nix::unistd::dup(h as i32).unwrap()); 55 | if close { 56 | nix::unistd::close(h as i32).unwrap() 57 | } 58 | 59 | out 60 | } 61 | 62 | pub fn close(h: i64) { 63 | nix::unistd::close(h as i32).unwrap() 64 | } 65 | -------------------------------------------------------------------------------- /src/invoker/tests/separated_feedback.rs: -------------------------------------------------------------------------------- 1 | #[tokio::test] 2 | async fn separated_feedback() { 3 | // TODO write this test when cfg is rewritten 4 | } 5 | -------------------------------------------------------------------------------- /src/jtl/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13.4) 2 | project(Jtl VERSION 0.1.0) 3 | set(CMAKE_CXX_STANDARD 17) 4 | 5 | set(SRC_FILES src/jtl.cpp src/testgen.cpp src/proto.cpp src/checker.cpp src/util.cpp) 6 | set(HEADER_FILES include/jtl.h include/testgen.h include/checker.h) 7 | 8 | set(TEMP_DIR ${CMAKE_CURRENT_BINARY_DIR}) 9 | 10 | # JTL itself 11 | add_library(Jtl STATIC ${SRC_FILES}) 12 | target_include_directories(Jtl PUBLIC include src ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR}/deps) 13 | set_target_properties(Jtl PROPERTIES PUBLIC_HEADER "${HEADER_FILES}") 14 | target_link_libraries(Jtl PUBLIC pthread rt dl m) 15 | 16 | install(TARGETS Jtl 17 | ARCHIVE DESTINATION /tmp 18 | PUBLIC_HEADER DESTINATION include/jjs) 19 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libJtl.a DESTINATION lib RENAME libjtl.a) 20 | install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/JtlConfig.cmake DESTINATION share/cmake) 21 | 22 | # Checkers, valuers, testgens, etc 23 | function(add_builtin builtin_name src_file) 24 | set(target_name builtin-${builtin_name}) 25 | add_executable(${target_name} ${src_file}) 26 | target_include_directories(${target_name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) 27 | target_link_options(${target_name} PUBLIC -L${CMAKE_CURRENT_BINARY_DIR}) 28 | target_link_libraries(${target_name} PUBLIC Jtl) 29 | add_dependencies(${target_name} Jtl) 30 | install(TARGETS ${target_name} 31 | RUNTIME DESTINATION bin) 32 | endfunction() 33 | add_builtin(checker-cmp-tokens src/builtin/checker-cmp-tokens.cpp) 34 | add_builtin(checker-polygon-compat src/builtin/checker-polygon-compat.cpp) -------------------------------------------------------------------------------- /src/jtl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-slim 2 | COPY / /jtl -------------------------------------------------------------------------------- /src/jtl/JtlConfig.cmake: -------------------------------------------------------------------------------- 1 | # TODO: Required etc 2 | # Output: 3 | # Jtl_LIBS: libraries you should link to 4 | # Jtl_INCLUDES: add to include path 5 | set(Jtl_HW foo) 6 | if (DEFINED ENV{JJS_PATH}) 7 | set(Jtl_LIBS $ENV{JJS_PATH}/lib/libjtl.a pthread m dl rt) 8 | set(Jtl_INCLUDES $ENV{JJS_PATH}/include/) 9 | else () 10 | message(FATAL_ERROR "$JJS_PATH env var is not set") 11 | endif () -------------------------------------------------------------------------------- /src/jtl/cbindgen.toml: -------------------------------------------------------------------------------- 1 | autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify it manually. */" 2 | language = "C++" 3 | 4 | [export] 5 | exclude = [] 6 | 7 | [enum] 8 | rename_variants = "QualifiedScreamingSnakeCase" -------------------------------------------------------------------------------- /src/jtl/include/checker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jtl.h" 4 | #include 5 | 6 | namespace checker { 7 | 8 | void comment(const char* format, ...) PRINT_FORMAT_FN(1); 9 | 10 | void sol_scanf(const char* format, ...) SCAN_FORMAT_FN(1); 11 | 12 | void corr_scanf(const char* format, ...) SCAN_FORMAT_FN(1); 13 | 14 | void test_scanf(const char* format, ...) SCAN_FORMAT_FN(1); 15 | 16 | void check_sol_eof(); 17 | 18 | void check_corr_eof(); 19 | 20 | void check_test_eof(); 21 | 22 | struct CheckerInput { 23 | /// Contestant's solution answer 24 | FILE* sol_answer = nullptr; 25 | /// Correct answer (answer generated by primary solution), if requested by 26 | /// problem config. Otherwise, refers to /dev/null 27 | FILE* corr_answer = nullptr; 28 | /// Test data 29 | FILE* test = nullptr; 30 | 31 | int64_t fd_sol = -1; 32 | int64_t fd_corr = -1; 33 | int64_t fd_test = -1; 34 | }; 35 | 36 | CheckerInput init(bool open_files = true); 37 | 38 | /// Reads next char sequence, followed by whitespace 39 | /// next_token() returns owning pointer to token. This pointer should be freed 40 | /// by free() 41 | char* next_token(FILE* f); 42 | 43 | enum class Outcome { 44 | 45 | /// Checker couldn't recognize answer 46 | PRESENTATION_ERROR, 47 | 48 | /// Answer was wrong 49 | WRONG_ANSWER, 50 | 51 | /// Correct answer 52 | OK, 53 | 54 | /// Checker is incorrect 55 | /// for example, contestant provided more optimal answer than jury 56 | CHECKER_LOGIC_ERROR, 57 | }; 58 | 59 | /// Checker exits using this function 60 | /// If checker simply exits with e.g. exit(0) protocol will be broken and 61 | /// internal judging error will be diagnosed 62 | void finish(Outcome outcome) ATTR_NORETURN; 63 | 64 | /// Some comparison functions 65 | 66 | bool compare_epsilon(long double expected, long double actual, 67 | long double epsilon); 68 | 69 | bool compare_strings_ignore_case(const char* lhs, const char* rhs); 70 | } // namespace checker -------------------------------------------------------------------------------- /src/jtl/include/jtl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "testgen.h" 4 | #include 5 | 6 | #ifdef __GNUC__ 7 | #define SCAN_FORMAT_FN(x) __attribute__((format(scanf, x, x + 1))) 8 | #define PRINT_FORMAT_FN(x) __attribute__((format(printf, x, x + 1))) 9 | #else 10 | #define SCAN_FORMAT_FN(x) 11 | #define PRINT_FORMAT_FN(x) 12 | #endif 13 | 14 | #ifdef __GNUC__ 15 | #define ATTR_NORETURN __attribute__((noreturn)) 16 | #else 17 | #define ATTR_NORETURN 18 | #endif 19 | 20 | #ifdef __GNUC__ 21 | #define ATTR_RET_NONNULL __attribute__((returns_nonnull)) 22 | #else 23 | #define ATTR_RET_NONNULL 24 | #endif 25 | 26 | /// utility functions checks than only whitespace chars are remaining in file 27 | bool is_file_eof(FILE* f); 28 | 29 | void oom() ATTR_NORETURN; 30 | 31 | void* check_oom(void* ptr) ATTR_RET_NONNULL; 32 | 33 | void die(char const* message, ...) ATTR_NORETURN PRINT_FORMAT_FN(1); -------------------------------------------------------------------------------- /src/jtl/include/testgen.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace testgen { 10 | class Generator { 11 | std::mt19937_64 gen; 12 | 13 | public: 14 | // disallow implicit copies 15 | Generator(const Generator& gen) noexcept = default; 16 | 17 | explicit Generator(uint64_t seed); 18 | 19 | uint64_t next_u64(); 20 | 21 | size_t next_usize(); 22 | 23 | /// generates number in [lo; hi) 24 | uint64_t next_range(uint64_t lo, uint64_t hi); 25 | 26 | template 27 | T choose_uniform(RAIter begin, RAIter end) { 28 | size_t const item_count = std::distance(begin, end); 29 | auto const selected_pos = (size_t) next_range(0, (uint64_t) item_count); 30 | *std::advance(begin, selected_pos); 31 | } 32 | 33 | /// returns new generator, which state is initially same with this 34 | Generator clone(); 35 | }; 36 | 37 | struct TestgenSession { 38 | int test_id = 0; 39 | Generator gen; 40 | 41 | TestgenSession(uint64_t _seed); 42 | }; 43 | 44 | /// Call this first in test generator 45 | TestgenSession init(); 46 | } // namespace testgen 47 | -------------------------------------------------------------------------------- /src/jtl/src/jtl.cpp: -------------------------------------------------------------------------------- 1 | #include "jtl.h" 2 | #include "proto.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | static bool is_char_whitespace(char c) { 10 | return c == ' ' || c == '\n' || c == '\t'; 11 | } 12 | 13 | bool is_file_eof(FILE* f) { 14 | const int BUF_SIZE = 256; 15 | char buf[BUF_SIZE]; 16 | while (true) { 17 | int nread = fread(buf, 1, BUF_SIZE, f); 18 | for (int i = 0; i < nread; ++i) { 19 | if (!is_char_whitespace(buf[i])) { 20 | return false; 21 | } 22 | } 23 | if (nread == 0) { 24 | break; 25 | } 26 | } 27 | return true; 28 | } 29 | 30 | void oom() { 31 | fprintf(stderr, "Out of memory"); 32 | abort(); 33 | } 34 | 35 | void* check_oom(void* ptr) { 36 | if (ptr) { 37 | return ptr; 38 | } else { 39 | oom(); 40 | } 41 | } 42 | 43 | void die(char const* message, ...) { 44 | va_list v; 45 | va_start(v, message); 46 | vfprintf(stderr, message, v); 47 | exit(1); 48 | } 49 | -------------------------------------------------------------------------------- /src/jtl/src/proto.cpp: -------------------------------------------------------------------------------- 1 | #include "proto.h" 2 | #include "jtl.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | char* get_env(const char* var_name) { 9 | char* res = getenv(var_name); 10 | if (res == nullptr) { 11 | die("ERROR: var %s not present\n", var_name); 12 | } 13 | return res; 14 | } 15 | 16 | int get_env_int(const char* var_name) { 17 | char* res = get_env(var_name); 18 | int ans; 19 | if (sscanf(res, "%d", &ans) == 0) { 20 | die("ERROR: var `%s` has value `%s`, which is not integer\n", var_name, 21 | res); 22 | } 23 | return ans; 24 | } 25 | 26 | FILE* get_env_file(const char* var_name, const char* mode) { 27 | int fd = get_env_int(var_name); 28 | FILE* file = fdopen(fd, mode); 29 | if (file == nullptr) { 30 | die("ERROR: var `%s` contains fd `%d`, which is not file of mode %s", 31 | var_name, fd, mode); 32 | } 33 | return file; 34 | } 35 | 36 | const uint8_t CHAR_BAD = 255; 37 | 38 | uint8_t decode_hex_char(char x) { 39 | if ('0' <= x && x <= '9') 40 | return x - '0'; 41 | if ('a' <= x && x <= 'f') 42 | return x - 'a' + 10; 43 | return CHAR_BAD; 44 | } 45 | 46 | BinString decode_hex(char* data) { 47 | size_t n = strlen(data); 48 | if (n % 2 != 0) 49 | return {}; 50 | auto out = new uint8_t[n / 2]; 51 | for (size_t i = 0; i < n / 2; ++i) { 52 | auto a = decode_hex_char(data[2 * i]); 53 | auto b = decode_hex_char(data[2 * i + 1]); 54 | if (a == CHAR_BAD || b == CHAR_BAD) { 55 | delete[] out; 56 | return {}; 57 | } 58 | out[i] = a * 16 + b; 59 | } 60 | BinString bs; 61 | bs.len = n / 2; 62 | bs.head.reset(out); 63 | return std::move(bs); 64 | } 65 | 66 | BinString get_env_hex(const char* var_name) { 67 | char* value = get_env(var_name); 68 | 69 | auto res = decode_hex(value); 70 | if (!res.head) { 71 | die("ERROR: var `%s` contains '%s', which is not hex\n", var_name, 72 | value); 73 | } 74 | return res; 75 | } 76 | -------------------------------------------------------------------------------- /src/jtl/src/proto.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | char* get_env(const char* var_name); 8 | 9 | int get_env_int(const char* var_name); 10 | 11 | FILE* get_env_file(const char* var_name, const char* mode); 12 | 13 | struct BinString { 14 | std::unique_ptr head; 15 | size_t len = 0; 16 | }; 17 | 18 | BinString get_env_hex(const char* var_name); 19 | -------------------------------------------------------------------------------- /src/jtl/src/testgen.cpp: -------------------------------------------------------------------------------- 1 | #include "testgen.h" 2 | #include "jtl.h" 3 | #include "proto.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | testgen::Generator::Generator(uint64_t seed) : gen(seed) {} 10 | 11 | uint64_t testgen::Generator::next_u64() { return gen(); } 12 | 13 | /// Returns random number in [0; n) 14 | static uint64_t get_rand(uint64_t n, std::mt19937_64& gen) { 15 | assert(n != 0); 16 | uint64_t bits = n; 17 | // step one: we want `bits` to contain highest bit of n, and all smaller 18 | bits |= (bits >> 1u); 19 | bits |= (bits >> 2u); 20 | bits |= (bits >> 4u); 21 | bits |= (bits >> 8u); 22 | bits |= (bits >> 16u); 23 | bits |= (bits >> 32u); 24 | while (true) { 25 | uint64_t s = gen(); 26 | s &= bits; 27 | // why is it fast: bits is smaller than 2*n, so probability that this 28 | // iteration succeed is at least 0.5 29 | if (s < n) { 30 | return s; 31 | } 32 | } 33 | } 34 | 35 | uint64_t testgen::Generator::next_range(uint64_t lo, uint64_t hi) { 36 | assert(lo < hi); 37 | return lo + get_rand(hi - lo, gen); 38 | } 39 | testgen::TestgenSession::TestgenSession(uint64_t _seed) : gen(_seed) {} 40 | testgen::TestgenSession testgen::init() { 41 | auto rand_seed = get_env_hex("JJS_RANDOM_SEED"); 42 | if (rand_seed.len != 8) { 43 | die("rand_seed has incorrect length (%zu instead of 8)\n", 44 | rand_seed.len); 45 | } 46 | uint64_t random_seed; 47 | memcpy(&random_seed, rand_seed.head.get(), 8); 48 | testgen::TestgenSession sess {random_seed}; 49 | sess.test_id = get_env_int("JJS_TEST_ID"); 50 | return sess; 51 | } 52 | -------------------------------------------------------------------------------- /src/jtl/src/util.cpp: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | #include 3 | 4 | static const uintptr_t SIGN_EXTENSION = 0xffff'0000'0000'0000; 5 | 6 | bool jtl::check_pointer(void* ptr) { 7 | const auto p = (uintptr_t) ptr; 8 | const auto sign_ext = p & SIGN_EXTENSION; 9 | return (sign_ext == SIGN_EXTENSION) || (sign_ext == 0); 10 | } -------------------------------------------------------------------------------- /src/jtl/src/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | namespace jtl { 5 | bool check_pointer(void* ptr); 6 | 7 | struct FileInfo { 8 | char* path; 9 | FILE* file; 10 | }; 11 | } // namespace jtl -------------------------------------------------------------------------------- /src/pom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pom" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | description = "problems&contests object model (shared between invoker and tt)" 7 | 8 | [dependencies] 9 | serde = { version = "1.0.117", features = ["derive"] } 10 | -------------------------------------------------------------------------------- /src/pps/Readme.md: -------------------------------------------------------------------------------- 1 | # JJS Problem preparation system (PPS) 2 | PPS goal is to provide next-generation problem preparation experience 3 | -------------------------------------------------------------------------------- /src/pps/api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pps-api" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serde = { version = "1.0.117", features = ["derive"] } 9 | reqwest = "0.10.8" 10 | anyhow = "1.0.33" 11 | serde_json = "1.0.59" 12 | tokio = { version = "0.2.22", features = ["sync"] } 13 | tracing = "0.1.21" 14 | rpc = { git = "https://github.com/jjs-dev/commons" } 15 | -------------------------------------------------------------------------------- /src/pps/api/src/compile_problem.rs: -------------------------------------------------------------------------------- 1 | //! Specifies that problem contained in workspace 2 | //! should be compiled 3 | use serde::{Deserialize, Serialize}; 4 | use std::path::PathBuf; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | pub struct Request { 8 | /// Path to problem source directory 9 | pub problem_path: PathBuf, 10 | /// Where to put compiled package 11 | pub out_path: PathBuf, 12 | /// Ignore existing files in out_path 13 | pub force: bool, 14 | } 15 | 16 | #[derive(Serialize, Deserialize)] 17 | pub enum Update { 18 | /// Contains some warnings that should be displayed to used. 19 | /// Appears at most once. 20 | Warnings(Vec), 21 | /// Solution with given name is being built 22 | BuildSolution(String), 23 | /// Test generator with given name is being built 24 | BuildTestgen(String), 25 | /// Checker building started 26 | BuildChecker, 27 | /// Test generation started. `count` tests will be processed. 28 | /// Appears at most once before `GenerateTest` updates. 29 | GenerateTests { count: usize }, 30 | /// Test `test_id` is being generated. Total test count is `count`. 31 | /// `test_id`s are in range 1..=`count`. It is gu 32 | GenerateTest { test_id: usize }, 33 | /// Valuer config is being copied 34 | CopyValuerConfig, 35 | } 36 | -------------------------------------------------------------------------------- /src/pps/api/src/import_problem.rs: -------------------------------------------------------------------------------- 1 | //! Import problem from some other format 2 | use serde::{Deserialize, Serialize}; 3 | use std::path::PathBuf; 4 | #[derive(Serialize, Deserialize)] 5 | pub struct Request { 6 | /// this path specifies file or files that should be imported 7 | pub src_path: PathBuf, 8 | /// where to put generated problem source 9 | pub out_path: PathBuf, 10 | /// do not check that dest is empty 11 | pub force: bool, 12 | } 13 | 14 | #[derive(Serialize, Deserialize)] 15 | pub enum Update { 16 | /// Contains one property of discovered problem. 17 | /// Each `property_name` will be reported at most once. 18 | Property { 19 | property_name: PropertyName, 20 | property_value: String, 21 | }, 22 | /// Contains one warnings. May appear multiple times. 23 | Warning(String), 24 | /// Started importing checker 25 | ImportChecker, 26 | /// Started importing tests 27 | ImportTests, 28 | /// Finished importing tests. `count` tests imported. 29 | ImportTestsDone { count: usize }, 30 | /// Started importing solutions 31 | ImportSolutions, 32 | /// Started importing solution with specific name 33 | ImportSolution(String), 34 | /// Valuer config is detected and will be imported 35 | ImportValuerConfig, 36 | /// Valuer config was not found, default will be used 37 | DefaultValuerConfig, 38 | } 39 | 40 | #[derive(Serialize, Deserialize)] 41 | pub enum PropertyName { 42 | /// Value is time limit in milliseconds. 43 | TimeLimit, 44 | /// Value is memory limit in milliseconds. 45 | MemoryLimit, 46 | /// Value is printf-style pattern of input files. 47 | InputPathPattern, 48 | /// Value is printf-style pattern of output files. 49 | OutputPathPattern, 50 | /// Value is problem title. 51 | ProblemTitle, 52 | } 53 | -------------------------------------------------------------------------------- /src/pps/api/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! All PPS apis. 2 | //! 3 | //! All paths are relative to workspace root. 4 | pub mod compile_problem; 5 | pub mod import_problem; 6 | 7 | use rpc::Route; 8 | use std::convert::Infallible; 9 | pub struct CompileProblem(Infallible); 10 | 11 | impl Route for CompileProblem { 12 | type Request = rpc::Unary; 13 | type Response = rpc::Streaming; 14 | 15 | const ENDPOINT: &'static str = "/problems/compile"; 16 | } 17 | 18 | pub struct ImportProblem(Infallible); 19 | 20 | impl Route for ImportProblem { 21 | type Request = rpc::Unary; 22 | type Response = rpc::Streaming; 23 | 24 | const ENDPOINT: &'static str = "/problems/import"; 25 | } 26 | 27 | /// Contains possible error os success 28 | #[derive(serde::Serialize, serde::Deserialize, Debug)] 29 | #[must_use = "this is Result in fact"] 30 | pub struct SimpleFinish(pub Result<(), StringError>); 31 | 32 | impl From> for SimpleFinish { 33 | fn from(r: anyhow::Result<()>) -> Self { 34 | Self(r.map_err(|e| StringError(format!("{:#}", e)))) 35 | } 36 | } 37 | 38 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 39 | #[serde(transparent)] 40 | pub struct StringError(pub String); 41 | 42 | impl std::fmt::Display for StringError { 43 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 44 | self.0.fmt(f) 45 | } 46 | } 47 | 48 | impl std::error::Error for StringError {} 49 | -------------------------------------------------------------------------------- /src/pps/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pps-cli" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serde = { version = "1.0.117", features = ["derive"] } 9 | serde_json = "1.0.59" 10 | anyhow = "1.0.33" 11 | tokio = { version = "0.2.22", features = ["process", "macros", "rt-threaded", "fs"] } 12 | clap = "3.0.0-beta.2" 13 | pps-api = { path = "../api" } 14 | pps-server = { path = "../server" } 15 | rand = "0.7.3" 16 | util = { path = "../../util" } 17 | tracing = "0.1.21" 18 | tracing-futures = "0.2.4" 19 | rpc = { git = "https://github.com/jjs-dev/commons" } 20 | -------------------------------------------------------------------------------- /src/pps/cli/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-slim 2 | # TODO: use rustls 3 | RUN apt-get update -y && apt-get install -y libssl-dev 4 | ENV JJS_AUTH_DATA=/auth/authdata.yaml JJS_PATH=/jtl 5 | COPY jjs-pps /jjs-pps 6 | VOLUME ["/auth"] 7 | ENTRYPOINT ["/jjs-pps"] 8 | -------------------------------------------------------------------------------- /src/pps/cli/src/client_util.rs: -------------------------------------------------------------------------------- 1 | // To start server, we need to know some free port. 2 | // Even if there is a way to get this information, it would 3 | // suffer from race conditions. 4 | // That's why, we simply select random port and try using it. 5 | // 20 iterations give negligible probality of failure. 6 | const BIND_ATTEMPTS: usize = 20; 7 | 8 | #[tracing::instrument] 9 | pub(crate) async fn create_server( 10 | cancel: tokio::sync::CancellationToken, 11 | ) -> anyhow::Result<(tokio::sync::oneshot::Receiver<()>, rpc::Client)> { 12 | // TODO provide way to customize port or port range 13 | tracing::info!("launching server"); 14 | let mut last_error = None; 15 | for _ in 0..BIND_ATTEMPTS { 16 | let port: u16 = rand::random(); 17 | match pps_server::serve(port, cancel.clone()).await { 18 | Ok(rx) => { 19 | let endpoint = format!("http://127.0.0.1:{}", port); 20 | let client = rpc::Client::new(rpc::ReqwestEngine::new(), endpoint); 21 | return Ok((rx, client)); 22 | } 23 | Err(err) => { 24 | tracing::warn!(error=?err, "bind attempt unsuccessful"); 25 | last_error = Some(err) 26 | } 27 | } 28 | } 29 | Err(last_error.expect("BIND_ATTEMPTS != 0")) 30 | } 31 | -------------------------------------------------------------------------------- /src/pps/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(is_sorted)] 2 | #![allow(clippy::needless_lifetimes)] 3 | 4 | mod client_util; 5 | mod compile; 6 | mod import; 7 | mod progress_notifier; 8 | 9 | #[derive(clap::Clap, Debug)] 10 | #[clap(author, about)] 11 | pub enum Args { 12 | Compile(compile::CompileArgs), 13 | Import(import::ImportArgs), 14 | } 15 | 16 | use anyhow::Context as _; 17 | use std::path::Path; 18 | 19 | fn check_dir(path: &Path, allow_nonempty: bool) -> anyhow::Result<()> { 20 | if !path.exists() { 21 | anyhow::bail!("error: path {} not exists", path.display()); 22 | } 23 | if !path.is_dir() { 24 | anyhow::bail!("error: path {} is not directory", path.display()); 25 | } 26 | if !allow_nonempty && path.read_dir().unwrap().next().is_some() { 27 | anyhow::bail!("error: dir {} is not empty", path.display()); 28 | } 29 | Ok(()) 30 | } 31 | 32 | #[tokio::main] 33 | async fn main() -> anyhow::Result<()> { 34 | use clap::Clap; 35 | util::log::setup(); 36 | let args = Args::parse(); 37 | tracing::info!("starting new server in background"); 38 | let cancel = tokio::sync::CancellationToken::new(); 39 | let (server_done_rx, mut client) = client_util::create_server(cancel.clone()).await?; 40 | process_args(args, &mut client) 41 | .await 42 | .context("failed to process args")?; 43 | cancel.cancel(); 44 | tracing::info!("waiting for server shutdown"); 45 | server_done_rx.await.ok(); 46 | Ok(()) 47 | } 48 | 49 | #[tracing::instrument(skip(args, client))] 50 | async fn process_args(args: Args, client: &mut rpc::Client) -> anyhow::Result<()> { 51 | tracing::info!(args=?args, "executing requested command"); 52 | match args { 53 | Args::Compile(compile_args) => compile::exec(client, compile_args).await, 54 | Args::Import(import_args) => import::exec(client, import_args).await, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/pps/cli/src/progress_notifier.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | const STEP_PERCENTAGE_THRESHOLD: usize = 20; 4 | const STEP_DURATION_THRESHOLD: Duration = Duration::from_secs(10); 5 | 6 | pub(super) struct Notifier { 7 | last_step: usize, 8 | total_step_count: usize, 9 | last_time: std::time::Instant, 10 | } 11 | 12 | impl Notifier { 13 | pub(super) fn new(cnt: usize) -> Notifier { 14 | assert_ne!(cnt, 0); 15 | Notifier { 16 | last_step: 0, 17 | total_step_count: cnt, 18 | last_time: Instant::now(), 19 | } 20 | } 21 | 22 | fn do_notify(&mut self, new_step: usize) { 23 | println!("Progress: {}/{}", new_step, self.total_step_count); 24 | self.last_step = new_step; 25 | self.last_time = Instant::now(); 26 | } 27 | 28 | pub(super) fn maybe_notify(&mut self, new_step: usize) { 29 | let mut should_notify = false; 30 | { 31 | let cnt_delta = new_step - self.last_step; 32 | if 100 * cnt_delta >= STEP_PERCENTAGE_THRESHOLD * self.total_step_count { 33 | should_notify = true; 34 | } 35 | } 36 | { 37 | let time_delta = Instant::now().duration_since(self.last_time); 38 | if time_delta >= STEP_DURATION_THRESHOLD { 39 | should_notify = true; 40 | } 41 | } 42 | if should_notify { 43 | self.do_notify(new_step); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/pps/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pps-server" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | tokio = { version = "0.2.22", features = ["macros", "tracing"] } 9 | serde = "1.0.117" 10 | serde_json = "1.0.59" 11 | anyhow = "1.0.33" 12 | toml = "0.5.7" 13 | thiserror = "1.0.21" 14 | async-trait = "0.1.41" 15 | getrandom = { version = "0.2.0", features = ["std"] } 16 | glob = "0.3.0" 17 | pom = { path = "../../pom" } 18 | libc = "0.2.80" 19 | roxmltree = "0.13.0" 20 | serde_yaml = "0.8.14" 21 | pest = "2.1.3" 22 | pest_derive = "2.1.0" 23 | formatf = { git = "https://github.com/mikailbag/formatf" } 24 | svaluer = { path = "../../svaluer" } 25 | hex = "0.4.2" 26 | pps-api = { path = "../api" } 27 | hyper = "0.13.8" 28 | tracing = "0.1.21" 29 | tracing-futures = { version = "0.2.4", features = ["futures-03"] } 30 | either = "1.6.1" 31 | rpc = { git = "https://github.com/jjs-dev/commons" } 32 | futures-util = "0.3.7" 33 | -------------------------------------------------------------------------------- /src/pps/server/src/import/checker_tpl.cmake: -------------------------------------------------------------------------------- 1 | project(Checker) 2 | cmake_minimum_required(VERSION 3.12) 3 | add_executable(Out main.cpp) 4 | target_include_directories(Out PUBLIC ../../) -------------------------------------------------------------------------------- /src/pps/server/src/import/contest_import.rs: -------------------------------------------------------------------------------- 1 | // TODO revive when contests are reintroduced 2 | use std::{borrow::Cow, path::Path}; 3 | use thiserror::Error; 4 | #[derive(Error, Debug)] 5 | pub(super) enum ImportContestError { 6 | #[error("io error: {0}")] 7 | Io(#[from] std::io::Error), 8 | #[error("syntax error: {0}")] 9 | XmlSyn(#[from] roxmltree::Error), 10 | #[error("bad context.xml: {0}")] 11 | BadManifest(Cow<'static, str>), 12 | } 13 | 14 | fn go(node: roxmltree::Node, cfg: &mut entity::Contest) -> Result<(), ImportContestError> { 15 | match node.tag_name().name() { 16 | "problem" => { 17 | let index = node.attribute("index").ok_or_else(|| { 18 | ImportContestError::BadManifest("index attribute missing in ".into()) 19 | })?; 20 | let url = node.attribute("url").ok_or_else(|| { 21 | ImportContestError::BadManifest("url attribute missing in ".into()) 22 | })?; 23 | let problem_name = url 24 | .rsplit('/') 25 | .next() 26 | .expect("rsplit() should never be empty"); 27 | let binding = entity::entities::contest::ProblemBinding { 28 | name: problem_name.to_string(), 29 | code: index.to_string(), 30 | }; 31 | cfg.problems.push(binding); 32 | Ok(()) 33 | } 34 | _ => { 35 | for child in node.children() { 36 | go(child, cfg)?; 37 | } 38 | Ok(()) 39 | } 40 | } 41 | } 42 | 43 | pub(super) fn import( 44 | path: &Path, 45 | contest_name: &str, 46 | ) -> Result { 47 | let data = std::fs::read_to_string(path)?; 48 | let doc = roxmltree::Document::parse(&data)?; 49 | let mut cfg = entity::Contest { 50 | title: "".to_string(), 51 | id: contest_name.to_string(), 52 | problems: vec![], 53 | judges: vec![], 54 | group: vec![], 55 | unregistered_visible: false, 56 | anon_visible: false, 57 | duration: None, 58 | end_time: None, 59 | start_time: None, 60 | is_virtual: false, 61 | }; 62 | go(doc.root(), &mut cfg)?; 63 | Ok(cfg) 64 | } 65 | -------------------------------------------------------------------------------- /src/pps/server/src/import/default_valuer_config.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: default 3 | score: 100 4 | feedback: brief 5 | -------------------------------------------------------------------------------- /src/pps/server/src/import/gen.cmake: -------------------------------------------------------------------------------- 1 | project(Generator) 2 | cmake_minimum_required(VERSION 3.12) 3 | 4 | find_package(Jtl CONFIG REQUIRED) 5 | 6 | add_executable(Out main.cpp) 7 | target_include_directories(Out PUBLIC ${Jtl_INCLUDES}) 8 | target_link_libraries(Out PUBLIC ${Jtl_LIBS}) -------------------------------------------------------------------------------- /src/pps/server/src/import/solution.cmake: -------------------------------------------------------------------------------- 1 | project(Solution) 2 | cmake_minimum_required(VERSION 3.12) 3 | 4 | add_executable(Out main.cpp) 5 | target_compile_options(Out PUBLIC -O2) -------------------------------------------------------------------------------- /src/pps/server/src/import/template.rs: -------------------------------------------------------------------------------- 1 | //! This module defines templates 2 | //! for CMakeLists.txt files 3 | 4 | static CHECKER_TPL: &str = include_str!("checker_tpl.cmake"); 5 | 6 | pub struct CheckerOptions {} 7 | 8 | pub fn get_checker_cmakefile(_options: CheckerOptions) -> String { 9 | CHECKER_TPL.to_string() 10 | } 11 | -------------------------------------------------------------------------------- /src/pps/server/src/import/valuer_cfg.pest: -------------------------------------------------------------------------------- 1 | config = {SOI ~ definition* ~ EOI} 2 | definition = {global_def | group_def} 3 | 4 | global_def = {"global" ~ "{" ~ (global_option ~ ";")* ~ "}"} 5 | global_option = {("stat_to_judges" | "stat_to_users") ~ num? } 6 | 7 | group_def = {"group" ~ num ~ "{" ~ (group_option ~ ";" )* ~ "}"} 8 | group_option = _{group_option_tests | group_option_score | 9 | group_option_test_score | group_option_requires | 10 | group_option_offline | group_option_sets_marked_if_passed | 11 | group_option_sets_marked | group_option_pass_if_count | 12 | group_option_skip_if_not_rejudge | group_option_skip | 13 | group_option_stat_to_users | group_option_stat_to_judges | 14 | group_option_user_status | group_option_test_all | 15 | group_option_zero_if } 16 | group_option_tests = {"tests" ~ num ~ "-" ~ num} 17 | group_option_score = {"score" ~ num} 18 | group_option_test_score = {"test_score" ~ num} 19 | group_option_requires = {"requires" ~ num_list} 20 | group_option_offline = {"offline"} 21 | group_option_sets_marked = {"sets_marked"} 22 | group_option_skip = {"skip"} 23 | group_option_sets_marked_if_passed = {"sets_marked_if_passed" ~ num_list} 24 | group_option_pass_if_count = {"pass_if_count" ~ num} 25 | group_option_skip_if_not_rejudge = {"skip_if_not_rejudge"} 26 | group_option_stat_to_users = {"stat_to_users"} 27 | group_option_stat_to_judges = {"stat_to_judges"} 28 | // TODO: fix this rule. find out what `STATUS` is. 29 | group_option_user_status = {"user_status" ~ num} 30 | group_option_test_all = {"test_all"} 31 | group_option_zero_if = {"zero_if"} 32 | 33 | num_list = _{num ~ ("," ~ num)*} 34 | num = @{"0" | (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT{0,7})} 35 | WHITESPACE = _{" " | "\n" | "\t"} 36 | COMMENT = _{"#" ~ (!"\n" ~ ANY)*} 37 | -------------------------------------------------------------------------------- /src/problem-loader/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "problem-loader" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | 8 | [dependencies] 9 | pom = {path = "../pom"} 10 | anyhow = "1.0.33" 11 | serde_json = "1.0.59" 12 | async-trait = "0.1.41" 13 | tokio = { version = "0.2.22", features = ["fs"] } 14 | fs_extra = "1.2.0" 15 | mongodb = "1.1.1" 16 | url = "2.1.1" 17 | bson = "1.1.0" 18 | flate2 = "1.0.18" 19 | tar = "0.4.30" 20 | serde = "1.0.117" 21 | tracing = "0.1.21" 22 | tracing-futures = "0.2.4" 23 | -------------------------------------------------------------------------------- /src/ranker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ranker" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serde = { version = "1.0.106", features = ["derive"] } 9 | 10 | [dev-dependencies] 11 | pretty_assertions = "0.6.1" 12 | maplit = "1.0.2" 13 | -------------------------------------------------------------------------------- /src/setup/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "setup" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | description = """ 7 | Simple utility to setup JJS on current host 8 | """ 9 | 10 | [dependencies] 11 | backtrace = "0.3.46" 12 | fs_extra = "1.1.0" 13 | structopt = "0.3.13" 14 | util = {path = "../util"} 15 | log = "0.4.8" 16 | url = "2.1.1" 17 | anyhow = "1.0.28" 18 | tokio-postgres = "0.5.3" 19 | tokio = { version = "0.2.18", features = ["macros", "fs", "process", "io-std"] } 20 | thiserror = "1.0.15" 21 | async-trait = "0.1.30" 22 | serde = { version = "1.0.106", features = ["derive"] } 23 | serde_yaml = "0.8.11" 24 | serde_json = "1.0.51" 25 | toml = "0.5.6" 26 | futures = "0.3.4" 27 | flate2 = {version = "1.0.14", features = ["tokio"]} 28 | tar = "0.4.26" 29 | -------------------------------------------------------------------------------- /src/setup/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(backtrace, or_patterns)] 2 | // for async-trait 3 | #![allow(clippy::needless_lifetimes)] 4 | pub mod certs; 5 | pub mod config; 6 | pub mod data; 7 | pub mod db; 8 | pub mod problems; 9 | pub mod toolchains; 10 | 11 | use async_trait::async_trait; 12 | 13 | #[async_trait] 14 | pub trait Component: std::fmt::Display { 15 | type Error: std::error::Error + Send + Sync; 16 | async fn state(&self) -> Result; 17 | async fn upgrade(&self) -> Result<(), Self::Error>; 18 | fn name(&self) -> &'static str; 19 | } 20 | 21 | #[derive(Debug)] 22 | pub enum StateKind { 23 | UpToDate, 24 | Upgradable, 25 | Errored, 26 | } 27 | 28 | impl std::fmt::Display for StateKind { 29 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 30 | std::fmt::Debug::fmt(self, f) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/svaluer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "svaluer" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | invoker-api = {path = "../invoker-api"} 9 | anyhow = "1.0.33" 10 | pom = {path = "../pom"} 11 | crossbeam-channel = "0.5.0" 12 | serde_json = "1.0.59" 13 | serde = "1.0.117" 14 | util = {path = "../util"} 15 | serde_yaml = "0.8.14" 16 | log = "0.4.11" 17 | either = "1.6.1" 18 | 19 | [dev-dependencies] 20 | simple_logger = "1.11.0" 21 | -------------------------------------------------------------------------------- /src/svaluer/Dockerfile: -------------------------------------------------------------------------------- 1 | # TODO use scratch ? 2 | FROM debian:stable-slim 3 | COPY jjs-svaluer /bin/jjs-svaluer 4 | ENTRYPOINT ["/jjs-svaluer"] -------------------------------------------------------------------------------- /src/toolkit/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-slim 2 | WORKDIR /opt/jjs 3 | ENV JJS_AUTH_DATA=/auth/authdata.yaml JJS_PATH=/opt/jjs PATH=/opt/jjs/bin:${PATH} \ 4 | LIBRARY_PATH=/opt/jjs/lib:${LIBRARY_PATH} \ 5 | CPLUS_INCLUDE_PATH=/opt/jjs/include:{$CPLUS_INCLUDE_PATH} \ 6 | CMAKE_PREFIX_PATH=/opt/jjs/share/cmake:${CMAKE_PREFIX_PATH} 7 | RUN apt-get update && apt-get install -y libssl-dev 8 | COPY /jtl . 9 | COPY /pps-cli ./bin/jjs-ppc 10 | COPY /cli ./bin/jjs-cli 11 | COPY /svaluer ./bin/jjs-svaluer 12 | VOLUME ["/auth"] 13 | -------------------------------------------------------------------------------- /src/userlist/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "userlist" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | client = {path = "../client"} 9 | futures = { version = "0.3.4", features = ["compat"] } 10 | structopt = "0.3.13" 11 | base64 = "0.12.0" 12 | pest = "2.1.3" 13 | pest_derive = "2.1.0" 14 | serde = { version = "1.0.106", features = ["derive"] } 15 | tokio = { version = "0.2.18", features = ["macros", "rt-core"] } 16 | thiserror = "1.0.15" 17 | -------------------------------------------------------------------------------- /src/userlist/src/create_user.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateUser($login: String!, $password: String!, $groups: [String!]!) { 2 | createUser(login: $login, password: $password, groups: $groups) { 3 | id 4 | } 5 | } -------------------------------------------------------------------------------- /src/userlist/src/gram.pest: -------------------------------------------------------------------------------- 1 | userlist = { SOI ~ statement* ~ EOI } 2 | statement = {(statement_adduser | statement_setopts) ~ "\n"?} 3 | 4 | statement_setopts = { 5 | ("." | "cfg") 6 | ~ options 7 | } 8 | 9 | statement_adduser = { 10 | ("+" | "add") 11 | ~ string /// username 12 | ~ string /// password 13 | ~ options? 14 | } 15 | 16 | // few more types 17 | options = ${option ~ ("," ~ option)*} 18 | flag = @{string} 19 | setting = ${string ~ "=" ~ string} 20 | option = ${setting | flag} 21 | 22 | // terminal rules 23 | string_char_head = @{'a'..'z' | 'A'..'Z' | "+" | "/" | "_" | ":"} 24 | string_char_any = @{string_char_head | '0'..'9'} 25 | string = @{string_char_head ~ string_char_any*} 26 | num = @{'1'..'9' ~ ('0'..'9')*} 27 | WHITESPACE = _{" " | "\t"} 28 | // TODO: 29 | // COMMENT=... -------------------------------------------------------------------------------- /src/util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "util" 3 | version = "0.1.0" 4 | authors = ["Mikail Bagishov "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | atty = "0.2.14" 9 | anyhow = "1.0.33" 10 | tracing-subscriber = "0.2.14" 11 | tracing = "0.1.21" 12 | -------------------------------------------------------------------------------- /src/util/src/cfg.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Debug)] 5 | pub struct CfgData { 6 | pub data_dir: PathBuf, 7 | } 8 | 9 | fn find_data_dir() -> anyhow::Result { 10 | match std::env::var_os("JJS_DATA") { 11 | Some(dir) => Ok(PathBuf::from(dir)), 12 | None => Err(anyhow::anyhow!("JJS_DATA env var is missing")), 13 | } 14 | } 15 | 16 | pub fn load_cfg_data() -> anyhow::Result { 17 | let data_dir = find_data_dir().context("failed to find data dir")?; 18 | Ok(CfgData { data_dir }) 19 | } 20 | -------------------------------------------------------------------------------- /src/util/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cfg; 2 | pub mod cmd; 3 | pub mod log; 4 | 5 | pub fn print_error(err: &dyn std::error::Error) { 6 | eprintln!("error: {}", err); 7 | let mut iter = err.source(); 8 | while let Some(cause) = iter { 9 | eprintln!("caused by: {}", cause); 10 | iter = cause.source(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/util/src/log.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Once; 2 | 3 | pub fn setup() { 4 | static ONCE: Once = Once::new(); 5 | ONCE.call_once(|| { 6 | tracing_subscriber::fmt() 7 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 8 | .with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE) 9 | .with_ansi(false) 10 | // TODO allow customization 11 | .without_time() 12 | .with_writer(std::io::stderr) 13 | .init(); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /systemd/jjs-apiserver.service.tera: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=JJS API server 3 | After=postgresql.service 4 | 5 | [Service] 6 | Type=notify 7 | Environment=JJS_DATA=/home/jjs JJS_SD_NOTIFY=1 8 | EnvironmentFile=/home/jjs/etc/env.txt 9 | User=jjs 10 | Group=jjs 11 | ExecStart={{ jjs_sysroot }}/bin/jjs-apiserver 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /systemd/jjs-invoker.service.tera: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=JJS invoker 3 | After=postgresql.service 4 | 5 | [Service] 6 | Type=notify 7 | Environment=JJS_DATA=/home/jjs JJS_SD_NOTIFY=1 8 | EnvironmentFile=/home/jjs/etc/env.txt 9 | User=root 10 | Group=root 11 | ExecStart={{ jjs_sysroot }}/bin/jjs-invoker 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /vm-sysroot/README.txt: -------------------------------------------------------------------------------- 1 | This is a tool for building jjs into a sysroot (e.g. as a VM disk image). 2 | All scripts must be run from this directory. 3 | 4 | Note: build JJS from the ../target (relative to vm-sysroot) directory before running any of these scripts! 5 | 6 | Executable scripts: 7 | 8 | ./build.sh [sysroot_path] 9 | Build a jjs sysroot at $sysroot_path (default = ./sysroot) 10 | This script assumes that you have working sudo command. Don't run directly as root! 11 | Note: PostgreSQL MUST be stopped for this to succeed! 12 | 13 | sudo image/build-image.sh 14 | Build the disk image image/hdd.img, using sysroot in ./sysroot. Uses UML to isolate itself. 15 | The resulting image is a single partition without any bootloader/kernel/whatsoever. 16 | 17 | ./uml-build.sh 18 | Executes the two previous scripts, using UML to simulate root access. Doesn't require to be launched as root. 19 | 20 | ./netns-build.sh 21 | Executes ./build.sh in a separate network namespace, to prevent conflicting with the local PostgreSQL instance. 22 | 23 | Other files: 24 | 25 | scripts/sysroot/*.sh 26 | These scripts are executed by ./build.sh and should output a newline-separated list of host files to be included. 27 | 28 | scripts/post-sysroot/*.sh 29 | These scripts are executed by ./build.sh after the core sysroot has been built to make some finishing touches. 30 | 31 | etc-network-interfaces.conf 32 | This file will be placed at /etc/network/interfaces inside the sysroot. Modify to match your network configuration. 33 | 34 | problems/*/ 35 | This directory (if exists) should contain source problems. Problems will get compiled and bundled into the image. 36 | -------------------------------------------------------------------------------- /vm-sysroot/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "x$2" != x ] || [ "x$1" == "x--help" ] 4 | then cat >&2 << EOF 5 | usage: $0 [sysroot_path] 6 | 7 | Build a jjs sysroot at \$sysroot_path (default = ./sysroot) 8 | This script assumes that you have working sudo command. Don't run directly as root! 9 | EOF 10 | exit 1 11 | fi 12 | 13 | if [ ! -f ../target/jjs.tgz ] 14 | then cat >&2 <&1 42 | sudo mkdir "$SYSROOT" || exit 1 43 | 44 | for i in scripts/sysroot/* 45 | do bash -x "$i" 46 | done | sort | uniq | tee /dev/stderr | while read -r path 47 | do 48 | sudo mkdir -p "$SYSROOT/$path" 49 | if ! sudo test -d "$path" 50 | then 51 | sudo rmdir "$SYSROOT/$path" 52 | sudo cp "$path" "$SYSROOT/$path" 53 | sudo chown "$(whoami):$(whoami)" "$SYSROOT/$path" 54 | fi 55 | done 56 | 57 | for i in scripts/post-sysroot/* 58 | do bash -x "$i" 59 | done 60 | -------------------------------------------------------------------------------- /vm-sysroot/env.txt: -------------------------------------------------------------------------------- 1 | export JJS_DATA=/var/lib/jjs 2 | export JJS_PATH=/usr 3 | export DATABASE_URL=postgres://jjs:internal@localhost:5432/jjs 4 | export RUST_BACKTRACE=1 5 | export JJS_HOST=0.0.0.0 6 | export JJS_SELF_ADDR=127.0.0.1 7 | export JJS_ENV=dev 8 | export RUST_LOG=debug,hyper=info,tokio_postgres=info 9 | #export JJS_SECRET_KEY= 10 | -------------------------------------------------------------------------------- /vm-sysroot/etc-network-interfaces.conf: -------------------------------------------------------------------------------- 1 | auto lo 2 | iface lo inet loopback 3 | 4 | auto eth0 5 | iface eth0 inet dhcp 6 | -------------------------------------------------------------------------------- /vm-sysroot/image/build-full.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | usage () 6 | { 7 | cat >&2 << EOF 8 | usage: $0 [--out ] [--sysroot ] 9 | 10 | Build the disk image \$out_path (default: image/full.img), using sysroot in \$sysroot_path (default: ./sysroot). 11 | Unlike image/build-image.sh, the resulting image is a ready-to-boot raw disk image. 12 | This script must be run as root in order to function properly. 13 | EOF 14 | exit 1 15 | } 16 | 17 | dir="$(dirname "$0")" 18 | out="$dir/full.img" 19 | sysroot="$dir/../sysroot" 20 | 21 | while [ "x$1" != x ] 22 | do 23 | if [ "x$1" == x--out ] 24 | then 25 | out="$2" 26 | shift; shift 27 | elif [ "x$1" == x--sysroot ] 28 | then 29 | sysroot="$2" 30 | shift; shift 31 | else 32 | usage 33 | fi 34 | done 35 | 36 | hdd_img="$(mktemp)" 37 | 38 | abspath () 39 | { 40 | local x="$1" 41 | if [ "${x:0:1}" == "/" ] 42 | then echo "$x" 43 | else echo "$(pwd)/$x" 44 | fi 45 | } 46 | 47 | dir="$(abspath "$dir")" 48 | out="$(abspath "$out")" 49 | sysroot="$(abspath "$sysroot")" 50 | 51 | bash "$dir/build-image.sh" --out "$hdd_img" --sysroot "$sysroot" 52 | 53 | dd if=/dev/null of="$out" 54 | dd if=/dev/null of="$out" bs=1048576 seek=1025 55 | fdisk "$out" << EOF 56 | n 57 | p 58 | 1 59 | 60 | 61 | w 62 | EOF 63 | dd if=/dev/null of="$out" bs=1048576 seek=1 64 | 65 | tmp_dir="$(mktemp -d)" 66 | ( 67 | cd "$tmp_dir" 68 | mkdir tmpfs #this is intentional, don't rename 69 | mount -t tmpfs tmpfs tmpfs 70 | cp /usr/lib/grub/i386-pc/boot.img tmpfs/ 71 | grub-mkimage -O i386-pc -o tmpfs/core.img -p '(hd0,1)/boot/grub' biosdisk part_msdos ext2 linux normal 72 | echo "(hd0) $out" > tmpfs/dmap.txt 73 | grub-bios-setup --device-map tmpfs/dmap.txt -d tmpfs -s "$out" 74 | umount tmpfs 75 | rmdir tmpfs 76 | cat "$hdd_img" >> "$out" 77 | ) 78 | rmdir "$tmp_dir" 79 | -------------------------------------------------------------------------------- /vm-sysroot/image/build-image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | usage () 6 | { 7 | cat >&2 << EOF 8 | usage: $0 [--out ] [--sysroot ] 9 | 10 | Build the disk image \$output_path (default: image/hdd.img), using sysroot in \$sysroot_path (default: ./sysroot). 11 | The resulting image is a single partition without any bootloader/kernel/whatsoever. 12 | This script must be run as root in order to function properly. 13 | EOF 14 | exit 1 15 | } 16 | 17 | dir="$(dirname "$0")" 18 | out="$dir/hdd.img" 19 | sysroot="$dir/../sysroot" 20 | 21 | while [ "x$1" != x ] 22 | do 23 | if [ "x$1" == x--out ] 24 | then 25 | out="$2" 26 | shift; shift 27 | elif [ "x$1" == x--sysroot ] 28 | then 29 | sysroot="$2" 30 | shift; shift 31 | else 32 | usage 33 | fi 34 | done 35 | 36 | dd if=/dev/null of="$out" 37 | dd if=/dev/null of="$out" bs=1048576 seek=1024 38 | mke2fs -d "$sysroot" "$out" 39 | 40 | -------------------------------------------------------------------------------- /vm-sysroot/netns-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | env | sed "s/'/'\"'\"'/g" | sed "s/=/='/" | sed "s/$/'/g" | sed 's/^/export /g' > netns-env.txt 4 | sudo ip netns add jjs-build-netns 5 | sudo ip netns exec jjs-build-netns sudo -u "$(whoami)" bash -c '. netns-env.txt; rm netns-env.txt; ./build.sh "$0"' "$1" 6 | -------------------------------------------------------------------------------- /vm-sysroot/problems/a-plus-b: -------------------------------------------------------------------------------- 1 | ../../example-problems/a-plus-b -------------------------------------------------------------------------------- /vm-sysroot/problems/array-sum: -------------------------------------------------------------------------------- 1 | ../../example-problems/array-sum -------------------------------------------------------------------------------- /vm-sysroot/problems/sqrt: -------------------------------------------------------------------------------- 1 | ../../example-problems/sqrt/ -------------------------------------------------------------------------------- /vm-sysroot/scripts/post-sysroot/boot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | kernel="$(find /boot/vmlinuz-* | sort -V | tail -1)" 4 | kernel="${kernel#/boot/vmlinuz-}" 5 | 6 | sudo mkdir "$SYSROOT/boot" 7 | sudo mkdir "$SYSROOT/boot/tmp" 8 | 9 | ( 10 | cd "$SYSROOT/boot" || exit 1 11 | sudo KERNEL="$kernel" unshare -m bash -c ' 12 | mount --bind tmp /etc/initramfs-tools/scripts 13 | mount --bind /dev/null /etc/crypttab 14 | mount --bind /dev/null /etc/fstab 15 | update-initramfs -b . -c -k "$KERNEL" 16 | ' 17 | ) 18 | 19 | sudo rmdir "$SYSROOT/boot/tmp" 20 | sudo mv "$SYSROOT/boot/initrd.img-$kernel" "$SYSROOT/boot/initrd.img" 21 | sudo cp "/boot/vmlinuz-$kernel" "$SYSROOT/boot/vmlinuz" 22 | 23 | sudo mkdir "$SYSROOT/boot/grub" 24 | sudo tee "$SYSROOT/boot/grub/grub.cfg" << EOF 25 | linux /boot/vmlinuz root=/dev/sda1 26 | initrd /boot/initrd.img 27 | boot 28 | EOF 29 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/post-sysroot/busybox.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo mkdir -p "$SYSROOT/bin" 4 | sudo cp /bin/busybox "$SYSROOT/bin" 5 | busybox --list-full | while read -r applet 6 | do 7 | if [ ! -e "$SYSROOT/$applet" ] 8 | then 9 | sudo mkdir -p "$SYSROOT/$applet" 10 | sudo rmdir "$SYSROOT/$applet" 11 | sudo ln -s /bin/busybox "$SYSROOT/$applet" 12 | fi 13 | done 14 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/post-sysroot/ect-network-interfaces.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo mkdir -p "$SYSROOT/etc/network" 4 | sudo cp etc-network-interfaces.conf "$SYSROOT/etc/network/interfaces" 5 | sudo chown root:root "$SYSROOT/etc/network/interfaces" 6 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/post-sysroot/hosts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo tee "$SYSROOT/etc/hosts" >/dev/null << EOF 4 | 127.0.0.1 localhost 5 | EOF 6 | sudo touch "$SYSROOT/etc/resolv.conf" 7 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/post-sysroot/init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo mkdir "$SYSROOT"/{dev,proc,sys} 4 | 5 | sudo tee "$SYSROOT/init" >/dev/null << EOF 6 | #!/bin/sh 7 | 8 | mount -t proc proc /proc 9 | mount -t sysfs sysfs /sys 10 | mount -t devpts /dev/pts 11 | mkdir -p /dev/shm 12 | chmod 1777 /dev/shm 13 | 14 | mount -t tmpfs tmpfs /tmp 15 | chmod 1777 /tmp 16 | 17 | mount -t tmpfs tmpfs /sys/fs/cgroup 18 | for i in cpuacct pids memory 19 | do 20 | mkdir /sys/fs/cgroup/\$i 21 | mount -t cgroup -o nosuid,nodev,noexec,\$i cgroup /sys/fs/cgroup/\$i 22 | done 23 | echo 1 > /sys/fs/cgroup/memory/memory.use_hierarchy 24 | 25 | mount -o remount,rw / 26 | 27 | if [ "x\$(readlink /proc/1/cwd)" == x/ ] 28 | then 29 | ifdown lo 30 | ifup lo 31 | fi 32 | 33 | haveged -F & 34 | 35 | su postgres -c 'postgres -D /var/lib/postgresql/*/main & while ! psql -c ""; do true; done' 36 | sleep 5 37 | 38 | echo "We are: \$(id)" 39 | 40 | su jjs -c ' 41 | $(cat env.txt) 42 | 43 | if [ "x\$JJS_ENV" == xdev ] 44 | then echo "WARNING: jjs-apiserver is running in development mode. To switch to production mode, run \\\`jjs-prod\\\`." >&2 45 | fi 46 | 47 | jjs-apiserver & 48 | ' 49 | 50 | $(cat env.txt) 51 | 52 | if [ "x\$JJS_ENV" == xdev ] 53 | then echo "WARNING: jjs-apiserver is running in development mode. To switch to production mode, run \\\`jjs-prod\\\`." >&2 54 | fi 55 | 56 | jjs-invoker & 57 | 58 | if [ "x\$(readlink /proc/1/cwd)" == x/ ] 59 | then 60 | grep '^auto [a-z0-9]*\$' /etc/network/interfaces | cut -d ' ' -f 2-2 | while read iface 61 | do 62 | if [ "x\$iface" != xlo ] 63 | then 64 | ifdown "\$iface" 65 | ifup "\$iface" 66 | fi 67 | done 68 | fi 69 | 70 | if [ "\$\$" == 1 ] 71 | then 72 | sh 73 | killall jjs-apiserver 74 | killall jjs-invoker 75 | killall -INT postgres 76 | while killall -0 postgres 77 | do true 78 | done 79 | mount -o remount,sync / 80 | mount -o remount,ro / 81 | sync 82 | poweroff -f 83 | fi 84 | EOF 85 | sudo chmod +x "$SYSROOT/init" 86 | sudo mkdir -p "$SYSROOT/etc/init.d" 87 | sudo ln -s /init "$SYSROOT/etc/init.d/rcS" 88 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/post-sysroot/jjs-prod.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo mkdir -p "$SYSROOT/usr/bin" 4 | sudo tee "$SYSROOT/usr/bin/jjs-prod" >/dev/null << EOF 5 | #!/bin/sh 6 | 7 | if ! grep -q '^export JJS_ENV=dev$' /init 8 | then if ! grep -q '^export JJS_ENV=prod$' /init 9 | then 10 | echo '/init has been modified, exiting' 11 | exit 1 12 | fi 13 | echo 'Already running in production mode.' 14 | exit 0 15 | fi 16 | echo 'Generating JJS_SECRET_KEY...' 17 | sed -i 's/^#export JJS_SECRET_KEY=$/export JJS_SECRET_KEY='"\$(dd if=/dev/random bs=64 count=1 | xxd -p -c 64)"'/g' /init 18 | echo 'Setting JJS_ENV=prod' 19 | sed -i 's/^export JJS_ENV=dev$/export JJS_ENV=prod/g' /init 20 | echo 'Done setting up. Reboot or restart JJS manually to apply changes.' 21 | EOF 22 | sudo chmod +x "$SYSROOT/usr/bin/jjs-prod" 23 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/post-sysroot/jjs-sysroot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ORIG_CWD="$(pwd)" 4 | 5 | sudo mkdir -p "$SYSROOT/var/lib/jjs" 6 | sudo chown "$(whoami):$(whoami)" "$SYSROOT/var/lib/jjs" 7 | 8 | cargo run --offline -p setup -- - upgrade << EOF 9 | data-dir: $SYSROOT/var/lib/jjs 10 | install-dir: ../pkg/ar_data 11 | EOF 12 | 13 | sudo mkdir "$SYSROOT/var/lib/jjs/var/problems" 14 | # shellcheck disable=SC2012 15 | if [ -d "$ORIG_CWD/problems" ] && ! ls "$ORIG_CWD/problems" | cmp - /dev/null 2>/dev/null 16 | then for i in "$ORIG_CWD"/problems/* 17 | do 18 | out="$SYSROOT/var/lib/jjs/var/problems/$(basename "$i")" 19 | mkdir "$out" 20 | CMAKE_PREFIX_PATH="$ORIG_CWD/../pkg/ar_data/share/cmake" CPLUS_INCLUDE_PATH="$ORIG_CWD/../pkg/ar_data/include" LIBRARY_PATH="$ORIG_CWD/../pkg/ar_data/lib" JJS_PATH="$ORIG_CWD/../pkg/ar_data" cargo run --offline -p ppc -- compile --pkg "$i" --out "$out" 21 | out= 22 | done 23 | fi 24 | 25 | sudo rm -rf "$SYSROOT/var/lib/jjs/opt" 26 | #rm -rf tmp 27 | ( cd ../toolchains && JJS_PATH="$PWD/../pkg/ar_data" cargo run --offline -p configure-toolchains -- "$(pwd)/../toolchains" "$SYSROOT/var/lib/jjs" --toolchains *; ) 28 | echo 'sandbox:x:179:179:sandbox:/:/bin/sh' > "$SYSROOT/var/lib/jjs/opt/etc/passwd" 29 | echo 'sandbox:x:179:' > "$SYSROOT/var/lib/jjs/opt/etc/group" 30 | #sudo mv tmp "$SYSROOT/var/lib/jjs/opt" 31 | 32 | cat > "$SYSROOT/var/lib/jjs/etc/apiserver.yaml" << EOF 33 | listen: 34 | host: 0.0.0.0 35 | port: 1779 36 | external-addr: 127.0.0.1 37 | EOF 38 | 39 | sudo chown -R 1:1 "$SYSROOT"/var/lib/jjs/* 40 | sudo chown root:root "$SYSROOT/var/lib/jjs" 41 | sudo chmod -R 0700 "$SYSROOT"/var/lib/jjs/* 42 | sudo chmod 0755 "$SYSROOT"/var/lib/jjs/var{,/submissions} 43 | sudo chown -R root:root "$SYSROOT/var/lib/jjs/opt" 44 | sudo chmod -R 755 "$SYSROOT/var/lib/jjs/opt" 45 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/post-sysroot/jjs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo mkdir -p "$SYSROOT"/usr/{bin,lib} 4 | sudo cp ../pkg/ar_data/bin/* "$SYSROOT/usr/bin" 5 | sudo cp ../pkg/ar_data/lib/* "$SYSROOT/usr/lib" 6 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/post-sysroot/ld-linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ldlinux="$(ldd /bin/bash | tail -1 | sed 's/^\s*//g' | sed 's/ (0x[0-9a-f]*)$//g')" 4 | sudo mkdir -p "$SYSROOT/$ldlinux" 5 | sudo rmdir "$SYSROOT/$ldlinux" 6 | sudo cp "$ldlinux" "$SYSROOT/$ldlinux" 7 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/post-sysroot/passwd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo mkdir -p "$SYSROOT/etc" 4 | sudo tee "$SYSROOT/etc/passwd" >/dev/null << EOF 5 | root:x:0:0:root:/:/bin/sh 6 | jjs:x:1:1:jjs:/:/bin/sh 7 | postgres:x:2:2:postgres:/:/bin/sh 8 | EOF 9 | sudo tee "$SYSROOT/etc/group" >/dev/null << EOF 10 | root:x:0: 11 | jjs:x:1: 12 | postgres:x:2: 13 | EOF 14 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/post-sysroot/postgres.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | firstof () 4 | { 5 | echo -n "$1" 6 | } 7 | 8 | sudo mkdir -p "$SYSROOT/usr/bin" 9 | sudo tee "$SYSROOT/usr/bin/postgres" >/dev/null << EOF 10 | #!/bin/sh 11 | 12 | exec /usr/lib/postgresql/*/bin/postgres "\$@" 13 | EOF 14 | sudo tee "$SYSROOT/usr/bin/psql" >/dev/null << EOF 15 | #!/bin/sh 16 | 17 | exec /usr/lib/postgresql/*/bin/psql "\$@" 18 | EOF 19 | sudo chmod +x "$SYSROOT"/usr/bin/{postgres,psql} 20 | 21 | sudo rm -rf "$SYSROOT"/var/lib/postgresql/*/main/* 22 | sudo chown "$(whoami):$(whoami)" "$SYSROOT"/var/lib/postgresql/*/main 23 | "$(firstof /usr/lib/postgresql/*/bin/initdb)" -U postgres "$SYSROOT"/var/lib/postgresql/*/main 24 | sudo sed -i 's/.*timezone.*//g' "$SYSROOT"/var/lib/postgresql/*/main/postgresql.conf 25 | 26 | sudo rm -rf tmp 27 | mkdir tmp 28 | "$(firstof /usr/lib/postgresql/*/bin/postgres)" -D "$SYSROOT"/var/lib/postgresql/*/main -k "$(pwd)/tmp" & 29 | sleep 15 30 | psql -h "$(pwd)/tmp" -U postgres -c "create role jjs with password 'internal';" 31 | psql -h "$(pwd)/tmp" -U postgres -c "alter role jjs with login;" 32 | psql -h "$(pwd)/tmp" -U postgres -c "create database jjs;" 33 | psql -h "$(pwd)/tmp" -U postgres -d jjs -c "grant all on all tables in schema public to jjs;" 34 | psql -h "$(pwd)/tmp" -U postgres -d jjs -c "grant all on all sequences in schema public to jjs;" 35 | echo "postgresql://jjs:internal@$(echo -n "$(pwd)" | xxd -p | sed 's/\(..\)/%\1/g')%2ftmp/jjs" 36 | cargo run -p setup -- - upgrade << EOF 37 | install-dir: ../pkg/ar_data 38 | pg: 39 | db-name: jjs 40 | conn-string: "postgresql://jjs:internal@$(echo -n "$(pwd)" | xxd -p | tr '\n' ' ' | sed 's/ //g' | sed 's/\(..\)/%\1/g')%2ftmp/jjs" 41 | EOF 42 | kill %1 43 | sleep 1 44 | sudo rm -rf tmp 45 | 46 | sudo chown -R 2:2 "$SYSROOT/var/lib/postgresql" 47 | sudo chmod -R 0700 "$SYSROOT/var/lib/postgresql" 48 | sudo rm -rf "$SYSROOT/var/run/postgresql" 49 | sudo mkdir -p "$SYSROOT/var/run/postgresql" 50 | sudo chown 2:2 "$SYSROOT/var/run/postgresql" 51 | sudo chmod 0700 "$SYSROOT/var/run/postgresql" 52 | 53 | sudo fuser -k "$SYSROOT"/usr/lib/postgresql/*/bin/postgres 54 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/sysroot/debug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo /usr/bin/strace 3 | echo /usr/bin/script 4 | echo /lib/x86_64-linux-gnu/libutil.so.1 5 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/sysroot/haveged.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | { strace -f -o /dev/fd/3 "$(sudo which haveged)" --help >&2; } 3>&1 | python3 ../scripts/strace-parser.py | python3 strace/soft.py 4 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/sysroot/jjs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd ../pkg/ar_data/bin || exit 1 3 | ldd ./* | grep ' => ' | sed 's/^.* => //g' | sed 's/ (0x[0-9a-f]*)$//g' 4 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/sysroot/localhost.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | { strace -f -o /dev/fd/3 busybox ping localhost >&2; } 3>&1 | python3 ../scripts/strace-parser.py | python3 strace/soft.py 4 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/sysroot/postgres.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | firstof () 4 | { 5 | echo -n "$1" 6 | } 7 | 8 | rm -rf tmp 9 | mkdir tmp 10 | "$(firstof /usr/lib/postgresql/*/bin/initdb)" tmp >&2 11 | # shellcheck disable=SC2016 12 | { strace -f -o /dev/fd/3 busybox sh -c "$(firstof /usr/lib/postgresql/*/bin/postgres)"' -D "$(pwd)/tmp" -k "$(pwd)/tmp" & sleep 3; psql -h "$(pwd)/tmp" -c ""; kill %1' >&2; } 3>&1 | python3 ../scripts/strace-parser.py | python3 strace/soft.py 13 | rm -rf tmp 14 | find /usr/lib/postgresql 15 | firstof /var/lib/postgresql/*/main 16 | echo /var/run/postgresql 17 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/sysroot/strace.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | { strace -f -o /dev/fd/3 strace >&2; } 3>&1 | python3 ../scripts/strace-parser.py | python3 strace/soft.py 4 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/sysroot/tmp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo /tmp 4 | -------------------------------------------------------------------------------- /vm-sysroot/scripts/sysroot/utc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo /usr/share/zoneinfo/UTC 3 | -------------------------------------------------------------------------------- /vm-sysroot/strace/soft.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | 5 | ans = set() 6 | 7 | while True: 8 | try: 9 | l = json.loads(input()) 10 | except EOFError: 11 | break 12 | if l['syscall'] in ('open', 'execve'): 13 | path = l['args'][0]['str_params'][0] 14 | elif l['syscall'] in ('openat',): 15 | path = l['args'][1]['str_params'][0] 16 | else: 17 | continue 18 | if path.startswith('/') and all(not path.startswith(i) for i in ('/dev', '/proc', '/sys')): 19 | ans.add(path) 20 | 21 | for i in ans: 22 | print(i) 23 | -------------------------------------------------------------------------------- /vm-sysroot/uml-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ "$$" != 1 ] && [ "x$1" != x ] 6 | then cat >&2 << EOF 7 | usage: $0 8 | 9 | Build a disk image at image/hdd.img, using scripts in the scripts/ directory. 10 | See ./build.sh and image/build-image.sh for details. 11 | This script doesn't require root access. 12 | EOF 13 | exit 1 14 | fi 15 | 16 | SELF="$0" 17 | 18 | if [ "${SELF:0:1}" != / ] 19 | then SELF="$(pwd)/$SELF" 20 | fi 21 | 22 | if [ "$$" != 1 ] 23 | then 24 | env | sed "s/'/'\"'\"'/g" | sed "s/=/='/" | sed "s/$/'/g" | sed 's/^/export /g' > uml-env.txt 25 | dd if=/dev/urandom of=random.bin bs=1024 count=1 26 | exec linux.uml mem=4096M root=/dev/root rw rootflags=/ rootfstype=hostfs init="$SELF" whoami="$(whoami)" 27 | fi 28 | 29 | mount -t proc proc /proc 30 | ln -s /proc/self/fd /dev/fd 31 | ln -s /proc/self/fd/0 /dev/stdin 32 | ln -s /proc/self/fd/1 /dev/stdout 33 | ln -s /proc/self/fd/2 /dev/stderr 34 | mkdir -p /dev/pts 35 | mount -t devpts devpts /dev/pts 36 | mount -t tmpfs -o mode=777 tmpfs /tmp 37 | mkdir -p /dev/shm 38 | chmod 777 /dev/shm 39 | hostname -F /etc/hostname 40 | 41 | cd "$(dirname "$0")" 42 | 43 | dd if=random.bin of=/dev/urandom 44 | rm random.bin 45 | 46 | WHOAMI="$(sed 's/.* whoami=//' /proc/cmdline | cut -d ' ' -f 1-1)" 47 | 48 | cat > /dev/sudoers << EOF 49 | Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 50 | 51 | $WHOAMI ALL=(ALL:ALL) NOPASSWD:ALL 52 | EOF 53 | 54 | mount --bind /dev/sudoers /etc/sudoers 55 | 56 | mkdir /dev/sysroot 57 | chown "$WHOAMI:$WHOAMI" /dev/sysroot 58 | 59 | su "$WHOAMI" -c 'script -c '"'"'. uml-env.txt; rm uml-env.txt; bash ./build.sh /dev/sysroot/sysroot'"'" 60 | 61 | rm -rf sysroot || true 62 | ln -s /dev/sysroot/sysroot sysroot 63 | 64 | umount /etc/sudoers 65 | umount -l /dev/pts 66 | rm /dev/fd /dev/std{in,out,err} 67 | umount -l /proc 68 | umount -l /tmp 69 | exec image/build-image.sh 70 | --------------------------------------------------------------------------------