Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions PolyShim/Net80/TimeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -90,6 +87,5 @@ public void Dispose()
public ValueTask DisposeAsync() => _timer.DisposeAsync();
#endif
}
#endif
}
#endif
119 changes: 119 additions & 0 deletions PolyShim/NetCore10/Timer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#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 : IDisposable
{
private readonly TimerCallback _callback;
private readonly object? _state;
private CancellationTokenSource _cts = new CancellationTokenSource();
private volatile bool _disposed;

public Timer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
Comment thread
Tyrrrz marked this conversation as resolved.
{
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated
_callback = callback;
_state = state;
Schedule(dueTime, period);
}
Comment thread
Tyrrrz marked this conversation as resolved.

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 void Schedule(TimeSpan dueTime, TimeSpan period)
{
if (_disposed)
throw new ObjectDisposedException(nameof(Timer));

var cts = new CancellationTokenSource();
var oldCts = Interlocked.Exchange(ref _cts, cts);
oldCts.Cancel();
oldCts.Dispose();

if (dueTime == Timeout.InfiniteTimeSpan)
return;

_ = Task.Run(async () =>
{
try
{
if (dueTime > TimeSpan.Zero)
await Task.Delay(dueTime, cts.Token);

if (cts.IsCancellationRequested)
return;

_callback(_state);

if (period == Timeout.InfiniteTimeSpan || period <= TimeSpan.Zero)
return;

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

period <= TimeSpan.Zero is treated as “no periodic callbacks”. In the BCL, period == TimeSpan.Zero is valid and schedules callbacks back-to-back, while negative values (other than -1 / Infinite) should throw. Similarly, negative dueTime values (other than Infinite) should throw rather than being treated as immediate execution.

Copilot uses AI. Check for mistakes.

while (!cts.IsCancellationRequested)
{
await Task.Delay(period, cts.Token);

if (cts.IsCancellationRequested)
return;

_callback(_state);
}
}
catch (OperationCanceledException) { }
});
Comment thread
Tyrrrz marked this conversation as resolved.
}

public bool Change(TimeSpan dueTime, TimeSpan period)
{
try
{
Schedule(dueTime, period);
return true;
}
catch
{
return false;
}
Comment thread
Tyrrrz marked this conversation as resolved.
Comment thread
Tyrrrz marked this conversation as resolved.
}

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;
Comment thread
Tyrrrz marked this conversation as resolved.
var cts = Interlocked.Exchange(ref _cts, new CancellationTokenSource());
cts.Cancel();
cts.Dispose();

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dispose() swaps _cts with a newly-created CancellationTokenSource and never disposes that new instance. This leaves an undisposed CTS (and potentially its wait handle) rooted by the Timer instance. Consider setting _cts to null (making the field nullable) or reusing/discarding the existing CTS without allocating a replacement.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address this please

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address this please

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fed084cDispose now cancels and disposes _cts directly without allocating a replacement.

}
Comment thread
Tyrrrz marked this conversation as resolved.
}
#endif
3 changes: 0 additions & 3 deletions PolyShim/NetCore30/Timer.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -27,4 +25,3 @@ public ValueTask DisposeAsync()
}
}
#endif
#endif
5 changes: 3 additions & 2 deletions PolyShim/Signatures.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Signatures

- **Total:** 372
- **Types:** 78
- **Total:** 373
- **Types:** 79
- **Members:** 294

___
Expand Down Expand Up @@ -462,6 +462,7 @@ ___
- `TimeProvider`
- [**[class]**](https://learn.microsoft.com/dotnet/api/system.timeprovider) <sup><sub>.NET 8.0</sub></sup>
- `Timer`
- [**[class]**](https://learn.microsoft.com/dotnet/api/system.threading.timer) <sup><sub>.NET Core 1.0</sub></sup>
- [`ValueTask DisposeAsync()`](https://learn.microsoft.com/dotnet/api/system.threading.timer.disposeasync) <sup><sub>.NET Core 3.0</sub></sup>
- `TimeSpan`
- [`TimeSpan FromMilliseconds(long, long)`](https://learn.microsoft.com/dotnet/api/system.timespan.frommilliseconds#system-timespan-frommilliseconds(system-int64-system-int64)) <sup><sub>.NET 9.0</sub></sup>
Expand Down
Loading