Skip to content

Commit

Permalink
implement api to check for the latest version and queue an update
Browse files Browse the repository at this point in the history
  • Loading branch information
hahn-kev committed Dec 2, 2024
1 parent 146856b commit 2c8dfae
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 11 deletions.
11 changes: 10 additions & 1 deletion backend/FwLite/FwLiteDesktop/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@

public partial class App : Application
{
private readonly MainPage _mainPage;

public App(MainPage mainPage)
{
_mainPage = mainPage;
InitializeComponent();
}

MainPage = mainPage;
protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(_mainPage)
{
Title = "FieldWorks lite " + AppVersion.Version
};
}
}
82 changes: 82 additions & 0 deletions backend/FwLite/FwLiteDesktop/AppUpdateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Buffers;
using System.Net.Http.Json;
using Windows.Management.Deployment;
using LexCore.Entities;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;

namespace FwLiteDesktop;

public class AppUpdateService(
IHttpClientFactory httpClientFactory,
ILogger<AppUpdateService> logger,
IPreferences preferences) : IMauiInitializeService
{


private const string LastUpdateCheck = "lastUpdateChecked";
private const string FwliteUpdateUrlEnvVar = "FWLITE_UPDATE_URL";
private const string ForceUpdateCheckEnvVar = "FWLITE_FORCE_UPDATE_CHECK";
private static readonly SearchValues<string> ValidPositiveEnvVarValues = SearchValues.Create([ "1", "true", "yes" ], StringComparison.OrdinalIgnoreCase);
private static readonly string UpdateUrl = Environment.GetEnvironmentVariable(FwliteUpdateUrlEnvVar) ?? "https://lexbox.org/api/fwlite-release/latest";

public void Initialize(IServiceProvider services)
{
_ = Task.Run(TryUpdate);
}

private async Task TryUpdate()
{
if (!ShouldCheckForUpdate()) return;
var latestRelease = await FetchRelease();
if (latestRelease is null) return;
var currentVersion = AppVersion.Version;
var shouldUpdateToRelease = String.Compare(latestRelease.Version, currentVersion, StringComparison.Ordinal) > 0;
if (!shouldUpdateToRelease)
{
logger.LogInformation("Version {CurrentVersion} is more recent than latest release {LatestRelease}, not updating", currentVersion, latestRelease.Version);
return;
}

logger.LogInformation("New version available: {Version}", latestRelease.Version);
var packageManager = new PackageManager();
var asyncOperation = packageManager.AddPackageAsync(new Uri(latestRelease.Url), [], DeploymentOptions.None);
asyncOperation.Progress = (info, progressInfo) =>
{
logger.LogInformation("Downloading update: {ProgressPercentage}%", progressInfo.percentage);
};
var result = await asyncOperation.AsTask();
if (!string.IsNullOrEmpty(result.ErrorText))
{
logger.LogError(result.ExtendedErrorCode, "Failed to download update: {ErrorText}", result.ErrorText);
return;
}
logger.LogInformation("Update downloaded, will install on next restart");
}

private async Task<FwLiteRelease?> FetchRelease()
{
try
{
var latestRelease = await httpClientFactory
.CreateClient("Lexbox")
.GetFromJsonAsync<FwLiteRelease>(UpdateUrl);
return latestRelease;
}
catch (Exception e)
{
logger.LogError(e, "Failed to fetch latest release");
return null;
}
}

private bool ShouldCheckForUpdate()
{
if (ValidPositiveEnvVarValues.Contains(Environment.GetEnvironmentVariable(ForceUpdateCheckEnvVar) ?? ""))
return true;
var lastChecked = preferences.Get(LastUpdateCheck, DateTime.MinValue);
if (lastChecked.AddDays(1) > DateTime.UtcNow) return false;
preferences.Set(LastUpdateCheck, DateTime.UtcNow);
return true;
}
}
9 changes: 9 additions & 0 deletions backend/FwLite/FwLiteDesktop/AppVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Reflection;

namespace FwLiteDesktop;

public class AppVersion
{
public static readonly string Version = typeof(AppVersion).Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "dev";
}
5 changes: 5 additions & 0 deletions backend/FwLite/FwLiteDesktop/FwLiteDesktopKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static void AddFwLiteDesktopServices(this IServiceCollection services,
#if DEBUG
environment = "Development";
#endif

var defaultDataPath = IsPackagedApp ? FileSystem.AppDataDirectory : Directory.GetCurrentDirectory();
var baseDataPath = Path.GetFullPath(configuration.GetSection("FwLiteDesktop").GetValue<string>("BaseDataDir") ?? defaultDataPath);
Directory.CreateDirectory(baseDataPath);
Expand All @@ -41,7 +42,11 @@ public static void AddFwLiteDesktopServices(this IServiceCollection services,
//using a lambda here means that the serverManager will be disposed when the app is disposed
services.AddSingleton<ServerManager>(_ => serverManager);
services.AddSingleton<IMauiInitializeService>(_ => _.GetRequiredService<ServerManager>());
services.AddHttpClient();
if (IsPackagedApp)
services.AddSingleton<IMauiInitializeService, AppUpdateService>();
services.AddSingleton<IHostEnvironment>(_ => _.GetRequiredService<ServerManager>().WebServices.GetRequiredService<IHostEnvironment>());
services.AddSingleton<IPreferences>(Preferences.Default);
configuration.Add<ServerConfigSource>(source => source.ServerManager = serverManager);
services.AddOptions<LocalWebAppConfig>().BindConfiguration("LocalWebApp");
logging.AddFile(Path.Combine(baseDataPath, "app.log"));
Expand Down
3 changes: 2 additions & 1 deletion backend/FwLite/FwLiteDesktop/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"Run": {
"commandName": "Project",
"environmentVariables": {
"COREHOST_TRACE": "0"
"COREHOST_TRACE": "0",
"FWLITE_UPDATE_URL": "http://localhost:3000/api/fwlite-release/latest"
}
},
"Windows Machine": {
Expand Down
37 changes: 28 additions & 9 deletions backend/LexBoxApi/Controllers/FwLiteReleaseController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using LexCore.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Hybrid;
Expand All @@ -8,23 +9,41 @@ namespace LexBoxApi.Controllers;

[ApiController]
[Route("/api/fwlite-release")]
[ApiExplorerSettings(GroupName = LexBoxKernel.OpenApiPublicDocumentName)]
public class FwLiteReleaseController(IHttpClientFactory factory, HybridCache cache) : ControllerBase
{
private const string GithubLatestRelease = "GithubLatestRelease";

[HttpGet("download-latest")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> LatestDownload()
{
var latestRelease = await GetLatestRelease(default);
if (latestRelease is null) return NotFound();
return Redirect(latestRelease.Url);
}

[HttpGet("latest")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> Latest()
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async ValueTask<ActionResult<FwLiteRelease>> LatestRelease()
{
var latestRelease = await GetLatestRelease(default);
if (latestRelease is null) return NotFound();
return latestRelease;
}

private async ValueTask<FwLiteRelease?> GetLatestRelease(CancellationToken token)
{
var latestReleaseUrl = await cache.GetOrCreateAsync(GithubLatestRelease,
LatestVersionFromGithub,
new HybridCacheEntryOptions() { Expiration = TimeSpan.FromDays(1) });
if (latestReleaseUrl is null) return NotFound();
return Redirect(latestReleaseUrl);
return await cache.GetOrCreateAsync(GithubLatestRelease,
FetchLatestReleaseFromGithub,
new HybridCacheEntryOptions() { Expiration = TimeSpan.FromDays(1) }, cancellationToken: token);
}

private async ValueTask<string?> LatestVersionFromGithub(CancellationToken token)
private async ValueTask<FwLiteRelease?> FetchLatestReleaseFromGithub(CancellationToken token)
{
var response = await factory.CreateClient("Github")
.SendAsync(new HttpRequestMessage(HttpMethod.Get,
Expand Down Expand Up @@ -55,7 +74,7 @@ public async Task<ActionResult> Latest()
var msixBundle = release.Assets.FirstOrDefault(a => a.Name.EndsWith(".msixbundle", StringComparison.InvariantCultureIgnoreCase));
if (msixBundle is not null)
{
return msixBundle.BrowserDownloadUrl;
return new FwLiteRelease(release.TagName, msixBundle.BrowserDownloadUrl);
}
}
return null;
Expand Down Expand Up @@ -137,7 +156,7 @@ public class Release
/// The name of the tag.
/// </summary>
[JsonPropertyName("tag_name")]
public string? TagName { get; set; }
public required string TagName { get; set; }

[JsonPropertyName("tarball_url")]
public string? TarballUrl { get; set; }
Expand Down
3 changes: 3 additions & 0 deletions backend/LexCore/Entities/FwLiteRelease.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace LexCore.Entities;

public record FwLiteRelease(string Version, string Url);

0 comments on commit 2c8dfae

Please sign in to comment.