Skip to content

Commit 46aea51

Browse files
committed
Add lock around in-memory hits collection
Closes #1053
1 parent ae0b343 commit 46aea51

4 files changed

Lines changed: 111 additions & 7 deletions

File tree

OpenBullet2.Native/ViewModels/MultiRunJobViewerViewModel.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,11 +342,12 @@ private void UpdateBots()
342342

343343
private void UpdateHitsCollection()
344344
{
345+
var hitsSnapshot = MultiRunJob.GetHitsSnapshot();
345346
var hits = HitsFilter switch
346347
{
347-
HitsFilter.Hits => MultiRunJob.Hits.Where(h => h.Type == "SUCCESS"),
348-
HitsFilter.ToCheck => MultiRunJob.Hits.Where(h => h.Type == "NONE"),
349-
HitsFilter.Custom => MultiRunJob.Hits.Where(h => h.Type != "SUCCESS" && h.Type != "NONE"),
348+
HitsFilter.Hits => hitsSnapshot.Where(h => h.Type == "SUCCESS"),
349+
HitsFilter.ToCheck => hitsSnapshot.Where(h => h.Type == "NONE"),
350+
HitsFilter.Custom => hitsSnapshot.Where(h => h.Type != "SUCCESS" && h.Type != "NONE"),
350351
_ => throw new NotImplementedException()
351352
};
352353

OpenBullet2.Web/Controllers/JobController.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ public ActionResult<MrjHitLogDto> GetHitLog(int jobId, string hitId)
544544
$"The job with id {jobId} is not a multi run job");
545545
}
546546

547-
var hit = mrJob.Hits.Find(h => h.Id == hitId) ?? throw new EntryNotFoundException(ErrorCode.HitNotFound,
547+
var hit = mrJob.FindHit(hitId) ?? throw new EntryNotFoundException(ErrorCode.HitNotFound,
548548
hitId, nameof(MultiRunJob.Hits));
549549
return new MrjHitLogDto { Log = hit.BotLogger?.Entries.ToList() };
550550
}
@@ -968,6 +968,8 @@ private async Task<MultiRunJobDto> MapMultiRunJobDto(MultiRunJob job, Cancellati
968968
_ => throw new NotImplementedException()
969969
};
970970

971+
var hits = job.GetHitsSnapshot();
972+
971973
return new MultiRunJobDto
972974
{
973975
Id = job.Id,
@@ -1018,7 +1020,7 @@ job.Config is not null
10181020
Elapsed = job.Elapsed,
10191021
Remaining = job.Remaining,
10201022
Progress = job.Progress < 0 ? 0 : job.Progress,
1021-
Hits = job.Hits.Select(h => new MrjHitDto
1023+
Hits = hits.Select(h => new MrjHitDto
10221024
{
10231025
Id = h.Id,
10241026
Date = h.Date,

RuriLib.Tests/Models/Jobs/MultiRunJobTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using RuriLib.Models.Jobs;
44
using RuriLib.Models.Jobs.StartConditions;
55
using System;
6+
using System.Collections.Generic;
67
using System.IO;
78
using System.Reflection;
89
using System.Runtime.CompilerServices;
@@ -159,6 +160,54 @@ public async Task Start_AfterCompletedRun_RestartsFromBeginning()
159160
Assert.Equal(0, job.Skip);
160161
}
161162

163+
[Fact]
164+
public async Task GetHitsSnapshot_DuringConcurrentWrites_DoesNotThrow()
165+
{
166+
var job = CreateJob();
167+
var hits = job.Hits;
168+
var hitsLock = typeof(MultiRunJob)
169+
.GetField("hitsLock", BindingFlags.Instance | BindingFlags.NonPublic)!
170+
.GetValue(job)!;
171+
using var cts = new CancellationTokenSource();
172+
173+
var writer = Task.Run(async () =>
174+
{
175+
var index = 0;
176+
177+
while (!cts.Token.IsCancellationRequested)
178+
{
179+
lock (hitsLock)
180+
{
181+
hits.Add(CreateHit(index++));
182+
}
183+
184+
await Task.Yield();
185+
}
186+
}, TestCancellationToken);
187+
188+
for (var i = 0; i < 200; i++)
189+
{
190+
var exception = Record.Exception(() => job.GetHitsSnapshot());
191+
Assert.Null(exception);
192+
await Task.Yield();
193+
}
194+
195+
await cts.CancelAsync();
196+
await writer.WaitAsync(TestCancellationToken);
197+
}
198+
199+
[Fact]
200+
public void FindHit_ReturnsMatchingHit()
201+
{
202+
var job = CreateJob();
203+
var hit = CreateHit(1);
204+
job.Hits.Add(hit);
205+
206+
var found = job.FindHit(hit.Id);
207+
208+
Assert.Same(hit, found);
209+
}
210+
162211
private static MultiRunJob CreateJob()
163212
=> new(CreateSettingsService(), CreatePluginRepository());
164213

@@ -179,6 +228,27 @@ private static async Task WaitUntilIdleAsync(MultiRunJob job)
179228
Assert.Equal(JobStatus.Idle, job.Status);
180229
}
181230

231+
private static global::RuriLib.Models.Hits.Hit CreateHit(int index)
232+
{
233+
const string wordlistTypeName = "default";
234+
235+
return new global::RuriLib.Models.Hits.Hit
236+
{
237+
Data = new DataLine(
238+
$"user{index}:pass{index}",
239+
new global::RuriLib.Models.Environment.WordlistType { Name = wordlistTypeName }),
240+
CapturedData = new Dictionary<string, object> { ["token"] = $"abc{index}" },
241+
Date = DateTime.UtcNow,
242+
Type = "SUCCESS",
243+
Config = new Config
244+
{
245+
Id = $"cfg-{index}",
246+
Metadata = new ConfigMetadata { Name = "Config", Category = "Cat" }
247+
},
248+
DataPool = new TestDataPool([$"user{index}:pass{index}"], wordlistTypeName)
249+
};
250+
}
251+
182252
private sealed class TestDataPool : DataPool
183253
{
184254
public TestDataPool()

RuriLib/Models/Jobs/MultiRunJob.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public class MultiRunJob : Job
9292

9393
// Private fields
9494
private readonly string[] badStatuses = ["FAIL", "RETRY", "BAN", "ERROR", "INVALID"];
95+
private readonly object hitsLock = new();
9596
private Parallelizer<MultiRunInput, CheckResult>? parallelizer;
9697
private ProxyPool? proxyPool;
9798
private Timer? tickTimer;
@@ -108,6 +109,28 @@ public class MultiRunJob : Job
108109
/// <summary>Gets the hits collected during the current run.</summary>
109110
public List<Hit> Hits { get; private set; } = [];
110111

112+
/// <summary>
113+
/// Gets a snapshot of the hits collected during the current run.
114+
/// </summary>
115+
public List<Hit> GetHitsSnapshot()
116+
{
117+
lock (hitsLock)
118+
{
119+
return [.. Hits];
120+
}
121+
}
122+
123+
/// <summary>
124+
/// Finds a hit by id in a thread-safe manner.
125+
/// </summary>
126+
public Hit? FindHit(string id)
127+
{
128+
lock (hitsLock)
129+
{
130+
return Hits.Find(h => h.Id == id);
131+
}
132+
}
133+
111134
// Events
112135
/// <summary>Raised when a worker task fails.</summary>
113136
public event EventHandler<ErrorDetails<MultiRunInput>>? OnTaskError;
@@ -1004,7 +1027,11 @@ private void ResetStats()
10041027
dataToCheck = 0;
10051028
dataInvalid = 0;
10061029
dataErrors = 0;
1007-
Hits = new();
1030+
1031+
lock (hitsLock)
1032+
{
1033+
Hits = [];
1034+
}
10081035
}
10091036

10101037
private void StatusChanged(object? sender, ParallelizerStatus status)
@@ -1075,7 +1102,11 @@ private async Task RegisterHit(CheckResult result)
10751102
};
10761103

10771104
// Add it to the local list of hits
1078-
Hits.Add(hit);
1105+
lock (hitsLock)
1106+
{
1107+
Hits.Add(hit);
1108+
}
1109+
10791110
OnHit?.Invoke(this, hit);
10801111

10811112
foreach (var hitOutput in HitOutputs)

0 commit comments

Comments
 (0)