Skip to content

Commit 5ceb24c

Browse files
authored
Merge pull request #592 from LogExperts/568-stop-following-tail-after-file-re-creation
568 stop following tail after file re creation
2 parents 277197c + edd5a66 commit 5ceb24c

3 files changed

Lines changed: 191 additions & 17 deletions

File tree

.vscode/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@
1515
"matchCommandLine": true
1616
},
1717
"git rev-parse": true
18-
}
18+
},
19+
"dotnet.preferCSharpExtension": true,
20+
"dotnet.defaultSolution": "src/LogExpert.sln"
1921
}

src/PluginRegistry/PluginHashGenerator.Generated.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,35 @@ public static partial class PluginValidator
1010
{
1111
/// <summary>
1212
/// Gets pre-calculated SHA256 hashes for built-in plugins.
13-
/// Generated: 2026-05-28 19:52:00 UTC
13+
/// Generated: 2026-06-01 09:31:40 UTC
1414
/// Configuration: Release
1515
/// Plugin count: 21
1616
/// </summary>
1717
public static Dictionary<string, string> GetBuiltInPluginHashes()
1818
{
1919
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
2020
{
21-
["AutoColumnizer.dll"] = "EDF4B48F71CF2192A99F63B9FE493661521A2E671D9185CCEB181B513DF0C1A5",
21+
["AutoColumnizer.dll"] = "1E5C3388943F4EB34382324E6A2C54F93C89B88CFE1D69ACF1203FB7416E672F",
2222
["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
2323
["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
24-
["CsvColumnizer.dll"] = "397CCA331A6FD1687C8CBDF78DF6DF9C080B7A16860C511F2DC0E275A5257C4F",
25-
["CsvColumnizer.dll (x86)"] = "397CCA331A6FD1687C8CBDF78DF6DF9C080B7A16860C511F2DC0E275A5257C4F",
26-
["DefaultPlugins.dll"] = "58B2ED626556C1B0E1B02D62DF0A0658618FA05B309BFAA2C2582B180594B7E6",
27-
["FlashIconHighlighter.dll"] = "0861244E3F7B52D44B8487732724E277658143E5940AACFF977AA74048FF1B9D",
28-
["GlassfishColumnizer.dll"] = "B75D7808007E9CA1235E282B07431C70A16D378B647B58BAEE605075D7B8FF08",
29-
["JsonColumnizer.dll"] = "A469FEC6C1E6F7B209D960CE7C3416CF80EFD5A2A062BCAD7B0E8AE6A3988ABF",
30-
["JsonCompactColumnizer.dll"] = "F6735973634AE8A986BC9FD8B89F8817D36458E488B6A7BCD2320883DD4BA5BC",
31-
["Log4jXmlColumnizer.dll"] = "D148D1FEC9A0152714AAE8CBAB0A9BDDF9ACFBFED3FF0CEB8D5966908DC24760",
32-
["LogExpert.Resources.dll"] = "A25FDA182EECF5BCC828C0C3F145FD774530980E9C72AC17591043677FA9E201",
24+
["CsvColumnizer.dll"] = "0DB8949CCFB20468D5C934474EEC98B3BD1AD0802D2F79F9DF9013DAF2A037C9",
25+
["CsvColumnizer.dll (x86)"] = "0DB8949CCFB20468D5C934474EEC98B3BD1AD0802D2F79F9DF9013DAF2A037C9",
26+
["DefaultPlugins.dll"] = "FE68CE75E429D0F29ABB2D221A4BBBC16AD6C06B77FB2B709134591F276DE9DF",
27+
["FlashIconHighlighter.dll"] = "A8C733BBA980A364B3739EFBF866E85E166C15B79A6B704456C7F92884BECB27",
28+
["GlassfishColumnizer.dll"] = "86D49BC1EAC7F843893134F7F5B64BE37A94C914F389DB56598DBC670D849835",
29+
["JsonColumnizer.dll"] = "6A6B27428F647DF29D03F4F17AED89DDF1FF24636B59644FBD12FB3D92A26F18",
30+
["JsonCompactColumnizer.dll"] = "04A7169C087181B49189AB8DD9C9FB260189EB1CF394452EA02987FFE6934794",
31+
["Log4jXmlColumnizer.dll"] = "301E7805F9BA5BA211256119636B7A8D34848475C5B7885737FD8E61CB1B5233",
32+
["LogExpert.Resources.dll"] = "A2AFABDCFDA0B558426706336C11FB8B02770383419BA1E0192FFFEBEB091A38",
3333
["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93",
3434
["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93",
3535
["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D",
3636
["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D",
37-
["RegexColumnizer.dll"] = "C0DC4D43DB02C2015A490BC4C62549D7CDD1B107C6944F20DB0B4C4285371288",
38-
["SftpFileSystem.dll"] = "AAD426FB4E53916B8B26F427374EA3E6706B940564F4CD0E059E69A613CBE02B",
39-
["SftpFileSystem.dll (x86)"] = "6FEC9B0EC9B9241ACA09999C55AD96F33FD52A2A1A14DC513D06BC42BF2C1B86",
40-
["SftpFileSystem.Resources.dll"] = "0F8C4D65FE7E8A79A11DF521116E36E65AC28DE732C67264A3790AD2F1E4CBB8",
41-
["SftpFileSystem.Resources.dll (x86)"] = "0F8C4D65FE7E8A79A11DF521116E36E65AC28DE732C67264A3790AD2F1E4CBB8",
37+
["RegexColumnizer.dll"] = "AC1E977392AD7A291174C357211480121C49898C2258E8608E06AF8DDA48DE7E",
38+
["SftpFileSystem.dll"] = "E5B7DD9D7038F68B501BD8398141FD6E8EBE9878B205D346E3C36FCD40DA9CA8",
39+
["SftpFileSystem.dll (x86)"] = "8B892CC370EE3E01693F282C32245B55F3D225F48D6631102EB2DC79692F762B",
40+
["SftpFileSystem.Resources.dll"] = "20CFD22D83D1581FF63348F3F0D3E824A73887F80BCDEB04E6AAF3E5A200C036",
41+
["SftpFileSystem.Resources.dll (x86)"] = "20CFD22D83D1581FF63348F3F0D3E824A73887F80BCDEB04E6AAF3E5A200C036",
4242

4343
};
4444
}

src/tools/LogRotator/LogRotator.cs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,31 @@
3535
Console.WriteLine($"Control characters in output: {(includeControlChars ? "ENABLED" : "disabled")}");
3636
Console.WriteLine("Press ENTER to perform a rotation (with oldest file deletion).");
3737
Console.WriteLine("Press A to append a single live line (no rotation) for tail testing.");
38+
Console.WriteLine("Press D to delete the log, wait, then recreate it AND start a 25 lines/s");
39+
Console.WriteLine(" background writer (repro for issue #568). Press D again to delete mid-stream.");
40+
Console.WriteLine("Press F to flicker: delete, wait, briefly recreate, delete again mid-reload,");
41+
Console.WriteLine(" wait, then recreate + writer. Tries to land LogExpert's new reader's");
42+
Console.WriteLine(" ReadFiles inside a deletion window.");
3843
Console.WriteLine("Press Q to quit.");
3944

4045
var rotationCount = 0;
46+
var delayedDeleteCount = 0;
47+
var flickerCount = 0;
48+
const int delayedDeleteSeconds = 5;
49+
const int liveWriterDelayMs = 40; // ~25 lines/s
50+
const int flickerInitialAbsentMs = 5000;
51+
const int flickerBriefVisibleMs = 200;
52+
const int flickerSecondAbsentMs = 2500;
53+
CancellationTokenSource? liveWriterCts = null;
54+
Task? liveWriterTask = null;
4155

4256
while (true)
4357
{
4458
var key = Console.ReadKey(true);
4559

4660
if (key.Key == ConsoleKey.Q)
4761
{
62+
StopLiveWriter();
4863
break;
4964
}
5065

@@ -54,6 +69,20 @@
5469
continue;
5570
}
5671

72+
if (key.Key == ConsoleKey.D)
73+
{
74+
delayedDeleteCount++;
75+
DelayedDelete(Path.Join(baseDir, safeBaseName), delayedDeleteCount, delayedDeleteSeconds);
76+
continue;
77+
}
78+
79+
if (key.Key == ConsoleKey.F)
80+
{
81+
flickerCount++;
82+
FlickerRepro(Path.Join(baseDir, safeBaseName), flickerCount);
83+
continue;
84+
}
85+
5786
if (key.Key != ConsoleKey.Enter)
5887
{
5988
continue;
@@ -131,6 +160,149 @@ void AppendLiveLine(string path)
131160
Console.WriteLine($" Appended live line to {name} ({new FileInfo(path).Length} bytes total)");
132161
}
133162

163+
// Repro path for issue #568: stop any background writer, delete the file and
164+
// keep it absent long enough (> LogExpert's 1.25s OpenStream retry budget) for
165+
// the watcher to enter FileNotFound state, then recreate it AND start a
166+
// continuous background writer (~25 lines/s). The next D press will delete the
167+
// file while the writer is actively appending — that mid-stream delete is the
168+
// scenario the reporter describes.
169+
void DelayedDelete(string path, int iteration, int delaySeconds)
170+
{
171+
var name = Path.GetFileName(path);
172+
Console.WriteLine($"\n--- Delete + delay + recreate #{iteration} ---");
173+
174+
StopLiveWriter();
175+
176+
if (File.Exists(path))
177+
{
178+
File.Delete(path);
179+
Console.WriteLine($" Deleted: {name}");
180+
}
181+
else
182+
{
183+
Console.WriteLine($" {name} was already missing.");
184+
}
185+
186+
Console.WriteLine($" File absent. Waiting {delaySeconds}s so LogExpert enters FileNotFound state...");
187+
for (var i = delaySeconds; i > 0; i--)
188+
{
189+
Console.Write($"\r Countdown: {i}s ");
190+
Thread.Sleep(1000);
191+
}
192+
Console.WriteLine("\r Countdown: done.");
193+
194+
WriteLogFile(path, fileId: 900 + iteration);
195+
Console.WriteLine($" Recreated {name} with {linesPerFile} lines ({new FileInfo(path).Length} bytes).");
196+
StartLiveWriter(path, iteration);
197+
Console.WriteLine($" Background writer started (~{1000 / liveWriterDelayMs} lines/s).");
198+
Console.WriteLine(" Watch LogExpert: lines should keep appearing.");
199+
Console.WriteLine(" If they do NOT, the bug is reproduced. Press D again to delete mid-stream.");
200+
}
201+
202+
// Tighter race than DelayedDelete: after the file has been absent long enough
203+
// for LogExpert to enter FileNotFound, we briefly recreate it (so the watcher
204+
// fires OnRespawned and the LogWindow schedules a Reload), then delete it
205+
// again before the new LogfileReader's first ReadFiles completes its
206+
// OpenStream retries (5 x 250ms = 1.25s). If the hypothesis about issue #568
207+
// is correct, the new reader's ReadFiles catches IOException, _isDeleted is
208+
// set, ReportLoadingFinished is skipped, and FileSizeChanged never gets wired
209+
// up. After we recreate the file for real and start the writer, those writes
210+
// should fail to propagate.
211+
void FlickerRepro(string path, int iteration)
212+
{
213+
var name = Path.GetFileName(path);
214+
Console.WriteLine($"\n--- Flicker repro #{iteration} ---");
215+
216+
StopLiveWriter();
217+
218+
if (File.Exists(path))
219+
{
220+
File.Delete(path);
221+
Console.WriteLine($" Deleted: {name}");
222+
}
223+
224+
Console.WriteLine($" Phase 1: file absent for {flickerInitialAbsentMs / 1000.0:0.0}s (LogExpert -> FileNotFound)");
225+
Thread.Sleep(flickerInitialAbsentMs);
226+
227+
WriteLogFile(path, fileId: 700 + iteration);
228+
Console.WriteLine($" Phase 2: briefly visible ({flickerBriefVisibleMs}ms) - LogExpert schedules a Reload");
229+
Thread.Sleep(flickerBriefVisibleMs);
230+
231+
File.Delete(path);
232+
Console.WriteLine($" Phase 3: deleted again, absent {flickerSecondAbsentMs / 1000.0:0.0}s");
233+
Console.WriteLine($" (exceeds 1.25s OpenStream retry budget - new reader's ReadFiles should fail)");
234+
Thread.Sleep(flickerSecondAbsentMs);
235+
236+
WriteLogFile(path, fileId: 750 + iteration);
237+
Console.WriteLine($" Phase 4: recreated with {linesPerFile} lines, starting writer.");
238+
StartLiveWriter(path, iteration);
239+
Console.WriteLine(" Watch LogExpert. If row count freezes, bug reproduced.");
240+
}
241+
242+
void StartLiveWriter(string path, int iteration)
243+
{
244+
StopLiveWriter();
245+
liveWriterCts = new CancellationTokenSource();
246+
var token = liveWriterCts.Token;
247+
var fileId = 800 + iteration;
248+
liveWriterTask = Task.Run(() => LiveWriterLoop(path, fileId, token));
249+
}
250+
251+
void StopLiveWriter()
252+
{
253+
if (liveWriterCts == null)
254+
{
255+
return;
256+
}
257+
258+
liveWriterCts.Cancel();
259+
try
260+
{
261+
liveWriterTask?.Wait(TimeSpan.FromSeconds(2));
262+
}
263+
catch (AggregateException)
264+
{
265+
// expected: task cancelled
266+
}
267+
268+
liveWriterCts.Dispose();
269+
liveWriterCts = null;
270+
liveWriterTask = null;
271+
}
272+
273+
void LiveWriterLoop(string path, int fileId, CancellationToken token)
274+
{
275+
var name = Path.GetFileName(path);
276+
var lineIndex = 0;
277+
while (!token.IsCancellationRequested)
278+
{
279+
try
280+
{
281+
using var fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete);
282+
using var writer = new StreamWriter(fs, Encoding.UTF8);
283+
writer.WriteLine(BuildLine(fileId, ++lineIndex, name));
284+
}
285+
catch (IOException)
286+
{
287+
// file may be momentarily inaccessible during a D-press; just keep
288+
// trying so writes resume once it reappears.
289+
}
290+
291+
try
292+
{
293+
Task.Delay(liveWriterDelayMs, token).Wait(token);
294+
}
295+
catch (OperationCanceledException)
296+
{
297+
return;
298+
}
299+
catch (AggregateException)
300+
{
301+
return;
302+
}
303+
}
304+
}
305+
134306
string BuildLine(int fileId, int lineIndex, string fileName)
135307
{
136308
var baseText = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [INFO] File#{fileId:D3} Line {lineIndex:D3} - {fileName} - Sample log message";

0 commit comments

Comments
 (0)