Skip to content

Commit

Permalink
Merge branch 'develop' into feat/1246-allow-reordering-example-sentences
Browse files Browse the repository at this point in the history
  • Loading branch information
myieye authored Jan 10, 2025
2 parents 8c8a9e5 + 2499cd6 commit 03e2271
Show file tree
Hide file tree
Showing 19 changed files with 269 additions and 99 deletions.
11 changes: 7 additions & 4 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,11 @@ tasks:
deps: [ infra-up, api:only, k8s:infra-forward ]
interactive: true

web-for-develop:
deps: [ ui:viewer-dev, fw-lite:lweb-for-develop, ui:https-oauth-authority ]

web:
fw-lite-web:
aliases:
- web
- web-for-develop
deps: [ ui:viewer-dev, fw-lite:web, ui:https-oauth-authority ]

fw-lite-win:
deps: [fw-lite:maui-windows, ui:viewer-dev, ui:https-oauth-authority]
2 changes: 1 addition & 1 deletion backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

namespace FwDataMiniLcmBridge.Api;

public class FwDataMiniLcmApi(Lazy<LcmCache> cacheLazy, bool onCloseSave, ILogger<FwDataMiniLcmApi> logger, FwDataProject project, MiniLcmValidators validators) : IMiniLcmApi, IDisposable
public class FwDataMiniLcmApi(Lazy<LcmCache> cacheLazy, bool onCloseSave, ILogger<FwDataMiniLcmApi> logger, FwDataProject project, MiniLcmValidators validators) : IMiniLcmApi, IMiniLcmSaveApi
{
internal LcmCache Cache => cacheLazy.Value;
public FwDataProject Project { get; } = project;
Expand Down
2 changes: 2 additions & 0 deletions backend/FwLite/FwDataMiniLcmBridge/FwDataBridgeKernel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using FwDataMiniLcmBridge.LcmUtils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MiniLcm;
using MiniLcm.Project;
using MiniLcm.Validators;
Expand All @@ -15,6 +16,7 @@ public static IServiceCollection AddFwDataBridge(this IServiceCollection service
services.AddLogging();
services.AddOptions<FwDataBridgeConfig>().BindConfiguration("FwDataBridge");
services.AddSingleton<FwDataFactory>();
services.AddSingleton<IHostedService>(sp => sp.GetRequiredService<FwDataFactory>());
services.AddSingleton<FieldWorksProjectList>();
services.AddSingleton<IProjectProvider>(s => s.GetRequiredService<FieldWorksProjectList>());
services.AddSingleton<IProjectLoader, ProjectLoader>();
Expand Down
19 changes: 17 additions & 2 deletions backend/FwLite/FwDataMiniLcmBridge/FwDataFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class FwDataFactory(
IMemoryCache cache,
ILogger<FwDataFactory> logger,
IProjectLoader projectLoader,
MiniLcmValidators validators) : IDisposable
MiniLcmValidators validators) : IDisposable, IHostedService
{
private bool _shuttingDown = false;
public FwDataFactory(ILogger<FwDataMiniLcmApi> fwdataLogger,
Expand Down Expand Up @@ -87,13 +87,16 @@ private static void OnLcmProjectCacheEviction(object key, object? value, Evictio
public void Dispose()
{
logger.LogInformation("Closing all projects");
foreach (var project in _projects)
//ensure a race condition doesn't cause us to dispose of a project that's already been disposed
var projects = Interlocked.Exchange(ref _projects, []);
foreach (var project in projects)
{
var lcmCache = cache.Get<LcmCache>(project);
if (lcmCache is null || lcmCache.IsDisposed) continue;
var name = lcmCache.ProjectId.Name;
lcmCache.Dispose(); //need to explicitly call dispose as that blocks, just removing from the cache does not block, meaning it will not finish disposing before the program exits.
logger.LogInformation("FW Data Project {ProjectFileName} disposed", name);
cache.Remove(project);
}
}

Expand All @@ -112,4 +115,16 @@ public IDisposable DeferClose(FwDataProject project)
{
return Defer.Action(() => CloseProject(project));
}

public Task StartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}

//Services in MAUI apps aren't disposed when the app is shut down, we have a workaround to shutdown HostedServices on shutdown, so we made this IHostedService to close projects on shutdown
public Task StopAsync(CancellationToken cancellationToken)
{
_shuttingDown = true;
return Task.Run(Dispose, cancellationToken);
}
}
7 changes: 4 additions & 3 deletions backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public static void AddFwLiteMauiServices(this IServiceCollection services,
services.AddSingleton<IHostEnvironment>(env);
services.AddFwLiteShared(env);
services.AddMauiBlazorWebView();
services.AddSingleton<IMauiInitializeService, HostedServiceAdapter>();
services.AddSingleton<HostedServiceAdapter>();
services.AddSingleton<IMauiInitializeService>(sp => sp.GetRequiredService<HostedServiceAdapter>());
#if INCLUDE_FWDATA_BRIDGE
//need to call them like this otherwise we need a using statement at the top of the file
FwDataMiniLcmBridge.FwDataBridgeKernel.AddFwDataBridge(services);
Expand Down Expand Up @@ -127,7 +128,7 @@ public static void AddFwLiteMauiServices(this IServiceCollection services,
public static bool IsPortableApp => false;
#endif

private class HostedServiceAdapter(IEnumerable<IHostedService> hostedServices, ILogger<HostedServiceAdapter> logger) : IMauiInitializeService, IAsyncDisposable
internal class HostedServiceAdapter(IEnumerable<IHostedService> hostedServices, ILogger<HostedServiceAdapter> logger) : IMauiInitializeService, IAsyncDisposable
{
private CancellationTokenSource _cts = new();
public void Initialize(IServiceProvider services)
Expand All @@ -141,8 +142,8 @@ public void Initialize(IServiceProvider services)

public async ValueTask DisposeAsync()
{
//todo this is never called because the service provider is not disposed
logger.LogInformation("Disposing hosted services");
//todo this should probably have a timeout so we don't hang forever
foreach (var hostedService in hostedServices)
{
await hostedService.StopAsync(_cts.Token);
Expand Down
8 changes: 5 additions & 3 deletions backend/FwLite/FwLiteMaui/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ public void Shutdown()
if (App == null) return;

var logger = App.Services.GetRequiredService<ILogger<MauiApp>>();
var adapter = App.Services.GetRequiredService<FwLiteMauiKernel.HostedServiceAdapter>();
try
{
logger.LogInformation("Disposing app");
App.DisposeAsync().GetAwaiter().GetResult();
logger.LogInformation("Disposing hosted services");
//I tried to dispose of the app, but that caused other issues on shutdown, so we're just going to dispose of the hosted services
adapter.DisposeAsync().GetAwaiter().GetResult();

Check warning on line 24 in backend/FwLite/FwLiteMaui/MauiProgram.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

Synchronously waiting on tasks or awaiters may cause deadlocks. Use await or JoinableTaskFactory.Run instead. (https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD002.md)
}
catch (Exception e)
{
logger.LogError(e, "Failed to dispose app");
logger.LogError(e, "Failed to dispose hosted services");
throw;
}
})
Expand Down
1 change: 1 addition & 0 deletions backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static IServiceCollection AddFwLiteShared(this IServiceCollection service
services.AddHttpClient();
services.AddAuthHelpers(environment);
services.AddLcmCrdtClient();
services.AddLogging();

services.AddSingleton<ImportFwdataService>();
services.AddScoped<SyncService>();
Expand Down
7 changes: 4 additions & 3 deletions backend/FwLite/FwLiteShared/Pages/FwdataProject.razor
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@

private IAsyncDisposable? _disposable;

protected override async Task OnAfterRenderAsync(bool firstRender)
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
if (!firstRender) return Task.CompletedTask;
var fwLiteProvider = ScopedServices.GetRequiredService<FwLiteProvider>();
_disposable = await fwLiteProvider.InjectFwDataProject(JS, ScopedServices, ProjectName);
_disposable = fwLiteProvider.InjectFwDataProject(ScopedServices, ProjectName);
return Task.CompletedTask;
}

protected override async ValueTask DisposeAsyncCore()
Expand Down
73 changes: 66 additions & 7 deletions backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
using MiniLcm;
using MiniLcm.Models;
using MiniLcm.Project;

Expand All @@ -22,11 +23,13 @@ public class FwLiteProvider(
ChangeEventBus changeEventBus,
IEnumerable<IProjectProvider> projectProviders,
ILogger<FwLiteProvider> logger,
IOptions<FwLiteConfig> config
IOptions<FwLiteConfig> config,
ILoggerFactory loggerFactory
) : IDisposable
{
public const string OverrideServiceFunctionName = "setOverrideService";
private readonly List<IDisposable> _disposables = [];
private readonly MiniLcmApiProvider _miniLcmApiProvider = new(loggerFactory.CreateLogger<MiniLcmApiProvider>());

private IProjectProvider? FwDataProjectProvider =>
projectProviders.FirstOrDefault(p => p.DataFormat == ProjectDataFormat.FwData);
Expand All @@ -53,6 +56,7 @@ public object GetService(DotnetService service)
DotnetService.AuthService => authService,
DotnetService.ImportFwdataService => importFwdataService,
DotnetService.FwLiteConfig => config.Value,
DotnetService.MiniLcmApiProvider => _miniLcmApiProvider,
_ => throw new ArgumentOutOfRangeException(nameof(service), service, null)
};
}
Expand Down Expand Up @@ -95,36 +99,91 @@ public async Task<IAsyncDisposable> InjectCrdtProject(IJSRuntime jsRuntime,
{
_ = jsRuntime.DurableInvokeVoidAsync("notifyEntryUpdated", projectName, entry);
});
var service = ActivatorUtilities.CreateInstance<MiniLcmJsInvokable>(scopedServices, project);
var reference = await SetService(jsRuntime,DotnetService.MiniLcmApi, service);
var cleanup = ProvideMiniLcmApi(ActivatorUtilities.CreateInstance<MiniLcmJsInvokable>(scopedServices, project));

return Defer.Async(() =>
{
reference?.Dispose();
cleanup.Dispose();
entryUpdatedSubscription.Dispose();
return Task.CompletedTask;
});
}

public async Task<IAsyncDisposable> InjectFwDataProject(IJSRuntime jsRuntime, IServiceProvider scopedServices, string projectName)
public IAsyncDisposable InjectFwDataProject(IServiceProvider scopedServices, string projectName)
{
if (FwDataProjectProvider is null) throw new InvalidOperationException("FwData Project provider is not available");
var project = FwDataProjectProvider.GetProject(projectName) ?? throw new InvalidOperationException($"FwData Project {projectName} not found");
var service = ActivatorUtilities.CreateInstance<MiniLcmJsInvokable>(scopedServices,
FwDataProjectProvider.OpenProject(project), project);
var reference = await SetService(jsRuntime, DotnetService.MiniLcmApi, service);
var cleanup = ProvideMiniLcmApi(service);
return Defer.Async(() =>
{
reference?.Dispose();
cleanup.Dispose();
service.Dispose();
return Task.CompletedTask;
});
}

private IDisposable ProvideMiniLcmApi(MiniLcmJsInvokable miniLcmApi)
{
var reference = DotNetObjectReference.Create(miniLcmApi);
_miniLcmApiProvider.SetMiniLcmApi(reference);
return Defer.Action(() =>
{
reference?.Dispose();
_miniLcmApiProvider.ClearMiniLcmApi();
});
}
}

/// <summary>
/// this service is used to allow the frontend to await the api being setup by the backend, this means the frontend doesn't need to poll for the api being ready
/// </summary>
internal class MiniLcmApiProvider(ILogger<MiniLcmApiProvider> logger)
{
private TaskCompletionSource<DotNetObjectReference<MiniLcmJsInvokable>> _tcs = new();

[JSInvokable]
public async Task<DotNetObjectReference<MiniLcmJsInvokable>> GetMiniLcmApi()
{
#pragma warning disable VSTHRD003
return await _tcs.Task;
#pragma warning restore VSTHRD003
}

public void SetMiniLcmApi(DotNetObjectReference<MiniLcmJsInvokable> miniLcmApi)
{
logger.LogInformation("Setting MiniLcmApi");
_tcs.SetResult(miniLcmApi);
}

[JSInvokable]
public void ClearMiniLcmApi()
{
logger.LogInformation("Clearing MiniLcmApi");
//we can't cancel a tcs if it's already completed
if (!_tcs.Task.IsCompleted)
{
//we need to tell any clients awaiting the tcs it's canceled. otherwise they will hang
_tcs.SetCanceled();
}
else if (_tcs.Task.IsCompletedSuccessfully)
{
#pragma warning disable VSTHRD002
_tcs.Task.Result.Value.Dispose();
#pragma warning restore VSTHRD002
}

//create a new tcs so any new clients will await the new api.
_tcs = new();
}
}

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum DotnetService
{
MiniLcmApi,
MiniLcmApiProvider,
CombinedProjectsService,
AuthService,
ImportFwdataService,
Expand Down
Loading

0 comments on commit 03e2271

Please sign in to comment.