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