159 lines
5.3 KiB
C#
159 lines
5.3 KiB
C#
using System.Net.Http;
|
|
using AVSCartonShipmentVerifier.Configuration;
|
|
using Microsoft.AspNetCore.Hosting.Server;
|
|
using Microsoft.AspNetCore.Hosting.Server.Features;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace AVSCartonShipmentVerifier.Hosting;
|
|
|
|
/// <summary>
|
|
/// Opens the default browser once the web server has started (Windows desktop-style launch).
|
|
/// Stops the host when the browser window closes when <see cref="DesktopHostOptions.ExitWhenBrowserCloses"/> is enabled.
|
|
/// </summary>
|
|
internal sealed class LaunchBrowserHostedService : IHostedService
|
|
{
|
|
private const string DefaultBrowserUrl = "http://127.0.0.1:5000";
|
|
private const string ReadinessPath = "/health";
|
|
|
|
private readonly IHostApplicationLifetime _lifetime;
|
|
private readonly IOptions<DesktopHostOptions> _options;
|
|
private readonly IServer _server;
|
|
private readonly DesktopShutdownService _shutdown;
|
|
private readonly ILogger<LaunchBrowserHostedService> _logger;
|
|
|
|
public LaunchBrowserHostedService(
|
|
IServer server,
|
|
IHostApplicationLifetime lifetime,
|
|
IOptions<DesktopHostOptions> options,
|
|
DesktopShutdownService shutdown,
|
|
ILogger<LaunchBrowserHostedService> logger)
|
|
{
|
|
_server = server;
|
|
_lifetime = lifetime;
|
|
_options = options;
|
|
_shutdown = shutdown;
|
|
_logger = logger;
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
var opts = _options.Value;
|
|
if (!opts.LaunchBrowser)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
_lifetime.ApplicationStarted.Register(() => _ = OpenBrowserAsync());
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private async Task OpenBrowserAsync()
|
|
{
|
|
try
|
|
{
|
|
var url = ResolveUrl();
|
|
if (string.IsNullOrWhiteSpace(url))
|
|
{
|
|
_logger.LogWarning("Could not determine URL to open in the browser.");
|
|
return;
|
|
}
|
|
|
|
if (!await WaitForServerReadyAsync(url).ConfigureAwait(false))
|
|
{
|
|
_logger.LogWarning(
|
|
"Server did not respond on readiness endpoint before opening the browser at {Url}.",
|
|
url);
|
|
}
|
|
|
|
var opts = _options.Value;
|
|
var preferAppMode = opts.PreferAppModeBrowser && opts.ExitWhenBrowserCloses;
|
|
var launch = DesktopBrowserLauncher.TryLaunch(url, preferAppMode, opts.BrowserUserDataDir);
|
|
var minimumLifetime = TimeSpan.FromSeconds(Math.Max(1, opts.MinimumBrowserProcessLifetimeSeconds));
|
|
|
|
if (opts.ExitWhenBrowserCloses && launch.TrackedProcess is not null)
|
|
{
|
|
_shutdown.WatchBrowserProcess(launch.TrackedProcess, minimumLifetime);
|
|
_logger.LogInformation(
|
|
"Opened browser in app mode (PID {ProcessId}) at {Url} using profile {ProfileDir}. Application will exit when the window is closed.",
|
|
launch.TrackedProcess.Id,
|
|
url,
|
|
opts.BrowserUserDataDir);
|
|
return;
|
|
}
|
|
|
|
if (opts.ExitWhenBrowserCloses)
|
|
{
|
|
_logger.LogInformation(
|
|
"Opened default browser at {Url}. Application will exit shortly after the app tab/window is closed.",
|
|
url);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to launch the default browser.");
|
|
}
|
|
}
|
|
|
|
private static async Task<bool> WaitForServerReadyAsync(string dashboardBaseUrl)
|
|
{
|
|
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
|
|
var probeUrl = dashboardBaseUrl.TrimEnd('/') + ReadinessPath;
|
|
|
|
for (var attempt = 0; attempt < 50; attempt++)
|
|
{
|
|
try
|
|
{
|
|
using var response = await client.GetAsync(probeUrl).ConfigureAwait(false);
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Server not ready yet.
|
|
}
|
|
|
|
await Task.Delay(100).ConfigureAwait(false);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private string ResolveUrl()
|
|
{
|
|
var configured = _options.Value.BrowserUrl;
|
|
if (!string.IsNullOrWhiteSpace(configured))
|
|
{
|
|
return configured.Trim();
|
|
}
|
|
|
|
var addresses = _server.Features.Get<IServerAddressesFeature>()?.Addresses;
|
|
if (addresses is { Count: > 0 })
|
|
{
|
|
var address = addresses.FirstOrDefault(static a => a.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
|
|
?? addresses.FirstOrDefault(static a => a.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
|
?? addresses.First();
|
|
|
|
if (!string.IsNullOrWhiteSpace(address))
|
|
{
|
|
return NormalizeLoopbackAddress(address);
|
|
}
|
|
}
|
|
|
|
return DefaultBrowserUrl;
|
|
}
|
|
|
|
private static string NormalizeLoopbackAddress(string address)
|
|
{
|
|
if (address.Contains("://localhost", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return address.Replace("://localhost", "://127.0.0.1", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
return address;
|
|
}
|
|
|
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
}
|