Skip to content

Commit 9ac65d0

Browse files
committed
refactor: address high-leverage audit findings
- Fix concurrency bug in GameScanner by removing shared mutable state - Implement real-time search filtering in MainViewModel using ICollectionView - Optimize Polly retry logic by removing retries on HTTP 404
1 parent a22c584 commit 9ac65d0

3 files changed

Lines changed: 62 additions & 54 deletions

File tree

OpenTweak/App.xaml.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ private static void ConfigureServices(IServiceCollection services)
103103
// Resilience Policy
104104
var retryPolicy = HttpPolicyExtensions
105105
.HandleTransientHttpError()
106-
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound) // Handling PCGW weirdness
107106
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
108107

109108
// Register HTTP Client with Polly

OpenTweak/Services/GameScanner.cs

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,15 @@ namespace OpenTweak.Services;
1616
/// </summary>
1717
public class GameScanner : IGameScanner
1818
{
19-
private readonly List<Game> _detectedGames = new();
20-
2119
/// <summary>
2220
/// Scans all supported launchers and returns detected games.
2321
/// </summary>
2422
public async Task<List<Game>> ScanAllLaunchersAsync(CancellationToken cancellationToken = default)
2523
{
26-
_detectedGames.Clear();
27-
2824
if (cancellationToken.IsCancellationRequested)
29-
return _detectedGames;
25+
return new List<Game>();
3026

31-
var tasks = new List<Task>
27+
var tasks = new List<Task<List<Game>>>
3228
{
3329
Task.Run(() => ScanSteam(cancellationToken), cancellationToken),
3430
Task.Run(() => ScanEpicGames(cancellationToken), cancellationToken),
@@ -38,30 +34,30 @@ public async Task<List<Game>> ScanAllLaunchersAsync(CancellationToken cancellati
3834

3935
try
4036
{
41-
await Task.WhenAll(tasks);
37+
var results = await Task.WhenAll(tasks);
38+
return results.SelectMany(g => g).OrderBy(g => g.Name).ToList();
4239
}
4340
catch (OperationCanceledException)
4441
{
45-
// Ignore cancellation exceptions
42+
return new List<Game>();
4643
}
47-
48-
return _detectedGames.OrderBy(g => g.Name).ToList();
4944
}
5045

5146
#region Steam Scanner
5247

5348
/// <summary>
5449
/// Scans Steam libraries by parsing libraryfolders.vdf and appmanifest files.
5550
/// </summary>
56-
private void ScanSteam(CancellationToken cancellationToken)
51+
private List<Game> ScanSteam(CancellationToken cancellationToken)
5752
{
53+
var games = new List<Game>();
5854
try
5955
{
6056
var steamPaths = GetSteamLibraryPaths();
6157

6258
foreach (var libraryPath in steamPaths)
6359
{
64-
if (cancellationToken.IsCancellationRequested) return;
60+
if (cancellationToken.IsCancellationRequested) break;
6561

6662
var steamAppsPath = Path.Combine(libraryPath, "steamapps");
6763
if (!Directory.Exists(steamAppsPath)) continue;
@@ -70,15 +66,12 @@ private void ScanSteam(CancellationToken cancellationToken)
7066

7167
foreach (var manifest in manifestFiles)
7268
{
73-
if (cancellationToken.IsCancellationRequested) return;
69+
if (cancellationToken.IsCancellationRequested) break;
7470

7571
var game = ParseSteamManifest(manifest, steamAppsPath);
7672
if (game != null)
7773
{
78-
lock (_detectedGames)
79-
{
80-
_detectedGames.Add(game);
81-
}
74+
games.Add(game);
8275
}
8376
}
8477
}
@@ -87,6 +80,7 @@ private void ScanSteam(CancellationToken cancellationToken)
8780
{
8881
System.Diagnostics.Debug.WriteLine($"Steam scan error: {ex.Message}");
8982
}
83+
return games;
9084
}
9185

9286
private List<string> GetSteamLibraryPaths()
@@ -171,36 +165,35 @@ private List<string> GetSteamLibraryPaths()
171165
/// <summary>
172166
/// Scans Epic Games by parsing .item manifest files.
173167
/// </summary>
174-
private void ScanEpicGames(CancellationToken cancellationToken)
168+
private List<Game> ScanEpicGames(CancellationToken cancellationToken)
175169
{
170+
var games = new List<Game>();
176171
try
177172
{
178173
var manifestsPath = Path.Combine(
179174
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
180175
"Epic", "EpicGamesLauncher", "Data", "Manifests");
181176

182-
if (!Directory.Exists(manifestsPath)) return;
177+
if (!Directory.Exists(manifestsPath)) return games;
183178

184179
var itemFiles = Directory.GetFiles(manifestsPath, "*.item");
185180

186181
foreach (var itemFile in itemFiles)
187182
{
188-
if (cancellationToken.IsCancellationRequested) return;
183+
if (cancellationToken.IsCancellationRequested) break;
189184

190185
var game = ParseEpicManifest(itemFile);
191186
if (game != null)
192187
{
193-
lock (_detectedGames)
194-
{
195-
_detectedGames.Add(game);
196-
}
188+
games.Add(game);
197189
}
198190
}
199191
}
200192
catch (Exception ex)
201193
{
202194
System.Diagnostics.Debug.WriteLine($"Epic scan error: {ex.Message}");
203195
}
196+
return games;
204197
}
205198

206199
private Game? ParseEpicManifest(string itemPath)
@@ -246,8 +239,9 @@ private void ScanEpicGames(CancellationToken cancellationToken)
246239
/// <summary>
247240
/// Scans GOG Galaxy by reading registry keys.
248241
/// </summary>
249-
private void ScanGOG(CancellationToken cancellationToken)
242+
private List<Game> ScanGOG(CancellationToken cancellationToken)
250243
{
244+
var games = new List<Game>();
251245
try
252246
{
253247
// Check both 32-bit and 64-bit registry locations
@@ -259,14 +253,14 @@ private void ScanGOG(CancellationToken cancellationToken)
259253

260254
foreach (var regPath in registryPaths)
261255
{
262-
if (cancellationToken.IsCancellationRequested) return;
256+
if (cancellationToken.IsCancellationRequested) break;
263257

264258
using var gamesKey = Registry.LocalMachine.OpenSubKey(regPath);
265259
if (gamesKey == null) continue;
266260

267261
foreach (var gameIdStr in gamesKey.GetSubKeyNames())
268262
{
269-
if (cancellationToken.IsCancellationRequested) return;
263+
if (cancellationToken.IsCancellationRequested) break;
270264

271265
using var gameKey = gamesKey.OpenSubKey(gameIdStr);
272266
if (gameKey == null) continue;
@@ -281,24 +275,22 @@ private void ScanGOG(CancellationToken cancellationToken)
281275
if (!Directory.Exists(gamePath))
282276
continue;
283277

284-
lock (_detectedGames)
278+
games.Add(new Game
285279
{
286-
_detectedGames.Add(new Game
287-
{
288-
Name = gameName,
289-
InstallPath = gamePath,
290-
LauncherType = LauncherType.GOG,
291-
AppId = gameId ?? gameIdStr,
292-
PCGWTitle = gameName
293-
});
294-
}
280+
Name = gameName,
281+
InstallPath = gamePath,
282+
LauncherType = LauncherType.GOG,
283+
AppId = gameId ?? gameIdStr,
284+
PCGWTitle = gameName
285+
});
295286
}
296287
}
297288
}
298289
catch (Exception ex)
299290
{
300291
System.Diagnostics.Debug.WriteLine($"GOG scan error: {ex.Message}");
301292
}
293+
return games;
302294
}
303295

304296
#endregion
@@ -308,8 +300,9 @@ private void ScanGOG(CancellationToken cancellationToken)
308300
/// <summary>
309301
/// Scans Xbox/Microsoft Store games (basic implementation).
310302
/// </summary>
311-
private void ScanXbox(CancellationToken cancellationToken)
303+
private List<Game> ScanXbox(CancellationToken cancellationToken)
312304
{
305+
var games = new List<Game>();
313306
try
314307
{
315308
// Xbox Game Pass games are in WindowsApps or XboxGames folders
@@ -322,30 +315,27 @@ private void ScanXbox(CancellationToken cancellationToken)
322315

323316
foreach (var basePath in xboxPaths)
324317
{
325-
if (cancellationToken.IsCancellationRequested) return;
318+
if (cancellationToken.IsCancellationRequested) break;
326319
if (!Directory.Exists(basePath)) continue;
327320

328321
// XboxGames folder has a simpler structure
329322
if (basePath.EndsWith("XboxGames"))
330323
{
331324
foreach (var gameDir in Directory.GetDirectories(basePath))
332325
{
333-
if (cancellationToken.IsCancellationRequested) return;
326+
if (cancellationToken.IsCancellationRequested) break;
334327

335328
var contentDir = Path.Combine(gameDir, "Content");
336329
if (Directory.Exists(contentDir))
337330
{
338331
var gameName = Path.GetFileName(gameDir);
339-
lock (_detectedGames)
332+
games.Add(new Game
340333
{
341-
_detectedGames.Add(new Game
342-
{
343-
Name = gameName,
344-
InstallPath = contentDir,
345-
LauncherType = LauncherType.Xbox,
346-
PCGWTitle = gameName
347-
});
348-
}
334+
Name = gameName,
335+
InstallPath = contentDir,
336+
LauncherType = LauncherType.Xbox,
337+
PCGWTitle = gameName
338+
});
349339
}
350340
}
351341
}
@@ -355,6 +345,7 @@ private void ScanXbox(CancellationToken cancellationToken)
355345
{
356346
System.Diagnostics.Debug.WriteLine($"Xbox scan error: {ex.Message}");
357347
}
348+
return games;
358349
}
359350

360351
#endregion

OpenTweak/ViewModels/MainViewModel.cs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
using System.IO;
77
using System.Collections.ObjectModel;
8+
using System.ComponentModel;
9+
using System.Windows.Data;
810
using CommunityToolkit.Mvvm.ComponentModel;
911
using CommunityToolkit.Mvvm.Input;
1012
using Microsoft.Extensions.DependencyInjection;
@@ -32,6 +34,7 @@ public partial class MainViewModel : ObservableObject
3234
private readonly IDatabaseService _databaseService;
3335
private readonly IPCGWService _pcgwService;
3436
private readonly INotificationService _notificationService;
37+
private readonly ICollectionView _gamesView;
3538

3639
[ObservableProperty]
3740
private ObservableCollection<Game> _games = new();
@@ -79,10 +82,25 @@ public MainViewModel(
7982
_pcgwService = pcgwService;
8083
_notificationService = notificationService;
8184

85+
// Create the view for filtering and sorting
86+
_gamesView = CollectionViewSource.GetDefaultView(Games);
87+
_gamesView.Filter = FilterGames;
88+
8289
// Load cached games on startup
8390
LoadCachedGames();
8491
}
8592

93+
/// <summary>
94+
/// Filter predicate for the games collection.
95+
/// </summary>
96+
private bool FilterGames(object item)
97+
{
98+
if (item is not Game game) return false;
99+
if (string.IsNullOrWhiteSpace(SearchQuery)) return true;
100+
101+
return game.Name.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase);
102+
}
103+
86104
/// <summary>
87105
/// Loads games from the local database cache.
88106
/// </summary>
@@ -265,16 +283,16 @@ public void CloseSlideOver()
265283
/// </summary>
266284
partial void OnSearchQueryChanged(string value)
267285
{
268-
// In a real app, this would filter the observable collection
269-
// For now, just show how many match
286+
_gamesView.Refresh();
287+
270288
if (string.IsNullOrWhiteSpace(value))
271289
{
272290
StatusMessage = $"{Games.Count} games";
273291
}
274292
else
275293
{
276-
var matches = Games.Count(g => g.Name.Contains(value, StringComparison.OrdinalIgnoreCase));
277-
StatusMessage = $"{matches} games match '{value}'";
294+
var matchedCount = Games.Count(g => FilterGames(g));
295+
StatusMessage = $"{matchedCount} games match '{value}'";
278296
}
279297
}
280298
}

0 commit comments

Comments
 (0)