From 3bb5530893fc8f5034e17ff79f4c5607f721f613 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Wed, 23 Aug 2023 14:53:09 +0200 Subject: [PATCH 01/36] Update score and rank of all teams of the contest after rejudging. This makes sure the rank and first to solve of all teams are correctly set. Fixes #2105. --- .../Test/RejudgingFirstToSolveFixture.php | 56 +++++++++++++ webapp/src/Service/RejudgingService.php | 42 +++++++--- .../Unit/Service/RejudgingServiceTest.php | 81 +++++++++++++++++++ 3 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php create mode 100644 webapp/tests/Unit/Service/RejudgingServiceTest.php diff --git a/webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php b/webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php new file mode 100644 index 00000000000..32132653e8b --- /dev/null +++ b/webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php @@ -0,0 +1,56 @@ +getRepository(Team::class)->findOneBy(['name' => 'Example teamname']); + $team2 = (new Team()) + ->setName('Another team') + ->setCategory($team1->getCategory()); + + $manager->persist($team2); + + $submissionData = [ + // team, submittime, result] + [$team1, '2021-01-01 12:34:56', 'correct'], + [$team2, '2021-01-01 12:33:56', 'wrong-answer'], + ]; + + $contest = $manager->getRepository(Contest::class)->findOneBy(['shortname' => 'demo']); + $language = $manager->getRepository(Language::class)->find('cpp'); + $problem = $contest->getProblems()->filter(fn(ContestProblem $problem) => $problem->getShortname() === 'A')->first(); + + foreach ($submissionData as $index => $submissionItem) { + $submission = (new Submission()) + ->setContest($contest) + ->setTeam($submissionItem[0]) + ->setContestProblem($problem) + ->setLanguage($language) + ->setValid(true) + ->setSubmittime(Utils::toEpochFloat($submissionItem[1])); + $judging = (new Judging()) + ->setContest($contest) + ->setStarttime(Utils::toEpochFloat($submissionItem[1])) + ->setEndtime(Utils::toEpochFloat($submissionItem[1]) + 5) + ->setValid(true) + ->setResult($submissionItem[2]); + $judging->setSubmission($submission); + $submission->addJudging($judging); + $manager->persist($submission); + $manager->persist($judging); + $manager->flush(); + } + } +} diff --git a/webapp/src/Service/RejudgingService.php b/webapp/src/Service/RejudgingService.php index 12c098d369d..427a91b96ab 100644 --- a/webapp/src/Service/RejudgingService.php +++ b/webapp/src/Service/RejudgingService.php @@ -251,13 +251,10 @@ public function finishRejudging(Rejudging $rejudging, string $action, ?callable ); // Update caches. - $rowIndex = $submission['cid'] . '-' . $submission['teamid'] . '-' . $submission['probid']; - if (!isset($scoreboardRowsToUpdate[$rowIndex])) { - $scoreboardRowsToUpdate[$rowIndex] = [ - 'cid' => $submission['cid'], - 'teamid' => $submission['teamid'], - 'probid' => $submission['probid'], - ]; + $cid = $submission['cid']; + $probid = $submission['probid']; + if (!isset($scoreboardRowsToUpdate[$cid][$probid])) { + $scoreboardRowsToUpdate[$cid][$probid] = true; } // Update event log. @@ -329,16 +326,35 @@ public function finishRejudging(Rejudging $rejudging, string $action, ?callable } // Now update the scoreboard - foreach ($scoreboardRowsToUpdate as $item) { - ['cid' => $cid, 'teamid' => $teamid, 'probid' => $probid] = $item; + foreach ($scoreboardRowsToUpdate as $cid => $probids) { $contest = $this->em->getRepository(Contest::class)->find($cid); - $team = $this->em->getRepository(Team::class)->find($teamid); - $problem = $this->em->getRepository(Problem::class)->find($probid); - $this->scoreboardService->calculateScoreRow($contest, $team, $problem); + $queryBuilder = $this->em->createQueryBuilder() + ->from(Team::class, 't') + ->select('t') + ->orderBy('t.teamid'); + if (!$contest->isOpenToAllTeams()) { + $queryBuilder + ->leftJoin('t.contests', 'c') + ->join('t.category', 'cat') + ->leftJoin('cat.contests', 'cc') + ->andWhere('c.cid = :cid OR cc.cid = :cid') + ->setParameter('cid', $contest->getCid()); + } + /** @var Team[] $teams */ + $teams = $queryBuilder->getQuery()->getResult(); + foreach ($teams as $team) { + foreach ($probids as $probid) { + $problem = $this->em->getRepository(Problem::class)->find($probid); + $this->scoreboardService->calculateScoreRow($contest, $team, $problem); + } + $this->scoreboardService->updateRankCache($contest, $team); + } } } - $progressReporter(100, $log); + if ($progressReporter !== null) { + $progressReporter(100, $log); + } // Update the rejudging itself. /** @var Rejudging $rejudging */ diff --git a/webapp/tests/Unit/Service/RejudgingServiceTest.php b/webapp/tests/Unit/Service/RejudgingServiceTest.php new file mode 100644 index 00000000000..1b77a275660 --- /dev/null +++ b/webapp/tests/Unit/Service/RejudgingServiceTest.php @@ -0,0 +1,81 @@ +logIn(); + + /** @var RejudgingService $rejudgingService */ + $rejudgingService = static::getContainer()->get(RejudgingService::class); + /** @var ScoreboardService $scoreboardService */ + $scoreboardService = static::getContainer()->get(ScoreboardService::class); + /** @var EntityManagerInterface $entityManager */ + $entityManager = static::getContainer()->get(EntityManagerInterface::class); + + $this->loadFixture(RejudgingFirstToSolveFixture::class); + + $contest = $entityManager->getRepository(Contest::class)->findOneBy(['shortname' => 'demo']); + $team1 = $entityManager->getRepository(Team::class)->findOneBy(['name' => 'Example teamname']); + $team2 = $entityManager->getRepository(Team::class)->findOneBy(['name' => 'Another team']); + $problem = $entityManager->getRepository(Problem::class)->findOneBy(['externalid' => 'hello']); + $contestProblem = $problem->getContestProblems()->first(); + + // Get the initial scoreboard. team 1 should have the FTS for the problem, team 2 shouldn't + foreach ([$team1, $team2] as $team) { + $scoreboardService->calculateScoreRow($contest, $team, $problem); + $scoreboardService->calculateTeamRank($contest, $team); + } + + $scoreboard = $scoreboardService->getScoreboard($contest, true); + + static::assertTrue($scoreboard->solvedFirst($team1, $contestProblem)); + static::assertFalse($scoreboard->solvedFirst($team2, $contestProblem)); + + // Now create a rejudging: it will apply a new judging for the submission of $team2 that is correct + $rejudging = (new Rejudging()) + ->setStarttime(Utils::now()) + ->setReason(__METHOD__); + $submissionToToggle = $team2->getSubmissions()->first(); + $existingJudging = $submissionToToggle->getJudgings()->first(); + $newJudging = (new Judging()) + ->setContest($contest) + ->setStarttime($existingJudging->getStarttime()) + ->setEndtime($existingJudging->getEndtime()) + ->setRejudging($rejudging) + ->setValid(false) + ->setResult('correct'); + $newJudging->setSubmission($submissionToToggle); + $submissionToToggle->addJudging($newJudging); + $submissionToToggle->setRejudging($rejudging); + $entityManager->persist($rejudging); + $entityManager->persist($newJudging); + $entityManager->flush(); + + // Now apply the rejudging + $rejudgingService->finishRejudging($rejudging, RejudgingService::ACTION_APPLY); + + // Finally, get the scoreboard again and test if the first to solve changed + $scoreboard = $scoreboardService->getScoreboard($contest, true); + + static::assertFalse($scoreboard->solvedFirst($team1, $contestProblem)); + static::assertTrue($scoreboard->solvedFirst($team2, $contestProblem)); + } +} From 9f90961d1886312d8fb712264d7e3552e94223dc Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Mon, 28 Aug 2023 20:10:59 +0200 Subject: [PATCH 02/36] =?UTF-8?q?Disable=20scoreboard=20refresh=20for=20st?= =?UTF-8?q?atic=20scoreboard=20after=20contest=20has=20be=E2=80=A6=20(#207?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Disable scoreboard refresh for static scoreboard after contest has been unfrozen. * Address Nickys review comments. --- webapp/public/js/domjudge.js | 14 +++++++++----- webapp/src/Service/ScoreboardService.php | 4 ++++ webapp/templates/partials/scoreboard.html.twig | 2 +- webapp/templates/public/scoreboard.html.twig | 6 +++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/webapp/public/js/domjudge.js b/webapp/public/js/domjudge.js index bfc2de7a9b8..bd1b254320d 100644 --- a/webapp/public/js/domjudge.js +++ b/webapp/public/js/domjudge.js @@ -85,7 +85,7 @@ function sendNotification(title, options = {}) if ( getCookie('domjudge_notify')==1 ) { var not = new Notification(title, options); if ( link!==null ) { - not.onclick = function() { + not.onclick = function() { window.location.href = link; }; } @@ -435,10 +435,14 @@ function processAjaxResponse(jqXHR, data) { window.location = jqXHR.getResponseHeader('X-Login-Page'); } else { var newCurrentContest = jqXHR.getResponseHeader('X-Current-Contest'); - var dataCurrentContest = $('[data-current-contest]').data('current-contest').toString(); - - // If the contest ID changed from another tab, reload or whole page - if (dataCurrentContest !== undefined && newCurrentContest !== dataCurrentContest) { + var dataCurrentContest = $('[data-current-contest]').data('current-contest'); + var refreshStop = $('[data-ajax-refresh-stop]').data('ajax-refresh-stop'); + + // Reload the whole page if + // - we were signaled to stop refreshing, or + // - if the contest ID changed from another tab. + if ((refreshStop !== undefined && refreshStop.toString() === "1") + || (dataCurrentContest !== undefined && newCurrentContest !== dataCurrentContest.toString())) { window.location.reload(); return; } diff --git a/webapp/src/Service/ScoreboardService.php b/webapp/src/Service/ScoreboardService.php index 5352292e78a..5510f179334 100644 --- a/webapp/src/Service/ScoreboardService.php +++ b/webapp/src/Service/ScoreboardService.php @@ -877,6 +877,10 @@ public function getScoreboardTwigData( ], 'static' => $static, ]; + if ($static && $contest && $contest->getFreezeData()->showFinal()) { + unset($data['refresh']); + $data['refreshstop'] = true; + } if ($contest) { if ($request && $response) { diff --git a/webapp/templates/partials/scoreboard.html.twig b/webapp/templates/partials/scoreboard.html.twig index 40e3811bbe8..e5f7de0b15d 100644 --- a/webapp/templates/partials/scoreboard.html.twig +++ b/webapp/templates/partials/scoreboard.html.twig @@ -17,7 +17,7 @@ {% endif %} -
+
{{ current_contest.name }} diff --git a/webapp/templates/public/scoreboard.html.twig b/webapp/templates/public/scoreboard.html.twig index 0bd79e4d7a6..953d0d83fc6 100644 --- a/webapp/templates/public/scoreboard.html.twig +++ b/webapp/templates/public/scoreboard.html.twig @@ -26,7 +26,7 @@ initFavouriteTeams(); pinScoreheader(); - {% if static %} + {% if static and refresh is defined %} function disableRefreshOnModal() { $('.modal') .on('show.bs.modal', function () { @@ -51,12 +51,12 @@ initFavouriteTeams(); pinScoreheader(); - {% if static %} + {% if static and refresh is defined %} disableRefreshOnModal(); {% endif %} }; - {% if static %} + {% if static and refresh is defined %} disableRefreshOnModal(); {% endif %} }); From 5e32eedba8df91a7d168c2ca301e1665a8d48339 Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Wed, 30 Aug 2023 19:00:25 +0200 Subject: [PATCH 03/36] Replace inline style with internal Better would be in an actual stylesheet but as we generate these dynamicly this is the best option for now. For the other one its minor enough for the basic stylesheet. --- webapp/public/style_domjudge.css | 5 +++++ webapp/templates/partials/scoreboard_table.html.twig | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/webapp/public/style_domjudge.css b/webapp/public/style_domjudge.css index 8884a7441e6..22e03c303df 100644 --- a/webapp/public/style_domjudge.css +++ b/webapp/public/style_domjudge.css @@ -630,3 +630,8 @@ blockquote { padding-left: .5em; color: darkgrey; } + +.category-best { + margin-right: 2em; + font-weight: normal; +} diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index 84fcb201384..7e2ab040239 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -201,7 +201,7 @@ {% if usedCategories | length > 1 and scoreboard.bestInCategory(score.team, limitToTeamIds) %} - + {{ score.team.category.name }} {% endif %} @@ -330,7 +330,7 @@ {% for category in scoreboard.categories | filter(category => usedCategories[category.categoryid] is defined) %} - + {% set link = null %} {% if jury %} From 8b7fd31e5474bc81e4d8166a4ad55992a681b872 Mon Sep 17 00:00:00 2001 From: MCJ Vasseur <14887731+vmcj@users.noreply.github.com> Date: Wed, 30 Aug 2023 20:10:22 +0200 Subject: [PATCH 04/36] Judgedaemon gather extensions based on domserver config (#2130) * Base file extensions in judgedaemon on language config in domserver We still had the .C extension here which has been removed from the domserver for quite a while. The old values are kept if some languages are not available in the API of the domserver. * Don't update config for every internal fetched --- judge/judgedaemon.main.php | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/judge/judgedaemon.main.php b/judge/judgedaemon.main.php index 367b92b4cd9..999bc3bfdbd 100644 --- a/judge/judgedaemon.main.php +++ b/judge/judgedaemon.main.php @@ -327,6 +327,7 @@ function fetch_executable_internal( $execid, $hash ]); + global $langexts; $execdeploypath = $execdir . '/.deployed'; $execbuilddir = $execdir . '/build'; $execbuildpath = $execbuilddir . '/build'; @@ -379,12 +380,6 @@ function fetch_executable_internal( $do_compile = false; } else { // detect lang and write build file - $langexts = [ - 'c' => ['c'], - 'cpp' => ['cpp', 'C', 'cc'], - 'java' => ['java'], - 'py' => ['py'], - ]; $buildscript = "#!/bin/sh\n\n"; $execlang = false; $source = ""; @@ -642,6 +637,21 @@ function fetch_executable_internal( // Populate the DOMjudge configuration initially djconfig_refresh(); +// Prepopulate default language extensions, afterwards update based on domserver config +$langexts = [ + 'c' => ['c'], + 'cpp' => ['cpp', 'C', 'cc'], + 'java' => ['java'], + 'py' => ['py'], +]; +$domserver_languages = dj_json_decode(request('languages', 'GET')); +foreach ($domserver_languages as $language) { + $id = $language['id']; + if (key_exists($id, $langexts)) { + $langexts[$id] = $language['extensions']; + } +} + // Constantly check API for unjudged submissions $endpointIDs = array_keys($endpoints); $currentEndpoint = 0; From e43b1455be15b74b1e28010470c63f7c427ec708 Mon Sep 17 00:00:00 2001 From: Kuan-Wei Chiu Date: Sat, 2 Sep 2023 20:44:56 +0800 Subject: [PATCH 05/36] Fix typo in documentation Fixed a typo in the word 'interfae' by replacing it with 'interface'. --- doc/manual/config-advanced.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/config-advanced.rst b/doc/manual/config-advanced.rst index 52c9dcd270f..aff385bd575 100644 --- a/doc/manual/config-advanced.rst +++ b/doc/manual/config-advanced.rst @@ -7,7 +7,7 @@ DOMjudge can optionally present country flags, affiliation logos, team pictures and a page-wide banner on the public interface. You can place the images under the path `public/images/` (see -the Config checker in the admin interfae for the full filesystem +the Config checker in the admin interface for the full filesystem path of your installation) as follows: - *Country flags* are shown when the ``show_flags`` configuration option From d14bb7b5eedd31b8eeac771ba7ac92fabecbe676 Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Sat, 2 Sep 2023 16:53:54 +0200 Subject: [PATCH 06/36] Fix requesting of full judging output. (#2138) Requesting the full judging output is a `debug_info` judge task type which does not have a run config set. See #2031 where the code in question was introduced to display historical time limits. --- webapp/src/Controller/Jury/SubmissionController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webapp/src/Controller/Jury/SubmissionController.php b/webapp/src/Controller/Jury/SubmissionController.php index b99110d4340..83cebaef3ca 100644 --- a/webapp/src/Controller/Jury/SubmissionController.php +++ b/webapp/src/Controller/Jury/SubmissionController.php @@ -230,10 +230,12 @@ public function viewAction( ->from(JudgeTask::class, 'jt', 'jt.jobid') ->select('jt') ->andWhere('jt.jobid IN (:jobIds)') + ->andWhere('jt.type = :type') ->setParameter( 'jobIds', array_map(static fn(Judging $judging) => $judging->getJudgingid(), $judgings) ) + ->setParameter('type', JudgeTaskType::JUDGING_RUN) ->getQuery() ->getResult(); $timelimits = array_map(function (JudgeTask $task) { From 078f1c724281165952d9e2d63120a24d80996403 Mon Sep 17 00:00:00 2001 From: Kuan-Wei Chiu Date: Sun, 3 Sep 2023 07:06:09 +0800 Subject: [PATCH 07/36] Fix typo in documentation Fixed a typo in the word 'interfae' by replacing it with 'interface'. --- doc/manual/config-advanced.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/config-advanced.rst b/doc/manual/config-advanced.rst index aff385bd575..72b8016740c 100644 --- a/doc/manual/config-advanced.rst +++ b/doc/manual/config-advanced.rst @@ -405,7 +405,7 @@ Clearing the PHP/Symfony cache ------------------------------ Some operations require you to clear the PHP/Symfony cache. To do this, execute -the `webapp/bin/console` (see the Config checker in the admin interfae for the +the `webapp/bin/console` (see the Config checker in the admin interface for the full filesystem path of your installation) binary with the `cache:clear` subcommand:: webapp/bin/console cache:clear From f98942688042bb1a7c8afd04dd94819f5bf7824d Mon Sep 17 00:00:00 2001 From: MCJ Vasseur <14887731+vmcj@users.noreply.github.com> Date: Sun, 3 Sep 2023 10:08:43 +0200 Subject: [PATCH 08/36] Add option to hide nonloggedin/nonsubmitted teams on the scoreboard (#2134) * Add option to hide nonloggedin/nonsubmitted teams on the scoreboard Done as a config option for the following 2 cases: - Normally you want a team which has logged on on the scoreboard - In case of the autologin tool of GEHACK/Nicky the user would also login without interaction of the actual team * Allow API logon as the submit client uses it Alternative would be to UNION over teams which submitted and teams which did a GUI logon but that is more expensive. --- etc/db-config.yaml | 9 +++++++++ webapp/src/Service/ScoreboardService.php | 13 +++++++++++++ .../Unit/Integration/ScoreboardIntegrationTest.php | 11 ++++++----- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/etc/db-config.yaml b/etc/db-config.yaml index d5f431f7649..f74df89c527 100644 --- a/etc/db-config.yaml +++ b/etc/db-config.yaml @@ -269,6 +269,15 @@ default_value: false public: true description: Show canonical compiler and runner version on the team pages. + - name: show_teams_on_scoreboard + type: int + default_value: 0 + public: true + description: Show teams on the scoreboard? + options: + 0: Always + 1: After login + 2: After first submission - category: Authentication description: Options related to authentication. items: diff --git a/webapp/src/Service/ScoreboardService.php b/webapp/src/Service/ScoreboardService.php index 5510f179334..0856f6064ee 100644 --- a/webapp/src/Service/ScoreboardService.php +++ b/webapp/src/Service/ScoreboardService.php @@ -32,6 +32,10 @@ class ScoreboardService { + final public const SHOW_TEAM_ALWAYS = 0; + final public const SHOW_TEAM_AFTER_LOGIN = 1; + final public const SHOW_TEAM_AFTER_SUBMIT = 2; + public function __construct( protected readonly EntityManagerInterface $em, protected readonly DOMJudgeService $dj, @@ -944,8 +948,17 @@ protected function getTeams(Contest $contest, bool $jury = false, Filter $filter ->setParameter('cid', $contest->getCid()); } + $show_filter = $this->config->get('show_teams_on_scoreboard'); if (!$jury) { $queryBuilder->andWhere('tc.visible = 1'); + if ($show_filter === self::SHOW_TEAM_AFTER_LOGIN) { + $queryBuilder + ->join('t.users', 'u', Join::WITH, 'u.last_login IS NOT NULL OR u.last_api_login IS NOT NULL'); + } elseif ($show_filter === self::SHOW_TEAM_AFTER_SUBMIT) { + $queryBuilder + ->join('t.submissions', 's', Join::WITH, 's.contest = :cid') + ->setParameter('cid', $contest->getCid()); + } } if ($filter) { diff --git a/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php b/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php index ba3e8580e5c..5be72912cb6 100644 --- a/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php +++ b/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php @@ -65,11 +65,12 @@ protected function setUp(): void // Default configuration values: $this->configValues = [ - 'verification_required' => false, - 'compile_penalty' => false, - 'penalty_time' => 20, - 'score_in_seconds' => false, - 'data_source' => 0, + 'verification_required' => false, + 'compile_penalty' => false, + 'penalty_time' => 20, + 'score_in_seconds' => false, + 'data_source' => 0, + 'show_teams_on_scoreboard' => 0, ]; $this->config = $this->createMock(ConfigurationService::class); From 99bc2c38028c56d149d14e54a6c27e9b10ff4ac0 Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Sun, 3 Sep 2023 12:46:21 +0200 Subject: [PATCH 09/36] Construct ZIP files while uploading executables. (#2139) This is an improvement on top of #2129, since you do not need to check in ZIP files into a ccsconfig repository, but directories which are zipped up on the fly. This makes changing and reviewing changes much easier. --- misc-tools/configure-domjudge.in | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/misc-tools/configure-domjudge.in b/misc-tools/configure-domjudge.in index 360f783d5e1..6fc6dabf546 100755 --- a/misc-tools/configure-domjudge.in +++ b/misc-tools/configure-domjudge.in @@ -17,6 +17,7 @@ import json import os.path import requests import requests.utils +import shutil import sys from typing import List, Set @@ -101,11 +102,16 @@ else: if os.path.exists('executables'): executables = [] for file in os.listdir('executables'): - if file.endswith(".zip"): - executables.append(file[:-4]) - if executables and dj_utils.confirm('Upload language executables (found: ' + ','.join(executables) + ')?', False): + if os.path.isdir(f'executables/{file}'): + executables.append(file) + shutil.make_archive(f'executables/{file}', 'zip', f'executables/{file}') + + if executables: + if dj_utils.confirm('Upload language executables (found: ' + ','.join(executables) + ')?', False): + for langid in executables: + dj_utils.upload_file(f'languages/{langid}/executable', 'executable', f'executables/{langid}.zip') for langid in executables: - dj_utils.upload_file(f'languages/{langid}/executable', 'executable', f'executables/{langid}.zip') + os.remove(f'executables/{langid}.zip') if os.path.exists('languages.json'): From bf1bd430ae016ea77fb6619d0f6491aa258e55d5 Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Sun, 3 Sep 2023 21:20:51 +0200 Subject: [PATCH 10/36] Deal with empty countries in filter. --- webapp/templates/partials/scoreboard.html.twig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webapp/templates/partials/scoreboard.html.twig b/webapp/templates/partials/scoreboard.html.twig index e5f7de0b15d..cdfa19de114 100644 --- a/webapp/templates/partials/scoreboard.html.twig +++ b/webapp/templates/partials/scoreboard.html.twig @@ -140,8 +140,10 @@
From aa7022cad287ba4b2d1423849736afa08d5e9c0f Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Mon, 18 Sep 2023 20:36:24 +0200 Subject: [PATCH 11/36] Add option to filter submissions which are currently judging. (#2143) * Add option to display submissions which are currently judging. * Remove dump --- webapp/src/Controller/Jury/SubmissionController.php | 5 ++++- webapp/src/Service/SubmissionService.php | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/webapp/src/Controller/Jury/SubmissionController.php b/webapp/src/Controller/Jury/SubmissionController.php index 83cebaef3ca..406440daffe 100644 --- a/webapp/src/Controller/Jury/SubmissionController.php +++ b/webapp/src/Controller/Jury/SubmissionController.php @@ -72,7 +72,7 @@ public function indexAction( #[MapQueryParameter(name: 'view')] ?string $viewFromRequest = null, ): Response { - $viewTypes = [0 => 'newest', 1 => 'unverified', 2 => 'unjudged', 3 => 'all']; + $viewTypes = [0 => 'newest', 1 => 'unverified', 2 => 'unjudged', 3 => 'judging', 4 => 'all']; $view = 0; if (($submissionViewCookie = $this->dj->getCookie('domjudge_submissionview')) && isset($viewTypes[$submissionViewCookie])) { @@ -101,6 +101,9 @@ public function indexAction( if ($viewTypes[$view] == 'unjudged') { $restrictions['judged'] = 0; } + if ($viewTypes[$view] == 'judging') { + $restrictions['judging'] = 1; + } $contests = $this->dj->getCurrentContests(); if ($contest = $this->dj->getCurrentContest()) { diff --git a/webapp/src/Service/SubmissionService.php b/webapp/src/Service/SubmissionService.php index 789a3e0a865..73f656e9e56 100644 --- a/webapp/src/Service/SubmissionService.php +++ b/webapp/src/Service/SubmissionService.php @@ -149,6 +149,13 @@ public function getSubmissionList( $queryBuilder->andWhere('j.result IS NULL OR j.endtime IS NULL'); } } + if (isset($restrictions['judging'])) { + if ($restrictions['judging']) { + $queryBuilder->andWhere('j.starttime IS NOT NULL AND j.result IS NULL'); + } else { + $queryBuilder->andWhere('j.starttime IS NULL OR j.result IS NOT NULL'); + } + } if (isset($restrictions['externally_judged'])) { if ($restrictions['externally_judged']) { From a88ba4c088d2759853e97307021bb48ffdfae6f6 Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Mon, 18 Sep 2023 20:37:42 +0200 Subject: [PATCH 12/36] Double hard wall time limit for interactive problems. (#2144) This accounts for wall time spent in the validator. We may likely want to make this configurable in the future. The current factor is under the assumption that the validator has to do approximately the same amount of work wall-time wise as the submission. --- judge/judgedaemon.main.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/judge/judgedaemon.main.php b/judge/judgedaemon.main.php index 999bc3bfdbd..f5ac190a3b6 100644 --- a/judge/judgedaemon.main.php +++ b/judge/judgedaemon.main.php @@ -1350,10 +1350,6 @@ function judge(array $judgeTask): bool } // do the actual test-run - $hardtimelimit = $run_config['time_limit'] + - overshoot_time($run_config['time_limit'], $overshoot); - - $combined_run_compare = $compare_config['combined_run_compare']; [$run_runpath, $error] = fetch_executable( $workdirpath, @@ -1383,6 +1379,16 @@ function judge(array $judgeTask): bool } } + $hardtimelimit = $run_config['time_limit'] + + overshoot_time($run_config['time_limit'], $overshoot); + if ($combined_run_compare) { + // This accounts for wall time spent in the validator. We may likely + // want to make this configurable in the future. The current factor is + // under the assumption that the validator has to do approximately the + // same amount of work wall-time wise as the submission. + $hardtimelimit *= 2; + } + // While we already set those above to likely the same values from the // compile config, we do set them again from the compare config here. putenv('SCRIPTTIMELIMIT=' . $compare_config['script_timelimit']); From 2be71002b050e04a6ba9164e258d93da06344b58 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Tue, 19 Sep 2023 11:23:27 -0400 Subject: [PATCH 13/36] Use array_keys to actually get the problem ID's when recalculating the scoreboard. --- webapp/src/Service/RejudgingService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/src/Service/RejudgingService.php b/webapp/src/Service/RejudgingService.php index 427a91b96ab..cc1a7db5cc6 100644 --- a/webapp/src/Service/RejudgingService.php +++ b/webapp/src/Service/RejudgingService.php @@ -327,6 +327,7 @@ public function finishRejudging(Rejudging $rejudging, string $action, ?callable // Now update the scoreboard foreach ($scoreboardRowsToUpdate as $cid => $probids) { + $probids = array_keys($probids); $contest = $this->em->getRepository(Contest::class)->find($cid); $queryBuilder = $this->em->createQueryBuilder() ->from(Team::class, 't') From 9001fb0d08755f80c826dffc2c11f0a349883a34 Mon Sep 17 00:00:00 2001 From: Andreas Kohn Date: Wed, 20 Sep 2023 22:10:48 +0200 Subject: [PATCH 14/36] Make sure all output goes to stderr --- judge/create_cgroups.in | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/judge/create_cgroups.in b/judge/create_cgroups.in index 1f9471d2844..efa71a26339 100755 --- a/judge/create_cgroups.in +++ b/judge/create_cgroups.in @@ -10,7 +10,8 @@ JUDGEHOSTUSER=@DOMJUDGE_USER@ CGROUPBASE=@judgehost_cgroupdir@ print_cgroup_instruction () { - echo "" + echo "$1" >&2 + echo "" >&2 echo "To fix this, please make the following changes: 1. In /etc/default/grub, add 'cgroup_enable=memory swapaccount=1' to GRUB_CMDLINE_LINUX_DEFAULT. On modern distros (e.g. Debian bullseye and Ubuntu Jammy Jellyfish) which have cgroup v2 enabled by default, @@ -24,16 +25,14 @@ for i in cpuset memory; do mkdir -p $CGROUPBASE/$i if [ ! -d $CGROUPBASE/$i/ ]; then if ! mount -t cgroup -o$i $i $CGROUPBASE/$i/; then - echo "Error: Can not mount $i cgroup. Probably cgroup support is missing from running kernel. Unable to continue." - print_cgroup_instruction + print_cgroup_instruction "Error: Can not mount $i cgroup. Probably cgroup support is missing from running kernel. Unable to continue." fi fi mkdir -p $CGROUPBASE/$i/domjudge done if [ ! -f $CGROUPBASE/memory/memory.limit_in_bytes ] || [ ! -f $CGROUPBASE/memory/memory.memsw.limit_in_bytes ]; then - echo "Error: cgroup support missing memory features in running kernel. Unable to continue." - print_cgroup_instruction + print_cgroup_instruction "Error: cgroup support missing memory features in running kernel. Unable to continue." fi chown -R $JUDGEHOSTUSER $CGROUPBASE/*/domjudge From 2c23c597681b9f158cae4f037371b24e441baf4c Mon Sep 17 00:00:00 2001 From: Andreas Kohn Date: Wed, 20 Sep 2023 22:12:53 +0200 Subject: [PATCH 15/36] Remove unneeded newline --- judge/create_cgroups.in | 1 - 1 file changed, 1 deletion(-) diff --git a/judge/create_cgroups.in b/judge/create_cgroups.in index efa71a26339..16190991f2b 100755 --- a/judge/create_cgroups.in +++ b/judge/create_cgroups.in @@ -11,7 +11,6 @@ CGROUPBASE=@judgehost_cgroupdir@ print_cgroup_instruction () { echo "$1" >&2 - echo "" >&2 echo "To fix this, please make the following changes: 1. In /etc/default/grub, add 'cgroup_enable=memory swapaccount=1' to GRUB_CMDLINE_LINUX_DEFAULT. On modern distros (e.g. Debian bullseye and Ubuntu Jammy Jellyfish) which have cgroup v2 enabled by default, From 251732b6293fcfef09b230cff257789eaa35431f Mon Sep 17 00:00:00 2001 From: Andreas Kohn Date: Thu, 21 Sep 2023 13:11:41 +0200 Subject: [PATCH 16/36] Rename function to match its (changed) behavior Suggested-by: @eldering --- judge/create_cgroups.in | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/judge/create_cgroups.in b/judge/create_cgroups.in index 16190991f2b..fbfe48898be 100755 --- a/judge/create_cgroups.in +++ b/judge/create_cgroups.in @@ -9,7 +9,7 @@ JUDGEHOSTUSER=@DOMJUDGE_USER@ CGROUPBASE=@judgehost_cgroupdir@ -print_cgroup_instruction () { +cgroup_error_and_usage () { echo "$1" >&2 echo "To fix this, please make the following changes: 1. In /etc/default/grub, add 'cgroup_enable=memory swapaccount=1' to GRUB_CMDLINE_LINUX_DEFAULT. @@ -24,14 +24,14 @@ for i in cpuset memory; do mkdir -p $CGROUPBASE/$i if [ ! -d $CGROUPBASE/$i/ ]; then if ! mount -t cgroup -o$i $i $CGROUPBASE/$i/; then - print_cgroup_instruction "Error: Can not mount $i cgroup. Probably cgroup support is missing from running kernel. Unable to continue." + cgroup_error_and_usage "Error: Can not mount $i cgroup. Probably cgroup support is missing from running kernel. Unable to continue." fi fi mkdir -p $CGROUPBASE/$i/domjudge done if [ ! -f $CGROUPBASE/memory/memory.limit_in_bytes ] || [ ! -f $CGROUPBASE/memory/memory.memsw.limit_in_bytes ]; then - print_cgroup_instruction "Error: cgroup support missing memory features in running kernel. Unable to continue." + cgroup_error_and_usage "Error: cgroup support missing memory features in running kernel. Unable to continue." fi chown -R $JUDGEHOSTUSER $CGROUPBASE/*/domjudge From faa365690ce2abacfedbbcec1e6384ec6ea86749 Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Sun, 17 Sep 2023 11:00:59 +0200 Subject: [PATCH 17/36] Show executable IDs as code tags --- webapp/public/style_jury.css | 5 +++++ webapp/src/Controller/Jury/ExecutableController.php | 1 + 2 files changed, 6 insertions(+) diff --git a/webapp/public/style_jury.css b/webapp/public/style_jury.css index 908b2a7088d..fe8e845e580 100644 --- a/webapp/public/style_jury.css +++ b/webapp/public/style_jury.css @@ -197,3 +197,8 @@ table.submissions-table { .devmode-icon { color: white; } + +.execid { + font-family: monospace; +} + diff --git a/webapp/src/Controller/Jury/ExecutableController.php b/webapp/src/Controller/Jury/ExecutableController.php index 26c0b10ef7e..11cf995c1e6 100644 --- a/webapp/src/Controller/Jury/ExecutableController.php +++ b/webapp/src/Controller/Jury/ExecutableController.php @@ -72,6 +72,7 @@ public function indexAction(Request $request): Response $execdata[$k] = ['value' => $propertyAccessor->getValue($e, $k)]; } } + $execdata['execid']['cssclass'] = 'execid'; if ($this->isGranted('ROLE_ADMIN')) { $execactions[] = [ From f9ccdfd674981c0f45031829a94b730b8976d82d Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Sun, 17 Sep 2023 21:54:14 +0200 Subject: [PATCH 18/36] Show type of executable with fontawesome icon The type is now removed as column from the table. --- .../Controller/Jury/ExecutableController.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/webapp/src/Controller/Jury/ExecutableController.php b/webapp/src/Controller/Jury/ExecutableController.php index 11cf995c1e6..223f3995c24 100644 --- a/webapp/src/Controller/Jury/ExecutableController.php +++ b/webapp/src/Controller/Jury/ExecutableController.php @@ -56,6 +56,7 @@ public function indexAction(Request $request): Response ->getQuery()->getResult(); $executables = array_column($executables, 'executable', 'execid'); $table_fields = [ + 'icon' => ['title' => 'type', 'sort' => false], 'execid' => ['title' => 'ID', 'sort' => true,], 'type' => ['title' => 'type', 'sort' => true,], 'description' => ['title' => 'description', 'sort' => true,], @@ -73,6 +74,23 @@ public function indexAction(Request $request): Response } } $execdata['execid']['cssclass'] = 'execid'; + $type = $execdata['type']['value']; + switch ($type) { + case 'compare': + $execdata['icon']['icon'] = 'code-compare'; + break; + case 'compile': + $execdata['icon']['icon'] = 'language'; + break; + case 'debug': + $execdata['icon']['icon'] = 'bug'; + break; + case 'run': + $execdata['icon']['icon'] = 'person-running'; + break; + default: + $execdata['icon']['icon'] = 'question'; + } if ($this->isGranted('ROLE_ADMIN')) { $execactions[] = [ @@ -103,6 +121,8 @@ public function indexAction(Request $request): Response 'link' => $this->generateUrl('jury_executable', ['execId' => $e->getExecid()]), ]; } + // This is replaced with the icon. + unset($table_fields['type']); return $this->render('jury/executables.html.twig', [ 'executables' => $executables_table, 'table_fields' => $table_fields, From a12c2604f98036297f3ac336971eb584e367168a Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Mon, 25 Sep 2023 19:55:04 +0200 Subject: [PATCH 19/36] Use correct printf format specifier for filesize. (#2158) The default value for filesize for scripts is 2.5GB, so more than 2^31 bytes, previously this caused a message like ``` /home/sitowert/domjudge/bin/runguard [56839 @ 0.001495]: verbose: setting filesize limit to -1610612736 bytes ``` After this commit, it is: ``` /home/sitowert/domjudge/bin/runguard [56839 @ 0.001521]: verbose: setting filesize limit to 2684354560 bytes ``` --- judge/runguard.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/judge/runguard.c b/judge/runguard.c index a1097b8b7b1..e1dc83f14d6 100644 --- a/judge/runguard.c +++ b/judge/runguard.c @@ -799,7 +799,7 @@ void setrestrictions() setlim(STACK); if ( filesize!=RLIM_INFINITY ) { - verbose("setting filesize limit to %d bytes",(int)filesize); + verbose("setting filesize limit to %lu bytes",filesize); lim.rlim_cur = lim.rlim_max = filesize; setlim(FSIZE); } From 0c5bf5eef9f854804d9a8548d436c0618d2b0458 Mon Sep 17 00:00:00 2001 From: Dominic Canora Date: Fri, 29 Sep 2023 01:52:05 +0000 Subject: [PATCH 20/36] Check if category exists using externalid instead of name --- webapp/src/DataFixtures/DefaultData/TeamCategoryFixture.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/DataFixtures/DefaultData/TeamCategoryFixture.php b/webapp/src/DataFixtures/DefaultData/TeamCategoryFixture.php index 106452f3394..630ba2f216e 100644 --- a/webapp/src/DataFixtures/DefaultData/TeamCategoryFixture.php +++ b/webapp/src/DataFixtures/DefaultData/TeamCategoryFixture.php @@ -23,7 +23,7 @@ public function load(ObjectManager $manager): void ]; foreach ($data as $item) { - if (!($category = $manager->getRepository(TeamCategory::class)->findOneBy(['name' => $item[0]]))) { + if (!($category = $manager->getRepository(TeamCategory::class)->findOneBy(['externalid' => $item[4]]))) { $category = (new TeamCategory()) ->setName($item[0]) ->setSortorder($item[1]) From 85e8f9e85c7e386eeb66786209006c75fbc33cd8 Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Sat, 30 Sep 2023 13:05:34 +0200 Subject: [PATCH 21/36] Disarm timer in runguard after child has exited. (#2157) If we would not disarm the timer, there is a possibility that the timer sends us a SIGALRM while we are still busy with cleaning the sandbox up. What you observe in these cases is a judging with wall time well below the time limit is judged as TLE, e.g. ``` Timelimit exceeded. runtime: 0.288s cpu, 0.302s wall memory used: 26066944 bytes ********** runguard stderr follows ********** /opt/domjudge/bin/runguard: warning: timelimit exceeded (hard wall time): aborting command ``` In practice, we saw the behavior happening when running many judgedaemons and domserver on a single machine while rejudging the whole contest (i.e. under quite high load). In that case, the call `cgroup_delete_cgroup_ext` did sometimes hang for multiple seconds. For easy reproducibility, you can also add an artificial delay in the clean up code, e.g. by adding something like: ``` const struct timespec artificial_delay = { 10, 0 }; nanosleep(&artificial_delay, NULL); ``` --- judge/runguard.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/judge/runguard.c b/judge/runguard.c index e1dc83f14d6..94e3dc2215c 100644 --- a/judge/runguard.c +++ b/judge/runguard.c @@ -1428,8 +1428,23 @@ int main(int argc, char **argv) } else { exitcode = WEXITSTATUS(status); } + verbose("child exited with exit code %d", exitcode); - check_remaining_procs(); + if ( use_walltime ) { + /* Disarm timer we set previously so if any of the + * clean-up steps below are slow we are not mistaking + * this for a wall-time timeout. */ + itimer.it_interval.tv_sec = 0; + itimer.it_interval.tv_usec = 0; + itimer.it_value.tv_sec = 0; + itimer.it_value.tv_usec = 0; + + if ( setitimer(ITIMER_REAL,&itimer,NULL)!=0 ) { + error(errno,"disarming timer"); + } + } + + check_remaining_procs(); double cputime = -1; output_cgroup_stats(&cputime); From 901ca3cca7ddc1da780ec59b099248f378a2fe50 Mon Sep 17 00:00:00 2001 From: MCJ Vasseur <14887731+vmcj@users.noreply.github.com> Date: Sat, 30 Sep 2023 00:39:25 +0200 Subject: [PATCH 22/36] Check if username confirms to Entity regex Explicit not done for the TSV to keep the old behaviour. We should check if this regex can be shared globally and used in the assertions on the Entity and through the different API endpoints for constraints. --- webapp/src/Service/ImportExportService.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/webapp/src/Service/ImportExportService.php b/webapp/src/Service/ImportExportService.php index 4ee8bc1a607..464141a9fbc 100644 --- a/webapp/src/Service/ImportExportService.php +++ b/webapp/src/Service/ImportExportService.php @@ -877,6 +877,15 @@ public function importAccountsJson(array $data, ?string &$message = null, ?array $juryTeam = null; $roles = []; $type = $account['type']; + $username = $account['username']; + + $icpcRegexChars = "[a-zA-Z0-9@._-]"; + $icpcRegex = "/^" . $icpcRegexChars . "+$/"; + if (!preg_match($icpcRegex, $username)) { + $message = sprintf('Username "%s" should be non empty and only contain: %s', $username, $icpcRegexChars); + return -1; + } + // Special case for the World Finals, if the username is CDS we limit the access. // The user can see what every admin can see, but can not log in via the UI. if (isset($account['username']) && $account['username'] === 'cds') { @@ -909,7 +918,7 @@ public function importAccountsJson(array $data, ?string &$message = null, ?array 'user' => [ 'name' => $account['name'] ?? null, 'externalid' => $account['id'] ?? $account['username'], - 'username' => $account['username'], + 'username' => $username, 'plain_password' => $account['password'] ?? null, 'teamid' => $account['team_id'] ?? null, 'user_roles' => $roles, From 1279a023e9518dda2d5f839e60800518259bcb7d Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Sun, 1 Oct 2023 10:08:11 +0200 Subject: [PATCH 23/36] Add option to upload languages configuration via API. (#2170) Fixes #2142. --- misc-tools/configure-domjudge.in | 39 +++++---- .../src/Controller/API/LanguageController.php | 84 +++++++++++++++++++ 2 files changed, 107 insertions(+), 16 deletions(-) diff --git a/misc-tools/configure-domjudge.in b/misc-tools/configure-domjudge.in index 6fc6dabf546..9f649686e84 100755 --- a/misc-tools/configure-domjudge.in +++ b/misc-tools/configure-domjudge.in @@ -99,21 +99,6 @@ else: print('Need a "config.json" to update general DOMjudge configuration.') -if os.path.exists('executables'): - executables = [] - for file in os.listdir('executables'): - if os.path.isdir(f'executables/{file}'): - executables.append(file) - shutil.make_archive(f'executables/{file}', 'zip', f'executables/{file}') - - if executables: - if dj_utils.confirm('Upload language executables (found: ' + ','.join(executables) + ')?', False): - for langid in executables: - dj_utils.upload_file(f'languages/{langid}/executable', 'executable', f'executables/{langid}.zip') - for langid in executables: - os.remove(f'executables/{langid}.zip') - - if os.path.exists('languages.json'): print('Comparing DOMjudge\'s language configuration:') with open('languages.json') as langConfigFile: @@ -132,7 +117,29 @@ if os.path.exists('languages.json'): if missing_keys: print(f' - missing keys from expected config = {missing_keys}') if diffs or new_keys or missing_keys: - print(' - We cannot update the configuration yet, but you might want to change it via the UI.') + if os.path.exists('executables'): + executables = [] + for file in os.listdir('executables'): + if os.path.isdir(f'executables/{file}'): + executables.append(file) + shutil.make_archive(f'executables/{file}', 'zip', f'executables/{file}') + + if executables: + if dj_utils.confirm('Upload language executables (found: ' + ','.join(executables) + ')?', False): + for langid in executables: + dj_utils.upload_file(f'languages/{langid}/executable', 'executable', f'executables/{langid}.zip') + for langid in executables: + os.remove(f'executables/{langid}.zip') + if dj_utils.confirm(' - Upload configuration changes?', True): + actual_config = _keyify_list(dj_utils.upload_file(f'languages', 'json', f'languages.json')) + diffs, new_keys, missing_keys = compare_configs( + actual_config=actual_config, + expected_config=expected_config + ) + if diffs: + print(' - There are still configuration differences after uploading:') + for d in diffs: + print(d) else: print(' - Language configuration is already up-to-date, nothing to update.') else: diff --git a/webapp/src/Controller/API/LanguageController.php b/webapp/src/Controller/API/LanguageController.php index 0facdfe48de..3128113d5c9 100644 --- a/webapp/src/Controller/API/LanguageController.php +++ b/webapp/src/Controller/API/LanguageController.php @@ -10,6 +10,7 @@ use FOS\RestBundle\Controller\Annotations as Rest; use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -122,6 +123,89 @@ public function updateExecutableActions(Request $request, string $id): void $this->dj->auditlog('executable', $language->getLangid(), 'updated'); } + #[IsGranted('ROLE_ADMIN')] + #[Rest\Post('languages')] + #[OA\Response( + response: 200, + description: 'Configure all specified languages.', + )] + public function configureLanguagesAction(Request $request): Response + { + /** @var UploadedFile|null $jsonFile */ + $jsonFile = $request->files->get('json'); + if (!$jsonFile) { + throw new BadRequestHttpException('No JSON file supplied.'); + } + $newLanguages = $this->dj->jsonDecode(file_get_contents($jsonFile->getRealPath())); + + // Disable submission for all current languages, we will enable it for all new languages below. + $curLanguages = $this->em->getRepository(Language::class)->findAll(); + foreach ($curLanguages as $language) { + /** @var Language $language */ + $language->setAllowSubmit(false); + } + + $idField = $this->eventLogService->externalIdFieldForEntity(Language::class) ?? 'langid'; + foreach ($newLanguages as $language) { + /** @var Language $language */ + $lang_id = $language['id']; + $lang = $this->em->getRepository(Language::class)->findOneBy( + [$idField => $lang_id] + ); + if (!$lang) { + // TODO: Decide how to handle this case, either erroring out or creating a new language. + continue; + } + + // We disallowed submission for all languages above, so we need to enable it for the given languages. + $lang->setAllowSubmit(true); + + if (isset($language['name'])) { + $lang->setName($language['name']); + } + if (isset($language['allow_submit'])) { + $lang->setAllowSubmit($language['allow_submit']); + } + if (isset($language['allow_judge'])) { + $lang->setAllowJudge($language['allow_judge']); + } + if (isset($language['entry_point_required'])) { + $lang->setRequireEntryPoint($language['entry_point_required']); + } + if (isset($language['entry_point_name'])) { + $lang->setEntryPointDescription($language['entry_point_name']); + } + if (isset($language['extensions'])) { + $lang->setExtensions($language['extensions']); + } + if (isset($language['filter_compiler_files'])) { + $lang->setFilterCompilerFiles($language['filter_compiler_files']); + } + if (isset($language['time_factor'])) { + $lang->setTimeFactor($language['time_factor']); + } + if (isset($language['compiler'])) { + if (isset($language['compiler']['version_command'])) { + $lang->setCompilerVersionCommand($language['compiler']['version_command']); + } + if (isset($language['compiler']['version'])) { + $lang->setCompilerVersion($language['compiler']['version']); + } + } + if (isset($language['runner'])) { + if (isset($language['runner']['version_command'])) { + $lang->setRunnerVersionCommand($language['runner']['version_command']); + } + if (isset($language['runner']['version'])) { + $lang->setRunnerVersion($language['runner']['version']); + } + } + } + $this->em->flush(); + + return parent::performListAction($request); + } + protected function getQueryBuilder(Request $request): QueryBuilder { if ($request->attributes->has('cid')) { From e2db36acc356fe4bd55d6a198d9b00b46292170c Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Sun, 1 Oct 2023 11:47:50 +0200 Subject: [PATCH 24/36] Properly check whether authenticated user has team linked in import script. (#2171) Fixes #2152. --- misc-tools/import-contest.in | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/misc-tools/import-contest.in b/misc-tools/import-contest.in index fe7a1bf63b2..a1cc8f9bf11 100755 --- a/misc-tools/import-contest.in +++ b/misc-tools/import-contest.in @@ -154,9 +154,10 @@ if cid is not None: # Problem import is also special: we need to upload each individual problem and detect what they are if os.path.exists('problems.yaml') or os.path.exists('problems.json') or os.path.exists('problemset.yaml'): if dj_utils.confirm('Import problems?', True): - # Check if our user is linked to a team + # Check if our user is linked to a team. user_data = dj_utils.do_api_request('user') - if not 'team' in user_data and not dj_utils.confirm('No team associated with your account. Jury submissions won\'t be imported. Really continue?', False): + has_team_linked = 'team' in user_data and user_data['team'] and 'roles' in user_data and 'team' in user_data['roles'] + if not has_team_linked and not dj_utils.confirm('No team associated with your account. Jury submissions won\'t be imported. Really continue?', False): exit(2) print('Importing problems.') From 14032410ad7b57b54601bbbddef30a6e7783af44 Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Thu, 31 Aug 2023 08:15:30 +0200 Subject: [PATCH 25/36] Show elapsed minutes on scoreboard instead of minutes left See: https://github.com/DOMjudge/domjudge/issues/2064 --- webapp/src/Twig/TwigExtension.php | 16 ++++++++-------- webapp/templates/partials/scoreboard.html.twig | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index 3161547716a..eeda610b61a 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -68,7 +68,7 @@ public function getFilters(): array { return [ new TwigFilter('printtimediff', $this->printtimediff(...)), - new TwigFilter('printremainingminutes', $this->printremainingminutes(...)), + new TwigFilter('printelapsedminutes', $this->printelapsedminutes(...)), new TwigFilter('printtime', $this->printtime(...)), new TwigFilter('printHumanTimeDiff', $this->printHumanTimeDiff(...)), new TwigFilter('printtimeHover', $this->printtimeHover(...), ['is_safe' => ['html']]), @@ -162,15 +162,15 @@ public function printtimediff(float $start, ?float $end = null): string return Utils::printtimediff($start, $end); } - public function printremainingminutes(float $start, float $end): string + public function printelapsedminutes(float $start, float $end): string { - $minutesRemaining = floor(($end - $start)/60); - if ($minutesRemaining < 1) { - return 'less than 1 minute to go'; - } elseif ($minutesRemaining == 1) { - return '1 minute to go'; + $minutesElapsed = floor(($end - $start)/60); + if ($minutesElapsed < 1) { + return 'started less than 1 minute ago'; + } elseif ($minutesElapsed == 1) { + return 'started 1 minute ago'; } else { - return $minutesRemaining . ' minutes to go'; + return 'started' . $minutesElapsed . ' minutes ago'; } } diff --git a/webapp/templates/partials/scoreboard.html.twig b/webapp/templates/partials/scoreboard.html.twig index cdfa19de114..48c211ed4cc 100644 --- a/webapp/templates/partials/scoreboard.html.twig +++ b/webapp/templates/partials/scoreboard.html.twig @@ -33,7 +33,7 @@ contest over, waiting for results {% elseif static %} {% set now = 'now'|date('U') %} - {{ now | printremainingminutes(current_contest.endtime) }} + {{ current_contest.starttime | printelapsedminutes(now) }} {% else %} {% if current_contest.freezeData.started %} started: From 8220ca85c99fd0ae6a6117b4c3d8c64432ae0c6c Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Tue, 3 Oct 2023 12:22:04 +0200 Subject: [PATCH 26/36] Add minor note about how to run development container. --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 7d53b7f91a5..b3a7333efe5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ # Note: this docker compose stack should only be used for development purposes. # Do not use it for production deployments. If you want to deploy DOMjudge in # production with Docker, see https://hub.docker.com/r/domjudge/domserver. +# It is recommended to use `docker compose up` to start this stack. Note, don't +# use sudo or the legacy docker-compose. version: '3' From 3536ed9bd4540369647ef22e6657f4f8c232b808 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Wed, 4 Oct 2023 09:05:51 +0200 Subject: [PATCH 27/36] Fix typo's reported by CI. --- judge/judgedaemon.main.php | 2 +- webapp/src/Utils/Utils.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/judge/judgedaemon.main.php b/judge/judgedaemon.main.php index f5ac190a3b6..bb3f9eeef12 100644 --- a/judge/judgedaemon.main.php +++ b/judge/judgedaemon.main.php @@ -1079,7 +1079,7 @@ function compile( ): bool { global $myhost, $EXITCODES; - // Re-use compilation if it already exists. + // Reuse compilation if it already exists. if (file_exists("$workdir/compile.success")) { return true; } diff --git a/webapp/src/Utils/Utils.php b/webapp/src/Utils/Utils.php index 2dfded802c6..17199ba908b 100644 --- a/webapp/src/Utils/Utils.php +++ b/webapp/src/Utils/Utils.php @@ -335,7 +335,7 @@ public static function parseHexColor(string $hex): array } /** - * Comvert an RGB component to its hex value. + * Convert an RGB component to its hex value. */ public static function componentToHex(int $component): string { From d42c41c6d706ac40c013221159e9f2e4349646da Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Tue, 3 Oct 2023 22:22:45 +0200 Subject: [PATCH 28/36] If an API returns an empty response, do not try to JSON decode it. This happens when the API returns a 204. --- webapp/src/Service/DOMJudgeService.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index c9da8e411c9..ca2ad891c38 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -654,7 +654,13 @@ public function internalApiRequest(string $url, string $method = Request::METHOD return null; } - return $this->jsonDecode($response->getContent()); + $content = $response->getContent(); + + if ($content === '') { + return null; + } + + return $this->jsonDecode($content); } public function getDomjudgeEtcDir(): string From b75625bab2407770d8b365de1784bb45a91b6c38 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Tue, 3 Oct 2023 22:23:16 +0200 Subject: [PATCH 29/36] Only import images for entities that actually exist. --- misc-tools/import-contest.in | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/misc-tools/import-contest.in b/misc-tools/import-contest.in index a1cc8f9bf11..1f6226bbad3 100755 --- a/misc-tools/import-contest.in +++ b/misc-tools/import-contest.in @@ -68,7 +68,10 @@ def import_images(entity: str, property: str, filename_regexes: List[str]): if not os.path.isdir(entity): return images_per_entity = {} - for entity_id in listdir(entity): + with open(f'{entity}.json') as entityFile: + entities = json.load(entityFile) + entity_ids = [entity['id'] for entity in entities] + for entity_id in entity_ids: entity_dir = f'{entity}/{entity_id}' if not os.path.isdir(entity_dir): continue From 1b77952725721d082f256c686ab39ae7f9724e22 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Tue, 3 Oct 2023 22:23:48 +0200 Subject: [PATCH 30/36] Fix organization logo and team photo properties in API when requesting without a contest. --- .../Controller/API/OrganizationController.php | 2 +- webapp/src/Controller/API/TeamController.php | 2 +- .../src/Serializer/TeamAffiliationVisitor.php | 18 +++++++++++------- webapp/src/Serializer/TeamVisitor.php | 18 +++++++++++------- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/webapp/src/Controller/API/OrganizationController.php b/webapp/src/Controller/API/OrganizationController.php index 0584351f798..0d2ef6b8bf0 100644 --- a/webapp/src/Controller/API/OrganizationController.php +++ b/webapp/src/Controller/API/OrganizationController.php @@ -100,7 +100,7 @@ public function singleAction(Request $request, string $id): Response * Get the logo for the given organization. */ #[Rest\Get('contests/{cid}/organizations/{id}/logo', name: 'organization_logo')] - #[Rest\Get('organizations/{id}/logo')] + #[Rest\Get('organizations/{id}/logo', name: 'no_contest_organization_logo')] #[OA\Response( response: 200, description: 'Returns the given organization logo in PNG, JPG or SVG format', diff --git a/webapp/src/Controller/API/TeamController.php b/webapp/src/Controller/API/TeamController.php index e32ab1ca7a5..dbc8ec7fdd9 100644 --- a/webapp/src/Controller/API/TeamController.php +++ b/webapp/src/Controller/API/TeamController.php @@ -113,7 +113,7 @@ public function singleAction(Request $request, string $id): Response * Get the photo for the given team. */ #[Rest\Get('contests/{cid}/teams/{id}/photo', name: 'team_photo')] - #[Rest\Get('teams/{id}/photo')] + #[Rest\Get('teams/{id}/photo', name: 'no_contest_team_photo')] #[OA\Response( response: 200, description: 'Returns the given team photo in PNG, JPG or SVG format', diff --git a/webapp/src/Serializer/TeamAffiliationVisitor.php b/webapp/src/Serializer/TeamAffiliationVisitor.php index 39b643d7eb2..b55b0784278 100644 --- a/webapp/src/Serializer/TeamAffiliationVisitor.php +++ b/webapp/src/Serializer/TeamAffiliationVisitor.php @@ -84,13 +84,17 @@ public function onPostSerialize(ObjectEvent $event): void $parts = explode('.', $affiliationLogo); $extension = $parts[count($parts) - 1]; - $route = $this->dj->apiRelativeUrl( - 'v4_organization_logo', - [ - 'cid' => $this->requestStack->getCurrentRequest()->attributes->get('cid'), - 'id' => $id, - ] - ); + if ($cid = $this->requestStack->getCurrentRequest()->attributes->get('cid')) { + $route = $this->dj->apiRelativeUrl( + 'v4_organization_logo', + [ + 'cid' => $cid, + 'id' => $id, + ] + ); + } else { + $route = $this->dj->apiRelativeUrl('v4_no_contest_organization_logo', ['id' => $id]); + } $property = new StaticPropertyMetadata( TeamAffiliation::class, 'logo', diff --git a/webapp/src/Serializer/TeamVisitor.php b/webapp/src/Serializer/TeamVisitor.php index f37319e7513..bb713a3529b 100644 --- a/webapp/src/Serializer/TeamVisitor.php +++ b/webapp/src/Serializer/TeamVisitor.php @@ -62,13 +62,17 @@ public function onPostSerialize(ObjectEvent $event): void $imageSize = Utils::getImageSize($teamPhoto); - $route = $this->dj->apiRelativeUrl( - 'v4_team_photo', - [ - 'cid' => $this->requestStack->getCurrentRequest()->attributes->get('cid'), - 'id' => $id, - ] - ); + if ($cid = $this->requestStack->getCurrentRequest()->attributes->get('cid')) { + $route = $this->dj->apiRelativeUrl( + 'v4_team_photo', + [ + 'cid' => $cid, + 'id' => $id, + ] + ); + } else { + $route = $this->dj->apiRelativeUrl('v4_no_contest_team_photo', ['id' => $id,]); + } $property = new StaticPropertyMetadata( Team::class, 'photo', From c249b1382e70cf244264f4aa75bf14f340d8b13e Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Sun, 17 Sep 2023 11:38:51 +0200 Subject: [PATCH 31/36] Show unused executables as greyed out. This needs to be further extended as language executables are now shown as disabled. --- .../Controller/Jury/ExecutableController.php | 35 +++++++++++++++---- webapp/src/Entity/Executable.php | 16 +++++++++ webapp/templates/jury/executables.html.twig | 6 ++-- .../Jury/ExecutableControllerTest.php | 2 +- 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/webapp/src/Controller/Jury/ExecutableController.php b/webapp/src/Controller/Jury/ExecutableController.php index 223f3995c24..df0f1002503 100644 --- a/webapp/src/Controller/Jury/ExecutableController.php +++ b/webapp/src/Controller/Jury/ExecutableController.php @@ -12,6 +12,7 @@ use App\Service\EventLogService; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; +use InvalidArgumentException as PHPInvalidArgumentException; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Form\Exception\InvalidArgumentException; @@ -42,6 +43,8 @@ public function __construct( #[Route(path: '', name: 'jury_executables')] public function indexAction(Request $request): Response { + $executables_tables_used = []; + $executables_tables_unused = []; $data = []; $form = $this->createForm(ExecutableUploadType::class, $data); $form->handleRequest($request); @@ -64,6 +67,15 @@ public function indexAction(Request $request): Response $propertyAccessor = PropertyAccess::createPropertyAccessor(); $executables_table = []; + $configScripts = []; + foreach (['compare', 'run', 'full_debug'] as $config_script) { + try { + $configScripts[] = (string)$this->config->get('default_' . $config_script); + } catch (PHPInvalidArgumentException $e) { + // If not found this is an older database, as we only use this for visual changes ignore this error; + } + } + foreach ($executables as $e) { $execdata = []; $execactions = []; @@ -115,16 +127,27 @@ public function indexAction(Request $request): Response 'link' => $this->generateUrl('jury_executable_download', ['execId' => $e->getExecid()]) ]; - $executables_table[] = [ - 'data' => $execdata, - 'actions' => $execactions, - 'link' => $this->generateUrl('jury_executable', ['execId' => $e->getExecid()]), - ]; + if ($e->checkUsed($configScripts)) { + $executables_tables_used[] = [ + 'data' => $execdata, + 'actions' => $execactions, + 'link' => $this->generateUrl('jury_executable', ['execId' => $e->getExecid()]), + ]; + } else { + $executables_tables_unused[] = [ + 'data' => $execdata, + 'actions' => $execactions, + 'link' => $this->generateUrl('jury_executable', ['execId' => $e->getExecid()]), + 'cssclass' => 'disabled', + ]; + } } // This is replaced with the icon. unset($table_fields['type']); + return $this->render('jury/executables.html.twig', [ - 'executables' => $executables_table, + 'executables_used' => $executables_tables_used, + 'executables_unused' => $executables_tables_unused, 'table_fields' => $table_fields, 'form' => $form, ]); diff --git a/webapp/src/Entity/Executable.php b/webapp/src/Entity/Executable.php index 30eb1aa4224..24e80c6cf42 100644 --- a/webapp/src/Entity/Executable.php +++ b/webapp/src/Entity/Executable.php @@ -181,4 +181,20 @@ public function getZipfileContent(string $tempdir): string unlink($tempzipFile); return $zipFileContents; } + + /** + * @param string[] $configScripts + */ + public function checkUsed(array $configScripts): bool + { + foreach ($configScripts as $config_script) { + if ($this->execid === $config_script) { + return true; + } + } + if (count($this->problems_compare) || count($this->problems_run)) { + return true; + } + return false; + } } diff --git a/webapp/templates/jury/executables.html.twig b/webapp/templates/jury/executables.html.twig index cd5276371e7..159492c57de 100644 --- a/webapp/templates/jury/executables.html.twig +++ b/webapp/templates/jury/executables.html.twig @@ -10,9 +10,11 @@ {% block content %} -

Executables

+

Used executables

+ {{ macros.table(executables_used, table_fields, {'ordering': 'false'}) }} - {{ macros.table(executables, table_fields, {'ordering': 'false'}) }} +

Unused executables

+ {{ macros.table(executables_unused, table_fields, {'ordering': 'false'}) }} {% if is_granted('ROLE_ADMIN') %}

diff --git a/webapp/tests/Unit/Controller/Jury/ExecutableControllerTest.php b/webapp/tests/Unit/Controller/Jury/ExecutableControllerTest.php index 86aef76a614..5b45c00ef23 100644 --- a/webapp/tests/Unit/Controller/Jury/ExecutableControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/ExecutableControllerTest.php @@ -17,7 +17,7 @@ class ExecutableControllerTest extends JuryControllerTestCase protected static string $deleteEntityIdentifier = 'description'; protected static string $getIDFunc = 'getExecid'; protected static string $className = Executable::class; - protected static array $DOM_elements = ['h1' => ['Executables']]; + protected static array $DOM_elements = ['h1' => ['Used executables', 'Unused executables']]; protected static string $addForm = 'executable_upload['; protected static array $addEntitiesShown = ['type']; protected static array $addEntities = []; From f141784a277b56cedb262432cb664176421cdea9 Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Sun, 17 Sep 2023 11:38:51 +0200 Subject: [PATCH 32/36] Mark executables as disabled for languages not allowed to submit --- webapp/src/Entity/Executable.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/webapp/src/Entity/Executable.php b/webapp/src/Entity/Executable.php index 24e80c6cf42..3203d230045 100644 --- a/webapp/src/Entity/Executable.php +++ b/webapp/src/Entity/Executable.php @@ -195,6 +195,11 @@ public function checkUsed(array $configScripts): bool if (count($this->problems_compare) || count($this->problems_run)) { return true; } + foreach ($this->languages as $lang) { + if ($lang->getAllowSubmit()) { + return true; + } + } return false; } } From 82a1cd013e0380f6fd6862160b9abf81cee5ba20 Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Sun, 17 Sep 2023 22:33:00 +0200 Subject: [PATCH 33/36] Added problem badges for executables --- .../Controller/Jury/ExecutableController.php | 42 +++++++++++++++++++ webapp/templates/jury/jury_macros.twig | 4 ++ 2 files changed, 46 insertions(+) diff --git a/webapp/src/Controller/Jury/ExecutableController.php b/webapp/src/Controller/Jury/ExecutableController.php index df0f1002503..ca81cbd36b5 100644 --- a/webapp/src/Controller/Jury/ExecutableController.php +++ b/webapp/src/Controller/Jury/ExecutableController.php @@ -3,6 +3,7 @@ namespace App\Controller\Jury; use App\Controller\BaseController; +use App\Entity\ContestProblem; use App\Entity\Executable; use App\Entity\ExecutableFile; use App\Entity\ImmutableExecutable; @@ -62,6 +63,7 @@ public function indexAction(Request $request): Response 'icon' => ['title' => 'type', 'sort' => false], 'execid' => ['title' => 'ID', 'sort' => true,], 'type' => ['title' => 'type', 'sort' => true,], + 'badges' => ['title' => 'problems', 'sort' => false], 'description' => ['title' => 'description', 'sort' => true,], ]; @@ -76,7 +78,46 @@ public function indexAction(Request $request): Response } } + $contestProblemsWithExecutables = []; + $executablesWithContestProblems = []; + if ($this->dj->getCurrentContest()) { + $contestProblemsWithExecutables = $em->createQueryBuilder() + ->select('cp', 'p', 'ecomp') + ->from(ContestProblem::class, 'cp') + ->where('cp.contest = :contest') + ->setParameter('contest', $this->dj->getCurrentContest()) + ->join('cp.problem', 'p') + ->leftJoin('p.compare_executable', 'ecomp') + ->leftJoin('p.run_executable', 'erun') + ->andWhere('ecomp IS NOT NULL OR erun IS NOT NULL') + ->getQuery()->getResult(); + $executablesWithContestProblems = $em->createQueryBuilder() + ->select('e') + ->from(Executable::class, 'e') + ->leftJoin('e.problems_compare', 'pcomp') + ->leftJoin('e.problems_run', 'prun') + ->where('pcomp IS NOT NULL OR prun IS NOT NULL') + ->leftJoin('pcomp.contest_problems', 'cpcomp') + ->leftJoin('prun.contest_problems', 'cprun') + ->andWhere('cprun.contest = :contest OR cpcomp.contest = :contest') + ->setParameter('contest', $this->dj->getCurrentContest()) + ->getQuery()->getResult(); + } + foreach ($executables as $e) { + $badges = []; + if (in_array($e, $executablesWithContestProblems)) { + foreach (array_merge($e->getProblemsRun()->toArray(), $e->getProblemsCompare()->toArray()) as $execProblem) { + $execContestProblems = $execProblem->getContestProblems(); + foreach($contestProblemsWithExecutables as $cp) { + if ($execContestProblems->contains($cp)) { + $badges[] = $cp; + } + } + } + } + sort($badges); + $execdata = []; $execactions = []; // Get whatever fields we can from the team object itself. @@ -103,6 +144,7 @@ public function indexAction(Request $request): Response default: $execdata['icon']['icon'] = 'question'; } + $execdata['badges']['value'] = $badges; if ($this->isGranted('ROLE_ADMIN')) { $execactions[] = [ diff --git a/webapp/templates/jury/jury_macros.twig b/webapp/templates/jury/jury_macros.twig index 6fc2287ffcc..b1460ac4985 100644 --- a/webapp/templates/jury/jury_macros.twig +++ b/webapp/templates/jury/jury_macros.twig @@ -141,6 +141,10 @@ {{- (item.value|default(item.default|default('')))|affiliationLogo(item.title) -}} {% elseif key == "warning_content" %} {{- item.value|printWarningContent -}} + {% elseif key == "badges" %} + {% for badge in item.value %} + {{- badge|problemBadge }} + {% endfor %} {% elseif (column.render | default('')) == "entity_id_badge" %} {% if item.value %}{{- item.value|entityIdBadge(item.idPrefix|default('')) -}}{% endif %} {% else %} From f6d29604b23742684d7d0c50659959adee952ea3 Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Fri, 6 Oct 2023 20:48:04 +0200 Subject: [PATCH 34/36] Add last HTTP code received for the upstream contestSource Exposes when externalContestSource (URI) returns a non HTTP 200. To trigger this: create a contest with http://URL// (the double // is important) --- webapp/migrations/Version20231006185028.php | 34 +++++++++++++++++++ webapp/src/Entity/ExternalContestSource.php | 18 ++++++++++ webapp/src/Service/DOMJudgeService.php | 2 +- .../Service/ExternalContestSourceService.php | 7 ++-- .../partials/external_contest_info.html.twig | 2 ++ 5 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 webapp/migrations/Version20231006185028.php diff --git a/webapp/migrations/Version20231006185028.php b/webapp/migrations/Version20231006185028.php new file mode 100644 index 00000000000..a31c26b8574 --- /dev/null +++ b/webapp/migrations/Version20231006185028.php @@ -0,0 +1,34 @@ +addSql('ALTER TABLE external_contest_source ADD last_httpcode SMALLINT UNSIGNED DEFAULT NULL COMMENT \'Last HTTP code received by event feed reader\''); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE external_contest_source DROP last_httpcode'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/src/Entity/ExternalContestSource.php b/webapp/src/Entity/ExternalContestSource.php index 86a1951422c..5f04102e679 100644 --- a/webapp/src/Entity/ExternalContestSource.php +++ b/webapp/src/Entity/ExternalContestSource.php @@ -49,6 +49,13 @@ class ExternalContestSource )] private ?float $lastPollTime = null; + #[ORM\Column( + type: 'smallint', + nullable: true, + options: ['comment' => 'Last HTTP code received by event feed reader', 'unsigned' => true] + )] + public ?int $lastHTTPCode = null; + #[ORM\ManyToOne(inversedBy: 'externalContestSources')] #[ORM\JoinColumn(name: 'cid', referencedColumnName: 'cid', onDelete: 'CASCADE')] private Contest $contest; @@ -176,6 +183,17 @@ public function getShortDescription(): string return $this->getSource(); } + public function setLastHTTPCode(?int $lastHTTPCode): ExternalContestSource + { + $this->lastHTTPCode = $lastHTTPCode; + return $this; + } + + public function getLastHTTPCode(): ?int + { + return $this->lastHTTPCode; + } + #[Assert\Callback] public function validate(ExecutionContextInterface $context): void { diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index ca2ad891c38..88fcf8d885f 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -394,7 +394,7 @@ public function getUpdates(): array ->select('ecs.extsourceid', 'ecs.lastPollTime') ->from(ExternalContestSource::class, 'ecs') ->andWhere('ecs.contest = :contest') - ->andWhere('ecs.lastPollTime < :i OR ecs.lastPollTime is NULL') + ->andWhere('ecs.lastPollTime < :i OR ecs.lastPollTime is NULL OR ecs.lastHTTPCode != 200') ->setParameter('contest', $contest) ->setParameter('i', time() - $this->config->get('external_contest_source_critical')) ->getQuery()->getOneOrNullResult(); diff --git a/webapp/src/Service/ExternalContestSourceService.php b/webapp/src/Service/ExternalContestSourceService.php index a872472f6fe..ef2683ed832 100644 --- a/webapp/src/Service/ExternalContestSourceService.php +++ b/webapp/src/Service/ExternalContestSourceService.php @@ -308,11 +308,14 @@ protected function importFromCcsApi(array $eventsToSkip, ?callable $progressRepo $fullUrl .= '&since_token=' . $this->getLastReadEventId(); } $response = $this->httpClient->request('GET', $fullUrl, ['buffer' => false]); - if ($response->getStatusCode() !== 200) { + $statusCode = $response->getStatusCode(); + $this->source->setLastHTTPCode($statusCode); + $this->em->flush(); + if ($statusCode !== 200) { $this->logger->warning( 'Received non-200 response code %d, waiting for five seconds ' . 'and trying again. Press ^C to quit.', - [$response->getStatusCode()] + [$statusCode] ); sleep(5); continue; diff --git a/webapp/templates/jury/partials/external_contest_info.html.twig b/webapp/templates/jury/partials/external_contest_info.html.twig index 49797d3309f..ab0c460c917 100644 --- a/webapp/templates/jury/partials/external_contest_info.html.twig +++ b/webapp/templates/jury/partials/external_contest_info.html.twig @@ -43,6 +43,8 @@ {% if not externalContestSource.lastPollTime %} Event feed reader never checked in. + {% elseif externalContestSource.lastHTTPCode is not same as(200) %} + Received {{ externalContestSource.lastHTTPCode }} HTTP code. {% else %} {{ status }}, last checked in {{ externalContestSource.lastPollTime | printtimediff }}s ago. {% endif %} From c9add074da01f48307c27295cd8d40afb991433b Mon Sep 17 00:00:00 2001 From: Alireza Ghasemi Date: Sat, 7 Oct 2023 11:00:47 +0330 Subject: [PATCH 35/36] Handle kotlinc symbolic link for determining KOTLIN_DIR Find the real path of `kotlinc` in case it is a symbolic link, ensuring that the correct compiler directory containing `kotlinc` is determined. Previously, it directly used `dirname` on the result of `command -v kotlinc`, which could have led to incorrect directory when `kotlinc` was a symbolic link, and as a result, not finding the `kotlin-stdlib.jar` latter in the script. --- sql/files/defaultdata/kt/run | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/files/defaultdata/kt/run b/sql/files/defaultdata/kt/run index a2447c58f54..e0770991058 100755 --- a/sql/files/defaultdata/kt/run +++ b/sql/files/defaultdata/kt/run @@ -19,7 +19,7 @@ COMPILESCRIPTDIR="$(dirname "$0")" # Note that you then also might want to fix this in the compiler and runner version commands. # For example # KOTLIN_DIR=/usr/lib/kotlinc/bin -KOTLIN_DIR="$(dirname "$(command -v kotlinc)")" +KOTLIN_DIR="$(dirname "$(realpath "$(command -v kotlinc)")")" # Stack size in the JVM in KB. Note that this will be deducted from # the total memory made available for the heap. From 74535eb47e06741a2e44a55eb3acba66e8c7333a Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Fri, 13 Oct 2023 21:42:15 +0200 Subject: [PATCH 36/36] Show mismatch in expected result directly on submission page. (#2178) Co-authored-by: Jaap Eldering --- webapp/templates/jury/submission.html.twig | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/webapp/templates/jury/submission.html.twig b/webapp/templates/jury/submission.html.twig index ad32cf165b2..2cbd205b7cc 100644 --- a/webapp/templates/jury/submission.html.twig +++ b/webapp/templates/jury/submission.html.twig @@ -42,6 +42,26 @@

{% endif %} + {% if selectedJudging is not null and selectedJudging.result is not empty %} + {% if selectedJudging.result|upper not in submission.expectedResults %} +
+ Actual result {{ selectedJudging.result | printValidJuryResult }} does NOT match expected result(s): + {% for expectedResult in submission.expectedResults %} + {{ expectedResult | printValidJuryResult }} + {% if not loop.last %}or{% endif %} + {% endfor %}. +
+ {% elseif submission.expectedResults|length > 1 %} +
+ Actual result {{ selectedJudging.result | printValidJuryResult }} matches one of multiple expected results: + {% for expectedResult in submission.expectedResults %} + {{ expectedResult | printValidJuryResult }} + {% if not loop.last %}or{% endif %} + {% endfor %}. +
+ {% endif %} + {% endif %} +

Submission {{ submission.submitid }}