118 lines
3.5 KiB
C#
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);
|
|
}
|
|
});
|
|
}
|
|
}
|