136 lines
4.1 KiB
C#
136 lines
4.1 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using System.Diagnostics;
|
|
|
|
namespace RobotNet10.Common;
|
|
|
|
public class WatchThread<T>(int Interval, Action Callback, ILogger<T>? Logger, ThreadPriority Priority = ThreadPriority.Highest) : IDisposable where T : class
|
|
{
|
|
public bool Disposed;
|
|
|
|
private Thread? Thread;
|
|
private CancellationTokenSource? ThreadCts;
|
|
|
|
private long NextDueTime;
|
|
private readonly Lock Lock = new();
|
|
private void Handler(CancellationToken cancellationToken)
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
bool shouldRun = false;
|
|
lock (Lock)
|
|
{
|
|
if (Disposed) return;
|
|
long now = GetCurrentTimeMs();
|
|
if (now >= NextDueTime)
|
|
{
|
|
shouldRun = true;
|
|
long scheduledTime = NextDueTime;
|
|
NextDueTime += Interval;
|
|
|
|
if (now - scheduledTime > Interval / 2)
|
|
{
|
|
NextDueTime = now + Interval;
|
|
if (Logger is not null && Logger.IsEnabled(LogLevel.Warning)) Logger.LogWarning("WatchThread Warning: Elapsed time {peak}ms exceeds interval {Interval}ms.", now - scheduledTime + Interval, Interval);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shouldRun)
|
|
{
|
|
try { Callback.Invoke(); }
|
|
catch (Exception ex) { if (Logger is not null && Logger.IsEnabled(LogLevel.Error)) Logger.LogError("Callback error: {ex}", ex.Message); }
|
|
}
|
|
|
|
lock (Lock)
|
|
{
|
|
if (Disposed) return;
|
|
long now = GetCurrentTimeMs();
|
|
long delay = NextDueTime - now;
|
|
if (delay < 0) delay = 0;
|
|
Thread.Sleep((int)delay);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (Logger is not null && Logger.IsEnabled(LogLevel.Error)) Logger.LogError("WatchThread Error: {ex}", ex.Message);
|
|
Thread.Sleep(Interval);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static long GetCurrentTimeMs()
|
|
{
|
|
return Stopwatch.GetTimestamp() * 1000 / Stopwatch.Frequency;
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
lock (Lock)
|
|
{
|
|
if (!Disposed)
|
|
{
|
|
if (Thread?.IsAlive == true) return;
|
|
|
|
NextDueTime = GetCurrentTimeMs() + Interval;
|
|
ThreadCts = new CancellationTokenSource();
|
|
Thread = new Thread(() => Handler(ThreadCts.Token))
|
|
{
|
|
Priority = Priority,
|
|
IsBackground = false,
|
|
Name = $"WatchThread-{typeof(T).Name}"
|
|
};
|
|
Thread.Start();
|
|
}
|
|
else throw new ObjectDisposedException(nameof(WatchThread<>));
|
|
}
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
Thread? threadToJoin;
|
|
lock (Lock)
|
|
{
|
|
if (Thread == null) return;
|
|
ThreadCts?.Cancel();
|
|
threadToJoin = Thread;
|
|
}
|
|
|
|
// If Stop() is called from within the Callback (same thread), skip Join to avoid deadlock
|
|
if (threadToJoin != null && threadToJoin != Thread.CurrentThread)
|
|
{
|
|
if (!threadToJoin.Join(TimeSpan.FromSeconds(5)))
|
|
{
|
|
Logger?.LogWarning("Thread did not stop gracefully");
|
|
}
|
|
}
|
|
|
|
lock (Lock)
|
|
{
|
|
ThreadCts?.Dispose();
|
|
ThreadCts = null;
|
|
Thread = null;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (Disposed) return;
|
|
|
|
Disposed = true;
|
|
if (disposing) Stop();
|
|
}
|
|
|
|
~WatchThread()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
}
|