Skip to content

Commit 9a9ff17

Browse files
committed
Implement Version Provider
This should make the update server the authority on what the next version is, and this allows us to have more advanced and sensible versions.
1 parent 1200684 commit 9a9ff17

9 files changed

Lines changed: 324 additions & 9 deletions

File tree

src/Client/UpdateClient/UpdateClient.Admin.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ public partial class UpdateClient
2121
Log("Cannot request cache refresh, as there is no configured admin access token.");
2222
return null;
2323
}
24-
25-
var httpRequest = new HttpRequestMessage(HttpMethod.Patch, $"{Constants.FullRouteName_Api_Admin_RefreshCache}?rc={rc.QueryStringValue}");
24+
25+
var httpRequest = new HttpRequestMessage(HttpMethod.Patch,
26+
$"{Constants.FullRouteName_Api_Admin_RefreshCache}?rc={rc.QueryStringValue}");
2627
ApplyAuthorization(httpRequest);
27-
28-
if (await _http.SendAsync(httpRequest) is { IsSuccessStatusCode: false} resp)
28+
29+
if (await _http.SendAsync(httpRequest) is { IsSuccessStatusCode: false } resp)
2930
{
3031
Log("Refreshing version cache failed: received status code {0}; content body: {1}",
3132
[Enum.GetName(resp.StatusCode) ?? $"{(int)resp.StatusCode}", await resp.Content.ReadAsStringAsync()]);
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using Ryujinx.Systems.Update.Common;
2+
3+
namespace Ryujinx.Systems.Update.Client;
4+
5+
public partial class UpdateClient
6+
{
7+
/// <summary>
8+
/// Query the next version for a release channel.
9+
/// </summary>
10+
/// <param name="rc">The target release channel.</param>
11+
/// <returns>true if request success; false if non-200 series HTTP status code, null if not configured to support this endpoint.</returns>
12+
public async Task<string?> NextVersionAsync(ReleaseChannel rc)
13+
{
14+
var httpRequest = new HttpRequestMessage(HttpMethod.Get,
15+
$"{Constants.FullRouteName_Api_Versioning}/{Constants.RouteName_Api_Versioning_GetNextVersion}?rc={rc.QueryStringValue}");
16+
ApplyAuthorization(httpRequest);
17+
18+
var resp = await _http.SendAsync(httpRequest);
19+
20+
if (!resp.IsSuccessStatusCode)
21+
{
22+
Log("Increment version failed: received status code {0}; content body: {1}",
23+
[Enum.GetName(resp.StatusCode) ?? $"{(int)resp.StatusCode}", await resp.Content.ReadAsStringAsync()]);
24+
return null;
25+
}
26+
27+
return await resp.Content.ReadAsStringAsync();
28+
}
29+
30+
/// <summary>
31+
/// Requests the configured update server to increment its version for a given release channel.
32+
/// </summary>
33+
/// <param name="rc">The target release channel.</param>
34+
/// <returns>true if request success; false if non-200 series HTTP status code, null if not configured to support this endpoint.</returns>
35+
/// <remarks>Requires an authorization-bearing client configuration.</remarks>
36+
public async Task<bool?> IncrementVersionAsync(ReleaseChannel rc)
37+
{
38+
if (!_config.CanUseAdminEndpoints)
39+
{
40+
Log("Cannot request version increment, as there is no configured admin access token.");
41+
return null;
42+
}
43+
44+
var httpRequest = new HttpRequestMessage(HttpMethod.Patch,
45+
$"{Constants.FullRouteName_Api_Versioning}/{Constants.RouteName_Api_Versioning_IncrementVersion}?rc={rc.QueryStringValue}");
46+
ApplyAuthorization(httpRequest);
47+
48+
if (await _http.SendAsync(httpRequest) is { IsSuccessStatusCode: false } resp)
49+
{
50+
Log("Increment version failed: received status code {0}; content body: {1}",
51+
[Enum.GetName(resp.StatusCode) ?? $"{(int)resp.StatusCode}", await resp.Content.ReadAsStringAsync()]);
52+
return false;
53+
}
54+
55+
return true;
56+
}
57+
58+
/// <summary>
59+
/// Requests the configured update server to increment the major version by one for both stable and canary, and to reset the build number to 0.
60+
/// </summary>
61+
/// <returns>true if request success; false if non-200 series HTTP status code, null if not configured to support this endpoint.</returns>
62+
/// <remarks>Requires an authorization-bearing client configuration.</remarks>
63+
public async Task<bool?> AdvanceVersionAsync()
64+
{
65+
if (!_config.CanUseAdminEndpoints)
66+
{
67+
Log("Cannot request version advance, as there is no configured admin access token.");
68+
return null;
69+
}
70+
71+
var httpRequest = new HttpRequestMessage(HttpMethod.Patch,
72+
$"{Constants.FullRouteName_Api_Versioning}/{Constants.RouteName_Api_Versioning_AdvanceVersion}");
73+
ApplyAuthorization(httpRequest);
74+
75+
if (await _http.SendAsync(httpRequest) is { IsSuccessStatusCode: false } resp)
76+
{
77+
Log("Advancing version failed: received status code {0}; content body: {1}",
78+
[Enum.GetName(resp.StatusCode) ?? $"{(int)resp.StatusCode}", await resp.Content.ReadAsStringAsync()]);
79+
return false;
80+
}
81+
82+
return true;
83+
}
84+
}

src/Common/Constants.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ internal static class Constants
1818
public const string RouteName_Download = "download";
1919
public const string RouteName_Latest = "latest";
2020
public const string RouteName_Api_Version = "version";
21+
public const string RouteName_Api_Versioning = "versioning";
22+
public const string RouteName_Api_Versioning_GetNextVersion = "next";
23+
public const string RouteName_Api_Versioning_AdvanceVersion = "advance";
24+
public const string RouteName_Api_Versioning_IncrementVersion = "increment";
2125
public const string RouteName_Api_Meta = "meta";
2226
public const string RouteName_Api_Admin = "admin";
2327
public const string RouteName_Api_Admin_RefreshCache = "refresh_cache";
2428

2529
public const string FullRouteName_Api_Meta = $"{FullApiPrefix}{RouteName_Api_Meta}";
2630
public const string FullRouteName_Api_Version = $"{FullApiPrefix}{RouteName_Api_Version}";
31+
public const string FullRouteName_Api_Versioning = $"{FullApiPrefix}{RouteName_Api_Versioning}";
2732
public const string FullRouteName_Api_Admin_RefreshCache = $"{FullApiPrefix}{RouteName_Api_Admin}/{RouteName_Api_Admin_RefreshCache}";
2833
}

src/Server/Config.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System.Diagnostics.CodeAnalysis;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
24
using Gommon;
35
using Microsoft.Extensions.Configuration.Json;
46
using Microsoft.Extensions.FileProviders;
7+
using Ryujinx.Systems.Update.Server.Services;
58

69
namespace Ryujinx.Systems.Update.Server;
710

@@ -41,7 +44,7 @@ public static bool UseVersionPinning(string[] args,
4144
);
4245
}
4346

44-
if (File.Exists("config/versionPinning.json"))
47+
if (File.Exists("config/versionPinning.json"))
4548
jcs = new()
4649
{
4750
FileProvider = DiskProvider,
@@ -52,4 +55,31 @@ public static bool UseVersionPinning(string[] args,
5255

5356
return jcs != null;
5457
}
58+
59+
public static void TryUseVersionProvider(this WebApplicationBuilder builder, string[] args)
60+
{
61+
if (args.Any(x => x.EqualsIgnoreCase("--gen-version-provider")))
62+
{
63+
if (!File.Exists("config/versionProvider.json"))
64+
File.WriteAllText("config/versionProvider.json",
65+
JsonSerializer.Serialize(new VersionProvider
66+
{
67+
Stable = new()
68+
{
69+
Format = "1.{MAJOR}.0",
70+
Major = 3,
71+
Build = 0
72+
},
73+
Canary = new()
74+
{
75+
Format = "1.{MAJOR}.{BUILD}",
76+
Major = 3,
77+
Build = 2000
78+
}
79+
}, JSCtx.ReadableDefault.VersionProvider));
80+
}
81+
82+
if (File.Exists("config/versionProvider.json"))
83+
builder.Services.AddSingleton<VersionProviderService>();
84+
}
5585
}

src/Server/Controllers/Api/v1/Admin/RefreshCacheController.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class RefreshCacheController : ControllerBase
1919
[HttpPatch]
2020
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
2121
[ProducesResponseType(StatusCodes.Status418ImATeapot)]
22+
[ProducesResponseType(StatusCodes.Status404NotFound)]
2223
[ProducesResponseType(StatusCodes.Status429TooManyRequests)]
2324
[ProducesResponseType(StatusCodes.Status202Accepted)]
2425
[SuppressMessage("ReSharper.DPA", "DPA0011: High execution time of MVC action")]
@@ -34,13 +35,13 @@ public async Task<ActionResult> Action([FromQuery] string rc)
3435

3536
if (!AdminEndpointMetadata.AccessToken.EqualsIgnoreCase(HttpContext.Request.Headers.Authorization))
3637
return Unauthorized();
37-
38+
3839
var minutesSinceLastRefresh = (DateTimeOffset.Now - LastRefreshes[releaseChannel]).TotalMinutes;
3940
if (minutesSinceLastRefresh <= 1)
4041
return Problem("Try again later.", statusCode: 429);
4142

4243
await HttpContext.RequestServices.GetCacheFor(releaseChannel).RefreshAsync();
43-
44+
4445
LastRefreshes[releaseChannel] = DateTimeOffset.Now;
4546

4647
return Accepted();
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using Gommon;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Ryujinx.Systems.Update.Common;
4+
using Ryujinx.Systems.Update.Server.Services;
5+
6+
namespace Ryujinx.Systems.Update.Server.Controllers.Admin;
7+
8+
[Route(Constants.FullRouteName_Api_Versioning)]
9+
[ApiController]
10+
public class VersioningController : Controller
11+
{
12+
[HttpGet(Constants.RouteName_Api_Versioning_GetNextVersion)]
13+
public async Task<ActionResult<string>> GetNext([FromQuery] string rc)
14+
{
15+
if (!rc.TryParseAsReleaseChannel(out var releaseChannel))
16+
return Problem(
17+
$"Unknown release channel '{rc}'; valid are '{Constants.StableRoute}' and '{Constants.CanaryRoute}'",
18+
statusCode: 404);
19+
20+
var versionProviderService = HttpContext.RequestServices
21+
.GetService<VersionProviderService>();
22+
23+
if (versionProviderService is null)
24+
return Problem("This instance of Ryubing UpdateServer is not configured to support this endpoint.",
25+
statusCode: 418);
26+
27+
return Ok(versionProviderService.GetNextVersion(releaseChannel));
28+
}
29+
30+
[HttpPatch(Constants.RouteName_Api_Versioning_IncrementVersion)]
31+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
32+
[ProducesResponseType(StatusCodes.Status418ImATeapot)]
33+
[ProducesResponseType(StatusCodes.Status404NotFound)]
34+
[ProducesResponseType(StatusCodes.Status202Accepted)]
35+
public async Task<ActionResult> Increment([FromQuery] string rc)
36+
{
37+
if (!AdminEndpointMetadata.Enabled)
38+
return Problem("This instance of Ryubing UpdateServer is not configured to support this endpoint.",
39+
statusCode: 418);
40+
41+
if (!rc.TryParseAsReleaseChannel(out var releaseChannel))
42+
return Problem(
43+
$"Unknown release channel '{rc}'; valid are '{Constants.StableRoute}' and '{Constants.CanaryRoute}'",
44+
statusCode: 404);
45+
46+
if (!AdminEndpointMetadata.AccessToken.EqualsIgnoreCase(HttpContext.Request.Headers.Authorization))
47+
return Unauthorized();
48+
49+
var versionProviderService = HttpContext.RequestServices
50+
.GetService<VersionProviderService>();
51+
52+
if (versionProviderService is null)
53+
return Problem("This instance of Ryubing UpdateServer is not configured to support this endpoint.",
54+
statusCode: 418);
55+
56+
versionProviderService.IncrementBuild(releaseChannel);
57+
58+
return Ok();
59+
}
60+
61+
[HttpPatch(Constants.RouteName_Api_Versioning_AdvanceVersion)]
62+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
63+
[ProducesResponseType(StatusCodes.Status418ImATeapot)]
64+
[ProducesResponseType(StatusCodes.Status202Accepted)]
65+
public async Task<ActionResult> Advance()
66+
{
67+
if (!AdminEndpointMetadata.Enabled)
68+
return Problem("This instance of Ryubing UpdateServer is not configured to support this endpoint.",
69+
statusCode: 418);
70+
71+
72+
if (!AdminEndpointMetadata.AccessToken.EqualsIgnoreCase(HttpContext.Request.Headers.Authorization))
73+
return Unauthorized();
74+
75+
var versionProviderService = HttpContext.RequestServices
76+
.GetService<VersionProviderService>();
77+
78+
if (versionProviderService is null)
79+
return Problem("This instance of Ryubing UpdateServer is not configured to support this endpoint.",
80+
statusCode: 418);
81+
82+
versionProviderService.Advance();
83+
84+
return Ok();
85+
}
86+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using Gommon;
4+
5+
namespace Ryujinx.Systems.Update.Server;
6+
7+
8+
[JsonSerializable(typeof(VersionProvider))]
9+
internal partial class JSCtx : JsonSerializerContext
10+
{
11+
public static JSCtx ReadableDefault { get; } = new(new JsonSerializerOptions
12+
{
13+
WriteIndented = true
14+
});
15+
}
16+
17+
public class VersionProvider
18+
{
19+
public Entry Stable { get; set; } = new();
20+
public Entry Canary { get; set; } = new();
21+
22+
public void IncrementAndReset()
23+
{
24+
Stable.Major++;
25+
Stable.Build = 0;
26+
Canary.Major = Stable.Major;
27+
Canary.Build = 0;
28+
Save();
29+
}
30+
31+
public void Save()
32+
=> File.WriteAllText("config/versionProvider.json",
33+
JsonSerializer.Serialize(this, JSCtx.ReadableDefault.VersionProvider)
34+
);
35+
36+
public static VersionProvider? Read() =>
37+
JsonSerializer.Deserialize(File.ReadAllText("config/versionProvider.json"),
38+
JSCtx.ReadableDefault.VersionProvider);
39+
40+
public record Entry
41+
{
42+
public string Format { get; set; } = "1.{MAJOR}.{BUILD}";
43+
public ulong Major { get; set; }
44+
public ulong Build { get; set; }
45+
46+
public Entry CopyIncrement() => this with { Build = Build + 1 };
47+
48+
public override string ToString() =>
49+
Format
50+
.ReplaceIgnoreCase("{MAJOR}", Major)
51+
.ReplaceIgnoreCase("{BUILD}", Build);
52+
}
53+
}

src/Server/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
if (Config.UseVersionPinning(args, out var configSource))
1212
builder.Configuration.Sources.Add(configSource);
1313

14+
builder.TryUseVersionProvider(args);
15+
1416
if (CommandLineState.ListenPort != null)
1517
builder.WebHost.ConfigureKestrel(options => options.ListenLocalhost(CommandLineState.ListenPort.Value));
1618

@@ -33,8 +35,6 @@
3335

3436
var app = builder.Build();
3537

36-
var b = app.Configuration;
37-
3838
app.UseForwardedHeaders();
3939

4040
Swagger.TryMapUi(app);

0 commit comments

Comments
 (0)