Skip to content

Commit 8ce5a60

Browse files
committed
feature: Added support for figuring out the latest version with System.Version comparer.
On Forgejo, the API gets confused with a bunch of releases on different tags pointing to the same commit. Before this change, Canary 1.3.220 was treated as being latest despite Canary 1.3.265 being a thing.
1 parent 112e2e4 commit 8ce5a60

4 files changed

Lines changed: 142 additions & 76 deletions

File tree

src/Server/Services/Forgejo/ForgejoVersionCache.cs

Lines changed: 125 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ public class ForgejoVersionCache : SafeDictionary<string, VersionCacheEntry>, IV
2121
public string ProjectPath => _cachedProject!.full_name!;
2222

2323
private string? _latestTag;
24+
private bool _deriveLatestManually;
2425

25-
private PinnedVersions _pinnedVersions = null!;
26+
private PinnedVersions _pinnedVersions = null!; //late-init, see Init method
2627

27-
PinnedVersions IVersionCache.PinnedVersions => _pinnedVersions; //late-init, see Init method
28+
PinnedVersions IVersionCache.PinnedVersions => _pinnedVersions;
2829

2930
// ReSharper disable once ReplaceWithFieldKeyword
3031

@@ -69,46 +70,49 @@ public ForgejoVersionCache(IConfiguration config, ForgejoService forgejoService,
6970

7071
public string ReleaseUrlFormat => $"{_forgejoEndpoint.TrimEnd('/')}/{ProjectPath}/releases/tag/{{0}}";
7172

72-
public void Init(string projectId, PinnedVersions pinnedVersions) => Executor.ExecuteBackgroundAsync(async () =>
73-
{
74-
try
75-
{
76-
_cachedProject ??= await _fj.Client.Repository.GetAsync(projectId.Split('/')[0], projectId.Split('/')[1]);
77-
}
78-
catch (ForgejoClientException e)
73+
public void Init(string projectId, bool deriveLatestVersionManually, PinnedVersions pinnedVersions) =>
74+
Executor.ExecuteBackgroundAsync(async () =>
7975
{
80-
_logger.LogError(
81-
"Encountered error when getting the project ({project}) for the version cache. Aborting. Error: {errorMessage}",
82-
projectId, e.Message);
83-
return;
84-
}
85-
86-
_logger.LogInformation("Initializing version cache for {project}", ProjectName);
76+
_deriveLatestManually = deriveLatestVersionManually;
77+
try
78+
{
79+
_cachedProject ??=
80+
await _fj.Client.Repository.GetAsync(projectId.Split('/')[0], projectId.Split('/')[1]);
81+
}
82+
catch (ForgejoClientException e)
83+
{
84+
_logger.LogError(
85+
"Encountered error when getting the project ({project}) for the version cache. Aborting. Error: {errorMessage}",
86+
projectId, e.Message);
87+
return;
88+
}
8789

88-
_pinnedVersions = pinnedVersions;
90+
_logger.LogInformation("Initializing version cache for {project}", ProjectName);
8991

90-
await RefreshAsync();
92+
_pinnedVersions = pinnedVersions;
9193

92-
if (_refreshTimer == null)
93-
{
94-
string howToRefresh = AdminEndpointMetadata.Enabled
95-
? $"using the {Constants.FullRouteName_Api_Admin_RefreshCache} endpoint or restarting the server."
96-
: "restarting the server. Set an admin access token in appsettings.json to enable an endpoint to do this.";
94+
await RefreshAsync();
9795

98-
_logger.LogInformation(
99-
"Periodic version cache refreshing is disabled for {project}. It can be refreshed by {means}",
100-
ProjectName, howToRefresh);
101-
return;
102-
}
96+
if (_refreshTimer == null)
97+
{
98+
string howToRefresh = AdminEndpointMetadata.Enabled
99+
? $"using the {Constants.FullRouteName_Api_Admin_RefreshCache} endpoint or restarting the server."
100+
: "restarting the server. Set an admin access token in appsettings.json to enable an endpoint to do this.";
101+
102+
_logger.LogInformation(
103+
"Periodic version cache refreshing is disabled for {project}. It can be refreshed by {means}",
104+
ProjectName, howToRefresh);
105+
return;
106+
}
103107

104-
_logger.LogInformation("Refreshing version cache for {project} every {timePeriod} minutes.",
105-
ProjectName, _refreshTimer.Period.TotalMinutes);
108+
_logger.LogInformation("Refreshing version cache for {project} every {timePeriod} minutes.",
109+
ProjectName, _refreshTimer.Period.TotalMinutes);
106110

107-
while (await _refreshTimer.WaitForNextTickAsync())
108-
{
109-
await RefreshAsync();
110-
}
111-
});
111+
while (await _refreshTimer.WaitForNextTickAsync())
112+
{
113+
await RefreshAsync();
114+
}
115+
});
112116

113117
public Task<IDisposable> TakeLockAsync() => _semaphore.TakeAsync();
114118

@@ -138,23 +142,28 @@ public async Task RefreshAsync()
138142
{
139143
_logger.LogInformation("Reloading version cache for {project}", ProjectName);
140144

141-
try
142-
{
143-
_latestTag = await _fj.Client.Repository.GetReleaseLatestAsync(
144-
ProjectPath.Split('/')[0], ProjectPath.Split('/')[1]
145-
).Then(r => r.tag_name);
146-
}
147-
catch (Exception e)
145+
if (!_deriveLatestManually)
148146
{
149-
_logger.LogWarning("Errored trying to get latest release for {project}; aborting. Message: {message}",
150-
ProjectName, e.Message);
151-
return;
152-
}
147+
_logger.LogInformation("Requesting latest version for {project}.", ProjectPath);
148+
try
149+
{
150+
_latestTag = await _fj.Client.Repository.GetReleaseLatestAsync(
151+
ProjectPath.Split('/')[0], ProjectPath.Split('/')[1]
152+
).Then(r => r.tag_name);
153+
_logger.LogInformation("Latest received: {latestTag}.", _latestTag);
154+
}
155+
catch (Exception e)
156+
{
157+
_logger.LogWarning("Errored trying to get latest release for {project}; aborting. Message: {message}",
158+
ProjectName, e.Message);
159+
return;
160+
}
153161

154-
if (_latestTag is null)
155-
{
156-
_logger.LogWarning("Latest version for {project} was a 404, aborting.", ProjectName);
157-
return;
162+
if (_latestTag is null)
163+
{
164+
_logger.LogWarning("Latest version for {project} was a 404, aborting.", ProjectName);
165+
return;
166+
}
158167
}
159168

160169
var sw = Stopwatch.StartNew();
@@ -164,6 +173,43 @@ public async Task RefreshAsync()
164173
ProjectPath.Split('/')[1]
165174
);
166175

176+
if (_deriveLatestManually)
177+
{
178+
_logger.LogInformation("Deriving latest version for {project}.", ProjectPath);
179+
try
180+
{
181+
Dictionary<Version, string> versionMapping = new();
182+
foreach (var ver in releases.Select(x => x.TagName).Where(x => x != null))
183+
{
184+
if (Version.TryParse(ver, out Version? result))
185+
{
186+
versionMapping[result] = ver;
187+
}
188+
else
189+
{
190+
_logger.LogWarning(
191+
"'{version}' is not parseable as a .NET version. It will not be included in figuring out what release is latest.",
192+
ver);
193+
}
194+
}
195+
196+
_latestTag = versionMapping.OrderByDescending(x => x.Key).FirstOrDefault().Value;
197+
_logger.LogInformation("Latest found: {latestTag}.", _latestTag);
198+
}
199+
catch (Exception e)
200+
{
201+
_logger.LogWarning("Errored trying to get latest release for {project}; aborting. Message: {message}",
202+
ProjectName, e.Message);
203+
return;
204+
}
205+
206+
if (_latestTag is null)
207+
{
208+
_logger.LogWarning("Latest version for {project} was a 404, aborting.", ProjectName);
209+
return;
210+
}
211+
}
212+
167213
var tempCacheEntries = releases.Select(release =>
168214
new VersionCacheEntry
169215
{
@@ -237,45 +283,53 @@ public static void InitializeVersionCaches(WebApplication app)
237283
var versionCacheSection = app.Configuration.GetSection("Forgejo")
238284
.GetRequiredSection("VersionCacheSources");
239285

240-
var stableSource = versionCacheSection.GetValue<string>("Stable");
286+
var stableSource = versionCacheSection.GetSection("Stable");
241287

242-
if (stableSource is null)
288+
if (!stableSource.Exists())
243289
throw new Exception(
244-
"Cannot start the server without a Forgejo repository in Forgejo:VersionCacheSources:Stable");
290+
"Cannot start the server without a Forgejo repository in Forgejo:VersionCacheSources:Stable:Project");
245291

246292
var vpSection = app.Configuration.GetSection("VersionPinning");
247293

248294
var pvLogger = app.Services.Get<ILoggerFactory>().CreateLogger<PinnedVersions>();
249295

250-
var stableCache = app.Services.GetRequiredKeyedService<ForgejoVersionCache>("stableCache");
251-
stableCache.Init(stableSource,
252-
new PinnedVersions(pvLogger, vpSection.GetSection("Stable")));
296+
app.Services.GetRequiredKeyedService<ForgejoVersionCache>("stableCache").Init(
297+
stableSource.GetValue<string>("Project")!,
298+
stableSource.GetValue<bool>("DeriveLatestVersionManually"),
299+
new PinnedVersions(pvLogger, vpSection.GetSection("Stable"))
300+
);
253301

254-
var canarySource = versionCacheSection.GetValue<string>("Canary");
302+
var canarySource = versionCacheSection.GetSection("Canary");
255303

256-
if (canarySource != null)
304+
if (canarySource.Exists())
257305
{
258-
var canaryCache = app.Services.GetRequiredKeyedService<ForgejoVersionCache>("canaryCache");
259-
canaryCache.Init(canarySource,
260-
new PinnedVersions(pvLogger, vpSection.GetSection("Canary")));
306+
app.Services.GetRequiredKeyedService<ForgejoVersionCache>("canaryCache").Init(
307+
canarySource.GetValue<string>("Project")!,
308+
canarySource.GetValue<bool>("DeriveLatestVersionManually"),
309+
new PinnedVersions(pvLogger, vpSection.GetSection("Canary"))
310+
);
261311
}
262312

263-
var custom1Source = versionCacheSection.GetValue<string>("Custom1");
313+
var custom1Source = versionCacheSection.GetSection("Custom1");
264314

265-
if (custom1Source != null)
315+
if (custom1Source.Exists())
266316
{
267-
var canaryCache = app.Services.GetRequiredKeyedService<ForgejoVersionCache>("custom1Cache");
268-
canaryCache.Init(custom1Source,
269-
new PinnedVersions(pvLogger, vpSection.GetSection("Custom1")));
317+
app.Services.GetRequiredKeyedService<ForgejoVersionCache>("custom1Cache").Init(
318+
custom1Source.GetValue<string>("Project")!,
319+
custom1Source.GetValue<bool>("DeriveLatestVersionManually"),
320+
new PinnedVersions(pvLogger, vpSection.GetSection("Custom1"))
321+
);
270322
}
271323

272-
var kenjiNxSource = versionCacheSection.GetValue<string>("KenjiNX");
324+
var kenjiNxSource = versionCacheSection.GetSection("KenjiNX");
273325

274-
if (kenjiNxSource != null)
326+
if (kenjiNxSource.Exists())
275327
{
276-
var kenjiCache = app.Services.GetRequiredKeyedService<ForgejoVersionCache>("kenjinxCache");
277-
kenjiCache.Init(kenjiNxSource,
278-
new PinnedVersions(pvLogger, vpSection.GetSection("KenjiNX")));
328+
app.Services.GetRequiredKeyedService<ForgejoVersionCache>("kenjinxCache").Init(
329+
kenjiNxSource.GetValue<string>("Project")!,
330+
kenjiNxSource.GetValue<bool>("DeriveLatestVersionManually"),
331+
new PinnedVersions(pvLogger, vpSection.GetSection("KenjiNX"))
332+
);
279333
}
280334
}
281335
}

src/Server/Services/IVersionCache.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public interface IVersionCache
1616

1717
public string ReleaseUrlFormat { get; }
1818

19-
public void Init(string projectPath, PinnedVersions pinnedVersions);
19+
public void Init(string projectPath, bool deriveLatestVersionManually, PinnedVersions pinnedVersions);
2020

2121
public Task<IDisposable> TakeLockAsync();
2222

src/Server/appsettings.Development.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@
99
"Endpoint": "https://git.ryujinx.app/",
1010
"AccessToken": "https://git.greemdev.net/user/settings/applications/tokens/new",
1111
"VersionCacheSources": {
12-
"Stable": "projects/Ryubing",
13-
"Canary": "Ryubing/Canary"
12+
"Stable": {
13+
"Project": "projects/Ryubing",
14+
"DeriveLatestVersionManually": false
15+
},
16+
"Canary": {
17+
"Project": "Ryubing/Canary",
18+
"DeriveLatestVersionManually": true
19+
}
1420
}
1521
},
1622
"Admin": {

src/Server/appsettings.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@
99
"Endpoint": "https://git.ryujinx.app/",
1010
"AccessToken": "https://git.greemdev.net/user/settings/applications/tokens/new",
1111
"VersionCacheSources": {
12-
"Stable": "projects/Ryujinx",
13-
"Canary": "Ryubing/Canary"
12+
"Stable": {
13+
"Project": "projects/Ryubing",
14+
"DeriveLatestVersionManually": false
15+
},
16+
"Canary": {
17+
"Project": "Ryubing/Canary",
18+
"DeriveLatestVersionManually": true
19+
}
1420
}
1521
},
1622
"Admin": {

0 commit comments

Comments
 (0)