using System.Diagnostics; namespace AVSCartonShipmentVerifier.Hosting; /// /// Coordinates graceful host shutdown when the desktop browser window or tab is closed. /// public sealed class DesktopShutdownService { private readonly IHostApplicationLifetime _lifetime; private readonly ILogger _logger; private readonly object _gate = new(); private CancellationTokenSource? _delayedShutdownCts; private int _shutdownInitiated; private int _dashboardSessionActive; public DesktopShutdownService( IHostApplicationLifetime lifetime, ILogger 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); } }); } }