From 3f2f448c03de7189e9a1f62498e22e94d5ea8c58 Mon Sep 17 00:00:00 2001 From: Lilac Date: Fri, 23 Oct 2026 18:16:37 -0400 Subject: [PATCH] tidied --- flake.nix | 21 ++++--- init.sh | 33 +++++----- pigeon.sh | 175 +++++++++++++++++++++++++----------------------------- 3 files changed, 109 insertions(+), 120 deletions(-) diff --git a/flake.nix b/flake.nix index 36842a0..3acc3ab 100644 --- a/flake.nix +++ b/flake.nix @@ -77,9 +77,9 @@ pigeon = pkgs.writeShellApplication { name = "pigeon"; - runtimeInputs = with pkgs; [ coreutils curl e2fsprogs firecracker gcc git gnumake ]; + runtimeInputs = with pkgs; [ coreutils curl diffutils e2fsprogs firecracker git jq sqlite ]; text = builtins.replaceStrings - [ "@kernel@" "@rootfs@" ] [ "${kernel}" "${rootfs}"] + [ "@kernel@" "@rootfs@" "@toolchain@" ] [ "${kernel}" "${rootfs}" "${toolchain}" ] (builtins.readFile ./pigeon.sh); }; @@ -92,8 +92,7 @@ pass-arguments-to-command = [ { source = "string"; name = "${pigeon}/bin/pigeon"; } { source = "payload"; name = "project.id"; } - { source = "payload"; name = "project.namespace"; } - { source = "payload"; name = "project.name"; } + { source = "payload"; name = "project.path_with_namespace"; } { source = "payload"; name = "commit.id"; } { source = "payload"; name = "tag"; } { source = "payload"; name = "created_at"; } @@ -117,7 +116,7 @@ match = { type = "value"; parameter = { source = "header"; name = "X-Gitlab-Token"; }; - value = "{{ getenv `PIGEON_GITLAB_SECRET` | js }}"; + value = "{{ getenv `SECRET` | js }}"; }; } ]; @@ -127,13 +126,13 @@ name = "pigeon-webhook"; runtimeInputs = with pkgs; [ coreutils jq nq sqlite webhook ]; text = '' - : "''${PIGEON_GITLAB_SECRET:?}" - : "''${PIGEON_GITLAB_TOKEN:?}" - : "''${PIGEON_PROJECTS:?}" - : "''${PIGEON_STATE}" + : "''${SECRET:?}" + : "''${TOKEN:?}" + : "''${GRADER:?}" + : "''${STATE:?}" - mkdir -p "$PIGEON_STATE" - export NQDIR="$PIGEON_STATE/nqdir" + mkdir -p "$STATE" + export NQDIR="$STATE/nqdir" exec webhook -hooks ${hooks} -template -port 3000 -verbose -debug ''; }; diff --git a/init.sh b/init.sh index 523e3fd..91c303c 100755 --- a/init.sh +++ b/init.sh @@ -13,22 +13,27 @@ mount -t ext4 /dev/vdd /root/results cp -a /root/student/. /tmp/ cp -a --remove-destination /root/project/build/. /tmp/ chmod -R 777 /tmp -cd /tmp +cd /tmp || exit 1 -( - ulimit -t 30 -u 256 -n 256 -f 2048 - timeout -s 9 30 \ +sandbox() { + LIMIT="$1" ; shift + ( + ulimit -t "$LIMIT" -u 256 -n 256 -f 2048 setpriv --reuid=1 --regid=1 --clear-groups --no-new-privs --inh-caps=-all \ - make >/root/results/build.log 2>&1 - echo $? >/root/results/build.status -) + timeout -s 9 "$LIMIT" "$@" + ) +} + +sandbox 30 make >/root/results/build.log 2>&1 +echo $? >/root/results/build.status for TEST in /root/project/tests/* ; do - ( - ulimit -t 2 -u 256 -n 256 -f 2048 - timeout -s 9 2 \ - setpriv --reuid=1 --regid=1 --clear-groups --no-new-privs --inh-caps=-all \ - "$TEST" >"/root/results/$(basename "$TEST")" 2>/dev/null - echo $? >"/root/results/$(basename "$TEST").status" - ) + [ -f "$TEST" ] || continue + NAME="$(basename "$TEST")" + + ARGS="/root/project/args/$NAME" + [ -f "$ARGS" ] || ARGS=/dev/null + + sandbox 2 "$TEST" <"$ARGS" >"/root/results/$NAME" 2>/dev/null + echo $? >"/root/results/$NAME.status" done diff --git a/pigeon.sh b/pigeon.sh index 3dc2168..fcb4ea9 100755 --- a/pigeon.sh +++ b/pigeon.sh @@ -1,67 +1,38 @@ -PROJECT_ID="$1" -PROJECT_NAMESPACE="$2" -PROJECT_NAME="$3" -RELEASE_COMMIT="$4" -RELEASE_TAG="$5" -RELEASE_TIME="$6" - -: "${PIGEON_GITLAB_TOKEN:?}" -: "${PIGEON_PROJECTS:?}" -: "${PIGEON_STATE:?}" - -RATE_LIMIT=3 - -PIGEON_DATABASE="$(realpath -m "$PIGEON_STATE/db")" -PIGEON_PROJECTS="$(realpath -m "$PIGEON_PROJECTS")" -RELEASE_TIME="$(date -d "$RELEASE_TIME" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || true)" - -db() { - sqlite3 -bail -cmd '.timeout 10000' "$PIGEON_DATABASE" -} -q() { - printf "'%s'" "$(printf %s "$1" | sed "s/'/''/g")" -} - -db << 'EOF' -CREATE TABLE IF NOT EXISTS results ( - id INTEGER PRIMARY KEY, - student TEXT NOT NULL, - project TEXT NOT NULL, - hash TEXT NOT NULL, - time TEXT NOT NULL DEFAULT (datetime('now')), - score INTEGER CHECK (score >= 0), - token INTEGER NOT NULL -); -EOF - +PROJECT="${2##*/}" +STUDENT="${2%/*}" +GRADER="$(realpath -m "${GRADER:?}")/$PROJECT" +DATABASE="$(realpath -m "${STATE:?}")/db" DIRECTORY="$(mktemp -d /tmp/pigeon.XXXXXXXXXX)" trap 'rm -rf "$DIRECTORY"' EXIT cd "$DIRECTORY" -git -c advice.detachedHead=false clone -q --rev "$RELEASE_COMMIT" --depth 1 \ - "https://oauth2:$PIGEON_GITLAB_TOKEN@gitlab.com/$PROJECT_NAMESPACE/$PROJECT_NAME.git" student/ +git -c advice.detachedHead=false clone -q --rev "$3" --depth 1 \ + "https://oauth2:${TOKEN:?}@gitlab.com/$2.git" student/ rm -rf student/.git -TESTS="$PIGEON_PROJECTS/$PROJECT_NAME/tests" -mkdir -p project/tests project/build -for DIR in "$TESTS"/*/ ; do - cp -a "$DIR/input.sh" "project/tests/$(basename "$DIR")" +mkdir -p project/tests project/build project/args +for DIR in "$GRADER"/tests/*/ ; do + NAME="$(basename "$DIR")" + cp -a "$DIR/input.sh" "project/tests/$NAME" + [ -f "$DIR/output.txt" ] || + head -c 4096 /dev/urandom | LC_ALL=C tr -dc ' -~' >"project/args/$NAME" done -cp -a "$PIGEON_PROJECTS/$PROJECT_NAME"/build/. project/build/ - -CANONICAL="" -if [ -d "$PIGEON_PROJECTS/$PROJECT_NAME/canonical" ] ; then - mkdir -p canonical - cp -a "$PIGEON_PROJECTS/$PROJECT_NAME"/build/. canonical/ - cp -a --remove-destination "$PIGEON_PROJECTS/$PROJECT_NAME"/canonical/. canonical/ - ( cd canonical && timeout 30 make >/dev/null 2>&1 ) || true - [ -x canonical/program ] && CANONICAL="$(realpath canonical/program)" -fi +cp -a "$GRADER"/build/. project/build/ mkfs.ext4 -q -F -d student student.img 16M mkfs.ext4 -q -F -d project project.img 16M mkfs.ext4 -q -F results.img 16M +if [ -d "$GRADER/canonical" ] ; then + mkdir -p canonical + cp -a "$GRADER"/canonical/. canonical/ + cp -a --remove-destination "$GRADER"/build/. canonical/ + ( + export PATH=@toolchain@/bin + cd canonical && timeout 30 make + ) >/dev/null 2>&1 || exit 1 +fi + cat >vm.json <<'EOF' { "boot-source": { @@ -78,76 +49,90 @@ cat >vm.json <<'EOF' } EOF -firecracker --no-api --level Error --config-file vm.json +timeout 60 firecracker --no-api --level Error --config-file vm.json SCORE=0 -PUBLIC_PASS=1 -: >public -: >release - -for DIR in "$TESTS"/*/ ; do +PUBLIC=1 +for DIR in "$GRADER"/tests/*/ ; do NAME="$(basename "$DIR")" debugfs -R "cat /$NAME" results.img >scratch 2>/dev/null EXIT="$(debugfs -R "cat /$NAME.status" results.img 2>/dev/null)" - EXPECTED_EXIT="$(cat "$DIR/status.txt" 2>/dev/null)" - case "$EXIT" in '' | *[!0-9]*) EXIT=0 ;; esac - case "$EXPECTED_EXIT" in '' | *[!0-9]*) EXPECTED_EXIT=0 ;; esac + EXPECTED="$(cat "$DIR/status.txt" 2>/dev/null || true)" + case "$EXPECTED" in '' | *[!0-9]*) EXPECTED=0 ;; esac + + ARGS="project/args/$NAME" + [ -f "$ARGS" ] || ARGS=/dev/null if [ -f "$DIR/output.txt" ] ; then - cmp -s scratch "$DIR/output.txt" && [ "$EXIT" -eq "$EXPECTED_EXIT" ] && PASS=1 || PASS=0 - elif [ -n "$CANONICAL" ] ; then - PASS=1 - while IFS="$(printf '\t')" read -r ARG OUT ; do - [ -n "$ARG" ] || continue - [ "$OUT" = "$("$CANONICAL" "$ARG" 2>/dev/null)" ] || PASS=0 - done /dev/null || true + fi >expected - if [ "$PASS" = 1 ] ; then - ANSWER=YES + if cmp -s scratch expected && [ "$EXIT" -eq "$EXPECTED" ] ; then + RESULT=YES SCORE=$((SCORE + 1)) elif [ "$EXIT" -eq 124 ] || [ "$EXIT" -eq 137 ] ; then - ANSWER="NO (timed out)" - elif [ "$EXIT" -eq "$EXPECTED_EXIT" ] ; then - ANSWER="NO (wrong output)" + RESULT="NO (timed out)" elif [ "$EXIT" -gt 128 ] ; then - ANSWER="NO (crashed, signal $((EXIT - 128)))" + RESULT="NO (crashed with signal $((EXIT - 128)))" + elif [ "$EXIT" -ne "$EXPECTED" ] ; then + RESULT="NO (exited with status $EXIT, expected $EXPECTED)" else - ANSWER="NO (exited with status $EXIT, expected $EXPECTED_EXIT)" + RESULT="NO (wrong output)" fi case "$NAME" in - p*) [ "$PASS" = 1 ] || PUBLIC_PASS=0; echo "$(cat "$DIR/test.txt") $ANSWER " >>public ;; - r*) echo "$(cat "$DIR/test.txt") $ANSWER " >>release ;; + p*) + if [ "$RESULT" != YES ] ; then PUBLIC=0 ; fi + echo "$(cat "$DIR/test.txt") $RESULT " >>public ;; + r*) + echo "$(cat "$DIR/test.txt") $RESULT " >>release ;; esac done -TOKEN="$(db </dev/null || date -u '+%Y-%m-%d %H:%M:%S')"), $SCORE, - CASE WHEN $PUBLIC_PASS = 1 AND ( + ($PUBLIC = 1) AND ( SELECT COUNT(*) FROM results - WHERE student = $(q "$PROJECT_NAMESPACE") AND project = $(q "$PROJECT_NAME") - AND token = 1 AND time > datetime('now', '-1 day') - ) < $RATE_LIMIT THEN 1 ELSE 0 END + WHERE student = $(q "$STUDENT") AND project = $(q "$PROJECT") + AND token AND time > datetime('now', '-1 day') + ) < 3 RETURNING token; EOF -)" +)" = 1 ] && MESSAGE="$(cat public release || true)" -if [ "$TOKEN" = 1 ] ; then - MESSAGE="$(cat public release)" -else - MESSAGE="$(cat public)" - [ "$PUBLIC_PASS" = 1 ] && MESSAGE="${MESSAGE}"$'\n'"All public tests passed, but you have used all $RATE_LIMIT submissions for today. Release test results are withheld until tomorrow." +if [ "$(debugfs -R "cat /build.status" results.img 2>/dev/null)" -ne 0 ] ; then + debugfs -R "cat /build.log" results.img >build.log 2>/dev/null + MESSAGE="$(printf '````\n%s\n````' "$(head -c 8192 build.log)")" fi curl -s --show-error --out-null --request PUT \ - --header "PRIVATE-TOKEN: $PIGEON_GITLAB_TOKEN" \ + --header "PRIVATE-TOKEN: ${TOKEN:?}" \ --header 'Content-Type: application/json' \ --data "$(jq -n --arg d "$MESSAGE" '{"description": $d}')" \ - --url "https://gitlab.com/api/v4/projects/$PROJECT_ID/releases/$RELEASE_TAG" + --url "https://gitlab.com/api/v4/projects/$1/releases/$4"