Skip to content

Commit c507624

Browse files
authored
Merge pull request #20 from donour/20251210/live_tuning
20251210/live tuning
2 parents 5a57434 + c4449d5 commit c507624

File tree

53 files changed

+62624
-110
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+62624
-110
lines changed
Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
namespace BinaryFileMonitor;
2+
3+
/// <summary>
4+
/// Monitors a binary file for changes by periodically reloading it from disk
5+
/// and detecting 32-bit word-level modifications.
6+
/// </summary>
7+
/// <remarks>
8+
/// This class loads a binary file into memory and spawns a background thread
9+
/// that continuously monitors the file for changes. When 32-bit words change, it emits
10+
/// events with the offset and new value information.
11+
///
12+
/// Monitoring operates on 4-byte aligned words. File size should ideally be a multiple of 4.
13+
/// Thread-safe for concurrent access to the buffer.
14+
/// </remarks>
15+
public class BinaryFileMonitor : IDisposable
16+
{
17+
private byte[] _currentBuffer;
18+
private readonly string _filePath;
19+
private readonly int _scanIntervalMs;
20+
private Thread? _monitorThread;
21+
private volatile bool _isRunning;
22+
private readonly object _bufferLock = new();
23+
private bool _disposed;
24+
25+
/// <summary>
26+
/// Raised when a 32-bit word in the monitored file changes.
27+
/// </summary>
28+
public event EventHandler<WordChangedEventArgs>? WordChanged;
29+
30+
/// <summary>
31+
/// Raised after the file has been reloaded and all changes have been detected.
32+
/// </summary>
33+
public event EventHandler<FileReloadedEventArgs>? FileReloaded;
34+
35+
/// <summary>
36+
/// Raised when an error occurs during file monitoring (e.g., file deleted, access denied).
37+
/// </summary>
38+
public event EventHandler<FileMonitorErrorEventArgs>? MonitorError;
39+
40+
/// <summary>
41+
/// Gets whether the monitor is currently running.
42+
/// </summary>
43+
public bool IsRunning => _isRunning;
44+
45+
/// <summary>
46+
/// Gets the path of the file being monitored.
47+
/// </summary>
48+
public string FilePath => _filePath;
49+
50+
/// <summary>
51+
/// Gets the current size of the buffer in bytes.
52+
/// </summary>
53+
public int BufferSize
54+
{
55+
get
56+
{
57+
lock (_bufferLock)
58+
{
59+
return _currentBuffer.Length;
60+
}
61+
}
62+
}
63+
64+
/// <summary>
65+
/// Initializes a new instance of the <see cref="BinaryFileMonitor"/> class.
66+
/// </summary>
67+
/// <param name="filePath">The path to the binary file to monitor.</param>
68+
/// <param name="scanIntervalMs">The interval in milliseconds between file scans. Default is 100ms.</param>
69+
/// <exception cref="ArgumentNullException">Thrown when filePath is null.</exception>
70+
/// <exception cref="ArgumentException">Thrown when filePath is empty or whitespace.</exception>
71+
/// <exception cref="ArgumentOutOfRangeException">Thrown when scanIntervalMs is less than or equal to 0.</exception>
72+
public BinaryFileMonitor(string filePath, int scanIntervalMs = 100)
73+
{
74+
if (string.IsNullOrWhiteSpace(filePath))
75+
{
76+
throw new ArgumentException("File path cannot be null or whitespace.", nameof(filePath));
77+
}
78+
79+
if (scanIntervalMs <= 0)
80+
{
81+
throw new ArgumentOutOfRangeException(nameof(scanIntervalMs), "Scan interval must be greater than 0.");
82+
}
83+
84+
_filePath = filePath;
85+
_scanIntervalMs = scanIntervalMs;
86+
_currentBuffer = Array.Empty<byte>();
87+
}
88+
89+
/// <summary>
90+
/// Starts monitoring the file for changes.
91+
/// </summary>
92+
/// <exception cref="InvalidOperationException">Thrown when the monitor is already running.</exception>
93+
/// <exception cref="FileNotFoundException">Thrown when the specified file does not exist.</exception>
94+
/// <exception cref="IOException">Thrown when the file cannot be read.</exception>
95+
/// <exception cref="ObjectDisposedException">Thrown when the monitor has been disposed.</exception>
96+
public void Start()
97+
{
98+
ObjectDisposedException.ThrowIf(_disposed, this);
99+
100+
if (_isRunning)
101+
{
102+
throw new InvalidOperationException("Monitor is already running.");
103+
}
104+
105+
// Load initial file contents
106+
try
107+
{
108+
lock (_bufferLock)
109+
{
110+
_currentBuffer = File.ReadAllBytes(_filePath);
111+
}
112+
}
113+
catch (Exception ex) when (ex is FileNotFoundException or IOException or UnauthorizedAccessException)
114+
{
115+
throw new IOException($"Failed to load file: {_filePath}", ex);
116+
}
117+
118+
// Start monitoring thread
119+
_isRunning = true;
120+
_monitorThread = new Thread(MonitorLoop)
121+
{
122+
IsBackground = true,
123+
Name = $"BinaryFileMonitor-{Path.GetFileName(_filePath)}"
124+
};
125+
_monitorThread.Start();
126+
}
127+
128+
/// <summary>
129+
/// Stops monitoring the file.
130+
/// </summary>
131+
/// <remarks>
132+
/// This method blocks until the monitoring thread has exited.
133+
/// </remarks>
134+
public void Stop()
135+
{
136+
if (!_isRunning)
137+
{
138+
return;
139+
}
140+
141+
_isRunning = false;
142+
_monitorThread?.Join();
143+
_monitorThread = null;
144+
}
145+
146+
/// <summary>
147+
/// Gets a copy of the current buffer contents.
148+
/// </summary>
149+
/// <returns>A byte array containing a copy of the current buffer.</returns>
150+
public byte[] GetCurrentBuffer()
151+
{
152+
lock (_bufferLock)
153+
{
154+
return (byte[])_currentBuffer.Clone();
155+
}
156+
}
157+
158+
/// <summary>
159+
/// Gets a copy of a specific range of bytes from the buffer.
160+
/// </summary>
161+
/// <param name="offset">The starting offset.</param>
162+
/// <param name="length">The number of bytes to retrieve.</param>
163+
/// <returns>A byte array containing the requested range.</returns>
164+
/// <exception cref="ArgumentOutOfRangeException">Thrown when offset or length is invalid.</exception>
165+
public byte[] GetBufferRange(int offset, int length)
166+
{
167+
lock (_bufferLock)
168+
{
169+
if (offset < 0 || offset >= _currentBuffer.Length)
170+
{
171+
throw new ArgumentOutOfRangeException(nameof(offset));
172+
}
173+
174+
if (length < 0 || offset + length > _currentBuffer.Length)
175+
{
176+
throw new ArgumentOutOfRangeException(nameof(length));
177+
}
178+
179+
byte[] result = new byte[length];
180+
Array.Copy(_currentBuffer, offset, result, 0, length);
181+
return result;
182+
}
183+
}
184+
185+
/// <summary>
186+
/// Gets a 32-bit word from the buffer at the specified byte offset.
187+
/// </summary>
188+
/// <param name="byteOffset">The byte offset of the word to retrieve (must be 4-byte aligned).</param>
189+
/// <returns>The 32-bit unsigned integer value at the specified offset.</returns>
190+
/// <exception cref="ArgumentOutOfRangeException">Thrown when offset is invalid or not 4-byte aligned.</exception>
191+
public uint GetWord(int byteOffset)
192+
{
193+
lock (_bufferLock)
194+
{
195+
if (byteOffset < 0 || byteOffset + 4 > _currentBuffer.Length)
196+
{
197+
throw new ArgumentOutOfRangeException(nameof(byteOffset));
198+
}
199+
200+
if (byteOffset % 4 != 0)
201+
{
202+
throw new ArgumentException("Byte offset must be 4-byte aligned.", nameof(byteOffset));
203+
}
204+
205+
return BitConverter.ToUInt32(_currentBuffer, byteOffset);
206+
}
207+
}
208+
209+
/// <summary>
210+
/// Gets the number of 32-bit words in the buffer.
211+
/// </summary>
212+
public int WordCount
213+
{
214+
get
215+
{
216+
lock (_bufferLock)
217+
{
218+
return _currentBuffer.Length / 4;
219+
}
220+
}
221+
}
222+
223+
private void MonitorLoop()
224+
{
225+
while (_isRunning)
226+
{
227+
try
228+
{
229+
Thread.Sleep(_scanIntervalMs);
230+
231+
if (!_isRunning)
232+
{
233+
break;
234+
}
235+
236+
ReloadAndCompare();
237+
}
238+
catch (ThreadInterruptedException)
239+
{
240+
break;
241+
}
242+
catch (Exception ex)
243+
{
244+
OnMonitorError(new FileMonitorErrorEventArgs(ex));
245+
}
246+
}
247+
}
248+
249+
private void ReloadAndCompare()
250+
{
251+
byte[] newBuffer;
252+
253+
try
254+
{
255+
newBuffer = File.ReadAllBytes(_filePath);
256+
}
257+
catch (Exception ex) when (ex is FileNotFoundException or IOException or UnauthorizedAccessException)
258+
{
259+
OnMonitorError(new FileMonitorErrorEventArgs(ex));
260+
return;
261+
}
262+
263+
int changeCount = 0;
264+
265+
lock (_bufferLock)
266+
{
267+
// Handle size changes
268+
if (newBuffer.Length != _currentBuffer.Length)
269+
{
270+
// Emit changes for all 32-bit words in the common range that differ
271+
int minLength = Math.Min(_currentBuffer.Length, newBuffer.Length);
272+
int wordCount = minLength / 4;
273+
274+
for (int i = 0; i < wordCount; i++)
275+
{
276+
int byteOffset = i * 4;
277+
uint oldWord = BitConverter.ToUInt32(_currentBuffer, byteOffset);
278+
uint newWord = BitConverter.ToUInt32(newBuffer, byteOffset);
279+
280+
if (oldWord != newWord)
281+
{
282+
OnWordChanged(new WordChangedEventArgs(byteOffset, oldWord, newWord));
283+
changeCount++;
284+
}
285+
}
286+
287+
// Update buffer
288+
_currentBuffer = newBuffer;
289+
OnFileReloaded(new FileReloadedEventArgs(changeCount, true));
290+
return;
291+
}
292+
293+
// Compare word-by-word (32-bit aligned)
294+
int totalWords = _currentBuffer.Length / 4;
295+
for (int i = 0; i < totalWords; i++)
296+
{
297+
int byteOffset = i * 4;
298+
uint oldWord = BitConverter.ToUInt32(_currentBuffer, byteOffset);
299+
uint newWord = BitConverter.ToUInt32(newBuffer, byteOffset);
300+
301+
if (oldWord != newWord)
302+
{
303+
OnWordChanged(new WordChangedEventArgs(byteOffset, oldWord, newWord));
304+
305+
// Update the word in current buffer
306+
Array.Copy(newBuffer, byteOffset, _currentBuffer, byteOffset, 4);
307+
changeCount++;
308+
}
309+
}
310+
}
311+
312+
if (changeCount > 0)
313+
{
314+
OnFileReloaded(new FileReloadedEventArgs(changeCount, false));
315+
}
316+
}
317+
318+
/// <summary>
319+
/// Raises the <see cref="WordChanged"/> event.
320+
/// </summary>
321+
/// <param name="e">The event arguments.</param>
322+
protected virtual void OnWordChanged(WordChangedEventArgs e)
323+
{
324+
WordChanged?.Invoke(this, e);
325+
}
326+
327+
/// <summary>
328+
/// Raises the <see cref="FileReloaded"/> event.
329+
/// </summary>
330+
/// <param name="e">The event arguments.</param>
331+
protected virtual void OnFileReloaded(FileReloadedEventArgs e)
332+
{
333+
FileReloaded?.Invoke(this, e);
334+
}
335+
336+
/// <summary>
337+
/// Raises the <see cref="MonitorError"/> event.
338+
/// </summary>
339+
/// <param name="e">The event arguments.</param>
340+
protected virtual void OnMonitorError(FileMonitorErrorEventArgs e)
341+
{
342+
MonitorError?.Invoke(this, e);
343+
}
344+
345+
/// <summary>
346+
/// Disposes the monitor and stops monitoring if running.
347+
/// </summary>
348+
public void Dispose()
349+
{
350+
if (_disposed)
351+
{
352+
return;
353+
}
354+
355+
Stop();
356+
_disposed = true;
357+
GC.SuppressFinalize(this);
358+
}
359+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
9+
<Version>1.0.0</Version>
10+
<Authors>LotusECMLogger Contributors</Authors>
11+
<Description>A lightweight library for monitoring binary files and detecting byte-level changes in real-time.</Description>
12+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
13+
</PropertyGroup>
14+
15+
</Project>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace BinaryFileMonitor;
2+
3+
/// <summary>
4+
/// Provides data for the <see cref="BinaryFileMonitor.MonitorError"/> event.
5+
/// </summary>
6+
public class FileMonitorErrorEventArgs : EventArgs
7+
{
8+
/// <summary>
9+
/// Gets the exception that occurred during monitoring.
10+
/// </summary>
11+
public Exception Exception { get; }
12+
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="FileMonitorErrorEventArgs"/> class.
15+
/// </summary>
16+
/// <param name="exception">The exception that occurred.</param>
17+
public FileMonitorErrorEventArgs(Exception exception)
18+
{
19+
Exception = exception ?? throw new ArgumentNullException(nameof(exception));
20+
}
21+
}

0 commit comments

Comments
 (0)