Skip to content

Commit

Permalink
Collect compiler and runner versions before compilation.
Browse files Browse the repository at this point in the history
Progress towards DOMjudge#1241

Each language has canonical information while we store the latest info
per judgehost in a separate table.
The information from the judgehost is currently just informative, a
next step would be to actually disable judging from this judgehost in
case of a version mismatch.

The canonical information can be exposed to the teams via a config
option.
  • Loading branch information
meisterT committed May 7, 2023
1 parent 6c46ec3 commit 9e0ba5f
Show file tree
Hide file tree
Showing 19 changed files with 1,089 additions and 25 deletions.
6 changes: 6 additions & 0 deletions etc/db-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
default_value: false
public: true
description: Is the scoreboard resolution measured in seconds instead of minutes?

- category: Judging
description: Options related to how judging is performed.
items:
Expand Down Expand Up @@ -259,6 +260,11 @@
default_value: true
public: true
description: Show submission and problem statistics on the team and public pages.
- name: show_canonical_versions
type: bool
default_value: false
public: true
description: Show canonical compiler and runner version on the team pages.
- category: Authentication
description: Options related to authentication.
items:
Expand Down
46 changes: 43 additions & 3 deletions judge/judgedaemon.main.php
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ function fetch_executable_internal(
if (!is_dir($execdir) || !file_exists($execdeploypath)) {
system('rm -rf ' . dj_escapeshellarg($execdir) . ' ' . dj_escapeshellarg($execbuilddir));
system('mkdir -p ' . dj_escapeshellarg($execbuilddir), $retval);
if ($retval!==0) {
if ($retval !== 0) {
error("Could not create directory '$execbuilddir'");
}

Expand Down Expand Up @@ -439,7 +439,7 @@ function fetch_executable_internal(
if ($do_compile) {
logmsg(LOG_DEBUG, "Building executable in $execdir, under 'build/'");
system(LIBJUDGEDIR . '/build_executable.sh ' . dj_escapeshellarg($execdir), $retval);
if ($retval!==0) {
if ($retval !== 0) {
return [null, "Failed to build executable in $execdir.", "$execdir/build.log"];
}
chmod($execrunpath, 0755);
Expand Down Expand Up @@ -1053,6 +1053,46 @@ function compile(
return true;
}

// Verify compile and runner versions.
$judgeTaskId = $judgeTask['judgetaskid'];
$version_verification = dj_json_decode(request(sprintf('judgehosts/get_version_commands/%s', $judgeTaskId), 'GET'));
if (isset($version_verification['compiler_version_command']) || isset($version_verification['runner_version_command'])) {
logmsg(LOG_INFO, " 📋 Verifying versions.");
$versions = [];
$version_output_file = $workdir . '/vcheck.out';
$args = 'hostname=' . urlencode($myhost);
foreach (['compiler', 'runner'] as $type) {
if (isset($version_verification[$type . '_version_command'])) {
$vcscript_content = $version_verification[$type . '_version_command'];
$vcscript = tempnam(TMPDIR, 'vcheck-');
file_put_contents($vcscript, $vcscript_content);
chmod($vcscript, 0755);
$compile_cmd = LIBJUDGEDIR . "/version_check.sh " .
implode(' ', array_map('dj_escapeshellarg', [
$vcscript,
$workdir,
]));

if (file_exists($version_output_file)) {
unlink($version_output_file);
}
system($compile_cmd, $retval);
$versions[$type] = trim(file_get_contents($version_output_file));
if ($retval !== 0) {
$versions[$type] =
"Getting $type version failed with exit code $retval\n"
. $versions[$type];
}
}
if (isset($versions[$type])) {
$args .= "&$type=" . urlencode(base64_encode($versions[$type]));
}
}

// TODO: Add actual check once implemented in the backend.
request(sprintf('judgehosts/check_versions/%s', $judgeTaskId), 'PUT', $args);
}

// Get the source code from the DB and store in local file(s).
$url = sprintf('judgehosts/get_files/source/%s', $judgeTask['submitid']);
$sources = request($url, 'GET');
Expand Down Expand Up @@ -1186,7 +1226,7 @@ function compile(
$url = sprintf('judgehosts/update-judging/%s/%s', urlencode($myhost), urlencode((string)$judgeTask['judgetaskid']));
request($url, 'PUT', $args);

// compile error: our job here is done
// Compile error: our job here is done.
if (! $compile_success) {
return false;
}
Expand Down
113 changes: 113 additions & 0 deletions judge/version_check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/bin/sh

# Script to verify compiler versions.

# Exit automatically, whenever a simple command fails and trap it:
set -e
trap error EXIT

cleanexit ()
{
trap - EXIT

chmod go= "$WORKDIR/vcheck" "$WORKDIR/vcheck-script"
logmsg $LOG_DEBUG "exiting, code = '$1'"
exit $1
}

# Error and logging functions
# shellcheck disable=SC1090
. "$DJ_LIBDIR/lib.error.sh"

# Logging:
LOGFILE="$DJ_LOGDIR/judge.$(hostname | cut -d . -f 1).log"
LOGLEVEL=$LOG_DEBUG
PROGNAME="$(basename "$0")"

# Check for judge backend debugging:
if [ "$DEBUG" ]; then
export DEBUG
export VERBOSE=$LOG_DEBUG
logmsg $LOG_NOTICE "debugging enabled, DEBUG='$DEBUG'"
else
export VERBOSE=$LOG_ERR
fi

# Location of scripts/programs:
SCRIPTDIR="$DJ_LIBJUDGEDIR"
GAINROOT="sudo -n"
RUNGUARD="$DJ_BINDIR/runguard"

logmsg $LOG_INFO "starting '$0', PID = $$"

[ $# -ge 2 ] || error "not enough arguments. See script-code for usage."
VERSION_CHECK_SCRIPT="$1"; shift
WORKDIR="$1"; shift
logmsg $LOG_DEBUG "arguments: '$VERSION_CHECK_SCRIPT' '$WORKDIR'"

if [ ! -d "$WORKDIR" ] || [ ! -w "$WORKDIR" ] || [ ! -x "$WORKDIR" ]; then
error "Workdir not found or not writable: $WORKDIR"
fi
[ -x "$VERSION_CHECK_SCRIPT" ] || error "compile script not found or not executable: $VERSION_CHECK_SCRIPT"
[ -x "$RUNGUARD" ] || error "runguard not found or not executable: $RUNGUARD"

OLDDIR="$PWD"
cd "$WORKDIR"

# Make compile dir accessible and writable for RUNUSER:
mkdir -p "$WORKDIR/vcheck"
chmod a+rwx "$WORKDIR/vcheck"

# Create files which are expected to exist: compiler output and runtime
touch vcheck.out vcheck.meta

# Copy compile script into chroot
# shellcheck disable=SC2174
if [ -e "$WORKDIR/vcheck-script" ]; then
mv "$WORKDIR/vcheck-script" "$WORKDIR/vcheck-script-old"
fi
mkdir -m 0777 -p "$WORKDIR/vcheck-script"
cp -a "$VERSION_CHECK_SCRIPT" "$PWD/vcheck-script/"

cd "$WORKDIR/vcheck"

logmsg $LOG_INFO "starting version checking"

if [ -n "$DEBUG" ]; then
ENVIRONMENT_VARS="$ENVIRONMENT_VARS -V DEBUG=$DEBUG"
fi

exitcode=0
$GAINROOT "$RUNGUARD" ${DEBUG:+-v} $CPUSET_OPT -u "$RUNUSER" -g "$RUNGROUP" \
-r "$PWD/.." -d "/vcheck" \
-m $SCRIPTMEMLIMIT -t $SCRIPTTIMELIMIT -c -f $SCRIPTFILELIMIT -s $SCRIPTFILELIMIT \
-M "$WORKDIR/vcheck.meta" $ENVIRONMENT_VARS -- \
"/vcheck-script/$(basename $VERSION_CHECK_SCRIPT)" >"$WORKDIR/vcheck.tmp" 2>&1 || \
exitcode=$?

# Make sure that all files are owned by the current user/group, so
# that we can delete the judging output tree without root access.
# We also remove group RUNGROUP so that this can safely be shared
# across multiple judgedaemons, and remove write permissions.
$GAINROOT chown -R "$(id -un):" "$WORKDIR/vcheck"
chmod -R go-w "$WORKDIR/vcheck"

cd "$WORKDIR"

if [ $exitcode -ne 0 ] && [ ! -s vcheck.meta ]; then
echo "internal-error: runguard crashed" > vcheck.meta
echo "Runguard exited with code $exitcode and 'vcheck.meta' is empty, it likely crashed." >vcheck.out
echo "Version check output:" >>vcheck.out
cat vcheck.tmp >>vcheck.out
cleanexit ${E_INTERNAL_ERROR:-1}
fi

if [ $exitcode -ne 0 ]; then
echo "Version checking failed with exitcode $exitcode, version check output:" >vcheck.out
cat vcheck.tmp >>vcheck.out
cleanexit ${E_COMPILER_ERROR:-1}
fi
cat vcheck.tmp >>vcheck.out

logmsg $LOG_INFO "Version check successful"
cleanexit 0
44 changes: 44 additions & 0 deletions webapp/migrations/Version20230507123700.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20230507123700 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE version (versionid INT UNSIGNED AUTO_INCREMENT NOT NULL COMMENT \'Version ID\', langid VARCHAR(32) DEFAULT NULL COMMENT \'Language ID (string)\', judgehostid INT UNSIGNED DEFAULT NULL COMMENT \'Judgehost ID\', compiler_version LONGBLOB DEFAULT NULL COMMENT \'Compiler version(DC2Type:blobtext)\', runner_version LONGBLOB DEFAULT NULL COMMENT \'Runner version(DC2Type:blobtext)\', runner_version_command VARCHAR(255) DEFAULT NULL COMMENT \'Runner version command\', compiler_version_command VARCHAR(255) DEFAULT NULL COMMENT \'Compiler version command\', last_changed_time NUMERIC(32, 9) UNSIGNED DEFAULT NULL COMMENT \'Time this version command output was last updated\', INDEX IDX_BF1CD3C32271845 (langid), INDEX IDX_BF1CD3C3E0E4FC3E (judgehostid), PRIMARY KEY(versionid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'Runner and compiler version commands per language.\' ');
$this->addSql('ALTER TABLE version ADD CONSTRAINT FK_BF1CD3C32271845 FOREIGN KEY (langid) REFERENCES language (langid)');
$this->addSql('ALTER TABLE version ADD CONSTRAINT FK_BF1CD3C3E0E4FC3E FOREIGN KEY (judgehostid) REFERENCES judgehost (judgehostid)');
$this->addSql('ALTER TABLE immutable_executable ADD compiler_version LONGBLOB DEFAULT NULL COMMENT \'Compiler version(DC2Type:blobtext)\', ADD compiler_version_command VARCHAR(255) DEFAULT NULL COMMENT \'Compiler version command\', ADD runner_version LONGBLOB DEFAULT NULL COMMENT \'Runner version(DC2Type:blobtext)\', ADD runner_version_command VARCHAR(255) DEFAULT NULL COMMENT \'Runner version command\'');
$this->addSql('ALTER TABLE language ADD compiler_version LONGBLOB DEFAULT NULL COMMENT \'Compiler version(DC2Type:blobtext)\', ADD runner_version LONGBLOB DEFAULT NULL COMMENT \'Runner version(DC2Type:blobtext)\', ADD runner_version_command VARCHAR(255) DEFAULT NULL COMMENT \'Runner version command\', ADD compiler_version_command VARCHAR(255) DEFAULT NULL COMMENT \'Compiler version command\'');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE version DROP FOREIGN KEY FK_BF1CD3C32271845');
$this->addSql('ALTER TABLE version DROP FOREIGN KEY FK_BF1CD3C3E0E4FC3E');
$this->addSql('DROP TABLE version');
$this->addSql('ALTER TABLE language DROP compiler_version, DROP runner_version, DROP runner_version_command, DROP compiler_version_command');
$this->addSql('ALTER TABLE immutable_executable DROP compiler_version, DROP compiler_version_command, DROP runner_version, DROP runner_version_command');
}

public function isTransactional(): bool
{
return false;
}
}
117 changes: 117 additions & 0 deletions webapp/src/Controller/API/JudgehostController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
use App\Entity\Judging;
use App\Entity\JudgingRun;
use App\Entity\JudgingRunOutput;
use App\Entity\Language;
use App\Entity\QueueTask;
use App\Entity\Rejudging;
use App\Entity\Submission;
use App\Entity\SubmissionFile;
use App\Entity\TestcaseContent;
use App\Entity\Version;
use App\Service\BalloonService;
use App\Service\ConfigurationService;
use App\Service\DOMJudgeService;
Expand Down Expand Up @@ -1166,6 +1168,121 @@ public function getFilesAction(
};
}

/**
* Get version commands for a given compile script.
*/
#[IsGranted(new Expression("is_granted('ROLE_JURY') or is_granted('ROLE_JUDGEHOST')"))]
#[Rest\Get('/get_version_commands/{judgetaskid<\d+>}')]
public function getVersionCommands(string $judgetaskid): array
{
/** @var JudgeTask $judgeTask */
$judgeTask = $this->em->getRepository(JudgeTask::class)
->findOneBy(['judgetaskid' => $judgetaskid]);
if (!$judgeTask) {
throw new BadRequestHttpException('Unknown judge task with id ' . $judgetaskid);
}

/** @var Submission $submission */
$submission = $this->em->getRepository(Submission::class)
->findOneBy(['submitid' => $judgeTask->getSubmitid()]);
if (!$submission) {
throw new BadRequestHttpException('Unknown submission with submitid ' . $judgeTask->getSubmitid());
}

/** @var Language $language */
$language = $submission->getLanguage();


$ret = [];
if (!empty($language->getCompilerVersionCommand())) {
$ret['compiler_version_command'] = $language->getCompilerVersionCommand();
}
if (!empty($language->getRunnerVersionCommand())) {
$ret['runner_version_command'] = $language->getRunnerVersionCommand();
}
return $ret;
}

#[IsGranted(new Expression("is_granted('ROLE_JURY') or is_granted('ROLE_JUDGEHOST')"))]
#[Rest\Put('/check_versions/{judgetaskid}')]
public function checkVersions(Request $request, string $judgetaskid)
{
/** @var JudgeTask $judgeTask */
$judgeTask = $this->em->getRepository(JudgeTask::class)
->findOneBy(['judgetaskid' => $judgetaskid]);
if (!$judgeTask) {
throw new BadRequestHttpException('Unknown judge task with id ' . $judgetaskid);
}

/** @var Submission $submission */
$submission = $this->em->getRepository(Submission::class)
->findOneBy(['submitid' => $judgeTask->getSubmitid()]);
if (!$submission) {
throw new BadRequestHttpException('Unknown submission with submitid ' . $judgeTask->getSubmitid());
}

/** @var Language $language */
$language = $submission->getLanguage();

$hostname = $request->request->get('hostname');
/** @var Judgehost $judgehost */
$judgehost = $this->em->getRepository(Judgehost::class)->findOneBy(['hostname' => $hostname]);
if (!$judgehost) {
throw new BadRequestHttpException(
'Register yourself first. You (' . $hostname . ') are not known to us yet.');
}

$reportedVersions = [];
if ($request->request->has('compiler')) {
$reportedVersions['compiler'] = base64_decode($request->request->get('compiler'));
}
if ($request->request->has('runner')) {
$reportedVersions['runner'] = base64_decode($request->request->get('runner'));
}
$this->em->wrapInTransaction(function () use (
$request,
$judgehost,
$reportedVersions,
$language
) {
/** @var Version $version */
$version = $this->em->getRepository(Version::class)
->findOneBy(['language' => $language, 'judgehost' => $judgehost]);

$newVersion = false;
if (!$version) {
$newVersion = true;
$version = new Version();
$version
->setLanguage($language)
->setJudgehost($judgehost);
$this->em->persist($version);
}
if (isset($reportedVersions['compiler'])) {
if ($version->getCompilerVersion() !== $reportedVersions['compiler']) {
$version
->setCompilerVersion($reportedVersions['compiler'])
->setCompilerVersionCommand($language->getCompilerVersionCommand());
$newVersion = true;
}
}
if (isset($reportedVersions['runner'])) {
if ($version->getRunnerVersion() !== $reportedVersions['runner']) {
$version
->setRunnerVersion($reportedVersions['runner'])
->setRunnerVersionCommand($language->getRunnerVersionCommand());
$newVersion = true;
}
}
if ($newVersion) {
$version->setLastChangedTime(Utils::now());
$this->em->flush();
}
// TODO: Optionally check version here against canonical version.
});
return [];
}

private function getSourceFiles(string $id): array
{
$queryBuilder = $this->em->createQueryBuilder()
Expand Down
Loading

0 comments on commit 9e0ba5f

Please sign in to comment.