|
6 | 6 | using EventLogExpert.Eventing.Providers; |
7 | 7 | using Microsoft.Extensions.DependencyInjection; |
8 | 8 | using System.CommandLine; |
| 9 | +using System.Text.RegularExpressions; |
9 | 10 |
|
10 | 11 | namespace EventLogExpert.EventDbTool; |
11 | 12 |
|
@@ -83,50 +84,120 @@ private void CreateDatabase(string path, string? source, string? filter, string? |
83 | 84 | return; |
84 | 85 | } |
85 | 86 |
|
86 | | - if (source is not null && !ProviderSource.TryValidate(source, Logger)) { return; } |
| 87 | + if (!RegexHelper.TryCreate(filter, Logger, out var regex)) { return; } |
87 | 88 |
|
88 | | - HashSet<string> skipProviderNames = new(StringComparer.OrdinalIgnoreCase); |
| 89 | + if (source is not null && !ProviderSource.TryValidate(source, Logger)) { return; } |
89 | 90 |
|
90 | | - if (!string.IsNullOrWhiteSpace(skipProvidersInFile)) |
| 91 | + try |
91 | 92 | { |
92 | | - if (!ProviderSource.TryValidate(skipProvidersInFile, Logger)) { return; } |
| 93 | + HashSet<string> skipProviderNames = new(StringComparer.OrdinalIgnoreCase); |
93 | 94 |
|
94 | | - foreach (var name in ProviderSource.LoadProviderNames(skipProvidersInFile, Logger)) |
| 95 | + if (!string.IsNullOrWhiteSpace(skipProvidersInFile)) |
95 | 96 | { |
96 | | - skipProviderNames.Add(name); |
| 97 | + if (!ProviderSource.TryValidate(skipProvidersInFile, Logger)) { return; } |
| 98 | + |
| 99 | + foreach (var name in ProviderSource.LoadProviderNames(skipProvidersInFile, Logger)) |
| 100 | + { |
| 101 | + skipProviderNames.Add(name); |
| 102 | + } |
| 103 | + |
| 104 | + Logger.Info($"Found {skipProviderNames.Count} providers in {skipProvidersInFile}. These will not be included in the new database."); |
97 | 105 | } |
98 | 106 |
|
99 | | - Logger.Info($"Found {skipProviderNames.Count} providers in {skipProvidersInFile}. These will not be included in the new database."); |
100 | | - } |
| 107 | + // Stream details directly into the DbContext. For .evtx sources, scanning the file |
| 108 | + // is expensive, so we deliberately do NOT pre-scan provider names just to size the |
| 109 | + // log header — that would cause a second full pass over the .evtx. Instead we |
| 110 | + // buffer the first batch of resolved details, derive the header column width from |
| 111 | + // those names, then log the header + buffered rows and continue streaming. |
| 112 | + const int batchSize = 100; |
| 113 | + var count = 0; |
| 114 | + var headerLogged = false; |
| 115 | + var pendingForHeader = new List<ProviderDetails>(batchSize); |
| 116 | + |
| 117 | + // Defer creating the DbContext (and therefore the .db file on disk) until we have |
| 118 | + // at least one provider to persist. This prevents leaving an empty database behind |
| 119 | + // when no provider details could be resolved (e.g., .evtx without LocaleMetaData). |
| 120 | + EventProviderDbContext? dbContext = null; |
| 121 | + |
| 122 | + try |
| 123 | + { |
| 124 | + IEnumerable<ProviderDetails> providersToAdd = source is null |
| 125 | + ? LoadLocalProviders(regex, skipProviderNames) |
| 126 | + : ProviderSource.LoadProviders(source, Logger, regex, skipProviderNames); |
| 127 | + |
| 128 | + foreach (var details in providersToAdd) |
| 129 | + { |
| 130 | + if (!headerLogged) |
| 131 | + { |
| 132 | + pendingForHeader.Add(details); |
| 133 | + |
| 134 | + if (pendingForHeader.Count < batchSize) { continue; } |
| 135 | + |
| 136 | + FlushHeaderAndBuffer(ref dbContext, path, pendingForHeader, ref count); |
| 137 | + headerLogged = true; |
| 138 | + continue; |
| 139 | + } |
| 140 | + |
| 141 | + dbContext ??= new EventProviderDbContext(path, false, Logger); |
| 142 | + dbContext.ProviderDetails.Add(details); |
| 143 | + LogProviderDetails(details); |
| 144 | + count++; |
| 145 | + |
| 146 | + if (count % batchSize != 0) { continue; } |
| 147 | + dbContext.SaveChanges(); |
| 148 | + dbContext.ChangeTracker.Clear(); |
| 149 | + } |
| 150 | + |
| 151 | + // Flush any buffered providers when the stream ended before the buffer filled. |
| 152 | + if (!headerLogged && pendingForHeader.Count > 0) |
| 153 | + { |
| 154 | + FlushHeaderAndBuffer(ref dbContext, path, pendingForHeader, ref count); |
| 155 | + } |
101 | 156 |
|
102 | | - IEnumerable<ProviderDetails> providersToAdd = source is null |
103 | | - ? LoadLocalProviders(filter, skipProviderNames) |
104 | | - : ProviderSource.LoadProviders(source, Logger, filter, skipProviderNames); |
| 157 | + if (dbContext is null) |
| 158 | + { |
| 159 | + Logger.Warn($"No provider details could be resolved from the source. Database was not created."); |
105 | 160 |
|
106 | | - var providersNotSkipped = providersToAdd.ToList(); |
| 161 | + return; |
| 162 | + } |
107 | 163 |
|
108 | | - if (providersNotSkipped.Count == 0) |
| 164 | + Logger.Info($""); |
| 165 | + Logger.Info($"Saving database. Please wait..."); |
| 166 | + |
| 167 | + dbContext.SaveChanges(); |
| 168 | + |
| 169 | + Logger.Info($"Done!"); |
| 170 | + } |
| 171 | + finally |
| 172 | + { |
| 173 | + dbContext?.Dispose(); |
| 174 | + } |
| 175 | + } |
| 176 | + catch (RegexMatchTimeoutException) |
109 | 177 | { |
110 | | - Logger.Warn($"No providers to add to the new database."); |
111 | | - return; |
| 178 | + Logger.Error($"The --filter regex timed out. The pattern may cause catastrophic backtracking."); |
112 | 179 | } |
| 180 | + } |
113 | 181 |
|
114 | | - using var dbContext = new EventProviderDbContext(path, false, Logger); |
| 182 | + private void FlushHeaderAndBuffer( |
| 183 | + ref EventProviderDbContext? dbContext, |
| 184 | + string path, |
| 185 | + List<ProviderDetails> buffer, |
| 186 | + ref int count) |
| 187 | + { |
| 188 | + LogProviderDetailHeader(buffer.Select(p => p.ProviderName)); |
115 | 189 |
|
116 | | - LogProviderDetailHeader(providersNotSkipped.Select(p => p.ProviderName)); |
| 190 | + dbContext ??= new EventProviderDbContext(path, false, Logger); |
117 | 191 |
|
118 | | - foreach (var details in providersNotSkipped) |
| 192 | + foreach (var details in buffer) |
119 | 193 | { |
120 | 194 | dbContext.ProviderDetails.Add(details); |
121 | 195 | LogProviderDetails(details); |
| 196 | + count++; |
122 | 197 | } |
123 | 198 |
|
124 | | - Logger.Info($""); |
125 | | - Logger.Info($"Saving database. Please wait..."); |
126 | | - |
127 | 199 | dbContext.SaveChanges(); |
128 | | - |
129 | | - Logger.Info($"Done!"); |
| 200 | + dbContext.ChangeTracker.Clear(); |
| 201 | + buffer.Clear(); |
130 | 202 | } |
131 | | - |
132 | 203 | } |
0 commit comments