// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT License. // See the LICENSE file in the project root for more information. using System.Collections.Generic; using System.ComponentModel; using System.Reactive.Concurrency; using System.Threading; namespace System.Reactive.PlatformServices { /// /// (Infrastructure) Provides access to local system clock services. /// /// /// This type is used by the Rx infrastructure and not meant for public consumption or implementation. /// No guarantees are made about forward compatibility of the type's functionality and its usage. /// [EditorBrowsable(EditorBrowsableState.Never)] public static class SystemClock { private static readonly Lazy ServiceSystemClock = new(InitializeSystemClock); private static readonly Lazy ServiceSystemClockChanged = new(InitializeSystemClockChanged); internal static readonly HashSet> SystemClockChanged = new(); private static IDisposable? _systemClockChangedHandlerCollector; private static int _refCount; /// /// Gets the local system clock time. /// public static DateTimeOffset UtcNow => ServiceSystemClock.Value.UtcNow; /// /// Adds a reference to the system clock monitor, causing it to be sending notifications. /// /// Thrown when the system doesn't support sending clock change notifications. public static void AddRef() { if (Interlocked.Increment(ref _refCount) == 1) { ServiceSystemClockChanged.Value.SystemClockChanged += OnSystemClockChanged; } } /// /// Removes a reference to the system clock monitor, causing it to stop sending notifications /// if the removed reference was the last one. /// public static void Release() { if (Interlocked.Decrement(ref _refCount) == 0) { ServiceSystemClockChanged.Value.SystemClockChanged -= OnSystemClockChanged; } } internal static void OnSystemClockChanged(object? sender, SystemClockChangedEventArgs e) { lock (SystemClockChanged) { // create a defensive copy as the callbacks may change the hashset var copySystemClockChanged = new List>(SystemClockChanged); foreach (var entry in copySystemClockChanged) { if (entry.TryGetTarget(out var scheduler)) { scheduler.SystemClockChanged(sender, e); } } } } private static ISystemClock InitializeSystemClock() { return PlatformEnlightenmentProvider.Current.GetService() ?? new DefaultSystemClock(); } private static INotifySystemClockChanged InitializeSystemClockChanged() { return PlatformEnlightenmentProvider.Current.GetService() ?? new DefaultSystemClockMonitor(); } internal static void Register(LocalScheduler scheduler) { // // LocalScheduler maintains per-instance work queues that need revisiting // upon system clock changes. We need to be careful to avoid keeping those // scheduler instances alive by the system clock monitor, so we use weak // references here. In particular, AsyncLockScheduler in ImmediateScheduler // can have a lot of instances, so we need to collect spurious handlers // at regular times. // lock (SystemClockChanged) { SystemClockChanged.Add(new WeakReference(scheduler)); if (SystemClockChanged.Count == 1) { _systemClockChangedHandlerCollector = ConcurrencyAbstractionLayer.Current.StartPeriodicTimer(CollectHandlers, TimeSpan.FromSeconds(30)); } else if (SystemClockChanged.Count % 64 == 0) { CollectHandlers(); } } } private static void CollectHandlers() { // // The handler collector merely collects the WeakReference instances // that are kept in the hash set. The underlying scheduler itself will // be collected due to the weak reference. Unfortunately, we can't use // the ConditionalWeakTable type here because we need to // be able to enumerate the keys. // lock (SystemClockChanged) { HashSet>? remove = null; foreach (var handler in SystemClockChanged) { if (!handler.TryGetTarget(out _)) { remove ??= new HashSet>(); remove.Add(handler); } } if (remove != null) { foreach (var handler in remove) { SystemClockChanged.Remove(handler); } } if (SystemClockChanged.Count == 0) { _systemClockChangedHandlerCollector?.Dispose(); _systemClockChangedHandlerCollector = null; } } } } /// /// (Infrastructure) Provides access to the local system clock. /// /// /// This type is used by the Rx infrastructure and not meant for public consumption or implementation. /// No guarantees are made about forward compatibility of the type's functionality and its usage. /// [EditorBrowsable(EditorBrowsableState.Never)] public interface ISystemClock { /// /// Gets the current time. /// DateTimeOffset UtcNow { get; } } /// /// (Infrastructure) Provides a mechanism to notify local schedulers about system clock changes. /// /// /// This type is used by the Rx infrastructure and not meant for public consumption or implementation. /// No guarantees are made about forward compatibility of the type's functionality and its usage. /// [EditorBrowsable(EditorBrowsableState.Never)] public interface INotifySystemClockChanged { /// /// Event that gets raised when a system clock change is detected. /// event EventHandler SystemClockChanged; } /// /// (Infrastructure) Event arguments for system clock change notifications. /// /// /// This type is used by the Rx infrastructure and not meant for public consumption or implementation. /// No guarantees are made about forward compatibility of the type's functionality and its usage. /// [EditorBrowsable(EditorBrowsableState.Never)] public class SystemClockChangedEventArgs : EventArgs { /// /// Creates a new system clock notification object with unknown old and new times. /// public SystemClockChangedEventArgs() : this(DateTimeOffset.MinValue, DateTimeOffset.MaxValue) { } /// /// Creates a new system clock notification object with the specified old and new times. /// /// Time before the system clock changed, or DateTimeOffset.MinValue if not known. /// Time after the system clock changed, or DateTimeOffset.MaxValue if not known. public SystemClockChangedEventArgs(DateTimeOffset oldTime, DateTimeOffset newTime) { OldTime = oldTime; NewTime = newTime; } /// /// Gets the time before the system clock changed, or DateTimeOffset.MinValue if not known. /// public DateTimeOffset OldTime { get; } /// /// Gets the time after the system clock changed, or DateTimeOffset.MaxValue if not known. /// public DateTimeOffset NewTime { get; } } }