diff --git a/PolyShim/Net80/TimeProvider.cs b/PolyShim/Net80/TimeProvider.cs index bf4dc54..ab01b0c 100644 --- a/PolyShim/Net80/TimeProvider.cs +++ b/PolyShim/Net80/TimeProvider.cs @@ -40,21 +40,18 @@ public virtual TimeSpan GetElapsedTime(long startingTimestamp, long endingTimest public TimeSpan GetElapsedTime(long startingTimestamp) => GetElapsedTime(startingTimestamp, GetTimestamp()); -#if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) public virtual ITimer CreateTimer( TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period ) => new SystemTimeProviderTimer(dueTime, period, callback, state); -#endif private sealed class SystemTimeProvider : TimeProvider { public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Local; } -#if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) private sealed class SystemTimeProviderTimer : ITimer { private readonly Timer _timer; @@ -90,6 +87,5 @@ public void Dispose() public ValueTask DisposeAsync() => _timer.DisposeAsync(); #endif } -#endif } #endif diff --git a/PolyShim/NetCore10/Timer.cs b/PolyShim/NetCore10/Timer.cs new file mode 100644 index 0000000..d1a5b3e --- /dev/null +++ b/PolyShim/NetCore10/Timer.cs @@ -0,0 +1,156 @@ +#if NETSTANDARD && !NETSTANDARD1_2_OR_GREATER +#nullable enable +// ReSharper disable RedundantUsingDirective +// ReSharper disable CheckNamespace +// ReSharper disable InconsistentNaming +// ReSharper disable PartialTypeWithSinglePart + +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace System.Threading; + +// https://learn.microsoft.com/dotnet/api/system.threading.timer +[ExcludeFromCodeCoverage] +internal sealed class Timer(TimerCallback callback, object? state) : IDisposable +{ + private CancellationTokenSource? _cts; + private volatile bool _disposed; + + public Timer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) + : this(callback ?? throw new ArgumentNullException(nameof(callback)), state) + { + Schedule(dueTime, period); + } + + public Timer(TimerCallback callback, object? state, int dueTime, int period) + : this( + callback, + state, + TimeSpan.FromMilliseconds(dueTime), + TimeSpan.FromMilliseconds(period) + ) { } + + public Timer(TimerCallback callback, object? state, long dueTime, long period) + : this( + callback, + state, + TimeSpan.FromMilliseconds(dueTime), + TimeSpan.FromMilliseconds(period) + ) { } + + public Timer(TimerCallback callback) + : this(callback, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan) { } + + private static void Start( + TimerCallback callback, + object? state, + TimeSpan dueTime, + TimeSpan period, + CancellationToken cancellationToken + ) + { + if (dueTime == Timeout.InfiniteTimeSpan) + return; + + _ = Task.Run(async () => + { + try + { + if (dueTime > TimeSpan.Zero) + await Task.Delay(dueTime, cancellationToken); + } + catch (OperationCanceledException) + { + return; + } + + if (cancellationToken.IsCancellationRequested) + return; + + callback(state); + + if (period == Timeout.InfiniteTimeSpan || period == TimeSpan.Zero) + return; + + while (!cancellationToken.IsCancellationRequested) + { + try + { + await Task.Delay(period, cancellationToken); + } + catch (OperationCanceledException) + { + return; + } + + if (cancellationToken.IsCancellationRequested) + return; + + callback(state); + } + }); + } + + private void Schedule(TimeSpan dueTime, TimeSpan period) + { + if (_disposed) + throw new ObjectDisposedException(nameof(Timer)); + + if (dueTime != Timeout.InfiniteTimeSpan && dueTime < TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(dueTime)); + if (period != Timeout.InfiniteTimeSpan && period < TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(period)); + + var cts = new CancellationTokenSource(); + var token = cts.Token; + var oldCts = Interlocked.Exchange(ref _cts, cts); + + try + { + oldCts?.Cancel(); + } + catch (ObjectDisposedException) { } + + oldCts?.Dispose(); + + Start(callback, state, dueTime, period, token); + + // Handle race where Dispose completes after the initial _disposed check + // but before/just after the exchange: ensure the newly created CTS + // is also cancelled and disposed so it doesn't leak or keep firing. + if (_disposed) + { + try + { + cts.Cancel(); + } + catch (ObjectDisposedException) { } + + cts.Dispose(); + } + } + + public bool Change(TimeSpan dueTime, TimeSpan period) + { + Schedule(dueTime, period); + return true; + } + + public bool Change(int dueTime, int period) => + Change(TimeSpan.FromMilliseconds(dueTime), TimeSpan.FromMilliseconds(period)); + + public bool Change(long dueTime, long period) => + Change(TimeSpan.FromMilliseconds(dueTime), TimeSpan.FromMilliseconds(period)); + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _cts?.Cancel(); + _cts?.Dispose(); + } +} +#endif diff --git a/PolyShim/NetCore30/Timer.cs b/PolyShim/NetCore30/Timer.cs index b2ae961..1afc8b5 100644 --- a/PolyShim/NetCore30/Timer.cs +++ b/PolyShim/NetCore30/Timer.cs @@ -1,6 +1,4 @@ #if (NETCOREAPP && !NETCOREAPP3_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD && !NETSTANDARD2_1_OR_GREATER) -// Timer is not available on .NET Standard 1.0 and 1.1 -#if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) #nullable enable // ReSharper disable RedundantUsingDirective // ReSharper disable CheckNamespace @@ -27,4 +25,3 @@ public ValueTask DisposeAsync() } } #endif -#endif diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index 8a19aca..0b398ad 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -1,7 +1,7 @@ # Signatures -- **Total:** 372 -- **Types:** 78 +- **Total:** 373 +- **Types:** 79 - **Members:** 294 ___ @@ -462,6 +462,7 @@ ___ - `TimeProvider` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.timeprovider) .NET 8.0 - `Timer` + - [**[class]**](https://learn.microsoft.com/dotnet/api/system.threading.timer) .NET Core 1.0 - [`ValueTask DisposeAsync()`](https://learn.microsoft.com/dotnet/api/system.threading.timer.disposeasync) .NET Core 3.0 - `TimeSpan` - [`TimeSpan FromMilliseconds(long, long)`](https://learn.microsoft.com/dotnet/api/system.timespan.frommilliseconds#system-timespan-frommilliseconds(system-int64-system-int64)) .NET 9.0