Skip to content

Commit 02d6baa

Browse files
committed
Fixed CI issue related to needs upgrade checks
1 parent a39b1ce commit 02d6baa

File tree

7 files changed

+238
-115
lines changed

7 files changed

+238
-115
lines changed

src/EventLogExpert.EventDbTool/DbToolCommand.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using EventLogExpert.Eventing.Helpers;
55
using EventLogExpert.Eventing.Providers;
66
using EventLogExpert.Eventing.Readers;
7-
using System.Text.RegularExpressions;
87

98
namespace EventLogExpert.EventDbTool;
109

@@ -14,21 +13,21 @@ public class DbToolCommand(ITraceLogger logger)
1413

1514
protected ITraceLogger Logger => logger;
1615

17-
protected static List<string> GetLocalProviderNames(string? filter)
16+
protected static List<string> GetLocalProviderNames(string? filter, ITraceLogger logger)
1817
{
1918
var providers = new List<string>(EventLogSession.GlobalSession.GetProviderNames().Distinct().OrderBy(name => name));
2019

21-
if (string.IsNullOrEmpty(filter)) { return providers; }
22-
23-
var regex = new Regex(filter, RegexOptions.IgnoreCase);
24-
providers = providers.Where(p => regex.IsMatch(p)).ToList();
20+
if (!RegexHelper.TryCreate(filter, logger, out var regex))
21+
{
22+
return [];
23+
}
2524

26-
return providers;
25+
return regex is null ? providers : providers.Where(p => regex.IsMatch(p)).ToList();
2726
}
2827

2928
protected IEnumerable<ProviderDetails> LoadLocalProviders(string? filter, IReadOnlySet<string>? skipProviderNames = null)
3029
{
31-
foreach (var providerName in GetLocalProviderNames(filter))
30+
foreach (var providerName in GetLocalProviderNames(filter, Logger))
3231
{
3332
// Skip BEFORE resolving so we don't pay the cost of loading metadata for providers we
3433
// are about to discard (e.g. when --skip-providers-in-file lists most local providers).

src/EventLogExpert.EventDbTool/DiffDatabaseCommand.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,13 @@ private void DiffDatabase(string firstSource, string secondSource, string newDb)
8181

8282
using var newDbContext = new EventProviderDbContext(newDb, false, Logger);
8383

84-
foreach (var details in ProviderSource.LoadProviders(secondSource, Logger))
85-
{
86-
if (firstProviderNames.Contains(details.ProviderName))
87-
{
88-
Logger.Info($"Skipping {details.ProviderName} because it is present in both sources.");
89-
continue;
90-
}
84+
// Pass firstProviderNames as the skip set so providers present in the first source are
85+
// never resolved from the second source's metadata path. This is especially important when
86+
// the second source is .evtx+MTA, where each provider triggers an expensive load.
87+
Logger.Info($"Skipping up to {firstProviderNames.Count} provider name(s) from the second source that also appear in the first source.");
9188

89+
foreach (var details in ProviderSource.LoadProviders(secondSource, Logger, filter: null, skipProviderNames: firstProviderNames))
90+
{
9291
Logger.Info($"Copying {details.ProviderName} because it is present in second source but not first.");
9392

9493
newDbContext.ProviderDetails.Add(new ProviderDetails

src/EventLogExpert.EventDbTool/MergeDatabaseCommand.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,28 @@ private void MergeDatabase(string source, string targetFile, bool overwriteProvi
104104
{
105105
Logger.Info($"Removing these providers from the target database...");
106106

107-
// Single round-trip to load just the rows we need to remove. Since these names came
108-
// from the same DB, exact (binary) matching here is correct.
109-
var toRemove = targetContext.ProviderDetails
110-
.Where(p => targetMatchingNames.Contains(p.ProviderName))
111-
.ToList();
107+
// Chunk the IN-clause to stay below SQLite's parameter limit (default 999). Without
108+
// chunking, an --overwrite of a large overlap could throw at runtime.
109+
const int maxInClauseParameters = 500;
110+
var removed = 0;
111+
112+
for (var offset = 0; offset < targetMatchingNames.Count; offset += maxInClauseParameters)
113+
{
114+
var chunk = targetMatchingNames
115+
.Skip(offset)
116+
.Take(maxInClauseParameters)
117+
.ToList();
118+
119+
var toRemove = targetContext.ProviderDetails
120+
.Where(p => chunk.Contains(p.ProviderName))
121+
.ToList();
122+
123+
targetContext.RemoveRange(toRemove);
124+
removed += toRemove.Count;
125+
}
112126

113-
targetContext.RemoveRange(toRemove);
114127
targetContext.SaveChanges();
115-
Logger.Info($"Removal of {toRemove.Count} provider row(s) completed.");
128+
Logger.Info($"Removal of {removed} provider row(s) completed.");
116129
}
117130
else
118131
{

src/EventLogExpert.EventDbTool/MtaProviderSource.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ internal static class MtaProviderSource
2222
public static IReadOnlyList<string> DiscoverProviderNames(
2323
string evtxPath,
2424
ITraceLogger logger,
25-
string? filter = null)
25+
string? filter = null) =>
26+
!RegexHelper.TryCreate(filter, logger, out var regex) ? [] : DiscoverProviderNamesCore(evtxPath, logger, regex);
27+
28+
private static IReadOnlyList<string> DiscoverProviderNamesCore(
29+
string evtxPath,
30+
ITraceLogger logger,
31+
Regex? regex)
2632
{
2733
if (!File.Exists(evtxPath))
2834
{
@@ -42,10 +48,10 @@ public static IReadOnlyList<string> DiscoverProviderNames(
4248
return [];
4349
}
4450

51+
// EventLogReader.TryGetEvents returns false (not an empty batch) when there are no more
52+
// events, so a separate batch.Length == 0 check is unnecessary.
4553
while (reader.TryGetEvents(out var batch))
4654
{
47-
if (batch.Length == 0) { break; }
48-
4955
foreach (var record in batch)
5056
{
5157
if (!string.IsNullOrEmpty(record.ProviderName))
@@ -61,14 +67,7 @@ public static IReadOnlyList<string> DiscoverProviderNames(
6167
return [];
6268
}
6369

64-
if (string.IsNullOrEmpty(filter))
65-
{
66-
return providerNames.ToList();
67-
}
68-
69-
var regex = new Regex(filter, RegexOptions.IgnoreCase);
70-
71-
return providerNames.Where(n => regex.IsMatch(n)).ToList();
70+
return regex is null ? providerNames.ToList() : providerNames.Where(n => regex.IsMatch(n)).ToList();
7271
}
7372

7473
/// <summary>
@@ -122,7 +121,8 @@ public static IEnumerable<ProviderDetails> LoadProviders(
122121
string evtxPath,
123122
ITraceLogger logger,
124123
string? filter = null) =>
125-
LoadProvidersCore(evtxPath, logger, string.IsNullOrEmpty(filter) ? null : new Regex(filter, RegexOptions.IgnoreCase), null, null);
124+
!RegexHelper.TryCreate(filter, logger, out var regex) ? [] :
125+
LoadProvidersCore(evtxPath, logger, regex, null, null);
126126

127127
/// <summary>
128128
/// Internal overload used by <see cref="ProviderSource" /> so name-based filtering and the de-dup
@@ -151,7 +151,7 @@ private static IEnumerable<ProviderDetails> LoadProvidersCore(
151151
IReadOnlySet<string>? skipProviderNames,
152152
HashSet<string>? seen)
153153
{
154-
var providerNames = DiscoverProviderNames(evtxPath, logger);
154+
var providerNames = DiscoverProviderNamesCore(evtxPath, logger, regex: null);
155155

156156
if (providerNames.Count == 0) { yield break; }
157157

src/EventLogExpert.EventDbTool/ProviderSource.cs

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,24 @@ internal static class ProviderSource
1919
private const string DbExtension = ".db";
2020
private const string EvtxExtension = ".evtx";
2121

22+
/// <summary>
23+
/// Conservative cap on the number of parameters in a single <c>Where(... Contains)</c> SQL IN
24+
/// clause. SQLite's default limit is 999 parameters; we stay well under that so the same code
25+
/// works on older SQLite builds too. Larger requests are split into multiple round-trips.
26+
/// </summary>
27+
private const int MaxInClauseParameters = 500;
28+
2229
/// <summary>
2330
/// Returns the distinct provider names available from <paramref name="path" />, applying an optional
2431
/// case-insensitive regex <paramref name="filter" />. Does not load full provider details.
2532
/// </summary>
2633
public static IReadOnlyList<string> LoadProviderNames(string path, ITraceLogger logger, string? filter = null)
2734
{
35+
if (!RegexHelper.TryCreate(filter, logger, out var regex))
36+
{
37+
return [];
38+
}
39+
2840
var names = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
2941

3042
foreach (var file in EnumerateSourceFiles(path))
@@ -35,7 +47,9 @@ public static IReadOnlyList<string> LoadProviderNames(string path, ITraceLogger
3547
}
3648
}
3749

38-
return ApplyFilter(names, filter).ToList();
50+
if (regex is null) { return names.ToList(); }
51+
52+
return names.Where(n => regex.IsMatch(n)).ToList();
3953
}
4054

4155
/// <summary>
@@ -49,19 +63,9 @@ public static IEnumerable<ProviderDetails> LoadProviders(
4963
string path,
5064
ITraceLogger logger,
5165
string? filter = null,
52-
IReadOnlySet<string>? skipProviderNames = null)
53-
{
54-
Regex? regex = string.IsNullOrEmpty(filter) ? null : new Regex(filter, RegexOptions.IgnoreCase);
55-
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
56-
57-
foreach (var file in EnumerateSourceFiles(path))
58-
{
59-
foreach (var details in LoadDetailsFromFile(file, logger, regex, skipProviderNames, seen))
60-
{
61-
yield return details;
62-
}
63-
}
64-
}
66+
IReadOnlySet<string>? skipProviderNames = null) =>
67+
!RegexHelper.TryCreate(filter, logger, out var regex) ? [] :
68+
LoadProvidersIterator(path, logger, regex, skipProviderNames);
6569

6670
/// <summary>Validates that <paramref name="path" /> exists and has a recognized form.</summary>
6771
public static bool TryValidate(string path, ITraceLogger logger)
@@ -98,15 +102,6 @@ internal static bool ShouldInclude(
98102
return seen.Add(providerName);
99103
}
100104

101-
private static IEnumerable<string> ApplyFilter(IEnumerable<string> names, string? filter)
102-
{
103-
if (string.IsNullOrEmpty(filter)) { return names; }
104-
105-
var regex = new Regex(filter, RegexOptions.IgnoreCase);
106-
107-
return names.Where(n => regex.IsMatch(n));
108-
}
109-
110105
/// <summary>
111106
/// Expands <paramref name="path" /> into the ordered list of source files: a single .db or .evtx
112107
/// when given a file; or all *.db files (sorted) followed by all *.evtx files (sorted) when given a
@@ -155,9 +150,21 @@ private static IEnumerable<ProviderDetails> LoadDetailsFromFile(
155150

156151
if (namesToLoad.Count == 0) { return []; }
157152

158-
return ctx.ProviderDetails
159-
.Where(p => namesToLoad.Contains(p.ProviderName))
160-
.ToList();
153+
// Chunk the IN-clause to stay below SQLite's parameter limit (default 999). This keeps
154+
// the optimization in finding #4 working for source DBs with thousands of providers.
155+
var loaded = new List<ProviderDetails>(namesToLoad.Count);
156+
157+
for (var offset = 0; offset < namesToLoad.Count; offset += MaxInClauseParameters)
158+
{
159+
var chunk = namesToLoad
160+
.Skip(offset)
161+
.Take(MaxInClauseParameters)
162+
.ToList();
163+
164+
loaded.AddRange(ctx.ProviderDetails.Where(p => chunk.Contains(p.ProviderName)));
165+
}
166+
167+
return loaded;
161168
}
162169

163170
if (string.Equals(ext, EvtxExtension, StringComparison.OrdinalIgnoreCase))
@@ -190,4 +197,21 @@ private static IEnumerable<string> LoadNamesFromFile(string file, ITraceLogger l
190197

191198
return [];
192199
}
200+
201+
private static IEnumerable<ProviderDetails> LoadProvidersIterator(
202+
string path,
203+
ITraceLogger logger,
204+
Regex? regex,
205+
IReadOnlySet<string>? skipProviderNames)
206+
{
207+
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
208+
209+
foreach (var file in EnumerateSourceFiles(path))
210+
{
211+
foreach (var details in LoadDetailsFromFile(file, logger, regex, skipProviderNames, seen))
212+
{
213+
yield return details;
214+
}
215+
}
216+
}
193217
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// // Copyright (c) Microsoft Corporation.
2+
// // Licensed under the MIT License.
3+
4+
using EventLogExpert.Eventing.Helpers;
5+
using System.Text.RegularExpressions;
6+
7+
namespace EventLogExpert.EventDbTool;
8+
9+
/// <summary>
10+
/// Centralized creation of <see cref="Regex" /> instances for user-supplied <c>--filter</c> patterns.
11+
/// Always sets a match timeout to bound worst-case execution against catastrophic backtracking, and
12+
/// converts <see cref="ArgumentException" /> from invalid patterns into a logged error rather than
13+
/// letting it terminate the process.
14+
/// </summary>
15+
internal static class RegexHelper
16+
{
17+
/// <summary>Maximum time a single regex match is allowed to take before throwing.</summary>
18+
private static readonly TimeSpan s_matchTimeout = TimeSpan.FromSeconds(1);
19+
20+
/// <summary>
21+
/// Attempts to compile <paramref name="pattern" /> into a case-insensitive <see cref="Regex" /> with
22+
/// a bounded match timeout. A null/empty pattern is treated as "no filter": <paramref name="regex" />
23+
/// is set to <see langword="null" /> and the method still returns <see langword="true" /> so callers
24+
/// can distinguish an absent filter from a malformed one.
25+
/// </summary>
26+
public static bool TryCreate(string? pattern, ITraceLogger logger, out Regex? regex)
27+
{
28+
if (string.IsNullOrEmpty(pattern))
29+
{
30+
regex = null;
31+
return true;
32+
}
33+
34+
try
35+
{
36+
regex = new Regex(pattern, RegexOptions.IgnoreCase, s_matchTimeout);
37+
return true;
38+
}
39+
catch (ArgumentException ex)
40+
{
41+
logger.Error($"Invalid --filter regex '{pattern}': {ex.Message}");
42+
regex = null;
43+
return false;
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)