Winify – Diff between revs 62 and 63

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