diff --git a/judge/judgedaemon.main.php b/judge/judgedaemon.main.php index eb0574fdf0..b0d0651958 100644 --- a/judge/judgedaemon.main.php +++ b/judge/judgedaemon.main.php @@ -463,10 +463,10 @@ function fetch_executable_internal( } switch ($execlang) { case 'c': - $buildscript .= "gcc -Wall -O2 -std=gnu11 $source -o run -lm\n"; + $buildscript .= "gcc -Wall -O2 -static -std=gnu11 $source -o run -lm\n"; break; case 'cpp': - $buildscript .= "g++ -Wall -O2 -std=gnu++20 $source -o run\n"; + $buildscript .= "g++ -Wall -O2 -static -std=gnu++20 $source -o run\n"; break; case 'java': $buildscript .= "javac -cp . -d . $source\n"; @@ -891,6 +891,40 @@ function fetch_executable_internal( } logmsg(LOG_INFO, " 🔥 Pre-heating judgehost completed."); continue; + } elseif ($type == 'generic_task') { + foreach ($row as $judgeTask) { + if (!(isset($judgeTask['run_script_id']) && isset($judgeTask['run_config']))) { + // TODO: Should we actually exit here, we do for malformed above but the mistake is not on our side. + error("Received judgehost_check task without run_script_id/run_config."); + } + + $run_config = dj_json_decode($judgeTask['run_config']); + $tmpfile = tempnam(TMPDIR, 'generic_task_'); + [$runpath, $error] = fetch_executable( + $workdirpath, + 'generic_task', + $judgeTask['run_script_id'], + $run_config['hash'], + $judgeTask['judgetaskid'] + ); + + if (!run_command_safe([$runpath, $tmpfile])) { + disable('run_script', 'run_script_id', $judgeTask['run_script_id'], "Running '$runpath' failed."); + } + + request( + sprintf('judgehosts/add-generic-task/%s/%s', urlencode($myhost), urlencode((string)$judgeTask['judgetaskid'])), + 'POST', + ['generic_task' => rest_encode_file($tmpfile, false)], + false + ); + + unlink($tmpfile); + logmsg(LOG_INFO, " ⇡ Uploading task output."); + } + + logmsg(LOG_INFO, " 🔥 Running generic judgehost tasks completed."); + continue; } // Create workdir for judging. diff --git a/sql/files/defaultdata/chroot_upgrade/build b/sql/files/defaultdata/chroot_upgrade/build new file mode 100755 index 0000000000..3ad1806766 --- /dev/null +++ b/sql/files/defaultdata/chroot_upgrade/build @@ -0,0 +1,2 @@ +#!/bin/sh +# nothing to compile diff --git a/sql/files/defaultdata/chroot_upgrade/run b/sql/files/defaultdata/chroot_upgrade/run new file mode 100755 index 0000000000..8d79a8f3d8 --- /dev/null +++ b/sql/files/defaultdata/chroot_upgrade/run @@ -0,0 +1,13 @@ +#!/bin/bash +# This is an example to upgrade the chroot packages +# The basic calling convention of these scripts is: +# ./script + +output_file="$1" + +exec &>> "${output_file}" + +bin/dj_run_chroot "apt-get update" +bin/dj_run_chroot "apt-get full-upgrade -y" + +exit 0 diff --git a/sql/files/defaultdata/compare/build b/sql/files/defaultdata/compare/build index 31bb255097..f9275e2544 100755 --- a/sql/files/defaultdata/compare/build +++ b/sql/files/defaultdata/compare/build @@ -1,2 +1,2 @@ #!/bin/sh -g++ -std=c++11 -pedantic -g -O1 -Wall -fstack-protector -D_FORTIFY_SOURCE=2 -fPIE -Wformat -Wformat-security -fPIE -Wl,-z,relro -Wl,-z,now compare.cc -o run +g++ -std=c++11 -pedantic -g -O1 -static -Wall -fstack-protector -D_FORTIFY_SOURCE=2 -fPIE -Wformat -Wformat-security -fPIE -Wl,-z,relro -Wl,-z,now compare.cc -o run diff --git a/sql/files/defaultdata/judgehost_info/build b/sql/files/defaultdata/judgehost_info/build new file mode 100755 index 0000000000..3ad1806766 --- /dev/null +++ b/sql/files/defaultdata/judgehost_info/build @@ -0,0 +1,2 @@ +#!/bin/sh +# nothing to compile diff --git a/sql/files/defaultdata/judgehost_info/run b/sql/files/defaultdata/judgehost_info/run new file mode 100755 index 0000000000..9b0792f868 --- /dev/null +++ b/sql/files/defaultdata/judgehost_info/run @@ -0,0 +1,33 @@ +#!/bin/sh +# This is the default script to retrieve a the configuration of both +# software & hardware of the judgehost. The basic +# calling convention of these scripts is: +# ./script + +output_file="$1" + +# Source - https://stackoverflow.com/a +# Posted by fjarlq, modified by community. See post 'Timeline' for change history +# Retrieved 2025-11-15, License - CC BY-SA 4.0 + +# We can't use &>> as it's not POSIX, +# this does introduce a racecondition +exec 1>> "${output_file}" +exec 2>> "${output_file}" + +# Generic linux/distro information +uname -a +cat /etc/os-release + +# Information about the php version +php -v + +# Generic hardware information +lsusb +lspci +lscpu +cat /proc/cpuinfo +free -h +cat /proc/meminfo + +exit 0 diff --git a/webapp/migrations/Version20251117185929.php b/webapp/migrations/Version20251117185929.php new file mode 100644 index 0000000000..c3c1e13012 --- /dev/null +++ b/webapp/migrations/Version20251117185929.php @@ -0,0 +1,42 @@ +addSql('CREATE TABLE generic_task (taskid INT UNSIGNED AUTO_INCREMENT NOT NULL COMMENT \'Task ID\', judgetaskid INT UNSIGNED DEFAULT NULL COMMENT \'JudgeTask ID\', runtime DOUBLE PRECISION DEFAULT NULL COMMENT \'Running time for this task\', endtime NUMERIC(32, 9) UNSIGNED DEFAULT NULL COMMENT \'Time task ended\', start_time NUMERIC(32, 9) UNSIGNED DEFAULT NULL COMMENT \'Time task started\', INDEX IDX_680437B63CBA64F2 (judgetaskid), PRIMARY KEY(taskid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'Result of a generic task\' '); + $this->addSql('CREATE TABLE generic_task_output (taskid INT UNSIGNED NOT NULL COMMENT \'Task ID\', output_task LONGBLOB DEFAULT NULL COMMENT \'Output of running the program(DC2Type:blobtext)\', output_error LONGBLOB DEFAULT NULL COMMENT \'Standard error output of the program(DC2Type:blobtext)\', INDEX taskid (taskid), PRIMARY KEY(taskid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'Stores output of generic task\' '); + $this->addSql('ALTER TABLE generic_task ADD CONSTRAINT FK_680437B63CBA64F2 FOREIGN KEY (judgetaskid) REFERENCES judgetask (judgetaskid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE generic_task_output ADD CONSTRAINT FK_6425C7BE46CBEE95 FOREIGN KEY (taskid) REFERENCES generic_task (taskid) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE generic_task DROP FOREIGN KEY FK_680437B63CBA64F2'); + $this->addSql('ALTER TABLE generic_task_output DROP FOREIGN KEY FK_6425C7BE46CBEE95'); + $this->addSql('DROP TABLE generic_task'); + $this->addSql('DROP TABLE generic_task_output'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/src/Controller/API/JudgehostController.php b/webapp/src/Controller/API/JudgehostController.php index e9b184606f..e2cf3b712f 100644 --- a/webapp/src/Controller/API/JudgehostController.php +++ b/webapp/src/Controller/API/JudgehostController.php @@ -8,6 +8,8 @@ use App\Entity\DebugPackage; use App\Entity\Executable; use App\Entity\ExecutableFile; +use App\Entity\GenericTask; +use App\Entity\GenericTaskOutput; use App\Entity\InternalError; use App\Entity\Judgehost; use App\Entity\JudgeTask; @@ -553,6 +555,63 @@ public function addDebugInfo( $this->em->flush(); } + /** + * Add generic task output. + */ + #[IsGranted('ROLE_JUDGEHOST')] + #[Rest\Post('/add-generic-task/{hostname}/{judgeTaskId<\d+>}')] + #[OA\Response(response: 200, description: 'When the task output has been added')] + public function addGenericTaskOutput( + Request $request, + #[OA\PathParameter(description: 'The hostname of the judgehost that wants to add the task output')] + string $hostname, + #[OA\PathParameter(description: 'The ID of the judgetask to add', schema: new OA\Schema(type: 'integer'))] + int $judgeTaskId + ): void { + $judgeTask = $this->em->getRepository(JudgeTask::class)->find($judgeTaskId); + if ($judgeTask === null) { + throw new BadRequestHttpException( + 'Inconsistent data, no judgetask known with judgetaskid = ' . $judgeTaskId . '.'); + } + + $required = ['generic_task']; + foreach ($required as $argument) { + if (!$request->request->has($argument)) { + throw new BadRequestHttpException( + sprintf("Argument '%s' is mandatory", $argument)); + } + } + + $judgehost = $this->em->getRepository(Judgehost::class)->findOneBy(['hostname' => $hostname]); + if (!$judgehost) { + throw new BadRequestHttpException("Who are you and why are you sending us any data?"); + } + + $genericTask = $judgeTask->getGenericTasks(); + if (count($genericTask) === 0) { + $genericTask = new GenericTask(); + $genericTask + ->setJudgeTask($judgeTask) + ->setStarttime($judgeTask->getStarttime()) + ->setEndtime(Utils::now()); + $genericTaskOutput = new GenericTaskOutput(); + $genericTaskOutput->setGenericTask($genericTask); + $genericTask->setOutput($genericTaskOutput); + $this->em->persist($genericTask); + $this->em->persist($genericTaskOutput); + } elseif (count($genericTask) !== 1) { + throw new BadRequestHttpException("There should be only one generic task for this judgetask."); + } else { + $genericTask = $genericTask->first(); + } + + $genericTask->setEndtime(Utils::now()); + $outputTask = base64_decode($request->request->get('generic_task')); + $genericTaskOutput = $genericTask->getOutput(); + $genericTaskOutput->setOutputTask($outputTask); + $this->em->flush(); + } + /** * Add one JudgingRun. When relevant, finalize the judging. * @throws DBALException @@ -1238,7 +1297,7 @@ public function getFilesAction( return match ($type) { 'source' => $this->getSourceFiles($id), 'testcase' => $this->getTestcaseFiles($id), - 'compare', 'compile', 'debug', 'run' => $this->getExecutableFiles($id), + 'compare', 'compile', 'debug', 'run', 'generic_task' => $this->getExecutableFiles($id), default => throw new BadRequestHttpException('Unknown type requested.'), }; } @@ -1581,9 +1640,10 @@ public function getJudgeTasksAction(Request $request): array ->andWhere('jt.judgehost = :judgehost') ->andWhere('jt.starttime IS NULL') ->andWhere('jt.valid = 1') - ->andWhere('jt.type = :type') + ->andWhere('jt.type in (:type)') ->setParameter('judgehost', $judgehost) - ->setParameter('type', JudgeTaskType::DEBUG_INFO) + ->setParameter('type', [JudgeTaskType::DEBUG_INFO, JudgeTaskType::GENERIC_TASK], + ArrayParameterType::STRING) ->addOrderBy('jt.priority') ->addOrderBy('jt.judgetaskid') ->setMaxResults(1) diff --git a/webapp/src/Controller/Jury/ExecutableController.php b/webapp/src/Controller/Jury/ExecutableController.php index e08e1b4afa..6117feadb5 100644 --- a/webapp/src/Controller/Jury/ExecutableController.php +++ b/webapp/src/Controller/Jury/ExecutableController.php @@ -60,6 +60,9 @@ public function indexAction(Request $request): Response ->addOrderBy('e.type', 'ASC') ->addOrderBy('e.execid', 'ASC') ->getQuery()->getResult(); + // PhpStorm doesn't pick this up without, + // based on a dump this is `"c" => App\Entity\Executable`. + /** @var Executable[] $executables */ $executables = array_column($executables, 'executable', 'execid'); $table_fields = [ 'icon' => ['title' => 'type', 'sort' => false], @@ -139,6 +142,9 @@ public function indexAction(Request $request): Response case 'debug': $execdata['icon']['icon'] = 'bug'; break; + case 'generic_task': + $execdata['icon']['icon'] = 'check-double'; + break; case 'run': $execdata['icon']['icon'] = 'person-running'; break; diff --git a/webapp/src/Controller/Jury/JudgehostController.php b/webapp/src/Controller/Jury/JudgehostController.php index 58e9eb103a..6c90e37dfb 100644 --- a/webapp/src/Controller/Jury/JudgehostController.php +++ b/webapp/src/Controller/Jury/JudgehostController.php @@ -4,6 +4,7 @@ use App\Controller\BaseController; use App\Doctrine\DBAL\Types\JudgeTaskType; +use App\Entity\Executable; use App\Entity\Judgehost; use App\Entity\JudgeTask; use App\Entity\Judging; @@ -210,8 +211,19 @@ public function indexAction(Request $request): Response return strnatcasecmp($a['data']['hostname']['value'], $b['data']['hostname']['value']); }); + /** @var Executable[] $executables */ + $executables = $this->em->createQueryBuilder() + ->select('e as executable, e.execid as execid') + ->from(Executable::class, 'e') + ->addOrderBy('e.type', 'ASC') + ->addOrderBy('e.execid', 'ASC') + ->andWhere('e.type = :type') + ->setParameter('type', JudgeTaskType::GENERIC_TASK) + ->getQuery()->getResult(); + $data = [ 'judgehosts' => $judgehosts_table, + 'executables' => $executables, 'table_fields' => $table_fields, 'all_checked_in_recently' => $all_checked_in_recently, 'refresh' => [ @@ -278,7 +290,18 @@ public function viewAction(Request $request, int $judgehostid): Response ->getResult(); } + /** @var Executable[] $executables */ + $executables = $this->em->createQueryBuilder() + ->select('e as executable, e.execid as execid') + ->from(Executable::class, 'e') + ->addOrderBy('e.type', 'ASC') + ->addOrderBy('e.execid', 'ASC') + ->andWhere('e.type = :type') + ->setParameter('type', JudgeTaskType::GENERIC_TASK) + ->getQuery()->getResult(); + $data = [ + 'executables' => $executables, 'judgehost' => $judgehost, 'status' => $status, 'statusIcon' => $statusIcon, @@ -296,6 +319,63 @@ public function viewAction(Request $request, int $judgehostid): Response } } + private function helperGenericTask(string $execid, ?JudgeHost $judgehost = null): void { + $executable = $this->em->getRepository(Executable::class)->findOneBy(['execid' => $execid]); + if (!$executable) { + throw new NotFoundHttpException(sprintf('Executable with ID %s not found', $execid)); + } + + $executable = $executable->getImmutableExecutable(); + + $judgehosts = []; + if ($judgehost) { + $judgehosts[] = $judgehost; + } else { + /** @var Judgehost[] $judgehosts */ + $judgehosts = $this->em->createQueryBuilder() + ->from(Judgehost::class, 'j') + ->select('j') + ->andWhere('j.hidden = 0') + ->getQuery()->getResult(); + } + foreach ($judgehosts as $judgehost) { + $judgeTask = new JudgeTask(); + $judgeTask + ->setType(JudgeTaskType::GENERIC_TASK) + ->setJudgehost($judgehost) + ->setPriority(JudgeTask::PRIORITY_HIGH) + ->setRunScriptId($executable->getImmutableExecId()) + ->setRunConfig(Utils::jsonEncode(['hash' => $executable->getHash()])); + $this->em->persist($judgeTask); + } + $this->em->flush(); + } + + #[IsGranted('ROLE_ADMIN')] + #[Route(path: '/{judgehostid}/request-generic-task/{execid}', name: 'jury_request_judgehost_generic')] + public function requestGenericTaskJudgehost(Request $request, int $judgehostid, string $execid): RedirectResponse + { + $judgehost = $this->em->getRepository(Judgehost::class)->find($judgehostid); + if (!$judgehost) { + throw new NotFoundHttpException(sprintf('Judgehost with ID %d not found', $judgehostid)); + } + + $this->helperGenericTask($execid, $judgehost); + + return $this->redirectToRoute('jury_judgehost', [ + 'judgehostid' => $judgehostid + ]); + } + + // TODO: Does the ordering matter in the file. + #[IsGranted('ROLE_ADMIN')] + #[Route(path: '/request-generic-task/{execid}', name: 'jury_request_generic')] + public function requestGenericTask(Request $request, string $execid): RedirectResponse + { + $this->helperGenericTask($execid); + return $this->redirectToRoute('jury_judgehosts'); + } + /** * @throws DBALException * @throws NoResultException diff --git a/webapp/src/DataFixtures/DefaultData/ExecutableFixture.php b/webapp/src/DataFixtures/DefaultData/ExecutableFixture.php index 87ad229594..b8ffeec871 100644 --- a/webapp/src/DataFixtures/DefaultData/ExecutableFixture.php +++ b/webapp/src/DataFixtures/DefaultData/ExecutableFixture.php @@ -23,10 +23,12 @@ public function load(ObjectManager $manager): void { $data = [ // ID, description, type - ['compare', 'default compare script', 'compare'], - ['full_debug', 'default full debug script', 'debug'], - ['java_javac', 'java_javac', 'compile'], - ['run', 'default run script', 'run'], + ['compare', 'default compare script', 'compare'], + ['full_debug', 'default full debug script', 'debug'], + ['java_javac', 'java_javac', 'compile'], + ['judgehost_info', 'generic information about the judgehost', 'generic_task'], + ['chroot_upgrade', 'upgrade chroot packages', 'generic_task'], + ['run', 'default run script', 'run'], ]; foreach ($data as $item) { diff --git a/webapp/src/Entity/Executable.php b/webapp/src/Entity/Executable.php index 2beff3a635..4c0324f549 100644 --- a/webapp/src/Entity/Executable.php +++ b/webapp/src/Entity/Executable.php @@ -31,7 +31,7 @@ class Executable private ?string $description = null; #[ORM\Column(length: 32, options: ['comment' => 'Type of executable'])] - #[Assert\Choice(['compare', 'compile', 'debug', 'run'])] + #[Assert\Choice(['compare', 'compile', 'debug', 'run', 'generic_task'])] private string $type; #[ORM\OneToOne(targetEntity: ImmutableExecutable::class)] @@ -187,6 +187,9 @@ public function getZipfileContent(string $tempdir): string */ public function checkUsed(array $configScripts): bool { + if ($this->getType() === 'generic_task') { + return true; + } if (in_array($this->execid, $configScripts, true)) { return true; } diff --git a/webapp/src/Entity/GenericTask.php b/webapp/src/Entity/GenericTask.php new file mode 100644 index 0000000000..e9c0c1f86e --- /dev/null +++ b/webapp/src/Entity/GenericTask.php @@ -0,0 +1,167 @@ + 'utf8mb4_unicode_ci', + 'charset' => 'utf8mb4', + 'comment' => 'Result of a generic task', +])] +class GenericTask extends BaseApiEntity +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(options: ['comment' => 'Task ID', 'unsigned' => true])] + #[Serializer\SerializedName('id')] + #[Serializer\Type('string')] + protected int $taskid; + + #[ORM\Column( + nullable: true, + options: ['comment' => 'JudgeTask ID', 'unsigned' => true, 'default' => null] + )] + #[Serializer\Exclude] + private ?int $judgetaskid = null; + + #[ORM\Column( + nullable: true, + options: ['comment' => 'Running time for this task'] + )] + #[Serializer\Exclude] + private string|float|null $runtime = null; + + #[ORM\Column( + type: 'decimal', + precision: 32, + scale: 9, + nullable: true, + options: ['comment' => 'Time task ended', 'unsigned' => true] + )] + #[Serializer\Exclude] + private string|float|null $endtime = null; + + #[ORM\Column( + type: 'decimal', + precision: 32, + scale: 9, + nullable: true, + options: ['comment' => 'Time task started', 'unsigned' => true] + )] + #[Serializer\Exclude] + private string|float|null $startTime = null; + + /** + * @var Collection + * + * We use a OneToMany instead of a OneToOne here, because otherwise this + * relation will always be loaded. See the commit message of commit + * 9e421f96691ec67ed62767fe465a6d8751edd884 for a more elaborate explanation + */ + #[ORM\OneToMany(mappedBy: 'run', targetEntity: GenericTaskOutput::class, cascade: ['persist'], orphanRemoval: true)] + #[Serializer\Exclude] + private Collection $output; + + #[ORM\ManyToOne(inversedBy: 'judging_runs')] + #[ORM\JoinColumn(name: 'judgetaskid', referencedColumnName: 'judgetaskid', onDelete: 'CASCADE')] + #[Serializer\Exclude] + private ?JudgeTask $judgetask = null; + + public function __construct() + { + $this->output = new ArrayCollection(); + } + + public function getTaskid(): int + { + return $this->taskid; + } + + public function setJudgeTaskId(int $judgetaskid): GenericTask + { + $this->judgetaskid = $judgetaskid; + return $this; + } + + public function getJudgeTaskId(): ?int + { + return $this->judgetaskid; + } + + public function getJudgeTask(): ?JudgeTask + { + return $this->judgetask; + } + + public function setJudgeTask(JudgeTask $judgeTask): GenericTask + { + $this->judgetask = $judgeTask; + return $this; + } + + public function setRuntime(string|float $runtime): GenericTask + { + $this->runtime = $runtime; + return $this; + } + + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('run_time')] + #[Serializer\Type('float')] + public function getRuntime(): string|float|null + { + return Utils::roundedFloat($this->runtime); + } + + public function setEndtime(string|float $endtime): GenericTask + { + $this->endtime = $endtime; + return $this; + } + + public function setStarttime(string|float $startTime): GenericTask + { + $this->startTime = $startTime; + return $this; + } + + public function getStarttime(): string|float|null + { + return $this->startTime; + } + + public function getEndtime(): string|float|null + { + return $this->endtime; + } + + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('time')] + #[Serializer\Type('string')] + public function getAbsoluteEndTime(): string + { + return Utils::absTime($this->getEndtime()); + } + + public function setOutput(GenericTaskOutput $output): GenericTask + { + $this->output->clear(); + $this->output->add($output); + $output->setGenericTask($this); + + return $this; + } + + public function getOutput(): ?GenericTaskOutput + { + return $this->output->first() ?: null; + } +} diff --git a/webapp/src/Entity/GenericTaskOutput.php b/webapp/src/Entity/GenericTaskOutput.php new file mode 100644 index 0000000000..6b41392d56 --- /dev/null +++ b/webapp/src/Entity/GenericTaskOutput.php @@ -0,0 +1,75 @@ + 'utf8mb4_unicode_ci', + 'charset' => 'utf8mb4', + 'comment' => 'Stores output of generic task', +])] +#[ORM\Index(columns: ['taskid'], name: 'taskid')] +class GenericTaskOutput +{ + /** + * We use a ManyToOne instead of a OneToOne here, because otherwise the + * reverse of this relation will always be loaded. See the commit message of commit + * 9e421f96691ec67ed62767fe465a6d8751edd884 for a more elaborate explanation + */ + #[ORM\Id] + #[ORM\ManyToOne(inversedBy: 'output')] + #[ORM\JoinColumn(name: 'taskid', referencedColumnName: 'taskid', onDelete: 'CASCADE')] + private GenericTask $task; + + #[ORM\Column( + type: 'blobtext', + nullable: true, + options: ['comment' => 'Output of running the program'] + )] + private ?string $output_task = null; + + #[ORM\Column( + type: 'blobtext', + nullable: true, + options: ['comment' => 'Standard error output of the program'] + )] + private ?string $output_error = null; + + public function setGenericTask(GenericTask $task): GenericTaskOutput + { + $this->task = $task; + return $this; + } + + public function getGenericTask(): GenericTask + { + return $this->task; + } + + public function setOutputTask(?string $outputTask): GenericTaskOutput + { + $this->output_task = $outputTask; + return $this; + } + + public function getOutputTask(): string + { + return $this->output_task; + } + + public function setOutputError(string $outputError): GenericTaskOutput + { + $this->output_error = $outputError; + return $this; + } + + public function getOutputError(): string + { + return $this->output_error; + } +} diff --git a/webapp/src/Entity/JudgeTask.php b/webapp/src/Entity/JudgeTask.php index 513efebee1..e72d9cd762 100644 --- a/webapp/src/Entity/JudgeTask.php +++ b/webapp/src/Entity/JudgeTask.php @@ -161,6 +161,13 @@ public function getSubmitid(): ?int #[Serializer\Exclude] private Collection $judging_runs; + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'judgetask', targetEntity: GenericTask::class)] + #[Serializer\Exclude] + private Collection $generic_tasks; + #[ORM\ManyToOne(inversedBy: 'judgeTasks')] #[ORM\JoinColumn(name: 'versionid', referencedColumnName: 'versionid', onDelete: 'SET NULL')] #[Serializer\Exclude] @@ -169,6 +176,7 @@ public function getSubmitid(): ?int public function __construct() { $this->judging_runs = new ArrayCollection(); + $this->generic_tasks = new ArrayCollection(); } public function getJudgetaskid(): int @@ -375,6 +383,20 @@ public function getJudgingRuns(): Collection return $this->judging_runs; } + public function addGenericTask(GenericTask $genericTask): JudgeTask + { + $this->generic_tasks[] = $genericTask; + return $this; + } + + /** + * @return Collection + */ + public function getGenericTasks(): Collection + { + return $this->generic_tasks; + } + /** * Gets the first judging run for this judgetask. * diff --git a/webapp/templates/jury/judgehost.html.twig b/webapp/templates/jury/judgehost.html.twig index 527fa92b56..d87286f498 100644 --- a/webapp/templates/jury/judgehost.html.twig +++ b/webapp/templates/jury/judgehost.html.twig @@ -67,4 +67,13 @@ {% include 'jury/partials/judgehost_judgings.html.twig' %} +
+

Request generic actions

+ {% for executable in executables %} + + {{ executable.executable.description }} +
+ {% endfor %} +
+ {% endblock %} diff --git a/webapp/templates/jury/judgehosts.html.twig b/webapp/templates/jury/judgehosts.html.twig index 9dca8cd41e..60552be551 100644 --- a/webapp/templates/jury/judgehosts.html.twig +++ b/webapp/templates/jury/judgehosts.html.twig @@ -44,6 +44,37 @@ {{ button(path('jury_judgehost_edit'), 'Edit all judgehosts', 'secondary', 'edit') }} +
+ + + + + + + + + {% for judgehost in judgehosts %} + + {% endfor %} + + + + {% for executable in executables %} + + + + {% for judgehost in judgehosts %} + + {% endfor %} + + {% endfor %} + +
ExecutableJudgehost
All{{ judgehost.data.hostname.value }}
{{ executable.executable.description }} + All + + {{ judgehost.data.hostname.value }} +
+
{%- endif %}