Winify – Diff between revs 59 and 61

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