Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 101 additions & 65 deletions judge/judgedaemon.main.php
Original file line number Diff line number Diff line change
Expand Up @@ -1342,21 +1342,6 @@ function judge(array $judgeTask): bool
return false;
}

// Copy program with all possible additional files to testcase
// dir. Use hardlinks to preserve space with big executables.
$programdir = $testcasedir . '/execdir';
system('mkdir -p ' . dj_escapeshellarg($programdir), $retval);
if ($retval!==0) {
error("Could not create directory '$programdir'");
}

foreach (glob("$workdir/compile/*") as $compile_file) {
system('cp -PRl ' . dj_escapeshellarg($compile_file) . ' ' . dj_escapeshellarg($programdir), $retval);
if ($retval!==0) {
error("Could not copy program to '$programdir'");
}
}

// do the actual test-run
$combined_run_compare = $compare_config['combined_run_compare'];
[$run_runpath, $error] = fetch_executable(
Expand Down Expand Up @@ -1403,60 +1388,109 @@ function judge(array $judgeTask): bool
putenv('SCRIPTMEMLIMIT=' . $compare_config['script_memory_limit']);
putenv('SCRIPTFILELIMIT=' . $compare_config['script_filesize_limit']);

$test_run_cmd = LIBJUDGEDIR . "/testcase_run.sh $cpuset_opt " .
implode(' ', array_map('dj_escapeshellarg', [
$tcfile['input'],
$tcfile['output'],
"$run_config[time_limit]:$hardtimelimit",
$testcasedir,
$run_runpath,
$compare_runpath,
$compare_config['compare_args']
]));
system($test_run_cmd, $retval);
$input = $tcfile['input'];
$output = $tcfile['output'];
$passLimit = $run_config['pass_limit'];
for ($passCnt = 1; $passCnt <= $passLimit; $passCnt++) {
$nextPass = false;
if ($passLimit > 1) {
logmsg(LOG_INFO, " πŸ”„ Running pass $passCnt...");
}

// What does the exitcode mean?
if (! isset($EXITCODES[$retval])) {
alert('error');
error("Unknown exitcode ($retval) from testcase_run.sh for s$judgeTask[submitid]");
}
$result = $EXITCODES[$retval];
$passdir = $testcasedir . '/' . $passCnt;
mkdir($passdir, 0755, true);

// Try to read metadata from file
$runtime = null;
$metadata = read_metadata($testcasedir . '/program.meta');
// Copy program with all possible additional files to testcase
// dir. Use hardlinks to preserve space with big executables.
$programdir = $passdir . '/execdir';
system('mkdir -p ' . dj_escapeshellarg($programdir), $retval);
if ($retval!==0) {
error("Could not create directory '$programdir'");
}

if (isset($metadata['time-used'])) {
$runtime = @$metadata[$metadata['time-used']];
}
foreach (glob("$workdir/compile/*") as $compile_file) {
system('cp -PRl ' . dj_escapeshellarg($compile_file) . ' ' . dj_escapeshellarg($programdir), $retval);
if ($retval!==0) {
error("Could not copy program to '$programdir'");
}
}

if ($result === 'compare-error') {
if ($combined_run_compare) {
logmsg(LOG_ERR, "comparing failed for combined run/compare script '" . $judgeTask['run_script_id'] . "'");
$description = 'combined run/compare script ' . $judgeTask['run_script_id'] . ' crashed';
disable('run_script', 'run_script_id', $judgeTask['run_script_id'], $description, $judgeTask['judgetaskid']);
} else {
logmsg(LOG_ERR, "comparing failed for compare script '" . $judgeTask['compare_script_id'] . "'");
$description = 'compare script ' . $judgeTask['compare_script_id'] . ' crashed';
disable('compare_script', 'compare_script_id', $judgeTask['compare_script_id'], $description, $judgeTask['judgetaskid']);
$test_run_cmd = LIBJUDGEDIR . "/testcase_run.sh $cpuset_opt " .
implode(' ', array_map('dj_escapeshellarg', [
$input,
$output,
"$run_config[time_limit]:$hardtimelimit",
$passdir,
$run_runpath,
$compare_runpath,
$compare_config['compare_args']
]));
system($test_run_cmd, $retval);

// What does the exitcode mean?
if (!isset($EXITCODES[$retval])) {
alert('error');
error("Unknown exitcode ($retval) from testcase_run.sh for s$judgeTask[submitid]");
}
return false;
}
$result = $EXITCODES[$retval];

$new_judging_run = [
'runresult' => urlencode($result),
'runtime' => urlencode((string)$runtime),
'output_run' => rest_encode_file($testcasedir . '/program.out', $output_storage_limit),
'output_error' => rest_encode_file($testcasedir . '/program.err', $output_storage_limit),
'output_system' => rest_encode_file($testcasedir . '/system.out', $output_storage_limit),
'metadata' => rest_encode_file($testcasedir . '/program.meta', false),
'output_diff' => rest_encode_file($testcasedir . '/feedback/judgemessage.txt', $output_storage_limit),
'hostname' => $myhost,
'testcasedir' => $testcasedir,
];
// Try to read metadata from file
$runtime = null;
$metadata = read_metadata($passdir . '/program.meta');

if (file_exists($testcasedir . '/feedback/teammessage.txt')) {
$new_judging_run['team_message'] = rest_encode_file($testcasedir . '/feedback/teammessage.txt', $output_storage_limit);
if (isset($metadata['time-used'])) {
$runtime = @$metadata[$metadata['time-used']];
}

if ($result === 'compare-error') {
if ($combined_run_compare) {
logmsg(LOG_ERR, "comparing failed for combined run/compare script '" . $judgeTask['run_script_id'] . "'");
$description = 'combined run/compare script ' . $judgeTask['run_script_id'] . ' crashed';
disable('run_script', 'run_script_id', $judgeTask['run_script_id'], $description, $judgeTask['judgetaskid']);
} else {
logmsg(LOG_ERR, "comparing failed for compare script '" . $judgeTask['compare_script_id'] . "'");
$description = 'compare script ' . $judgeTask['compare_script_id'] . ' crashed';
disable('compare_script', 'compare_script_id', $judgeTask['compare_script_id'], $description, $judgeTask['judgetaskid']);
}
return false;
}

$new_judging_run = [
'runresult' => urlencode($result),
'runtime' => urlencode((string)$runtime),
'output_run' => rest_encode_file($passdir . '/program.out', $output_storage_limit),
'output_error' => rest_encode_file($passdir . '/program.err', $output_storage_limit),
'output_system' => rest_encode_file($passdir . '/system.out', $output_storage_limit),
'metadata' => rest_encode_file($passdir . '/program.meta', false),
'output_diff' => rest_encode_file($passdir . '/feedback/judgemessage.txt', $output_storage_limit),
'hostname' => $myhost,
'testcasedir' => $testcasedir,
];

if (file_exists($passdir . '/feedback/teammessage.txt')) {
$new_judging_run['team_message'] = rest_encode_file($passdir . '/feedback/teammessage.txt', $output_storage_limit);
}

if ($passLimit > 1) {
$walltime = $metadata['wall-time'] ?? '?';
logmsg(LOG_INFO, ' ' . ($result === 'correct' ? " \033[0;32mβœ”\033[0m" : " \033[1;31mβœ—\033[0m")
. ' ...done in ' . $walltime . 's (CPU: ' . $runtime . 's), result: ' . $result);
}

if ($result !== 'correct') {
break;
}
if (file_exists($passdir . '/feedback/nextpass.in')) {
$input = $passdir . '/feedback/nextpass.in';
$nextPass = true;
} else {
break;
}
}
if ($nextPass) {
$description = 'validator produced more passes than allowed ($passLimit)';
disable('compare_script', 'compare_script_id', $judgeTask['compare_script_id'], $description, $judgeTask['judgetaskid']);
return false;
}

$ret = true;
Expand Down Expand Up @@ -1485,9 +1519,11 @@ function judge(array $judgeTask): bool
$ret = (bool)$needsMoreWork;
}

$walltime = $metadata['wall-time'] ?? '?';
logmsg(LOG_INFO, ' ' . ($result === 'correct' ? " \033[0;32mβœ”\033[0m" : " \033[1;31mβœ—\033[0m")
. ' ...done in ' . $walltime . 's (CPU: ' . $runtime . 's), result: ' . $result);
if ($passLimit == 1) {
$walltime = $metadata['wall-time'] ?? '?';
logmsg(LOG_INFO, ' ' . ($result === 'correct' ? " \033[0;32mβœ”\033[0m" : " \033[1;31mβœ—\033[0m")
. ' ...done in ' . $walltime . 's (CPU: ' . $runtime . 's), result: ' . $result);
}

// done!
return $ret;
Expand Down
2 changes: 1 addition & 1 deletion judge/run-interactive.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ MYDIR="$(dirname $0)"

# Run the program while redirecting its stdin/stdout to 'runjury' via
# 'runpipe'. Note that "$@" expands to separate, quoted arguments.
exec ../dj-bin/runpipe ${DEBUG:+-v} -M "$META" -o "$PROGOUT" "$MYDIR/runjury" "$TESTIN" "$TESTOUT" "$FEEDBACK" = "$@"
exec ../../dj-bin/runpipe ${DEBUG:+-v} -M "$META" -o "$PROGOUT" "$MYDIR/runjury" "$TESTIN" "$TESTOUT" "$FEEDBACK" = "$@"
13 changes: 7 additions & 6 deletions judge/testcase_run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ cleanup ()
{
# Remove some copied files to save disk space
if [ "$WORKDIR" ]; then
rm -f "$WORKDIR/../dj-bin/runpipe" 2> /dev/null || true
rm -f "$WORKDIR/../../dj-bin/runpipe" 2> /dev/null || true

# Replace testdata by symlinks to reduce disk usage
if [ -f "$WORKDIR/testdata.in" ]; then
Expand Down Expand Up @@ -157,7 +157,8 @@ fi

cd "$WORKDIR"

PREFIX="/$(basename "$PWD")"
# Get the last two directory entries of $PWD
PREFIX="/$(basename $(realpath "$PWD/.."))/$(basename "$PWD")"

# Make testing/execute dir accessible for RUNUSER:
chmod a+x "$WORKDIR" "$WORKDIR/execdir"
Expand All @@ -174,10 +175,10 @@ logmsg $LOG_INFO "setting up testing (chroot) environment"
cp "$TESTIN" "$WORKDIR/testdata.in"

# shellcheck disable=SC2174
mkdir -p -m 0711 ../bin ../dj-bin ../dev
mkdir -p -m 0711 ../../bin ../../dj-bin ../../dev
# copy a support program for interactive problems:
cp -pL "$RUNPIPE" ../dj-bin/runpipe
chmod a+rx ../dj-bin/runpipe
cp -pL "$RUNPIPE" ../../dj-bin/runpipe
chmod a+rx ../../dj-bin/runpipe

# If we need to create a writable temp directory, do so
if [ "$CREATE_WRITABLE_TEMP_DIR" ]; then
Expand All @@ -203,7 +204,7 @@ exitcode=0
# shellcheck disable=SC2153
runcheck "$RUN_SCRIPT" $RUNARGS \
$GAINROOT "$RUNGUARD" ${DEBUG:+-v -V "DEBUG=$DEBUG"} ${TMPDIR:+ -V "TMPDIR=$TMPDIR"} $CPUSET_OPT \
-r "$PWD/.." \
-r "$PWD/../.." \
--nproc=$PROCLIMIT \
--no-core --streamsize=$FILELIMIT \
--user="$RUNUSER" --group="$RUNGROUP" \
Expand Down
36 changes: 36 additions & 0 deletions webapp/migrations/Version20240921081301.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?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 Version20240921081301 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add multipass problem support';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE problem ADD is_multipass_problem TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Whether this problem is a multi-pass problem.\', ADD multipass_limit INT UNSIGNED DEFAULT NULL COMMENT \'Optional limit on the number of rounds for multi-pass problems; defaults to 2 if not specified.\'');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE problem DROP is_multipass_problem, DROP multipass_limit');
}

public function isTransactional(): bool
{
return false;
}
}
17 changes: 17 additions & 0 deletions webapp/src/Controller/Jury/ProblemController.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public function indexAction(): Response
'memlimit' => ['title' => 'memory limit', 'sort' => true],
'outputlimit' => ['title' => 'output limit', 'sort' => true],
'num_testcases' => ['title' => '# test cases', 'sort' => true],
'type' => ['title' => 'type', 'sort' => true],
];

$contestCountData = $this->em->createQueryBuilder()
Expand Down Expand Up @@ -206,9 +207,17 @@ public function indexAction(): Response
$problemdata['badges'] = ['value' => $badges];

// merge in the rest of the data
$type = '';
if ($p->getCombinedRunCompare()) {
$type .= ' interactive';
}
if ($p->isMultipassProblem()) {
$type .= ' multi-pass';
}
$problemdata = array_merge($problemdata, [
'num_contests' => ['value' => (int)($contestCounts[$p->getProbid()] ?? 0)],
'num_testcases' => ['value' => (int)$row['testdatacount']],
'type' => ['value' => $type],
]);

// Save this to our list of rows
Expand Down Expand Up @@ -484,6 +493,13 @@ public function viewAction(Request $request, SubmissionService $submissionServic
new SubmissionRestriction(problemId: $problem->getProbid()),
);

$type = '';
if ($problem->getCombinedRunCompare()) {
$type .= ' interactive';
}
if ($problem->isMultipassProblem()) {
$type .= ' multi-pass';
}
$data = [
'problem' => $problem,
'problemAttachmentForm' => $problemAttachmentForm->createView(),
Expand All @@ -493,6 +509,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic
'defaultOutputLimit' => (int)$this->config->get('output_limit'),
'defaultRunExecutable' => (string)$this->config->get('default_run'),
'defaultCompareExecutable' => (string)$this->config->get('default_compare'),
'type' => $type,
'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1,
'showExternalResult' => $this->dj->shadowMode(),
'lockedProblem' => $lockedProblem,
Expand Down
1 change: 1 addition & 0 deletions webapp/src/Controller/Jury/SubmissionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,7 @@ public function viewAction(
'combinedRunCompare' => $submission->getProblem()->getCombinedRunCompare(),
'requestedOutputCount' => $requestedOutputCount,
'version_warnings' => [],
'isMultiPassProblem' => $submission->getProblem()->isMultipassProblem(),
];

if ($selectedJudging === null) {
Expand Down
40 changes: 40 additions & 0 deletions webapp/src/Entity/Problem.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ class Problem extends BaseApiEntity implements
#[Serializer\Exclude]
private ?string $problemstatement_type = null;

#[ORM\Column(options: [
'comment' => 'Whether this problem is a multi-pass problem.',
'default' => 0,
])]
#[Serializer\Exclude]
private bool $isMultipassProblem = false;

#[ORM\Column(
nullable: true,
options: ['comment' => 'Optional limit on the number of rounds; defaults to 1 for traditional problems, 2 for multi-pass problems if not specified.', 'unsigned' => true]
)]
#[Assert\GreaterThan(0)]
#[Serializer\Exclude]
private ?int $multipassLimit = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the code becomes much easier if we already set this to 2 (like the comment implies). Now this is only a constant which we use in other parts of the code.

It would lose the information if a problem uses the default value but I don't think we'll need that often in practice?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, this was leaking info into other classes. I changed just the comment here and the getter below to do the right thing.


/**
* @var Collection<int, Submission>
*/
Expand Down Expand Up @@ -273,6 +288,31 @@ public function getCombinedRunCompare(): bool
return $this->combined_run_compare;
}

public function setMultipassProblem(bool $isMultipassProblem): Problem
{
$this->isMultipassProblem = $isMultipassProblem;
return $this;
}

public function isMultipassProblem(): bool
{
return $this->isMultipassProblem;
}

public function setMultipassLimit(?int $multipassLimit): Problem
{
$this->multipassLimit = $multipassLimit;
return $this;
}

public function getMultipassLimit(): int
{
if ($this->isMultipassProblem) {
return $this->multipassLimit ?? 2;
}
return 1;
}

public function setProblemstatementFile(?UploadedFile $problemstatementFile): Problem
{
$this->problemstatementFile = $problemstatementFile;
Expand Down
1 change: 1 addition & 0 deletions webapp/src/Service/DOMJudgeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -1439,6 +1439,7 @@ public function getRunConfig(ContestProblem $problem, Submission $submission): s
'output_limit' => $outputLimit,
'process_limit' => $this->config->get('process_limit'),
'entry_point' => $submission->getEntryPoint(),
'pass_limit' => $problem->getProblem()->getMultipassLimit(),
'hash' => $runExecutable->getHash(),
]
);
Expand Down
Loading
Loading