Skip to content

Commit 708f9cf

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 c4a923f commit 708f9cf

File tree

5 files changed

+126
-0
lines changed

5 files changed

+126
-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/Service/DOMJudgeService.php

Lines changed: 93 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

@@ -108,6 +112,10 @@ public function __construct(
108112
protected string $projectDir,
109113
#[Autowire('%domjudge.vendordir%')]
110114
protected string $vendorDir,
115+
#[Autowire('%domjudge.version%')]
116+
protected readonly string $domjudgeVersion,
117+
#[Autowire('%domjudge.installmethod%')]
118+
protected readonly string $domjudgeInstallMethod,
111119
) {}
112120

113121
/**
@@ -1699,4 +1707,89 @@ public function getAllowedLanguagesForContest(?Contest $contest) : array {
16991707
->getQuery()
17001708
->getResult();
17011709
}
1710+
1711+
/**
1712+
* Returns either the next strictly higher version or false when nothing is found/requested.
1713+
*/
1714+
public function checkNewVersion(): string|false {
1715+
if ($this->config->get('check_new_version', false) === UpdateStrategy::Strategy_none) {
1716+
return false;
1717+
}
1718+
// The local version is something like "x.y.z / commit hash", e.g. "8.4.0DEV/4e25adb13" for development
1719+
// or 8.3.2 for a released version
1720+
// 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
1721+
$localVersionString = strtok($this->domjudgeVersion, "/");
1722+
$localVersion = explode(".", $localVersionString);
1723+
if (count($localVersion) !== 3) {
1724+
// Unknown version, someone might have locally modified and used their own versioning
1725+
return false;
1726+
}
1727+
1728+
$cache = new FilesystemAdapter();
1729+
$versions = $cache->get('domjudge_versions', function (ItemInterface $item, string $localVersionString): string|false {
1730+
$item->expiresAfter(86400);
1731+
1732+
$versionUrl = 'https://versions.domjudge.org';
1733+
$options = ['http' => ['timeout' => 1, 'method' => 'GET', 'header' => "User-Agent: DOMjudge#" . $this->domjudgeInstallMethod . "/" . $localVersionString . "\r\n"]];
1734+
$context = stream_context_create($options);
1735+
$response = @file_get_contents($versionUrl, false, $context);
1736+
if ($response === false) {
1737+
return false;
1738+
}
1739+
// 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"]
1740+
$tmp_versions = json_decode($response, true);
1741+
natsort($tmp_versions);
1742+
return array_reverse($tmp_versions);
1743+
});
1744+
1745+
if (!$versions) {
1746+
return false;
1747+
}
1748+
1749+
preg_match("/\d.\d.\d/", $this->domjudgeVersion, $matches);
1750+
$extractedLocalVersionString = $matches[0];
1751+
if ($this->config->get('check_new_version', false) === UpdateStrategy::Strategy_incremental) {
1752+
/* Steer towards the nearest highest patch release first
1753+
* So the expected path would be:
1754+
* DJ6.0.0 -> DJ6.0.6 -> DJ6.6.0 -> DJ9.1.2 instead of
1755+
* -> DJ6.0.[1..6] -> DJ6.[1..6].* -> DJ[7..9].*.*
1756+
* skipping all patch releases in between, when no patch release
1757+
* is available, try the highest minor and otherwise the highest Major
1758+
* instead of going to the latest release:
1759+
* DJ6.0.0 -> DJ9.1.2
1760+
*/
1761+
$patch = "/" . $localVersion[0] . "." . $localVersion[1] . ".\d/";
1762+
$minor = "/" . $localVersion[0] . ".\d.\d/";
1763+
$major = "/\d.\d.\d/";
1764+
foreach ([$patch, $minor, $major] as $regex) {
1765+
foreach ($versions as $release) {
1766+
if (preg_match($regex, $release)) {
1767+
if (strnatcmp($release, $extractedLocalVersionString) === 1) {
1768+
return $release;
1769+
}
1770+
if (strnatcmp($release, $extractedLocalVersionString) === 0 && str_contains($localVersionString, "DEV")) {
1771+
// Special case, the development version is now released
1772+
return $release;
1773+
}
1774+
}
1775+
}
1776+
}
1777+
}
1778+
elseif ($this->config->get('check_new_version', false) === UpdateStrategy::Strategy_major_release) {
1779+
/* Steer towards the latest version directly
1780+
* So the expected path would be:
1781+
* DJ6.0.0 -> DJ9.1.2
1782+
* This should be safe as doctrine migrations check for upgrades regardless of current DOMjudge release
1783+
*/
1784+
$latest = $versions[0];
1785+
if (strnatcmp($latest, $extractedLocalVersionString) === 1) {
1786+
return $latest;
1787+
}
1788+
if (strnatcmp($latest, $extractedLocalVersionString) === 0 && str_contains($localVersionString, "DEV")) {
1789+
// Special case, the development version is now released
1790+
return $latest;
1791+
}
1792+
}
1793+
return false;
1794+
}
17021795
}

webapp/src/Twig/TwigExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ public function getGlobals(): array
163163
'doc_links' => $this->dj->getDocLinks(),
164164
'allow_registration' => $selfRegistrationCategoriesCount !== 0,
165165
'enable_ranking' => $this->config->get('enable_ranking'),
166+
'new_version_available' => $this->dj->checkNewVersion(),
166167
'editor_themes' => [
167168
'vs' => ['name' => 'Visual Studio (light)'],
168169
'vs-dark' => ['name' => 'Visual Studio (dark)'],
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+
}

webapp/templates/jury/menu.html.twig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@
9393
</li>
9494
</ul>
9595
{% endif %}
96+
{% if new_version_available %}
97+
<ul class="navbar-nav">
98+
<li class="nav-item">
99+
<a class="nav-link" href="https://www.domjudge.org/download"><i class="fa-solid fa-ship fa-2x"></i> New release {{ new_version_available }} available.</a>
100+
</li>
101+
</ul>
102+
{% endif %}
96103

97104
<ul class="navbar-nav ml-auto">
98105

0 commit comments

Comments
 (0)