diff --git a/src/libraries/Common/src/System/ITimer.cs b/src/libraries/Common/src/System/Threading/ITimer.cs similarity index 100% rename from src/libraries/Common/src/System/ITimer.cs rename to src/libraries/Common/src/System/Threading/ITimer.cs diff --git a/src/libraries/Common/src/System/TimeProvider.cs b/src/libraries/Common/src/System/TimeProvider.cs index 316cde1dc3768..b8d7fb9fa6aa2 100644 --- a/src/libraries/Common/src/System/TimeProvider.cs +++ b/src/libraries/Common/src/System/TimeProvider.cs @@ -29,7 +29,15 @@ public abstract class TimeProvider /// Frequency of the values returned from method. protected TimeProvider(long timestampFrequency) { +#if SYSTEM_PRIVATE_CORELIB ArgumentOutOfRangeException.ThrowIfNegativeOrZero(timestampFrequency); +#else + if (timestampFrequency <= 0) + { + throw new ArgumentOutOfRangeException(nameof(timestampFrequency), timestampFrequency, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeNonNegativeNonZero, nameof(timestampFrequency))); + } +#endif // SYSTEM_PRIVATE_CORELIB + TimestampFrequency = timestampFrequency; _timeToTicksRatio = (double)TimeSpan.TicksPerSecond / TimestampFrequency; } @@ -41,6 +49,9 @@ protected TimeProvider(long timestampFrequency) /// public abstract DateTimeOffset UtcNow { get; } + private static readonly long s_minDateTicks = DateTime.MinValue.Ticks; + private static readonly long s_maxDateTicks = DateTime.MaxValue.Ticks; + /// /// Gets a value that is set to the current date and time according to this 's /// notion of time based on , with the offset set to the 's offset from Coordinated Universal Time (UTC). @@ -53,9 +64,9 @@ public DateTimeOffset LocalNow TimeSpan offset = LocalTimeZone.GetUtcOffset(utcDateTime); long localTicks = utcDateTime.Ticks + offset.Ticks; - if ((ulong)localTicks > DateTime.MaxTicks) + if ((ulong)localTicks > (ulong)s_maxDateTicks) { - localTicks = localTicks < DateTime.MinTicks ? DateTime.MinTicks : DateTime.MaxTicks; + localTicks = localTicks < s_minDateTicks ? s_minDateTicks : s_maxDateTicks; } return new DateTimeOffset(localTicks, offset); @@ -82,7 +93,15 @@ public DateTimeOffset LocalNow /// is null. public static TimeProvider FromLocalTimeZone(TimeZoneInfo timeZone) { +#if SYSTEM_PRIVATE_CORELIB ArgumentNullException.ThrowIfNull(timeZone); +#else + if (timeZone is null) + { + throw new ArgumentNullException(nameof(timeZone)); + } +#endif // SYSTEM_PRIVATE_CORELIB + return new SystemTimeProvider(timeZone); } @@ -155,7 +174,15 @@ private sealed class SystemTimeProvider : TimeProvider /// public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) { +#if SYSTEM_PRIVATE_CORELIB ArgumentNullException.ThrowIfNull(callback); +#else + if (callback is null) + { + throw new ArgumentNullException(nameof(callback)); + } +#endif // SYSTEM_PRIVATE_CORELIB + return new SystemTimeProviderTimer(dueTime, period, callback, state); } @@ -165,7 +192,7 @@ public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSp /// public override DateTimeOffset UtcNow => DateTimeOffset.UtcNow; - /// Thin wrapper for a . + /// Thin wrapper for a . /// /// We don't return a TimerQueueTimer directly as it implements IThreadPoolWorkItem and we don't /// want it exposed in a way that user code could directly queue the timer to the thread pool. @@ -174,33 +201,85 @@ public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSp /// private sealed class SystemTimeProviderTimer : ITimer { +#if SYSTEM_PRIVATE_CORELIB private readonly TimerQueueTimer _timer; - +#else + private readonly Timer _timer; +#endif // SYSTEM_PRIVATE_CORELIB public SystemTimeProviderTimer(TimeSpan dueTime, TimeSpan period, TimerCallback callback, object? state) { (uint duration, uint periodTime) = CheckAndGetValues(dueTime, period); +#if SYSTEM_PRIVATE_CORELIB _timer = new TimerQueueTimer(callback, state, duration, periodTime, flowExecutionContext: true); +#else + // We want to ensure the timer we create will be tracked as long as it is scheduled. + // To do that, we call the constructor which track only the callback which will make the time to be tracked by the scheduler + // then we call Change on the timer to set the desired duration and period. + _timer = new Timer(_ => callback(state)); + _timer.Change(duration, periodTime); +#endif // SYSTEM_PRIVATE_CORELIB } public bool Change(TimeSpan dueTime, TimeSpan period) { (uint duration, uint periodTime) = CheckAndGetValues(dueTime, period); - return _timer.Change(duration, periodTime); + try + { + return _timer.Change(duration, periodTime); + } + catch (ObjectDisposedException) + { + return false; + } } public void Dispose() => _timer.Dispose(); + +#if SYSTEM_PRIVATE_CORELIB public ValueTask DisposeAsync() => _timer.DisposeAsync(); +#else + public ValueTask DisposeAsync() + { + _timer.Dispose(); + return default; + } +#endif // SYSTEM_PRIVATE_CORELIB private static (uint duration, uint periodTime) CheckAndGetValues(TimeSpan dueTime, TimeSpan periodTime) { long dueTm = (long)dueTime.TotalMilliseconds; + long periodTm = (long)periodTime.TotalMilliseconds; + +#if SYSTEM_PRIVATE_CORELIB ArgumentOutOfRangeException.ThrowIfLessThan(dueTm, -1, nameof(dueTime)); ArgumentOutOfRangeException.ThrowIfGreaterThan(dueTm, Timer.MaxSupportedTimeout, nameof(dueTime)); - long periodTm = (long)periodTime.TotalMilliseconds; ArgumentOutOfRangeException.ThrowIfLessThan(periodTm, -1, nameof(periodTime)); ArgumentOutOfRangeException.ThrowIfGreaterThan(periodTm, Timer.MaxSupportedTimeout, nameof(periodTime)); +#else + const uint MaxSupportedTimeout = 0xfffffffe; + + if (dueTm < -1) + { + throw new ArgumentOutOfRangeException(nameof(dueTime), dueTm, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeGreaterOrEqual, nameof(dueTime), -1)); + } + + if (dueTm > MaxSupportedTimeout) + { + throw new ArgumentOutOfRangeException(nameof(dueTime), dueTm, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeLessOrEqual, nameof(dueTime), MaxSupportedTimeout)); + } + + if (periodTm < -1) + { + throw new ArgumentOutOfRangeException(nameof(periodTm), periodTm, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeGreaterOrEqual, nameof(periodTm), -1)); + } + + if (periodTm > MaxSupportedTimeout) + { + throw new ArgumentOutOfRangeException(nameof(periodTm), periodTm, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeLessOrEqual, nameof(periodTm), MaxSupportedTimeout)); + } +#endif // SYSTEM_PRIVATE_CORELIB return ((uint)dueTm, (uint)periodTm); } diff --git a/src/libraries/Common/tests/Tests/System/TimeProviderTests.cs b/src/libraries/Common/tests/System/TimeProviderTests.cs similarity index 71% rename from src/libraries/Common/tests/Tests/System/TimeProviderTests.cs rename to src/libraries/Common/tests/System/TimeProviderTests.cs index 8b00756367d88..2f169512adabd 100644 --- a/src/libraries/Common/tests/Tests/System/TimeProviderTests.cs +++ b/src/libraries/Common/tests/System/TimeProviderTests.cs @@ -44,7 +44,11 @@ public void TestSystemProviderWithTimeZone() { Assert.Equal(TimeZoneInfo.Local.Id, TimeProvider.System.LocalTimeZone.Id); +#if NETFRAMEWORK + TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); +#else TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById(OperatingSystem.IsWindows() ? "Pacific Standard Time" : "America/Los_Angeles"); +#endif // NETFRAMEWORK TimeProvider tp = TimeProvider.FromLocalTimeZone(tzi); Assert.Equal(tzi.Id, tp.LocalTimeZone.Id); @@ -57,6 +61,15 @@ public void TestSystemProviderWithTimeZone() Assert.InRange(utcConvertedDto.Ticks, utcDto1.Ticks, utcDto2.Ticks); } +#if NETFRAMEWORK + public static double s_tickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency; + public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => + new TimeSpan((long)((endingTimestamp - startingTimestamp) * s_tickFrequency)); +#else + public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => + Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp); +#endif // NETFRAMEWORK + [Fact] public void TestSystemTimestamp() { @@ -68,7 +81,7 @@ public void TestSystemTimestamp() Assert.InRange(providerTimestamp1, timestamp1, timestamp2); Assert.True(providerTimestamp2 > timestamp2); - Assert.Equal(Stopwatch.GetElapsedTime(providerTimestamp1, providerTimestamp2), TimeProvider.System.GetElapsedTime(providerTimestamp1, providerTimestamp2)); + Assert.Equal(GetElapsedTime(providerTimestamp1, providerTimestamp2), TimeProvider.System.GetElapsedTime(providerTimestamp1, providerTimestamp2)); Assert.Equal(Stopwatch.Frequency, TimeProvider.System.TimestampFrequency); } @@ -111,7 +124,7 @@ public void TestProviderTimer(TimeProvider provider, int MaxMilliseconds) state, TimeSpan.FromMilliseconds(state.Period), TimeSpan.FromMilliseconds(state.Period)); - state.TokenSource.Token.WaitHandle.WaitOne(30000); + state.TokenSource.Token.WaitHandle.WaitOne(60000); state.TokenSource.Dispose(); Assert.Equal(4, state.Counter); @@ -151,6 +164,32 @@ public static IEnumerable TimersProvidersListData() yield return new object[] { new FastClock() }; } + public static IEnumerable TimersProvidersWithTaskFactorData() + { + yield return new object[] { TimeProvider.System, taskFactory}; + yield return new object[] { new FastClock(), taskFactory }; + +#if TESTEXTENSIONS + yield return new object[] { TimeProvider.System, extensionsTaskFactory}; + yield return new object[] { new FastClock(), extensionsTaskFactory }; +#endif // TESTEXTENSIONS + } + +#if NETFRAMEWORK + private static void CancelAfter(TimeProvider provider, CancellationTokenSource cts, TimeSpan delay) + { + if (provider == TimeProvider.System) + { + cts.CancelAfter(delay); + } + else + { + ITimer timer = provider.CreateTimer(s => ((CancellationTokenSource)s).Cancel(), cts, delay, Timeout.InfiniteTimeSpan); + cts.Token.Register(t => ((ITimer)t).Dispose(), timer); + } + } +#endif // NETFRAMEWORK + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] [MemberData(nameof(TimersProvidersListData))] public static void CancellationTokenSourceWithTimer(TimeProvider provider) @@ -158,7 +197,11 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) // // Test out some int-based timeout logic // +#if NETFRAMEWORK + CancellationTokenSource cts = new CancellationTokenSource(Timeout.InfiniteTimeSpan); // should be an infinite timeout +#else CancellationTokenSource cts = new CancellationTokenSource(Timeout.InfiniteTimeSpan, provider); // should be an infinite timeout +#endif // NETFRAMEWORK CancellationToken token = cts.Token; ManualResetEventSlim mres = new ManualResetEventSlim(false); CancellationTokenRegistration ctr = token.Register(() => mres.Set()); @@ -166,12 +209,20 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) Assert.False(token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on infinite timeout (int)!"); +#if NETFRAMEWORK + CancelAfter(provider, cts, TimeSpan.FromMilliseconds(1000000)); +#else cts.CancelAfter(1000000); +#endif // NETFRAMEWORK Assert.False(token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (int) !"); +#if NETFRAMEWORK + CancelAfter(provider, cts, TimeSpan.FromMilliseconds(1)); +#else cts.CancelAfter(1); +#endif // NETFRAMEWORK Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (int)... if we hang, something bad happened"); @@ -183,7 +234,12 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) // Test out some TimeSpan-based timeout logic // TimeSpan prettyLong = new TimeSpan(1, 0, 0); +#if NETFRAMEWORK + cts = new CancellationTokenSource(prettyLong); +#else cts = new CancellationTokenSource(prettyLong, provider); +#endif // NETFRAMEWORK + token = cts.Token; mres = new ManualResetEventSlim(false); ctr = token.Register(() => mres.Set()); @@ -191,12 +247,20 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) Assert.False(token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (TimeSpan,1)!"); +#if NETFRAMEWORK + CancelAfter(provider, cts, prettyLong); +#else cts.CancelAfter(prettyLong); +#endif // NETFRAMEWORK Assert.False(token.IsCancellationRequested, "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (TimeSpan,2) !"); +#if NETFRAMEWORK + CancelAfter(provider, cts, TimeSpan.FromMilliseconds(1000)); +#else cts.CancelAfter(new TimeSpan(1000)); +#endif // NETFRAMEWORK Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (TimeSpan)... if we hang, something bad happened"); @@ -206,15 +270,15 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider) } [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] - [MemberData(nameof(TimersProvidersListData))] - public static void RunDelayTests(TimeProvider provider) + [MemberData(nameof(TimersProvidersWithTaskFactorData))] + public static void RunDelayTests(TimeProvider provider, ITestTaskFactory taskFactory) { CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken token = cts.Token; // These should all complete quickly, with RAN_TO_COMPLETION status. - Task task1 = Task.Delay(new TimeSpan(0), provider); - Task task2 = Task.Delay(new TimeSpan(0), provider, token); + Task task1 = taskFactory.Delay(provider, new TimeSpan(0)); + Task task2 = taskFactory.Delay(provider, new TimeSpan(0), token); Debug.WriteLine("RunDelayTests: > Waiting for 0-delayed uncanceled tasks to complete. If we hang, something went wrong."); try @@ -230,67 +294,69 @@ public static void RunDelayTests(TimeProvider provider) Assert.True(task2.Status == TaskStatus.RanToCompletion, " > FAILED. Expected Delay(TimeSpan(0), timeProvider, uncanceledToken) to run to completion"); // This should take some time - Task task3 = Task.Delay(TimeSpan.FromMilliseconds(20000), provider); + Task task3 = taskFactory.Delay(provider, TimeSpan.FromMilliseconds(20000)); + Assert.False(task3.IsCompleted, "RunDelayTests: > FAILED. Delay(20000) appears to have completed too soon(1)."); Task t2 = Task.Delay(TimeSpan.FromMilliseconds(10)); Assert.False(task3.IsCompleted, "RunDelayTests: > FAILED. Delay(10000) appears to have completed too soon(2)."); } [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] - [MemberData(nameof(TimersProvidersListData))] - public static async void RunWaitAsyncTests(TimeProvider provider) + [MemberData(nameof(TimersProvidersWithTaskFactorData))] + public static async void RunWaitAsyncTests(TimeProvider provider, ITestTaskFactory taskFactory) { CancellationTokenSource cts = new CancellationTokenSource(); - var tcs1 = new TaskCompletionSource(); - Task task1 = tcs1.Task.WaitAsync(TimeSpan.FromDays(1), provider); + var tcs1 = new TaskCompletionSource(); + Task task1 = taskFactory.WaitAsync(tcs1.Task, TimeSpan.FromDays(1), provider); Assert.False(task1.IsCompleted); - tcs1.SetResult(); + tcs1.SetResult(true); await task1; - var tcs2 = new TaskCompletionSource(); - Task task2 = tcs2.Task.WaitAsync(TimeSpan.FromDays(1), provider, cts.Token); + var tcs2 = new TaskCompletionSource(); + Task task2 = taskFactory.WaitAsync(tcs2.Task, TimeSpan.FromDays(1), provider, cts.Token); Assert.False(task2.IsCompleted); - tcs2.SetResult(); + tcs2.SetResult(true); await task2; var tcs3 = new TaskCompletionSource(); - Task task3 = tcs3.Task.WaitAsync(TimeSpan.FromDays(1), provider); + Task task3 = taskFactory.WaitAsync(tcs3.Task, TimeSpan.FromDays(1), provider); Assert.False(task3.IsCompleted); tcs3.SetResult(42); Assert.Equal(42, await task3); var tcs4 = new TaskCompletionSource(); - Task task4 = tcs4.Task.WaitAsync(TimeSpan.FromDays(1), provider, cts.Token); + Task task4 = taskFactory.WaitAsync(tcs4.Task, TimeSpan.FromDays(1), provider, cts.Token); Assert.False(task4.IsCompleted); tcs4.SetResult(42); Assert.Equal(42, await task4); using CancellationTokenSource cts1 = new CancellationTokenSource(); Task task5 = Task.Run(() => { while (!cts1.Token.IsCancellationRequested) { Thread.Sleep(10); } }); - await Assert.ThrowsAsync(() => task5.WaitAsync(TimeSpan.FromMilliseconds(10), provider)); + await Assert.ThrowsAsync(() => taskFactory.WaitAsync(task5, TimeSpan.FromMilliseconds(10), provider)); cts1.Cancel(); await task5; using CancellationTokenSource cts2 = new CancellationTokenSource(); Task task6 = Task.Run(() => { while (!cts2.Token.IsCancellationRequested) { Thread.Sleep(10); } }); - await Assert.ThrowsAsync(() => task6.WaitAsync(TimeSpan.FromMilliseconds(10), provider, cts2.Token)); + await Assert.ThrowsAsync(() => taskFactory.WaitAsync(task6, TimeSpan.FromMilliseconds(10), provider, cts2.Token)); cts1.Cancel(); await task5; using CancellationTokenSource cts3 = new CancellationTokenSource(); Task task7 = Task.Run(() => { while (!cts3.Token.IsCancellationRequested) { Thread.Sleep(10); } return 100; }); - await Assert.ThrowsAsync(() => task7.WaitAsync(TimeSpan.FromMilliseconds(10), provider)); + await Assert.ThrowsAsync(() => taskFactory.WaitAsync(task7, TimeSpan.FromMilliseconds(10), provider)); cts3.Cancel(); Assert.Equal(100, await task7); using CancellationTokenSource cts4 = new CancellationTokenSource(); Task task8 = Task.Run(() => { while (!cts4.Token.IsCancellationRequested) { Thread.Sleep(10); } return 200; }); - await Assert.ThrowsAsync(() => task8.WaitAsync(TimeSpan.FromMilliseconds(10), provider, cts4.Token)); + await Assert.ThrowsAsync(() => taskFactory.WaitAsync(task8, TimeSpan.FromMilliseconds(10), provider, cts4.Token)); cts4.Cancel(); Assert.Equal(200, await task8); } +#if !NETFRAMEWORK [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] [MemberData(nameof(TimersProvidersListData))] public static async void PeriodicTimerTests(TimeProvider provider) @@ -304,6 +370,7 @@ public static async void PeriodicTimerTests(TimeProvider provider) timer.Dispose(); Assert.False(timer.WaitForNextTickAsync().Result); } +#endif // !NETFRAMEWORK [Fact] public static void NegativeTests() @@ -316,9 +383,11 @@ public static void NegativeTests() Assert.Throws(() => TimeProvider.System.CreateTimer(obj => { }, null, TimeSpan.FromMilliseconds(-2), Timeout.InfiniteTimeSpan)); Assert.Throws(() => TimeProvider.System.CreateTimer(obj => { }, null, Timeout.InfiniteTimeSpan, TimeSpan.FromMilliseconds(-2))); +#if !NETFRAMEWORK Assert.Throws(() => new CancellationTokenSource(Timeout.InfiniteTimeSpan, null)); Assert.Throws(() => new PeriodicTimer(TimeSpan.FromMilliseconds(1), null)); +#endif // !NETFRAMEWORK } class TimerState @@ -414,7 +483,59 @@ public bool Change(TimeSpan dueTime, TimeSpan period) } public void Dispose() => _timer.Dispose(); + +#if NETFRAMEWORK + public ValueTask DisposeAsync() + { + _timer.Dispose(); + return default; + } +#else public ValueTask DisposeAsync() => _timer.DisposeAsync(); +#endif // NETFRAMEWORK + } + + public interface ITestTaskFactory + { + Task Delay(TimeProvider provider, TimeSpan delay, CancellationToken cancellationToken = default); + Task WaitAsync(Task task, TimeSpan timeout, TimeProvider provider, CancellationToken cancellationToken = default); + Task WaitAsync(Task task, TimeSpan timeout, TimeProvider provider, CancellationToken cancellationToken = default); + } + + private class TestTaskFactory : ITestTaskFactory + { + public Task Delay(TimeProvider provider, TimeSpan delay, CancellationToken cancellationToken = default) + { +#if NETFRAMEWORK + return provider.Delay(delay, cancellationToken); +#else + return Task.Delay(delay, provider, cancellationToken); +#endif // NETFRAMEWORK + } + + public Task WaitAsync(Task task, TimeSpan timeout, TimeProvider provider, CancellationToken cancellationToken = default) + => task.WaitAsync(timeout, provider, cancellationToken); + + public Task WaitAsync(Task task, TimeSpan timeout, TimeProvider provider, CancellationToken cancellationToken = default) + => task.WaitAsync(timeout, provider, cancellationToken); } + + private static TestTaskFactory taskFactory = new(); + +#if TESTEXTENSIONS + private class TestExtensionsTaskFactory : ITestTaskFactory + { + public Task Delay(TimeProvider provider, TimeSpan delay, CancellationToken cancellationToken = default) + => TimeProviderTaskExtensions.Delay(provider, delay, cancellationToken); + + public Task WaitAsync(Task task, TimeSpan timeout, TimeProvider provider, CancellationToken cancellationToken = default) + => TimeProviderTaskExtensions.WaitAsync(task, timeout, provider, cancellationToken); + + public Task WaitAsync(Task task, TimeSpan timeout, TimeProvider provider, CancellationToken cancellationToken = default) + => TimeProviderTaskExtensions.WaitAsync(task, timeout, provider, cancellationToken); + } + + private static TestExtensionsTaskFactory extensionsTaskFactory = new(); +#endif // TESTEXTENSIONS } } diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/Microsoft.Bcl.TimeProvider.sln b/src/libraries/Microsoft.Bcl.TimeProvider/Microsoft.Bcl.TimeProvider.sln new file mode 100644 index 0000000000000..e04f2b057d036 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/Microsoft.Bcl.TimeProvider.sln @@ -0,0 +1,41 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{7E9F6DE1-771B-4E25-A603-EC43D0291C8B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.TimeProvider", "ref\Microsoft.Bcl.TimeProvider.csproj", "{63655B2E-6A06-4E48-9F01-D0B910063165}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{98708A22-7268-4EDB-AE37-70AA958A772A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.TimeProvider", "src\Microsoft.Bcl.TimeProvider.csproj", "{B0716D7E-B824-4866-A1ED-DF31BA2970B9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{8C3BD4AD-1A56-4204-9826-F8B74251D19F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.TimeProvider.Tests", "tests\Microsoft.Bcl.TimeProvider.Tests.csproj", "{E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {63655B2E-6A06-4E48-9F01-D0B910063165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63655B2E-6A06-4E48-9F01-D0B910063165}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63655B2E-6A06-4E48-9F01-D0B910063165}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63655B2E-6A06-4E48-9F01-D0B910063165}.Release|Any CPU.Build.0 = Release|Any CPU + {B0716D7E-B824-4866-A1ED-DF31BA2970B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0716D7E-B824-4866-A1ED-DF31BA2970B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0716D7E-B824-4866-A1ED-DF31BA2970B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0716D7E-B824-4866-A1ED-DF31BA2970B9}.Release|Any CPU.Build.0 = Release|Any CPU + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {63655B2E-6A06-4E48-9F01-D0B910063165} = {7E9F6DE1-771B-4E25-A603-EC43D0291C8B} + {B0716D7E-B824-4866-A1ED-DF31BA2970B9} = {98708A22-7268-4EDB-AE37-70AA958A772A} + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191} = {8C3BD4AD-1A56-4204-9826-F8B74251D19F} + EndGlobalSection +EndGlobal diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Common.cs b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Common.cs new file mode 100644 index 0000000000000..627405d04ac53 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Common.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading.Tasks +{ + public static class TimeProviderTaskExtensions + { + public static System.Threading.Tasks.Task Delay(this System.TimeProvider timeProvider, System.TimeSpan delay, System.Threading.CancellationToken cancellationToken = default) { throw null; } + public static System.Threading.Tasks.Task WaitAsync(this System.Threading.Tasks.Task task, System.TimeSpan timeout, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken = default) { throw null; } + public static System.Threading.Tasks.Task WaitAsync(this System.Threading.Tasks.Task task, System.TimeSpan timeout, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken = default) { throw null; } + } +} diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Forwards.cs b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Forwards.cs new file mode 100644 index 0000000000000..cfdda781b4248 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.Forwards.cs @@ -0,0 +1,5 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.TimeProvider))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Threading.ITimer))] diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.cs b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.cs new file mode 100644 index 0000000000000..3d11ef6fa5418 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// ------------------------------------------------------------------------------ +// Changes to this file must follow the https://aka.ms/api-review process. +// ------------------------------------------------------------------------------ +namespace System +{ + public abstract class TimeProvider + { + public static TimeProvider System { get; } + protected TimeProvider(long timestampFrequency) { throw null; } + public abstract System.DateTimeOffset UtcNow { get; } + public System.DateTimeOffset LocalNow { get; } + public abstract System.TimeZoneInfo LocalTimeZone { get; } + public long TimestampFrequency { get; } + public static TimeProvider FromLocalTimeZone(System.TimeZoneInfo timeZone) { throw null; } + public abstract long GetTimestamp(); + public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) { throw null; } + public abstract System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period); + } +} +namespace System.Threading +{ + public interface ITimer : System.IDisposable, System.IAsyncDisposable + { + bool Change(System.TimeSpan dueTime, System.TimeSpan period); + } +} diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.csproj b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.csproj new file mode 100644 index 0000000000000..11d55537a3a3b --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/ref/Microsoft.Bcl.TimeProvider.csproj @@ -0,0 +1,18 @@ + + + $(NetCoreAppCurrent);netstandard2.0;$(NetFrameworkMinimum) + + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj b/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj new file mode 100644 index 0000000000000..4ecd0ecb26f80 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/src/Microsoft.Bcl.TimeProvider.csproj @@ -0,0 +1,38 @@ + + + $(NetCoreAppCurrent);netstandard2.0;$(NetFrameworkMinimum) + true + + true + + Provides support for system time abstraction primitives for .NET Framework and .NET Standard. + +Commonly Used Types: +System.TimeProvider +System.ITimer + + + + + + true + true + + + + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/src/Resources/Strings.resx b/src/libraries/Microsoft.Bcl.TimeProvider/src/Resources/Strings.resx new file mode 100644 index 0000000000000..48056b82ad12a --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/src/Resources/Strings.resx @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + '{0}' must be a non-negative and non-zero value. + + + '{0}' must be greater than or equal to '{1}'. + + + '{0}' must be less than or equal to '{1}'. + + diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs b/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs new file mode 100644 index 0000000000000..3a6fa1debbb67 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading.Tasks +{ + /// + /// Provide extensions methods for operations with . + /// + public static class TimeProviderTaskExtensions + { +#if !NET8_0_OR_GREATER + private sealed class DelayState : TaskCompletionSource + { + public DelayState() : base(TaskCreationOptions.RunContinuationsAsynchronously) {} + public ITimer Timer { get; set; } + public CancellationTokenRegistration Registration { get; set; } + } + + private sealed class WaitAsyncState : TaskCompletionSource + { + public WaitAsyncState() : base(TaskCreationOptions.RunContinuationsAsynchronously) { } + public readonly CancellationTokenSource ContinuationCancellation = new CancellationTokenSource(); + public CancellationTokenRegistration Registration; + public ITimer? Timer; + } +#endif // !NET8_0_OR_GREATER + + /// Creates a task that completes after a specified time interval. + /// The with which to interpret . + /// The to wait before completing the returned task, or to wait indefinitely. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the time delay. + /// The argument is null. + /// represents a negative time interval other than . + public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, CancellationToken cancellationToken = default) + { +#if NET8_0_OR_GREATER + return Task.Delay(delay, timeProvider, cancellationToken); +#else + if (timeProvider == TimeProvider.System) + { + return Task.Delay(delay, cancellationToken); + } + + if (timeProvider is null) + { + throw new ArgumentNullException(nameof(timeProvider)); + } + + if (delay != Timeout.InfiniteTimeSpan && delay < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(delay)); + } + + if (delay == TimeSpan.Zero) + { + return Task.CompletedTask; + } + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + DelayState state = new(); + + // To prevent a race condition where the timer may fire before being assigned to s.Timer, + // we initialize it with an InfiniteTimeSpan and then set it to the state variable, followed by calling Time.Change. + state.Timer = timeProvider.CreateTimer(delayState => + { + DelayState s = (DelayState)delayState; + s.TrySetResult(true); + s.Registration.Dispose(); + s.Timer.Dispose(); + }, state, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + + state.Timer.Change(delay, Timeout.InfiniteTimeSpan); + + state.Registration = cancellationToken.Register(delayState => + { + DelayState s = (DelayState)delayState; + s.TrySetCanceled(cancellationToken); + s.Timer.Dispose(); + s.Registration.Dispose(); + }, state); + + // To prevent a race condition where the timer fires after we have attached the cancellation callback + // but before the registration is stored in state.Registration, we perform a subsequent check to ensure + // that the registration is not left dangling. + if (state.Task.IsCompleted) + { + state.Registration.Dispose(); + } + + return state.Task; +#endif // NET8_0_OR_GREATER + } + + /// + /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested. + /// + /// The task for which to wait on until completion. + /// The timeout after which the should be faulted with a if it hasn't otherwise completed. + /// The with which to interpret . + /// The to monitor for a cancellation request. + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + /// The argument is null. + /// The argument is null. + /// represents a negative time interval other than . + public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) + { +#if NET8_0_OR_GREATER + return task.WaitAsync(timeout, timeProvider, cancellationToken); +#else + if (task is null) + { + throw new ArgumentNullException(nameof(task)); + } + + if (timeout != Timeout.InfiniteTimeSpan && timeout < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(timeout)); + } + + if (timeProvider is null) + { + throw new ArgumentNullException(nameof(timeProvider)); + } + + if (task.IsCompleted) + { + return task; + } + + if (timeout == Timeout.InfiniteTimeSpan && !cancellationToken.CanBeCanceled) + { + return task; + } + + if (timeout == TimeSpan.Zero) + { + Task.FromException(new TimeoutException()); + } + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + var state = new WaitAsyncState(); + + // To prevent a race condition where the timer may fire before being assigned to s.Timer, + // we initialize it with an InfiniteTimeSpan and then set it to the state variable, followed by calling Time.Change. + state.Timer = timeProvider.CreateTimer(static s => + { + var state = (WaitAsyncState)s!; + + state.TrySetException(new TimeoutException()); + + state.Registration.Dispose(); + state.Timer!.Dispose(); + state.ContinuationCancellation.Cancel(); + }, state, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + state.Timer.Change(timeout, Timeout.InfiniteTimeSpan); + + _ = task.ContinueWith(static (t, s) => + { + var state = (WaitAsyncState)s!; + + if (t.IsFaulted) state.TrySetException(t.Exception.InnerExceptions); + else if (t.IsCanceled) state.TrySetCanceled(); + else state.TrySetResult(true); + + state.Registration.Dispose(); + state.Timer?.Dispose(); + }, state, state.ContinuationCancellation.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + + state.Registration = cancellationToken.Register(static s => + { + var state = (WaitAsyncState)s!; + + state.TrySetCanceled(); + + state.Timer?.Dispose(); + state.ContinuationCancellation.Cancel(); + }, state); + + // To prevent a race condition where the timer fires after we have attached the cancellation callback + // but before the registration is stored in state.Registration, we perform a subsequent check to ensure + // that the registration is not left dangling. + if (state.Task.IsCompleted) + { + state.Registration.Dispose(); + } + + return state.Task; +#endif // NET8_0_OR_GREATER + } + + /// + /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested. + /// + /// The task for which to wait on until completion. + /// The timeout after which the should be faulted with a if it hasn't otherwise completed. + /// The with which to interpret . + /// The to monitor for a cancellation request. + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + /// The argument is null. + /// The argument is null. + /// represents a negative time interval other than . +#if NET8_0_OR_GREATER + public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) + => task.WaitAsync(timeout, timeProvider, cancellationToken); +#else + public static async Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) + { + await ((Task)task).WaitAsync(timeout, timeProvider, cancellationToken).ConfigureAwait(false); + return task.Result; + } +#endif // NET8_0_OR_GREATER + } +} diff --git a/src/libraries/Microsoft.Bcl.TimeProvider/tests/Microsoft.Bcl.TimeProvider.Tests.csproj b/src/libraries/Microsoft.Bcl.TimeProvider/tests/Microsoft.Bcl.TimeProvider.Tests.csproj new file mode 100644 index 0000000000000..f7e1b14dc4a66 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.TimeProvider/tests/Microsoft.Bcl.TimeProvider.Tests.csproj @@ -0,0 +1,14 @@ + + + $(NetCoreAppCurrent);$(NetFrameworkMinimum) + $(DefineConstants);TESTEXTENSIONS + + + + + + + + + + diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 79a84e6da1840..e216ff5b01529 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1322,9 +1322,6 @@ Common\SkipLocalsInit.cs - - Common\System\ITimer.cs - Common\System\TimeProvider.cs @@ -1385,6 +1382,9 @@ Common\System\Text\ValueStringBuilder.AppendSpanFormattable.cs + + Common\System\Threading\ITimer.cs + Common\System\Threading\OpenExistingResult.cs diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj index 266ac502fa531..82ff64b861d30 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj @@ -1,7 +1,7 @@  $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-browser - $(DefineConstants);TARGET_BROWSER + $(DefineConstants);TARGET_BROWSER true $(NoWarn),1718,SYSLIB0013 true @@ -41,8 +41,8 @@ + -