From 4b62d73d47b71f3113b91ab228dad7cfc3a317d0 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 29 Apr 2026 15:00:32 -0700 Subject: [PATCH 1/6] Migrate Apple reachability from SCNetworkReachability to NWPathMonitor Replace the deprecated SCNetworkReachability APIs with NWPathMonitor for modern Apple deployment targets (iOS 12+, macOS 10.14+). The legacy SCNetworkReachability path is retained behind a compile-time check for older targets. Changes: - NetworkInformationImpl.mm: refactor to use NWPathMonitor as the primary reachability mechanism, with SCNetworkReachability as fallback for older deployment targets only - ODWReachability.h/m: add NWPathMonitor-based implementation gated on availability, keeping SCNetworkReachability for backward compatibility - Remove dead private header imports from tests This eliminates the -Wdeprecated-declarations build failures on Xcode 26.4+ without needing pragma suppressions. Fixes #1425 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/pal/posix/NetworkInformationImpl.mm | 220 ++++++++++-------- tests/unittests/obj-c/ODWReachabilityTests.mm | 2 - third_party/Reachability/ODWReachability.h | 12 + third_party/Reachability/ODWReachability.m | 57 ++++- 4 files changed, 188 insertions(+), 103 deletions(-) diff --git a/lib/pal/posix/NetworkInformationImpl.mm b/lib/pal/posix/NetworkInformationImpl.mm index 24c7839eb..943c22667 100644 --- a/lib/pal/posix/NetworkInformationImpl.mm +++ b/lib/pal/posix/NetworkInformationImpl.mm @@ -20,7 +20,7 @@ class NetworkInformation : public NetworkInformationImpl, public std::enable_shared_from_this { - public: + public: /// /// /// @@ -59,21 +59,27 @@ virtual NetworkCost GetNetworkCost() /// /// Setup initial network information and start net monitor if requested. /// This cannot be put in constructor because we need to use shared_from_this. - /// - void SetupNetDetect(); + /// + void SetupNetDetect(); - private: - void UpdateType(NetworkType type) noexcept; - void UpdateCost(NetworkCost cost) noexcept; - std::string m_network_provider {}; + private: + void SetupModernNetDetect() API_AVAILABLE(macos(10.14), ios(12.0)); +#if ODW_LEGACY_REACHABILITY_REQUIRED + void SetupLegacyNetDetect(); +#endif + void UpdateType(NetworkType type) noexcept; + void UpdateCost(NetworkCost cost) noexcept; + std::string m_network_provider {}; - // iOS 12 and newer - nw_path_monitor_t m_monitor = nil; + // iOS 12+ / macOS 10.14+ + nw_path_monitor_t m_monitor = nil; - // iOS 11 and older - ODWReachability* m_reach = nil; - id m_notificationId = nil; - }; +#if ODW_LEGACY_REACHABILITY_REQUIRED + // Older Apple deployment targets still need the legacy fallback. + ODWReachability* m_reach = nil; + id m_notificationId = nil; +#endif + }; NetworkInformation::NetworkInformation(IRuntimeConfig& configuration) : NetworkInformationImpl(configuration) @@ -84,6 +90,7 @@ virtual NetworkCost GetNetworkCost() NetworkInformation::~NetworkInformation() noexcept { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { if (m_isNetDetectEnabled) @@ -99,108 +106,130 @@ virtual NetworkCost GetNetworkCost() [m_reach stopNotifier]; } } +#else + if (m_isNetDetectEnabled) + { + nw_path_monitor_cancel(m_monitor); + } +#endif } - void NetworkInformation::SetupNetDetect() + void NetworkInformation::SetupModernNetDetect() { - if (@available(macOS 10.14, iOS 12.0, *)) + auto weak_this = std::weak_ptr(shared_from_this()); + + m_monitor = nw_path_monitor_create(); + nw_path_monitor_set_queue(m_monitor, dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)); + nw_path_monitor_set_update_handler(m_monitor, ^(nw_path_t path) { - auto weak_this = std::weak_ptr(shared_from_this()); + auto strong_this = weak_this.lock(); + if (!strong_this) + { + return; + } - m_monitor = nw_path_monitor_create(); - nw_path_monitor_set_queue(m_monitor, dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)); - nw_path_monitor_set_update_handler(m_monitor, ^(nw_path_t path) + NetworkType type = NetworkType_Unknown; + NetworkCost cost = NetworkCost_Unknown; + nw_path_status_t status = nw_path_get_status(path); + bool connected = status == nw_path_status_satisfied || status == nw_path_status_satisfiable; + if (connected) { - auto strong_this = weak_this.lock(); - if (!strong_this) + if (nw_path_uses_interface_type(path, nw_interface_type_wifi)) { - return; + type = NetworkType_Wifi; } - - NetworkType type = NetworkType_Unknown; - NetworkCost cost = NetworkCost_Unknown; - nw_path_status_t status = nw_path_get_status(path); - bool connected = status == nw_path_status_satisfied || status == nw_path_status_satisfiable; - if (connected) + else if (nw_path_uses_interface_type(path, nw_interface_type_cellular)) { - if (nw_path_uses_interface_type(path, nw_interface_type_wifi)) - { - type = NetworkType_Wifi; - } - else if (nw_path_uses_interface_type(path, nw_interface_type_cellular)) - { - type = NetworkType_WWAN; - } - else if (nw_path_uses_interface_type(path, nw_interface_type_wired)) - { - type = NetworkType_Wired; - } - cost = nw_path_is_expensive(path) ? NetworkCost_Metered : NetworkCost_Unmetered; - if (@available(macOS 10.15, iOS 13.0, *)) + type = NetworkType_WWAN; + } + else if (nw_path_uses_interface_type(path, nw_interface_type_wired)) + { + type = NetworkType_Wired; + } + cost = nw_path_is_expensive(path) ? NetworkCost_Metered : NetworkCost_Unmetered; + if (@available(macOS 10.15, iOS 13.0, *)) + { + if (nw_path_is_constrained(path)) { - if (nw_path_is_constrained(path)) - { - cost = NetworkCost_Roaming; - } + cost = NetworkCost_Roaming; } } - strong_this->UpdateType(type); - strong_this->UpdateCost(cost); - }); - nw_path_monitor_start(m_monitor); - - // nw_path_monitor_start will invoke the callback for once. So if - // we don't want to listen for changes, we can just start the - // monitor and stop it right away. - if (!m_isNetDetectEnabled) - { - nw_path_monitor_cancel(m_monitor); } - } - else + strong_this->UpdateType(type); + strong_this->UpdateCost(cost); + }); + nw_path_monitor_start(m_monitor); + + // nw_path_monitor_start will invoke the callback for once. So if + // we don't want to listen for changes, we can just start the + // monitor and stop it right away. + if (!m_isNetDetectEnabled) { - auto weak_this = std::weak_ptr(shared_from_this()); + nw_path_monitor_cancel(m_monitor); + } + } - m_reach = [ODWReachability reachabilityForInternetConnection]; - void (^block)(NSNotification*) = ^(NSNotification*) - { - auto strong_this = weak_this.lock(); - if (!strong_this) - { - return; - } +#if ODW_LEGACY_REACHABILITY_REQUIRED + void NetworkInformation::SetupLegacyNetDetect() + { + auto weak_this = std::weak_ptr(shared_from_this()); - // NetworkCost information is not available until iOS 12. - // Just make the best guess here. - switch (m_reach.currentReachabilityStatus) - { - case NotReachable: - strong_this->UpdateType(NetworkType_Unknown); - strong_this->UpdateCost(NetworkCost_Unknown); - break; - case ReachableViaWiFi: - strong_this->UpdateType(NetworkType_Wifi); - strong_this->UpdateCost(NetworkCost_Unmetered); - break; - case ReachableViaWWAN: - strong_this->UpdateType(NetworkType_WWAN); - strong_this->UpdateCost(NetworkCost_Metered); - break; - } - }; - block(nil); // Update the initial status. + m_reach = [ODWReachability reachabilityForInternetConnection]; + void (^block)(NSNotification*) = ^(NSNotification*) + { + auto strong_this = weak_this.lock(); + if (!strong_this) + { + return; + } - if (m_isNetDetectEnabled) + // NetworkCost information is not available until iOS 12. + // Just make the best guess here. + switch (m_reach.currentReachabilityStatus) { - m_notificationId = - [[NSNotificationCenter defaultCenter] - addObserverForName: kNetworkReachabilityChangedNotification - object: nil - queue: nil - usingBlock: block]; - [m_reach startNotifier]; + case NotReachable: + strong_this->UpdateType(NetworkType_Unknown); + strong_this->UpdateCost(NetworkCost_Unknown); + break; + case ReachableViaWiFi: + strong_this->UpdateType(NetworkType_Wifi); + strong_this->UpdateCost(NetworkCost_Unmetered); + break; + case ReachableViaWWAN: + strong_this->UpdateType(NetworkType_WWAN); + strong_this->UpdateCost(NetworkCost_Metered); + break; } + }; + block(nil); // Update the initial status. + + if (m_isNetDetectEnabled) + { + m_notificationId = + [[NSNotificationCenter defaultCenter] + addObserverForName: kNetworkReachabilityChangedNotification + object: nil + queue: nil + usingBlock: block]; + [m_reach startNotifier]; + } + } +#endif + + void NetworkInformation::SetupNetDetect() + { +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { + SetupModernNetDetect(); + } + else + { + SetupLegacyNetDetect(); } +#else + SetupModernNetDetect(); +#endif } void NetworkInformation::UpdateType(NetworkType type) noexcept @@ -229,4 +258,3 @@ virtual NetworkCost GetNetworkCost() } } PAL_NS_END - diff --git a/tests/unittests/obj-c/ODWReachabilityTests.mm b/tests/unittests/obj-c/ODWReachabilityTests.mm index cc8a7c24e..d231bbec8 100644 --- a/tests/unittests/obj-c/ODWReachabilityTests.mm +++ b/tests/unittests/obj-c/ODWReachabilityTests.mm @@ -11,8 +11,6 @@ #import #import "ODWReachability.h" -#import -#import #import @interface ODWReachabilityTests : XCTestCase diff --git a/third_party/Reachability/ODWReachability.h b/third_party/Reachability/ODWReachability.h index b2d218417..57f3702f9 100644 --- a/third_party/Reachability/ODWReachability.h +++ b/third_party/Reachability/ODWReachability.h @@ -25,8 +25,10 @@ POSSIBILITY OF SUCH DAMAGE. */ +#import #import #import +#import /** @@ -40,6 +42,16 @@ extern NSString* const kNetworkReachabilityChangedNotification; +// Older Apple deployment targets still need the legacy SCNetworkReachability +// backend at runtime. Newer targets can compile directly to the modern path. +#if TARGET_OS_IPHONE +#define ODW_LEGACY_REACHABILITY_REQUIRED (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_12_0) +#elif TARGET_OS_OSX +#define ODW_LEGACY_REACHABILITY_REQUIRED (__MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_14) +#else +#define ODW_LEGACY_REACHABILITY_REQUIRED 0 +#endif + typedef NS_ENUM(NSInteger, ODWNetworkStatus) { // Apple NetworkStatus Compatible Names. NotReachable = 0, diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index 7947f7df0..dc8deb38f 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -27,8 +27,6 @@ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #import "ODWReachability.h" -#import -#import #import @@ -43,6 +41,8 @@ @interface ODWReachability () -(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; ++(ODWReachability *)handleReachabilityResponse:(NSURLResponse *)response error:(NSError *)error url:(NSURL *)url; +-(SCNetworkReachabilityFlags)checkNetworkReachability:(BOOL)checkData; @end @@ -65,6 +65,7 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; } +#if ODW_LEGACY_REACHABILITY_REQUIRED // Start listening for reachability notifications on the current run loop static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) { @@ -79,6 +80,7 @@ static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkRea [reachability reachabilityChanged:flags]; } } +#endif @implementation ODWReachability @@ -99,8 +101,10 @@ +(ODWReachability*)reachabilityWithHostName:(NSString*)hostname +(instancetype)reachabilityWithHostname:(NSString*)hostname { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif // Use URLSession for macOS 10.14 or higher NSString *formattedHostname = hostname; if (![formattedHostname hasPrefix:@"https://"] && ![formattedHostname hasPrefix:@"http://"]) { @@ -116,6 +120,7 @@ +(instancetype)reachabilityWithHostname:(NSString*)hostname }]; [dataTask resume]; return reachabilityInstance; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // Use SCNetworkReachability for macOS 10.14 or lower @@ -131,6 +136,7 @@ +(instancetype)reachabilityWithHostname:(NSString*)hostname } return nil; +#endif } +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress @@ -140,8 +146,10 @@ +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress return nil; } +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif // Use URLSession for macOS 10.14 or higher NSString *addressString = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)hostAddress)->sin_addr)]; NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", addressString]]; @@ -153,6 +161,7 @@ +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress }]; [dataTask resume]; return reachabilityInstance; // Return the instance after resuming the data task +#if ODW_LEGACY_REACHABILITY_REQUIRED } // Use SCNetworkReachability for macOS 10.14 or lower @@ -168,6 +177,7 @@ +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress } return nil; +#endif } +(ODWReachability *)handleReachabilityResponse:(NSURLResponse *)response error:(NSError *)error url:(NSURL *)url @@ -284,8 +294,10 @@ -(void)dealloc -(BOOL)startNotifier { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif // Use URLSession for macOS 10.14 or higher NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *task = [session dataTaskWithURL:[self url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { @@ -304,6 +316,7 @@ -(BOOL)startNotifier NSLog(@"Failed to create URLSessionDataTask"); return NO; } +#if ODW_LEGACY_REACHABILITY_REQUIRED } // Use SCNetworkReachability for macOS 10.14 or lower @@ -346,14 +359,19 @@ -(BOOL)startNotifier // if we get here we fail at the internet self.reachabilityObject = nil; return NO; +#endif } -(void)stopNotifier { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif // Use URLSession for macOS 10.14 or higher, no specific action is needed for URLSession self.reachabilityObject = nil; +#if ODW_LEGACY_REACHABILITY_REQUIRED + return; } // Use SCNetworkReachability for macOS 10.14 or lower @@ -366,6 +384,7 @@ -(void)stopNotifier SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, NULL); #pragma clang diagnostic pop self.reachabilityObject = nil; +#endif } @@ -408,9 +427,12 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags -(BOOL)isReachable { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif return [self checkNetworkReachability:true]; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // for macOS 10.14 or lower @@ -423,11 +445,13 @@ -(BOOL)isReachable #pragma clang diagnostic pop return [self isReachableWithFlags:flags]; +#endif } -(BOOL)isReachableViaWWAN { +#if ODW_LEGACY_REACHABILITY_REQUIRED #if TARGET_OS_IPHONE SCNetworkReachabilityFlags flags = 0; @@ -447,13 +471,19 @@ -(BOOL)isReachableViaWWAN #endif return NO; +#else + return NO; +#endif } -(BOOL)isReachableViaWiFi { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif return [self checkNetworkReachability:true]; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // for macOS 10.14 or lower @@ -479,6 +509,7 @@ -(BOOL)isReachableViaWiFi } return NO; +#endif } @@ -491,9 +522,12 @@ -(BOOL)isConnectionRequired -(BOOL)connectionRequired { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif return [self checkNetworkReachability:false]; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // for macOS 10.14 or lower @@ -508,15 +542,19 @@ -(BOOL)connectionRequired } return NO; +#endif } // Dynamic, on demand connection? -(BOOL)isConnectionOnDemand { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif return [self checkNetworkReachability:true]; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // for macOS 10.14 or lower @@ -532,15 +570,19 @@ -(BOOL)isConnectionOnDemand } return NO; +#endif } // Is user intervention required? -(BOOL)isInterventionRequired { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif return [self checkNetworkReachability:false]; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // for macOS 10.14 or lower @@ -556,6 +598,7 @@ -(BOOL)isInterventionRequired } return NO; +#endif } @@ -579,11 +622,13 @@ -(ODWNetworkStatus)currentReachabilityStatus -(SCNetworkReachabilityFlags)reachabilityFlags { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { +#endif __block SCNetworkReachabilityFlags flags = 0; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - + NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *task = [session dataTaskWithURL:[self url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (error == nil && data != nil) { @@ -591,11 +636,12 @@ -(SCNetworkReachabilityFlags)reachabilityFlags } dispatch_semaphore_signal(semaphore); }]; - + [task resume]; dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, kTimeoutDurationInSeconds * NSEC_PER_SEC)); - + return flags; +#if ODW_LEGACY_REACHABILITY_REQUIRED } // for macOS 10.14 or lower @@ -610,6 +656,7 @@ -(SCNetworkReachabilityFlags)reachabilityFlags } return 0; +#endif } -(NSString*)currentReachabilityString From 3906d9ef0f1f6fdaff5ce6297b697439bdfdd667 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 29 Apr 2026 15:32:41 -0700 Subject: [PATCH 2/6] Restore ODWReachabilityTests includes to keep #1431 focused PR #1431 should not carry changes in ODWReachabilityTests.mm. Restore the socket header imports so the branch only contains the reachability implementation changes. Files changed: - tests/unittests/obj-c/ODWReachabilityTests.mm Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/unittests/obj-c/ODWReachabilityTests.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unittests/obj-c/ODWReachabilityTests.mm b/tests/unittests/obj-c/ODWReachabilityTests.mm index d231bbec8..cc8a7c24e 100644 --- a/tests/unittests/obj-c/ODWReachabilityTests.mm +++ b/tests/unittests/obj-c/ODWReachabilityTests.mm @@ -11,6 +11,8 @@ #import #import "ODWReachability.h" +#import +#import #import @interface ODWReachabilityTests : XCTestCase From 12d89582375126d4382886a5d85b962603c4f991 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 29 Apr 2026 15:34:13 -0700 Subject: [PATCH 3/6] Restore ODWReachability.m header section to match main Keep PR #1431 focused on the reachability implementation changes by reverting the top-of-file/header-area edits in ODWReachability.m. Files changed: - third_party/Reachability/ODWReachability.m Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.m | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index dc8deb38f..38faf54d5 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -27,6 +27,8 @@ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #import "ODWReachability.h" +#import +#import #import @@ -41,8 +43,6 @@ @interface ODWReachability () -(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; -+(ODWReachability *)handleReachabilityResponse:(NSURLResponse *)response error:(NSError *)error url:(NSURL *)url; --(SCNetworkReachabilityFlags)checkNetworkReachability:(BOOL)checkData; @end @@ -65,7 +65,6 @@ -(SCNetworkReachabilityFlags)checkNetworkReachability:(BOOL)checkData; (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; } -#if ODW_LEGACY_REACHABILITY_REQUIRED // Start listening for reachability notifications on the current run loop static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) { @@ -80,7 +79,6 @@ static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkRea [reachability reachabilityChanged:flags]; } } -#endif @implementation ODWReachability From 770250810e3e0067db8340e4814fb2661513136c Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 1 May 2026 19:20:16 -0500 Subject: [PATCH 4/6] Use NWPathMonitor in ODWReachability Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.m | 341 +++++++++++++++++---- 1 file changed, 286 insertions(+), 55 deletions(-) diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index 38faf54d5..a24c4506e 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -27,22 +27,48 @@ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #import "ODWReachability.h" +#import + #import #import #import +#import NSString *const kNetworkReachabilityChangedNotification = @"NetworkReachabilityChangedNotification"; +@class ODWReachability; + +@interface ODWReachabilityMonitorContext : NSObject + +@property (nonatomic, assign) ODWReachability *owner; + +@end + +@implementation ODWReachabilityMonitorContext +@end + @interface ODWReachability () @property (nonatomic, assign) SCNetworkReachabilityRef reachabilityRef; @property (nonatomic, strong) dispatch_queue_t reachabilitySerialQueue; @property (nonatomic, strong) id reachabilityObject; +@property (nonatomic, strong) nw_path_monitor_t pathMonitor; +@property (nonatomic, strong) ODWReachabilityMonitorContext *pathMonitorContext; +@property (nonatomic, strong) dispatch_semaphore_t initialPathSemaphore; +@property (nonatomic, assign) nw_path_status_t currentPathStatus; +@property (nonatomic, assign) BOOL currentPathUsesWiFi; +@property (nonatomic, assign) BOOL currentPathUsesWWAN; +@property (nonatomic, assign) BOOL hasObservedPath; +@property (nonatomic, assign) BOOL monitorLocalWiFiOnly; -(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; +-(BOOL)ensureModernPathMonitor; +-(BOOL)awaitModernPathSnapshot; +-(void)handleModernPathUpdate:(nw_path_t)path; +-(void)notifyModernPathChange; @end @@ -62,9 +88,36 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-', (flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-', (flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-', - (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; + (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; +} + +static BOOL ODWModernPathIsReachable(nw_path_status_t status) +{ + return status == nw_path_status_satisfied || status == nw_path_status_satisfiable; +} + +static BOOL ODWHostResolves(NSString *hostname) +{ + if (hostname == nil || hostname.length == 0) + { + return NO; + } + + struct addrinfo hints = {}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + struct addrinfo *result = NULL; + int lookupResult = getaddrinfo(hostname.UTF8String, NULL, &hints, &result); + if (result != NULL) + { + freeaddrinfo(result); + } + + return lookupResult == 0; } +#if ODW_LEGACY_REACHABILITY_REQUIRED // Start listening for reachability notifications on the current run loop static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) { @@ -79,6 +132,7 @@ static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkRea [reachability reachabilityChanged:flags]; } } +#endif @implementation ODWReachability @@ -103,20 +157,20 @@ +(instancetype)reachabilityWithHostname:(NSString*)hostname if (@available(macOS 10.14, iOS 12.0, *)) { #endif - // Use URLSession for macOS 10.14 or higher + // Use Network.framework reachability for macOS 10.14 or higher. NSString *formattedHostname = hostname; if (![formattedHostname hasPrefix:@"https://"] && ![formattedHostname hasPrefix:@"http://"]) { formattedHostname = [NSString stringWithFormat:@"https://%@", hostname]; } NSURL *url = [NSURL URLWithString:formattedHostname]; + if (url == nil || url.host == nil || !ODWHostResolves(url.host)) + { + NSLog(@"Invalid hostname"); + return nil; + } - NSURLSession *session = [NSURLSession sharedSession]; - __block ODWReachability *reachabilityInstance = [[self alloc] init]; + ODWReachability *reachabilityInstance = [[self alloc] init]; reachabilityInstance.url = url; - NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - reachabilityInstance = [self handleReachabilityResponse:response error:error url:reachabilityInstance.url]; - }]; - [dataTask resume]; return reachabilityInstance; #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -148,17 +202,19 @@ +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress if (@available(macOS 10.14, iOS 12.0, *)) { #endif - // Use URLSession for macOS 10.14 or higher - NSString *addressString = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)hostAddress)->sin_addr)]; + // Use Network.framework reachability for macOS 10.14 or higher. + struct sockaddr_in *address = (struct sockaddr_in *)hostAddress; + if (address->sin_addr.s_addr == INADDR_ANY) + { + NSLog(@"Invalid address"); + return nil; + } + + NSString *addressString = [NSString stringWithUTF8String:inet_ntoa(address->sin_addr)]; NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", addressString]]; - NSURLSession *session = [NSURLSession sharedSession]; - __block ODWReachability *reachabilityInstance = [[self alloc] init]; + ODWReachability *reachabilityInstance = [[self alloc] init]; reachabilityInstance.url = url; - NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - reachabilityInstance = [self handleReachabilityResponse:response error:error url:reachabilityInstance.url]; - }]; - [dataTask resume]; - return reachabilityInstance; // Return the instance after resuming the data task + return reachabilityInstance; #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -207,6 +263,13 @@ +(ODWReachability *)handleReachabilityResponse:(NSURLResponse *)response error:( +(ODWReachability *)reachabilityForInternetConnection { +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { + return [[self alloc] init]; + } +#endif + struct sockaddr_in zeroAddress; bzero(&zeroAddress, sizeof(zeroAddress)); zeroAddress.sin_len = sizeof(zeroAddress); @@ -217,6 +280,15 @@ +(ODWReachability *)reachabilityForInternetConnection +(ODWReachability*)reachabilityForLocalWiFi { +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { + ODWReachability *reachability = [[self alloc] init]; + reachability.monitorLocalWiFiOnly = YES; + return reachability; + } +#endif + struct sockaddr_in localWifiAddress; bzero(&localWifiAddress, sizeof(localWifiAddress)); localWifiAddress.sin_len = sizeof(localWifiAddress); @@ -230,23 +302,139 @@ +(ODWReachability*)reachabilityForLocalWiFi // Initialization methods --(ODWReachability *)initWithReachabilityRef:(SCNetworkReachabilityRef)ref +-(instancetype)init { self = [super init]; if (self != nil) { self.reachableOnWWAN = YES; - self.reachabilityRef = ref; + self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL); + self.currentPathStatus = nw_path_status_invalid; + } - // We need to create a serial queue. - // We allocate this once for the lifetime of the notifier. + return self; +} - self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL); +-(ODWReachability *)initWithReachabilityRef:(SCNetworkReachabilityRef)ref +{ + self = [self init]; + if (self != nil) + { + self.reachabilityRef = ref; } return self; } +-(BOOL)ensureModernPathMonitor +{ + if (self.pathMonitor != nil) + { + return YES; + } + + self.hasObservedPath = NO; + self.currentPathStatus = nw_path_status_invalid; + self.currentPathUsesWiFi = NO; + self.currentPathUsesWWAN = NO; + self.initialPathSemaphore = dispatch_semaphore_create(0); + self.pathMonitor = self.monitorLocalWiFiOnly + ? nw_path_monitor_create_with_type(nw_interface_type_wifi) + : nw_path_monitor_create(); + + if (self.pathMonitor == nil) + { + return NO; + } + + ODWReachabilityMonitorContext *context = [[ODWReachabilityMonitorContext alloc] init]; + context.owner = self; + self.pathMonitorContext = context; +#if !__has_feature(objc_arc) + [context release]; +#endif + + nw_path_monitor_set_queue(self.pathMonitor, self.reachabilitySerialQueue); + nw_path_monitor_set_update_handler(self.pathMonitor, ^(nw_path_t path) { + ODWReachability *owner = context.owner; + if (owner == nil) + { + return; + } + + [owner handleModernPathUpdate:path]; + }); + nw_path_monitor_start(self.pathMonitor); + + return YES; +} + +-(BOOL)awaitModernPathSnapshot +{ + if (![self ensureModernPathMonitor]) + { + return NO; + } + + if (self.hasObservedPath) + { + return YES; + } + + if (self.initialPathSemaphore == nil) + { + return NO; + } + + long waitResult = dispatch_semaphore_wait( + self.initialPathSemaphore, + dispatch_time(DISPATCH_TIME_NOW, kTimeoutDurationInSeconds * NSEC_PER_SEC)); + return waitResult == 0 && self.hasObservedPath; +} + +-(void)handleModernPathUpdate:(nw_path_t)path +{ + self.currentPathStatus = nw_path_get_status(path); + self.currentPathUsesWiFi = nw_path_uses_interface_type(path, nw_interface_type_wifi); +#if TARGET_OS_IPHONE + self.currentPathUsesWWAN = nw_path_uses_interface_type(path, nw_interface_type_cellular); +#else + self.currentPathUsesWWAN = NO; +#endif + + BOOL firstPath = !self.hasObservedPath; + self.hasObservedPath = YES; + if (firstPath && self.initialPathSemaphore != nil) + { + dispatch_semaphore_signal(self.initialPathSemaphore); + } + + if (self.reachabilityObject == self) + { + [self notifyModernPathChange]; + } +} + +-(void)notifyModernPathChange +{ + if (ODWModernPathIsReachable(self.currentPathStatus)) + { + if (self.reachableBlock) + { + self.reachableBlock(self); + } + } + else if (self.unreachableBlock) + { + self.unreachableBlock(self); + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:kNetworkReachabilityChangedNotification + object:self]; + }); +} + +(void)setTimeoutDurationInSeconds:(int)timeoutDuration { if (timeoutDuration >= kTimeoutDurationInSeconds) @@ -296,24 +484,17 @@ -(BOOL)startNotifier if (@available(macOS 10.14, iOS 12.0, *)) { #endif - // Use URLSession for macOS 10.14 or higher - NSURLSession *session = [NSURLSession sharedSession]; - NSURLSessionDataTask *task = [session dataTaskWithURL:[self url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (error) { - NSLog(@"URLSession failed: %@", error.localizedDescription); - self.reachabilityObject = nil; - } else { - self.reachabilityObject = self; - [[NSNotificationCenter defaultCenter] postNotificationName:kNetworkReachabilityChangedNotification object:self]; + // Use NWPathMonitor for macOS 10.14 or higher. + if ([self ensureModernPathMonitor]) + { + self.reachabilityObject = self; + if ([self awaitModernPathSnapshot]) + { + [self notifyModernPathChange]; } - }]; - if (task) { - [task resume]; return YES; - } else { - NSLog(@"Failed to create URLSessionDataTask"); - return NO; } + return NO; #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -366,8 +547,20 @@ -(void)stopNotifier if (@available(macOS 10.14, iOS 12.0, *)) { #endif - // Use URLSession for macOS 10.14 or higher, no specific action is needed for URLSession + // Use NWPathMonitor for macOS 10.14 or higher. self.reachabilityObject = nil; + if (self.pathMonitor != nil) + { + self.pathMonitorContext.owner = nil; + nw_path_monitor_cancel(self.pathMonitor); + self.pathMonitor = nil; + } + self.pathMonitorContext = nil; + self.initialPathSemaphore = nil; + self.hasObservedPath = NO; + self.currentPathStatus = nw_path_status_invalid; + self.currentPathUsesWiFi = NO; + self.currentPathUsesWWAN = NO; #if ODW_LEGACY_REACHABILITY_REQUIRED return; } @@ -429,7 +622,7 @@ -(BOOL)isReachable if (@available(macOS 10.14, iOS 12.0, *)) { #endif - return [self checkNetworkReachability:true]; + return [self awaitModernPathSnapshot] && ODWModernPathIsReachable(self.currentPathStatus); #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -450,6 +643,17 @@ -(BOOL)isReachable -(BOOL)isReachableViaWWAN { #if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { +#if TARGET_OS_IPHONE + return [self awaitModernPathSnapshot] && + ODWModernPathIsReachable(self.currentPathStatus) && + self.currentPathUsesWWAN; +#else + return NO; +#endif + } + #if TARGET_OS_IPHONE SCNetworkReachabilityFlags flags = 0; @@ -469,9 +673,16 @@ -(BOOL)isReachableViaWWAN #endif return NO; +#endif +#if !ODW_LEGACY_REACHABILITY_REQUIRED +#if TARGET_OS_IPHONE + return [self awaitModernPathSnapshot] && + ODWModernPathIsReachable(self.currentPathStatus) && + self.currentPathUsesWWAN; #else return NO; #endif +#endif } -(BOOL)isReachableViaWiFi @@ -480,7 +691,20 @@ -(BOOL)isReachableViaWiFi if (@available(macOS 10.14, iOS 12.0, *)) { #endif - return [self checkNetworkReachability:true]; + if (![self awaitModernPathSnapshot] || !ODWModernPathIsReachable(self.currentPathStatus)) + { + return NO; + } +#if TARGET_OS_IPHONE + if (self.monitorLocalWiFiOnly) + { + return self.currentPathUsesWiFi; + } + + return !self.currentPathUsesWWAN; +#else + return self.monitorLocalWiFiOnly ? self.currentPathUsesWiFi : YES; +#endif #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -524,7 +748,8 @@ -(BOOL)connectionRequired if (@available(macOS 10.14, iOS 12.0, *)) { #endif - return [self checkNetworkReachability:false]; + return [self awaitModernPathSnapshot] && + self.currentPathStatus == nw_path_status_satisfiable; #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -551,7 +776,7 @@ -(BOOL)isConnectionOnDemand if (@available(macOS 10.14, iOS 12.0, *)) { #endif - return [self checkNetworkReachability:true]; + return NO; #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -579,7 +804,7 @@ -(BOOL)isInterventionRequired if (@available(macOS 10.14, iOS 12.0, *)) { #endif - return [self checkNetworkReachability:false]; + return NO; #if ODW_LEGACY_REACHABILITY_REQUIRED } @@ -624,20 +849,26 @@ -(SCNetworkReachabilityFlags)reachabilityFlags if (@available(macOS 10.14, iOS 12.0, *)) { #endif - __block SCNetworkReachabilityFlags flags = 0; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - - NSURLSession *session = [NSURLSession sharedSession]; - NSURLSessionDataTask *task = [session dataTaskWithURL:[self url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (error == nil && data != nil) { - flags = kSCNetworkReachabilityFlagsReachable; + if (![self awaitModernPathSnapshot]) + { + return 0; } - dispatch_semaphore_signal(semaphore); - }]; - - [task resume]; - dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, kTimeoutDurationInSeconds * NSEC_PER_SEC)); + SCNetworkReachabilityFlags flags = 0; + if (ODWModernPathIsReachable(self.currentPathStatus)) + { + flags |= kSCNetworkReachabilityFlagsReachable; + } + if (self.currentPathStatus == nw_path_status_satisfiable) + { + flags |= kSCNetworkReachabilityFlagsConnectionRequired; + } +#if TARGET_OS_IPHONE + if (self.currentPathUsesWWAN) + { + flags |= kSCNetworkReachabilityFlagsIsWWAN; + } +#endif return flags; #if ODW_LEGACY_REACHABILITY_REQUIRED } From 675cad4e2dcab9803fcc3f2847ff52522a5eeee3 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Sat, 2 May 2026 00:38:54 -0500 Subject: [PATCH 5/6] Clarify modern WWAN reachability handling Make the modern ODWReachability WWAN path explicit so iOS 12+ builds unambiguously use the NWPathMonitor-backed state, while the legacy SCNetworkReachability fallback remains only for older Apple deployment targets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.m | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index a24c4506e..f7d6aa158 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -642,20 +642,16 @@ -(BOOL)isReachable -(BOOL)isReachableViaWWAN { +#if TARGET_OS_IPHONE + BOOL modernWWANReachable = [self awaitModernPathSnapshot] && + ODWModernPathIsReachable(self.currentPathStatus) && + self.currentPathUsesWWAN; #if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { -#if TARGET_OS_IPHONE - return [self awaitModernPathSnapshot] && - ODWModernPathIsReachable(self.currentPathStatus) && - self.currentPathUsesWWAN; -#else - return NO; -#endif + return modernWWANReachable; } -#if TARGET_OS_IPHONE - SCNetworkReachabilityFlags flags = 0; if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) @@ -670,19 +666,15 @@ -(BOOL)isReachableViaWWAN } } } -#endif return NO; #endif #if !ODW_LEGACY_REACHABILITY_REQUIRED -#if TARGET_OS_IPHONE - return [self awaitModernPathSnapshot] && - ODWModernPathIsReachable(self.currentPathStatus) && - self.currentPathUsesWWAN; + return modernWWANReachable; +#endif #else return NO; #endif -#endif } -(BOOL)isReachableViaWiFi From 366f87a7b18a4bc5139e123e9f3dca187a793ded Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Sat, 2 May 2026 16:34:04 -0500 Subject: [PATCH 6/6] Avoid blocking Apple reachability construction Modern reachability should not synchronously resolve DNS or reject the generic internet reachability address, and the path monitor context must not hold a stale raw owner pointer. Also avoid blocking the main thread while waiting for the first NWPathMonitor snapshot. Files changed: - third_party/Reachability/ODWReachability.m Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- third_party/Reachability/ODWReachability.m | 41 ++++++++-------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index f7d6aa158..76a040648 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -32,7 +32,6 @@ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #import #import #import -#import NSString *const kNetworkReachabilityChangedNotification = @"NetworkReachabilityChangedNotification"; @@ -41,7 +40,11 @@ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF @interface ODWReachabilityMonitorContext : NSObject -@property (nonatomic, assign) ODWReachability *owner; +#if __has_feature(objc_arc) +@property (nonatomic, weak) ODWReachability *owner; +#else +@property (nonatomic, unsafe_unretained) ODWReachability *owner; +#endif @end @@ -96,27 +99,6 @@ static BOOL ODWModernPathIsReachable(nw_path_status_t status) return status == nw_path_status_satisfied || status == nw_path_status_satisfiable; } -static BOOL ODWHostResolves(NSString *hostname) -{ - if (hostname == nil || hostname.length == 0) - { - return NO; - } - - struct addrinfo hints = {}; - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - - struct addrinfo *result = NULL; - int lookupResult = getaddrinfo(hostname.UTF8String, NULL, &hints, &result); - if (result != NULL) - { - freeaddrinfo(result); - } - - return lookupResult == 0; -} - #if ODW_LEGACY_REACHABILITY_REQUIRED // Start listening for reachability notifications on the current run loop static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) @@ -163,7 +145,7 @@ +(instancetype)reachabilityWithHostname:(NSString*)hostname formattedHostname = [NSString stringWithFormat:@"https://%@", hostname]; } NSURL *url = [NSURL URLWithString:formattedHostname]; - if (url == nil || url.host == nil || !ODWHostResolves(url.host)) + if (url == nil || url.host == nil) { NSLog(@"Invalid hostname"); return nil; @@ -206,8 +188,7 @@ +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress struct sockaddr_in *address = (struct sockaddr_in *)hostAddress; if (address->sin_addr.s_addr == INADDR_ANY) { - NSLog(@"Invalid address"); - return nil; + return [[self alloc] init]; } NSString *addressString = [NSString stringWithUTF8String:inet_ntoa(address->sin_addr)]; @@ -386,6 +367,14 @@ -(BOOL)awaitModernPathSnapshot return NO; } + // Avoid blocking reachability queries on the main thread before the first + // NWPathMonitor update arrives. Callers get a conservative "unknown yet" + // result until the async update handler records the first snapshot. + if ([NSThread isMainThread]) + { + return NO; + } + long waitResult = dispatch_semaphore_wait( self.initialPathSemaphore, dispatch_time(DISPATCH_TIME_NOW, kTimeoutDurationInSeconds * NSEC_PER_SEC));