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;
///
/// Opens the default browser once the web server has started (Windows desktop-style launch).
/// Stops the host when the browser window closes when is enabled.
///
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 _options;
private readonly IServer _server;
private readonly DesktopShutdownService _shutdown;
private readonly ILogger _logger;
public LaunchBrowserHostedService(
IServer server,
IHostApplicationLifetime lifetime,
IOptions options,
DesktopShutdownService shutdown,
ILogger 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 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()?.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;
}