240 lines
7.4 KiB
C#
240 lines
7.4 KiB
C#
using System;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace RobotNet10.Realtime;
|
|
|
|
/// <summary>
|
|
/// Provides high-resolution real-time timers using Linux timerfd.
|
|
/// More accurate than System.Threading.Timer for real-time applications.
|
|
/// </summary>
|
|
public class RealtimeTimer : IDisposable
|
|
{
|
|
private int _fd = -1;
|
|
private readonly RealtimeClock _clock;
|
|
private bool _disposed = false;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of RealtimeTimer.
|
|
/// </summary>
|
|
/// <param name="clock">Clock type to use (default: CLOCK_MONOTONIC)</param>
|
|
/// <param name="nonBlocking">Whether the timer should be non-blocking</param>
|
|
public RealtimeTimer(RealtimeClockType clock = RealtimeClockType.Monotonic, bool nonBlocking = false)
|
|
{
|
|
_clock = new RealtimeClock(clock);
|
|
int flags = LinuxNative.TFD_CLOEXEC;
|
|
if (nonBlocking)
|
|
{
|
|
flags |= LinuxNative.TFD_NONBLOCK;
|
|
}
|
|
|
|
_fd = LinuxNative.timerfd_create((int)clock, flags);
|
|
if (_fd < 0)
|
|
{
|
|
LinuxNative.ThrowLastError("timerfd_create");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the file descriptor for the timer.
|
|
/// </summary>
|
|
public int FileDescriptor => _fd;
|
|
|
|
/// <summary>
|
|
/// Sets the timer to fire periodically.
|
|
/// </summary>
|
|
/// <param name="interval">Interval between timer expirations</param>
|
|
public void SetPeriodic(TimeSpan interval)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
throw new ObjectDisposedException(nameof(RealtimeTimer));
|
|
}
|
|
|
|
var spec = new LinuxNative.itimerspec
|
|
{
|
|
it_interval = TimeSpanToTimespec(interval),
|
|
it_value = TimeSpanToTimespec(interval) // First expiration
|
|
};
|
|
|
|
var oldSpec = new LinuxNative.itimerspec();
|
|
int result = LinuxNative.timerfd_settime(_fd, 0, ref spec, ref oldSpec);
|
|
if (result != 0)
|
|
{
|
|
LinuxNative.ThrowLastError("timerfd_settime");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the timer to fire once after a delay.
|
|
/// </summary>
|
|
/// <param name="delay">Delay before timer expiration</param>
|
|
public void SetOneShot(TimeSpan delay)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
throw new ObjectDisposedException(nameof(RealtimeTimer));
|
|
}
|
|
|
|
var spec = new LinuxNative.itimerspec
|
|
{
|
|
it_interval = new LinuxNative.timespec { tv_sec = 0, tv_nsec = 0 }, // No repeat
|
|
it_value = TimeSpanToTimespec(delay)
|
|
};
|
|
|
|
var oldSpec = new LinuxNative.itimerspec();
|
|
int result = LinuxNative.timerfd_settime(_fd, 0, ref spec, ref oldSpec);
|
|
if (result != 0)
|
|
{
|
|
LinuxNative.ThrowLastError("timerfd_settime");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the timer to fire at an absolute time.
|
|
/// </summary>
|
|
/// <param name="absoluteTime">Absolute time when timer should expire</param>
|
|
public void SetAbsolute(DateTime absoluteTime)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
throw new ObjectDisposedException(nameof(RealtimeTimer));
|
|
}
|
|
|
|
var clockTime = _clock.GetTime();
|
|
var targetTime = absoluteTime.ToUniversalTime();
|
|
var delay = targetTime - DateTime.UtcNow;
|
|
|
|
var spec = new LinuxNative.itimerspec
|
|
{
|
|
it_interval = new LinuxNative.timespec { tv_sec = 0, tv_nsec = 0 },
|
|
it_value = TimeSpanToTimespec(delay)
|
|
};
|
|
|
|
var oldSpec = new LinuxNative.itimerspec();
|
|
int result = LinuxNative.timerfd_settime(_fd, LinuxNative.TFD_TIMER_ABSTIME, ref spec, ref oldSpec);
|
|
if (result != 0)
|
|
{
|
|
LinuxNative.ThrowLastError("timerfd_settime");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disarms the timer.
|
|
/// </summary>
|
|
public void Disarm()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
throw new ObjectDisposedException(nameof(RealtimeTimer));
|
|
}
|
|
|
|
var spec = new LinuxNative.itimerspec
|
|
{
|
|
it_interval = new LinuxNative.timespec { tv_sec = 0, tv_nsec = 0 },
|
|
it_value = new LinuxNative.timespec { tv_sec = 0, tv_nsec = 0 }
|
|
};
|
|
|
|
var oldSpec = new LinuxNative.itimerspec();
|
|
int result = LinuxNative.timerfd_settime(_fd, 0, ref spec, ref oldSpec);
|
|
if (result != 0)
|
|
{
|
|
LinuxNative.ThrowLastError("timerfd_settime");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current timer value.
|
|
/// </summary>
|
|
/// <returns>Current timer interval and value</returns>
|
|
public (TimeSpan interval, TimeSpan value) GetTime()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
throw new ObjectDisposedException(nameof(RealtimeTimer));
|
|
}
|
|
|
|
var spec = new LinuxNative.itimerspec();
|
|
int result = LinuxNative.timerfd_gettime(_fd, ref spec);
|
|
if (result != 0)
|
|
{
|
|
LinuxNative.ThrowLastError("timerfd_gettime");
|
|
}
|
|
|
|
return (TimespecToTimeSpan(spec.it_interval), TimespecToTimeSpan(spec.it_value));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the timer expiration count (number of times timer has expired since last read).
|
|
/// In non-blocking mode, returns 0 if timer hasn't expired yet (EAGAIN).
|
|
/// </summary>
|
|
/// <returns>Number of expirations, or 0 if timer hasn't expired (non-blocking mode)</returns>
|
|
public ulong ReadExpirations()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
throw new ObjectDisposedException(nameof(RealtimeTimer));
|
|
}
|
|
|
|
IntPtr bufferPtr = Marshal.AllocHGlobal(8);
|
|
try
|
|
{
|
|
long bytesRead = LinuxNative.read(_fd, bufferPtr, new IntPtr(8));
|
|
if (bytesRead < 0)
|
|
{
|
|
int errno = LinuxNative.GetLastError();
|
|
if (errno == 11) // EAGAIN - timer hasn't expired yet (non-blocking mode)
|
|
{
|
|
return 0;
|
|
}
|
|
LinuxNative.ThrowLastError("read");
|
|
}
|
|
if (bytesRead != 8)
|
|
{
|
|
throw new InvalidOperationException($"Expected to read 8 bytes, got {bytesRead}");
|
|
}
|
|
return (ulong)Marshal.ReadInt64(bufferPtr);
|
|
}
|
|
finally
|
|
{
|
|
Marshal.FreeHGlobal(bufferPtr);
|
|
}
|
|
}
|
|
|
|
private static LinuxNative.timespec TimeSpanToTimespec(TimeSpan ts)
|
|
{
|
|
long totalSeconds = (long)ts.TotalSeconds;
|
|
long nanoseconds = (long)((ts.TotalMilliseconds % 1000) * 1000000);
|
|
return new LinuxNative.timespec
|
|
{
|
|
tv_sec = totalSeconds,
|
|
tv_nsec = nanoseconds
|
|
};
|
|
}
|
|
|
|
private static TimeSpan TimespecToTimeSpan(LinuxNative.timespec ts)
|
|
{
|
|
return TimeSpan.FromSeconds(ts.tv_sec) + TimeSpan.FromTicks(ts.tv_nsec / 100);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (!_disposed && _fd >= 0)
|
|
{
|
|
LinuxNative.close(_fd);
|
|
_fd = -1;
|
|
_disposed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Additional P/Invoke declarations needed for timerfd
|
|
internal static partial class LinuxNative
|
|
{
|
|
[LibraryImport("libc", SetLastError = true)]
|
|
public static partial long read(int fd, IntPtr buf, IntPtr count);
|
|
|
|
[LibraryImport("libc", SetLastError = true)]
|
|
public static partial int close(int fd);
|
|
}
|
|
|