Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
TelemetrySender.cs
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | using System; |
| 2 | using System.Globalization; |
| 3 | using System.Net.Http; |
| 4 | using System.Net.Http.Headers; |
| 5 | using System.Threading; |
| 6 | using System.Threading.Tasks; |
| 7 | using OpenCKey.Interop; |
| 8 | |
| 9 | namespace OpenCKey |
| 10 | { |
| 11 | /// <summary> |
| 12 | /// The HTTP half of telemetry (the core never does networking). A |
| 13 | /// thread-pool timer asks the core whether a batch is due |
| 14 | /// (<c>ckey_telemetry_should_send</c>: mode 1, something to send, at most |
| 15 | /// once per hour); if so it takes the batch, POSTs it asynchronously and |
| 16 | /// reports the HTTP status (0 for a network error) back to the core, which |
| 17 | /// keeps or drops the batch per docs/adoption-and-telemetry.md. Nothing |
| 18 | /// here runs on a scan thread or blocks a caller. |
| 19 | /// </summary> |
| 20 | internal static class TelemetrySender |
| 21 | { |
| 22 | private static readonly object Gate = new object(); |
| 23 | private static Timer? _timer; |
| 24 | private static int _sending; |
| 25 | private static HttpClient? _client; |
| 26 | |
| 27 | /// <summary>How often the timer asks the core; the core enforces the hourly cadence.</summary> |
| 28 | private static readonly TimeSpan Poll = TimeSpan.FromMinutes(1); |
| 29 | |
| 30 | /// <summary>The batch endpoint; tests point it at a local listener.</summary> |
| 31 | internal static Uri Endpoint { get; set; } = new Uri(CKeyTelemetry.DefaultEndpoint); |
| 32 | |
| 33 | /// <summary>TEST ONLY: false keeps the poll timer from starting, so tests drive sends themselves.</summary> |
| 34 | internal static bool AutoSend { get; set; } = true; |
| 35 | |
| 36 | /// <summary>Starts the poll timer once. It does not keep the process alive.</summary> |
| 37 | internal static void EnsureStarted() |
| 38 | { |
| 39 | if (_timer != null || !AutoSend) return; |
| 40 | lock (Gate) |
| 41 | { |
| 42 | if (_timer != null) return; |
| 43 | _timer = new Timer(state => { _ = TrySendAsync(); }, null, Poll, Poll); |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | private static HttpClient Client |
| 48 | { |
| 49 | get |
| 50 | { |
| 51 | if (_client != null) return _client; |
| 52 | lock (Gate) |
| 53 | { |
| 54 | // No cookies, no credentials, no identifying headers. |
| 55 | var handler = new HttpClientHandler { UseCookies = false, UseDefaultCredentials = false, AllowAutoRedirect = false }; |
| 56 | return _client ??= new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) }; |
| 57 | } |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | /// <summary>One attempt: true when a batch was posted and accepted (2xx).</summary> |
| 62 | internal static async Task<bool> TrySendAsync() |
| 63 | { |
| 64 | if (Interlocked.Exchange(ref _sending, 1) == 1) return false; |
| 65 | try |
| 66 | { |
| 67 | if (!CKeyTelemetry.IsAvailable) return false; |
| 68 | CKeyTelemetry.EnsureClient(); |
| 69 | if (NativeTelemetry.ckey_telemetry_should_send(MonotonicClock.NowMs) == 0) return false; |
| 70 | if (!TakeBatch(out byte[] body, out int length, out ulong batchId)) return false; |
| 71 | |
| 72 | int status = 0; |
| 73 | try |
| 74 | { |
| 75 | using var content = new ByteArrayContent(body, 0, length); |
| 76 | content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); |
| 77 | using HttpResponseMessage response = await Client.PostAsync(Endpoint, content).ConfigureAwait(false); |
| 78 | status = (int)response.StatusCode; |
| 79 | } |
| 80 | catch (Exception) |
| 81 | { |
| 82 | status = 0; // network error, timeout, no connectivity: the core keeps the batch |
| 83 | } |
| 84 | if (status < 0 || status > 999) status = 0; |
| 85 | // A mode switch while in flight makes the id stale; the core then ignores it. |
| 86 | NativeTelemetry.ckey_telemetry_batch_result(batchId, status, MonotonicClock.NowMs); |
| 87 | return status >= 200 && status <= 299; |
| 88 | } |
| 89 | catch (Exception) |
| 90 | { |
| 91 | return false; // telemetry never surfaces errors to the host |
| 92 | } |
| 93 | finally |
| 94 | { |
| 95 | Volatile.Write(ref _sending, 0); |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | private static unsafe bool TakeBatch(out byte[] body, out int length, out ulong batchId) |
| 100 | { |
| 101 | byte[] day = Native.ToUtf8Z(DateTime.UtcNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))!; |
| 102 | body = new byte[4096]; // CKEY_TELEMETRY_MAX_BODY_BYTES; the body is always well under it |
| 103 | nuint len = 0; |
| 104 | ulong id = 0; |
| 105 | int status; |
| 106 | fixed (byte* d = day) |
| 107 | { |
| 108 | fixed (byte* b = body) status = NativeTelemetry.ckey_telemetry_take_batch(d, b, (nuint)body.Length, &len, &id); |
| 109 | if (status == (int)CKeyStatus.InsufficientBuffer) |
| 110 | { |
| 111 | body = new byte[Native.ToInt32Length(len + 1, "Telemetry batch")]; |
| 112 | fixed (byte* b = body) status = NativeTelemetry.ckey_telemetry_take_batch(d, b, (nuint)body.Length, &len, &id); |
| 113 | } |
| 114 | } |
| 115 | length = (int)len; |
| 116 | batchId = id; |
| 117 | return status == 0 && id != 0 && length > 0; |
| 118 | } |
| 119 | } |
| 120 | } |