Skip to content

Commit 918f7c2

Browse files
committed
✨ better filewatcher
1 parent d3917a1 commit 918f7c2

8 files changed

Lines changed: 348 additions & 127 deletions

File tree

EliteAPI.Tests/FileWatcherTests.cs

Lines changed: 168 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,28 @@ public async Task Cleanup()
2121
}
2222

2323
[Test]
24-
public async Task EntireFileWatcher_Create_ReturnsInitialContent()
24+
public async Task EntireFile_Create_ReturnsInitialContent()
2525
{
2626
var testFile = new FileInfo(Path.Combine(_testDirectory.FullName, "test.txt"));
2727
await File.WriteAllTextAsync(testFile.FullName, "Initial content");
2828

29-
var (watcher, initialContent) = EntireFileWatcher.Create(testFile);
29+
var (watcher, initialContent) = FileWatcher.Create(testFile, FileWatchMode.EntireFile);
3030

3131
initialContent.Should().Be("Initial content");
3232
watcher.Should().NotBeNull();
3333
}
3434

3535
[Test]
36-
public async Task EntireFileWatcher_OnChange_EmitsEntireFileContent()
36+
public async Task EntireFile_OnChange_EmitsEntireFileContent()
3737
{
3838
var testFile = new FileInfo(Path.Combine(_testDirectory.FullName, "test.txt"));
3939
await File.WriteAllTextAsync(testFile.FullName, "Initial content");
4040

41-
var (watcher, _) = EntireFileWatcher.Create(testFile);
41+
var (watcher, _) = FileWatcher.Create(testFile, FileWatchMode.EntireFile);
4242
var changedContent = "";
4343
var tcs = new TaskCompletionSource<bool>();
4444

45-
watcher.OnChange(content =>
45+
watcher.OnContentChanged(content =>
4646
{
4747
changedContent = content;
4848
tcs.TrySetResult(true);
@@ -57,29 +57,29 @@ public async Task EntireFileWatcher_OnChange_EmitsEntireFileContent()
5757
}
5858

5959
[Test]
60-
public async Task LineByLineFileWatcher_Create_ReturnsInitialLines()
60+
public async Task LineByLine_Create_ReturnsInitialContent()
6161
{
6262
var testFile = new FileInfo(Path.Combine(_testDirectory.FullName, "test.txt"));
6363
await File.WriteAllLinesAsync(testFile.FullName, ["Line 1", "Line 2", "Line 3"]);
6464

65-
var (watcher, initialContent) = LineByLineFileWatcher.Create(testFile);
65+
var (watcher, initialContent) = FileWatcher.Create(testFile, FileWatchMode.LineByLine);
6666

67-
initialContent.Should().BeEquivalentTo(["Line 1", "Line 2", "Line 3"]);
67+
initialContent.Should().Contain("Line");
6868
watcher.Should().NotBeNull();
6969
}
7070

7171
[Test]
72-
public async Task LineByLineFileWatcher_OnChange_EmitsOnlyNewLines()
72+
public async Task LineByLine_OnChange_EmitsOnlyNewLines()
7373
{
7474
var testFile = new FileInfo(Path.Combine(_testDirectory.FullName, "test.txt"));
7575
await File.WriteAllLinesAsync(testFile.FullName, ["Line 1", "Line 2"]);
7676

77-
var (watcher, _) = LineByLineFileWatcher.Create(testFile);
77+
var (watcher, _) = FileWatcher.Create(testFile, FileWatchMode.LineByLine);
7878
var newLines = new List<string>();
7979
var tcs = new TaskCompletionSource<bool>();
8080
var expectedNewLines = 2;
8181

82-
watcher.OnChange(line =>
82+
watcher.OnContentChanged(line =>
8383
{
8484
newLines.Add(line);
8585
if (newLines.Count >= expectedNewLines)
@@ -89,23 +89,24 @@ public async Task LineByLineFileWatcher_OnChange_EmitsOnlyNewLines()
8989
await Task.Delay(100); // Give watcher time to start
9090
await File.WriteAllLinesAsync(testFile.FullName, ["Line 1", "Line 2", "Line 3", "Line 4"]);
9191

92+
// Wait for change event (with timeout)
9293
await Task.WhenAny(tcs.Task, Task.Delay(5000));
9394

9495
newLines.Should().BeEquivalentTo(["Line 3", "Line 4"]);
9596
}
9697

9798
[Test]
98-
public async Task LineByLineFileWatcher_OnChange_HandlesMultipleChanges()
99+
public async Task LineByLine_OnChange_HandlesMultipleChanges()
99100
{
100101
var testFile = new FileInfo(Path.Combine(_testDirectory.FullName, "test.txt"));
101102
await File.WriteAllLinesAsync(testFile.FullName, ["Line 1"]);
102103

103-
var (watcher, _) = LineByLineFileWatcher.Create(testFile);
104+
var (watcher, _) = FileWatcher.Create(testFile, FileWatchMode.LineByLine);
104105
var allNewLines = new List<string>();
105106
var tcs = new TaskCompletionSource<bool>();
106107
var expectedTotalLines = 3; // Line 2, Line 3, Line 4
107108

108-
watcher.OnChange(line =>
109+
watcher.OnContentChanged(line =>
109110
{
110111
allNewLines.Add(line);
111112
if (allNewLines.Count >= expectedTotalLines)
@@ -118,21 +119,22 @@ public async Task LineByLineFileWatcher_OnChange_HandlesMultipleChanges()
118119
await Task.Delay(200);
119120
await File.WriteAllLinesAsync(testFile.FullName, ["Line 1", "Line 2", "Line 3", "Line 4"]);
120121

122+
// Wait for change event (with timeout)
121123
await Task.WhenAny(tcs.Task, Task.Delay(5000));
122124

123125
allNewLines.Should().BeEquivalentTo(["Line 2", "Line 3", "Line 4"]);
124126
}
125127

126128
[Test]
127-
public async Task LineByLineFileWatcher_OnChange_DoesNotEmitWhenNoNewLines()
129+
public async Task LineByLine_OnChange_DoesNotEmitWhenNoNewLines()
128130
{
129131
var testFile = new FileInfo(Path.Combine(_testDirectory.FullName, "test.txt"));
130132
await File.WriteAllLinesAsync(testFile.FullName, ["Line 1", "Line 2"]);
131133

132-
var (watcher, _) = LineByLineFileWatcher.Create(testFile);
134+
var (watcher, _) = FileWatcher.Create(testFile, FileWatchMode.LineByLine);
133135
var newLines = new List<string>();
134136

135-
watcher.OnChange(line =>
137+
watcher.OnContentChanged(line =>
136138
{
137139
newLines.Add(line);
138140
});
@@ -145,4 +147,153 @@ public async Task LineByLineFileWatcher_OnChange_DoesNotEmitWhenNoNewLines()
145147

146148
newLines.Should().BeEmpty();
147149
}
150+
151+
[Test]
152+
public async Task LineByLine_CreateWithDirectory_ReturnsInitialContentFromLatestFile()
153+
{
154+
var file1 = new FileInfo(Path.Combine(_testDirectory.FullName, "Journal.2025-11-22T100000.01.log"));
155+
var file2 = new FileInfo(Path.Combine(_testDirectory.FullName, "Journal.2025-11-22T110000.01.log"));
156+
157+
await File.WriteAllLinesAsync(file1.FullName, ["Old line 1", "Old line 2"]);
158+
await Task.Delay(10); // Ensure file2 has a later timestamp
159+
await File.WriteAllLinesAsync(file2.FullName, ["New line 1", "New line 2"]);
160+
161+
var (watcher, initialContent) = FileWatcher.Create(_testDirectory, "Journal.*.log", FileWatchMode.LineByLine);
162+
163+
initialContent.Should().Contain("New line");
164+
watcher.Should().NotBeNull();
165+
watcher.CurrentFile.Name.Should().Be("Journal.2025-11-22T110000.01.log");
166+
}
167+
168+
[Test]
169+
public async Task LineByLine_CreateWithDirectory_SwitchesToNewFile()
170+
{
171+
var file1 = new FileInfo(Path.Combine(_testDirectory.FullName, "Journal.2025-11-22T100000.01.log"));
172+
await File.WriteAllLinesAsync(file1.FullName, ["Line 1"]);
173+
174+
var (watcher, _) = FileWatcher.Create(_testDirectory, "Journal.*.log", FileWatchMode.LineByLine);
175+
var allLines = new List<string>();
176+
var tcs = new TaskCompletionSource<bool>();
177+
178+
watcher.OnContentChanged(line =>
179+
{
180+
allLines.Add(line);
181+
if (allLines.Count >= 3) // Expecting 3 total lines
182+
tcs.TrySetResult(true);
183+
});
184+
185+
await Task.Delay(100); // Give watcher time to start
186+
187+
// Add line to first file
188+
await File.WriteAllLinesAsync(file1.FullName, ["Line 1", "Line 2"]);
189+
await Task.Delay(200);
190+
191+
// Create a new journal file
192+
var file2 = new FileInfo(Path.Combine(_testDirectory.FullName, "Journal.2025-11-22T110000.01.log"));
193+
await File.WriteAllLinesAsync(file2.FullName, ["New file line 1"]);
194+
await Task.Delay(300); // Give watcher time to detect and switch
195+
196+
// Add another line to the new file
197+
await File.WriteAllLinesAsync(file2.FullName, ["New file line 1", "New file line 2"]);
198+
199+
// Wait for all expected lines
200+
await Task.WhenAny(tcs.Task, Task.Delay(5000));
201+
202+
allLines.Should().Contain("Line 2");
203+
allLines.Should().Contain("New file line 2");
204+
}
205+
206+
[Test]
207+
public async Task LineByLine_CreateWithDirectory_IgnoresIrrelevantFiles()
208+
{
209+
var journalFile = new FileInfo(Path.Combine(_testDirectory.FullName, "Journal.2025-11-22T100000.01.log"));
210+
await File.WriteAllLinesAsync(journalFile.FullName, ["Journal line 1"]);
211+
212+
var (watcher, _) = FileWatcher.Create(_testDirectory, "Journal.*.log", FileWatchMode.LineByLine);
213+
var allLines = new List<string>();
214+
215+
watcher.OnContentChanged(line =>
216+
{
217+
allLines.Add(line);
218+
});
219+
220+
await Task.Delay(100); // Give watcher time to start
221+
222+
// Create and write to an irrelevant file (doesn't match pattern)
223+
var irrelevantFile = new FileInfo(Path.Combine(_testDirectory.FullName, "Status.json"));
224+
await File.WriteAllTextAsync(irrelevantFile.FullName, "{ \"status\": \"test\" }");
225+
await Task.Delay(300);
226+
227+
// Write to another irrelevant file
228+
var anotherIrrelevant = new FileInfo(Path.Combine(_testDirectory.FullName, "SomeOtherFile.txt"));
229+
await File.WriteAllTextAsync(anotherIrrelevant.FullName, "Irrelevant content");
230+
await Task.Delay(300);
231+
232+
// Write to the journal file
233+
await File.WriteAllLinesAsync(journalFile.FullName, ["Journal line 1", "Journal line 2"]);
234+
await Task.Delay(300);
235+
236+
watcher.CurrentFile.Name.Should().Be("Journal.2025-11-22T100000.01.log");
237+
allLines.Should().ContainSingle();
238+
allLines.Should().Contain("Journal line 2");
239+
}
240+
241+
[Test]
242+
public async Task LineByLine_OnFileChanged_InvokesCallbackWhenSwitchingFiles()
243+
{
244+
var file1 = new FileInfo(Path.Combine(_testDirectory.FullName, "Journal.2025-11-22T100000.01.log"));
245+
await File.WriteAllLinesAsync(file1.FullName, ["Line 1"]);
246+
247+
var (watcher, _) = FileWatcher.Create(_testDirectory, "Journal.*.log", FileWatchMode.LineByLine);
248+
var fileChanges = new List<string>();
249+
var tcs = new TaskCompletionSource<bool>();
250+
251+
watcher.OnFileChanged(newFile =>
252+
{
253+
fileChanges.Add(newFile.Name);
254+
tcs.TrySetResult(true);
255+
});
256+
257+
watcher.OnContentChanged(line => { /* Do nothing, just need to start watching */ });
258+
259+
await Task.Delay(100); // Give watcher time to start
260+
261+
// Create and write to a new journal file
262+
var file2 = new FileInfo(Path.Combine(_testDirectory.FullName, "Journal.2025-11-22T110000.01.log"));
263+
await File.WriteAllLinesAsync(file2.FullName, ["New file line 1"]);
264+
await Task.Delay(300); // Give watcher time to detect and switch
265+
266+
// Wait for file change callback
267+
await Task.WhenAny(tcs.Task, Task.Delay(5000));
268+
269+
fileChanges.Should().ContainSingle();
270+
fileChanges[0].Should().Be("Journal.2025-11-22T110000.01.log");
271+
watcher.CurrentFile.Name.Should().Be("Journal.2025-11-22T110000.01.log");
272+
}
273+
274+
[Test]
275+
public async Task LineByLine_OnFileChanged_NotInvokedWhenNoFileSwitching()
276+
{
277+
var testFile = new FileInfo(Path.Combine(_testDirectory.FullName, "Journal.2025-11-22T100000.01.log"));
278+
await File.WriteAllLinesAsync(testFile.FullName, ["Line 1"]);
279+
280+
var (watcher, _) = FileWatcher.Create(_testDirectory, "Journal.*.log", FileWatchMode.LineByLine);
281+
var fileChanges = new List<string>();
282+
283+
watcher.OnFileChanged(newFile =>
284+
{
285+
fileChanges.Add(newFile.Name);
286+
});
287+
288+
watcher.OnContentChanged(line => { /* Do nothing */ });
289+
290+
await Task.Delay(100); // Give watcher time to start
291+
292+
// Write to the same file (no file switching)
293+
await File.WriteAllLinesAsync(testFile.FullName, ["Line 1", "Line 2", "Line 3"]);
294+
await Task.Delay(300);
295+
296+
fileChanges.Should().BeEmpty();
297+
watcher.CurrentFile.Name.Should().Be("Journal.2025-11-22T100000.01.log");
298+
}
148299
}

EliteAPI/EliteDangerousApi.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
using EliteAPI.Journals;
2-
using EliteAPI.Json;
32
using EliteAPI.Watcher;
43

54
namespace EliteApi;
65

76
public class EliteDangerousApi
87
{
9-
private readonly LineByLineFileWatcher _journalWatcher;
8+
private readonly FileWatcher _journalWatcher;
109

1110
public EliteDangerousApi()
1211
{
1312
var journalDirectory = JournalUtils.GetJournalsDirectory();
1413

15-
_journalWatcher = LineByLineFileWatcher.Create(journalDirectory, "Journal.*.log").watcher;
14+
_journalWatcher = FileWatcher.Create(journalDirectory, "Journal.*.log", FileWatchMode.LineByLine).watcher;
1615
}
1716

1817
public void OnJournalEvent(Action<(string eventName, string json)> onEvent)
1918
{
20-
_journalWatcher.OnChange(json =>
19+
_journalWatcher.OnContentChanged(json =>
2120
{
22-
var eventName = JsonUtils.GetEventName(json);
21+
var eventName = JournalUtils.GetEventName(json);
2322
onEvent((eventName, json));
2423
});
2524
}

EliteAPI/Journals/JournalUtils.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
using System.Runtime.InteropServices;
2+
using System.Text.Json;
23

34
namespace EliteAPI.Journals;
45

56
public static class JournalUtils
67
{
8+
public static string GetEventName(string json)
9+
{
10+
using var document = JsonDocument.Parse(json);
11+
if (document.RootElement.TryGetProperty("event", out var eventProperty))
12+
{
13+
return eventProperty.GetString() ?? string.Empty;
14+
}
15+
16+
return string.Empty;
17+
}
18+
719
public static DirectoryInfo GetJournalsDirectory()
820
{
921
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

EliteAPI/Json/JsonUtils.cs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,12 @@ namespace EliteAPI.Json;
66

77
public static class JsonUtils
88
{
9-
public static string GetEventName(string json)
10-
{
11-
using var document = JsonDocument.Parse(json);
12-
if (document.RootElement.TryGetProperty("event", out var eventProperty))
13-
{
14-
return eventProperty.GetString() ?? string.Empty;
15-
}
16-
17-
return string.Empty;
18-
}
19-
209
public static List<JsonPath> FlattenJson(string json)
2110
{
2211
// TODO: call flattening function
2312
// arrays need Length
2413
// key + localisation
2514
// controls mapping
26-
var eventName = GetEventName(json);
27-
2815
List<JsonPath> paths = [];
2916
var token = JToken.Parse(json);
3017

EliteAPI/Watcher/EntireFileWatcher.cs

Lines changed: 0 additions & 31 deletions
This file was deleted.

EliteAPI/Watcher/FileWatchMode.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace EliteAPI.Watcher;
2+
3+
public enum FileWatchMode
4+
{
5+
LineByLine,
6+
EntireFile
7+
}

0 commit comments

Comments
 (0)