Winify – Diff between revs 83 and 84

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