Winify – Blame information for rev 64
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | office | 1 | using System; |
61 | office | 2 | using System.Collections.Generic; |
3 | using System.Diagnostics; |
||
1 | office | 4 | using System.Drawing; |
61 | office | 5 | using System.Globalization; |
1 | office | 6 | using System.IO; |
61 | office | 7 | using System.Linq; |
50 | office | 8 | using System.Net; |
1 | office | 9 | using System.Net.Http; |
10 | using System.Net.Http.Headers; |
||
61 | office | 11 | using System.Runtime.Caching; |
12 | using System.Runtime.CompilerServices; |
||
50 | office | 13 | using System.Security.Authentication; |
46 | office | 14 | using System.Security.Cryptography.X509Certificates; |
1 | office | 15 | using System.Text; |
16 | using System.Threading; |
||
17 | using System.Threading.Tasks; |
||
61 | office | 18 | using System.Threading.Tasks.Dataflow; |
19 | using System.Windows.Forms; |
||
1 | office | 20 | using Newtonsoft.Json; |
18 | office | 21 | using Serilog; |
24 | office | 22 | using Servers; |
44 | office | 23 | using WebSocketSharp; |
50 | office | 24 | using WebSocketSharp.Net; |
61 | office | 25 | using Winify.Utilities; |
44 | office | 26 | using ErrorEventArgs = WebSocketSharp.ErrorEventArgs; |
50 | office | 27 | using NetworkCredential = System.Net.NetworkCredential; |
1 | office | 28 | |
29 | namespace Winify.Gotify |
||
30 | { |
||
31 | public class GotifyConnection : IDisposable |
||
32 | { |
||
33 | #region Public Events & Delegates |
||
34 | |||
35 | public event EventHandler<GotifyNotificationEventArgs> GotifyNotification; |
||
36 | |||
37 | #endregion |
||
38 | |||
39 | #region Private Delegates, Events, Enums, Properties, Indexers and Fields |
||
40 | |||
25 | office | 41 | private readonly Server _server; |
42 | |||
1 | office | 43 | private CancellationToken _cancellationToken; |
44 | |||
45 | private CancellationTokenSource _cancellationTokenSource; |
||
46 | |||
47 | private Task _runTask; |
||
48 | |||
39 | office | 49 | private HttpClient _httpClient; |
50 | |||
51 | private readonly Uri _webSocketsUri; |
||
52 | |||
53 | private readonly Uri _httpUri; |
||
44 | office | 54 | private WebSocket _webSocketSharp; |
59 | office | 55 | private readonly Configuration.Configuration _configuration; |
56 | private Task _initTask; |
||
61 | office | 57 | private IDisposable _tplRetrievePastMessagesLink; |
58 | private IDisposable _tplWebSocketsBufferBlockTransformLink; |
||
59 | private IDisposable _tplWebSocketsTransformActionLink; |
||
60 | private IDisposable _tplWebSocketsTransformActionNullLink; |
||
61 | private readonly BufferBlock<byte[]> _webSocketMessageBufferBlock; |
||
62 | private readonly Stopwatch _webSocketsClientPingStopWatch; |
||
63 | private readonly ScheduledContinuation _webSocketsServerResponseScheduledContinuation; |
||
39 | office | 64 | |
61 | office | 65 | private readonly MemoryCache _applicationImageCache; |
25 | office | 66 | #endregion |
1 | office | 67 | |
25 | office | 68 | #region Constructors, Destructors and Finalizers |
24 | office | 69 | |
44 | office | 70 | private GotifyConnection() |
24 | office | 71 | { |
61 | office | 72 | _applicationImageCache = new MemoryCache("GotifyApplicationImageCache"); |
73 | _webSocketsServerResponseScheduledContinuation = new ScheduledContinuation(); |
||
74 | _webSocketsClientPingStopWatch = new Stopwatch(); |
||
75 | |||
76 | _webSocketMessageBufferBlock = new BufferBlock<byte[]>(new DataflowBlockOptions { CancellationToken = _cancellationToken }); |
||
77 | var webSocketTransformBlock = new TransformBlock<byte[], GotifyMessage>(bytes => |
||
78 | { |
||
79 | if (bytes.Length == 0) |
||
80 | { |
||
81 | return null; |
||
82 | } |
||
83 | |||
84 | var message = Encoding.UTF8.GetString(bytes, 0, bytes.Length); |
||
85 | |||
86 | GotifyMessage gotifyNotification; |
||
87 | |||
88 | try |
||
89 | { |
||
90 | gotifyNotification = JsonConvert.DeserializeObject<GotifyMessage>(message); |
||
91 | } |
||
92 | catch (JsonSerializationException exception) |
||
93 | { |
||
94 | Log.Warning($"Could not deserialize notification: {exception.Message}"); |
||
95 | |||
96 | return null; |
||
97 | } |
||
98 | |||
99 | if (gotifyNotification == null) |
||
100 | { |
||
101 | Log.Warning($"Could not deserialize gotify notification: {message}"); |
||
102 | |||
103 | return null; |
||
104 | } |
||
105 | |||
106 | return gotifyNotification; |
||
107 | |||
108 | }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken }); |
||
109 | |||
110 | var webSocketActionBlock = new ActionBlock<GotifyMessage>(async message => |
||
111 | { |
||
112 | message.Server = _server; |
||
113 | |||
114 | var cachedImage = _applicationImageCache.Get($"{message.AppId}"); |
||
64 | office | 115 | if (cachedImage is Stream cachedImageStream) |
61 | office | 116 | { |
64 | office | 117 | using var cachedImageMemoryStream = new MemoryStream(); |
118 | await cachedImageStream.CopyToAsync(cachedImageMemoryStream); |
||
119 | |||
120 | var cachedApplicationImage = new Bitmap(cachedImageMemoryStream); |
||
61 | office | 121 | GotifyNotification?.Invoke(this, |
64 | office | 122 | new GotifyNotificationEventArgs(message, cachedApplicationImage)); |
123 | |||
61 | office | 124 | return; |
125 | } |
||
126 | |||
64 | office | 127 | using var imageStream = await RetrieveGotifyApplicationImage(message.AppId, _cancellationToken); |
61 | office | 128 | if (imageStream == null || imageStream.Length == 0) |
129 | { |
||
130 | Log.Warning("Could not find any application image for notification"); |
||
131 | return; |
||
132 | } |
||
133 | |||
64 | office | 134 | using var memoryStream = new MemoryStream(); |
61 | office | 135 | |
64 | office | 136 | await imageStream.CopyToAsync(memoryStream); |
62 | office | 137 | |
64 | office | 138 | var imageBytes = memoryStream.ToArray(); |
139 | |||
140 | _applicationImageCache.Add($"{message.AppId}", imageBytes, |
||
61 | office | 141 | new CacheItemPolicy |
142 | { |
||
143 | SlidingExpiration = TimeSpan.FromHours(1) |
||
144 | }); |
||
145 | |||
64 | office | 146 | var image = new Bitmap(memoryStream); |
147 | |||
61 | office | 148 | GotifyNotification?.Invoke(this, |
149 | new GotifyNotificationEventArgs(message, image)); |
||
150 | |||
151 | Log.Debug($"Notification message received: {message.Message}"); |
||
152 | |||
153 | }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken }); |
||
154 | |||
155 | _tplWebSocketsBufferBlockTransformLink = _webSocketMessageBufferBlock.LinkTo(webSocketTransformBlock, |
||
156 | new DataflowLinkOptions { PropagateCompletion = true }); |
||
157 | _tplWebSocketsTransformActionLink = webSocketTransformBlock.LinkTo(webSocketActionBlock, |
||
158 | new DataflowLinkOptions { PropagateCompletion = true }, message => message != null); |
||
159 | _tplWebSocketsTransformActionNullLink = webSocketTransformBlock.LinkTo(DataflowBlock.NullTarget<GotifyMessage>(), |
||
160 | new DataflowLinkOptions() { PropagateCompletion = true }); |
||
39 | office | 161 | } |
162 | |||
59 | office | 163 | public GotifyConnection(Server server, Configuration.Configuration configuration) : this() |
39 | office | 164 | { |
24 | office | 165 | _server = server; |
44 | office | 166 | _configuration = configuration; |
39 | office | 167 | |
54 | office | 168 | ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; |
59 | office | 169 | var httpClientHandler = new HttpClientHandler |
50 | office | 170 | { |
54 | office | 171 | // mono does not implement this |
172 | //SslProtocols = SslProtocols.Tls12 |
||
50 | office | 173 | }; |
174 | |||
44 | office | 175 | _httpClient = new HttpClient(httpClientHandler); |
176 | if (_configuration.IgnoreSelfSignedCertificates) |
||
177 | httpClientHandler.ServerCertificateCustomValidationCallback = |
||
46 | office | 178 | (httpRequestMessage, cert, cetChain, policyErrors) => true; |
39 | office | 179 | |
50 | office | 180 | if (_configuration.Proxy.Enable) |
181 | httpClientHandler.Proxy = new WebProxy(_configuration.Proxy.Url, false, new string[] { }, |
||
182 | new NetworkCredential(_configuration.Proxy.Username, _configuration.Proxy.Password)); |
||
183 | |||
47 | office | 184 | _httpClient = new HttpClient(httpClientHandler); |
185 | if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password)) |
||
186 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", |
||
187 | Convert.ToBase64String(Encoding.Default.GetBytes($"{_server.Username}:{_server.Password}"))); |
||
44 | office | 188 | |
47 | office | 189 | if (!Uri.TryCreate(_server.Url, UriKind.Absolute, out _httpUri)) |
39 | office | 190 | { |
51 | office | 191 | Log.Error($"No HTTP URL could be built out of the supplied server URI {_server.Url}"); |
47 | office | 192 | return; |
193 | } |
||
44 | office | 194 | |
47 | office | 195 | // Build the web sockets URI. |
196 | var webSocketsUriBuilder = new UriBuilder(_httpUri); |
||
197 | switch (webSocketsUriBuilder.Scheme.ToUpperInvariant()) |
||
198 | { |
||
199 | case "HTTP": |
||
200 | webSocketsUriBuilder.Scheme = "ws"; |
||
201 | break; |
||
202 | case "HTTPS": |
||
203 | webSocketsUriBuilder.Scheme = "wss"; |
||
204 | break; |
||
205 | } |
||
206 | |||
207 | try |
||
208 | { |
||
39 | office | 209 | webSocketsUriBuilder.Path = Path.Combine(webSocketsUriBuilder.Path, "stream"); |
210 | } |
||
47 | office | 211 | catch (ArgumentException exception) |
212 | { |
||
59 | office | 213 | Log.Error( |
214 | $"No WebSockets URL could be built from the provided URL {_server.Url} due to {exception.Message}"); |
||
47 | office | 215 | } |
216 | |||
217 | _webSocketsUri = webSocketsUriBuilder.Uri; |
||
24 | office | 218 | } |
219 | |||
1 | office | 220 | public void Dispose() |
221 | { |
||
222 | if (_cancellationTokenSource != null) |
||
223 | { |
||
224 | _cancellationTokenSource.Dispose(); |
||
225 | _cancellationTokenSource = null; |
||
226 | } |
||
39 | office | 227 | |
61 | office | 228 | if (_tplWebSocketsBufferBlockTransformLink != null) |
229 | { |
||
230 | _tplWebSocketsBufferBlockTransformLink.Dispose(); |
||
231 | _tplWebSocketsBufferBlockTransformLink = null; |
||
232 | } |
||
233 | |||
234 | if (_tplWebSocketsTransformActionLink != null) |
||
235 | { |
||
236 | _tplWebSocketsTransformActionLink.Dispose(); |
||
237 | _tplWebSocketsTransformActionLink = null; |
||
238 | } |
||
239 | |||
240 | if (_tplWebSocketsTransformActionNullLink != null) |
||
241 | { |
||
242 | _tplWebSocketsTransformActionNullLink.Dispose(); |
||
243 | _tplWebSocketsTransformActionNullLink = null; |
||
244 | } |
||
245 | |||
246 | if (_tplRetrievePastMessagesLink != null) |
||
247 | { |
||
248 | _tplRetrievePastMessagesLink.Dispose(); |
||
249 | _tplRetrievePastMessagesLink = null; |
||
250 | } |
||
251 | |||
48 | office | 252 | if (_webSocketSharp != null) |
253 | { |
||
254 | _webSocketSharp.Close(); |
||
255 | _webSocketSharp = null; |
||
256 | } |
||
44 | office | 257 | |
48 | office | 258 | if (_httpClient != null) |
259 | { |
||
260 | _httpClient.Dispose(); |
||
261 | _httpClient = null; |
||
262 | } |
||
1 | office | 263 | } |
264 | |||
265 | #endregion |
||
266 | |||
267 | #region Public Methods |
||
268 | |||
25 | office | 269 | public void Start() |
1 | office | 270 | { |
47 | office | 271 | if (_webSocketsUri == null || _httpUri == null) |
272 | { |
||
51 | office | 273 | Log.Error("Could not start connection to server due to unreadable URLs"); |
47 | office | 274 | return; |
275 | } |
||
276 | |||
1 | office | 277 | _cancellationTokenSource = new CancellationTokenSource(); |
278 | _cancellationToken = _cancellationTokenSource.Token; |
||
279 | |||
44 | office | 280 | Connect(); |
281 | |||
59 | office | 282 | if (_configuration.RetrievePastNotificationHours != 0) |
283 | { |
||
284 | _initTask = RetrievePastMessages(_cancellationToken); |
||
285 | } |
||
286 | |||
61 | office | 287 | _runTask = HeartBeat(_cancellationToken); |
1 | office | 288 | } |
289 | |||
44 | office | 290 | private void Connect() |
1 | office | 291 | { |
44 | office | 292 | _webSocketSharp = new WebSocket(_webSocketsUri.AbsoluteUri); |
61 | office | 293 | _webSocketSharp.EmitOnPing = true; |
294 | _webSocketSharp.WaitTime = TimeSpan.FromMinutes(1); |
||
50 | office | 295 | _webSocketSharp.SslConfiguration = new ClientSslConfiguration(_webSocketsUri.Host, |
296 | new X509CertificateCollection(new X509Certificate[] { }), SslProtocols.Tls12, false); |
||
297 | if (_configuration.Proxy.Enable) |
||
59 | office | 298 | _webSocketSharp.SetProxy(_configuration.Proxy.Url, _configuration.Proxy.Username, |
299 | _configuration.Proxy.Password); |
||
50 | office | 300 | |
47 | office | 301 | if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password)) |
302 | _webSocketSharp.SetCredentials(_server.Username, _server.Password, true); |
||
303 | |||
44 | office | 304 | if (_configuration.IgnoreSelfSignedCertificates) |
305 | _webSocketSharp.SslConfiguration.ServerCertificateValidationCallback += |
||
306 | (sender, certificate, chain, errors) => true; |
||
307 | |||
51 | office | 308 | _webSocketSharp.Log.Output = (logData, s) => |
309 | { |
||
310 | Log.Information($"WebSockets low level logging reported: {logData.Message}"); |
||
311 | }; |
||
312 | |||
44 | office | 313 | _webSocketSharp.OnMessage += WebSocketSharp_OnMessage; |
314 | _webSocketSharp.OnError += WebSocketSharp_OnError; |
||
315 | _webSocketSharp.OnOpen += WebSocketSharp_OnOpen; |
||
316 | _webSocketSharp.OnClose += WebSocketSharp_OnClose; |
||
59 | office | 317 | |
44 | office | 318 | _webSocketSharp.ConnectAsync(); |
1 | office | 319 | } |
320 | |||
44 | office | 321 | private void WebSocketSharp_OnClose(object sender, CloseEventArgs e) |
322 | { |
||
59 | office | 323 | Log.Information( |
324 | $"WebSockets connection to server {_webSocketsUri.AbsoluteUri} closed with reason {e.Reason}"); |
||
44 | office | 325 | } |
1 | office | 326 | |
44 | office | 327 | private void WebSocketSharp_OnOpen(object sender, EventArgs e) |
328 | { |
||
51 | office | 329 | Log.Information($"WebSockets connection to server {_webSocketsUri.AbsoluteUri} is now open"); |
61 | office | 330 | |
331 | _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken); |
||
44 | office | 332 | } |
1 | office | 333 | |
61 | office | 334 | private void OnUnresponsiveServer() |
335 | { |
||
336 | Log.Warning($"Server {_server} has not responded in a long while..."); |
||
337 | } |
||
338 | |||
44 | office | 339 | private async void WebSocketSharp_OnError(object sender, ErrorEventArgs e) |
1 | office | 340 | { |
59 | office | 341 | Log.Error( |
342 | $"Connection to WebSockets server {_webSocketsUri.AbsoluteUri} terminated unexpectedly with message {e.Message}", |
||
343 | e.Exception); |
||
48 | office | 344 | |
44 | office | 345 | if (_cancellationToken.IsCancellationRequested) |
1 | office | 346 | { |
44 | office | 347 | Stop(); |
348 | return; |
||
349 | } |
||
18 | office | 350 | |
44 | office | 351 | await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken); |
61 | office | 352 | |
51 | office | 353 | Log.Information($"Reconnecting to websocket server {_webSocketsUri.AbsoluteUri}"); |
18 | office | 354 | |
44 | office | 355 | Connect(); |
356 | } |
||
1 | office | 357 | |
44 | office | 358 | private async void WebSocketSharp_OnMessage(object sender, MessageEventArgs e) |
359 | { |
||
61 | office | 360 | if (e.IsPing) |
44 | office | 361 | { |
61 | office | 362 | Log.Information($"Server {_server} sent PING message"); |
1 | office | 363 | |
61 | office | 364 | _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken); |
1 | office | 365 | |
44 | office | 366 | return; |
367 | } |
||
12 | office | 368 | |
61 | office | 369 | await _webSocketMessageBufferBlock.SendAsync(e.RawData, _cancellationToken); |
44 | office | 370 | } |
12 | office | 371 | |
44 | office | 372 | public void Stop() |
373 | { |
||
59 | office | 374 | if (_cancellationTokenSource == null) return; |
375 | |||
376 | _cancellationTokenSource.Cancel(); |
||
44 | office | 377 | } |
378 | |||
379 | #endregion |
||
380 | |||
381 | #region Private Methods |
||
382 | |||
59 | office | 383 | private async Task RetrievePastMessages(CancellationToken cancellationToken) |
384 | { |
||
385 | var messageUriBuilder = new UriBuilder(_httpUri); |
||
61 | office | 386 | |
387 | var gotifyApplicationBufferBlock = new BufferBlock<GotifyApplication>(new DataflowBlockOptions { CancellationToken = cancellationToken }); |
||
388 | var gotifyApplicationActionBlock = new ActionBlock<GotifyApplication>(async application => |
||
59 | office | 389 | { |
390 | try |
||
391 | { |
||
392 | messageUriBuilder.Path = Path.Combine(messageUriBuilder.Path, "application", $"{application.Id}", |
||
393 | "message"); |
||
394 | } |
||
395 | catch (ArgumentException exception) |
||
396 | { |
||
397 | Log.Error($"No application URL could be built for {_server.Url} due to {exception.Message}"); |
||
398 | |||
61 | office | 399 | return; |
59 | office | 400 | } |
401 | |||
61 | office | 402 | HttpResponseMessage messagesResponse; |
403 | try |
||
404 | { |
||
405 | messagesResponse = await _httpClient.GetAsync(messageUriBuilder.Uri, cancellationToken); |
||
406 | } |
||
407 | catch (Exception exception) |
||
408 | { |
||
409 | Log.Error($"Could not get application {application.Id} due to {exception.Message}"); |
||
59 | office | 410 | |
61 | office | 411 | return; |
412 | } |
||
59 | office | 413 | |
61 | office | 414 | |
59 | office | 415 | var messages = await messagesResponse.Content.ReadAsStringAsync(); |
416 | |||
417 | GotifyMessageQuery gotifyMessageQuery; |
||
418 | try |
||
419 | { |
||
420 | gotifyMessageQuery = |
||
421 | JsonConvert.DeserializeObject<GotifyMessageQuery>(messages); |
||
422 | } |
||
423 | catch (JsonSerializationException exception) |
||
424 | { |
||
425 | Log.Warning($"Could not deserialize the message response: {exception.Message}"); |
||
426 | |||
61 | office | 427 | return; |
59 | office | 428 | } |
429 | |||
61 | office | 430 | foreach (var message in gotifyMessageQuery.Messages.Where(message => message.Date >= DateTime.Now - TimeSpan.FromHours(_configuration.RetrievePastNotificationHours))) |
59 | office | 431 | { |
61 | office | 432 | message.Server = _server; |
59 | office | 433 | |
61 | office | 434 | var cachedImage = _applicationImageCache.Get($"{message.AppId}"); |
64 | office | 435 | if (cachedImage is Stream cachedImageStream) |
61 | office | 436 | { |
64 | office | 437 | using var cachedImageMemoryStream = new MemoryStream(); |
438 | await cachedImageStream.CopyToAsync(cachedImageMemoryStream); |
||
439 | |||
440 | var cachedApplicationImage = new Bitmap(cachedImageMemoryStream); |
||
61 | office | 441 | GotifyNotification?.Invoke(this, |
64 | office | 442 | new GotifyNotificationEventArgs(message, cachedApplicationImage)); |
443 | |||
61 | office | 444 | return; |
445 | } |
||
59 | office | 446 | |
61 | office | 447 | using var imageStream = await RetrieveGotifyApplicationImage(message.AppId, _cancellationToken); |
64 | office | 448 | |
61 | office | 449 | if (imageStream == null || imageStream.Length == 0) |
450 | { |
||
451 | Log.Warning("Could not find any application image for notification"); |
||
59 | office | 452 | continue; |
61 | office | 453 | } |
59 | office | 454 | |
64 | office | 455 | using var memoryStream = new MemoryStream(); |
59 | office | 456 | |
64 | office | 457 | await imageStream.CopyToAsync(memoryStream); |
62 | office | 458 | |
64 | office | 459 | var imageBytes = memoryStream.ToArray(); |
460 | |||
461 | _applicationImageCache.Add($"{message.AppId}", imageBytes, |
||
61 | office | 462 | new CacheItemPolicy |
59 | office | 463 | { |
61 | office | 464 | SlidingExpiration = TimeSpan.FromHours(1) |
465 | }); |
||
59 | office | 466 | |
64 | office | 467 | var image = new Bitmap(memoryStream); |
468 | |||
61 | office | 469 | GotifyNotification?.Invoke(this, |
470 | new GotifyNotificationEventArgs(message, image)); |
||
471 | } |
||
59 | office | 472 | |
61 | office | 473 | }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken }); |
474 | |||
475 | gotifyApplicationBufferBlock.LinkTo(gotifyApplicationActionBlock, |
||
476 | new DataflowLinkOptions { PropagateCompletion = true }); |
||
477 | |||
478 | await foreach (var application in RetrieveGotifyApplications(cancellationToken)) |
||
479 | { |
||
480 | await gotifyApplicationBufferBlock.SendAsync(application, cancellationToken); |
||
59 | office | 481 | } |
61 | office | 482 | |
483 | gotifyApplicationBufferBlock.Complete(); |
||
484 | await gotifyApplicationActionBlock.Completion; |
||
485 | |||
59 | office | 486 | } |
487 | |||
61 | office | 488 | private async Task HeartBeat(CancellationToken cancellationToken) |
44 | office | 489 | { |
490 | try |
||
491 | { |
||
492 | do |
||
493 | { |
||
61 | office | 494 | await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); |
495 | |||
496 | _webSocketsClientPingStopWatch.Restart(); |
||
497 | if (!_webSocketSharp.Ping()) |
||
498 | { |
||
499 | Log.Warning($"Server {_server} did not respond to PING message."); |
||
500 | continue; |
||
501 | } |
||
502 | |||
503 | var delta = _webSocketsClientPingStopWatch.ElapsedMilliseconds; |
||
504 | |||
505 | Log.Information($"PING response latency for {_server} is {delta}ms"); |
||
506 | |||
507 | _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken); |
||
1 | office | 508 | } while (!cancellationToken.IsCancellationRequested); |
509 | } |
||
59 | office | 510 | catch (Exception exception) when (exception is OperationCanceledException || |
511 | exception is ObjectDisposedException) |
||
1 | office | 512 | { |
513 | } |
||
44 | office | 514 | catch (Exception exception) |
1 | office | 515 | { |
61 | office | 516 | Log.Warning(exception, $"Heartbeat for server {_server} has failed due to {exception.Message}"); |
1 | office | 517 | } |
518 | } |
||
519 | |||
61 | office | 520 | private async IAsyncEnumerable<GotifyApplication> RetrieveGotifyApplications([EnumeratorCancellation] CancellationToken cancellationToken) |
59 | office | 521 | { |
522 | var applicationsUriBuilder = new UriBuilder(_httpUri); |
||
523 | try |
||
524 | { |
||
525 | applicationsUriBuilder.Path = Path.Combine(applicationsUriBuilder.Path, "application"); |
||
526 | } |
||
527 | catch (ArgumentException exception) |
||
528 | { |
||
529 | Log.Error($"No application URL could be built for {_server.Url} due to {exception}"); |
||
61 | office | 530 | |
531 | yield break; |
||
59 | office | 532 | } |
533 | |||
61 | office | 534 | HttpResponseMessage applicationsResponse; |
59 | office | 535 | |
536 | try |
||
537 | { |
||
61 | office | 538 | applicationsResponse = await _httpClient.GetAsync(applicationsUriBuilder.Uri, cancellationToken); |
59 | office | 539 | } |
61 | office | 540 | catch (Exception exception) |
59 | office | 541 | { |
61 | office | 542 | Log.Error($"Could not retrieve applications: {exception.Message}"); |
59 | office | 543 | |
61 | office | 544 | yield break; |
59 | office | 545 | } |
546 | |||
61 | office | 547 | var applications = await applicationsResponse.Content.ReadAsStringAsync(); |
59 | office | 548 | |
44 | office | 549 | GotifyApplication[] gotifyApplications; |
550 | try |
||
551 | { |
||
552 | gotifyApplications = |
||
553 | JsonConvert.DeserializeObject<GotifyApplication[]>(applications); |
||
554 | } |
||
555 | catch (JsonSerializationException exception) |
||
556 | { |
||
61 | office | 557 | Log.Warning($"Could not deserialize the list of applications from the server: {exception}"); |
25 | office | 558 | |
61 | office | 559 | yield break; |
39 | office | 560 | } |
25 | office | 561 | |
39 | office | 562 | foreach (var application in gotifyApplications) |
563 | { |
||
61 | office | 564 | yield return application; |
565 | } |
||
566 | } |
||
567 | |||
568 | private async Task<Stream> RetrieveGotifyApplicationImage(int appId, CancellationToken cancellationToken) |
||
569 | { |
||
570 | await foreach (var application in RetrieveGotifyApplications(cancellationToken)) |
||
571 | { |
||
44 | office | 572 | if (application.Id != appId) continue; |
25 | office | 573 | |
61 | office | 574 | if (!Uri.TryCreate(Path.Combine($"{_httpUri}", $"{application.Image}"), UriKind.Absolute, out var applicationImageUri)) |
25 | office | 575 | { |
51 | office | 576 | Log.Warning("Could not build URL path to application icon"); |
39 | office | 577 | continue; |
578 | } |
||
25 | office | 579 | |
61 | office | 580 | HttpResponseMessage imageResponse; |
25 | office | 581 | |
61 | office | 582 | try |
583 | { |
||
584 | imageResponse = await _httpClient.GetAsync(applicationImageUri, cancellationToken); |
||
585 | } |
||
586 | catch (Exception exception) |
||
587 | { |
||
588 | Log.Error($"Could not retrieve application image: {exception.Message}"); |
||
589 | |||
590 | return new MemoryStream(); |
||
591 | } |
||
592 | |||
44 | office | 593 | var memoryStream = new MemoryStream(); |
25 | office | 594 | |
44 | office | 595 | await imageResponse.Content.CopyToAsync(memoryStream); |
596 | |||
61 | office | 597 | memoryStream.Position = 0L; |
598 | |||
44 | office | 599 | return memoryStream; |
25 | office | 600 | } |
601 | |||
61 | office | 602 | return new MemoryStream(); |
25 | office | 603 | } |
604 | |||
1 | office | 605 | #endregion |
606 | } |
||
607 | } |