AVS-Carton-Shipment-Verifier/Hosting/DesktopShutdownService.cs

118 lines
3.5 KiB
C#

using System.Diagnostics;
namespace AVSCartonShipmentVerifier.Hosting;
/// <summary>
/// Coordinates graceful host shutdown when the desktop browser window or tab is closed.
/// </summary>
public sealed class DesktopShutdownService
{
private readonly IHostApplicationLifetime _lifetime;
private readonly ILogger<DesktopShutdownService> _logger;
private readonly object _gate = new();
private CancellationTokenSource? _delayedShutdownCts;
private int _shutdownInitiated;
private int _dashboardSessionActive;
public DesktopShutdownService(
IHostApplicationLifetime lifetime,
ILogger<DesktopShutdownService> logger)
{
_lifetime = lifetime;
_logger = logger;
}
public void MarkDashboardSessionActive()
{
Interlocked.Exchange(ref _dashboardSessionActive, 1);
}
public bool IsLifecycleShutdownAllowed => Volatile.Read(ref _dashboardSessionActive) == 1;
public void RequestShutdown(string reason)
{
if (Interlocked.Exchange(ref _shutdownInitiated, 1) != 0)
{
return;
}
_logger.LogInformation("Stopping application: {Reason}", reason);
_lifetime.StopApplication();
}
public void ScheduleShutdown(TimeSpan delay)
{
if (!IsLifecycleShutdownAllowed)
{
_logger.LogDebug("Ignored lifecycle shutdown request before dashboard session was active.");
return;
}
lock (_gate)
{
_delayedShutdownCts?.Cancel();
_delayedShutdownCts?.Dispose();
_delayedShutdownCts = new CancellationTokenSource();
var token = _delayedShutdownCts.Token;
_ = Task.Run(async () =>
{
try
{
await Task.Delay(delay, token).ConfigureAwait(false);
RequestShutdown("Browser session ended");
}
catch (OperationCanceledException)
{
// Reload or cancel-shutdown superseded this timer.
}
}, token);
}
}
public void CancelScheduledShutdown()
{
lock (_gate)
{
if (_delayedShutdownCts is null)
{
return;
}
_delayedShutdownCts.Cancel();
_delayedShutdownCts.Dispose();
_delayedShutdownCts = null;
}
}
public void WatchBrowserProcess(Process process, TimeSpan minimumLifetime)
{
var processId = process.Id;
var startedUtc = DateTime.UtcNow;
_ = Task.Run(async () =>
{
try
{
await process.WaitForExitAsync().ConfigureAwait(false);
var lifetime = DateTime.UtcNow - startedUtc;
if (lifetime < minimumLifetime)
{
_logger.LogInformation(
"Browser launcher process (PID {ProcessId}) exited after {LifetimeSeconds:F1}s; likely handoff to an existing browser. Process tracking disabled; application will keep running.",
processId,
lifetime.TotalSeconds);
return;
}
RequestShutdown("Browser window closed");
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Browser process watch ended unexpectedly for PID {ProcessId}.", processId);
}
});
}
}