diff --git a/backend/FwLite/FwLiteDesktop/App.xaml.cs b/backend/FwLite/FwLiteDesktop/App.xaml.cs index db4ab86a6..7c66fda27 100644 --- a/backend/FwLite/FwLiteDesktop/App.xaml.cs +++ b/backend/FwLite/FwLiteDesktop/App.xaml.cs @@ -2,10 +2,10 @@ public partial class App : Application { - public App() + public App(MainPage mainPage) { InitializeComponent(); - MainPage = new AppShell(); + MainPage = mainPage; } } diff --git a/backend/FwLite/FwLiteDesktop/AppShell.xaml b/backend/FwLite/FwLiteDesktop/AppShell.xaml deleted file mode 100644 index fd1aa4386..000000000 --- a/backend/FwLite/FwLiteDesktop/AppShell.xaml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/backend/FwLite/FwLiteDesktop/AppShell.xaml.cs b/backend/FwLite/FwLiteDesktop/AppShell.xaml.cs deleted file mode 100644 index 16f8c1aaa..000000000 --- a/backend/FwLite/FwLiteDesktop/AppShell.xaml.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace FwLiteDesktop; - -public partial class AppShell : Shell -{ - public AppShell() - { - InitializeComponent(); - } -} diff --git a/backend/FwLite/FwLiteDesktop/MainPage.xaml b/backend/FwLite/FwLiteDesktop/MainPage.xaml index 024cbab1d..dca7fed91 100644 --- a/backend/FwLite/FwLiteDesktop/MainPage.xaml +++ b/backend/FwLite/FwLiteDesktop/MainPage.xaml @@ -1,14 +1,8 @@ - + - - - - - + diff --git a/backend/FwLite/FwLiteDesktop/MainPage.xaml.cs b/backend/FwLite/FwLiteDesktop/MainPage.xaml.cs index b72e7e649..014c647dc 100644 --- a/backend/FwLite/FwLiteDesktop/MainPage.xaml.cs +++ b/backend/FwLite/FwLiteDesktop/MainPage.xaml.cs @@ -1,10 +1,17 @@ -namespace FwLiteDesktop; +using Microsoft.Extensions.Options; + +namespace FwLiteDesktop; public partial class MainPage : ContentPage { - public MainPage() + public MainPage(IOptionsMonitor options) { InitializeComponent(); + options.OnChange(o => + { + webView.Dispatcher.Dispatch(() => webView.Source = o.Url); + }); + webView.Source = options.CurrentValue.Url; } } diff --git a/backend/FwLite/FwLiteDesktop/MauiProgram.cs b/backend/FwLite/FwLiteDesktop/MauiProgram.cs index 19cbe534d..c92970feb 100644 --- a/backend/FwLite/FwLiteDesktop/MauiProgram.cs +++ b/backend/FwLite/FwLiteDesktop/MauiProgram.cs @@ -1,4 +1,8 @@ -using Microsoft.Extensions.Logging; +using FwLiteDesktop.ServerBridge; +using LocalWebApp; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Maui.LifecycleEvents; namespace FwLiteDesktop; @@ -15,17 +19,19 @@ public static MauiApp CreateMauiApp() fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); + builder.Services.AddSingleton(); - builder.Services.AddOptions().Configure(config => - { - config.Url = "http://localhost:5000"; - } - ); + var serverManager = new ServerManager(); + builder.Services.AddSingleton(serverManager); + builder.Configuration.Add(source => source.ServerManager = serverManager); + builder.Services.AddOptions().BindConfiguration("LocalWebApp"); #if DEBUG builder.Logging.AddDebug(); #endif - return builder.Build(); + var app = builder.Build(); + app.Services.GetRequiredService().Start(); + return app; } } diff --git a/backend/FwLite/FwLiteDesktop/Properties/launchSettings.json b/backend/FwLite/FwLiteDesktop/Properties/launchSettings.json index bccb9bf16..1f221c031 100644 --- a/backend/FwLite/FwLiteDesktop/Properties/launchSettings.json +++ b/backend/FwLite/FwLiteDesktop/Properties/launchSettings.json @@ -3,7 +3,7 @@ "Run": { "commandName": "Project", "environmentVariables": { - "COREHOST_TRACE": "1" + "COREHOST_TRACE": "0" } }, "Windows Machine": { diff --git a/backend/FwLite/FwLiteDesktop/ServerBridge/ServerConfigProvider.cs b/backend/FwLite/FwLiteDesktop/ServerBridge/ServerConfigProvider.cs new file mode 100644 index 000000000..634f62d38 --- /dev/null +++ b/backend/FwLite/FwLiteDesktop/ServerBridge/ServerConfigProvider.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Configuration; + +namespace FwLiteDesktop.ServerBridge; + +public class ServerConfigSource: IConfigurationSource +{ + public ServerManager? ServerManager { get; set; } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + if (ServerManager is null) + throw new InvalidOperationException("ServerManager is not set"); + return new ServerConfigProvider(ServerManager); + } +} + +public class ServerConfigProvider : ConfigurationProvider +{ + public ServerConfigProvider(ServerManager serverManager) + { + _ = serverManager.Started.ContinueWith(t => + { + Data = new Dictionary { ["LocalWebApp:Url"] = t.Result.Urls.First() }; + OnReload(); + }, + scheduler: TaskScheduler.Default); + } + + public override void Load() + { + } +} diff --git a/backend/FwLite/FwLiteDesktop/ServerBridge/ServerManager.cs b/backend/FwLite/FwLiteDesktop/ServerBridge/ServerManager.cs new file mode 100644 index 000000000..8835a6586 --- /dev/null +++ b/backend/FwLite/FwLiteDesktop/ServerBridge/ServerManager.cs @@ -0,0 +1,38 @@ +using LocalWebApp; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; + +namespace FwLiteDesktop; + +public class ServerManager : IAsyncDisposable +{ + private readonly TaskCompletionSource _started = new(); + public Task Started => _started.Task; + private WebApplication? _webApp; + + public void Start() + { + _webApp = LocalWebAppServer.SetupAppServer([]); + _ = Task.Run(async () => + { + try + { + await _webApp.StartAsync(); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + _started.SetResult(_webApp); + }); + } + + public async ValueTask DisposeAsync() + { + if (_webApp is null) return; + await _webApp.StopAsync(TimeSpan.FromSeconds(10)); + await _webApp.DisposeAsync(); + } +} diff --git a/backend/FwLite/LocalWebApp/LocalWebAppServer.cs b/backend/FwLite/LocalWebApp/LocalWebAppServer.cs new file mode 100644 index 000000000..4eba3deda --- /dev/null +++ b/backend/FwLite/LocalWebApp/LocalWebAppServer.cs @@ -0,0 +1,100 @@ +#if !DISABLE_FW_BRIDGE +using FwDataMiniLcmBridge; +using FwDataMiniLcmBridge.LcmUtils; +#endif +using LcmCrdt; +using LocalWebApp; +using LocalWebApp.Hubs; +using LocalWebApp.Auth; +using LocalWebApp.Routes; +using LocalWebApp.Utils; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.StaticFiles.Infrastructure; +using Microsoft.Extensions.FileProviders; + +namespace LocalWebApp; + +public static class LocalWebAppServer +{ + public static WebApplication SetupAppServer(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + if (!builder.Environment.IsDevelopment()) + builder.WebHost.UseUrls("http://127.0.0.1:0"); + if (builder.Environment.IsDevelopment()) + { +#if !DISABLE_FW_BRIDGE + //do this early so we catch bugs on startup + ProjectLoader.Init(); +#endif + } + + builder.ConfigureDev(config => + config.DefaultAuthority = new("https://lexbox.dev.languagetechnology.org")); +//for now prod builds will also use lt dev until we deploy oauth to prod + builder.ConfigureProd(config => + config.DefaultAuthority = new("https://lexbox.dev.languagetechnology.org")); + builder.Services.Configure(c => c.ClientId = "becf2856-0690-434b-b192-a4032b72067f"); + + builder.Services.AddLocalAppServices(builder.Environment); + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + builder.Services.AddSignalR(options => + { + options.AddFilter(new LockedProjectFilter()); + options.EnableDetailedErrors = true; + }).AddJsonProtocol(); + + var app = builder.Build(); +// Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + +//configure dotnet to serve static files from the embedded resources + var sharedOptions = + new SharedOptions() { FileProvider = new ManifestEmbeddedFileProvider(typeof(Program).Assembly) }; + app.UseDefaultFiles(new DefaultFilesOptions(sharedOptions)); + var staticFileOptions = new StaticFileOptions(sharedOptions); + app.UseStaticFiles(staticFileOptions); + + app.Use(async (context, next) => + { + var projectName = context.GetProjectName(); + if (!string.IsNullOrWhiteSpace(projectName)) + { + var projectsService = context.RequestServices.GetRequiredService(); + projectsService.SetProjectScope(projectsService.GetProject(projectName) ?? + throw new InvalidOperationException( + $"Project {projectName} not found")); + await context.RequestServices.GetRequiredService().PopulateProjectDataCache(); + } +#if !DISABLE_FW_BRIDGE + var fwData = context.GetFwDataName(); + if (!string.IsNullOrWhiteSpace(fwData)) + { + var fwDataProjectContext = context.RequestServices.GetRequiredService(); + fwDataProjectContext.Project = + FieldWorksProjectList.GetProject(fwData) ?? throw new InvalidOperationException($"FwData {fwData} not found"); + } +#endif + + await next(context); + }); + app.MapHub($"/api/hub/{{{CrdtMiniLcmApiHub.ProjectRouteKey}}}/lexbox"); +#if !DISABLE_FW_BRIDGE +app.MapHub($"/api/hub/{{{FwDataMiniLcmHub.ProjectRouteKey}}}/fwdata"); +#endif + app.MapHistoryRoutes(); + app.MapActivities(); + app.MapProjectRoutes(); + app.MapFwIntegrationRoutes(); + app.MapTest(); + app.MapImport(); + app.MapAuthRoutes(); + app.MapFallbackToFile("index.html", staticFileOptions); + return app; + } +} diff --git a/backend/FwLite/LocalWebApp/Program.cs b/backend/FwLite/LocalWebApp/Program.cs index 928c4277e..99f815b90 100644 --- a/backend/FwLite/LocalWebApp/Program.cs +++ b/backend/FwLite/LocalWebApp/Program.cs @@ -1,89 +1,7 @@ -#if !DISABLE_FW_BRIDGE -using FwDataMiniLcmBridge; -using FwDataMiniLcmBridge.LcmUtils; -#endif -using LcmCrdt; -using LocalWebApp; -using LocalWebApp.Hubs; -using LocalWebApp.Auth; -using LocalWebApp.Routes; -using LocalWebApp.Utils; -using Microsoft.AspNetCore.SignalR; -using Microsoft.AspNetCore.StaticFiles.Infrastructure; -using Microsoft.Extensions.FileProviders; - -var builder = WebApplication.CreateBuilder(args); -if (!builder.Environment.IsDevelopment()) - builder.WebHost.UseUrls("http://127.0.0.1:0"); -if (builder.Environment.IsDevelopment()) -{ - #if !DISABLE_FW_BRIDGE - //do this early so we catch bugs on startup - ProjectLoader.Init(); - #endif -} -builder.ConfigureDev(config => config.DefaultAuthority = new("https://lexbox.dev.languagetechnology.org")); -//for now prod builds will also use lt dev until we deploy oauth to prod -builder.ConfigureProd(config => config.DefaultAuthority = new("https://lexbox.dev.languagetechnology.org")); -builder.Services.Configure(c => c.ClientId = "becf2856-0690-434b-b192-a4032b72067f"); - -builder.Services.AddLocalAppServices(builder.Environment); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); -builder.Services.AddSignalR(options => -{ - options.AddFilter(new LockedProjectFilter()); - options.EnableDetailedErrors = true; -}).AddJsonProtocol(); - -var app = builder.Build(); -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} -//configure dotnet to serve static files from the embedded resources -var sharedOptions = new SharedOptions() { FileProvider = new ManifestEmbeddedFileProvider(typeof(Program).Assembly) }; -app.UseDefaultFiles(new DefaultFilesOptions(sharedOptions)); -var staticFileOptions = new StaticFileOptions(sharedOptions); -app.UseStaticFiles(staticFileOptions); - -app.Use(async (context, next) => -{ - var projectName = context.GetProjectName(); - if (!string.IsNullOrWhiteSpace(projectName)) - { - var projectsService = context.RequestServices.GetRequiredService(); - projectsService.SetProjectScope(projectsService.GetProject(projectName) ?? - throw new InvalidOperationException($"Project {projectName} not found")); - await context.RequestServices.GetRequiredService().PopulateProjectDataCache(); - } - #if !DISABLE_FW_BRIDGE - var fwData = context.GetFwDataName(); - if (!string.IsNullOrWhiteSpace(fwData)) - { - var fwDataProjectContext = context.RequestServices.GetRequiredService(); - fwDataProjectContext.Project = FieldWorksProjectList.GetProject(fwData) ?? throw new InvalidOperationException($"FwData {fwData} not found"); - } - #endif - - await next(context); -}); -app.MapHub($"/api/hub/{{{CrdtMiniLcmApiHub.ProjectRouteKey}}}/lexbox"); -#if !DISABLE_FW_BRIDGE -app.MapHub($"/api/hub/{{{FwDataMiniLcmHub.ProjectRouteKey}}}/fwdata"); -#endif -app.MapHistoryRoutes(); -app.MapActivities(); -app.MapProjectRoutes(); -app.MapFwIntegrationRoutes(); -app.MapTest(); -app.MapImport(); -app.MapAuthRoutes(); -app.MapFallbackToFile("index.html", staticFileOptions); +using LocalWebApp; +var app = LocalWebAppServer.SetupAppServer(args); await using (app) { await app.StartAsync();