Winify – Diff between revs 61 and 62

Subversion Repositories:
Rev:
Only display areas with differencesIgnore whitespace
Rev 61 Rev 62
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  
-   132 var clone = image.Clone();
131   133  
132 _applicationImageCache.Add($"{message.AppId}", image.Clone(), 134 _applicationImageCache.Add($"{message.AppId}", clone,
133 new CacheItemPolicy 135 new CacheItemPolicy
134 { 136 {
135 SlidingExpiration = TimeSpan.FromHours(1) 137 SlidingExpiration = TimeSpan.FromHours(1)
136 }); 138 });
137   139  
138 GotifyNotification?.Invoke(this, 140 GotifyNotification?.Invoke(this,
139 new GotifyNotificationEventArgs(message, image)); 141 new GotifyNotificationEventArgs(message, image));
140 } 142 }
141   143  
142 Log.Debug($"Notification message received: {message.Message}"); 144 Log.Debug($"Notification message received: {message.Message}");
143   145  
144 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken }); 146 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
145   147  
146 _tplWebSocketsBufferBlockTransformLink = _webSocketMessageBufferBlock.LinkTo(webSocketTransformBlock, 148 _tplWebSocketsBufferBlockTransformLink = _webSocketMessageBufferBlock.LinkTo(webSocketTransformBlock,
147 new DataflowLinkOptions { PropagateCompletion = true }); 149 new DataflowLinkOptions { PropagateCompletion = true });
148 _tplWebSocketsTransformActionLink = webSocketTransformBlock.LinkTo(webSocketActionBlock, 150 _tplWebSocketsTransformActionLink = webSocketTransformBlock.LinkTo(webSocketActionBlock,
149 new DataflowLinkOptions { PropagateCompletion = true }, message => message != null); 151 new DataflowLinkOptions { PropagateCompletion = true }, message => message != null);
150 _tplWebSocketsTransformActionNullLink = webSocketTransformBlock.LinkTo(DataflowBlock.NullTarget<GotifyMessage>(), 152 _tplWebSocketsTransformActionNullLink = webSocketTransformBlock.LinkTo(DataflowBlock.NullTarget<GotifyMessage>(),
151 new DataflowLinkOptions() { PropagateCompletion = true }); 153 new DataflowLinkOptions() { PropagateCompletion = true });
152 } 154 }
153   155  
154 public GotifyConnection(Server server, Configuration.Configuration configuration) : this() 156 public GotifyConnection(Server server, Configuration.Configuration configuration) : this()
155 { 157 {
156 _server = server; 158 _server = server;
157 _configuration = configuration; 159 _configuration = configuration;
158   160  
159 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; 161 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
160 var httpClientHandler = new HttpClientHandler 162 var httpClientHandler = new HttpClientHandler
161 { 163 {
162 // mono does not implement this 164 // mono does not implement this
163 //SslProtocols = SslProtocols.Tls12 165 //SslProtocols = SslProtocols.Tls12
164 }; 166 };
165   167  
166 _httpClient = new HttpClient(httpClientHandler); 168 _httpClient = new HttpClient(httpClientHandler);
167 if (_configuration.IgnoreSelfSignedCertificates) 169 if (_configuration.IgnoreSelfSignedCertificates)
168 httpClientHandler.ServerCertificateCustomValidationCallback = 170 httpClientHandler.ServerCertificateCustomValidationCallback =
169 (httpRequestMessage, cert, cetChain, policyErrors) => true; 171 (httpRequestMessage, cert, cetChain, policyErrors) => true;
170   172  
171 if (_configuration.Proxy.Enable) 173 if (_configuration.Proxy.Enable)
172 httpClientHandler.Proxy = new WebProxy(_configuration.Proxy.Url, false, new string[] { }, 174 httpClientHandler.Proxy = new WebProxy(_configuration.Proxy.Url, false, new string[] { },
173 new NetworkCredential(_configuration.Proxy.Username, _configuration.Proxy.Password)); 175 new NetworkCredential(_configuration.Proxy.Username, _configuration.Proxy.Password));
174   176  
175 _httpClient = new HttpClient(httpClientHandler); 177 _httpClient = new HttpClient(httpClientHandler);
176 if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password)) 178 if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password))
177 _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", 179 _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
178 Convert.ToBase64String(Encoding.Default.GetBytes($"{_server.Username}:{_server.Password}"))); 180 Convert.ToBase64String(Encoding.Default.GetBytes($"{_server.Username}:{_server.Password}")));
179   181  
180 if (!Uri.TryCreate(_server.Url, UriKind.Absolute, out _httpUri)) 182 if (!Uri.TryCreate(_server.Url, UriKind.Absolute, out _httpUri))
181 { 183 {
182 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}");
183 return; 185 return;
184 } 186 }
185   187  
186 // Build the web sockets URI. 188 // Build the web sockets URI.
187 var webSocketsUriBuilder = new UriBuilder(_httpUri); 189 var webSocketsUriBuilder = new UriBuilder(_httpUri);
188 switch (webSocketsUriBuilder.Scheme.ToUpperInvariant()) 190 switch (webSocketsUriBuilder.Scheme.ToUpperInvariant())
189 { 191 {
190 case "HTTP": 192 case "HTTP":
191 webSocketsUriBuilder.Scheme = "ws"; 193 webSocketsUriBuilder.Scheme = "ws";
192 break; 194 break;
193 case "HTTPS": 195 case "HTTPS":
194 webSocketsUriBuilder.Scheme = "wss"; 196 webSocketsUriBuilder.Scheme = "wss";
195 break; 197 break;
196 } 198 }
197   199  
198 try 200 try
199 { 201 {
200 webSocketsUriBuilder.Path = Path.Combine(webSocketsUriBuilder.Path, "stream"); 202 webSocketsUriBuilder.Path = Path.Combine(webSocketsUriBuilder.Path, "stream");
201 } 203 }
202 catch (ArgumentException exception) 204 catch (ArgumentException exception)
203 { 205 {
204 Log.Error( 206 Log.Error(
205 $"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}");
206 } 208 }
207   209  
208 _webSocketsUri = webSocketsUriBuilder.Uri; 210 _webSocketsUri = webSocketsUriBuilder.Uri;
209 } 211 }
210   212  
211 public void Dispose() 213 public void Dispose()
212 { 214 {
213 if (_cancellationTokenSource != null) 215 if (_cancellationTokenSource != null)
214 { 216 {
215 _cancellationTokenSource.Dispose(); 217 _cancellationTokenSource.Dispose();
216 _cancellationTokenSource = null; 218 _cancellationTokenSource = null;
217 } 219 }
218   220  
219 if (_tplWebSocketsBufferBlockTransformLink != null) 221 if (_tplWebSocketsBufferBlockTransformLink != null)
220 { 222 {
221 _tplWebSocketsBufferBlockTransformLink.Dispose(); 223 _tplWebSocketsBufferBlockTransformLink.Dispose();
222 _tplWebSocketsBufferBlockTransformLink = null; 224 _tplWebSocketsBufferBlockTransformLink = null;
223 } 225 }
224   226  
225 if (_tplWebSocketsTransformActionLink != null) 227 if (_tplWebSocketsTransformActionLink != null)
226 { 228 {
227 _tplWebSocketsTransformActionLink.Dispose(); 229 _tplWebSocketsTransformActionLink.Dispose();
228 _tplWebSocketsTransformActionLink = null; 230 _tplWebSocketsTransformActionLink = null;
229 } 231 }
230   232  
231 if (_tplWebSocketsTransformActionNullLink != null) 233 if (_tplWebSocketsTransformActionNullLink != null)
232 { 234 {
233 _tplWebSocketsTransformActionNullLink.Dispose(); 235 _tplWebSocketsTransformActionNullLink.Dispose();
234 _tplWebSocketsTransformActionNullLink = null; 236 _tplWebSocketsTransformActionNullLink = null;
235 } 237 }
236   238  
237 if (_tplRetrievePastMessagesLink != null) 239 if (_tplRetrievePastMessagesLink != null)
238 { 240 {
239 _tplRetrievePastMessagesLink.Dispose(); 241 _tplRetrievePastMessagesLink.Dispose();
240 _tplRetrievePastMessagesLink = null; 242 _tplRetrievePastMessagesLink = null;
241 } 243 }
242   244  
243 if (_webSocketSharp != null) 245 if (_webSocketSharp != null)
244 { 246 {
245 _webSocketSharp.Close(); 247 _webSocketSharp.Close();
246 _webSocketSharp = null; 248 _webSocketSharp = null;
247 } 249 }
248   250  
249 if (_httpClient != null) 251 if (_httpClient != null)
250 { 252 {
251 _httpClient.Dispose(); 253 _httpClient.Dispose();
252 _httpClient = null; 254 _httpClient = null;
253 } 255 }
254 } 256 }
255   257  
256 #endregion 258 #endregion
257   259  
258 #region Public Methods 260 #region Public Methods
259   261  
260 public void Start() 262 public void Start()
261 { 263 {
262 if (_webSocketsUri == null || _httpUri == null) 264 if (_webSocketsUri == null || _httpUri == null)
263 { 265 {
264 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");
265 return; 267 return;
266 } 268 }
267   269  
268 _cancellationTokenSource = new CancellationTokenSource(); 270 _cancellationTokenSource = new CancellationTokenSource();
269 _cancellationToken = _cancellationTokenSource.Token; 271 _cancellationToken = _cancellationTokenSource.Token;
270   272  
271 Connect(); 273 Connect();
272   274  
273 if (_configuration.RetrievePastNotificationHours != 0) 275 if (_configuration.RetrievePastNotificationHours != 0)
274 { 276 {
275 _initTask = RetrievePastMessages(_cancellationToken); 277 _initTask = RetrievePastMessages(_cancellationToken);
276 } 278 }
277   279  
278 _runTask = HeartBeat(_cancellationToken); 280 _runTask = HeartBeat(_cancellationToken);
279 } 281 }
280   282  
281 private void Connect() 283 private void Connect()
282 { 284 {
283 _webSocketSharp = new WebSocket(_webSocketsUri.AbsoluteUri); 285 _webSocketSharp = new WebSocket(_webSocketsUri.AbsoluteUri);
284 _webSocketSharp.EmitOnPing = true; 286 _webSocketSharp.EmitOnPing = true;
285 _webSocketSharp.WaitTime = TimeSpan.FromMinutes(1); 287 _webSocketSharp.WaitTime = TimeSpan.FromMinutes(1);
286 _webSocketSharp.SslConfiguration = new ClientSslConfiguration(_webSocketsUri.Host, 288 _webSocketSharp.SslConfiguration = new ClientSslConfiguration(_webSocketsUri.Host,
287 new X509CertificateCollection(new X509Certificate[] { }), SslProtocols.Tls12, false); 289 new X509CertificateCollection(new X509Certificate[] { }), SslProtocols.Tls12, false);
288 if (_configuration.Proxy.Enable) 290 if (_configuration.Proxy.Enable)
289 _webSocketSharp.SetProxy(_configuration.Proxy.Url, _configuration.Proxy.Username, 291 _webSocketSharp.SetProxy(_configuration.Proxy.Url, _configuration.Proxy.Username,
290 _configuration.Proxy.Password); 292 _configuration.Proxy.Password);
291   293  
292 if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password)) 294 if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password))
293 _webSocketSharp.SetCredentials(_server.Username, _server.Password, true); 295 _webSocketSharp.SetCredentials(_server.Username, _server.Password, true);
294   296  
295 if (_configuration.IgnoreSelfSignedCertificates) 297 if (_configuration.IgnoreSelfSignedCertificates)
296 _webSocketSharp.SslConfiguration.ServerCertificateValidationCallback += 298 _webSocketSharp.SslConfiguration.ServerCertificateValidationCallback +=
297 (sender, certificate, chain, errors) => true; 299 (sender, certificate, chain, errors) => true;
298   300  
299 _webSocketSharp.Log.Output = (logData, s) => 301 _webSocketSharp.Log.Output = (logData, s) =>
300 { 302 {
301 Log.Information($"WebSockets low level logging reported: {logData.Message}"); 303 Log.Information($"WebSockets low level logging reported: {logData.Message}");
302 }; 304 };
303   305  
304 _webSocketSharp.OnMessage += WebSocketSharp_OnMessage; 306 _webSocketSharp.OnMessage += WebSocketSharp_OnMessage;
305 _webSocketSharp.OnError += WebSocketSharp_OnError; 307 _webSocketSharp.OnError += WebSocketSharp_OnError;
306 _webSocketSharp.OnOpen += WebSocketSharp_OnOpen; 308 _webSocketSharp.OnOpen += WebSocketSharp_OnOpen;
307 _webSocketSharp.OnClose += WebSocketSharp_OnClose; 309 _webSocketSharp.OnClose += WebSocketSharp_OnClose;
308   310  
309 _webSocketSharp.ConnectAsync(); 311 _webSocketSharp.ConnectAsync();
310 } 312 }
311   313  
312 private void WebSocketSharp_OnClose(object sender, CloseEventArgs e) 314 private void WebSocketSharp_OnClose(object sender, CloseEventArgs e)
313 { 315 {
314 Log.Information( 316 Log.Information(
315 $"WebSockets connection to server {_webSocketsUri.AbsoluteUri} closed with reason {e.Reason}"); 317 $"WebSockets connection to server {_webSocketsUri.AbsoluteUri} closed with reason {e.Reason}");
316 } 318 }
317   319  
318 private void WebSocketSharp_OnOpen(object sender, EventArgs e) 320 private void WebSocketSharp_OnOpen(object sender, EventArgs e)
319 { 321 {
320 Log.Information($"WebSockets connection to server {_webSocketsUri.AbsoluteUri} is now open"); 322 Log.Information($"WebSockets connection to server {_webSocketsUri.AbsoluteUri} is now open");
321   323  
322 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken); 324 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken);
323 } 325 }
324   326  
325 private void OnUnresponsiveServer() 327 private void OnUnresponsiveServer()
326 { 328 {
327 Log.Warning($"Server {_server} has not responded in a long while..."); 329 Log.Warning($"Server {_server} has not responded in a long while...");
328 } 330 }
329   331  
330 private async void WebSocketSharp_OnError(object sender, ErrorEventArgs e) 332 private async void WebSocketSharp_OnError(object sender, ErrorEventArgs e)
331 { 333 {
332 Log.Error( 334 Log.Error(
333 $"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}",
334 e.Exception); 336 e.Exception);
335   337  
336 if (_cancellationToken.IsCancellationRequested) 338 if (_cancellationToken.IsCancellationRequested)
337 { 339 {
338 Stop(); 340 Stop();
339 return; 341 return;
340 } 342 }
341   343  
342 await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken); 344 await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken);
343   345  
344 Log.Information($"Reconnecting to websocket server {_webSocketsUri.AbsoluteUri}"); 346 Log.Information($"Reconnecting to websocket server {_webSocketsUri.AbsoluteUri}");
345   347  
346 Connect(); 348 Connect();
347 } 349 }
348   350  
349 private async void WebSocketSharp_OnMessage(object sender, MessageEventArgs e) 351 private async void WebSocketSharp_OnMessage(object sender, MessageEventArgs e)
350 { 352 {
351 if (e.IsPing) 353 if (e.IsPing)
352 { 354 {
353 Log.Information($"Server {_server} sent PING message"); 355 Log.Information($"Server {_server} sent PING message");
354   356  
355 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken); 357 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken);
356   358  
357 return; 359 return;
358 } 360 }
359   361  
360 await _webSocketMessageBufferBlock.SendAsync(e.RawData, _cancellationToken); 362 await _webSocketMessageBufferBlock.SendAsync(e.RawData, _cancellationToken);
361 } 363 }
362   364  
363 public void Stop() 365 public void Stop()
364 { 366 {
365 if (_cancellationTokenSource == null) return; 367 if (_cancellationTokenSource == null) return;
366   368  
367 _cancellationTokenSource.Cancel(); 369 _cancellationTokenSource.Cancel();
368 } 370 }
369   371  
370 #endregion 372 #endregion
371   373  
372 #region Private Methods 374 #region Private Methods
373   375  
374 private async Task RetrievePastMessages(CancellationToken cancellationToken) 376 private async Task RetrievePastMessages(CancellationToken cancellationToken)
375 { 377 {
376 var messageUriBuilder = new UriBuilder(_httpUri); 378 var messageUriBuilder = new UriBuilder(_httpUri);
377   379  
378 var gotifyApplicationBufferBlock = new BufferBlock<GotifyApplication>(new DataflowBlockOptions { CancellationToken = cancellationToken }); 380 var gotifyApplicationBufferBlock = new BufferBlock<GotifyApplication>(new DataflowBlockOptions { CancellationToken = cancellationToken });
379 var gotifyApplicationActionBlock = new ActionBlock<GotifyApplication>(async application => 381 var gotifyApplicationActionBlock = new ActionBlock<GotifyApplication>(async application =>
380 { 382 {
381 try 383 try
382 { 384 {
383 messageUriBuilder.Path = Path.Combine(messageUriBuilder.Path, "application", $"{application.Id}", 385 messageUriBuilder.Path = Path.Combine(messageUriBuilder.Path, "application", $"{application.Id}",
384 "message"); 386 "message");
385 } 387 }
386 catch (ArgumentException exception) 388 catch (ArgumentException exception)
387 { 389 {
388 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}");
389   391  
390 return; 392 return;
391 } 393 }
392   394  
393 HttpResponseMessage messagesResponse; 395 HttpResponseMessage messagesResponse;
394 try 396 try
395 { 397 {
396 messagesResponse = await _httpClient.GetAsync(messageUriBuilder.Uri, cancellationToken); 398 messagesResponse = await _httpClient.GetAsync(messageUriBuilder.Uri, cancellationToken);
397 } 399 }
398 catch (Exception exception) 400 catch (Exception exception)
399 { 401 {
400 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}");
401   403  
402 return; 404 return;
403 } 405 }
404   406  
405   407  
406 var messages = await messagesResponse.Content.ReadAsStringAsync(); 408 var messages = await messagesResponse.Content.ReadAsStringAsync();
407   409  
408 GotifyMessageQuery gotifyMessageQuery; 410 GotifyMessageQuery gotifyMessageQuery;
409 try 411 try
410 { 412 {
411 gotifyMessageQuery = 413 gotifyMessageQuery =
412 JsonConvert.DeserializeObject<GotifyMessageQuery>(messages); 414 JsonConvert.DeserializeObject<GotifyMessageQuery>(messages);
413 } 415 }
414 catch (JsonSerializationException exception) 416 catch (JsonSerializationException exception)
415 { 417 {
416 Log.Warning($"Could not deserialize the message response: {exception.Message}"); 418 Log.Warning($"Could not deserialize the message response: {exception.Message}");
417   419  
418 return; 420 return;
419 } 421 }
420   422  
421 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)))
422 { 424 {
423 message.Server = _server; 425 message.Server = _server;
424   426  
425 var cachedImage = _applicationImageCache.Get($"{message.AppId}"); 427 var cachedImage = _applicationImageCache.Get($"{message.AppId}");
426 if (cachedImage is Image applicationImage) 428 if (cachedImage is Image applicationImage)
427 { 429 {
428 GotifyNotification?.Invoke(this, 430 GotifyNotification?.Invoke(this,
429 new GotifyNotificationEventArgs(message, applicationImage)); 431 new GotifyNotificationEventArgs(message, applicationImage));
430 return; 432 return;
431 } 433 }
432   434  
433 using var imageStream = await RetrieveGotifyApplicationImage(message.AppId, _cancellationToken); 435 using var imageStream = await RetrieveGotifyApplicationImage(message.AppId, _cancellationToken);
434 if (imageStream == null || imageStream.Length == 0) 436 if (imageStream == null || imageStream.Length == 0)
435 { 437 {
436 Log.Warning("Could not find any application image for notification"); 438 Log.Warning("Could not find any application image for notification");
437 continue; 439 continue;
438 } 440 }
439   441  
440 var image = Image.FromStream(imageStream); 442 var image = Image.FromStream(imageStream);
-   443  
-   444 var clone = image.Clone();
441   445  
442 _applicationImageCache.Add($"{message.AppId}", image.Clone(), 446 _applicationImageCache.Add($"{message.AppId}", clone,
443 new CacheItemPolicy 447 new CacheItemPolicy
444 { 448 {
445 SlidingExpiration = TimeSpan.FromHours(1) 449 SlidingExpiration = TimeSpan.FromHours(1)
446 }); 450 });
447   451  
448 GotifyNotification?.Invoke(this, 452 GotifyNotification?.Invoke(this,
449 new GotifyNotificationEventArgs(message, image)); 453 new GotifyNotificationEventArgs(message, image));
450 } 454 }
451   455  
452 }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken }); 456 }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken });
453   457  
454 gotifyApplicationBufferBlock.LinkTo(gotifyApplicationActionBlock, 458 gotifyApplicationBufferBlock.LinkTo(gotifyApplicationActionBlock,
455 new DataflowLinkOptions { PropagateCompletion = true }); 459 new DataflowLinkOptions { PropagateCompletion = true });
456   460  
457 await foreach (var application in RetrieveGotifyApplications(cancellationToken)) 461 await foreach (var application in RetrieveGotifyApplications(cancellationToken))
458 { 462 {
459 await gotifyApplicationBufferBlock.SendAsync(application, cancellationToken); 463 await gotifyApplicationBufferBlock.SendAsync(application, cancellationToken);
460 } 464 }
461   465  
462 gotifyApplicationBufferBlock.Complete(); 466 gotifyApplicationBufferBlock.Complete();
463 await gotifyApplicationActionBlock.Completion; 467 await gotifyApplicationActionBlock.Completion;
464   468  
465 } 469 }
466   470  
467 private async Task HeartBeat(CancellationToken cancellationToken) 471 private async Task HeartBeat(CancellationToken cancellationToken)
468 { 472 {
469 try 473 try
470 { 474 {
471 do 475 do
472 { 476 {
473 await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); 477 await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
474   478  
475 _webSocketsClientPingStopWatch.Restart(); 479 _webSocketsClientPingStopWatch.Restart();
476 if (!_webSocketSharp.Ping()) 480 if (!_webSocketSharp.Ping())
477 { 481 {
478 Log.Warning($"Server {_server} did not respond to PING message."); 482 Log.Warning($"Server {_server} did not respond to PING message.");
479 continue; 483 continue;
480 } 484 }
481   485  
482 var delta = _webSocketsClientPingStopWatch.ElapsedMilliseconds; 486 var delta = _webSocketsClientPingStopWatch.ElapsedMilliseconds;
483   487  
484 Log.Information($"PING response latency for {_server} is {delta}ms"); 488 Log.Information($"PING response latency for {_server} is {delta}ms");
485   489  
486 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken); 490 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken);
487 } while (!cancellationToken.IsCancellationRequested); 491 } while (!cancellationToken.IsCancellationRequested);
488 } 492 }
489 catch (Exception exception) when (exception is OperationCanceledException || 493 catch (Exception exception) when (exception is OperationCanceledException ||
490 exception is ObjectDisposedException) 494 exception is ObjectDisposedException)
491 { 495 {
492 } 496 }
493 catch (Exception exception) 497 catch (Exception exception)
494 { 498 {
495 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}");
496 } 500 }
497 } 501 }
498   502  
499 private async IAsyncEnumerable<GotifyApplication> RetrieveGotifyApplications([EnumeratorCancellation] CancellationToken cancellationToken) 503 private async IAsyncEnumerable<GotifyApplication> RetrieveGotifyApplications([EnumeratorCancellation] CancellationToken cancellationToken)
500 { 504 {
501 var applicationsUriBuilder = new UriBuilder(_httpUri); 505 var applicationsUriBuilder = new UriBuilder(_httpUri);
502 try 506 try
503 { 507 {
504 applicationsUriBuilder.Path = Path.Combine(applicationsUriBuilder.Path, "application"); 508 applicationsUriBuilder.Path = Path.Combine(applicationsUriBuilder.Path, "application");
505 } 509 }
506 catch (ArgumentException exception) 510 catch (ArgumentException exception)
507 { 511 {
508 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}");
509   513  
510 yield break; 514 yield break;
511 } 515 }
512   516  
513 HttpResponseMessage applicationsResponse; 517 HttpResponseMessage applicationsResponse;
514   518  
515 try 519 try
516 { 520 {
517 applicationsResponse = await _httpClient.GetAsync(applicationsUriBuilder.Uri, cancellationToken); 521 applicationsResponse = await _httpClient.GetAsync(applicationsUriBuilder.Uri, cancellationToken);
518 } 522 }
519 catch (Exception exception) 523 catch (Exception exception)
520 { 524 {
521 Log.Error($"Could not retrieve applications: {exception.Message}"); 525 Log.Error($"Could not retrieve applications: {exception.Message}");
522   526  
523 yield break; 527 yield break;
524 } 528 }
525   529  
526 var applications = await applicationsResponse.Content.ReadAsStringAsync(); 530 var applications = await applicationsResponse.Content.ReadAsStringAsync();
527   531  
528 GotifyApplication[] gotifyApplications; 532 GotifyApplication[] gotifyApplications;
529 try 533 try
530 { 534 {
531 gotifyApplications = 535 gotifyApplications =
532 JsonConvert.DeserializeObject<GotifyApplication[]>(applications); 536 JsonConvert.DeserializeObject<GotifyApplication[]>(applications);
533 } 537 }
534 catch (JsonSerializationException exception) 538 catch (JsonSerializationException exception)
535 { 539 {
536 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}");
537   541  
538 yield break; 542 yield break;
539 } 543 }
540   544  
541 foreach (var application in gotifyApplications) 545 foreach (var application in gotifyApplications)
542 { 546 {
543 yield return application; 547 yield return application;
544 } 548 }
545 } 549 }
546   550  
547 private async Task<Stream> RetrieveGotifyApplicationImage(int appId, CancellationToken cancellationToken) 551 private async Task<Stream> RetrieveGotifyApplicationImage(int appId, CancellationToken cancellationToken)
548 { 552 {
549 await foreach (var application in RetrieveGotifyApplications(cancellationToken)) 553 await foreach (var application in RetrieveGotifyApplications(cancellationToken))
550 { 554 {
551 if (application.Id != appId) continue; 555 if (application.Id != appId) continue;
552   556  
553 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))
554 { 558 {
555 Log.Warning("Could not build URL path to application icon"); 559 Log.Warning("Could not build URL path to application icon");
556 continue; 560 continue;
557 } 561 }
558   562  
559 HttpResponseMessage imageResponse; 563 HttpResponseMessage imageResponse;
560   564  
561 try 565 try
562 { 566 {
563 imageResponse = await _httpClient.GetAsync(applicationImageUri, cancellationToken); 567 imageResponse = await _httpClient.GetAsync(applicationImageUri, cancellationToken);
564 } 568 }
565 catch (Exception exception) 569 catch (Exception exception)
566 { 570 {
567 Log.Error($"Could not retrieve application image: {exception.Message}"); 571 Log.Error($"Could not retrieve application image: {exception.Message}");
568   572  
569 return new MemoryStream(); 573 return new MemoryStream();
570 } 574 }
571   575  
572 var memoryStream = new MemoryStream(); 576 var memoryStream = new MemoryStream();
573   577  
574 await imageResponse.Content.CopyToAsync(memoryStream); 578 await imageResponse.Content.CopyToAsync(memoryStream);
575   579  
576 memoryStream.Position = 0L; 580 memoryStream.Position = 0L;
577   581  
578 return memoryStream; 582 return memoryStream;
579 } 583 }
580   584  
581 return new MemoryStream(); 585 return new MemoryStream();
582 } 586 }
583   587  
584 #endregion 588 #endregion
585 } 589 }
586 } 590 }
587   591  
588
Generated by GNU Enscript 1.6.5.90.
592
Generated by GNU Enscript 1.6.5.90.
589   593  
590   594  
591   595