Implemented version fetching from source control git server. Added self version detection and update checks #8
7
Moonlight.Api/Configuration/VersionOptions.cs
Normal file
7
Moonlight.Api/Configuration/VersionOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Api.Configuration;
|
||||||
|
|
||||||
|
public class VersionOptions
|
||||||
|
{
|
||||||
|
public bool OfflineMode { get; set; }
|
||||||
|
public string? CurrentOverride { get; set; }
|
||||||
|
}
|
||||||
46
Moonlight.Api/Http/Controllers/Admin/VersionsController.cs
Normal file
46
Moonlight.Api/Http/Controllers/Admin/VersionsController.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Moonlight.Api.Mappers;
|
||||||
|
using Moonlight.Api.Services;
|
||||||
|
using Moonlight.Shared;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/versions")]
|
||||||
|
[Authorize(Policy = Permissions.System.Versions)]
|
||||||
|
public class VersionsController : Controller
|
||||||
|
{
|
||||||
|
private readonly VersionService VersionService;
|
||||||
|
|
||||||
|
public VersionsController(VersionService versionService)
|
||||||
|
{
|
||||||
|
VersionService = versionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<VersionDto[]>> GetAsync()
|
||||||
|
{
|
||||||
|
var versions = await VersionService.GetVersionsAsync();
|
||||||
|
return VersionMapper.ToDtos(versions).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("instance")]
|
||||||
|
public async Task<ActionResult<VersionDto>> GetInstanceAsync()
|
||||||
|
{
|
||||||
|
var version = await VersionService.GetInstanceVersionAsync();
|
||||||
|
return VersionMapper.ToDto(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("latest")]
|
||||||
|
public async Task<ActionResult<VersionDto>> GetLatestAsync()
|
||||||
|
{
|
||||||
|
var version = await VersionService.GetLatestVersionAsync();
|
||||||
|
|
||||||
|
if(version == null)
|
||||||
|
return Problem("Unable to retrieve latest version", statusCode: 404);
|
||||||
|
|
||||||
|
return VersionMapper.ToDto(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Moonlight.Api/Mappers/VersionMapper.cs
Normal file
15
Moonlight.Api/Mappers/VersionMapper.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Moonlight.Api.Models;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin;
|
||||||
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Mappers;
|
||||||
|
|
||||||
|
[Mapper]
|
||||||
|
[SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")]
|
||||||
|
[SuppressMessage("Mapper", "RMG012:Source member was not found for target member")]
|
||||||
|
public static partial class VersionMapper
|
||||||
|
{
|
||||||
|
public static partial IEnumerable<VersionDto> ToDtos(IEnumerable<MoonlightVersion> versions);
|
||||||
|
public static partial VersionDto ToDto(MoonlightVersion version);
|
||||||
|
}
|
||||||
12
Moonlight.Api/Models/MoonlightVersion.cs
Normal file
12
Moonlight.Api/Models/MoonlightVersion.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Moonlight.Api.Models;
|
||||||
|
|
||||||
|
// Notes:
|
||||||
|
// Identifier - This needs to be the branch to clone to build this version if
|
||||||
|
// you want to use the container helper
|
||||||
|
|
||||||
|
public record MoonlightVersion(
|
||||||
|
string Identifier,
|
||||||
|
bool IsPreRelease,
|
||||||
|
bool IsDevelopment,
|
||||||
|
DateTimeOffset CreatedAt
|
||||||
|
);
|
||||||
@@ -1,26 +1,36 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Moonlight.Api.Helpers;
|
using Moonlight.Api.Helpers;
|
||||||
|
|
||||||
namespace Moonlight.Api.Services;
|
namespace Moonlight.Api.Services;
|
||||||
|
|
||||||
public class ApplicationService : IHostedLifecycleService
|
public class ApplicationService : IHostedService
|
||||||
{
|
{
|
||||||
|
private readonly VersionService VersionService;
|
||||||
|
private readonly ILogger<ApplicationService> Logger;
|
||||||
|
|
||||||
public DateTimeOffset StartedAt { get; private set; }
|
public DateTimeOffset StartedAt { get; private set; }
|
||||||
public string VersionName { get; private set; } = "N/A";
|
public string VersionName { get; private set; } = "N/A";
|
||||||
public bool IsUpToDate { get; set; } = true;
|
public bool IsUpToDate { get; set; } = true;
|
||||||
public string OperatingSystem { get; private set; } = "N/A";
|
public string OperatingSystem { get; private set; } = "N/A";
|
||||||
|
|
||||||
|
public ApplicationService(VersionService versionService, ILogger<ApplicationService> logger)
|
||||||
|
{
|
||||||
|
VersionService = versionService;
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<long> GetMemoryUsageAsync()
|
public Task<long> GetMemoryUsageAsync()
|
||||||
{
|
{
|
||||||
using var currentProcess = Process.GetCurrentProcess();
|
using var currentProcess = Process.GetCurrentProcess();
|
||||||
return Task.FromResult(currentProcess.WorkingSet64);
|
return Task.FromResult(currentProcess.WorkingSet64);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<double> GetCpuUsageAsync()
|
public async Task<double> GetCpuUsageAsync()
|
||||||
{
|
{
|
||||||
using var currentProcess = Process.GetCurrentProcess();
|
using var currentProcess = Process.GetCurrentProcess();
|
||||||
|
|
||||||
// Get initial values
|
// Get initial values
|
||||||
var startCpuTime = currentProcess.TotalProcessorTime;
|
var startCpuTime = currentProcess.TotalProcessorTime;
|
||||||
var startTime = DateTime.UtcNow;
|
var startTime = DateTime.UtcNow;
|
||||||
@@ -39,26 +49,37 @@ public class ApplicationService : IHostedLifecycleService
|
|||||||
|
|
||||||
return Math.Round(cpuUsagePercent, 2);
|
return Math.Round(cpuUsagePercent, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartedAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
StartedAt = DateTimeOffset.UtcNow;
|
StartedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
// TODO: Update / version check
|
|
||||||
|
|
||||||
VersionName = "v2.1.0 (a2d4edc0e5)";
|
|
||||||
IsUpToDate = true;
|
|
||||||
|
|
||||||
OperatingSystem = OsHelper.GetName();
|
OperatingSystem = OsHelper.GetName();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentVersion = await VersionService.GetInstanceVersionAsync();
|
||||||
|
var latestVersion = await VersionService.GetLatestVersionAsync();
|
||||||
|
|
||||||
|
VersionName = currentVersion.Identifier;
|
||||||
|
IsUpToDate = latestVersion == null || currentVersion.Identifier == latestVersion.Identifier;
|
||||||
|
|
||||||
|
Logger.LogInformation("Running Moonlight Panel {version} on {operatingSystem}", VersionName, OperatingSystem);
|
||||||
|
|
||||||
|
if (!IsUpToDate)
|
||||||
|
Logger.LogWarning("Your instance is not up-to-date");
|
||||||
|
|
||||||
|
if (currentVersion.IsDevelopment)
|
||||||
|
Logger.LogWarning("Your instance is running a development version");
|
||||||
|
|
||||||
|
if (currentVersion.IsPreRelease)
|
||||||
|
Logger.LogWarning("Your instance is running a pre-release version");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "An unhandled exception occurred while fetching version details");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Unused
|
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
||||||
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
||||||
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
140
Moonlight.Api/Services/VersionService.cs
Normal file
140
Moonlight.Api/Services/VersionService.cs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Moonlight.Api.Configuration;
|
||||||
|
using Moonlight.Api.Models;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Services;
|
||||||
|
|
||||||
|
public partial class VersionService
|
||||||
|
{
|
||||||
|
private readonly IOptions<VersionOptions> Options;
|
||||||
|
private readonly IHttpClientFactory HttpClientFactory;
|
||||||
|
|
||||||
|
private const string VersionPath = "/app/version";
|
||||||
|
private const string GiteaServer = "https://git.battlestati.one";
|
||||||
|
private const string GiteaRepository = "Moonlight-Panel/Moonlight";
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^v(?!1(\.|$))\d+\.[A-Za-z0-9]+(\.[A-Za-z0-9]+)*$")]
|
||||||
|
private static partial Regex RegexFilter();
|
||||||
|
|
||||||
|
public VersionService(
|
||||||
|
IOptions<VersionOptions> options,
|
||||||
|
IHttpClientFactory httpClientFactory
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Options = options;
|
||||||
|
HttpClientFactory = httpClientFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MoonlightVersion[]> GetVersionsAsync()
|
||||||
|
{
|
||||||
|
if (Options.Value.OfflineMode)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var versions = new List<MoonlightVersion>();
|
||||||
|
var httpClient = HttpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
const string tagsPath = $"{GiteaServer}/api/v1/repos/{GiteaRepository}/tags";
|
||||||
|
|
||||||
|
await using var tagsJsonStream = await httpClient.GetStreamAsync(tagsPath);
|
||||||
|
var tagsJson = await JsonNode.ParseAsync(tagsJsonStream);
|
||||||
|
|
||||||
|
if (tagsJson != null)
|
||||||
|
{
|
||||||
|
foreach (var node in tagsJson.AsArray())
|
||||||
|
{
|
||||||
|
if (node == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var name = node["name"]?.GetValue<string>() ?? "N/A";
|
||||||
|
var createdAt = node["createdAt"]?.GetValue<DateTimeOffset>() ?? DateTimeOffset.MinValue;
|
||||||
|
|
||||||
|
if(!RegexFilter().IsMatch(name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
versions.Add(new MoonlightVersion(
|
||||||
|
name,
|
||||||
|
name.EndsWith('b'),
|
||||||
|
false,
|
||||||
|
createdAt
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branches
|
||||||
|
const string branchesPath = $"{GiteaServer}/api/v1/repos/{GiteaRepository}/branches";
|
||||||
|
|
||||||
|
await using var branchesJsonStream = await httpClient.GetStreamAsync(branchesPath);
|
||||||
|
var branchesJson = await JsonNode.ParseAsync(branchesJsonStream);
|
||||||
|
|
||||||
|
if (branchesJson != null)
|
||||||
|
{
|
||||||
|
foreach (var node in branchesJson.AsArray())
|
||||||
|
{
|
||||||
|
if (node == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var name = node["name"]?.GetValue<string>() ?? "N/A";
|
||||||
|
var commit = node["commit"];
|
||||||
|
|
||||||
|
if (commit == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var createdAt = commit["timestamp"]?.GetValue<DateTimeOffset>() ?? DateTimeOffset.MinValue;
|
||||||
|
|
||||||
|
if(!RegexFilter().IsMatch(name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
versions.Add(new MoonlightVersion(
|
||||||
|
name,
|
||||||
|
name.EndsWith('b'),
|
||||||
|
true,
|
||||||
|
createdAt
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MoonlightVersion> GetInstanceVersionAsync()
|
||||||
|
{
|
||||||
|
var knownVersions = await GetVersionsAsync();
|
||||||
|
|
||||||
|
string versionIdentifier;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(Options.Value.CurrentOverride))
|
||||||
|
versionIdentifier = Options.Value.CurrentOverride;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (File.Exists(VersionPath))
|
||||||
|
versionIdentifier = await File.ReadAllTextAsync(VersionPath);
|
||||||
|
else
|
||||||
|
versionIdentifier = "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
var version = knownVersions.FirstOrDefault(x => x.Identifier == versionIdentifier);
|
||||||
|
|
||||||
|
if (version != null)
|
||||||
|
return version;
|
||||||
|
|
||||||
|
return new MoonlightVersion(
|
||||||
|
versionIdentifier,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
DateTimeOffset.UtcNow
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MoonlightVersion?> GetLatestVersionAsync()
|
||||||
|
{
|
||||||
|
var versions = await GetVersionsAsync();
|
||||||
|
|
||||||
|
return versions
|
||||||
|
.Where(x => x is { IsDevelopment: false, IsPreRelease: false })
|
||||||
|
.OrderByDescending(x => x.CreatedAt)
|
||||||
|
.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ using Moonlight.Api.Helpers;
|
|||||||
using Moonlight.Api.Implementations;
|
using Moonlight.Api.Implementations;
|
||||||
using Moonlight.Api.Interfaces;
|
using Moonlight.Api.Interfaces;
|
||||||
using Moonlight.Api.Services;
|
using Moonlight.Api.Services;
|
||||||
using SessionOptions = Microsoft.AspNetCore.Builder.SessionOptions;
|
using SessionOptions = Moonlight.Api.Configuration.SessionOptions;
|
||||||
|
|
||||||
namespace Moonlight.Api.Startup;
|
namespace Moonlight.Api.Startup;
|
||||||
|
|
||||||
@@ -38,6 +38,11 @@ public partial class Startup
|
|||||||
|
|
||||||
builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
|
builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
|
||||||
builder.Services.AddScoped<FrontendService>();
|
builder.Services.AddScoped<FrontendService>();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version");
|
||||||
|
builder.Services.AddSingleton<VersionService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UseBase(WebApplication application)
|
private static void UseBase(WebApplication application)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public sealed class PermissionProvider : IPermissionProvider
|
|||||||
new PermissionCategory("System", typeof(CogIcon), [
|
new PermissionCategory("System", typeof(CogIcon), [
|
||||||
new Permission(Permissions.System.Info, "Info", "View system info"),
|
new Permission(Permissions.System.Info, "Info", "View system info"),
|
||||||
new Permission(Permissions.System.Diagnose, "Diagnose", "Run diagnostics"),
|
new Permission(Permissions.System.Diagnose, "Diagnose", "Run diagnostics"),
|
||||||
|
new Permission(Permissions.System.Versions, "Versions", "Look at the available versions"),
|
||||||
]),
|
]),
|
||||||
new PermissionCategory("API Keys", typeof(KeyIcon), [
|
new PermissionCategory("API Keys", typeof(KeyIcon), [
|
||||||
new Permission(Permissions.ApiKeys.Create, "Create", "Create new API keys"),
|
new Permission(Permissions.ApiKeys.Create, "Create", "Create new API keys"),
|
||||||
|
|||||||
3
Moonlight.Shared/Http/Responses/Admin/VersionDto.cs
Normal file
3
Moonlight.Shared/Http/Responses/Admin/VersionDto.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Moonlight.Shared.Http.Responses.Admin;
|
||||||
|
|
||||||
|
public record VersionDto(string Identifier, bool IsPreRelease, bool IsDevelopment, DateTimeOffset CreatedAt);
|
||||||
@@ -43,6 +43,9 @@ namespace Moonlight.Shared.Http;
|
|||||||
[JsonSerializable(typeof(UpdateThemeDto))]
|
[JsonSerializable(typeof(UpdateThemeDto))]
|
||||||
[JsonSerializable(typeof(PagedData<ThemeDto>))]
|
[JsonSerializable(typeof(PagedData<ThemeDto>))]
|
||||||
[JsonSerializable(typeof(ThemeDto))]
|
[JsonSerializable(typeof(ThemeDto))]
|
||||||
|
|
||||||
|
//Misc
|
||||||
|
[JsonSerializable(typeof(VersionDto))]
|
||||||
public partial class SerializationContext : JsonSerializerContext
|
public partial class SerializationContext : JsonSerializerContext
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -53,5 +53,6 @@ public static class Permissions
|
|||||||
|
|
||||||
public const string Info = $"{Prefix}{Section}.{nameof(Info)}";
|
public const string Info = $"{Prefix}{Section}.{nameof(Info)}";
|
||||||
public const string Diagnose = $"{Prefix}{Section}.{nameof(Diagnose)}";
|
public const string Diagnose = $"{Prefix}{Section}.{nameof(Diagnose)}";
|
||||||
|
public const string Versions = $"{Prefix}{Section}.{nameof(Versions)}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user