using System; using System.Linq; using System.Net; using System.Net.Http; using System.Net.NetworkInformation; using System.Threading; using System.Threading.Tasks; namespace Tango.Telemetry.Helpers { /// /// Active internet connectivity monitor with fast cached reads. /// - Immediate wake on network changes (address/availability) -> near-instant pickup after Wi-Fi reconnect. /// - Burst probing after reconnect to avoid brief false negatives. /// - .NET Framework 4.6.1 / C# 7 compatible. /// public static class InternetConnectivity { // -------- Public API -------- /// /// Returns the last known internet status instantly (safe to call every 200ms). /// First call primes quickly so you don't get default false. /// public static bool IsInternetAvailable() { if (!_primed) { EnsureStarted(); PrimeOnce(); } return _lastIsUp; } /// Raised when status flips. public static event Action StatusChanged; public static void EnsureStarted() { if (_started) return; lock (_startLock) { if (_started) return; _started = true; if (ServicePointManager.DefaultConnectionLimit < 16) ServicePointManager.DefaultConnectionLimit = 16; // Subscribe to network changes to wake the loop immediately. try { NetworkChange.NetworkAddressChanged += OnNetworkChanged; NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; } catch { /* non-fatal */ } Task.Run(() => ProbeLoop()); } } public static void Dispose() { if (_disposed) return; _disposed = true; try { NetworkChange.NetworkAddressChanged -= OnNetworkChanged; NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; } catch { } try { _cts.Cancel(); } catch { } try { _http.Dispose(); } catch { } try { _wake.Set(); } catch { } } // -------- Config -------- private static readonly TimeSpan MinRefreshIntervalWhenUp = TimeSpan.FromSeconds(3); private static readonly TimeSpan MinRefreshIntervalWhenDown = TimeSpan.FromSeconds(2); private static readonly TimeSpan MaxBackoffWhenDown = TimeSpan.FromSeconds(30); private static readonly TimeSpan DnsTimeout = TimeSpan.FromMilliseconds(800); private static readonly TimeSpan HttpTimeout = TimeSpan.FromMilliseconds(1200); // First-call quick prime private static readonly TimeSpan PrimeBudget = TimeSpan.FromMilliseconds(300); // After a reconnect, probe aggressively a few times to avoid stale state private const int ReconnectBurstAttempts = 3; private static readonly TimeSpan ReconnectBurstDelay = TimeSpan.FromMilliseconds(300); private const string DnsProbeHost = "dns.google"; private static readonly Uri NcsiUri = new Uri("http://www.msftconnecttest.com/connecttest.txt"); // -------- State -------- private static volatile bool _lastIsUp; private static volatile bool _started; private static volatile bool _disposed; private static volatile bool _primed; private static readonly object _startLock = new object(); private static readonly CancellationTokenSource _cts = new CancellationTokenSource(); private static readonly HttpClient _http = CreateHttpClient(); // AsyncAutoResetEvent (since we’re on .NET Fx) to wake sleep early. private static readonly AsyncAutoResetEvent _wake = new AsyncAutoResetEvent(); private static DateTime _lastAddressChangeUtc = DateTime.MinValue; // -------- Network change hooks -------- private static void OnNetworkChanged(object sender, EventArgs e) { _lastAddressChangeUtc = DateTime.UtcNow; _wake.Set(); // wake probe loop immediately } private static void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e) { _lastAddressChangeUtc = DateTime.UtcNow; _wake.Set(); // wake probe loop immediately } // -------- Prime-on-first-call -------- private static void PrimeOnce() { if (_primed) return; lock (_startLock) { if (_primed) return; _primed = true; try { bool isUp = false; if (HasViableLocalNetwork()) { var primeCts = new CancellationTokenSource(); primeCts.CancelAfter(PrimeBudget); isUp = TryDns(primeCts.Token).GetAwaiter().GetResult(); } _lastIsUp = isUp; if (isUp) SafeRaise(true); } catch { _lastIsUp = false; } } } // -------- Probe loop -------- private static async Task ProbeLoop() { var ct = _cts.Token; var last = _lastIsUp; var backoff = TimeSpan.Zero; var lastHttpConfirmedUpUtc = DateTime.MinValue; while (!ct.IsCancellationRequested) { bool isUp = false; bool localUp = HasViableLocalNetwork(); if (localUp) { // If we just had a network change, run a small aggressive burst. if ((DateTime.UtcNow - _lastAddressChangeUtc) < TimeSpan.FromSeconds(3)) { for (int i = 0; i < ReconnectBurstAttempts; i++) { if (await TryDns(ct).ConfigureAwait(false)) { // Confirm once via HTTP (short timeout) after DNS says yes if (await TryHttp(ct).ConfigureAwait(false)) { isUp = true; break; } } await SleepNoThrow(ReconnectBurstDelay, ct).ConfigureAwait(false); } } else { // Regular cadence isUp = await TryDns(ct).ConfigureAwait(false); if (isUp) { bool needHttpConfirm = !last || (DateTime.UtcNow - lastHttpConfirmedUpUtc) > TimeSpan.FromSeconds(30); if (needHttpConfirm) { isUp = await TryHttp(ct).ConfigureAwait(false); if (isUp) lastHttpConfirmedUpUtc = DateTime.UtcNow; } } } } if (isUp != last) { _lastIsUp = isUp; last = isUp; SafeRaise(isUp); } else { _lastIsUp = isUp; } // Compute next wait (with backoff when down) TimeSpan wait; if (isUp) { backoff = TimeSpan.Zero; wait = MinRefreshIntervalWhenUp; } else { backoff = backoff == TimeSpan.Zero ? MinRefreshIntervalWhenDown : TimeSpan.FromMilliseconds(Math.Min( MaxBackoffWhenDown.TotalMilliseconds, backoff.TotalMilliseconds * 2)); wait = backoff; } // Wait for either the timer OR a wake signal (network change), whichever comes first await WaitWithWake(wait, ct).ConfigureAwait(false); } } private static async Task WaitWithWake(TimeSpan delay, CancellationToken ct) { // Race delay vs wake; return as soon as either completes. var delayTask = Task.Delay(delay, ct); var wakeTask = _wake.WaitAsync(ct); var completed = await Task.WhenAny(delayTask, wakeTask).ConfigureAwait(false); // No need to do anything with the result; loop will run again immediately. } // -------- Tiers -------- private static bool HasViableLocalNetwork() { try { var nics = NetworkInterface.GetAllNetworkInterfaces(); return nics.Any(nic => nic.OperationalStatus == OperationalStatus.Up && nic.NetworkInterfaceType != NetworkInterfaceType.Loopback && nic.NetworkInterfaceType != NetworkInterfaceType.Tunnel && nic.GetIPProperties().GatewayAddresses.Any()); } catch { return false; } } private static async Task TryDns(CancellationToken ct) { try { var dnsTask = Dns.GetHostEntryAsync(DnsProbeHost); var delayTask = Task.Delay(DnsTimeout, ct); var completed = await Task.WhenAny(dnsTask, delayTask).ConfigureAwait(false); if (completed == dnsTask) { var entry = await dnsTask.ConfigureAwait(false); return entry != null && entry.AddressList != null && entry.AddressList.Length > 0; } return false; } catch { return false; } } private static async Task TryHttp(CancellationToken ct) { try { using (var cts = CancellationTokenSource.CreateLinkedTokenSource(ct)) { cts.CancelAfter(HttpTimeout); using (var headReq = new HttpRequestMessage(HttpMethod.Head, NcsiUri)) using (var headResp = await _http.SendAsync( headReq, HttpCompletionOption.ResponseHeadersRead, cts.Token).ConfigureAwait(false)) { if (headResp.IsSuccessStatusCode) return true; } using (var getReq = new HttpRequestMessage(HttpMethod.Get, NcsiUri)) using (var getResp = await _http.SendAsync( getReq, HttpCompletionOption.ResponseHeadersRead, cts.Token).ConfigureAwait(false)) { return getResp.IsSuccessStatusCode; } } } catch { return false; } } // -------- Helpers -------- private static HttpClient CreateHttpClient() { var handler = new HttpClientHandler { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseProxy = true }; var client = new HttpClient(handler, true) { Timeout = TimeSpan.FromSeconds(2) }; return client; } private static void SafeRaise(bool value) { try { var h = StatusChanged; if (h != null) h(value); } catch { } } private static async Task SleepNoThrow(TimeSpan delay, CancellationToken ct) { try { await Task.Delay(delay, ct).ConfigureAwait(false); } catch { } } /// /// Minimal AsyncAutoResetEvent for .NET Framework. /// private sealed class AsyncAutoResetEvent { private static readonly Task s_completed = Task.FromResult(true); private readonly object _mutex = new object(); private TaskCompletionSource _tcs = new TaskCompletionSource(); public Task WaitAsync(CancellationToken ct) { lock (_mutex) { if (_tcs.Task.IsCompleted) { _tcs = new TaskCompletionSource(); return s_completed; } // Register cancellation against the current waiter var tcs = _tcs; if (ct.CanBeCanceled) { ct.Register(() => { try { tcs.TrySetCanceled(); } catch { } }); } return tcs.Task; } } public void Set() { lock (_mutex) { if (!_tcs.Task.IsCompleted) { _tcs.TrySetResult(true); } else { // already signaled; keep it signaled for the next waiter } } } } } }