-
Notifications
You must be signed in to change notification settings - Fork 2k
Replace deprecated NetworkReachability with NWPathMonitor on iOS/macOS #32354
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
42c6985
04e9faa
d191b18
d63d96c
e26987f
fcc6bfa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,12 +1,11 @@ | ||||||||||||||||||||||||||||||
| using System; | ||||||||||||||||||||||||||||||
| using System.Collections.Generic; | ||||||||||||||||||||||||||||||
| using System.Net; | ||||||||||||||||||||||||||||||
| using System.Threading.Tasks; | ||||||||||||||||||||||||||||||
| #if !(MACCATALYST || MACOS) | ||||||||||||||||||||||||||||||
| using CoreTelephony; | ||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||
| using CoreFoundation; | ||||||||||||||||||||||||||||||
| using SystemConfiguration; | ||||||||||||||||||||||||||||||
| using Network; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| namespace Microsoft.Maui.Networking | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
|
|
@@ -19,144 +18,131 @@ enum NetworkStatus | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| static class Reachability | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| internal const string HostName = "www.microsoft.com"; | ||||||||||||||||||||||||||||||
| static NWPathMonitor sharedMonitor; | ||||||||||||||||||||||||||||||
| static readonly object monitorLock = new object(); | ||||||||||||||||||||||||||||||
| static volatile bool pathInitialized = false; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| internal static NetworkStatus RemoteHostStatus() | ||||||||||||||||||||||||||||||
| static NWPathMonitor SharedMonitor | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| using (var remoteHostReachability = new NetworkReachability(HostName)) | ||||||||||||||||||||||||||||||
| get | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| var reachable = remoteHostReachability.TryGetFlags(out var flags); | ||||||||||||||||||||||||||||||
| lock (monitorLock) | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| if (sharedMonitor == null) | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| sharedMonitor = new NWPathMonitor(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Set up a handler to track when path becomes available | ||||||||||||||||||||||||||||||
| Action<NWPath> initHandler = null; | ||||||||||||||||||||||||||||||
| initHandler = (path) => | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| pathInitialized = true; | ||||||||||||||||||||||||||||||
| // Remove the handler after first call to avoid keeping reference | ||||||||||||||||||||||||||||||
| sharedMonitor.SnapshotHandler = null; | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
| sharedMonitor.SnapshotHandler = initHandler; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| sharedMonitor.SetQueue(DispatchQueue.DefaultGlobalQueue); | ||||||||||||||||||||||||||||||
| sharedMonitor.Start(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Wait synchronously for the first path update (up to 5 seconds) | ||||||||||||||||||||||||||||||
| var timeout = DateTime.UtcNow.AddSeconds(5); | ||||||||||||||||||||||||||||||
| while (!pathInitialized && DateTime.UtcNow < timeout) | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| System.Threading.Thread.Sleep(10); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return sharedMonitor; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!reachable) | ||||||||||||||||||||||||||||||
| return NetworkStatus.NotReachable; | ||||||||||||||||||||||||||||||
| static NWPath GetCurrentPath() | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| var monitor = SharedMonitor; | ||||||||||||||||||||||||||||||
| return monitor?.CurrentPath; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (!IsReachableWithoutRequiringConnection(flags)) | ||||||||||||||||||||||||||||||
| return NetworkStatus.NotReachable; | ||||||||||||||||||||||||||||||
| internal static NetworkStatus RemoteHostStatus() | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| var path = GetCurrentPath(); | ||||||||||||||||||||||||||||||
| if (path == null || path.Status != NWPathStatus.Satisfied) | ||||||||||||||||||||||||||||||
| return NetworkStatus.NotReachable; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #if __IOS__ | ||||||||||||||||||||||||||||||
| if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) | ||||||||||||||||||||||||||||||
| return NetworkStatus.ReachableViaCarrierDataNetwork; | ||||||||||||||||||||||||||||||
| if (path.UsesInterfaceType(NWInterfaceType.Cellular)) | ||||||||||||||||||||||||||||||
|
kubaflo marked this conversation as resolved.
|
||||||||||||||||||||||||||||||
| return NetworkStatus.ReachableViaCarrierDataNetwork; | ||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return NetworkStatus.ReachableViaWiFiNetwork; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return NetworkStatus.ReachableViaWiFiNetwork; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| internal static NetworkStatus InternetConnectionStatus() | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| var status = NetworkStatus.NotReachable; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| var defaultNetworkAvailable = IsNetworkAvailable(out var flags); | ||||||||||||||||||||||||||||||
| var path = GetCurrentPath(); | ||||||||||||||||||||||||||||||
| if (path == null || path.Status != NWPathStatus.Satisfied) | ||||||||||||||||||||||||||||||
| return NetworkStatus.NotReachable; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #if __IOS__ | ||||||||||||||||||||||||||||||
| // If it's a WWAN connection.. | ||||||||||||||||||||||||||||||
| if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) | ||||||||||||||||||||||||||||||
| status = NetworkStatus.ReachableViaCarrierDataNetwork; | ||||||||||||||||||||||||||||||
| if (path.UsesInterfaceType(NWInterfaceType.Cellular)) | ||||||||||||||||||||||||||||||
| return NetworkStatus.ReachableViaCarrierDataNetwork; | ||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // If the connection is reachable and no connection is required, then assume it's WiFi | ||||||||||||||||||||||||||||||
| if (defaultNetworkAvailable) | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| status = NetworkStatus.ReachableViaWiFiNetwork; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // If the connection is on-demand or on-traffic and no user intervention | ||||||||||||||||||||||||||||||
| // is required, then assume WiFi. | ||||||||||||||||||||||||||||||
| if (((flags & NetworkReachabilityFlags.ConnectionOnDemand) != 0 || (flags & NetworkReachabilityFlags.ConnectionOnTraffic) != 0) && | ||||||||||||||||||||||||||||||
| (flags & NetworkReachabilityFlags.InterventionRequired) == 0) | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| status = NetworkStatus.ReachableViaWiFiNetwork; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return status; | ||||||||||||||||||||||||||||||
| return NetworkStatus.ReachableViaWiFiNetwork; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| internal static IEnumerable<NetworkStatus> GetActiveConnectionType() | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| var status = new List<NetworkStatus>(); | ||||||||||||||||||||||||||||||
| var path = GetCurrentPath(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| var defaultNetworkAvailable = IsNetworkAvailable(out var flags); | ||||||||||||||||||||||||||||||
| if (path == null || path.Status != NWPathStatus.Satisfied) | ||||||||||||||||||||||||||||||
| return status; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #if __IOS__ | ||||||||||||||||||||||||||||||
| // If it's a WWAN connection. | ||||||||||||||||||||||||||||||
| if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) | ||||||||||||||||||||||||||||||
| if (path.UsesInterfaceType(NWInterfaceType.Cellular)) | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| status.Add(NetworkStatus.ReachableViaCarrierDataNetwork); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| else if (defaultNetworkAvailable) | ||||||||||||||||||||||||||||||
| else if (path.UsesInterfaceType(NWInterfaceType.Wifi) || path.UsesInterfaceType(NWInterfaceType.Wired)) | ||||||||||||||||||||||||||||||
| #else | ||||||||||||||||||||||||||||||
| // If the connection is reachable and no connection is required, then assume it's WiFi | ||||||||||||||||||||||||||||||
| if (defaultNetworkAvailable) | ||||||||||||||||||||||||||||||
| if (path.UsesInterfaceType(NWInterfaceType.Wifi) || path.UsesInterfaceType(NWInterfaceType.Wired)) | ||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| status.Add(NetworkStatus.ReachableViaWiFiNetwork); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| else if (((flags & NetworkReachabilityFlags.ConnectionOnDemand) != 0 || (flags & NetworkReachabilityFlags.ConnectionOnTraffic) != 0) && | ||||||||||||||||||||||||||||||
| (flags & NetworkReachabilityFlags.InterventionRequired) == 0) | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| // If the connection is on-demand or on-traffic and no user intervention | ||||||||||||||||||||||||||||||
| // is required, then assume WiFi. | ||||||||||||||||||||||||||||||
| status.Add(NetworkStatus.ReachableViaWiFiNetwork); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return status; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| internal static bool IsNetworkAvailable(out NetworkReachabilityFlags flags) | ||||||||||||||||||||||||||||||
| internal static bool IsNetworkAvailable() | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| var ip = new IPAddress(0); | ||||||||||||||||||||||||||||||
| using (var defaultRouteReachability = new NetworkReachability(ip)) | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| if (!defaultRouteReachability.TryGetFlags(out flags)) | ||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return IsReachableWithoutRequiringConnection(flags); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| internal static bool IsReachableWithoutRequiringConnection(NetworkReachabilityFlags flags) | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| // Is it reachable with the current network configuration? | ||||||||||||||||||||||||||||||
| var isReachable = (flags & NetworkReachabilityFlags.Reachable) != 0; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Do we need a connection to reach it? | ||||||||||||||||||||||||||||||
| var noConnectionRequired = (flags & NetworkReachabilityFlags.ConnectionRequired) == 0; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #if __IOS__ | ||||||||||||||||||||||||||||||
| // Since the network stack will automatically try to get the WAN up, | ||||||||||||||||||||||||||||||
| // probe that | ||||||||||||||||||||||||||||||
| if ((flags & NetworkReachabilityFlags.IsWWAN) != 0) | ||||||||||||||||||||||||||||||
| noConnectionRequired = true; | ||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return isReachable && noConnectionRequired; | ||||||||||||||||||||||||||||||
| var path = GetCurrentPath(); | ||||||||||||||||||||||||||||||
| return path != null && path.Status == NWPathStatus.Satisfied; | ||||||||||||||||||||||||||||||
|
jfversluis marked this conversation as resolved.
|
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| class ReachabilityListener : IDisposable | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| NetworkReachability defaultRouteReachability; | ||||||||||||||||||||||||||||||
| NetworkReachability remoteHostReachability; | ||||||||||||||||||||||||||||||
| // Delay to allow connection status to stabilize before notifying listeners | ||||||||||||||||||||||||||||||
| const int ConnectionStatusChangeDelayMs = 100; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| NWPathMonitor pathMonitor; | ||||||||||||||||||||||||||||||
| Action<NWPath> pathUpdateHandler; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| internal ReachabilityListener() | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| var ip = new IPAddress(0); | ||||||||||||||||||||||||||||||
| defaultRouteReachability = new NetworkReachability(ip); | ||||||||||||||||||||||||||||||
| #pragma warning disable CA1422 // obsolete in MacCatalyst 15, iOS 13 | ||||||||||||||||||||||||||||||
| defaultRouteReachability.SetNotification(OnChange); | ||||||||||||||||||||||||||||||
| defaultRouteReachability.Schedule(CFRunLoop.Main, CFRunLoop.ModeDefault); | ||||||||||||||||||||||||||||||
| #pragma warning restore CA1422 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| remoteHostReachability = new NetworkReachability(Reachability.HostName); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Need to probe before we queue, or we wont get any meaningful values | ||||||||||||||||||||||||||||||
| // this only happens when you create NetworkReachability from a hostname | ||||||||||||||||||||||||||||||
| remoteHostReachability.TryGetFlags(out var flags); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #pragma warning disable CA1422 // obsolete in MacCatalyst 15, iOS 13 | ||||||||||||||||||||||||||||||
| remoteHostReachability.SetNotification(OnChange); | ||||||||||||||||||||||||||||||
| remoteHostReachability.Schedule(CFRunLoop.Main, CFRunLoop.ModeDefault); | ||||||||||||||||||||||||||||||
| #pragma warning restore CA1422 | ||||||||||||||||||||||||||||||
| pathMonitor = new NWPathMonitor(); | ||||||||||||||||||||||||||||||
| pathUpdateHandler = async (NWPath path) => | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| // Add in artificial delay so the connection status has time to change | ||||||||||||||||||||||||||||||
| await Task.Delay(ConnectionStatusChangeDelayMs); | ||||||||||||||||||||||||||||||
| ReachabilityChanged?.Invoke(); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| // Add in artificial delay so the connection status has time to change | |
| await Task.Delay(ConnectionStatusChangeDelayMs); | |
| ReachabilityChanged?.Invoke(); | |
| try | |
| { | |
| // Add in artificial delay so the connection status has time to change | |
| await Task.Delay(ConnectionStatusChangeDelayMs); | |
| ReachabilityChanged?.Invoke(); | |
| } | |
| catch (Exception ex) | |
| { | |
| // Optionally log the exception, or ignore | |
| System.Diagnostics.Debug.WriteLine($"Exception in ReachabilityListener pathUpdateHandler: {ex}"); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of creating a Action it would be ideal to have a method instead. It can be async void if needed here as there is an awaited Task.Delay
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async void lambda — unhandled exceptions will crash the app. Since this is assigned to Action<NWPath>, it's implicitly async void. Per @Cheesebaron's earlier (unresolved) feedback, extract this to a named method and add a try-catch:
async void OnPathUpdate(NWPath path)
{
try
{
await Task.Delay(ConnectionStatusChangeDelayMs);
ReachabilityChanged?.Invoke();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(
$"ReachabilityListener handler failed: {ex}");
}
}Then: pathMonitor.SnapshotHandler = OnPathUpdate;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition: The handler nulls itself on line 41, which could happen before the while loop on line 50 checks
pathInitialized. If the handler is invoked on the dispatch queue between whenpathInitializedis set to true and when the handler is nulled, the logic is correct. However, if there's any delay, the spin-wait loop might still be spinning when the handler is already null, though this is unlikely to cause issues in practice sincepathInitializedis set first. A safer approach would be to null the handler outside the lock after the wait completes, or use a ManualResetEventSlim for proper synchronization.