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
}
}
}
}
}
}