Winify – Diff between revs 59 and 61

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