using Microsoft.Extensions.Logging; using System.Diagnostics; namespace RobotNet10.Common; public class WatchThreadAsync(int Interval, Func Callback, ILogger? 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 async Task 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("WatchThreadAsync Warning: Elapsed time {peak}ms exceeds interval {Interval}ms.", now - scheduledTime + Interval, Interval); } } } if (shouldRun) { try { await 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("WatchThreadAsync 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(async () => await Handler(ThreadCts.Token)) { Priority = Priority, IsBackground = false, Name = $"WatchThreadAsync-{typeof(T).Name}" }; Thread.Start(); } else throw new ObjectDisposedException(nameof(WatchThreadAsync<>)); } } 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(); } ~WatchThreadAsync() { Dispose(false); } }