aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Tango.Telemetry/Helpers/InternetConnectivity.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Software/Visual_Studio/Tango.Telemetry/Helpers/InternetConnectivity.cs')
-rw-r--r--Software/Visual_Studio/Tango.Telemetry/Helpers/InternetConnectivity.cs388
1 files changed, 388 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Tango.Telemetry/Helpers/InternetConnectivity.cs b/Software/Visual_Studio/Tango.Telemetry/Helpers/InternetConnectivity.cs
new file mode 100644
index 000000000..f747bb263
--- /dev/null
+++ b/Software/Visual_Studio/Tango.Telemetry/Helpers/InternetConnectivity.cs
@@ -0,0 +1,388 @@
+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
+{
+ /// <summary>
+ /// 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.
+ /// </summary>
+ public static class InternetConnectivity
+ {
+ // -------- Public API --------
+
+ /// <summary>
+ /// Returns the last known internet status instantly (safe to call every 200ms).
+ /// First call primes quickly so you don't get default false.
+ /// </summary>
+ public static bool IsInternetAvailable()
+ {
+ if (!_primed)
+ {
+ EnsureStarted();
+ PrimeOnce();
+ }
+ return _lastIsUp;
+ }
+
+ /// <summary>Raised when status flips.</summary>
+ public static event Action<bool> 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<bool> 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<bool> 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 { }
+ }
+
+ /// <summary>
+ /// Minimal AsyncAutoResetEvent for .NET Framework.
+ /// </summary>
+ private sealed class AsyncAutoResetEvent
+ {
+ private static readonly Task s_completed = Task.FromResult(true);
+ private readonly object _mutex = new object();
+ private TaskCompletionSource<bool> _tcs = new TaskCompletionSource<bool>();
+
+ public Task WaitAsync(CancellationToken ct)
+ {
+ lock (_mutex)
+ {
+ if (_tcs.Task.IsCompleted)
+ {
+ _tcs = new TaskCompletionSource<bool>();
+ 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
+ }
+ }
+ }
+ }
+ }
+}