From bda71b704d17773316b4b08e7dae7e5e536d0d0c Mon Sep 17 00:00:00 2001 From: Roy Ben Shabat Date: Mon, 18 Aug 2025 21:23:02 +0300 Subject: Improved Telemetry IoT Destination. --- .../Tango.Telemetry/DateTimeUTCFixer.cs | 209 ----------- .../Destinations/TelemetryAzureHubDestination.cs | 47 ++- .../Tango.Telemetry/Helpers/DateTimeUTCFixer.cs | 209 +++++++++++ .../Helpers/InternetConnectivity.cs | 388 +++++++++++++++++++++ .../Tango.Telemetry/Helpers/JsonFlattener.cs | 89 +++++ .../Visual_Studio/Tango.Telemetry/JsonFlattener.cs | 89 ----- .../Tango.Telemetry/Tango.Telemetry.csproj | 8 +- .../Visual_Studio/Tango.Telemetry/TelemetryBase.cs | 1 + .../TelemetryLiteDBStorageManager.cs | 1 + .../TelemetryPendingStorageSource.cs | 2 +- .../Visual_Studio/Tango.Telemetry/packages.config | 1 + .../Tango.Telemetry.Tester.IOT.CLI/Program.cs | 4 +- 12 files changed, 731 insertions(+), 317 deletions(-) delete mode 100644 Software/Visual_Studio/Tango.Telemetry/DateTimeUTCFixer.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Helpers/DateTimeUTCFixer.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Helpers/InternetConnectivity.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Helpers/JsonFlattener.cs delete mode 100644 Software/Visual_Studio/Tango.Telemetry/JsonFlattener.cs diff --git a/Software/Visual_Studio/Tango.Telemetry/DateTimeUTCFixer.cs b/Software/Visual_Studio/Tango.Telemetry/DateTimeUTCFixer.cs deleted file mode 100644 index 490a49e53..000000000 --- a/Software/Visual_Studio/Tango.Telemetry/DateTimeUTCFixer.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Reflection; -using System.Runtime.CompilerServices; - -namespace Tango.Telemetry -{ - internal static class DateTimeUtcFixer - { - /// - /// Recursively scans the object graph starting at and - /// sets Kind=Utc on any DateTime/DateTime? properties or collection elements that are Kind=Unspecified. - /// Uses DateTime.SpecifyKind(value, DateTimeKind.Utc) (no clock adjustment). - /// Notes: - /// - Only properties are updated (not fields). - /// - Collections: updates elements for IList/arrays and dictionary values; traverses other IEnumerable to reach nested objects. - /// - Read-only properties (no setter) are skipped. - /// - Root objects that are a DateTime value cannot be changed in place (struct); wrap them in a container if needed. - /// - public static void EnsureDateTimeUTC(object obj) - { - var visited = new HashSet(ReferenceEqualityComparer.Instance); - EnsureUtcInternal(obj, visited); - } - - private static void EnsureUtcInternal(object obj, HashSet visited) - { - if (obj == null) return; - - var type = obj.GetType(); - - // Avoid revisiting reference objects (handle cycles, EF proxies, etc.) - if (!type.IsValueType) // only track reference types - { - if (!visited.Add(obj)) return; - } - - // Handle dictionaries: update values, recurse into objects - if (obj is IDictionary dict) - { - var keys = new List(); - foreach (var k in dict.Keys) keys.Add(k); - - foreach (var key in keys) - { - var value = dict[key]; - if (TrySpecifyUtcOnBoxedDateTime(ref value)) - { - dict[key] = value; - } - else - { - EnsureUtcInternal(value, visited); - } - } - return; - } - - // Handle lists/arrays: update elements, recurse into objects - if (obj is IList list) - { - for (int i = 0; i < list.Count; i++) - { - var element = list[i]; - if (TrySpecifyUtcOnBoxedDateTime(ref element)) - { - list[i] = element; - } - else - { - EnsureUtcInternal(element, visited); - } - } - return; - } - - // Traverse other enumerables just to reach nested objects (cannot replace DateTime elements) - if (obj is IEnumerable enumerable && !(obj is string)) - { - foreach (var element in enumerable) - { - EnsureUtcInternal(element, visited); - } - } - - // Inspect properties - const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - foreach (var prop in type.GetProperties(flags)) - { - // indexers not supported - if (prop.GetIndexParameters().Length != 0) continue; - - // must be readable - if (!prop.CanRead) continue; - - var pType = prop.PropertyType; - - // DateTime - if (pType == typeof(DateTime)) - { - var val = (DateTime)prop.GetValue(obj); - if (val.Kind == DateTimeKind.Unspecified) - { - var newVal = DateTime.SpecifyKind(val, DateTimeKind.Utc); - TrySetProperty(obj, prop, newVal); - } - continue; - } - - // Nullable - var underlying = Nullable.GetUnderlyingType(pType); - if (underlying == typeof(DateTime)) - { - var val = (DateTime?)prop.GetValue(obj); - if (val.HasValue && val.Value.Kind == DateTimeKind.Unspecified) - { - var newVal = (DateTime?)DateTime.SpecifyKind(val.Value, DateTimeKind.Utc); - TrySetProperty(obj, prop, newVal); - } - continue; - } - - // Skip obvious simple types - if (IsSimple(pType)) continue; - - // Recurse into complex objects - var child = prop.GetValue(obj); - EnsureUtcInternal(child, visited); - } - } - - private static bool TrySpecifyUtcOnBoxedDateTime(ref object obj) - { - if (obj == null) return false; - - var t = obj.GetType(); - - // Exact DateTime - if (t == typeof(DateTime)) - { - var dt = (DateTime)obj; - if (dt.Kind == DateTimeKind.Unspecified) - { - obj = DateTime.SpecifyKind(dt, DateTimeKind.Utc); - } - return true; - } - - // Nullable boxed - var underlying = Nullable.GetUnderlyingType(t); - if (underlying == typeof(DateTime)) - { - // boxed Nullable can be unboxed to DateTime? safely - var ndt = (DateTime?)obj; - if (ndt.HasValue && ndt.Value.Kind == DateTimeKind.Unspecified) - { - obj = (DateTime?)DateTime.SpecifyKind(ndt.Value, DateTimeKind.Utc); - } - return true; - } - - return false; - } - - private static bool TrySetProperty(object target, PropertyInfo prop, object value) - { - try - { - // Prefer invoking the setter (even if non-public) - var setter = prop.GetSetMethod(nonPublic: true); - if (setter == null) return false; - setter.Invoke(target, new[] { value }); - return true; - } - catch - { - // Fall back to PropertyInfo.SetValue (may still work in some runtimes) - try - { - prop.SetValue(target, value); - return true; - } - catch - { - return false; - } - } - } - - private static bool IsSimple(Type t) - { - if (t.IsPrimitive || t.IsEnum) return true; - if (t == typeof(string) || t == typeof(decimal) || t == typeof(Guid) || - t == typeof(Uri) || t == typeof(TimeSpan) || t == typeof(DateTimeOffset)) - return true; - // Value types other than DateTime/Nullable are considered simple - return t.IsValueType; - } - - /// Reference equality comparer for the visited set. - private sealed class ReferenceEqualityComparer : IEqualityComparer - { - public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); - public new bool Equals(object x, object y) => ReferenceEquals(x, y); - public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); - } - } -} diff --git a/Software/Visual_Studio/Tango.Telemetry/Destinations/TelemetryAzureHubDestination.cs b/Software/Visual_Studio/Tango.Telemetry/Destinations/TelemetryAzureHubDestination.cs index 94e949ba3..61a4bd880 100644 --- a/Software/Visual_Studio/Tango.Telemetry/Destinations/TelemetryAzureHubDestination.cs +++ b/Software/Visual_Studio/Tango.Telemetry/Destinations/TelemetryAzureHubDestination.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading.Tasks; using Tango.Core; using Tango.Logging; +using Tango.Telemetry.Helpers; namespace Tango.Telemetry.Destinations { @@ -96,13 +97,21 @@ namespace Tango.Telemetry.Destinations /// True if the destination is available; otherwise, false. public Task IsAvailable() { - if (_hubClient == null) + //if (NetworkListManager.GetNetworks(NetworkConnectivityLevels.Connected).Any(x => x.IsConnectedToInternet)) + if (InternetConnectivity.IsInternetAvailable()) { - return Task.FromResult(true); + if (_hubClient == null) + { + return Task.FromResult(true); + } + else + { + return Task.FromResult(HubConnectionStatus == ConnectionStatus.Connected); + } } else { - return Task.FromResult(HubConnectionStatus == ConnectionStatus.Connected); + return Task.FromResult(false); } } @@ -117,6 +126,7 @@ namespace Tango.Telemetry.Destinations if (_hubClient == null) { _hubClient = DeviceClient.CreateFromConnectionString(ConnectionString, TransportType.Mqtt); + _hubClient.OperationTimeoutInMilliseconds = 2000; _hubClient.SetConnectionStatusChangesHandler((status, reason) => { HubConnectionStatus = status; @@ -147,25 +157,34 @@ namespace Tango.Telemetry.Destinations message.Properties.Add(prop.Key, prop.Value); } - if (BatchSize > 1) + try { - _batch.Add(message); - - if (_batch.Count >= BatchSize) + if (BatchSize > 1) { - LogManager.Log($"Sending telemetry batch of {_batch.Count} messages to Azure IoT Hub.", LogCategory.Debug); - await _hubClient.SendEventBatchAsync(_batch.ToList()); - _batch.Clear(); + _batch.Add(message); + + if (_batch.Count >= BatchSize) + { + LogManager.Log($"Sending telemetry batch of {_batch.Count} messages to Azure IoT Hub.", LogCategory.Debug); + await _hubClient.SendEventBatchAsync(_batch.ToList()); + _batch.Clear(); + } + else + { + LogManager.Log($"Queued telemetry message for batching. {_batch.Count}/{BatchSize} currently queued.", LogCategory.Debug); + } } else { - LogManager.Log($"Queued telemetry message for batching. {_batch.Count}/{BatchSize} currently queued.", LogCategory.Debug); + LogManager.Log("Sending single telemetry message to Azure IoT Hub.", LogCategory.Debug); + await _hubClient.SendEventAsync(message); } } - else + catch (Exception) { - LogManager.Log("Sending single telemetry message to Azure IoT Hub.", LogCategory.Debug); - await _hubClient.SendEventAsync(message); + _hubClient?.Dispose(); + _hubClient = null; + throw; } } diff --git a/Software/Visual_Studio/Tango.Telemetry/Helpers/DateTimeUTCFixer.cs b/Software/Visual_Studio/Tango.Telemetry/Helpers/DateTimeUTCFixer.cs new file mode 100644 index 000000000..3ce0c700f --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Helpers/DateTimeUTCFixer.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Tango.Telemetry.Helpers +{ + internal static class DateTimeUtcFixer + { + /// + /// Recursively scans the object graph starting at and + /// sets Kind=Utc on any DateTime/DateTime? properties or collection elements that are Kind=Unspecified. + /// Uses DateTime.SpecifyKind(value, DateTimeKind.Utc) (no clock adjustment). + /// Notes: + /// - Only properties are updated (not fields). + /// - Collections: updates elements for IList/arrays and dictionary values; traverses other IEnumerable to reach nested objects. + /// - Read-only properties (no setter) are skipped. + /// - Root objects that are a DateTime value cannot be changed in place (struct); wrap them in a container if needed. + /// + public static void EnsureDateTimeUTC(object obj) + { + var visited = new HashSet(ReferenceEqualityComparer.Instance); + EnsureUtcInternal(obj, visited); + } + + private static void EnsureUtcInternal(object obj, HashSet visited) + { + if (obj == null) return; + + var type = obj.GetType(); + + // Avoid revisiting reference objects (handle cycles, EF proxies, etc.) + if (!type.IsValueType) // only track reference types + { + if (!visited.Add(obj)) return; + } + + // Handle dictionaries: update values, recurse into objects + if (obj is IDictionary dict) + { + var keys = new List(); + foreach (var k in dict.Keys) keys.Add(k); + + foreach (var key in keys) + { + var value = dict[key]; + if (TrySpecifyUtcOnBoxedDateTime(ref value)) + { + dict[key] = value; + } + else + { + EnsureUtcInternal(value, visited); + } + } + return; + } + + // Handle lists/arrays: update elements, recurse into objects + if (obj is IList list) + { + for (int i = 0; i < list.Count; i++) + { + var element = list[i]; + if (TrySpecifyUtcOnBoxedDateTime(ref element)) + { + list[i] = element; + } + else + { + EnsureUtcInternal(element, visited); + } + } + return; + } + + // Traverse other enumerables just to reach nested objects (cannot replace DateTime elements) + if (obj is IEnumerable enumerable && !(obj is string)) + { + foreach (var element in enumerable) + { + EnsureUtcInternal(element, visited); + } + } + + // Inspect properties + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + foreach (var prop in type.GetProperties(flags)) + { + // indexers not supported + if (prop.GetIndexParameters().Length != 0) continue; + + // must be readable + if (!prop.CanRead) continue; + + var pType = prop.PropertyType; + + // DateTime + if (pType == typeof(DateTime)) + { + var val = (DateTime)prop.GetValue(obj); + if (val.Kind == DateTimeKind.Unspecified) + { + var newVal = DateTime.SpecifyKind(val, DateTimeKind.Utc); + TrySetProperty(obj, prop, newVal); + } + continue; + } + + // Nullable + var underlying = Nullable.GetUnderlyingType(pType); + if (underlying == typeof(DateTime)) + { + var val = (DateTime?)prop.GetValue(obj); + if (val.HasValue && val.Value.Kind == DateTimeKind.Unspecified) + { + var newVal = (DateTime?)DateTime.SpecifyKind(val.Value, DateTimeKind.Utc); + TrySetProperty(obj, prop, newVal); + } + continue; + } + + // Skip obvious simple types + if (IsSimple(pType)) continue; + + // Recurse into complex objects + var child = prop.GetValue(obj); + EnsureUtcInternal(child, visited); + } + } + + private static bool TrySpecifyUtcOnBoxedDateTime(ref object obj) + { + if (obj == null) return false; + + var t = obj.GetType(); + + // Exact DateTime + if (t == typeof(DateTime)) + { + var dt = (DateTime)obj; + if (dt.Kind == DateTimeKind.Unspecified) + { + obj = DateTime.SpecifyKind(dt, DateTimeKind.Utc); + } + return true; + } + + // Nullable boxed + var underlying = Nullable.GetUnderlyingType(t); + if (underlying == typeof(DateTime)) + { + // boxed Nullable can be unboxed to DateTime? safely + var ndt = (DateTime?)obj; + if (ndt.HasValue && ndt.Value.Kind == DateTimeKind.Unspecified) + { + obj = (DateTime?)DateTime.SpecifyKind(ndt.Value, DateTimeKind.Utc); + } + return true; + } + + return false; + } + + private static bool TrySetProperty(object target, PropertyInfo prop, object value) + { + try + { + // Prefer invoking the setter (even if non-public) + var setter = prop.GetSetMethod(nonPublic: true); + if (setter == null) return false; + setter.Invoke(target, new[] { value }); + return true; + } + catch + { + // Fall back to PropertyInfo.SetValue (may still work in some runtimes) + try + { + prop.SetValue(target, value); + return true; + } + catch + { + return false; + } + } + } + + private static bool IsSimple(Type t) + { + if (t.IsPrimitive || t.IsEnum) return true; + if (t == typeof(string) || t == typeof(decimal) || t == typeof(Guid) || + t == typeof(Uri) || t == typeof(TimeSpan) || t == typeof(DateTimeOffset)) + return true; + // Value types other than DateTime/Nullable are considered simple + return t.IsValueType; + } + + /// Reference equality comparer for the visited set. + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); + public new bool Equals(object x, object y) => ReferenceEquals(x, y); + public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); + } + } +} 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 +{ + /// + /// 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 + } + } + } + } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/Helpers/JsonFlattener.cs b/Software/Visual_Studio/Tango.Telemetry/Helpers/JsonFlattener.cs new file mode 100644 index 000000000..1355a8fc4 --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Helpers/JsonFlattener.cs @@ -0,0 +1,89 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Tango.Telemetry.Helpers +{ + internal static class JsonFlattener + { + public static string FlattenObjectToFlatJson(object obj, Formatting format) + { + var flat = new JObject(); + FlattenRecursive(obj, flat, prefix: null); + return flat.ToString(format); + } + + private static void FlattenRecursive(object obj, JObject target, string prefix) + { + if (obj == null) + return; + + var type = obj.GetType(); + if (type == typeof(JObject)) + { + foreach (var prop in ((JObject)obj).Properties()) + { + FlattenRecursive(prop.Value, target, Combine(prefix, prop.Name)); + } + return; + } + + if (obj is JValue jVal) + { + target[Combine(prefix, "Value")] = JToken.FromObject(jVal.Value); + return; + } + + if (obj is JToken jToken && jToken.Type == JTokenType.Object) + { + foreach (var prop in ((JObject)jToken).Properties()) + { + FlattenRecursive(prop.Value, target, Combine(prefix, prop.Name)); + } + return; + } + + foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (!prop.CanRead) continue; + + var value = prop.GetValue(obj); + + if (value == null) continue; + + var valueType = value.GetType(); + + if (IsSimpleType(valueType)) + { + target[Combine(prefix, prop.Name)] = JToken.FromObject(value); + } + else if (value is IEnumerable enumerable && !(value is string)) + { + int index = 0; + foreach (var item in enumerable) + { + FlattenRecursive(item, target, Combine(prefix, $"{prop.Name}_{index}")); + index++; + } + } + else + { + FlattenRecursive(value, target, Combine(prefix, prop.Name)); + } + } + } + + private static string Combine(string prefix, string name) + { + return string.IsNullOrEmpty(prefix) ? name : $"{prefix}_{name}"; + } + + private static bool IsSimpleType(Type type) + { + return type.IsPrimitive || type.IsValueType || type == typeof(string); + } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/JsonFlattener.cs b/Software/Visual_Studio/Tango.Telemetry/JsonFlattener.cs deleted file mode 100644 index 6658dcd9f..000000000 --- a/Software/Visual_Studio/Tango.Telemetry/JsonFlattener.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Reflection; - -namespace Tango.Telemetry -{ - internal static class JsonFlattener - { - public static string FlattenObjectToFlatJson(object obj, Formatting format) - { - var flat = new JObject(); - FlattenRecursive(obj, flat, prefix: null); - return flat.ToString(format); - } - - private static void FlattenRecursive(object obj, JObject target, string prefix) - { - if (obj == null) - return; - - var type = obj.GetType(); - if (type == typeof(JObject)) - { - foreach (var prop in ((JObject)obj).Properties()) - { - FlattenRecursive(prop.Value, target, Combine(prefix, prop.Name)); - } - return; - } - - if (obj is JValue jVal) - { - target[Combine(prefix, "Value")] = JToken.FromObject(jVal.Value); - return; - } - - if (obj is JToken jToken && jToken.Type == JTokenType.Object) - { - foreach (var prop in ((JObject)jToken).Properties()) - { - FlattenRecursive(prop.Value, target, Combine(prefix, prop.Name)); - } - return; - } - - foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - if (!prop.CanRead) continue; - - var value = prop.GetValue(obj); - - if (value == null) continue; - - var valueType = value.GetType(); - - if (IsSimpleType(valueType)) - { - target[Combine(prefix, prop.Name)] = JToken.FromObject(value); - } - else if (value is IEnumerable enumerable && !(value is string)) - { - int index = 0; - foreach (var item in enumerable) - { - FlattenRecursive(item, target, Combine(prefix, $"{prop.Name}_{index}")); - index++; - } - } - else - { - FlattenRecursive(value, target, Combine(prefix, prop.Name)); - } - } - } - - private static string Combine(string prefix, string name) - { - return string.IsNullOrEmpty(prefix) ? name : $"{prefix}_{name}"; - } - - private static bool IsSimpleType(Type type) - { - return type.IsPrimitive || type.IsValueType || type == typeof(string); - } - } -} diff --git a/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj b/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj index 14cd6e904..035625149 100644 --- a/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj +++ b/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj @@ -100,6 +100,9 @@ ..\packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll + + ..\packages\WindowsAPICodePack-Core.1.1.1\lib\Microsoft.WindowsAPICodePack.dll + ..\packages\WindowsAzure.Storage.8.7.0\lib\net45\Microsoft.WindowsAzure.Storage.dll @@ -239,9 +242,10 @@ - + + @@ -296,7 +300,7 @@ - + diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryBase.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryBase.cs index c2879e9db..72fcdd681 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetryBase.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryBase.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Tango.Telemetry.Helpers; namespace Tango.Telemetry { diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs index 538a3a0d7..aa50041ca 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Tango.Core; using Tango.Core.ExtensionMethods; using Tango.Logging; +using Tango.Telemetry.Helpers; namespace Tango.Telemetry { diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryPendingStorageSource.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryPendingStorageSource.cs index 96022341d..a5e176ca7 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetryPendingStorageSource.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryPendingStorageSource.cs @@ -9,7 +9,7 @@ namespace Tango.Telemetry public class TelemetryPendingStorageSource : ITelemetrySource { public string Name { get; private set; } = "Pending Storage"; - public bool RequiresTelemetryDuplicationTracking { get => true; } + public bool RequiresTelemetryDuplicationTracking { get => false; } public void Dispose() { diff --git a/Software/Visual_Studio/Tango.Telemetry/packages.config b/Software/Visual_Studio/Tango.Telemetry/packages.config index f15b9fef6..b6e99cfef 100644 --- a/Software/Visual_Studio/Tango.Telemetry/packages.config +++ b/Software/Visual_Studio/Tango.Telemetry/packages.config @@ -87,5 +87,6 @@ + \ No newline at end of file diff --git a/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Program.cs b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Program.cs index 3346b425e..b11c75f6f 100644 --- a/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Program.cs +++ b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Program.cs @@ -253,7 +253,7 @@ namespace Tango.Telemetry.Tester.IOT.CLI using (ObservablesContext db = ObservablesContext.CreateDefault()) { - foreach (var evType in db.EventTypes.ToList()) + foreach (var evType in db.EventTypes.Take(20).ToList()) { TelemetryEvent ev = new TelemetryEvent(); ev.Description = $"Description of {evType.Description}"; @@ -262,7 +262,7 @@ namespace Tango.Telemetry.Tester.IOT.CLI ev.Time = DateTime.UtcNow; ev.Time = DateTime.SpecifyKind(ev.Time, DateTimeKind.Unspecified); TelemetryAvailable?.Invoke(this, new TelemetryAvailableEventArgs() { TelemetryObject = ev }); - Thread.Sleep(200); + Thread.Sleep(5000); } } }); -- cgit v1.3.1