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

Size
5.1 KB
Lines
120
Language
csharp
SHA-256
8994e5dd91d2…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1using System;
2using System.Globalization;
3using System.Net.Http;
4using System.Net.Http.Headers;
5using System.Threading;
6using System.Threading.Tasks;
7using OpenCKey.Interop;
8
9namespace 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}