Skip to content

Commit 5f3b2bd

Browse files
vmcjmeisterT
andcommitted
Autocheck for new DOMjudge releases
Discussed during the hackathon in 2023. We allow admins to toggle an autoquery to domjudge.org for 2 reasons: - Alerting users that a new version might exist, helping us in case of security releases. - Giving a gentle nudge for people to upgrade making support easier. - Getting some information on what installations are out there. Co-authored-by: Tobias Werth <[email protected]>
1 parent dfde430 commit 5f3b2bd

File tree

4 files changed

+134
-0
lines changed

4 files changed

+134
-0
lines changed

etc/db-config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,12 @@
393393
description: Time in seconds after an external contest source reader last checked in before showing its status as `critical`.
394394
regex: /^\d+$/
395395
error_message: A non-negative number is required.
396+
- name: check_new_version
397+
type: enum
398+
enum_class: App\Utils\UpdateStrategy
399+
default_value: none
400+
public: false
401+
description: Automatically check and notify for new DOMjudge releases?
396402
- name: adminer_enabled
397403
type: bool
398404
default_value: false

webapp/src/Controller/Jury/JuryMiscController.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public function indexAction(ConfigurationService $config): Response
5858
}
5959
}
6060

61+
$newestVersion = $this->dj->checkNewVersion();
62+
if ($newestVersion) {
63+
$this->addFlash('info', 'New release ' . $newestVersion . ' available at: https://www.domjudge.org/download.');
64+
}
65+
6166
return $this->render('jury/index.html.twig', [
6267
'adminer_enabled' => $config->get('adminer_enabled'),
6368
'CCS_SPEC_API_URL' => GI::CCS_SPEC_API_URL,

webapp/src/Service/DOMJudgeService.php

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,19 @@
3131
use App\Entity\Testcase;
3232
use App\Entity\User;
3333
use App\Utils\FreezeData;
34+
use App\Utils\UpdateStrategy;
3435
use App\Utils\Utils;
3536
use DateTime;
3637
use Doctrine\ORM\EntityManagerInterface;
3738
use Doctrine\ORM\NonUniqueResultException;
3839
use Doctrine\ORM\NoResultException;
3940
use Doctrine\ORM\Query\Expr\Join;
4041
use Doctrine\ORM\QueryBuilder;
42+
use Exception;
4143
use InvalidArgumentException;
4244
use Psr\Log\LoggerInterface;
4345
use ReflectionClass;
46+
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
4447
use Symfony\Component\DependencyInjection\Attribute\Autowire;
4548
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
4649
use Symfony\Component\HttpFoundation\Cookie;
@@ -62,6 +65,7 @@
6265
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
6366
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
6467
use Symfony\Component\Security\Core\User\UserInterface;
68+
use Symfony\Contracts\Cache\ItemInterface;
6569
use Twig\Environment;
6670
use ZipArchive;
6771

@@ -70,6 +74,8 @@ class DOMJudgeService
7074
protected ?Executable $defaultCompareExecutable = null;
7175
protected ?Executable $defaultRunExecutable = null;
7276

77+
private string $localVersionString = '';
78+
7379
final public const EVAL_DEFAULT = 0;
7480
final public const EVAL_LAZY = 1;
7581
final public const EVAL_FULL = 2;
@@ -108,6 +114,10 @@ public function __construct(
108114
protected string $projectDir,
109115
#[Autowire('%domjudge.vendordir%')]
110116
protected string $vendorDir,
117+
#[Autowire('%domjudge.version%')]
118+
protected readonly string $domjudgeVersion,
119+
#[Autowire('%domjudge.installmethod%')]
120+
protected readonly string $domjudgeInstallMethod,
111121
) {}
112122

113123
/**
@@ -1715,4 +1725,98 @@ public function getTeamsForContest(?Contest $contest) : array {
17151725
->getQuery()
17161726
->getResult();
17171727
}
1728+
1729+
/**
1730+
* @return String[]|false
1731+
*/
1732+
public function cacherCheckNewVersion(ItemInterface $item): array|false {
1733+
$item->expiresAfter(86400);
1734+
1735+
$versionUrl = 'https://versions.domjudge.org';
1736+
$options = ['http' => ['timeout' => 1, 'method' => 'GET', 'header' => "User-Agent: DOMjudge#" . $this->domjudgeInstallMethod . "/" . $this->localVersionString . "\r\n"]];
1737+
$context = stream_context_create($options);
1738+
$response = @file_get_contents($versionUrl, false, $context);
1739+
if ($response === false) {
1740+
return false;
1741+
}
1742+
// Assume we get a one-level unordered JSON list with the released versions e.g. ["10.0.0", "9.11.0", "12.0.12", "10.0.1"]
1743+
$tmp_versions = json_decode($response, true);
1744+
natsort($tmp_versions);
1745+
return array_reverse($tmp_versions);
1746+
}
1747+
1748+
/**
1749+
* Returns either the next strictly higher version or false when nothing is found/requested.
1750+
*/
1751+
public function checkNewVersion(): string|false {
1752+
if ($this->config->get('check_new_version', false) === UpdateStrategy::Strategy_none) {
1753+
return false;
1754+
}
1755+
// The local version is something like "x.y.z / commit hash", e.g. "8.4.0DEV/4e25adb13" for development
1756+
// or 8.3.2 for a released version
1757+
// In case of development we remove the commit hash for some anonymity but keep the DEV to not count those as the (possibly) released version
1758+
$this->localVersionString = (string)strtok($this->domjudgeVersion, "/");
1759+
$localVersion = explode(".", $this->localVersionString);
1760+
if (count($localVersion) !== 3) {
1761+
// Unknown version, someone might have locally modified and used their own versioning
1762+
return false;
1763+
}
1764+
1765+
$cache = new FilesystemAdapter();
1766+
try {
1767+
$versions = $cache->get('domjudge_versions', [$this, 'cacherCheckNewVersion']);
1768+
} catch (InvalidArgumentException $e) {
1769+
return false;
1770+
}
1771+
1772+
if (!$versions) {
1773+
return false;
1774+
}
1775+
1776+
preg_match("/\d.\d.\d/", $this->domjudgeVersion, $matches);
1777+
$extractedLocalVersionString = $matches[0];
1778+
if ($this->config->get('check_new_version', false) === UpdateStrategy::Strategy_incremental) {
1779+
/* Steer towards the nearest highest patch release first
1780+
* So the expected path would be:
1781+
* DJ6.0.0 -> DJ6.0.6 -> DJ6.6.0 -> DJ9.1.2 instead of
1782+
* -> DJ6.0.[1..6] -> DJ6.[1..6].* -> DJ[7..9].*.*
1783+
* skipping all patch releases in between, when no patch release
1784+
* is available, try the highest minor and otherwise the highest Major
1785+
* instead of going to the latest release:
1786+
* DJ6.0.0 -> DJ9.1.2
1787+
*/
1788+
$patch = "/" . $localVersion[0] . "." . $localVersion[1] . ".\d/";
1789+
$minor = "/" . $localVersion[0] . ".\d.\d/";
1790+
$major = "/\d.\d.\d/";
1791+
foreach ([$patch, $minor, $major] as $regex) {
1792+
foreach ($versions as $release) {
1793+
if (preg_match($regex, $release)) {
1794+
if (strnatcmp($release, $extractedLocalVersionString) === 1) {
1795+
return $release;
1796+
}
1797+
if (strnatcmp($release, $extractedLocalVersionString) === 0 && str_contains($this->localVersionString, "DEV")) {
1798+
// Special case, the development version is now released
1799+
return $release;
1800+
}
1801+
}
1802+
}
1803+
}
1804+
}
1805+
elseif ($this->config->get('check_new_version', false) === UpdateStrategy::Strategy_major_release) {
1806+
/* Steer towards the latest version directly
1807+
* So the expected path would be:
1808+
* DJ6.0.0 -> DJ9.1.2
1809+
* This should be safe as doctrine migrations check for upgrades regardless of current DOMjudge release
1810+
*/
1811+
$latest = $versions[0];
1812+
if (strnatcmp($latest, $extractedLocalVersionString) === 1) {
1813+
return $latest;
1814+
}
1815+
if (strnatcmp($latest, $extractedLocalVersionString) === 0 && str_contains($this->localVersionString, "DEV")) {
1816+
// Special case, the development version is now released
1817+
return $latest;
1818+
}
1819+
}
1820+
return false;
1821+
}
17181822
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace App\Utils;
4+
5+
enum UpdateStrategy: string
6+
{
7+
case Strategy_incremental = 'incremental';
8+
case Strategy_major_release = 'major';
9+
case Strategy_none = 'none';
10+
11+
public function getConfigDescription(): string
12+
{
13+
return match ($this) {
14+
self::Strategy_incremental => 'Report on next patch releases, favoring reliability over features',
15+
self::Strategy_major_release => 'Report on newest Major/minor releases, favoring being close to the version maintainers run',
16+
self::Strategy_none => 'Do not report on any new versions',
17+
};
18+
}
19+
}

0 commit comments

Comments
 (0)