1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
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
}
}
}
}
}
}
|