Winify – Diff between revs 64 and 66

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