diff --git a/.github/workflows/gdbus_proxy_L1_test.yml b/.github/workflows/gdbus_proxy_L1_test.yml index 242ca6bd..16edc038 100644 --- a/.github/workflows/gdbus_proxy_L1_test.yml +++ b/.github/workflows/gdbus_proxy_L1_test.yml @@ -107,6 +107,11 @@ jobs: && cmake --build build/ThunderInterfaces --target install -j8 + - name: Install IPowerManager header + run: | + IFACE_DIR=$(find ${{github.workspace}}/install/usr/include -maxdepth 2 -name "interfaces" -type d | head -1) + cp ${{github.workspace}}/networkmanager/tests/mocks/thunder/IPowerManager.h "$IFACE_DIR/" + - name: Build networkmanager with Gnome GDBUS Proxy run: > cmake diff --git a/.github/workflows/legacy_L1_L2_test.yml b/.github/workflows/legacy_L1_L2_test.yml index b3645cce..03538a0a 100644 --- a/.github/workflows/legacy_L1_L2_test.yml +++ b/.github/workflows/legacy_L1_L2_test.yml @@ -110,6 +110,11 @@ jobs: && cmake --build build/ThunderInterfaces --target install -j8 + - name: Install IPowerManager header + run: | + IFACE_DIR=$(find ${{github.workspace}}/install/usr/include -maxdepth 2 -name "interfaces" -type d | head -1) + cp ${{github.workspace}}/networkmanager/tests/mocks/thunder/IPowerManager.h "$IFACE_DIR/" + - name: Generate IARM headers run: | touch install/usr/lib/libIARMBus.so diff --git a/.github/workflows/libnm_proxy_L1_test.yml b/.github/workflows/libnm_proxy_L1_test.yml index 80d945d5..e5f64def 100644 --- a/.github/workflows/libnm_proxy_L1_test.yml +++ b/.github/workflows/libnm_proxy_L1_test.yml @@ -12,7 +12,8 @@ env: jobs: L1-tests: name: Build and run unit tests - runs-on: ubuntu-22.04 + # Note: Ubuntu 24.04 is required for libnm 1.46 which is needed for NetworkManagerPowerClient support + runs-on: ubuntu-24.04 steps: # Set up Thunder cache @@ -107,11 +108,16 @@ jobs: && cmake --build build/ThunderInterfaces --target install -j8 + - name: Install IPowerManager header + run: | + IFACE_DIR=$(find ${{github.workspace}}/install/usr/include -maxdepth 2 -name "interfaces" -type d | head -1) + cp ${{github.workspace}}/networkmanager/tests/mocks/thunder/IPowerManager.h "$IFACE_DIR/" + - name: Generate dependency files run: | sudo bash -c 'echo "ETHERNET_INTERFACE=eth0 WIFI_INTERFACE=wlan0 - DEVICE_NAME=rdk_test_device " > /etc/device.properties' + DEFAULT_HOSTNAME=rdk_test_device " > /etc/device.properties' - name: Generate IARM headers run: | @@ -160,7 +166,7 @@ jobs: - name: Generate coverage run: | - lcov -c -o coverage.info -d build/networkmanager_libnm/ + lcov --rc geninfo_unexecuted_blocks=1 -c -o coverage.info -d build/networkmanager_libnm/ --ignore-errors mismatch lcov -e coverage.info '*/networkmanager/plugin/gnome/*' -o filtered_coverage.info - name: Generate the html report diff --git a/.github/workflows/rdk_proxy_L1_L2_test.yml b/.github/workflows/rdk_proxy_L1_L2_test.yml index 4fb464fe..fc89bc38 100644 --- a/.github/workflows/rdk_proxy_L1_L2_test.yml +++ b/.github/workflows/rdk_proxy_L1_L2_test.yml @@ -107,6 +107,11 @@ jobs: && cmake --build build/ThunderInterfaces --target install -j8 + - name: Install IPowerManager header + run: | + IFACE_DIR=$(find ${{github.workspace}}/install/usr/include -maxdepth 2 -name "interfaces" -type d | head -1) + cp ${{github.workspace}}/networkmanager/tests/mocks/thunder/IPowerManager.h "$IFACE_DIR/" + - name: Generate IARM headers run: | touch install/usr/lib/libIARMBus.so diff --git a/.github/workflows/validate_pr_desc.yml b/.github/workflows/validate_pr_desc.yml index b6249ff7..3264db93 100644 --- a/.github/workflows/validate_pr_desc.yml +++ b/.github/workflows/validate_pr_desc.yml @@ -15,7 +15,7 @@ jobs: PR_TITLE: ${{ github.event.pull_request.title }} run: | # Define valid ticket IDs - VALID_TICKET_IDS=("RDKEMW") + VALID_TICKET_IDS=("RDKEMW" "RDK") # Function to validate ticket format and ID validate_ticket() { diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c24818..d262c5e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,25 @@ All notable changes to this RDK Service will be documented in this file. * Changes in CHANGELOG should be updated when commits are added to the main or release branches. There should be one CHANGELOG entry per JIRA Ticket. This is not enforced on sprint branches since there could be multiple changes for the same JIRA ticket during development. +## [3.3.0] - 2026-06-12 +### Fixed +- Fixed the memory leak that caused by the persistent m_nmClient and m_nmContext. + +## [3.2.0] - 2026-06-09 +### Added +- Implemented IP Caching logic to avoid multiple hard fetches from Gnome & invalidating the cache on change. + +## [3.1.0] - 2026-06-08 +### Changed +- Handled Power State changes within NetworkManager as below, +- Transition to DeepSleep will disconnect when NSM is OFF +- Transition from DeepSleep will Renew IP when NSM is ON + +## [3.0.0] - 2026-05-28 +### Changed +- The device hostname header that used to retrive has changed as "DEFAULT_HOSTNAME" +- Updated the WiFiStartScan to scan for specific SSID and also updated to take array of freq band as input instead of single freq + ## [2.3.0] - 2026-05-21 ### Fixed - Fixed the issue which leads Enabling and Disabling of interfaces are taking longer diff --git a/CMakeLists.txt b/CMakeLists.txt index a04ad16b..6d4cc1db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,7 @@ if (NOT WPEFramework_FOUND AND NOT Thunder_FOUND) endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") -set(VERSION_MAJOR 2) +set(VERSION_MAJOR 3) set(VERSION_MINOR 3) set(VERSION_PATCH 0) @@ -55,6 +55,21 @@ option(ENABLE_LEGACY_PLUGINS "Enable Legacy Plugins" ON) option(USE_RDK_LOGGER "Enable RDK Logger for logging" OFF ) option(ENABLE_UNIT_TESTING "Enable unit tests" OFF) option(USE_TELEMETRY "Enable Telemetry T2 support" OFF) +option(ENABLE_ETHERNET_CONNECTION_HANDLING + "Enable pre-sleep Ethernet deactivation" OFF) + +if(ENABLE_ETHERNET_CONNECTION_HANDLING) + add_definitions(-DENABLE_ETHERNET_CONNECTION_HANDLING) + message(STATUS "Ethernet connection handling: enabled") +endif() + +# Backend identity macros are consumed by shared headers; define them globally +# so all targets (plugin, tests, tools) compile against the same API surface. +if(ENABLE_GNOME_NETWORKMANAGER AND ENABLE_GNOME_GDBUS) + add_compile_definitions(NM_BACKEND_GDBUS=1) +elseif(NOT ENABLE_GNOME_NETWORKMANAGER) + add_compile_definitions(NM_BACKEND_RDK=1) +endif() if (USE_TELEMETRY) find_package(T2 REQUIRED) diff --git a/definition/NetworkManager.json b/definition/NetworkManager.json index 65fb6e6c..f79bd812 100644 --- a/definition/NetworkManager.json +++ b/definition/NetworkManager.json @@ -8,7 +8,7 @@ "status": "production", "description": "A Unified `NetworkManager` plugin that allows you to manage Ethernet and Wifi interfaces on the device.", "sourcelocation": "https://github.com/rdkcentral/networkmanager/blob/main/definition/NetworkManager.json", - "version": "2.3.0" + "version": "3.3.0" }, "definitions": { "success": { @@ -47,9 +47,9 @@ "example": 24 }, "ula": { - "summary": "The IPv6 Unified Local Address", + "summary": "The IPv6 Unique Local Address", "type": "string", - "example": "d00:410:2016::" + "example": "fd00:410:2016::" }, "gateway": { "summary": "The gateway address", @@ -846,17 +846,27 @@ } }, "StartWiFiScan":{ - "summary": "Initiates WiFi scaning. This method supports scanning for specific range of frequency like 2.4GHz only or 5GHz only or 6GHz only or ALL. When no input passed about the frequency to be scanned, it scans for all. When list of SSIDs to be scanned specifically, it can be passed as input. It publishes 'onAvailableSSIDs' event upon completion.", + "summary": "Initiates WiFi scanning. This method supports scanning specific frequency bands (2.4GHz, 5GHz, 6GHz). When no input is passed for frequency, it scans all supported frequencies. When list of SSIDs to be scanned specifically, it can be passed as input. It publishes 'onAvailableSSIDs' event upon completion.", "events": { "onAvailableSSIDs" : "Triggered when list of SSIDs is available after the scan completes." }, "params": { "type": "object", "properties": { - "frequency": { - "summary": "The frequency to scan. An empty or `null` value scans all frequencies.", - "type": "string", - "example": "5" + "frequencies": { + "summary": "Frequency bands to scan. Omit this field or pass \"ALL\" to scan all frequencies.", + "type": "array", + "items": { + "summary": "The frequency to scan.", + "type": "string", + "enum": [ + "ALL", + "2.4", + "5", + "6" + ], + "example": "2.4" + } }, "ssids": { "summary": "The list of SSIDs to be scanned.", @@ -1233,7 +1243,7 @@ } }, "GetWiFiSignalQuality":{ - "summary": "Get WiFi signal quality of currently connected SSID. The signal quality is identifed based on the Signal to Noise ratio which is calculated as SNR = rssi - noise. The possible states are\n* 'Excellent' : More than 40 dBm\n* 'Good' : 40 dBm to 25 dBm\n* 'Fair' : 25 dBm to 18 dBm\n* 'Weak' : 18 dBm to 0 dBm\n* 'Disconnected' : 0 dBm\n", + "summary": "Get WiFi signal quality of currently connected SSID. The signal quality is identified based on the Signal to Noise ratio which is calculated as SNR = rssi - noise. The possible states are\n* 'Excellent' : More than 40 dBm\n* 'Good' : 40 dBm to 25 dBm\n* 'Fair' : 25 dBm to 18 dBm\n* 'Weak' : 18 dBm to 0 dBm\n* 'Disconnected' : 0 dBm\n", "events":{ "onWiFiSignalQualityChange" : "Triggered when Wifi signal strength switches between Excellent, Good, Fair, Weak." }, @@ -1294,7 +1304,7 @@ "example": 2 }, "EAP": { - "summary": "Supports security mode WPA enterpise", + "summary": "Supports security mode WPA enterprise", "type": "integer", "example": 3 } @@ -1341,7 +1351,7 @@ } }, "SetHostname": { - "summary": "To configure a custom DHCP hostname instead of the default (which is typically the device name).\n\nSetting host name will take effect upon reconnect; like, device reboot, wake-up from deepsleep, while connecting to new Wi-Fi connection, WiFi On/Off, or renewal of the DHCP lease.", + "summary": "To configure a custom DHCP hostname instead of the default (which is typically the default hostname).\n\nSetting host name will take effect upon reconnect; like, device reboot, wake-up from deepsleep, while connecting to new Wi-Fi connection, WiFi On/Off, or renewal of the DHCP lease.", "params": { "type": "object", "properties": { @@ -1455,7 +1465,7 @@ "type": "object", "properties": { "prevState":{ - "summary": "The privious internet connection state", + "summary": "The previous internet connection state", "type": "integer", "example": 1 }, diff --git a/docs/NetworkManagerPlugin.md b/docs/NetworkManagerPlugin.md index 2c31542f..3d109f0a 100644 --- a/docs/NetworkManagerPlugin.md +++ b/docs/NetworkManagerPlugin.md @@ -2,7 +2,7 @@ # NetworkManager Plugin -**Version: 2.3.0** +**Version: 3.3.0** **Status: :black_circle::black_circle::black_circle:** @@ -23,7 +23,7 @@ org.rdk.NetworkManager interface for Thunder framework. ## Scope -This document describes purpose and functionality of the org.rdk.NetworkManager interface (version 2.3.0). It includes detailed specification about its methods provided and notifications sent. +This document describes purpose and functionality of the org.rdk.NetworkManager interface (version 3.3.0). It includes detailed specification about its methods provided and notifications sent. ## Case Sensitivity @@ -89,7 +89,7 @@ NetworkManager interface methods: | [GetPublicIP](#method.GetPublicIP) | Gets the internet/public IP Address of the device | | [Ping](#method.Ping) | Pings the specified endpoint with the specified number of packets | | [Trace](#method.Trace) | Traces the specified endpoint with the specified number of packets using `traceroute` | -| [StartWiFiScan](#method.StartWiFiScan) | Initiates WiFi scaning | +| [StartWiFiScan](#method.StartWiFiScan) | Initiates WiFi scanning | | [StopWiFiScan](#method.StopWiFiScan) | Stops WiFi scanning | | [GetKnownSSIDs](#method.GetKnownSSIDs) | Gets list of saved SSIDs | | [AddToKnownSSIDs](#method.AddToKnownSSIDs) | Saves the SSID, passphrase, and security mode for upcoming and future sessions | @@ -103,7 +103,7 @@ NetworkManager interface methods: | [GetWiFiSignalQuality](#method.GetWiFiSignalQuality) | Get WiFi signal quality of currently connected SSID | | [GetSupportedSecurityModes](#method.GetSupportedSecurityModes) | Returns the Wifi security modes that the device supports | | [GetWifiState](#method.GetWifiState) | Returns the current Wifi State | -| [SetHostname](#method.SetHostname) | To configure a custom DHCP hostname instead of the default (which is typically the device name) | +| [SetHostname](#method.SetHostname) | To configure a custom DHCP hostname instead of the default (which is typically the default hostname) | ## *SetLogLevel [method](#head.Methods)* @@ -418,7 +418,7 @@ Gets the IP setting for the given interface. | result.ipaddress | string | The IP address | | result.prefix | integer | The prefix number | | result.gateway | string | The gateway address | -| result.ula | string | The IPv6 Unified Local Address | +| result.ula | string | The IPv6 Unique Local Address | | result.primarydns | string | The primary DNS address | | result.secondarydns | string | The secondary DNS address | | result.success | boolean | Whether the request succeeded | @@ -453,7 +453,7 @@ Gets the IP setting for the given interface. "ipaddress": "192.168.1.101", "prefix": 24, "gateway": "192.168.1.1", - "ula": "d00:410:2016::", + "ula": "fd00:410:2016::", "primarydns": "192.168.1.1", "secondarydns": "192.168.1.2", "success": true @@ -1006,7 +1006,7 @@ Traces the specified endpoint with the specified number of packets using `tracer ## *StartWiFiScan [method](#head.Methods)* -Initiates WiFi scaning. This method supports scanning for specific range of frequency like 2.4GHz only or 5GHz only or 6GHz only or ALL. When no input passed about the frequency to be scanned, it scans for all. When list of SSIDs to be scanned specifically, it can be passed as input. It publishes 'onAvailableSSIDs' event upon completion. +Initiates WiFi scanning. This method supports scanning specific frequency bands (2.4GHz, 5GHz, 6GHz). When no input is passed for frequency, it scans all supported frequencies. When list of SSIDs to be scanned specifically, it can be passed as input. It publishes 'onAvailableSSIDs' event upon completion. Also see: [onAvailableSSIDs](#event.onAvailableSSIDs) @@ -1015,7 +1015,8 @@ Also see: [onAvailableSSIDs](#event.onAvailableSSIDs) | Name | Type | Description | | :-------- | :-------- | :-------- | | params | object | | -| params?.frequency | string | *(optional)* The frequency to scan. An empty or `null` value scans all frequencies | +| params?.frequencies | array | *(optional)* Frequency bands to scan. Omit this field or pass "ALL" to scan all frequencies | +| params?.frequencies[#] | string | *(optional)* The frequency to scan | | params?.ssids | array | *(optional)* The list of SSIDs to be scanned | | params?.ssids[#] | string | *(optional)* The SSID to scan | @@ -1036,7 +1037,9 @@ Also see: [onAvailableSSIDs](#event.onAvailableSSIDs) "id": 42, "method": "org.rdk.NetworkManager.1.StartWiFiScan", "params": { - "frequency": "5", + "frequencies": [ + "2.4" + ], "ssids": [ "Xfinity Mobile" ] @@ -1558,7 +1561,7 @@ This method takes no parameters. ## *GetWiFiSignalQuality [method](#head.Methods)* -Get WiFi signal quality of currently connected SSID. The signal quality is identifed based on the Signal to Noise ratio which is calculated as SNR = rssi - noise. The possible states are +Get WiFi signal quality of currently connected SSID. The signal quality is identified based on the Signal to Noise ratio which is calculated as SNR = rssi - noise. The possible states are * 'Excellent' : More than 40 dBm * 'Good' : 40 dBm to 25 dBm * 'Fair' : 25 dBm to 18 dBm @@ -1631,7 +1634,7 @@ This method takes no parameters. | result.security.NONE | integer | Security mode for open network | | result.security.WPA_PSK | integer | Supports security mode WPA,WPA-PSK,WPA2-PSK, WPA3-Personal-Transition | | result.security.SAE | integer | Supports security mode WPA3-Personal | -| result.security.EAP | integer | Supports security mode WPA enterpise | +| result.security.EAP | integer | Supports security mode WPA enterprise | | result.success | boolean | Whether the request succeeded | ### Example @@ -1726,7 +1729,7 @@ This method takes no parameters. ## *SetHostname [method](#head.Methods)* -To configure a custom DHCP hostname instead of the default (which is typically the device name). +To configure a custom DHCP hostname instead of the default (which is typically the default hostname). Setting host name will take effect upon reconnect; like, device reboot, wake-up from deepsleep, while connecting to new Wi-Fi connection, WiFi On/Off, or renewal of the DHCP lease. @@ -1891,7 +1894,7 @@ Triggered when internet connection state changed.The possible internet connectio | Name | Type | Description | | :-------- | :-------- | :-------- | | params | object | | -| params.prevState | integer | The privious internet connection state | +| params.prevState | integer | The previous internet connection state | | params.prevStatus | string | The previous internet connection status | | params.state | integer | The internet connection state | | params.status | string | The internet connection status | diff --git a/interface/INetworkManager.h b/interface/INetworkManager.h index 15c739cf..ce8cb509 100644 --- a/interface/INetworkManager.h +++ b/interface/INetworkManager.h @@ -105,7 +105,7 @@ namespace WPEFramework enum WIFIFrequency : uint8_t { - WIFI_FREQUENCY_NONE /* @text: NONE */, + WIFI_FREQUENCY_ALL /* @text: ALL */, WIFI_FREQUENCY_2_4_GHZ /* @text: 2.4GHz */, WIFI_FREQUENCY_5_GHZ /* @text: 5GHz */, WIFI_FREQUENCY_6_GHZ /* @text: 6GHz */, @@ -248,7 +248,7 @@ namespace WPEFramework // WiFi Specific Methods /* @brief Initiate a WIFI Scan; This is Async method and returns the scan results as Event */ - virtual uint32_t StartWiFiScan(const string& frequency /* @in */, IStringIterator* const ssids/* @in */) = 0; + virtual uint32_t StartWiFiScan(IStringIterator* const frequencies /* @in */, IStringIterator* const ssids/* @in */) = 0; virtual uint32_t StopWiFiScan(void) = 0; virtual uint32_t GetKnownSSIDs(IStringIterator*& ssids /* @out */) = 0; diff --git a/legacy/LegacyWiFiManagerAPIs.cpp b/legacy/LegacyWiFiManagerAPIs.cpp index 9132ed09..9a6573ff 100644 --- a/legacy/LegacyWiFiManagerAPIs.cpp +++ b/legacy/LegacyWiFiManagerAPIs.cpp @@ -634,12 +634,27 @@ namespace WPEFramework { LOG_INPARAM(); uint32_t rc = Core::ERROR_GENERAL; - string frequency{}; + Exchange::INetworkManager::IStringIterator* frequencies = nullptr; Exchange::INetworkManager::IStringIterator* ssids = NULL; - if (parameters.HasLabel("frequency")) - frequency = parameters["frequency"].String(); + { + std::vector frequencyList; + if (Core::JSON::Variant::type::STRING == parameters["frequency"].Content()) + { + frequencyList.push_back(parameters["frequency"].String()); + } + else + { + NMLOG_ERROR("Unexpected variant type in frequency parameter."); + returnJson(rc); + } + + frequencies = (Core::Service::Create(frequencyList)); + if (frequencies == nullptr) { + returnJson(rc); + } + } if (parameters.HasLabel("ssid")) { @@ -654,6 +669,8 @@ namespace WPEFramework ssids = (Core::Service::Create(inputSSIDlist)); if (ssids == nullptr) { + if (frequencies) + frequencies->Release(); returnJson(rc); } } @@ -661,12 +678,15 @@ namespace WPEFramework auto _nwmgr = m_service->QueryInterfaceByCallsign(NETWORK_MANAGER_CALLSIGN); if (_nwmgr) { - rc = _nwmgr->StartWiFiScan(frequency, ssids); + rc = _nwmgr->StartWiFiScan(frequencies, ssids); _nwmgr->Release(); } else rc = Core::ERROR_UNAVAILABLE; + if (frequencies) + frequencies->Release(); + if (ssids) ssids->Release(); diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt index a5b12d94..6090b8f1 100644 --- a/plugin/CMakeLists.txt +++ b/plugin/CMakeLists.txt @@ -81,6 +81,7 @@ add_library(${MODULE_IMPL_NAME} SHARED NetworkManagerConnectivity.cpp NetworkManagerStunClient.cpp NetworkManagerLogger.cpp + NetworkManagerPowerClient.cpp Module.cpp) if(ENABLE_GNOME_NETWORKMANAGER) diff --git a/plugin/NetworkManagerImplementation.cpp b/plugin/NetworkManagerImplementation.cpp index b4b5bca6..b10b375b 100644 --- a/plugin/NetworkManagerImplementation.cpp +++ b/plugin/NetworkManagerImplementation.cpp @@ -19,6 +19,9 @@ #include #include +#include +#include +#include #include "NetworkManagerImplementation.h" #if USE_TELEMETRY @@ -54,6 +57,8 @@ namespace WPEFramework m_wlanConnected.store(false); m_ethEnabled.store(false); m_wlanEnabled.store(false); + m_ethDisconnectedForSleep.store(false); + m_wlanDisconnectedForSleep.store(false); /* Set NetworkManager Out-Process name to be NWMgrPlugin */ Core::ProcessInfo().Name("NWMgrPlugin"); @@ -71,6 +76,7 @@ namespace WPEFramework NetworkManagerImplementation::~NetworkManagerImplementation() { NMLOG_INFO("NetworkManager Out-Of-Process Shutdown/Cleanup"); + m_powerClient.reset(); connectivityMonitor.stopConnectivityMonitor(); _instance = nullptr; platform_deinit(); @@ -199,6 +205,7 @@ namespace WPEFramework NetworkManagerImplementation::platform_init(); /* change gnome networkmanager or netsrvmgr logg level */ NetworkManagerImplementation::platform_logging(static_cast (config.loglevel.Value())); + m_powerClient.reset(new NetworkManagerPowerClient(*this)); return(Core::ERROR_NONE); } @@ -590,7 +597,6 @@ namespace WPEFramework return; } - void NetworkManagerImplementation::filterScanResults(JsonArray &ssids) { JsonArray result; @@ -598,27 +604,41 @@ namespace WPEFramework std::unordered_set scanForSsidsSet(m_filterSsidslist.begin(), m_filterSsidslist.end()); // If neither SSID list nor frequency is provided, exit - if (m_filterSsidslist.empty() && m_filterfrequency.empty()) + if (m_filterSsidslist.empty() && m_filterFrequencies.empty()) { NMLOG_DEBUG("Neither SSID nor Frequency is provided. Exiting function."); return; } - if (!m_filterfrequency.empty()) - { - filterFreq = std::stod(m_filterfrequency); - } - for (int i = 0; i < ssids.Length(); i++) { JsonObject object = ssids[i].Object(); string ssid = object["ssid"].String(); string frequency = object["frequency"].String(); - double frequencyValue = std::stod(frequency); bool ssidMatches = scanForSsidsSet.empty() || scanForSsidsSet.find(ssid) != scanForSsidsSet.end(); - bool freqMatches = m_filterfrequency.empty() || (filterFreq == frequencyValue); + bool freqMatches = m_filterFrequencies.empty(); + if (!freqMatches) + { + for (const auto& selectedFrequency : m_filterFrequencies) + { + if (selectedFrequency == "ALL") + { + freqMatches = true; + break; + } + else + { + filterFreq = std::stod(selectedFrequency); + if (filterFreq == frequencyValue) + { + freqMatches = true; + break; + } + } + } + } if (ssidMatches && freqMatches) result.Add(object); @@ -654,8 +674,10 @@ namespace WPEFramework { if(interface == "eth0") { +#if defined(NM_BACKEND_GDBUS) || defined(NM_BACKEND_RDK) m_ethIPv4Address = {}; m_ethIPv6Address = {}; +#endif m_ethConnected.store(false); setDefaultInterface("wlan0"); // If WiFi is connected, make it the default interface // As default interface is changed to wlan0, switch connectivity monitor to initial check @@ -663,8 +685,10 @@ namespace WPEFramework } else if(interface == "wlan0") { +#if defined(NM_BACKEND_GDBUS) || defined(NM_BACKEND_RDK) m_wlanIPv4Address = {}; m_wlanIPv6Address = {}; +#endif m_wlanConnected.store(false); bool triggerConnectivityCheck; if(m_ethConnected.load()) @@ -775,7 +799,9 @@ namespace WPEFramework } _notificationLock.Lock(); - NMLOG_INFO("Posting onIPAddressChange %s - %s", ipaddress.c_str(), interface.c_str()); + NMLOG_INFO("Posting onIPAddressChange %s: %s %s %s", + (Exchange::INetworkManager::IP_ACQUIRED == status) ? "IP acquired" : "IP lost", + interface.c_str(), ipversion.c_str(), ipaddress.c_str()); for (const auto callback : _notificationCallbacks) { callback->onIPAddressChange(interface, ipversion, ipaddress, status); } @@ -1184,5 +1210,272 @@ namespace WPEFramework } #endif } + + void NetworkManagerImplementation::OnPowerModePreChange( + const Exchange::IPowerManager::PowerState currentState, + const Exchange::IPowerManager::PowerState newState, + std::function sendAck) + { + // Called from NetworkManagerPowerClient's power thread. + NMLOG_DEBUG("OnPowerModePreChange: current=%d new=%d", + static_cast(currentState), static_cast(newState)); + + using PowerState = Exchange::IPowerManager::PowerState; + + if (newState == PowerState::POWER_STATE_STANDBY_DEEP_SLEEP) + { + if (m_wlanEnabled.load() && m_wlanConnected.load()) + { + NMLOG_INFO("OnPowerModePreChange: going to DeepSleep — disconnecting WiFi"); + + uint32_t rcWifiDown = WiFiDisconnect(); + if (rcWifiDown == Core::ERROR_NONE) + { + m_wlanDisconnectedForSleep.store(true); + } + else + { + NMLOG_ERROR("OnPowerModePreChange: WiFiDisconnect failed (rc=%u), will not reconnect on wakeup", rcWifiDown); + } + } + else + { + NMLOG_DEBUG("OnPowerModePreChange: going to DeepSleep — WiFi not connected, skipping disconnect"); + } +#ifdef ENABLE_ETHERNET_CONNECTION_HANDLING + if (m_ethEnabled.load() && m_ethConnected.load()) + { + NMLOG_INFO("OnPowerModePreChange: going to DeepSleep — deactivating Ethernet"); + + uint32_t rcEthDown = EthernetDeactivate(); + if (rcEthDown == Core::ERROR_NONE) + { + m_ethDisconnectedForSleep.store(true); + } + else + { + NMLOG_ERROR("OnPowerModePreChange: EthernetDeactivate failed (rc=%u), will not activate on wakeup", rcEthDown); + } + } + else + { + NMLOG_DEBUG("OnPowerModePreChange: going to DeepSleep — Ethernet not activated, skipping deactivate"); + } +#endif + } + else if (currentState == PowerState::POWER_STATE_STANDBY_DEEP_SLEEP) + { + if (m_wlanDisconnectedForSleep.load()) + { + if (!m_lastConnectedSSID.empty()) + { + NMLOG_INFO("OnPowerModePreChange: waking from DeepSleep — reconnecting to '%s'", + m_lastConnectedSSID.c_str()); + uint32_t rcWifiUp = ConnectToKnownSSID(m_lastConnectedSSID); + if (rcWifiUp == Core::ERROR_NONE) + { + m_wlanDisconnectedForSleep.store(false); + } + else + { + NMLOG_ERROR("OnPowerModePreChange: ConnectToKnownSSID failed (rc=%u)", rcWifiUp); + } + } + else + { + NMLOG_INFO("OnPowerModePreChange: waking from DeepSleep — no last SSID, skipping reconnect"); + } + } + else + { + NMLOG_INFO("OnPowerModePreChange: waking from DeepSleep — WiFi was not connected or was already down before sleep, skipping reconnect"); + } + } + sendAck(); + } + + void NetworkManagerImplementation::OnPowerModeChanged( + const Exchange::IPowerManager::PowerState currentState, + const Exchange::IPowerManager::PowerState newState) + { + NMLOG_INFO("OnPowerModeChanged: current=%d new=%d", + static_cast(currentState), static_cast(newState)); + if (currentState == Exchange::IPowerManager::PowerState::POWER_STATE_STANDBY_DEEP_SLEEP) { + + if (m_wlanEnabled.load() && m_wlanConnected.load()) + { + // Waking from DeepSleep with Network Standby ON: the AP may have + // changed channel while the device slept (802.11 CSA). Trigger an + // active scan so the driver discovers the AP on its new channel. + NMLOG_INFO("OnPowerModeChanged: waking from DeepSleep, triggering active WiFi scan"); + if (StartWiFiScan(nullptr, nullptr) != Core::ERROR_NONE) + { + NMLOG_ERROR("OnPowerModeChanged: StartWiFiScan failed"); + } + + NMLOG_INFO("OnPowerModeChanged: waking from DeepSleep, requesting DHCP lease on wlan0"); + if (ReacquireDHCPLease("wlan0") != Core::ERROR_NONE) + { + NMLOG_ERROR("OnPowerModeChanged: ReacquireDHCPLease(wlan0) failed"); + } + } + if (m_ethEnabled.load() && m_ethConnected.load()) + { + NMLOG_INFO("OnPowerModeChanged: waking from DeepSleep, requesting DHCP lease on eth0"); + if (ReacquireDHCPLease("eth0") != Core::ERROR_NONE) + { + NMLOG_ERROR("OnPowerModeChanged: ReacquireDHCPLease(eth0) failed"); + } + } + } + } + + bool isIPv4LinkLocal(const std::string& addr) + { + struct in_addr sa{}; + return inet_pton(AF_INET, addr.c_str(), &sa) == 1 && + (ntohl(sa.s_addr) & 0xffff0000u) == 0xa9fe0000u; + } + + bool isIPv6LinkLocal(const std::string& addr) + { + struct in6_addr sa6{}; + return inet_pton(AF_INET6, addr.c_str(), &sa6) == 1 && + sa6.s6_addr[0] == 0xfe && (sa6.s6_addr[1] & 0xc0) == 0x80; + } + + bool isIPv6ULA(const std::string& addr) + { + struct in6_addr sa6{}; + return inet_pton(AF_INET6, addr.c_str(), &sa6) == 1 && + (sa6.s6_addr[0] & 0xfe) == 0xfc; + } + + /* Parse a MAC string into 6 bytes. Accepts both "AA:BB:CC:DD:EE:FF" and "aabbccddeeff". */ + static bool parseMac(const std::string& mac, uint8_t out[6]) + { + unsigned int b[6]; + if (mac.size() >= 17 && + sscanf(mac.c_str(), "%02x:%02x:%02x:%02x:%02x:%02x", + &b[0], &b[1], &b[2], &b[3], &b[4], &b[5]) == 6) + { + for (int i = 0; i < 6; ++i) out[i] = static_cast(b[i]); + return true; + } + if (mac.size() >= 12 && + sscanf(mac.c_str(), "%02x%02x%02x%02x%02x%02x", + &b[0], &b[1], &b[2], &b[3], &b[4], &b[5]) == 6) + { + for (int i = 0; i < 6; ++i) out[i] = static_cast(b[i]); + return true; + } + return false; + } + + bool isIPv6MacBased(const std::string& ipv6Addr, const std::string& macAddr) + { + struct in6_addr sa6{}; + uint8_t mac[6]; + if (inet_pton(AF_INET6, ipv6Addr.c_str(), &sa6) != 1 || !parseMac(macAddr, mac)) + return false; + + /* Build 8-byte EUI-64 identifier from 6-byte MAC: + mac[0..2] | ff:fe | mac[3..5], then flip the U/L bit (bit 1) of byte 0. */ + uint8_t eui64[8]; + eui64[0] = mac[0] ^ 0x02; // flip Universal/Local bit + eui64[1] = mac[1]; + eui64[2] = mac[2]; + eui64[3] = 0xff; + eui64[4] = 0xfe; + eui64[5] = mac[3]; + eui64[6] = mac[4]; + eui64[7] = mac[5]; + + /* Compare against the interface-ID (last 8 bytes) of the IPv6 address. */ + if (memcmp(&sa6.s6_addr[8], eui64, 8) == 0) + { + NMLOG_DEBUG("MAC %s based global v6 address %s", macAddr.c_str(), ipv6Addr.c_str()); + return true; + } + return false; + } + + bool NetworkManagerImplementation::lookupIpCache( + const std::string& iface, const std::string& ipFamily, + Exchange::INetworkManager::IPAddress& out) const + { + std::lock_guard lock(m_ipCacheMutex); + auto it = m_ipCacheMap.find({iface, ipFamily}); + if (it != m_ipCacheMap.end() && it->second.valid) { + out = it->second.toIPAddress(); + out.ipversion = ipFamily; + return true; + } + return false; + } + + std::set NetworkManagerImplementation::swapIpCache( + const std::string& iface, const std::string& ipFamily, + IpFamilyCache newCache) + { + std::set oldKeys; + std::lock_guard lock(m_ipCacheMutex); + IpFamilyCache& cache = m_ipCacheMap[{iface, ipFamily}]; + for (const auto& kv : cache.globalAddresses) + oldKeys.insert(kv.first); + cache = std::move(newCache); + return oldKeys; + } + + Exchange::INetworkManager::IPAddress IpFamilyCache::toIPAddress() const + { + Exchange::INetworkManager::IPAddress addr{}; + /* Detect IP version from any available address. */ + bool isIPv6 = false; + { + const std::string* sample = nullptr; + if (!globalAddresses.empty()) + sample = &globalAddresses.begin()->first; + else if (!uniqueLocalAddresses.empty()) + sample = &(*uniqueLocalAddresses.begin()); + else if (!linkLocalAddresses.empty()) + sample = &(*linkLocalAddresses.begin()); + if (sample) { + struct in6_addr sa6{}; + isIPv6 = (inet_pton(AF_INET6, sample->c_str(), &sa6) == 1); + } + } + addr.ipversion = isIPv6 ? "IPv6" : "IPv4"; + addr.autoconfig = autoconfig; + addr.dhcpserver = dhcpserver; + addr.ula = uniqueLocalAddresses.empty() ? "" : *uniqueLocalAddresses.begin(); + addr.gateway = gateway; + addr.primarydns = primarydns; + addr.secondarydns = secondarydns; + /* Prefer non-MAC-based global; fall back to MAC-based if all are MAC-based. */ + const std::string* bestGlobal = nullptr; + uint32_t bestPrefix = 0; + const std::string* fallbackMac = nullptr; + uint32_t fallbackMacPrefix = 0; + for (const auto& kv : globalAddresses) { + if (kv.second.type == ADDR_GLOBAL) { + bestGlobal = &kv.first; + bestPrefix = kv.second.prefix; + break; + } + if (!fallbackMac) { + fallbackMac = &kv.first; + fallbackMacPrefix = kv.second.prefix; + } + } + if (bestGlobal) { + addr.ipaddress = *bestGlobal; + addr.prefix = bestPrefix; + } else if (fallbackMac) { + addr.ipaddress = *fallbackMac; + addr.prefix = fallbackMacPrefix; + } + return addr; + } } } diff --git a/plugin/NetworkManagerImplementation.h b/plugin/NetworkManagerImplementation.h index f5bd49b1..65baa0b1 100644 --- a/plugin/NetworkManagerImplementation.h +++ b/plugin/NetworkManagerImplementation.h @@ -26,7 +26,10 @@ #include #include #include +#include #include +#include +#include using namespace std; @@ -34,9 +37,9 @@ using namespace std; #include "NetworkManagerLogger.h" #include "NetworkManagerConnectivity.h" #include "NetworkManagerStunClient.h" +#include "NetworkManagerPowerClient.h" /* Forward declarations to avoid pulling GLib/libnm headers into this header */ -typedef struct _NMClient NMClient; typedef struct _GMainContext GMainContext; /* @@ -62,7 +65,49 @@ namespace WPEFramework { namespace Plugin { + /* Returns true if the given string is an IPv4 link-local address (169.254.0.0/16). */ + bool isIPv4LinkLocal(const std::string& addr); + + /* Returns true if the given string is an IPv6 link-local address (fe80::/10). */ + bool isIPv6LinkLocal(const std::string& addr); + + /* Returns true if the given string is an IPv6 Unique Local Address (ULA, fc00::/7). */ + bool isIPv6ULA(const std::string& addr); + + /* Returns true if the given global IPv6 address is derived (EUI-64) from the MAC. */ + bool isIPv6MacBased(const std::string& ipv6Addr, const std::string& macAddr); + + /* Sub-classification of global-scope addresses in the IP cache. */ + enum GlobalAddressType : uint8_t { + ADDR_GLOBAL, // non-MAC-based global (preferred by GetIPSettings) + ADDR_GLOBAL_MAC_BASED, // EUI-64 global derived from interface MAC (fallback) + }; + + struct GlobalAddressInfo { + uint32_t prefix; + GlobalAddressType type; + GlobalAddressInfo() : prefix(0), type(ADDR_GLOBAL) {} + GlobalAddressInfo(uint32_t p, GlobalAddressType t) : prefix(p), type(t) {} + }; + + /* Per-interface, per-address-family cache populated by libnm events. */ + struct IpFamilyCache { + bool valid = false; + std::map globalAddresses; // event-diffable global addresses + std::set linkLocalAddresses; // fe80::/10 or 169.254.x.x — not diffed for events + std::set uniqueLocalAddresses; // fc00::/7 (IPv6 only) — not diffed for events + std::string gateway; + std::string primarydns; + std::string secondarydns; + std::string dhcpserver; + bool autoconfig = false; + + Exchange::INetworkManager::IPAddress toIPAddress() const; + void clear() { *this = IpFamilyCache{}; } + }; + class NetworkManagerImplementation : public Exchange::INetworkManager + , public INetworkPowerCallback { enum NetworkEvents { @@ -216,7 +261,7 @@ namespace WPEFramework // WiFi Specific Methods /* @brief Initiate a WIFI Scan; This is Async method and returns the scan results as Event */ - uint32_t StartWiFiScan(const string& frequency /* @in */, IStringIterator* const ssids/* @in */) override; + uint32_t StartWiFiScan(IStringIterator* const frequencies /* @in */, IStringIterator* const ssids/* @in */) override; uint32_t StopWiFiScan(void) override; uint32_t GetKnownSSIDs(IStringIterator*& ssids /* @out */) override; @@ -226,6 +271,8 @@ namespace WPEFramework uint32_t WiFiConnect(const WiFiConnectTo& ssid /* @in */) override; uint32_t WiFiDisconnect(void) override; + uint32_t EthernetDeactivate(void); + uint32_t ReacquireDHCPLease(const string& iface); uint32_t GetConnectedSSID(WiFiSSIDInfo& ssidInfo /* @out */) override; uint32_t StartWPS(const WiFiWPS& method /* @in */, const string& wps_pin /* @in */) override; @@ -277,6 +324,13 @@ namespace WPEFramework void ReportWiFiSignalQualityChange(const string ssid, const int strength, const int noise, const int snr, const Exchange::INetworkManager::WiFiSignalQuality quality); void logTelemetry(const std::string& eventName, const std::string& message); + // INetworkPowerCallback overrides + void OnPowerModePreChange(const Exchange::IPowerManager::PowerState currentState, + const Exchange::IPowerManager::PowerState newState, + std::function sendAck) override; + void OnPowerModeChanged(const Exchange::IPowerManager::PowerState currentState, + const Exchange::IPowerManager::PowerState newState) override; + private: void platform_init(void); void platform_deinit(void); @@ -301,7 +355,7 @@ namespace WPEFramework uint16_t m_stunBindTimeout; uint16_t m_stunCacheTimeout; std::thread m_registrationThread; - string m_filterfrequency; + std::vector m_filterFrequencies; std::vector m_filterSsidslist; std::thread m_monitorThread; @@ -314,18 +368,28 @@ namespace WPEFramework std::atomic m_stopThread{false}; std::mutex m_condVariableMutex; std::condition_variable m_condVariable; + std::unique_ptr m_powerClient; public: +#if defined(NM_BACKEND_GDBUS) || defined(NM_BACKEND_RDK) IPAddress m_ethIPv4Address; IPAddress m_wlanIPv4Address; IPAddress m_ethIPv6Address; IPAddress m_wlanIPv6Address; +#endif + bool lookupIpCache(const std::string& iface, const std::string& ipFamily, + Exchange::INetworkManager::IPAddress& out) const; + std::set swapIpCache(const std::string& iface, + const std::string& ipFamily, + IpFamilyCache newCache); + std::atomic m_ethConnected; std::atomic m_wlanConnected; std::atomic m_ethEnabled; std::atomic m_wlanEnabled; + std::atomic m_ethDisconnectedForSleep; + std::atomic m_wlanDisconnectedForSleep; std::string m_lastConnectedSSID; - NMClient *m_nmClient{nullptr}; /* proxy NMClient — bound to m_nmContext */ - GMainContext *m_nmContext{nullptr}; /* isolated context, not the global default */ + GMainContext *m_nmContext{nullptr}; /* isolated context for per-call NMClient creation */ mutable ConnectivityMonitor connectivityMonitor; string getDefaultInterface() const @@ -343,6 +407,8 @@ namespace WPEFramework private: string m_defaultInterface; mutable std::mutex m_defaultInterfaceMutex; + std::map, IpFamilyCache> m_ipCacheMap; + mutable std::mutex m_ipCacheMutex; }; } } diff --git a/plugin/NetworkManagerJsonEnum.h b/plugin/NetworkManagerJsonEnum.h index 07b9318a..d47768e1 100644 --- a/plugin/NetworkManagerJsonEnum.h +++ b/plugin/NetworkManagerJsonEnum.h @@ -96,4 +96,11 @@ ENUM_CONVERSION_BEGIN(Exchange::INetworkManager::IPStatus) { Exchange::INetworkManager::IPStatus::IP_ACQUIRED, _TXT("ACQUIRED") }, ENUM_CONVERSION_END(Exchange::INetworkManager::IPStatus) +ENUM_CONVERSION_BEGIN(Exchange::INetworkManager::WIFIFrequency) + { Exchange::INetworkManager::WIFIFrequency::WIFI_FREQUENCY_ALL, _TXT("ALL") }, + { Exchange::INetworkManager::WIFIFrequency::WIFI_FREQUENCY_2_4_GHZ, _TXT("2.4") }, + { Exchange::INetworkManager::WIFIFrequency::WIFI_FREQUENCY_5_GHZ, _TXT("5") }, + { Exchange::INetworkManager::WIFIFrequency::WIFI_FREQUENCY_6_GHZ, _TXT("6") }, +ENUM_CONVERSION_END(Exchange::INetworkManager::WIFIFrequency) + } diff --git a/plugin/NetworkManagerJsonRpc.cpp b/plugin/NetworkManagerJsonRpc.cpp index 5ed0b60f..d49e6c5d 100644 --- a/plugin/NetworkManagerJsonRpc.cpp +++ b/plugin/NetworkManagerJsonRpc.cpp @@ -642,19 +642,40 @@ namespace WPEFramework { LOG_INPARAM(); uint32_t rc = Core::ERROR_GENERAL; - string frequency{}; - Exchange::INetworkManager::IStringIterator* ssids = NULL; + Exchange::INetworkManager::IStringIterator* frequencies = nullptr; + Exchange::INetworkManager::IStringIterator* ssids = NULL; - if (parameters.HasLabel("frequency")) - frequency = parameters["frequency"].String(); + if (parameters.HasLabel("frequencies")) + { + JsonArray array = parameters["frequencies"].Array(); + std::vector frequencyList; + JsonArray::Iterator index(array.Elements()); - if (parameters.HasLabel("ssids")) + while (index.Next() == true) + { + if (Core::JSON::Variant::type::STRING == index.Current().Content()) + { + frequencyList.push_back(index.Current().String()); + } + else + { + NMLOG_ERROR("Unexpected variant type in frequency array."); + returnJson(rc); + } + } + frequencies = Core::Service::Create(frequencyList); + if (frequencies == nullptr) { + returnJson(rc); + } + } + + if (parameters.HasLabel("ssids")) { JsonArray array = parameters["ssids"].Array(); std::vector ssidslist; - JsonArray::Iterator index(array.Elements()); + JsonArray::Iterator index(array.Elements()); - while (index.Next() == true) + while (index.Next() == true) { if (Core::JSON::Variant::type::STRING == index.Current().Content()) { @@ -663,20 +684,27 @@ namespace WPEFramework else { NMLOG_ERROR("Unexpected variant type in SSID array."); + if (frequencies) + frequencies->Release(); returnJson(rc); } } ssids = (Core::Service::Create(ssidslist)); if(ssids == nullptr){ + if (frequencies) + frequencies->Release(); returnJson(rc); } } if (_networkManager) - rc = _networkManager->StartWiFiScan(frequency, ssids); + rc = _networkManager->StartWiFiScan(frequencies, ssids); else rc = Core::ERROR_UNAVAILABLE; + if (frequencies) + frequencies->Release(); + if (ssids) ssids->Release(); diff --git a/plugin/NetworkManagerPowerClient.cpp b/plugin/NetworkManagerPowerClient.cpp new file mode 100644 index 00000000..3d6d2ed9 --- /dev/null +++ b/plugin/NetworkManagerPowerClient.cpp @@ -0,0 +1,303 @@ +/** +* If not stated otherwise in this file or this component's LICENSE +* file the following copyright and licenses apply: +* +* Copyright 2026 RDK Management +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +**/ + +#include "NetworkManagerPowerClient.h" +#include "NetworkManagerLogger.h" +#include + +using namespace WPEFramework; +using namespace WPEFramework::Exchange; +using namespace WPEFramework::Plugin; + +// --------------------------------------------------------------------------- +// NetworkManagerPowerClient +// --------------------------------------------------------------------------- + +NetworkManagerPowerClient::NetworkManagerPowerClient(INetworkPowerCallback& callback) + : mCallback(callback) + , mPreChangeNotification(*this) + , mChangedNotification(*this) +{ + NMLOG_INFO("connecting to PowerManager"); + if (auto r = Open(RPC::CommunicationTimeOut, Connector(), "org.rdk.PowerManager"); r == Core::ERROR_NONE) { + // Connected; Operational() will be called by the framework when the proxy is ready + } else { + NMLOG_ERROR("failed to open link to PowerManager (error %u)", r); + } +} + +NetworkManagerPowerClient::~NetworkManagerPowerClient() +{ + NMLOG_INFO("shutting down"); + // Stop the power-event thread first so any in-flight work completes + // before we release the COM-RPC proxy. + mStopThread = true; + mQueueCv.notify_one(); + if (mPowerThread.joinable()) { + mPowerThread.join(); + } + unregisterEvents(); + Close(Core::infinite); +} + +bool NetworkManagerPowerClient::IsValid() const +{ + LOG_ENTRY_FUNCTION(); + + return mPowerManager != nullptr; +} + +bool NetworkManagerPowerClient::getNetworkStandbyMode() const +{ + LOG_ENTRY_FUNCTION(); + + bool standbyMode = false; + if (IsValid()) { + if (auto r = mPowerManager->GetNetworkStandbyMode(standbyMode); r != Core::ERROR_NONE) { + NMLOG_ERROR("GetNetworkStandbyMode failed (%u)", r); + } + } + return standbyMode; +} + +void NetworkManagerPowerClient::sendPowerModePreChangeComplete(int transactionId) +{ + LOG_ENTRY_FUNCTION(); + + if (IsValid()) { + if (mClientId == 0) { + NMLOG_ERROR("sendPowerModePreChangeComplete called with invalid clientId=0, skipping"); + return; + } + NMLOG_DEBUG("sending PowerModePreChangeComplete for transactionId=%d, mClientId=%u", transactionId, mClientId); + mPowerManager->PowerModePreChangeComplete(mClientId, transactionId); + } +} + +void NetworkManagerPowerClient::sendDelayPowerModeChange(int transactionId, int seconds) +{ + LOG_ENTRY_FUNCTION(); + + if (IsValid()) { + if (mClientId == 0) { + NMLOG_ERROR("sendDelayPowerModeChange called with invalid clientId=0, skipping"); + return; + } + if (auto r = mPowerManager->DelayPowerModeChangeBy(mClientId, transactionId, seconds); r != Core::ERROR_NONE) { + NMLOG_ERROR("DelayPowerModeChangeBy failed (%u)", r); + } + } +} + +void NetworkManagerPowerClient::Operational(bool upAndRunning) +{ + NMLOG_DEBUG("Operational(%s)", upAndRunning ? "true" : "false"); + if (upAndRunning) { + if (!IsValid()) { + mPowerManager = Interface(); + registerEvents(); + // Start the dedicated power-event thread after registration so it + // is ready to handle events as soon as they can arrive. + mStopThread = false; + mPowerThread = std::thread(&NetworkManagerPowerClient::powerThreadLoop, this); + } + } else { + // Stop the power-event thread before unregistering so any in-flight + // event that was already enqueued is drained. + mStopThread = true; + mQueueCv.notify_one(); + if (mPowerThread.joinable()) { + mPowerThread.join(); + } + unregisterEvents(); + } +} + +void NetworkManagerPowerClient::registerEvents() +{ + NMLOG_DEBUG("registering events"); + if (!IsValid()) { + NMLOG_ERROR("not in valid state, skipping event registration"); + return; + } + if (auto r = mPowerManager->AddPowerModePreChangeClient("org.rdk.NetworkManager", mClientId); r != Core::ERROR_NONE) { + NMLOG_ERROR("AddPowerModePreChangeClient failed (%u) — skipping pre-change sink", r); + // mClientId stays 0; do NOT register mPreChangeNotification + } else { + NMLOG_INFO("registered as pre-change client, mClientId=%u", mClientId); + if (auto r2 = mPowerManager->Register(&mPreChangeNotification); r2 != Core::ERROR_NONE) { + NMLOG_ERROR("register(preChange) failed (%u)", r2); + } + } + if (auto r = mPowerManager->Register(&mChangedNotification); r != Core::ERROR_NONE) { + NMLOG_ERROR("register(changed) failed (%u)", r); + } +} + +void NetworkManagerPowerClient::unregisterEvents() +{ + NMLOG_DEBUG("unregistering events"); + if (!IsValid()) { + NMLOG_ERROR("not in valid state, skipping event unregistration"); + return; + } + // NOTE: RemovePowerModePreChangeClient MUST be called before Unregister + if (mClientId != 0) { + if (auto r = mPowerManager->RemovePowerModePreChangeClient(mClientId); r != Core::ERROR_NONE) { + NMLOG_ERROR("removePowerModePreChangeClient failed (%u)", r); + } + if (auto r = mPowerManager->Unregister(&mPreChangeNotification); r != Core::ERROR_NONE) { + NMLOG_ERROR("unregister(preChange) failed (%u)", r); + } + mClientId = 0; + } + if (auto r = mPowerManager->Unregister(&mChangedNotification); r != Core::ERROR_NONE) { + NMLOG_ERROR("unregister(changed) failed (%u)", r); + } + + mPowerManager->Release(); + mPowerManager = nullptr; +} + +// --------------------------------------------------------------------------- +// Power event thread +// --------------------------------------------------------------------------- + +void NetworkManagerPowerClient::powerThreadLoop() +{ + NMLOG_DEBUG("power event thread started"); + while (true) { + PowerEvent event{}; + { + std::unique_lock lock(mQueueMutex); + mQueueCv.wait(lock, [this]{ return !mEventQueue.empty() || mStopThread.load(); }); + + if (mStopThread) { + // Drain remaining events with fast acks before exiting so + // PowerManager is never left waiting on a stale transaction. + // CHANGED events have no ack protocol — skip them. + std::vector pending; + while (!mEventQueue.empty()) { + pending.push_back(mEventQueue.front()); + mEventQueue.pop(); + } + lock.unlock(); + for (const auto& e : pending) { + if (e.type == PowerEvent::EventType::PRE_CHANGE) { + sendPowerModePreChangeComplete(e.transactionId); + } + } + break; + } + + event = mEventQueue.front(); + mEventQueue.pop(); + } + // Lock released — process event on this thread (blocking is fine here) + + const bool toDeepSleep = (event.newState == PowerState::POWER_STATE_STANDBY_DEEP_SLEEP); + const bool fromDeepSleep = (event.currentState == PowerState::POWER_STATE_STANDBY_DEEP_SLEEP); + + if (event.type == PowerEvent::EventType::CHANGED) { + // Wakeup notification — no ack required. + if (fromDeepSleep && event.standbyMode) { + NMLOG_INFO("power thread — wakeup from DeepSleep standby ON"); + mCallback.OnPowerModeChanged(event.currentState, event.newState); + } else { + NMLOG_DEBUG("power thread — CHANGED event, no action (fromDeepSleep=%d networkStandbyMode=%d)", + fromDeepSleep, event.standbyMode); + } + continue; + } + + // PRE_CHANGE event processing below + auto sendAck = [transactionId = event.transactionId, this]() { + sendPowerModePreChangeComplete(transactionId); + }; + + if ((toDeepSleep || fromDeepSleep) && !event.standbyMode) { + // Deep-sleep transition with Network Standby OFF: delegate to + // NetworkManagerImplementation (WiFiDisconnect / reconnect) which + // will call sendAck() when done. + NMLOG_INFO("power thread — %s DeepSleep standby OFF", + toDeepSleep ? "to" : "from"); + mCallback.OnPowerModePreChange(event.currentState, event.newState, sendAck); + } else { + // standby ON or non-DeepSleep: no WiFi action needed, ack immediately. + NMLOG_DEBUG("power thread ack (standbyMode=%d toDeepSleep=%d fromDeepSleep=%d)", + event.standbyMode, toDeepSleep, fromDeepSleep); + sendAck(); + } + } + NMLOG_INFO("power event thread stopped"); +} + +// --------------------------------------------------------------------------- +// PreChangeNotification +// --------------------------------------------------------------------------- + +void NetworkManagerPowerClient::PreChangeNotification::OnPowerModePreChange( + const PowerState currentState, const PowerState newState, + const int transactionId, const int stateChangeAfter) +{ + NMLOG_DEBUG("OnPowerModePreChange current=%d new=%d txId=%d after=%ds", + static_cast(currentState), static_cast(newState), transactionId, stateChangeAfter); + + // Query standby mode + const bool standbyMode = mClient.getNetworkStandbyMode(); + + // Cache for use by ChangedNotification + mClient.mLastChangeStandbyMode = standbyMode; + + // Reserve a delay window now (before returning) so PowerManager knows to + // wait at least 5 s. + if (newState == PowerState::POWER_STATE_STANDBY_DEEP_SLEEP && !standbyMode) { + mClient.sendDelayPowerModeChange(transactionId, 5); + } + + // Enqueue and return immediately so the COM-RPC dispatcher thread is freed. + { + std::lock_guard lock(mClient.mQueueMutex); + mClient.mEventQueue.push(PowerEvent{PowerEvent::EventType::PRE_CHANGE, + currentState, newState, standbyMode, transactionId}); + } + mClient.mQueueCv.notify_one(); +} + +// --------------------------------------------------------------------------- +// ChangedNotification +// --------------------------------------------------------------------------- + +void NetworkManagerPowerClient::ChangedNotification::OnPowerModeChanged( + const PowerState currentState, const PowerState newState) +{ + NMLOG_DEBUG("OnPowerModeChanged current=%d new=%d", + static_cast(currentState), static_cast(newState)); + + // Use the cached standby mode + const bool standbyMode = mClient.mLastChangeStandbyMode; + + // Enqueue and return immediately so the COM-RPC dispatcher thread is freed. + { + std::lock_guard lock(mClient.mQueueMutex); + mClient.mEventQueue.push(PowerEvent{PowerEvent::EventType::CHANGED, + currentState, newState, standbyMode, 0}); + } + mClient.mQueueCv.notify_one(); +} diff --git a/plugin/NetworkManagerPowerClient.h b/plugin/NetworkManagerPowerClient.h new file mode 100644 index 00000000..73949e0e --- /dev/null +++ b/plugin/NetworkManagerPowerClient.h @@ -0,0 +1,175 @@ +/** +* If not stated otherwise in this file or this component's LICENSE +* file the following copyright and licenses apply: +* +* Copyright 2026 RDK Management +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +**/ + +#pragma once + +#include "Module.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WPEFramework { +namespace Plugin { + +/** + * Callback interface that decouples NetworkManagerPowerClient from + * NetworkManagerImplementation. The implementation receives power state + * transitions and must call sendAck() exactly once per OnPowerModePreChange. + */ +struct INetworkPowerCallback { + virtual ~INetworkPowerCallback() = default; + + /** + * Called when a power mode pre-change event arrives that involves + * POWER_STATE_STANDBY_DEEP_SLEEP (either as the new state or the current + * state). The implementation MUST call sendAck() exactly once. + */ + virtual void OnPowerModePreChange(const Exchange::IPowerManager::PowerState currentState, + const Exchange::IPowerManager::PowerState newState, + std::function sendAck) = 0; + + /** + * Called when a power mode changed event arrives (informational only; + * no ack required). + */ + virtual void OnPowerModeChanged(const Exchange::IPowerManager::PowerState currentState, + const Exchange::IPowerManager::PowerState newState) = 0; +}; + +/** + * - Inherits SmartInterfaceType for automatic + * reconnect / Operational() lifecycle callbacks. + * - Registers as an AddPowerModePreChangeClient so it participates + * in the pre-change ack protocol. + * - Delegates DeepSleep transitions to INetworkPowerCallback. + * - Sends a fast-path PowerModePreChangeComplete for all other transitions. + * + * Lifecycle: + * Construction → Open() connects to PowerManager (async). + * Operational(true) → registers events; IsValid() returns true. + * Operational(false) → unregisters events and releases proxy. + * Destruction → unregisterEvents() then Close() . + */ +class NetworkManagerPowerClient : protected RPC::SmartInterfaceType { +public: + using PowerState = Exchange::IPowerManager::PowerState; + + explicit NetworkManagerPowerClient(INetworkPowerCallback& callback); + ~NetworkManagerPowerClient() override; + + NetworkManagerPowerClient(const NetworkManagerPowerClient&) = delete; + NetworkManagerPowerClient& operator=(const NetworkManagerPowerClient&) = delete; + + /** Returns true when the PowerManager COMRPC proxy is available. */ + bool IsValid() const; + + /** Queries the current Network Standby mode from PowerManager. */ + bool getNetworkStandbyMode() const; + + /** Sends PowerModePreChangeComplete to PowerManager. */ + void sendPowerModePreChangeComplete(int transactionId); + + /** Requests a delay window extension via DelayPowerModeChangeBy. */ + void sendDelayPowerModeChange(int transactionId, int seconds); + +private: + // ----------------------------------------------------------------------- + // IModePreChangeNotification sink + // ----------------------------------------------------------------------- + class PreChangeNotification : public Exchange::IPowerManager::IModePreChangeNotification { + public: + explicit PreChangeNotification(NetworkManagerPowerClient& client) + : mClient(client) {} + + void OnPowerModePreChange(const PowerState currentState, const PowerState newState, + const int transactionId, const int stateChangeAfter) override; + + BEGIN_INTERFACE_MAP(PreChangeNotification) + INTERFACE_ENTRY(Exchange::IPowerManager::IModePreChangeNotification) + END_INTERFACE_MAP + + private: + NetworkManagerPowerClient& mClient; + }; + + // ----------------------------------------------------------------------- + // IModeChangedNotification sink + // ----------------------------------------------------------------------- + class ChangedNotification : public Exchange::IPowerManager::IModeChangedNotification { + public: + explicit ChangedNotification(NetworkManagerPowerClient& client) : mClient(client) {} + + void OnPowerModeChanged(const PowerState currentState, const PowerState newState) override; + + BEGIN_INTERFACE_MAP(ChangedNotification) + INTERFACE_ENTRY(Exchange::IPowerManager::IModeChangedNotification) + END_INTERFACE_MAP + + private: + NetworkManagerPowerClient& mClient; + }; + + // ----------------------------------------------------------------------- + // SmartInterfaceType lifecycle callback + // ----------------------------------------------------------------------- + void Operational(bool upAndRunning) override; + + void registerEvents(); + void unregisterEvents(); + void powerThreadLoop(); + + + // ----------------------------------------------------------------------- + // Members + // ----------------------------------------------------------------------- + INetworkPowerCallback& mCallback; + Exchange::IPowerManager* mPowerManager{nullptr}; + Core::Sink mPreChangeNotification; + Core::Sink mChangedNotification; + uint32_t mClientId{0}; + + // Power-event thread: receives events enqueued by the COM-RPC dispatcher + // thread and processes them (WiFiDisconnect etc.) without blocking the + // dispatcher. + struct PowerEvent { + enum class EventType { PRE_CHANGE, CHANGED }; + + EventType type; + PowerState currentState; + PowerState newState; + bool standbyMode; + int transactionId; + }; + + std::thread mPowerThread; + std::queue mEventQueue; + std::mutex mQueueMutex; + std::condition_variable mQueueCv; + std::atomic mStopThread{false}; + // Cached standby mode from the last PRE_CHANGE event. + std::atomic mLastChangeStandbyMode{false}; +}; + +} // namespace Plugin +} // namespace WPEFramework diff --git a/plugin/gnome/NetworkManagerGnomeEvents.cpp b/plugin/gnome/NetworkManagerGnomeEvents.cpp index 761c39fb..7a87cc20 100644 --- a/plugin/gnome/NetworkManagerGnomeEvents.cpp +++ b/plugin/gnome/NetworkManagerGnomeEvents.cpp @@ -30,6 +30,8 @@ #include "NetworkManagerGnomeUtils.h" #include "NetworkManagerImplementation.h" #include "INetworkManager.h" +#include + #ifdef ENABLE_MIGRATION_MFRMGR_SUPPORT #include "NetworkManagerGnomeMfrMgr.h" #endif @@ -74,6 +76,198 @@ namespace WPEFramework } } + /* Refresh the per-interface/per-family IP cache from current libnm state and + emit acquired/lost events for address-set differences. + + Build a fresh IpFamilyCache from current libnm state for one device/family, + swap it into _instance under the cache mutex, then emit acquired/lost events + for any address-set differences outside the lock. */ + static void refreshIpFamilyCache(NMDevice* device, bool isIPv6) + { + if (!device || !NM_IS_DEVICE(device) || !_instance) + return; + + const char* iface = nm_device_get_iface(device); + if (!iface) return; + std::string ifname = iface; + + bool isEth = (ifname == nmUtils::ethIface()); + bool isWlan = (ifname == nmUtils::wlanIface()); + if (!isEth && !isWlan) return; + + /* Build the new snapshot locally (no locks held during NM calls). + * Skip the NM read when the device is in a disconnected/down state + * so that newCache stays empty and the diff emits IP_LOST for every + * address still in the cache. This also prevents spurious + * "IP acquired" events from intermediate NM signals (nameserver, + * gateway clearing) that fire after the cache has been emptied + * but before NM clears addresses on the config object. */ + NMDeviceState devState = nm_device_get_state(device); + bool skipRead = (devState <= NM_DEVICE_STATE_DISCONNECTED); + IpFamilyCache newCache; + NMActiveConnection* conn = skipRead ? nullptr : nm_device_get_active_connection(device); + if (conn) { + /* autoconfig: method "auto" or "dhcp" → true */ + NMConnection* nmConn = NM_CONNECTION(nm_active_connection_get_connection(conn)); + if (nmConn) { + NMSettingIPConfig* ipSetting = isIPv6 + ? NM_SETTING_IP_CONFIG(nm_connection_get_setting_ip6_config(nmConn)) + : NM_SETTING_IP_CONFIG(nm_connection_get_setting_ip4_config(nmConn)); + if (ipSetting) { + const char* method = nm_setting_ip_config_get_method(ipSetting); + newCache.autoconfig = method && + (g_strcmp0(method, "auto") == 0 || g_strcmp0(method, "dhcp") == 0); + } + } + } + + /* IP config read is device-level and does not require an active connection. */ + NMIPConfig* ipConfig = skipRead ? nullptr + : (isIPv6 ? nm_device_get_ip6_config(device) + : nm_device_get_ip4_config(device)); + + if (ipConfig) { + GPtrArray* ipAddresses = nm_ip_config_get_addresses(ipConfig); + std::string macAddr; + if (isIPv6) { + const char* hw = nm_device_get_hw_address(device); + if (hw) macAddr = hw; + } + if (ipAddresses) { + for (guint i = 0; i < ipAddresses->len; i++) { + NMIPAddress* addr = (NMIPAddress*)g_ptr_array_index(ipAddresses, i); + if (!addr) continue; + const char* addrStr = nm_ip_address_get_address(addr); + if (!addrStr) continue; + std::string addrString = addrStr; + uint32_t prefix = nm_ip_address_get_prefix(addr); + if (isIPv6) { + if (isIPv6LinkLocal(addrString)) { + newCache.linkLocalAddresses.insert(addrString); + } else if (isIPv6ULA(addrString)) { + newCache.uniqueLocalAddresses.insert(addrString); + } else { + GlobalAddressType type = (!macAddr.empty() && isIPv6MacBased(addrString, macAddr)) + ? ADDR_GLOBAL_MAC_BASED : ADDR_GLOBAL; + newCache.globalAddresses.emplace(addrString, GlobalAddressInfo{prefix, type}); + } + } else { + if (isIPv4LinkLocal(addrString)) { + newCache.linkLocalAddresses.insert(addrString); + } else { + newCache.globalAddresses.emplace(addrString, GlobalAddressInfo{prefix, ADDR_GLOBAL}); + } + } + } + } + + const char* gw = nm_ip_config_get_gateway(ipConfig); + if (gw) newCache.gateway = gw; + + const char* const* dnsArr = nm_ip_config_get_nameservers(ipConfig); + if (dnsArr && dnsArr[0]) { + newCache.primarydns = dnsArr[0]; + if (dnsArr[1]) newCache.secondarydns = dnsArr[1]; + } + + NMDhcpConfig* dhcpConfig = isIPv6 + ? nm_device_get_dhcp6_config(device) + : nm_device_get_dhcp4_config(device); + if (dhcpConfig) { + const char* server = nm_dhcp_config_get_one_option(dhcpConfig, "dhcp_server_identifier"); + if (server) newCache.dhcpserver = server; + } + + newCache.valid = true; + } + + /* Swap new snapshot into instance cache; collect old global address keys for diff. */ + std::set oldKeys = _instance->swapIpCache( + ifname, isIPv6 ? "IPv6" : "IPv4", newCache); + + /* Emit address acquired/lost events from global-address key diff (outside the lock). */ + std::string family = isIPv6 ? "IPv6" : "IPv4"; + for (const auto& kv : newCache.globalAddresses) { + if (oldKeys.find(kv.first) == oldKeys.end()) { + _instance->ReportIPAddressChange(ifname, family, kv.first, Exchange::INetworkManager::IP_ACQUIRED); + } + } + for (const auto& key : oldKeys) { + if (newCache.globalAddresses.find(key) == newCache.globalAddresses.end()) { + _instance->ReportIPAddressChange(ifname, family, key, Exchange::INetworkManager::IP_LOST); + } + } + } + + static void ip4ChangedCb(NMIPConfig *ipConfig, GParamSpec *pspec, gpointer userData) + { + NMDevice *device = (NMDevice*)userData; + if (!device || !NM_IS_DEVICE(device)) return; + refreshIpFamilyCache(device, false); + } + + static void ip6ChangedCb(NMIPConfig *ipConfig, GParamSpec *pspec, gpointer userData) + { + NMDevice *device = (NMDevice*)userData; + if (!device || !NM_IS_DEVICE(device)) return; + refreshIpFamilyCache(device, true); + } + + /* Called when DHCP options change mid-lease (e.g. renewed with different server/options). */ + static void dhcp4OptionsCb(NMDhcpConfig *dhcpConfig, GParamSpec *pspec, gpointer userData) + { + NMDevice *device = (NMDevice*)userData; + if (!device || !NM_IS_DEVICE(device)) return; + refreshIpFamilyCache(device, false); + } + + static void dhcp6OptionsCb(NMDhcpConfig *dhcpConfig, GParamSpec *pspec, gpointer userData) + { + NMDevice *device = (NMDevice*)userData; + if (!device || !NM_IS_DEVICE(device)) return; + refreshIpFamilyCache(device, true); + } + + /* Called when the ip4-config or ip6-config object on a device is replaced + (e.g. after reconnect). Re-attaches notify handlers to the new object. */ + static void ip4ConfigChangedCb(NMDevice *device, GParamSpec *pspec, gpointer userData) + { + if (!device || !NM_IS_DEVICE(device)) return; + NMIPConfig* ipv4Config = nm_device_get_ip4_config(device); + if (ipv4Config) { + g_signal_handlers_disconnect_by_func(ipv4Config, (gpointer)ip4ChangedCb, device); + g_signal_connect(ipv4Config, "notify::addresses", G_CALLBACK(ip4ChangedCb), device); + g_signal_connect(ipv4Config, "notify::gateway", G_CALLBACK(ip4ChangedCb), device); + g_signal_connect(ipv4Config, "notify::nameservers", G_CALLBACK(ip4ChangedCb), device); + } + /* Re-attach DHCP options handler to the (possibly new) DHCP config object. */ + NMDhcpConfig* dhcp4 = nm_device_get_dhcp4_config(device); + if (dhcp4) { + g_signal_handlers_disconnect_by_func(dhcp4, (gpointer)dhcp4OptionsCb, device); + g_signal_connect(dhcp4, "notify::options", G_CALLBACK(dhcp4OptionsCb), device); + } + refreshIpFamilyCache(device, false); + } + + static void ip6ConfigChangedCb(NMDevice *device, GParamSpec *pspec, gpointer userData) + { + if (!device || !NM_IS_DEVICE(device)) return; + NMIPConfig* ipv6Config = nm_device_get_ip6_config(device); + if (ipv6Config) { + g_signal_handlers_disconnect_by_func(ipv6Config, (gpointer)ip6ChangedCb, device); + g_signal_connect(ipv6Config, "notify::addresses", G_CALLBACK(ip6ChangedCb), device); + g_signal_connect(ipv6Config, "notify::gateway", G_CALLBACK(ip6ChangedCb), device); + g_signal_connect(ipv6Config, "notify::nameservers", G_CALLBACK(ip6ChangedCb), device); + } + /* Re-attach DHCP options handler to the (possibly new) DHCP config object. */ + NMDhcpConfig* dhcp6 = nm_device_get_dhcp6_config(device); + if (dhcp6) { + g_signal_handlers_disconnect_by_func(dhcp6, (gpointer)dhcp6OptionsCb, device); + g_signal_connect(dhcp6, "notify::options", G_CALLBACK(dhcp6OptionsCb), device); + } + refreshIpFamilyCache(device, true); + } + void GnomeNetworkManagerEvents::deviceStateChangeCb(NMDevice *device, GParamSpec *pspec, NMEvents *nmEvents) { static bool isEthDisabled = false; @@ -125,11 +319,15 @@ namespace WPEFramework case NM_DEVICE_STATE_UNKNOWN: wifiState = "WIFI_STATE_UNINSTALLED"; GnomeNetworkManagerEvents::onWIFIStateChanged(Exchange::INetworkManager::WIFI_STATE_UNINSTALLED); + refreshIpFamilyCache(device, false); + refreshIpFamilyCache(device, true); GnomeNetworkManagerEvents::onInterfaceStateChangeCb(Exchange::INetworkManager::INTERFACE_REMOVED, nmUtils::wlanIface()); break; case NM_DEVICE_STATE_UNMANAGED: wifiState = "WIFI_STATE_DISABLED"; GnomeNetworkManagerEvents::onWIFIStateChanged(Exchange::INetworkManager::WIFI_STATE_DISABLED); + refreshIpFamilyCache(device, false); + refreshIpFamilyCache(device, true); GnomeNetworkManagerEvents::onInterfaceStateChangeCb(Exchange::INetworkManager::INTERFACE_REMOVED, nmUtils::wlanIface()); isWlanDisabled = true; break; @@ -137,6 +335,8 @@ namespace WPEFramework case NM_DEVICE_STATE_DISCONNECTED: wifiState = "WIFI_STATE_DISCONNECTED"; GnomeNetworkManagerEvents::onWIFIStateChanged(Exchange::INetworkManager::WIFI_STATE_DISCONNECTED); + refreshIpFamilyCache(device, false); + refreshIpFamilyCache(device, true); GnomeNetworkManagerEvents::onInterfaceStateChangeCb(Exchange::INetworkManager::INTERFACE_LINK_DOWN, nmUtils::wlanIface()); break; case NM_DEVICE_STATE_PREPARE: @@ -212,11 +412,15 @@ namespace WPEFramework { case NM_DEVICE_STATE_UNKNOWN: case NM_DEVICE_STATE_UNMANAGED: + refreshIpFamilyCache(device, false); + refreshIpFamilyCache(device, true); GnomeNetworkManagerEvents::onInterfaceStateChangeCb(Exchange::INetworkManager::INTERFACE_REMOVED, nmUtils::ethIface()); isEthDisabled = true; break; case NM_DEVICE_STATE_UNAVAILABLE: case NM_DEVICE_STATE_DISCONNECTED: + refreshIpFamilyCache(device, false); + refreshIpFamilyCache(device, true); GnomeNetworkManagerEvents::onInterfaceStateChangeCb(Exchange::INetworkManager::INTERFACE_LINK_DOWN, nmUtils::ethIface()); break; case NM_DEVICE_STATE_PREPARE: @@ -261,101 +465,6 @@ namespace WPEFramework } } - static void ip4ChangedCb(NMIPConfig *ipConfig, GParamSpec *pspec, gpointer userData) - { - if (!ipConfig) { - NMLOG_ERROR("IP config is null"); - return; - } - - NMDevice *device = (NMDevice*)userData; - if((device == NULL) || (!NM_IS_DEVICE(device))) - return; - - const char* iface = nm_device_get_iface(device); - if(iface == NULL) - return; - std::string ifname = iface; - - GPtrArray *addresses = nm_ip_config_get_addresses(ipConfig); - if (!addresses) { - NMLOG_ERROR("No addresses found"); - return; - } - else { - if(addresses->len == 0) { - GnomeNetworkManagerEvents::onAddressChangeCb(ifname, "", false, false); - return; - } - } - - for (guint i = 0; i < addresses->len; ++i) { - NMIPAddress *address = (NMIPAddress *)g_ptr_array_index(addresses, i); - if(address == NULL) - { - NMLOG_WARNING("IPv4 address is null"); - continue; - } - if (nm_ip_address_get_family(address) == AF_INET) { - const char *ipAddress = nm_ip_address_get_address(address); - if(ipAddress != NULL) { - GnomeNetworkManagerEvents::onAddressChangeCb(iface, ipAddress, true, false); - } - } - } - } - - static void ip6ChangedCb(NMIPConfig *ipConfig, GParamSpec *pspec, gpointer userData) - { - if (!ipConfig) { - NMLOG_ERROR("ip config is null"); - return; - } - - NMDevice *device = (NMDevice*)userData; - if( ((device != NULL) && NM_IS_DEVICE(device)) ) - { - const char* iface = nm_device_get_iface(device); - if(iface == NULL) - return; - std::string ifname = iface; - GPtrArray *addresses = nm_ip_config_get_addresses(ipConfig); - if (!addresses) { - NMLOG_ERROR("No addresses found"); - return; - } - else { - if(addresses->len == 0) { - GnomeNetworkManagerEvents::onAddressChangeCb(ifname, "", false, true); - return; - } - } - - for (guint i = 0; i < addresses->len; ++i) { - NMIPAddress *address = (NMIPAddress *)g_ptr_array_index(addresses, i); - if(address == NULL) - { - NMLOG_WARNING("IPv6 address is null"); - continue; - } - if (nm_ip_address_get_family(address) == AF_INET6) { - const char *ipaddr = nm_ip_address_get_address(address); - //int prefix = nm_ip_address_get_prefix(address); - if(ipaddr != NULL) { - std::string ipAddress = ipaddr; - if (ipAddress.compare(0, 5, "fe80:") == 0 || - ipAddress.compare(0, 6, "fe80::") == 0) { - NMLOG_DEBUG("%s It's link-local ip", ipAddress.c_str()); - continue; // It's link-local so skiping - } - GnomeNetworkManagerEvents::onAddressChangeCb(iface, ipAddress, true, true); - break; // SLAAC protocol may include multip ipv6 address posting only one Global address - } - } - } - } - } - static void deviceAddedCB(NMClient *client, NMDevice *device, NMEvents *nmEvents) { if( ((device != NULL) && NM_IS_DEVICE(device)) ) @@ -374,17 +483,35 @@ namespace WPEFramework if(ifname == nmUtils::ethIface() || ifname == nmUtils::wlanIface()) { g_signal_connect(device, "notify::" NM_DEVICE_STATE, G_CALLBACK(GnomeNetworkManagerEvents::deviceStateChangeCb), nmEvents); - // TODO call notify::" NM_DEVICE_ACTIVE_CONNECTION if needed + g_signal_connect(device, "notify::ip4-config", G_CALLBACK(ip4ConfigChangedCb), nmEvents); + g_signal_connect(device, "notify::ip6-config", G_CALLBACK(ip6ConfigChangedCb), nmEvents); NMIPConfig *ipv4Config = nm_device_get_ip4_config(device); NMIPConfig *ipv6Config = nm_device_get_ip6_config(device); if (ipv4Config) { - g_signal_connect(ipv4Config, "notify::addresses", G_CALLBACK(ip4ChangedCb), device); + g_signal_connect(ipv4Config, "notify::addresses", G_CALLBACK(ip4ChangedCb), device); + g_signal_connect(ipv4Config, "notify::gateway", G_CALLBACK(ip4ChangedCb), device); + g_signal_connect(ipv4Config, "notify::nameservers", G_CALLBACK(ip4ChangedCb), device); } if (ipv6Config) { - g_signal_connect(ipv6Config, "notify::addresses", G_CALLBACK(ip6ChangedCb), device); + g_signal_connect(ipv6Config, "notify::addresses", G_CALLBACK(ip6ChangedCb), device); + g_signal_connect(ipv6Config, "notify::gateway", G_CALLBACK(ip6ChangedCb), device); + g_signal_connect(ipv6Config, "notify::nameservers", G_CALLBACK(ip6ChangedCb), device); } + /* Subscribe to DHCP option changes so dhcpserver stays current mid-lease. */ + NMDhcpConfig* dhcp4Added = nm_device_get_dhcp4_config(device); + NMDhcpConfig* dhcp6Added = nm_device_get_dhcp6_config(device); + if (dhcp4Added) + g_signal_connect(dhcp4Added, "notify::options", G_CALLBACK(dhcp4OptionsCb), device); + if (dhcp6Added) + g_signal_connect(dhcp6Added, "notify::options", G_CALLBACK(dhcp6OptionsCb), device); + + /* Seed the IP cache so GetIPSettings works immediately if the + device already has an address (e.g. hotplug in activated state). */ + refreshIpFamilyCache(device, false); + refreshIpFamilyCache(device, true); + if (NM_IS_DEVICE_WIFI(device)) { // Register signal handler for WiFi scanning events to detect when scan operations complete @@ -405,14 +532,45 @@ namespace WPEFramework std::string ifname = nm_device_get_iface(device); if(ifname == nmUtils::wlanIface()) { GnomeNetworkManagerEvents::onInterfaceStateChangeCb(Exchange::INetworkManager::INTERFACE_REMOVED, nmUtils::wlanIface()); - g_signal_handlers_disconnect_by_func(device, (gpointer)GnomeNetworkManagerEvents::deviceStateChangeCb, nmEvents); NMLOG_INFO("WIFI device removed: %s", ifname.c_str()); } else if(ifname == nmUtils::ethIface()) { GnomeNetworkManagerEvents::onInterfaceStateChangeCb(Exchange::INetworkManager::INTERFACE_REMOVED, nmUtils::ethIface()); - g_signal_handlers_disconnect_by_func(device, (gpointer)GnomeNetworkManagerEvents::deviceStateChangeCb, nmEvents); NMLOG_INFO("ETHERNET device removed: %s", ifname.c_str()); } + else { + return; // not a tracked interface + } + + /* Disconnect all device-level signals (state, ip4/ip6-config changes). */ + g_signal_handlers_disconnect_by_data(device, nmEvents); + + /* Disconnect IP config property signals (addresses, gateway, nameservers). */ + NMIPConfig *ipv4Config = nm_device_get_ip4_config(device); + NMIPConfig *ipv6Config = nm_device_get_ip6_config(device); + if (ipv4Config) + g_signal_handlers_disconnect_by_func(ipv4Config, (gpointer)ip4ChangedCb, device); + if (ipv6Config) + g_signal_handlers_disconnect_by_func(ipv6Config, (gpointer)ip6ChangedCb, device); + + /* Disconnect DHCP option signals. */ + NMDhcpConfig* dhcp4 = nm_device_get_dhcp4_config(device); + NMDhcpConfig* dhcp6 = nm_device_get_dhcp6_config(device); + if (dhcp4) + g_signal_handlers_disconnect_by_func(dhcp4, (gpointer)dhcp4OptionsCb, device); + if (dhcp6) + g_signal_handlers_disconnect_by_func(dhcp6, (gpointer)dhcp6OptionsCb, device); + + /* Clear IP cache for the removed device (emits IP_LOST for any cached addresses). */ + if (_instance) { + for (const char* family : {"IPv4", "IPv6"}) { + IpFamilyCache empty; + std::set oldKeys = _instance->swapIpCache(ifname, family, empty); + for (const auto& key : oldKeys) { + _instance->ReportIPAddressChange(ifname, family, key, Exchange::INetworkManager::IP_LOST); + } + } + } } // guint disconnected_count = g_signal_handlers_disconnect_matched( _nmEventInstance->activeConn, @@ -492,6 +650,8 @@ namespace WPEFramework /* Register device state change event */ g_signal_connect(device, "notify::" NM_DEVICE_STATE, G_CALLBACK(GnomeNetworkManagerEvents::deviceStateChangeCb), nmEvents); + g_signal_connect(device, "notify::ip4-config", G_CALLBACK(ip4ConfigChangedCb), nmEvents); + g_signal_connect(device, "notify::ip6-config", G_CALLBACK(ip6ConfigChangedCb), nmEvents); if(NM_IS_DEVICE_WIFI(device)) { nmEvents->wifiDevice = NM_DEVICE_WIFI(device); g_signal_connect(nmEvents->wifiDevice, "notify::" NM_DEVICE_WIFI_LAST_SCAN, G_CALLBACK(GnomeNetworkManagerEvents::onAvailableSSIDsCb), nmEvents); @@ -500,18 +660,32 @@ namespace WPEFramework NMIPConfig *ipv4Config = nm_device_get_ip4_config(device); NMIPConfig *ipv6Config = nm_device_get_ip6_config(device); if (ipv4Config) { - ip4ChangedCb(ipv4Config, NULL, device); // posting event if interface already connected - g_signal_connect(ipv4Config, "notify::addresses", G_CALLBACK(ip4ChangedCb), device); + g_signal_connect(ipv4Config, "notify::addresses", G_CALLBACK(ip4ChangedCb), device); + g_signal_connect(ipv4Config, "notify::gateway", G_CALLBACK(ip4ChangedCb), device); + g_signal_connect(ipv4Config, "notify::nameservers", G_CALLBACK(ip4ChangedCb), device); } else NMLOG_WARNING("IPv4 config is null for device: %s, No IPv4 monitor", ifname.c_str()); if (ipv6Config) { - ip6ChangedCb(ipv6Config, NULL, device); - g_signal_connect(ipv6Config, "notify::addresses", G_CALLBACK(ip6ChangedCb), device); + g_signal_connect(ipv6Config, "notify::addresses", G_CALLBACK(ip6ChangedCb), device); + g_signal_connect(ipv6Config, "notify::gateway", G_CALLBACK(ip6ChangedCb), device); + g_signal_connect(ipv6Config, "notify::nameservers", G_CALLBACK(ip6ChangedCb), device); } else NMLOG_WARNING("IPv6 config is null for device: %s, No IPv6 monitor", ifname.c_str()); + + /* Subscribe to DHCP option changes so dhcpserver stays current mid-lease. */ + NMDhcpConfig* dhcp4Init = nm_device_get_dhcp4_config(device); + NMDhcpConfig* dhcp6Init = nm_device_get_dhcp6_config(device); + if (dhcp4Init) + g_signal_connect(dhcp4Init, "notify::options", G_CALLBACK(dhcp4OptionsCb), device); + if (dhcp6Init) + g_signal_connect(dhcp6Init, "notify::options", G_CALLBACK(dhcp6OptionsCb), device); + + /* Seed the IP cache from current state for already-connected devices. */ + refreshIpFamilyCache(device, false); + refreshIpFamilyCache(device, true); } else NMLOG_DEBUG("device type not eth/wifi %s", ifname.c_str()); @@ -593,6 +767,14 @@ namespace WPEFramework if (ipv6Config) { g_signal_handlers_disconnect_by_func(ipv6Config, (gpointer)ip6ChangedCb, device); } + + // Clean up DHCP option signals + NMDhcpConfig* dhcp4 = nm_device_get_dhcp4_config(device); + NMDhcpConfig* dhcp6 = nm_device_get_dhcp6_config(device); + if (dhcp4) + g_signal_handlers_disconnect_by_func(dhcp4, (gpointer)dhcp4OptionsCb, device); + if (dhcp6) + g_signal_handlers_disconnect_by_func(dhcp6, (gpointer)dhcp6OptionsCb, device); } } } @@ -714,53 +896,6 @@ namespace WPEFramework } } - void GnomeNetworkManagerEvents::onAddressChangeCb(std::string iface, std::string ipAddress, bool acquired, bool isIPv6) - { - /* - * notify::addresses g signal only send ipaddress when accuired time only. - * we need to post ip address when ipaddress lost case also so we caching the ip address per interface - */ - static std::map ipv6Map; - static std::map ipv4Map; - - if(acquired) - { - if (isIPv6) - { - if (ipv6Map[iface].find(ipAddress) == std::string::npos) { // same ip comes multiple time so avoding that - ipv6Map[iface] = ipAddress; - } - else // same ip not posting - return; - } - else - { - ipv4Map[iface] = ipAddress; - } - } - else - { - if (isIPv6) - { - ipAddress = ipv6Map[iface]; - ipv6Map[iface].clear(); - } - else - { - ipAddress = ipv4Map[iface]; - ipv4Map[iface].clear(); - } - if(ipAddress.empty()) - return; // empty ip address not posting event - } - Exchange::INetworkManager::IPStatus ipStatus{}; - if (acquired) - ipStatus = Exchange::INetworkManager::IP_ACQUIRED; - if(_instance != nullptr) - _instance->ReportIPAddressChange(iface, isIPv6?"IPv6":"IPv4", ipAddress, ipStatus); - NMLOG_INFO("iface:%s - ipaddress:%s - %s - %s", iface.c_str(), ipAddress.c_str(), acquired?"acquired":"lost", isIPv6?"isIPv6":"isIPv4"); - } - bool GnomeNetworkManagerEvents::apToJsonObject(NMAccessPoint *ap, JsonObject& ssidObj) { GBytes *ssid = NULL; diff --git a/plugin/gnome/NetworkManagerGnomeEvents.h b/plugin/gnome/NetworkManagerGnomeEvents.h index ee29ce6d..bf282de7 100644 --- a/plugin/gnome/NetworkManagerGnomeEvents.h +++ b/plugin/gnome/NetworkManagerGnomeEvents.h @@ -44,7 +44,6 @@ namespace WPEFramework public: static void onInterfaceStateChangeCb(uint8_t newState, std::string iface); // ReportInterfaceStateChange - static void onAddressChangeCb(std::string iface, std::string ipAddress, bool acqired, bool isIPv6); // ReportIPAddressChange static void onActiveInterfaceChangeCb(std::string newInterface); // ReportActiveInterfaceChange static void onAvailableSSIDsCb(NMDeviceWifi *wifiDevice, GParamSpec *pspec, gpointer userData); // ReportAvailableSSIDs static void onWIFIStateChanged(uint8_t state); // ReportWiFiStateChange diff --git a/plugin/gnome/NetworkManagerGnomeProxy.cpp b/plugin/gnome/NetworkManagerGnomeProxy.cpp index b661a8de..932dd9a8 100644 --- a/plugin/gnome/NetworkManagerGnomeProxy.cpp +++ b/plugin/gnome/NetworkManagerGnomeProxy.cpp @@ -22,9 +22,6 @@ #include "NetworkManagerGnomeUtils.h" #include #include -#include - -#define IN_IS_ADDR_LINKLOCAL(a) ((((uint32_t)ntohl(a)) & 0xffff0000U) == 0xa9fe0000U) using namespace WPEFramework; using namespace WPEFramework::Plugin; using namespace std; @@ -33,6 +30,40 @@ namespace WPEFramework { namespace Plugin { + /* + * Per-call NMClient helpers (same pattern as wifiManager). + * A fresh NMClient is created for each proxy API call and destroyed + * immediately after use, so no D-Bus signals accumulate between calls. + */ + static NMClient* createProxyClient(GMainContext *ctx) + { + GError *error = NULL; + g_main_context_push_thread_default(ctx); + NMClient *client = nm_client_new(NULL, &error); + g_main_context_pop_thread_default(ctx); + if (!client) { + if (error) { + NMLOG_ERROR("Failed to create NMClient: %s", error->message); + g_error_free(error); + } + } + return client; + } + + static void deleteProxyClient(NMClient *client) + { + if (!client) + return; + GMainContext *context = g_main_context_ref(nm_client_get_main_context(client)); + GObject *contextBusyWatcher = nm_client_get_context_busy_watcher(client); + g_object_add_weak_pointer(contextBusyWatcher, (gpointer *)&contextBusyWatcher); + g_clear_object(&client); + while (contextBusyWatcher) { + g_main_context_iteration(context, TRUE); + } + g_main_context_unref(context); + } + wifiManager *wifi = nullptr; GnomeNetworkManagerEvents *nmEvent = nullptr; NetworkManagerImplementation* _instance = nullptr; @@ -150,7 +181,14 @@ namespace WPEFramework // read persistent hostname if exist if(!nmUtils::readPersistentHostname(hostname)) { - hostname = nmUtils::deviceHostname(); // default hostname as device name + hostname = nmUtils::deviceHostname(); // default hostname as default hostname + } + + // Validate hostname is non-empty regardless of source (persistent or default) + if(hostname.empty()) + { + NMLOG_WARNING("Hostname is empty. No modification will be made to NM connections."); + return false; } connections = nm_client_get_connections(client); @@ -192,11 +230,8 @@ namespace WPEFramework /* @brief Set the dhcp hostname */ uint32_t NetworkManagerImplementation::SetHostname(const string& hostname /* @in */) { - const GPtrArray *connections = NULL; - NMConnection *connection = NULL; - - if (m_nmClient == nullptr) { - NMLOG_ERROR("NMClient is NULL"); + if (m_nmContext == nullptr) { + NMLOG_ERROR("NMContext is NULL"); return Core::ERROR_GENERAL; } @@ -206,10 +241,17 @@ namespace WPEFramework return Core::ERROR_BAD_REQUEST; } - connections = nm_client_get_connections(m_nmClient); + NMClient *client = createProxyClient(m_nmContext); + if (client == nullptr) { + NMLOG_ERROR("Failed to create NMClient for SetHostname"); + return Core::ERROR_GENERAL; + } + + const GPtrArray *connections = nm_client_get_connections(client); if (connections == NULL || connections->len == 0) { NMLOG_ERROR("Could not get nm connections"); + deleteProxyClient(client); return Core::ERROR_GENERAL; } @@ -217,7 +259,7 @@ namespace WPEFramework for (uint32_t i = 0; i < connections->len; i++) { - connection = NM_CONNECTION(connections->pdata[i]); + NMConnection *connection = NM_CONNECTION(connections->pdata[i]); if(connection != NULL) { const char *iface = nm_connection_get_interface_name(connection); @@ -236,6 +278,7 @@ namespace WPEFramework if(!setHostname(connection, hostname)) { NMLOG_ERROR("Failed to set hostname for connection at index %d", i); + deleteProxyClient(client); return Core::ERROR_GENERAL; } } @@ -243,6 +286,8 @@ namespace WPEFramework NMLOG_ERROR("Connection at index %d is NULL", i); } + deleteProxyClient(client); + // Write the hostname to persistent storage nmUtils::writePersistentHostname(hostname); @@ -251,7 +296,6 @@ namespace WPEFramework void NetworkManagerImplementation::platform_deinit() { - if(m_nmClient) { g_object_unref(m_nmClient); m_nmClient = nullptr; } if(m_nmContext) { g_main_context_unref(m_nmContext); m_nmContext = nullptr; } } @@ -266,38 +310,31 @@ namespace WPEFramework void NetworkManagerImplementation::platform_init() { ::_instance = this; - GError *error = NULL; - // initialize the NMClient object - // Create an isolated GMainContext so this m_nmClient's D-Bus socket is NOT a - // source on the global default context. The event thread runs the default - // context via g_main_loop_run(); without isolation it would own and mutate - // this m_nmClient's GObjects concurrently with the RPC thread. + // Create an isolated GMainContext for per-call NMClient creation. m_nmContext = g_main_context_new(); - g_main_context_push_thread_default(m_nmContext); - m_nmClient = nm_client_new(NULL, &error); - g_main_context_pop_thread_default(m_nmContext); - if (m_nmClient == NULL) { - if (error) { - NMLOG_FATAL("Error initializing NMClient: %s", error->message); - g_error_free(error); - } - if (m_nmContext) { - g_main_context_unref(m_nmContext); - m_nmContext = nullptr; - } + + // Create a temporary client for one-time init work + NMClient *initClient = createProxyClient(m_nmContext); + if (initClient == NULL) { + NMLOG_FATAL("Error initializing NMClient during platform_init"); + g_main_context_unref(m_nmContext); + m_nmContext = nullptr; return; } nmUtils::getDeviceProperties(); // get interface name form '/etc/device.proprties' - modifyDefaultConnConfig(m_nmClient); - NMDeviceState ethState = ifaceState(m_nmClient, nmUtils::ethIface()); + modifyDefaultConnConfig(initClient); + NMDeviceState ethState = ifaceState(initClient, nmUtils::ethIface()); if(ethState > NM_DEVICE_STATE_DISCONNECTED && ethState < NM_DEVICE_STATE_DEACTIVATING) setDefaultInterface(nmUtils::ethIface()); else setDefaultInterface(nmUtils::wlanIface()); NMLOG_INFO("default interface is %s", getDefaultInterface().c_str()); + + deleteProxyClient(initClient); + // getInitialConnectionState function not called here, as event monitor will report the initial state nmEvent = GnomeNetworkManagerEvents::getInstance(); nmEvent->startNetworkMangerEventMonitor(); @@ -310,20 +347,22 @@ namespace WPEFramework std::vector interfaceList; std::string wifiname = nmUtils::wlanIface(), ethname = nmUtils::ethIface(); - if(m_nmClient == nullptr) { - NMLOG_FATAL("NMClient is null"); + if(m_nmContext == nullptr) { + NMLOG_FATAL("NMContext is null"); return Core::ERROR_GENERAL; } - if (m_nmContext) { - for (int i = 0; i < 100 && g_main_context_iteration(m_nmContext, FALSE); ++i){ - // Intentional empty body: just flushing the event queue - } + NMClient *client = createProxyClient(m_nmContext); + if (client == nullptr) { + NMLOG_FATAL("Failed to create NMClient for GetAvailableInterfaces"); + return Core::ERROR_GENERAL; } - GPtrArray *devices = const_cast(nm_client_get_devices(m_nmClient)); + + GPtrArray *devices = const_cast(nm_client_get_devices(client)); if (devices == NULL) { NMLOG_ERROR("Failed to get device list."); - return Core::ERROR_GENERAL; + deleteProxyClient(client); + return rc; } for (guint j = 0; j < devices->len; j++) @@ -369,6 +408,11 @@ namespace WPEFramework } } + deleteProxyClient(client); + + if (rc != Core::ERROR_NONE) + return rc; + using Implementation = RPC::IteratorType; interfacesItr = Core::Service::Create(interfaceList); if(interfacesItr == nullptr) { @@ -450,9 +494,9 @@ namespace WPEFramework uint32_t NetworkManagerImplementation::SetInterfaceState(const string& interface/* @in */, const bool enabled /* @in */) { - if(m_nmClient == nullptr) + if(m_nmContext == nullptr) { - NMLOG_WARNING("NMClient is null"); + NMLOG_WARNING("NMContext is null"); return Core::ERROR_RPC_CALL_FAILED; } @@ -488,57 +532,62 @@ namespace WPEFramework { NMLOG_INFO("BOOT_MIGRATION detected, deleting all wired NM connections"); - // Bring down the ethernet interface before wiping its connections - // so NM doesn't immediately re-activate them during deletion. - NMDevice *ethDev = nm_client_get_device_by_iface(m_nmClient, interface.c_str()); - if(ethDev) + NMClient *client = createProxyClient(m_nmContext); + if (client != nullptr) { - GError *discError = nullptr; - if(!nm_device_disconnect(ethDev, nullptr, &discError)) + // Bring down the ethernet interface before wiping its connections + // so NM doesn't immediately re-activate them during deletion. + NMDevice *ethDev = nm_client_get_device_by_iface(client, interface.c_str()); + if(ethDev) { - NMLOG_WARNING("Failed to disconnect %s before migration cleanup: %s", - interface.c_str(), - discError ? discError->message : "unknown error"); - if(discError) g_error_free(discError); + GError *discError = nullptr; + if(!nm_device_disconnect(ethDev, nullptr, &discError)) + { + NMLOG_WARNING("Failed to disconnect %s before migration cleanup: %s", + interface.c_str(), + discError ? discError->message : "unknown error"); + if(discError) g_error_free(discError); + } } - } - const GPtrArray *connections = nm_client_get_connections(m_nmClient); - if(connections && connections->len > 0) - { - /* Snapshot the list before iterating: nm_client_get_connections() - * returns an internal array that can be mutated as connections - * are removed, so we must not iterate it while deleting. */ - GPtrArray *snapshot = g_ptr_array_new_full(connections->len, g_object_unref); - for(guint i = 0; i < connections->len; ++i) + const GPtrArray *connections = nm_client_get_connections(client); + if(connections && connections->len > 0) { - NMRemoteConnection *conn = NM_REMOTE_CONNECTION(connections->pdata[i]); - if(!conn) continue; - NMSettingConnection *sCon = nm_connection_get_setting_connection(NM_CONNECTION(conn)); - if(!sCon) continue; - const char *connType = nm_setting_connection_get_connection_type(sCon); - if(g_strcmp0(connType, NM_SETTING_WIRED_SETTING_NAME) != 0) + /* Snapshot the list before iterating: nm_client_get_connections() + * returns an internal array that can be mutated as connections + * are removed, so we must not iterate it while deleting. */ + GPtrArray *snapshot = g_ptr_array_new_full(connections->len, g_object_unref); + for(guint i = 0; i < connections->len; ++i) { - NMLOG_DEBUG("Skipping non-wired connection type: %s", connType ? connType : "null"); - continue; + NMRemoteConnection *conn = NM_REMOTE_CONNECTION(connections->pdata[i]); + if(!conn) continue; + NMSettingConnection *sCon = nm_connection_get_setting_connection(NM_CONNECTION(conn)); + if(!sCon) continue; + const char *connType = nm_setting_connection_get_connection_type(sCon); + if(g_strcmp0(connType, NM_SETTING_WIRED_SETTING_NAME) != 0) + { + NMLOG_DEBUG("Skipping non-wired connection type: %s", connType ? connType : "null"); + continue; + } + g_ptr_array_add(snapshot, g_object_ref(conn)); } - g_ptr_array_add(snapshot, g_object_ref(conn)); - } - for(guint i = 0; i < snapshot->len; ++i) - { - NMRemoteConnection *conn = NM_REMOTE_CONNECTION(snapshot->pdata[i]); - GError *error = nullptr; - if(!nm_remote_connection_delete(conn, nullptr, &error)) + for(guint i = 0; i < snapshot->len; ++i) { - const char *connId = nm_connection_get_id(NM_CONNECTION(conn)); - NMLOG_ERROR("Failed to delete connection %s: %s", - connId ? connId : "", - error ? error->message : "unknown error"); - if(error) g_error_free(error); + NMRemoteConnection *conn = NM_REMOTE_CONNECTION(snapshot->pdata[i]); + GError *error = nullptr; + if(!nm_remote_connection_delete(conn, nullptr, &error)) + { + const char *connId = nm_connection_get_id(NM_CONNECTION(conn)); + NMLOG_ERROR("Failed to delete connection %s: %s", + connId ? connId : "", + error ? error->message : "unknown error"); + if(error) g_error_free(error); + } } + g_ptr_array_unref(snapshot); } - g_ptr_array_unref(snapshot); + deleteProxyClient(client); } } } @@ -600,22 +649,23 @@ namespace WPEFramework return Core::ERROR_GENERAL; } - if(m_nmClient == nullptr) + if(m_nmContext == nullptr) { - NMLOG_WARNING("NMClient is null"); + NMLOG_WARNING("NMContext is null"); return Core::ERROR_RPC_CALL_FAILED; } - if (m_nmContext) { - for (int i = 0; i < 100 && g_main_context_iteration(m_nmContext, FALSE); ++i){ - // Intentional empty body: just flushing the event queue - } + NMClient *client = createProxyClient(m_nmContext); + if (client == nullptr) { + NMLOG_ERROR("Failed to create NMClient for GetInterfaceState"); + return Core::ERROR_RPC_CALL_FAILED; } - GPtrArray *devices = const_cast(nm_client_get_devices(m_nmClient)); + GPtrArray *devices = const_cast(nm_client_get_devices(client)); if (devices == NULL) { NMLOG_ERROR("Failed to get device list."); + deleteProxyClient(client); return Core::ERROR_GENERAL; } @@ -642,6 +692,8 @@ namespace WPEFramework } } + deleteProxyClient(client); + if(isIfaceFound) return Core::ERROR_NONE; else @@ -649,40 +701,9 @@ namespace WPEFramework return Core::ERROR_GENERAL; } - bool static isAutoConnectEnabled(NMActiveConnection* activeConn) - { - NMConnection *connection = NM_CONNECTION(nm_active_connection_get_connection(activeConn)); - if(connection == NULL) - return false; - - NMSettingIPConfig *ipConfig = nm_connection_get_setting_ip4_config(connection); - if(ipConfig) - { - const char* ipConfMethod = nm_setting_ip_config_get_method (ipConfig); - if(ipConfMethod != NULL && g_strcmp0(ipConfMethod, "auto") == 0) - return true; - else - NMLOG_WARNING("ip configuration: %s", ipConfMethod != NULL? ipConfMethod: "null"); - } - - return false; - } - /* @brief Get IP Address Of the Interface */ uint32_t NetworkManagerImplementation::GetIPSettings(string& interface /* @inout */, const string &ipversion /* @in */, IPAddress& result /* @out */) { - NMActiveConnection *conn = NULL; - NMIPConfig *ip4_config = NULL; - NMIPConfig *ip6_config = NULL; - const gchar *gateway = NULL; - char **dnsArr = NULL; - NMDhcpConfig *dhcp4_config = NULL; - NMDhcpConfig *dhcp6_config = NULL; - const char* dhcpserver; - NMSettingConnection *settings = NULL; - NMDevice *device = NULL; - string ipversionStr; - std::string wifiname = nmUtils::wlanIface(), ethname = nmUtils::ethIface(); if(interface.empty()) @@ -705,271 +726,22 @@ namespace WPEFramework return Core::ERROR_GENERAL; } - if(ipversion.empty()) - { - ipversionStr = "IPV4"; - } - else - { - ipversionStr = ipversion; - } + string ipversionStr = ipversion.empty() ? "IPv4" : ipversion; + std::string family = nmUtils::caseInsensitiveCompare(ipversionStr, "IPv6") ? "IPv6" : "IPv4"; - // Add caching optimization similar to RDK proxy - if (wifiname == interface) - { - if(nmUtils::caseInsensitiveCompare(ipversionStr, "IPV4") && !m_wlanIPv4Address.ipaddress.empty()) - { - NMLOG_DEBUG("%s IPv4 address from cache", wifiname.c_str()); - result = m_wlanIPv4Address; - return Core::ERROR_NONE; - } - else if(nmUtils::caseInsensitiveCompare(ipversion, "IPV6") && !m_wlanIPv6Address.ipaddress.empty()) - { - NMLOG_DEBUG("%s IPv6 address from cache", wifiname.c_str()); - result = m_wlanIPv6Address; - return Core::ERROR_NONE; - } - } - else if (ethname == interface) - { - if(nmUtils::caseInsensitiveCompare(ipversionStr, "IPV4") && !m_ethIPv4Address.ipaddress.empty()) - { - NMLOG_DEBUG("%s IPv4 address from cache", ethname.c_str()); - result = m_ethIPv4Address; - return Core::ERROR_NONE; - } - else if(nmUtils::caseInsensitiveCompare(ipversion, "IPV6") && !m_ethIPv6Address.ipaddress.empty()) - { - NMLOG_DEBUG("%s IPv6 address from cache", ethname.c_str()); - result = m_ethIPv6Address; - return Core::ERROR_NONE; - } - } + result = IPAddress{}; - if(m_nmClient == nullptr) + // Serve from event-driven cache + if (lookupIpCache(interface, family, result)) { - NMLOG_WARNING("NMClient is null"); - return Core::ERROR_RPC_CALL_FAILED; - } - - /* Drain any pending D-Bus property-change events queued on m_nmContext - * before reading libnm GObject state. Because m_nmContext is isolated - * from the event thread, nobody else can run it — so this loop is - * single-threaded and safe. It ensures m_nmClient reflects the latest state - * from NetworkManager before we start iterating connections/addresses. */ - if (m_nmContext) { - for (int i = 0; i < 100 && g_main_context_iteration(m_nmContext, FALSE); ++i){ - // Intentional empty body: just flushing the event queue - } - } - - device = nm_client_get_device_by_iface(m_nmClient, interface.c_str()); - if (device == NULL) - { - NMLOG_FATAL("libnm doesn't have device corresponding to %s", interface.c_str()); - return Core::ERROR_GENERAL; + NMLOG_DEBUG("%s %s address from cache", interface.c_str(), family.c_str()); } - - NMDeviceState deviceState = NM_DEVICE_STATE_UNKNOWN; - deviceState = nm_device_get_state(device); - if(deviceState < NM_DEVICE_STATE_DISCONNECTED) - { - NMLOG_WARNING("%s state is not a valid state: (%d)", interface.c_str(), deviceState); - return Core::ERROR_GENERAL; - } - - // if(ipversion.empty()) - // NMLOG_DEBUG("ipversion is empty default value IPv4"); - - const GPtrArray *connections = nm_client_get_active_connections(m_nmClient); - if(connections == NULL) - { - NMLOG_WARNING("no active connection; ip is not assigned to interface"); - return Core::ERROR_GENERAL; - } - - for (guint i = 0; i < connections->len; i++) - { - if(connections->pdata[i] == NULL) - continue; - - NMActiveConnection *connection = NM_ACTIVE_CONNECTION(connections->pdata[i]); - if (connection == nullptr) - continue; - - NMRemoteConnection* retConn = nm_active_connection_get_connection(connection); - if(retConn == NULL) - { - NMLOG_INFO("remote connection is null"); - continue; - } - - settings = nm_connection_get_setting_connection(NM_CONNECTION(retConn)); - if(settings == NULL) - continue; - if (g_strcmp0(nm_setting_connection_get_interface_name(settings), interface.c_str()) == 0) - { - conn = connection; - break; - } - } - - if (conn == NULL) + else { - NMLOG_WARNING("no active connection on %s interface", interface.c_str()); - return Core::ERROR_GENERAL; + NMLOG_DEBUG("no %s address on %s", family.c_str(), interface.c_str()); } + result.ipversion = family; - result.autoconfig = isAutoConnectEnabled(conn); - - if(ipversion.empty() || nmUtils::caseInsensitiveCompare(ipversion, "IPV4")) - { - const GPtrArray *ipByte = nullptr; - result.ipversion = "IPv4"; - ip4_config = nm_active_connection_get_ip4_config(conn); - NMIPAddress *ipAddr = NULL; - std::string ipStr; - struct sockaddr_in sa; - if (ip4_config) - ipByte = nm_ip_config_get_addresses(ip4_config); - else - NMLOG_WARNING("no IPv4 configurtion on %s", interface.c_str()); - if(ipByte) - { - for (guint i = 0; i < ipByte->len; i++) - { - ipStr.clear(); - ipAddr = static_cast(ipByte->pdata[i]); - if(ipAddr) - { - const char* addr = nm_ip_address_get_address(ipAddr); - if(addr) - ipStr = addr; - } - if(!ipStr.empty()) - { - // Skip link-local IPv4 addresses (169.254.x.x) - inet_pton(AF_INET, ipStr.c_str(), &(sa.sin_addr)); - if(IN_IS_ADDR_LINKLOCAL(sa.sin_addr.s_addr)) - { - NMLOG_DEBUG("Skipping link-local IPv4 address: %s", ipStr.c_str()); - continue; - } - result.ipaddress = ipStr; - result.prefix = nm_ip_address_get_prefix(ipAddr); - NMLOG_DEBUG("IPv4 addr: %s/%d", result.ipaddress.c_str(), result.prefix); - } - } - gateway = nm_ip_config_get_gateway(ip4_config); - if(gateway) - result.gateway = gateway; - dnsArr = (char **)nm_ip_config_get_nameservers(ip4_config); - if(dnsArr) - { - if(dnsArr[0]) - result.primarydns = std::string(dnsArr[0]); - if(dnsArr[1]) - result.secondarydns = std::string(dnsArr[1]); - } - dhcp4_config = nm_active_connection_get_dhcp4_config(conn); - if(dhcp4_config) - { - dhcpserver = nm_dhcp_config_get_one_option (dhcp4_config, "dhcp_server_identifier"); - if(dhcpserver) - result.dhcpserver = dhcpserver; - } - result.ula = ""; - - // Check if only link-local IPv4 is available (no valid global address found) - if(result.ipaddress.empty()) - { - NMLOG_WARNING("Only link-local IPv4 available on %s, not returning it", interface.c_str()); - // Clear cache for link-local only - if(ethname == interface) - m_ethIPv4Address = IPAddress(); - else if(wifiname == interface) - m_wlanIPv4Address = IPAddress(); - return Core::ERROR_GENERAL; - } - - // Cache the IPv4 address - if(ethname == interface) - m_ethIPv4Address = result; - else if(wifiname == interface) - m_wlanIPv4Address = result; - } - } - if((result.ipaddress.empty() && !(nmUtils::caseInsensitiveCompare(ipversion, "IPV4"))) || nmUtils::caseInsensitiveCompare(ipversion, "IPV6")) - { - std::string ipStr; - const GPtrArray *ipArray = nullptr; - result.ipversion = "IPv6"; - NMIPAddress *ipAddr = nullptr; - ip6_config = nm_active_connection_get_ip6_config(conn); - if(ip6_config) - ipArray = nm_ip_config_get_addresses(ip6_config); - else - NMLOG_WARNING("no IPv6 configurtion on %s", interface.c_str()); - if(ipArray) - { - for (guint i = 0; i < ipArray->len; i++) - { - ipStr.clear(); - ipAddr = static_cast(ipArray->pdata[i]); - if(ipAddr) - { - const char* addr = nm_ip_address_get_address(ipAddr); - if(addr) - ipStr = addr; - } - if(!ipStr.empty()) - { - if (ipStr.compare(0, 5, "fe80:") == 0 || ipStr.compare(0, 6, "fe80::") == 0) - { - result.ula = ipStr; - NMLOG_DEBUG("link-local ip: %s", result.ula.c_str()); - } - else - { - result.prefix = nm_ip_address_get_prefix(ipAddr); - if(result.ipaddress.empty()) // SLAAC mutiple ip not added - result.ipaddress = ipStr; - NMLOG_DEBUG("global ip %s/%d", ipStr.c_str(), result.prefix); - } - } - } - - gateway = nm_ip_config_get_gateway(ip6_config); - if(gateway) - result.gateway= gateway; - dnsArr = (char **)nm_ip_config_get_nameservers(ip6_config); - if(dnsArr) - { - if(dnsArr[0]) - result.primarydns = std::string(dnsArr[0]); - if(dnsArr[1]) - result.secondarydns = std::string(dnsArr[1]); - } - dhcp6_config = nm_active_connection_get_dhcp6_config(conn); - if(dhcp6_config) - { - dhcpserver = nm_dhcp_config_get_one_option (dhcp6_config, "dhcp_server_identifier"); - if(dhcpserver) - result.dhcpserver = dhcpserver; - } - // Cache the IPv6 address - if(ethname == interface) - m_ethIPv6Address = result; - else if(wifiname == interface) - m_wlanIPv6Address = result; - } - } - if(result.ipaddress.empty()) - { - result.autoconfig = true; - if(ipversion.empty()) - result.ipversion = "IPv4"; - } return Core::ERROR_NONE; } @@ -986,32 +758,61 @@ namespace WPEFramework return rc; } - uint32_t NetworkManagerImplementation::StartWiFiScan(const string& frequency /* @in */, IStringIterator* const ssids/* @in */) + uint32_t NetworkManagerImplementation::StartWiFiScan(IStringIterator* const frequencies /* @in */, IStringIterator* const ssids/* @in */) { uint32_t rc = Core::ERROR_RPC_CALL_FAILED; //Cleared the Existing Store filterred SSID list m_filterSsidslist.clear(); - m_filterfrequency.clear(); + m_filterFrequencies.clear(); if(ssids) { string tmpssidlist{}; while (ssids->Next(tmpssidlist) == true) { - m_filterSsidslist.push_back(tmpssidlist.c_str()); - NMLOG_DEBUG("%s added to SSID filtering", tmpssidlist.c_str()); + if (!tmpssidlist.empty()) + { + m_filterSsidslist.push_back(tmpssidlist.c_str()); + NMLOG_DEBUG("%s added to SSID filtering", tmpssidlist.c_str()); + } + else + { + NMLOG_DEBUG("Empty SSID encountered in input list; skipping."); + } } } - if (!frequency.empty()) + if (frequencies) { - m_filterfrequency = frequency; - NMLOG_DEBUG("Scan SSIDs of frequency %s", m_filterfrequency.c_str()); + string frequency{}; + while (frequencies->Next(frequency) == true) + { + if (!frequency.empty()) + { + Core::JSON::EnumType parsedFrequency; + parsedFrequency.FromString(frequency); + const string normalizedFrequency = parsedFrequency.Data(); + if ((!normalizedFrequency.empty()) && (normalizedFrequency == frequency)) + { + m_filterFrequencies.push_back(normalizedFrequency); + NMLOG_DEBUG("Frequency %s added to scan filtering", normalizedFrequency.c_str()); + } + else + { + NMLOG_ERROR("Invalid frequency value: %s", frequency.c_str()); + return Core::ERROR_BAD_REQUEST; + } + } + else + { + NMLOG_DEBUG("Empty frequency encountered in input list; skipping."); + } + } } nmEvent->setwifiScanOptions(true); - if(wifi->wifiScanRequest(m_filterSsidslist.size() == 1 ? m_filterSsidslist[0] : "")) + if(wifi->wifiScanRequest(m_filterSsidslist)) rc = Core::ERROR_NONE; return rc; } @@ -1118,6 +919,22 @@ namespace WPEFramework return rc; } + uint32_t NetworkManagerImplementation::EthernetDeactivate(void) + { + uint32_t rc = Core::ERROR_GENERAL; + if(wifi->ethernetDeactivate()) + rc = Core::ERROR_NONE; + return rc; + } + + uint32_t NetworkManagerImplementation::ReacquireDHCPLease(const string& iface) + { + uint32_t rc = Core::ERROR_GENERAL; + if(wifi->reacquireDhcpLease(iface)) + rc = Core::ERROR_NONE; + return rc; + } + uint32_t NetworkManagerImplementation::GetConnectedSSID(WiFiSSIDInfo& ssidInfo /* @out */) { uint32_t rc = Core::ERROR_RPC_CALL_FAILED; diff --git a/plugin/gnome/NetworkManagerGnomeUtils.cpp b/plugin/gnome/NetworkManagerGnomeUtils.cpp index 7d97e5e8..fa2b2869 100644 --- a/plugin/gnome/NetworkManagerGnomeUtils.cpp +++ b/plugin/gnome/NetworkManagerGnomeUtils.cpp @@ -40,7 +40,7 @@ namespace WPEFramework { static std::string m_ethifname = "eth0"; static std::string m_wlanifname = "wlan0"; - static std::string m_deviceHostname = "rdk-device"; // Device name can be empty if not set in /etc/device.properties + static std::string m_deviceHostname = "rdk-device"; // default hostname can be empty if not set in /etc/device.properties const char* nmUtils::wlanIface() {return m_wlanifname.c_str();} const char* nmUtils::ethIface() {return m_ethifname.c_str();} @@ -261,14 +261,14 @@ namespace WPEFramework } } - if (line.find("DEVICE_NAME=") != std::string::npos) { + if (line.find("DEFAULT_HOSTNAME=") != std::string::npos) { deviceHostname = line.substr(line.find('=') + 1); deviceHostname.erase(deviceHostname.find_last_not_of("\r\n\t") + 1); deviceHostname.erase(0, deviceHostname.find_first_not_of("\r\n\t")); if(deviceHostname.empty()) { - NMLOG_WARNING("DEVICE_NAME is empty in /etc/device.properties"); - deviceHostname = ""; // set empty device name + NMLOG_WARNING("DEFAULT_HOSTNAME is empty in /etc/device.properties"); + deviceHostname = ""; // set empty default hostname } } } @@ -277,7 +277,7 @@ namespace WPEFramework m_wlanifname = wifiIfname; m_ethifname = ethIfname; m_deviceHostname = deviceHostname; - NMLOG_INFO("/etc/device.properties eth: %s, wlan: %s, device name: %s", m_ethifname.c_str(), m_wlanifname.c_str(), m_deviceHostname.c_str()); + NMLOG_INFO("/etc/device.properties eth: %s, wlan: %s, default hostname: %s", m_ethifname.c_str(), m_wlanifname.c_str(), m_deviceHostname.c_str()); return true; } diff --git a/plugin/gnome/NetworkManagerGnomeWIFI.cpp b/plugin/gnome/NetworkManagerGnomeWIFI.cpp index 2348124d..58872d48 100644 --- a/plugin/gnome/NetworkManagerGnomeWIFI.cpp +++ b/plugin/gnome/NetworkManagerGnomeWIFI.cpp @@ -45,7 +45,11 @@ namespace WPEFramework wifiManager::wifiManager() : m_client(nullptr), m_loop(nullptr), m_createNewConnection(false), m_objectPath(nullptr), m_wifidevice(nullptr), m_source(nullptr), m_cancellable(nullptr){ NMLOG_INFO("wifiManager"); m_nmContext = g_main_context_new(); - g_main_context_push_thread_default(m_nmContext); + // g_main_context_push_thread_default(m_nmContext); + // Do NOT push m_nmContext here. Pushing here permanently locks ownership + // to the constructor thread (owner_count stays at 1, never released). + // All callers — must push/pop around + // each createClientNewConnection()/deleteClientConnection() pair instead. m_loop = g_main_loop_new(m_nmContext, FALSE); } @@ -53,12 +57,21 @@ namespace WPEFramework { GError *error = NULL; + // Serialize concurrent wifi operations from different threads + m_opMutex.lock(); + + g_main_context_push_thread_default(m_nmContext); + m_client = nm_client_new(NULL, &error); if (!m_client || !m_loop) { if (error) { NMLOG_ERROR("Could not connect to NetworkManager: %s.", error->message); g_error_free(error); } + g_clear_object(&m_client); + m_client = nullptr; + g_main_context_pop_thread_default(m_nmContext); + m_opMutex.unlock(); return false; } @@ -79,7 +92,7 @@ namespace WPEFramework NMLOG_DEBUG("Cancelling pending async operations"); g_cancellable_cancel(m_cancellable); g_clear_object(&m_cancellable); - m_cancellable = NULL; + m_cancellable = nullptr; } } @@ -93,15 +106,21 @@ namespace WPEFramework g_main_context_iteration(context, TRUE); } g_main_context_unref(context); - m_client = NULL; + m_client = nullptr; } if(m_objectPath) { NMLOG_DEBUG("Freeing object path"); g_free(m_objectPath); - m_objectPath = NULL; + m_objectPath = nullptr; } + + // Pop the context pushed in createClientNewConnection() + if (m_nmContext) + g_main_context_pop_thread_default(m_nmContext); + // Release operation lock acquired in createClientNewConnection() + m_opMutex.unlock(); } bool wifiManager::quit(NMDevice *wifiNMDevice) @@ -407,6 +426,168 @@ namespace WPEFramework return m_isSuccess; } + static void ethernetDeactivateCb(GObject *object, GAsyncResult *result, gpointer user_data) + { + NMClient *client = NM_CLIENT(object); + GError *error = NULL; + wifiManager *_wifiManager = static_cast(user_data); + + NMLOG_DEBUG("ethernet connection deactivating..."); + _wifiManager->m_isSuccess = true; + if (!nm_client_deactivate_connection_finish(client, result, &error)) + { + NMLOG_ERROR("ethernet connection deactivate failed !"); + if(error != NULL) + { + if(g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + NMLOG_DEBUG("Deactivate operation was cancelled"); + } + else + { + NMLOG_ERROR("Deactivate error: %s", error->message); + } + g_error_free(error); + } + _wifiManager->m_isSuccess = false; + } + _wifiManager->quit(NULL); + } + + bool wifiManager::ethernetDeactivate() + { + NMDeviceState deviceState = NM_DEVICE_STATE_UNKNOWN; + if(!createClientNewConnection()) + return false; + + NMDevice *ethDevice = nm_client_get_device_by_iface(m_client, nmUtils::ethIface()); + if(ethDevice == NULL) { + NMLOG_WARNING("ethernet device not found !"); + deleteClientConnection(); + return false; + } + + deviceState = nm_device_get_state(ethDevice); + NMLOG_DEBUG("ethernet device current state is %d !", deviceState); + if (deviceState <= NM_DEVICE_STATE_DISCONNECTED || deviceState == NM_DEVICE_STATE_FAILED || deviceState == NM_DEVICE_STATE_DEACTIVATING) + { + NMLOG_WARNING("ethernet already disconnected !"); + deleteClientConnection(); + return true; + } + + NMActiveConnection *activeConn = nm_device_get_active_connection(ethDevice); + if(activeConn == NULL) { + NMLOG_WARNING("ethernet has no active connection, nothing to deactivate !"); + deleteClientConnection(); + return true; + } + + nm_client_deactivate_connection_async(m_client, activeConn, m_cancellable, ethernetDeactivateCb, this); + wait(m_loop); + deleteClientConnection(); + return m_isSuccess; + } + + static void appliedConnCb(GObject *src, GAsyncResult *res, gpointer user_data) + { + wifiManager *_wifiManager = static_cast(user_data); + GError *error = NULL; + guint64 versionId = 0; + NMConnection *conn = nm_device_get_applied_connection_finish( + NM_DEVICE(src), res, &versionId, &error); + if (error) { + NMLOG_ERROR("reacquireDhcpLease: get_applied_connection failed: %s", error->message); + g_error_free(error); + _wifiManager->m_appliedConn = nullptr; + _wifiManager->m_isSuccess = false; + } else { + _wifiManager->m_appliedConn = conn; + _wifiManager->m_versionId = versionId; + _wifiManager->m_isSuccess = true; + } + if (_wifiManager->m_loop) + g_main_loop_quit(_wifiManager->m_loop); + } + + static void reappliedCb(GObject *src, GAsyncResult *res, gpointer user_data) + { + wifiManager *_wifiManager = static_cast(user_data); + GError *error = NULL; + nm_device_reapply_finish(NM_DEVICE(src), res, &error); + if (error) { + NMLOG_ERROR("reacquireDhcpLease: reapply failed: %s", error->message); + g_error_free(error); + _wifiManager->m_isSuccess = false; + } else { + _wifiManager->m_isSuccess = true; + } + if (_wifiManager->m_loop) + g_main_loop_quit(_wifiManager->m_loop); + NMLOG_DEBUG("reacquireDhcpLease: reapply completed for '%s'", nm_device_get_iface(NM_DEVICE(src))); + } + + bool wifiManager::reacquireDhcpLease(const std::string& iface) + { + /* No direct libnm API to trigger a DHCP renew, hence by toggling ipv4.auto-route-ext-gw on the + * APPLIED connection (not the stored profile) and calling reapply(). + */ + if(!createClientNewConnection()) + return false; + + NMDevice *device = nm_client_get_device_by_iface(m_client, iface.c_str()); + if (device == NULL) { + NMLOG_ERROR("reacquireDhcpLease: device '%s' not found", iface.c_str()); + deleteClientConnection(); + return false; + } + + /* Round 1: fetch what NM actually has applied in memory */ + m_isSuccess = false; + m_appliedConn = nullptr; + nm_device_get_applied_connection_async(device, 0, m_cancellable, appliedConnCb, this); + wait(m_loop); + + if (!m_isSuccess || m_appliedConn == nullptr) { + NMLOG_ERROR("reacquireDhcpLease: could not get applied connection for '%s'", iface.c_str()); + deleteClientConnection(); + return false; + } + + NMSettingIPConfig *s_ip4 = NM_SETTING_IP_CONFIG( + nm_connection_get_setting(m_appliedConn, NM_TYPE_SETTING_IP4_CONFIG)); + if (s_ip4 == NULL) { + NMLOG_ERROR("reacquireDhcpLease: no IPv4 settings on '%s'", iface.c_str()); + g_object_unref(m_appliedConn); + m_appliedConn = nullptr; + deleteClientConnection(); + return false; + } + + NMTernary currentVal = NM_TERNARY_DEFAULT; + g_object_get(s_ip4, NM_SETTING_IP_CONFIG_AUTO_ROUTE_EXT_GW, ¤tVal, NULL); + NMTernary newVal = (currentVal == NM_TERNARY_DEFAULT) ? NM_TERNARY_TRUE : NM_TERNARY_DEFAULT; + NMLOG_DEBUG("reacquireDhcpLease: '%s' auto-route-ext-gw %d -> %d (in-memory only)", + iface.c_str(), static_cast(currentVal), static_cast(newVal)); + g_object_set(s_ip4, NM_SETTING_IP_CONFIG_AUTO_ROUTE_EXT_GW, newVal, NULL); + + /* Round 2: reapply with version_id for race safety — no disk write */ + m_isSuccess = false; + nm_device_reapply_async(device, m_appliedConn, m_versionId, 0, m_cancellable, reappliedCb, this); + wait(m_loop); + + if (!m_isSuccess) { + NMLOG_ERROR("reacquireDhcpLease: reapply failed for '%s'", iface.c_str()); + } else { + NMLOG_INFO("reacquireDhcpLease: reapply successful on '%s'", iface.c_str()); + } + + g_object_unref(m_appliedConn); + m_appliedConn = nullptr; + deleteClientConnection(); + return m_isSuccess; + } + static NMAccessPoint* findMatchingSSID(const GPtrArray* ApList, Exchange::INetworkManager::WiFiConnectTo& ssidInfo) { NMAccessPoint *AccessPoint = nullptr; @@ -637,18 +818,27 @@ namespace WPEFramework NMLOG_DEBUG("No persistent hostname found, using device hostname"); } + if(hostname.empty()) + NMLOG_WARNING("dhcp hostname: "); + else + NMLOG_INFO("dhcp hostname: %s", hostname.c_str()); + // IPv4 settings with DHCP NMSettingIP4Config *sIpv4 = (NMSettingIP4Config *)nm_setting_ip4_config_new(); g_object_set(G_OBJECT(sIpv4), NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP4_CONFIG_METHOD_AUTO, NULL); - g_object_set(G_OBJECT(sIpv4), NM_SETTING_IP_CONFIG_DHCP_HOSTNAME, hostname.c_str(), NULL); - g_object_set(G_OBJECT(sIpv4), NM_SETTING_IP_CONFIG_DHCP_SEND_HOSTNAME, TRUE, NULL); + if(!hostname.empty()) { + g_object_set(G_OBJECT(sIpv4), NM_SETTING_IP_CONFIG_DHCP_HOSTNAME, hostname.c_str(), NULL); + g_object_set(G_OBJECT(sIpv4), NM_SETTING_IP_CONFIG_DHCP_SEND_HOSTNAME, TRUE, NULL); + } nm_connection_add_setting(connection, NM_SETTING(sIpv4)); // IPv6 settings with DHCP NMSettingIP6Config *sIpv6 = (NMSettingIP6Config *)nm_setting_ip6_config_new(); g_object_set(G_OBJECT(sIpv6), NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP6_CONFIG_METHOD_AUTO, NULL); - g_object_set(G_OBJECT(sIpv6), NM_SETTING_IP_CONFIG_DHCP_HOSTNAME, hostname.c_str(), NULL); - g_object_set(G_OBJECT(sIpv6), NM_SETTING_IP_CONFIG_DHCP_SEND_HOSTNAME, TRUE, NULL); + if(!hostname.empty()) { + g_object_set(G_OBJECT(sIpv6), NM_SETTING_IP_CONFIG_DHCP_HOSTNAME, hostname.c_str(), NULL); + g_object_set(G_OBJECT(sIpv6), NM_SETTING_IP_CONFIG_DHCP_SEND_HOSTNAME, TRUE, NULL); + } nm_connection_add_setting(connection, NM_SETTING(sIpv6)); NMLOG_DEBUG("Created minimal ethernet connection with autoconnect=true"); @@ -755,7 +945,7 @@ namespace WPEFramework g_object_set(sWireless, NM_SETTING_WIRELESS_BSSID, ssidinfo.bssid.c_str(), NULL); } - if(ssidinfo.frequency != Exchange::INetworkManager::WIFIFrequency::WIFI_FREQUENCY_NONE) + if(ssidinfo.frequency != Exchange::INetworkManager::WIFIFrequency::WIFI_FREQUENCY_ALL) { if(ssidinfo.frequency == Exchange::INetworkManager::WIFIFrequency::WIFI_FREQUENCY_2_4_GHZ) { @@ -894,23 +1084,30 @@ namespace WPEFramework if(!nmUtils::readPersistentHostname(hostname)) { hostname = nmUtils::deviceHostname(); - NMLOG_DEBUG("no persistent hostname found taking device name as hostname !"); + NMLOG_DEBUG("No persistent hostname found, using device hostname"); } - NMLOG_INFO("dhcp hostname: %s", hostname.c_str()); + if(hostname.empty()) + NMLOG_WARNING("dhcp hostname: "); + else + NMLOG_INFO("dhcp hostname: %s", hostname.c_str()); /* Build up the 'IPv4' Setting */ NMSettingIP4Config *sIpv4Conf = (NMSettingIP4Config *) nm_setting_ip4_config_new(); g_object_set(G_OBJECT(sIpv4Conf), NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP4_CONFIG_METHOD_AUTO, NULL); // autoconf = true - g_object_set(G_OBJECT(sIpv4Conf), NM_SETTING_IP_CONFIG_DHCP_HOSTNAME, hostname.c_str(), NULL); - g_object_set(G_OBJECT(sIpv4Conf), NM_SETTING_IP_CONFIG_DHCP_SEND_HOSTNAME, TRUE, NULL); // hostname send enabled + if(!hostname.empty()) { + g_object_set(G_OBJECT(sIpv4Conf), NM_SETTING_IP_CONFIG_DHCP_HOSTNAME, hostname.c_str(), NULL); + g_object_set(G_OBJECT(sIpv4Conf), NM_SETTING_IP_CONFIG_DHCP_SEND_HOSTNAME, TRUE, NULL); // hostname send enabled + } nm_connection_add_setting(m_connection, NM_SETTING(sIpv4Conf)); /* Build up the 'IPv6' Setting */ NMSettingIP6Config *sIpv6Conf = (NMSettingIP6Config *) nm_setting_ip6_config_new(); g_object_set(G_OBJECT(sIpv6Conf), NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP6_CONFIG_METHOD_AUTO, NULL); // autoconf = true - g_object_set(G_OBJECT(sIpv6Conf), NM_SETTING_IP_CONFIG_DHCP_HOSTNAME, hostname.c_str(), NULL); - g_object_set(G_OBJECT(sIpv6Conf), NM_SETTING_IP_CONFIG_DHCP_SEND_HOSTNAME, TRUE, NULL); // hostname send enabled + if(!hostname.empty()) { + g_object_set(G_OBJECT(sIpv6Conf), NM_SETTING_IP_CONFIG_DHCP_HOSTNAME, hostname.c_str(), NULL); + g_object_set(G_OBJECT(sIpv6Conf), NM_SETTING_IP_CONFIG_DHCP_SEND_HOSTNAME, TRUE, NULL); // hostname send enabled + } nm_connection_add_setting(m_connection, NM_SETTING(sIpv6Conf)); return true; } @@ -1685,7 +1882,7 @@ namespace WPEFramework g_main_loop_quit(_wifiManager->m_loop); } - bool wifiManager::wifiScanRequest(std::string ssidReq) + bool wifiManager::wifiScanRequest(const std::vector& ssidsToFilter) { if(!createClientNewConnection()) return false; @@ -1696,23 +1893,25 @@ namespace WPEFramework return false; } m_isSuccess = false; - if(!ssidReq.empty()) + if(!ssidsToFilter.empty()) { - NMLOG_INFO("starting wifi scanning .. %s", ssidReq.c_str()); - GVariantBuilder builder, array_builder; + NMLOG_INFO("Starting wifi scanning for %d SSIDs:",static_cast(ssidsToFilter.size())); + GVariantBuilder nm_variant, nm_array_variant; GVariant *options; - g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); - g_variant_builder_init(&array_builder, G_VARIANT_TYPE("aay")); - g_variant_builder_add(&array_builder, "@ay", - g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, (const guint8 *) ssidReq.c_str(), ssidReq.length(), 1) - ); - g_variant_builder_add(&builder, "{sv}", "ssids", g_variant_builder_end(&array_builder)); - options = g_variant_builder_end(&builder); + g_variant_builder_init(&nm_variant, G_VARIANT_TYPE_VARDICT); + g_variant_builder_init(&nm_array_variant, G_VARIANT_TYPE("aay")); + for (const auto& ssid : ssidsToFilter) { + g_variant_builder_add(&nm_array_variant, "@ay", + g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, (const guint8 *) ssid.c_str(), ssid.length(), 1) + ); + } + g_variant_builder_add(&nm_variant, "{sv}", "ssids", g_variant_builder_end(&nm_array_variant)); + options = g_variant_builder_end(&nm_variant); nm_device_wifi_request_scan_options_async(wifiDevice, options, m_cancellable, wifiScanCb, this); g_variant_unref(options); // Unreference the GVariant after passing it to the async function } else { - NMLOG_INFO("staring normal wifi scanning .."); + NMLOG_INFO("Starting normal wifi scanning .."); nm_device_wifi_request_scan_async(wifiDevice, m_cancellable, wifiScanCb, this); } wait(m_loop); diff --git a/plugin/gnome/NetworkManagerGnomeWIFI.h b/plugin/gnome/NetworkManagerGnomeWIFI.h index 51164aa5..cfc741fb 100644 --- a/plugin/gnome/NetworkManagerGnomeWIFI.h +++ b/plugin/gnome/NetworkManagerGnomeWIFI.h @@ -31,6 +31,7 @@ #include #include #include +#include #define WPS_RETRY_WAIT_IN_MS 10 // 10 sec #define WPS_RETRY_COUNT 10 @@ -51,10 +52,12 @@ namespace WPEFramework bool getWifiState(Exchange::INetworkManager::WiFiState& state); bool wifiDisconnect(); + bool ethernetDeactivate(); + bool reacquireDhcpLease(const std::string& iface); bool activateKnownConnection(std::string iface, std::string knowConnectionID=""); bool wifiConnectedSSIDInfo(Exchange::INetworkManager::WiFiSSIDInfo &ssidinfo); bool wifiConnect(const Exchange::INetworkManager::WiFiConnectTo &ssidInfo); - bool wifiScanRequest(std::string ssidReq = ""); + bool wifiScanRequest(const std::vector& ssidsToFilter = {}); bool isWifiScannedRecently(int timelimitInSec = 5); // default 5 sec as shotest scanning interval bool getKnownSSIDs(std::list& ssids); bool addToKnownSSIDs(const Exchange::INetworkManager::WiFiConnectTo &ssidinfo); @@ -75,14 +78,14 @@ namespace WPEFramework wifiManager(); ~wifiManager() { NMLOG_INFO("~wifiManager"); + if(m_client != NULL) { + deleteClientConnection(); // handles pop + } if (m_nmContext) { - g_main_context_pop_thread_default(m_nmContext); + // Do NOT pop here — deleteClientConnection already popped g_main_context_unref(m_nmContext); m_nmContext = NULL; } - if(m_client != NULL) { - deleteClientConnection(); - } if(m_loop != NULL) { g_main_loop_unref(m_loop); m_loop = NULL; @@ -106,7 +109,10 @@ namespace WPEFramework GSource *m_source; GCancellable *m_cancellable; std::mutex m_cancellableMutex; + std::mutex m_opMutex; // serializes concurrent wifi operations from different threads bool m_isSuccess = false; + NMConnection *m_appliedConn = nullptr; + guint64 m_versionId = 0; SecretAgent m_secretAgent; }; } // Plugin diff --git a/plugin/gnome/gdbus/NetworkManagerGdbusProxy.cpp b/plugin/gnome/gdbus/NetworkManagerGdbusProxy.cpp index 73aa49d9..3cbb977e 100644 --- a/plugin/gnome/gdbus/NetworkManagerGdbusProxy.cpp +++ b/plugin/gnome/gdbus/NetworkManagerGdbusProxy.cpp @@ -216,7 +216,7 @@ namespace WPEFramework return rc; } - uint32_t NetworkManagerImplementation::StartWiFiScan(const string& frequency /* @in */, IStringIterator* const ssids/* @in */) + uint32_t NetworkManagerImplementation::StartWiFiScan(IStringIterator* const frequencies /* @in */, IStringIterator* const ssids/* @in */) { uint32_t rc = Core::ERROR_GENERAL; _nmGdbusEvents->setwifiScanOptions(true); /* Enable event posting */ diff --git a/plugin/rdk/NetworkManagerRDKProxy.cpp b/plugin/rdk/NetworkManagerRDKProxy.cpp index f218d61c..b163ae3c 100644 --- a/plugin/rdk/NetworkManagerRDKProxy.cpp +++ b/plugin/rdk/NetworkManagerRDKProxy.cpp @@ -965,7 +965,7 @@ const string CIDR_PREFIXES[CIDR_NETMASK_IP_LEN+1] = { return rc; } - uint32_t NetworkManagerImplementation::StartWiFiScan(const string& frequency /* @in */, IStringIterator* const ssids/* @in */) + uint32_t NetworkManagerImplementation::StartWiFiScan(IStringIterator* const frequencies /* @in */, IStringIterator* const ssids/* @in */) { LOG_ENTRY_FUNCTION(); uint32_t rc = Core::ERROR_RPC_CALL_FAILED; @@ -974,8 +974,7 @@ const string CIDR_PREFIXES[CIDR_NETMASK_IP_LEN+1] = { //Cleared the Existing Store filterred SSID list m_filterSsidslist.clear(); - m_filterfrequency.clear(); - + m_filterFrequencies.clear(); if(ssids) { string ssidlist{}; @@ -986,10 +985,14 @@ const string CIDR_PREFIXES[CIDR_NETMASK_IP_LEN+1] = { } } - if (!frequency.empty()) + if (frequencies) { - m_filterfrequency = frequency; - NMLOG_DEBUG("Scan SSIDs of frequency %s", m_filterfrequency.c_str()); + string frequencyList{}; + while (frequencies->Next(frequencyList) == true) + { + m_filterFrequencies.push_back(frequencyList.c_str()); + NMLOG_DEBUG("%s added to Frequency filtering", frequencyList.c_str()); + } } memset(¶m, 0, sizeof(param)); @@ -1177,6 +1180,20 @@ const string CIDR_PREFIXES[CIDR_NETMASK_IP_LEN+1] = { return rc; } + uint32_t NetworkManagerImplementation::EthernetDeactivate(void) + { + /* No-op on RDK platform */ + NMLOG_INFO("EthernetDeactivate: no-op on RDK platform"); + return Core::ERROR_UNAVAILABLE; + } + + uint32_t NetworkManagerImplementation::ReacquireDHCPLease(const string& iface) + { + /* No-op on RDK platform */ + NMLOG_INFO("ReacquireDHCPLease: no-op on RDK platform (iface=%s)", iface.c_str()); + return Core::ERROR_UNAVAILABLE; + } + uint32_t NetworkManagerImplementation::GetConnectedSSID(WiFiSSIDInfo& ssidInfo /* @out */) { LOG_ENTRY_FUNCTION(); diff --git a/tests/l2Test/libnm/CMakeLists.txt b/tests/l2Test/libnm/CMakeLists.txt index 092cc1d3..9b1a44d0 100644 --- a/tests/l2Test/libnm/CMakeLists.txt +++ b/tests/l2Test/libnm/CMakeLists.txt @@ -39,6 +39,7 @@ add_executable(${NM_LIBNM_PROXY_L2_TEST} ${CMAKE_SOURCE_DIR}/plugin/NetworkManagerImplementation.cpp ${CMAKE_SOURCE_DIR}/plugin/NetworkManagerConnectivity.cpp ${CMAKE_SOURCE_DIR}/plugin/NetworkManagerStunClient.cpp + ${CMAKE_SOURCE_DIR}/plugin/NetworkManagerPowerClient.cpp ${CMAKE_SOURCE_DIR}/plugin/gnome/NetworkManagerGnomeProxy.cpp ${CMAKE_SOURCE_DIR}/plugin/gnome/NetworkManagerGnomeWIFI.cpp ${CMAKE_SOURCE_DIR}/plugin/gnome/NetworkManagerGnomeEvents.cpp diff --git a/tests/l2Test/libnm/l2_test_libnmproxy.cpp b/tests/l2Test/libnm/l2_test_libnmproxy.cpp index e4f9b1ec..0a3dad69 100644 --- a/tests/l2Test/libnm/l2_test_libnmproxy.cpp +++ b/tests/l2Test/libnm/l2_test_libnmproxy.cpp @@ -40,6 +40,8 @@ using namespace WPEFramework; using ::testing::NiceMock; +namespace WPEFramework { namespace Plugin { extern NetworkManagerImplementation* _instance; } } + class NetworkManagerTest : public ::testing::Test { protected: Core::ProxyType plugin; @@ -515,24 +517,15 @@ TEST_F(NetworkManagerTest, GetIPSettings_unknown_iface) EXPECT_TRUE(response.find("\"success\":false") != std::string::npos); } -TEST_F(NetworkManagerTest, GetIPSettings_invalidDevice) -{ - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_device_by_iface(::testing::_, ::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(NULL))); - EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\"}"), response)); - EXPECT_TRUE(response.find("\"success\":false") != std::string::npos); -} - -TEST_F(NetworkManagerTest, GetIPSettings_invalid_state) +TEST_F(NetworkManagerTest, GetIPSettings_emptyCache) { - EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_UNMANAGED)); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_device_by_iface(::testing::_, ::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100178))); - + /* With no cache populated, GetIPSettings should still succeed but return no IP data */ EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\"}"), response)); - EXPECT_TRUE(response.find("\"success\":false") != std::string::npos); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"interface\":\"eth0\"") != std::string::npos); + EXPECT_TRUE(response.find("\"ipversion\":\"IPv4\"") != std::string::npos); + /* No ipaddress key when cache is empty */ + EXPECT_TRUE(response.find("\"ipaddress\"") == std::string::npos); } TEST_F(NetworkManagerTest, GetIPSettings_interface_Empty) @@ -548,348 +541,408 @@ TEST_F(NetworkManagerTest, GetIPSettings_GetPrimary_failed) EXPECT_EQ(Core::ERROR_GENERAL, NetworkManagerImpl2->GetIPSettings(interface, ipversion, address)); } -TEST_F(NetworkManagerTest, GetIPSettings_Invalid_ActiveConnection) +TEST_F(NetworkManagerTest, GetIPSettings_ipv4_fromCache) { - EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_ACTIVATED)); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_active_connections(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(NULL))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_device_by_iface(::testing::_, ::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100178))); + /* Populate IPv4 cache for eth0, then verify GetIPSettings returns cached data */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.globalAddresses["192.168.1.2"] = Plugin::GlobalAddressInfo(24, Plugin::ADDR_GLOBAL); + cache.gateway = "192.168.1.1"; + cache.primarydns = "8.8.8.8"; + cache.secondarydns = "8.8.4.4"; + cache.dhcpserver = "192.168.1.11"; + cache.autoconfig = true; + Plugin::_instance->swapIpCache("eth0", "IPv4", cache); EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\"}"), response)); - EXPECT_TRUE(response.find("\"success\":false") != std::string::npos); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"interface\":\"eth0\"") != std::string::npos); + EXPECT_TRUE(response.find("\"ipversion\":\"IPv4\"") != std::string::npos); + EXPECT_TRUE(response.find("\"autoconfig\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"ipaddress\":\"192.168.1.2\"") != std::string::npos); + EXPECT_TRUE(response.find("\"prefix\":24") != std::string::npos); + EXPECT_TRUE(response.find("\"gateway\":\"192.168.1.1\"") != std::string::npos); + EXPECT_TRUE(response.find("\"primarydns\":\"8.8.8.8\"") != std::string::npos); + EXPECT_TRUE(response.find("\"secondarydns\":\"8.8.4.4\"") != std::string::npos); + EXPECT_TRUE(response.find("\"dhcpserver\":\"192.168.1.11\"") != std::string::npos); } -TEST_F(NetworkManagerTest, GetIPSettings_Invalid_Connection) +TEST_F(NetworkManagerTest, GetIPSettings_ipv4_autoconfig) { - GPtrArray* dummyActiveConn = g_ptr_array_new(); - NMActiveConnection *nullConnection = static_cast(NULL); - NMActiveConnection *ethActiveConn = static_cast(g_object_new(NM_TYPE_ACTIVE_CONNECTION, NULL)); - g_ptr_array_add(dummyActiveConn, nullConnection); - g_ptr_array_add(dummyActiveConn, ethActiveConn); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_connection(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(NULL))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_ACTIVATED)); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_active_connections(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(dummyActiveConn))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_device_by_iface(::testing::_, ::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100178))); + /* Populate IPv4 cache with autoconfig=true but no IP address */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.autoconfig = true; + Plugin::_instance->swapIpCache("eth0", "IPv4", cache); EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\"}"), response)); - EXPECT_TRUE(response.find("\"success\":false") != std::string::npos); - - g_object_unref(ethActiveConn); - g_ptr_array_free(dummyActiveConn, TRUE); + std::string expectedResponse = + _T("{\"interface\":\"eth0\",\"ipversion\":\"IPv4\",\"autoconfig\":true,\"success\":true}"); + EXPECT_EQ(response, expectedResponse); } -TEST_F(NetworkManagerTest, GetIPSettings_valid_ConnectionSettingsEmpty) +TEST_F(NetworkManagerTest, GetIPSettings_ipv4_staticConfig) { - GPtrArray* dummyActiveConn = g_ptr_array_new(); - NMActiveConnection *ethActiveConn = static_cast(g_object_new(NM_TYPE_ACTIVE_CONNECTION, NULL)); - NMRemoteConnection* retConn = static_cast(g_object_new(NM_TYPE_REMOTE_CONNECTION, NULL)); - g_ptr_array_add(dummyActiveConn, ethActiveConn); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_connection_get_setting_connection(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(NULL))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_connection(::testing::_)) - .WillOnce(::testing::Return(retConn)); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_ACTIVATED)); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_active_connections(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(dummyActiveConn))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_device_by_iface(::testing::_, ::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100178))); + /* Populate IPv4 cache with autoconfig=false (static config) */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.autoconfig = false; + cache.globalAddresses["192.168.1.100"] = Plugin::GlobalAddressInfo(24, Plugin::ADDR_GLOBAL); + cache.gateway = "192.168.1.1"; + Plugin::_instance->swapIpCache("eth0", "IPv4", cache); EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\"}"), response)); - EXPECT_TRUE(response.find("\"success\":false") != std::string::npos); - - g_object_unref(ethActiveConn); - g_ptr_array_free(dummyActiveConn, TRUE); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"autoconfig\":false") != std::string::npos); + EXPECT_TRUE(response.find("\"ipaddress\":\"192.168.1.100\"") != std::string::npos); } -TEST_F(NetworkManagerTest, GetIPSettings_ipv4_config) +TEST_F(NetworkManagerTest, GetIPSettings_wlan0_fromCache) { - GPtrArray* dummyActiveConn = g_ptr_array_new(); - NMActiveConnection *ethActiveConn = static_cast(g_object_new(NM_TYPE_ACTIVE_CONNECTION, NULL)); - NMRemoteConnection* retConn = static_cast(g_object_new(NM_TYPE_REMOTE_CONNECTION, NULL)); - g_ptr_array_add(dummyActiveConn, ethActiveConn); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_ip4_config(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(NULL))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_setting_connection_get_interface_name(::testing::_)) - .WillOnce(::testing::Return("eth0")); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_connection_get_setting_connection(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100173))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_connection(::testing::_)) - .WillOnce(::testing::Return(retConn)) - .WillOnce(::testing::Return(reinterpret_cast(NULL))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_ACTIVATED)); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_active_connections(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(dummyActiveConn))); + /* Populate IPv4 cache for wlan0 */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.autoconfig = true; + cache.globalAddresses["10.0.0.5"] = Plugin::GlobalAddressInfo(8, Plugin::ADDR_GLOBAL); + cache.gateway = "10.0.0.1"; + cache.primarydns = "1.1.1.1"; + Plugin::_instance->swapIpCache("wlan0", "IPv4", cache); + + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"wlan0\"}"), response)); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"interface\":\"wlan0\"") != std::string::npos); + EXPECT_TRUE(response.find("\"ipaddress\":\"10.0.0.5\"") != std::string::npos); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_device_by_iface(::testing::_, ::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100178))); +TEST_F(NetworkManagerTest, GetIPSettings_ipv4_config_valid) +{ + /* Populate full IPv4 cache for eth0 and verify all fields */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.autoconfig = true; + cache.globalAddresses["192.168.1.2"] = Plugin::GlobalAddressInfo(24, Plugin::ADDR_GLOBAL); + cache.gateway = "192.168.1.0"; + cache.primarydns = "8.8.8.8"; + cache.secondarydns = "8.8.4.4"; + cache.dhcpserver = "192.168.1.11"; + Plugin::_instance->swapIpCache("eth0", "IPv4", cache); EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\"}"), response)); - std::string expectedResponse = - _T("{\"interface\":\"eth0\",\"ipversion\":\"IPv4\",\"autoconfig\":true,\"success\":true}"); - EXPECT_EQ(response, expectedResponse); - g_object_unref(ethActiveConn); - g_ptr_array_free(dummyActiveConn, TRUE); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"secondarydns\":\"8.8.4.4\"") != std::string::npos); + EXPECT_TRUE(response.find("\"primarydns\":\"8.8.8.8\"") != std::string::npos); + EXPECT_TRUE(response.find("\"interface\":\"eth0\"") != std::string::npos); + EXPECT_TRUE(response.find("\"ipaddress\":\"192.168.1.2\"") != std::string::npos); + EXPECT_TRUE(response.find("\"ula\":\"\"") != std::string::npos); + EXPECT_TRUE(response.find("\"dhcpserver\":\"192.168.1.11\"") != std::string::npos); + EXPECT_TRUE(response.find("\"gateway\":\"192.168.1.0\"") != std::string::npos); } -TEST_F(NetworkManagerTest, GetIPSettings_ipv4_configAutoConftrue) +TEST_F(NetworkManagerTest, GetIPSettings_ipv6_config_valid) { - GPtrArray* dummyActiveConn = g_ptr_array_new(); - NMActiveConnection *ethActiveConn = static_cast(g_object_new(NM_TYPE_ACTIVE_CONNECTION, NULL)); - NMRemoteConnection* retConn = static_cast(g_object_new(NM_TYPE_REMOTE_CONNECTION, NULL)); - g_ptr_array_add(dummyActiveConn, ethActiveConn); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_ip4_config(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(NULL))); + /* Populate full IPv6 cache for eth0 and verify all fields */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.autoconfig = true; + cache.globalAddresses["2001:db8:1:2:3:4:5:6"] = Plugin::GlobalAddressInfo(64, Plugin::ADDR_GLOBAL); + cache.uniqueLocalAddresses.insert("fd12::1234:5678:abcd:ef01"); + cache.gateway = "2001:4860:4860::1"; + cache.primarydns = "2001:4860:4860::8888"; + cache.secondarydns = "2001:4860:4860::8844"; + cache.dhcpserver = "2001:db8::1"; + Plugin::_instance->swapIpCache("eth0", "IPv6", cache); - EXPECT_CALL(*p_libnmWrapsImplMock, nm_setting_connection_get_interface_name(::testing::_)) - .WillOnce(::testing::Return("eth0")); + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\", \"ipversion\":\"IPv6\"}"), response)); - EXPECT_CALL(*p_libnmWrapsImplMock, nm_connection_get_setting_connection(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100173))); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"ipversion\":\"IPv6\"") != std::string::npos); + EXPECT_TRUE(response.find("\"secondarydns\":\"2001:4860:4860::8844\"") != std::string::npos); + EXPECT_TRUE(response.find("\"primarydns\":\"2001:4860:4860::8888\"") != std::string::npos); + EXPECT_TRUE(response.find("\"interface\":\"eth0\"") != std::string::npos); + EXPECT_TRUE(response.find("\"ipaddress\":\"2001:db8:1:2:3:4:5:6\"") != std::string::npos); + EXPECT_TRUE(response.find("\"ula\":\"fd12::1234:5678:abcd:ef01\"") != std::string::npos); + EXPECT_TRUE(response.find("\"prefix\":64") != std::string::npos); + EXPECT_TRUE(response.find("\"dhcpserver\":\"2001:db8::1\"") != std::string::npos); + EXPECT_TRUE(response.find("\"gateway\":\"2001:4860:4860::1\"") != std::string::npos); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_setting_ip_config_get_method(::testing::_)) - .WillOnce(::testing::Return("auto")); +TEST_F(NetworkManagerTest, GetIPSettings_ipv6_mac_based_fallback) +{ + /* When all global addresses are MAC-based, toIPAddress should use the MAC-based one */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.autoconfig = true; + cache.globalAddresses["2001:db8::aabb:ccff:fedd:eeff"] = Plugin::GlobalAddressInfo(64, Plugin::ADDR_GLOBAL_MAC_BASED); + cache.gateway = "fe80::1"; + Plugin::_instance->swapIpCache("eth0", "IPv6", cache); - EXPECT_CALL(*p_libnmWrapsImplMock, nm_connection_get_setting_ip4_config(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100173))); + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\", \"ipversion\":\"IPv6\"}"), response)); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"ipaddress\":\"2001:db8::aabb:ccff:fedd:eeff\"") != std::string::npos); + EXPECT_TRUE(response.find("\"prefix\":64") != std::string::npos); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_connection(::testing::_)) - .WillOnce(::testing::Return(retConn)) - .WillOnce(::testing::Return(reinterpret_cast(retConn))); +TEST_F(NetworkManagerTest, GetIPSettings_ipv6_prefer_non_mac_global) +{ + /* ADDR_GLOBAL should be preferred over ADDR_GLOBAL_MAC_BASED */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.autoconfig = true; + /* Insert MAC-based first to ensure it's not selected by insertion order */ + cache.globalAddresses["2001:db8::aabb:ccff:fedd:eeff"] = Plugin::GlobalAddressInfo(64, Plugin::ADDR_GLOBAL_MAC_BASED); + cache.globalAddresses["2001:db8::1234:5678"] = Plugin::GlobalAddressInfo(64, Plugin::ADDR_GLOBAL); + Plugin::_instance->swapIpCache("eth0", "IPv6", cache); - EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_ACTIVATED)); + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\", \"ipversion\":\"IPv6\"}"), response)); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"ipaddress\":\"2001:db8::1234:5678\"") != std::string::npos); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_active_connections(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(dummyActiveConn))); +TEST_F(NetworkManagerTest, GetIPSettings_ipv6_only_cached_request_ipv4) +{ + /* Only IPv6 cached, but IPv4 requested — should return success with no IP */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.autoconfig = true; + cache.globalAddresses["2001:db8::1"] = Plugin::GlobalAddressInfo(64, Plugin::ADDR_GLOBAL); + Plugin::_instance->swapIpCache("eth0", "IPv6", cache); + + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\", \"ipversion\":\"IPv4\"}"), response)); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"ipversion\":\"IPv4\"") != std::string::npos); + EXPECT_TRUE(response.find("\"ipaddress\"") == std::string::npos); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_device_by_iface(::testing::_, ::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100178))); +TEST_F(NetworkManagerTest, GetIPSettings_cache_invalidated) +{ + /* Cache exists but valid=false — should return success with no IP data */ + Plugin::IpFamilyCache cache; + cache.valid = false; + cache.globalAddresses["192.168.1.5"] = Plugin::GlobalAddressInfo(24, Plugin::ADDR_GLOBAL); + Plugin::_instance->swapIpCache("eth0", "IPv4", cache); EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\"}"), response)); - std::string expectedResponse = - _T("{\"interface\":\"eth0\",\"ipversion\":\"IPv4\",\"autoconfig\":true,\"success\":true}"); - EXPECT_EQ(response, expectedResponse); - - g_object_unref(ethActiveConn); - g_ptr_array_free(dummyActiveConn, TRUE); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"ipaddress\"") == std::string::npos); } -TEST_F(NetworkManagerTest, GetIPSettings_ipv4_configAutoConfNull) +TEST_F(NetworkManagerTest, GetIPSettings_ipversion_case_insensitive) { - GPtrArray* dummyActiveConn = g_ptr_array_new(); - NMActiveConnection *ethActiveConn = static_cast(g_object_new(NM_TYPE_ACTIVE_CONNECTION, NULL)); - NMRemoteConnection* retConn = static_cast(g_object_new(NM_TYPE_REMOTE_CONNECTION, NULL)); - g_ptr_array_add(dummyActiveConn, ethActiveConn); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_ip4_config(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(NULL))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_setting_connection_get_interface_name(::testing::_)) - .WillOnce(::testing::Return("eth0")); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_connection_get_setting_connection(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100173))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_setting_ip_config_get_method(::testing::_)) - .WillOnce(::testing::Return("not auto")); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_connection_get_setting_ip4_config(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100173))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_connection(::testing::_)) - .WillOnce(::testing::Return(retConn)) - .WillOnce(::testing::Return(reinterpret_cast(retConn))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_ACTIVATED)); + /* ipversion "ipv6" (lowercase) should be treated as IPv6 */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.autoconfig = true; + cache.globalAddresses["2001:db8::99"] = Plugin::GlobalAddressInfo(128, Plugin::ADDR_GLOBAL); + Plugin::_instance->swapIpCache("eth0", "IPv6", cache); + + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\", \"ipversion\":\"ipv6\"}"), response)); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"ipaddress\":\"2001:db8::99\"") != std::string::npos); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_active_connections(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(dummyActiveConn))); +TEST_F(NetworkManagerTest, GetIPSettings_ipv6_ula_only) +{ + /* Cache has only ULA addresses, no global — ipaddress is empty so the + JSON-RPC layer (NetworkManagerJsonRpc.cpp) skips all address fields + including ula, gateway, etc. Only interface/ipversion/autoconfig/success + are returned. */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.autoconfig = true; + cache.uniqueLocalAddresses.insert("fd00::1234:abcd"); + cache.gateway = "fe80::1"; + Plugin::_instance->swapIpCache("eth0", "IPv6", cache); - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_device_by_iface(::testing::_, ::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100178))); + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\", \"ipversion\":\"IPv6\"}"), response)); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + /* No global address → ipaddress empty → JSON-RPC omits address detail fields */ + EXPECT_TRUE(response.find("\"ipaddress\"") == std::string::npos); + EXPECT_TRUE(response.find("\"ula\"") == std::string::npos); + EXPECT_TRUE(response.find("\"gateway\"") == std::string::npos); + EXPECT_TRUE(response.find("\"autoconfig\":true") != std::string::npos); +} +TEST_F(NetworkManagerTest, GetIPSettings_swapIpCache_returns_old_keys) +{ + /* Verify swapIpCache returns the old global address keys */ + Plugin::IpFamilyCache cache1; + cache1.valid = true; + cache1.globalAddresses["192.168.1.10"] = Plugin::GlobalAddressInfo(24, Plugin::ADDR_GLOBAL); + cache1.globalAddresses["192.168.1.20"] = Plugin::GlobalAddressInfo(24, Plugin::ADDR_GLOBAL); + Plugin::_instance->swapIpCache("eth0", "IPv4", cache1); + + /* Now swap with a new cache and check old keys are returned */ + Plugin::IpFamilyCache cache2; + cache2.valid = true; + cache2.globalAddresses["10.0.0.1"] = Plugin::GlobalAddressInfo(8, Plugin::ADDR_GLOBAL); + std::set oldKeys = Plugin::_instance->swapIpCache("eth0", "IPv4", cache2); + + EXPECT_EQ(oldKeys.size(), 2u); + EXPECT_TRUE(oldKeys.count("192.168.1.10") == 1); + EXPECT_TRUE(oldKeys.count("192.168.1.20") == 1); + + /* Verify the new cache is now active */ EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\"}"), response)); - std::string expectedResponse = - _T("{\"interface\":\"eth0\",\"ipversion\":\"IPv4\",\"autoconfig\":true,\"success\":true}"); - EXPECT_EQ(response, expectedResponse); - - g_object_unref(ethActiveConn); - g_ptr_array_free(dummyActiveConn, TRUE); + EXPECT_TRUE(response.find("\"ipaddress\":\"10.0.0.1\"") != std::string::npos); } -TEST_F(NetworkManagerTest, GetIPSettings_ipv4_config_valid) +TEST_F(NetworkManagerTest, GetIPSettings_separate_ipv4_ipv6_caches) { - NMActiveConnection *ethActiveConn = static_cast(g_object_new(NM_TYPE_ACTIVE_CONNECTION, NULL)); - NMRemoteConnection* retConn = static_cast(g_object_new(NM_TYPE_REMOTE_CONNECTION, NULL)); - NMIPAddress* ipv4Addr = static_cast(g_object_new(NM_TYPE_REMOTE_CONNECTION, NULL)); - - GPtrArray* dummyActiveConn = g_ptr_array_new(); - GPtrArray* ipvAddr = g_ptr_array_new(); - g_ptr_array_add(dummyActiveConn, ethActiveConn); - g_ptr_array_add(ipvAddr, ipv4Addr); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_dhcp_config_get_one_option(::testing::_, ::testing::_)) - .WillOnce(::testing::Return("192.168.1.11")); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_dhcp4_config(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100170))); - - const char* fakeDnsServers[] = {"8.8.8.8", "8.8.4.4", nullptr}; - EXPECT_CALL(*p_libnmWrapsImplMock, nm_ip_config_get_nameservers(::testing::_)) - .WillOnce(::testing::Return(fakeDnsServers)); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_ip_config_get_gateway(::testing::_)) - .WillOnce(::testing::Return("192.168.1.0")); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_ip_address_get_address(::testing::_)) - .WillOnce(::testing::Return("192.168.1.2")); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_ip_config_get_addresses(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(ipvAddr))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_ip4_config(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100171))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_setting_connection_get_interface_name(::testing::_)) - .WillOnce(::testing::Return("eth0")); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_connection_get_setting_connection(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100173))); + /* IPv4 and IPv6 caches for the same interface should be independent */ + Plugin::IpFamilyCache cache4; + cache4.valid = true; + cache4.autoconfig = true; + cache4.globalAddresses["192.168.1.50"] = Plugin::GlobalAddressInfo(24, Plugin::ADDR_GLOBAL); + Plugin::_instance->swapIpCache("eth0", "IPv4", cache4); + + Plugin::IpFamilyCache cache6; + cache6.valid = true; + cache6.autoconfig = true; + cache6.globalAddresses["2001:db8::50"] = Plugin::GlobalAddressInfo(64, Plugin::ADDR_GLOBAL); + cache6.uniqueLocalAddresses.insert("fd12::50"); + Plugin::_instance->swapIpCache("eth0", "IPv6", cache6); + + /* Query IPv4 */ + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\", \"ipversion\":\"IPv4\"}"), response)); + EXPECT_TRUE(response.find("\"ipaddress\":\"192.168.1.50\"") != std::string::npos); + EXPECT_TRUE(response.find("\"ipversion\":\"IPv4\"") != std::string::npos); + + /* Query IPv6 */ + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\", \"ipversion\":\"IPv6\"}"), response)); + EXPECT_TRUE(response.find("\"ipaddress\":\"2001:db8::50\"") != std::string::npos); + EXPECT_TRUE(response.find("\"ipversion\":\"IPv6\"") != std::string::npos); + EXPECT_TRUE(response.find("\"ula\":\"fd12::50\"") != std::string::npos); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_connection(::testing::_)) - .WillOnce(::testing::Return(retConn)) - .WillOnce(::testing::Return(reinterpret_cast(NULL))); +TEST_F(NetworkManagerTest, GetIPSettings_cache_cleared) +{ + /* Populate cache, then swap with empty/invalid cache — simulates disconnect */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.globalAddresses["192.168.1.99"] = Plugin::GlobalAddressInfo(24, Plugin::ADDR_GLOBAL); + Plugin::_instance->swapIpCache("eth0", "IPv4", cache); - EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_ACTIVATED)); + /* Verify it's there */ + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\"}"), response)); + EXPECT_TRUE(response.find("\"ipaddress\":\"192.168.1.99\"") != std::string::npos); - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_active_connections(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(dummyActiveConn))); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_device_by_iface(::testing::_, ::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100178))); + /* Clear cache by swapping with default (valid=false) */ + Plugin::IpFamilyCache empty; + Plugin::_instance->swapIpCache("eth0", "IPv4", empty); EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\"}"), response)); - EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); - EXPECT_TRUE(response.find("\"secondarydns\":\"8.8.4.4\"") != std::string::npos); - EXPECT_TRUE(response.find("\"primarydns\":\"8.8.8.8\"") != std::string::npos); - EXPECT_TRUE(response.find("\"interface\":\"eth0\"") != std::string::npos); - EXPECT_TRUE(response.find("\"ipaddress\":\"192.168.1.2\"") != std::string::npos); - EXPECT_TRUE(response.find("\"ula\":\"\"") != std::string::npos); - EXPECT_TRUE(response.find("\"dhcpserver\":\"192.168.1.11\"") != std::string::npos); - EXPECT_TRUE(response.find("\"gateway\":\"192.168.1.0\"") != std::string::npos); - - g_object_unref(ethActiveConn); - g_object_unref(retConn); - g_object_unref(ipv4Addr); - g_ptr_array_free(dummyActiveConn, TRUE); - g_ptr_array_free(ipvAddr, TRUE); + EXPECT_TRUE(response.find("\"ipaddress\"") == std::string::npos); } -TEST_F(NetworkManagerTest, GetIPSettings_ipv6_config_valid) -{ - NMActiveConnection *ethActiveConn = static_cast(g_object_new(NM_TYPE_ACTIVE_CONNECTION, NULL)); - NMRemoteConnection* retConn = static_cast(g_object_new(NM_TYPE_REMOTE_CONNECTION, NULL)); - NMIPAddress* ipv6Addr = static_cast(g_object_new(NM_TYPE_REMOTE_CONNECTION, NULL)); - - GPtrArray* dummyActiveConn = g_ptr_array_new(); - GPtrArray* ipvAddr = g_ptr_array_new(); - g_ptr_array_add(dummyActiveConn, ethActiveConn); - g_ptr_array_add(ipvAddr, ipv6Addr); - g_ptr_array_add(ipvAddr, reinterpret_cast(0x100176)); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_dhcp_config_get_one_option(::testing::_, ::testing::_)) - .WillOnce(::testing::Return("2001:db8::1")); - - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_dhcp6_config(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100170))); - - const char* fakeDnsServers[] = {"2001:4860:4860::8888", "2001:4860:4860::8844", nullptr}; - EXPECT_CALL(*p_libnmWrapsImplMock, nm_ip_config_get_nameservers(::testing::_)) - .WillOnce(::testing::Return(fakeDnsServers)); +/* ──────────────────────────────────────────────────────────────────────────── + * Utility function tests — isIPv4LinkLocal, isIPv6LinkLocal, isIPv6ULA, + * isIPv6MacBased (which exercises parseMac internally). + * ──────────────────────────────────────────────────────────────────────────── */ - EXPECT_CALL(*p_libnmWrapsImplMock, nm_ip_config_get_gateway(::testing::_)) - .WillOnce(::testing::Return("2001:4860:4860::1")); +TEST_F(NetworkManagerTest, isIPv4LinkLocal_true_for_169_254) +{ + EXPECT_TRUE(Plugin::isIPv4LinkLocal("169.254.0.1")); + EXPECT_TRUE(Plugin::isIPv4LinkLocal("169.254.255.255")); + EXPECT_TRUE(Plugin::isIPv4LinkLocal("169.254.100.50")); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_ip_address_get_prefix(::testing::_)) - .WillOnce(::testing::Return(64)); +TEST_F(NetworkManagerTest, isIPv4LinkLocal_false_for_non_link_local) +{ + EXPECT_FALSE(Plugin::isIPv4LinkLocal("192.168.1.1")); + EXPECT_FALSE(Plugin::isIPv4LinkLocal("10.0.0.1")); + EXPECT_FALSE(Plugin::isIPv4LinkLocal("169.253.255.255")); + EXPECT_FALSE(Plugin::isIPv4LinkLocal("169.255.0.1")); + EXPECT_FALSE(Plugin::isIPv4LinkLocal("0.0.0.0")); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_ip_address_get_address(::testing::_)) - .WillOnce(::testing::Return("2001:db8:1:2:3:4:5:6")) - .WillOnce(::testing::Return("fe80::1234:5678:abcd:ef01")); +TEST_F(NetworkManagerTest, isIPv4LinkLocal_false_for_invalid_input) +{ + EXPECT_FALSE(Plugin::isIPv4LinkLocal("")); + EXPECT_FALSE(Plugin::isIPv4LinkLocal("not_an_ip")); + EXPECT_FALSE(Plugin::isIPv4LinkLocal("fe80::1")); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_ip_config_get_addresses(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(ipvAddr))); +TEST_F(NetworkManagerTest, isIPv6LinkLocal_true_for_fe80) +{ + EXPECT_TRUE(Plugin::isIPv6LinkLocal("fe80::1")); + EXPECT_TRUE(Plugin::isIPv6LinkLocal("fe80::abcd:1234:5678:9abc")); + EXPECT_TRUE(Plugin::isIPv6LinkLocal("fe80::ffff:ffff:ffff:ffff")); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_ip6_config(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100171))); +TEST_F(NetworkManagerTest, isIPv6LinkLocal_false_for_non_link_local) +{ + EXPECT_FALSE(Plugin::isIPv6LinkLocal("2001:db8::1")); + EXPECT_FALSE(Plugin::isIPv6LinkLocal("fd00::1")); + EXPECT_FALSE(Plugin::isIPv6LinkLocal("::1")); + EXPECT_FALSE(Plugin::isIPv6LinkLocal("fc00::1")); + EXPECT_FALSE(Plugin::isIPv6LinkLocal("fec0::1")); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_setting_connection_get_interface_name(::testing::_)) - .WillOnce(::testing::Return("eth0")); +TEST_F(NetworkManagerTest, isIPv6LinkLocal_false_for_invalid_input) +{ + EXPECT_FALSE(Plugin::isIPv6LinkLocal("")); + EXPECT_FALSE(Plugin::isIPv6LinkLocal("not_an_ip")); + EXPECT_FALSE(Plugin::isIPv6LinkLocal("169.254.1.1")); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_connection_get_setting_connection(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100173))); +TEST_F(NetworkManagerTest, isIPv6ULA_true_for_fc_fd) +{ + EXPECT_TRUE(Plugin::isIPv6ULA("fc00::1")); + EXPECT_TRUE(Plugin::isIPv6ULA("fd00::1")); + EXPECT_TRUE(Plugin::isIPv6ULA("fd12:3456:789a::1")); + EXPECT_TRUE(Plugin::isIPv6ULA("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_active_connection_get_connection(::testing::_)) - .WillOnce(::testing::Return(retConn)) - .WillOnce(::testing::Return(reinterpret_cast(NULL))); +TEST_F(NetworkManagerTest, isIPv6ULA_false_for_non_ula) +{ + EXPECT_FALSE(Plugin::isIPv6ULA("2001:db8::1")); + EXPECT_FALSE(Plugin::isIPv6ULA("fe80::1")); + EXPECT_FALSE(Plugin::isIPv6ULA("::1")); + EXPECT_FALSE(Plugin::isIPv6ULA("fb00::1")); + EXPECT_FALSE(Plugin::isIPv6ULA("fe00::1")); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_ACTIVATED)); +TEST_F(NetworkManagerTest, isIPv6ULA_false_for_invalid_input) +{ + EXPECT_FALSE(Plugin::isIPv6ULA("")); + EXPECT_FALSE(Plugin::isIPv6ULA("garbage")); + EXPECT_FALSE(Plugin::isIPv6ULA("192.168.1.1")); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_active_connections(::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(dummyActiveConn))); +TEST_F(NetworkManagerTest, isIPv6MacBased_true_for_eui64_colon_mac) +{ + /* MAC AA:BB:CC:DD:EE:FF → EUI-64: A8:BB:CC:FF:FE:DD:EE:FF + (byte 0: 0xAA ^ 0x02 = 0xA8, insert FF:FE in middle) */ + EXPECT_TRUE(Plugin::isIPv6MacBased("2001:db8::a8bb:ccff:fedd:eeff", "AA:BB:CC:DD:EE:FF")); +} - EXPECT_CALL(*p_libnmWrapsImplMock, nm_client_get_device_by_iface(::testing::_, ::testing::_)) - .WillOnce(::testing::Return(reinterpret_cast(0x100178))); +TEST_F(NetworkManagerTest, isIPv6MacBased_true_for_eui64_plain_mac) +{ + /* Same MAC in plain hex format */ + EXPECT_TRUE(Plugin::isIPv6MacBased("2001:db8::a8bb:ccff:fedd:eeff", "aabbccddeeff")); +} - EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\", \"ipversion\":\"IPv6\"}"), response)); +TEST_F(NetworkManagerTest, isIPv6MacBased_false_for_non_eui64) +{ + /* Address that doesn't match the MAC's EUI-64 */ + EXPECT_FALSE(Plugin::isIPv6MacBased("2001:db8::1234:5678:9abc:def0", "AA:BB:CC:DD:EE:FF")); +} - EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); - EXPECT_TRUE(response.find("\"ipversion\":\"IPv6\"") != std::string::npos); - EXPECT_TRUE(response.find("\"secondarydns\":\"2001:4860:4860::8844\"") != std::string::npos); - EXPECT_TRUE(response.find("\"primarydns\":\"2001:4860:4860::8888\"") != std::string::npos); - EXPECT_TRUE(response.find("\"interface\":\"eth0\"") != std::string::npos); - EXPECT_TRUE(response.find("\"ipaddress\":\"2001:db8:1:2:3:4:5:6\"") != std::string::npos); - EXPECT_TRUE(response.find("\"ula\":\"fe80::1234:5678:abcd:ef01\"") != std::string::npos); - EXPECT_TRUE(response.find("\"prefix\":64") != std::string::npos); - EXPECT_TRUE(response.find("\"dhcpserver\":\"2001:db8::1\"") != std::string::npos); - EXPECT_TRUE(response.find("\"gateway\":\"2001:4860:4860::1\"") != std::string::npos); +TEST_F(NetworkManagerTest, isIPv6MacBased_false_for_privacy_address) +{ + /* Privacy extension address — random interface ID, not MAC-based */ + EXPECT_FALSE(Plugin::isIPv6MacBased("2001:db8::4f2a:8c91:e3d7:b560", "00:11:22:33:44:55")); +} - g_object_unref(ethActiveConn); - g_object_unref(retConn); - g_object_unref(ipv6Addr); - g_ptr_array_free(dummyActiveConn, TRUE); - g_ptr_array_free(ipvAddr, TRUE); +TEST_F(NetworkManagerTest, isIPv6MacBased_false_for_invalid_inputs) +{ + EXPECT_FALSE(Plugin::isIPv6MacBased("", "AA:BB:CC:DD:EE:FF")); + EXPECT_FALSE(Plugin::isIPv6MacBased("2001:db8::1", "")); + EXPECT_FALSE(Plugin::isIPv6MacBased("not_ipv6", "AA:BB:CC:DD:EE:FF")); + EXPECT_FALSE(Plugin::isIPv6MacBased("2001:db8::1", "not_a_mac")); + EXPECT_FALSE(Plugin::isIPv6MacBased("192.168.1.1", "AA:BB:CC:DD:EE:FF")); } TEST_F(NetworkManagerTest, SetInterfaceState_deviceFailed_wlan0) diff --git a/tests/l2Test/libnm/l2_test_libnmproxyEvent.cpp b/tests/l2Test/libnm/l2_test_libnmproxyEvent.cpp index c587688a..dfecf5fd 100644 --- a/tests/l2Test/libnm/l2_test_libnmproxyEvent.cpp +++ b/tests/l2Test/libnm/l2_test_libnmproxyEvent.cpp @@ -42,6 +42,8 @@ using namespace WPEFramework; using ::testing::NiceMock; +namespace WPEFramework { namespace Plugin { extern NetworkManagerImplementation* _instance; } } + class NetworkManagerEventTest : public ::testing::Test { protected: Core::ProxyType plugin; @@ -208,31 +210,6 @@ TEST_F(NetworkManagerEventTest, onInterfaceStateChangeCb) WPEFramework::Plugin::GnomeNetworkManagerEvents::onInterfaceStateChangeCb(Exchange::INetworkManager::INTERFACE_DISABLED, "eth0"); } -TEST_F(NetworkManagerEventTest, onAddressChangeCb) -{ - // Test acquiring IPv4 address - WPEFramework::Plugin::GnomeNetworkManagerEvents::onAddressChangeCb("eth0", "192.168.1.100", true, false); - - // Test acquiring IPv6 address - WPEFramework::Plugin::GnomeNetworkManagerEvents::onAddressChangeCb("eth0", "2001:db8::1", true, true); - - // Test acquiring same IPv6 address again (should skip posting) - WPEFramework::Plugin::GnomeNetworkManagerEvents::onAddressChangeCb("eth0", "2001:db8::1", true, true); - - // Test acquiring different IPv6 address - WPEFramework::Plugin::GnomeNetworkManagerEvents::onAddressChangeCb("eth0", "2001:db8::2", true, true); - - // Test losing IPv4 address - WPEFramework::Plugin::GnomeNetworkManagerEvents::onAddressChangeCb("eth0", "", false, false); - - // Test losing IPv6 address - WPEFramework::Plugin::GnomeNetworkManagerEvents::onAddressChangeCb("eth0", "", false, true); - - // Test losing IP on interface with empty cache (should skip posting) - WPEFramework::Plugin::GnomeNetworkManagerEvents::onAddressChangeCb("eth1", "", false, false); - WPEFramework::Plugin::GnomeNetworkManagerEvents::onAddressChangeCb("eth1", "", false, true); -} - TEST_F(NetworkManagerEventTest, onAvailableSSIDsCb) { GPtrArray* fakeDevices = g_ptr_array_new(); @@ -372,9 +349,9 @@ TEST_F(NetworkManagerEventTest, deviceStateChangeCb_disconnected) { NMDevice *wifiDummyDevice = static_cast(g_object_new(NM_TYPE_DEVICE_WIFI, NULL)); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_DISCONNECTED)); + .WillRepeatedly(::testing::Return(NM_DEVICE_STATE_DISCONNECTED)); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_iface(::testing::_)) - .WillOnce(::testing::Return("wlan0")); + .WillRepeatedly(::testing::Return("wlan0")); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state_reason(::testing::_)) .WillOnce(::testing::Return(NM_DEVICE_STATE_REASON_NONE)); WPEFramework::Plugin::GnomeNetworkManagerEvents::deviceStateChangeCb(reinterpret_cast(wifiDummyDevice), nullptr, nullptr); @@ -384,9 +361,9 @@ TEST_F(NetworkManagerEventTest, deviceStateChangeCb_unmanaged) { NMDevice *wifiDummyDevice = static_cast(g_object_new(NM_TYPE_DEVICE_WIFI, NULL)); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_UNMANAGED)); + .WillRepeatedly(::testing::Return(NM_DEVICE_STATE_UNMANAGED)); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_iface(::testing::_)) - .WillOnce(::testing::Return("wlan0")); + .WillRepeatedly(::testing::Return("wlan0")); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state_reason(::testing::_)) .WillOnce(::testing::Return(NM_DEVICE_STATE_REASON_NONE)); WPEFramework::Plugin::GnomeNetworkManagerEvents::deviceStateChangeCb(reinterpret_cast(wifiDummyDevice), nullptr, nullptr); @@ -492,9 +469,9 @@ TEST_F(NetworkManagerEventTest, deviceStateChangeCb_unknown) { NMDevice *wifiDummyDevice = static_cast(g_object_new(NM_TYPE_DEVICE_WIFI, NULL)); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_UNKNOWN)); + .WillRepeatedly(::testing::Return(NM_DEVICE_STATE_UNKNOWN)); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_iface(::testing::_)) - .WillOnce(::testing::Return("wlan0")); + .WillRepeatedly(::testing::Return("wlan0")); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state_reason(::testing::_)) .WillOnce(::testing::Return(NM_DEVICE_STATE_REASON_NONE)); WPEFramework::Plugin::GnomeNetworkManagerEvents::deviceStateChangeCb(reinterpret_cast(wifiDummyDevice), nullptr, nullptr); @@ -504,9 +481,9 @@ TEST_F(NetworkManagerEventTest, deviceStateChangeCb_eth0_unmanaged) { NMDevice *DummyDevice = static_cast(g_object_new(NM_TYPE_DEVICE_ETHERNET, NULL)); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_UNMANAGED)); + .WillRepeatedly(::testing::Return(NM_DEVICE_STATE_UNMANAGED)); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_iface(::testing::_)) - .WillOnce(::testing::Return("eth0")); + .WillRepeatedly(::testing::Return("eth0")); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state_reason(::testing::_)) .WillOnce(::testing::Return(NM_DEVICE_STATE_REASON_NONE)); WPEFramework::Plugin::GnomeNetworkManagerEvents::deviceStateChangeCb(reinterpret_cast(DummyDevice), nullptr, nullptr); @@ -517,9 +494,9 @@ TEST_F(NetworkManagerEventTest, deviceStateChangeCb_eth0_disconnected) { NMDevice *DummyDevice = static_cast(g_object_new(NM_TYPE_DEVICE_ETHERNET, NULL)); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) - .WillOnce(::testing::Return(NM_DEVICE_STATE_DISCONNECTED)); + .WillRepeatedly(::testing::Return(NM_DEVICE_STATE_DISCONNECTED)); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_iface(::testing::_)) - .WillOnce(::testing::Return("eth0")); + .WillRepeatedly(::testing::Return("eth0")); EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state_reason(::testing::_)) .WillOnce(::testing::Return(NM_DEVICE_STATE_REASON_NONE)); WPEFramework::Plugin::GnomeNetworkManagerEvents::deviceStateChangeCb(reinterpret_cast(DummyDevice), nullptr, nullptr); @@ -564,3 +541,102 @@ TEST_F(NetworkManagerEventTest, deviceStateChangeCb_eth0_activated) WPEFramework::Plugin::GnomeNetworkManagerEvents::deviceStateChangeCb(DummyDevice, nullptr, nullptr); g_object_unref(DummyDevice); } + +/* ──────────────────────────────────────────────────────────────────────────── + * Cache-clearing tests — verify that disconnect events clear the IP cache. + * These test observable behavior (cache state after disconnect) rather than + * internal NM API call sequences, so they remain stable across refactors. + * ──────────────────────────────────────────────────────────────────────────── */ + +TEST_F(NetworkManagerEventTest, disconnect_clears_ipv4_cache_eth0) +{ + /* Pre-populate the IPv4 cache for eth0 */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.globalAddresses["192.168.1.50"] = Plugin::GlobalAddressInfo(24, Plugin::ADDR_GLOBAL); + cache.gateway = "192.168.1.1"; + Plugin::_instance->swapIpCache("eth0", "IPv4", cache); + + /* Verify cache is populated */ + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\",\"ipversion\":\"IPv4\"}"), response)); + EXPECT_TRUE(response.find("\"ipaddress\":\"192.168.1.50\"") != std::string::npos); + + /* Trigger eth0 disconnect — refreshIpFamilyCache runs with skipRead=true, + swapping an empty cache and clearing the old addresses. */ + NMDevice *DummyDevice = static_cast(g_object_new(NM_TYPE_DEVICE_ETHERNET, NULL)); + EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) + .WillRepeatedly(::testing::Return(NM_DEVICE_STATE_DISCONNECTED)); + EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_iface(::testing::_)) + .WillRepeatedly(::testing::Return("eth0")); + EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state_reason(::testing::_)) + .WillOnce(::testing::Return(NM_DEVICE_STATE_REASON_NONE)); + WPEFramework::Plugin::GnomeNetworkManagerEvents::deviceStateChangeCb(DummyDevice, nullptr, nullptr); + g_object_unref(DummyDevice); + + /* Cache should now be empty — GetIPSettings returns success but no address fields */ + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\",\"ipversion\":\"IPv4\"}"), response)); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"ipaddress\"") == std::string::npos); +} + +TEST_F(NetworkManagerEventTest, disconnect_clears_ipv6_cache_wlan0) +{ + /* Pre-populate the IPv6 cache for wlan0 */ + Plugin::IpFamilyCache cache; + cache.valid = true; + cache.globalAddresses["2001:db8::1"] = Plugin::GlobalAddressInfo(64, Plugin::ADDR_GLOBAL); + cache.gateway = "fe80::1"; + Plugin::_instance->swapIpCache("wlan0", "IPv6", cache); + + /* Verify cache is populated */ + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"wlan0\",\"ipversion\":\"IPv6\"}"), response)); + EXPECT_TRUE(response.find("\"ipaddress\":\"2001:db8::1\"") != std::string::npos); + + /* Trigger wlan0 disconnect */ + NMDevice *DummyDevice = static_cast(g_object_new(NM_TYPE_DEVICE_WIFI, NULL)); + EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) + .WillRepeatedly(::testing::Return(NM_DEVICE_STATE_DISCONNECTED)); + EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_iface(::testing::_)) + .WillRepeatedly(::testing::Return("wlan0")); + EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state_reason(::testing::_)) + .WillOnce(::testing::Return(NM_DEVICE_STATE_REASON_NONE)); + WPEFramework::Plugin::GnomeNetworkManagerEvents::deviceStateChangeCb(reinterpret_cast(DummyDevice), nullptr, nullptr); + g_object_unref(DummyDevice); + + /* Cache should now be empty */ + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"wlan0\",\"ipversion\":\"IPv6\"}"), response)); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); + EXPECT_TRUE(response.find("\"ipaddress\"") == std::string::npos); +} + +TEST_F(NetworkManagerEventTest, disconnect_clears_both_ip_family_caches) +{ + /* Pre-populate both IPv4 and IPv6 caches for eth0 */ + Plugin::IpFamilyCache cache4; + cache4.valid = true; + cache4.globalAddresses["192.168.1.50"] = Plugin::GlobalAddressInfo(24, Plugin::ADDR_GLOBAL); + Plugin::_instance->swapIpCache("eth0", "IPv4", cache4); + + Plugin::IpFamilyCache cache6; + cache6.valid = true; + cache6.globalAddresses["2001:db8::99"] = Plugin::GlobalAddressInfo(64, Plugin::ADDR_GLOBAL); + Plugin::_instance->swapIpCache("eth0", "IPv6", cache6); + + /* Trigger disconnect — refreshIpFamilyCache is called for BOTH families */ + NMDevice *DummyDevice = static_cast(g_object_new(NM_TYPE_DEVICE_ETHERNET, NULL)); + EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) + .WillRepeatedly(::testing::Return(NM_DEVICE_STATE_DISCONNECTED)); + EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_iface(::testing::_)) + .WillRepeatedly(::testing::Return("eth0")); + EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state_reason(::testing::_)) + .WillOnce(::testing::Return(NM_DEVICE_STATE_REASON_NONE)); + WPEFramework::Plugin::GnomeNetworkManagerEvents::deviceStateChangeCb(DummyDevice, nullptr, nullptr); + g_object_unref(DummyDevice); + + /* Both families should be cleared */ + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\",\"ipversion\":\"IPv4\"}"), response)); + EXPECT_TRUE(response.find("\"ipaddress\"") == std::string::npos); + + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"eth0\",\"ipversion\":\"IPv6\"}"), response)); + EXPECT_TRUE(response.find("\"ipaddress\"") == std::string::npos); +} diff --git a/tests/l2Test/libnm/l2_test_libnmproxyInit.cpp b/tests/l2Test/libnm/l2_test_libnmproxyInit.cpp index e47f24fb..b550b3d6 100644 --- a/tests/l2Test/libnm/l2_test_libnmproxyInit.cpp +++ b/tests/l2Test/libnm/l2_test_libnmproxyInit.cpp @@ -158,7 +158,7 @@ TEST_F(NetworkManagerInitTest, platformInit) EXPECT_EQ(response, _T("{\"success\":false}")); EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("GetIPSettings"), _T("{\"interface\":\"wlan0\"}"), response)); - EXPECT_EQ(response, _T("{\"success\":false}")); + EXPECT_TRUE(response.find("\"success\":true") != std::string::npos); EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("SetHostname"), _T("{\"hostname\":\"test-host\"}"), response)); EXPECT_EQ(response, _T("{\"success\":false}")); diff --git a/tests/l2Test/libnm/l2_test_libnmproxyWifi.cpp b/tests/l2Test/libnm/l2_test_libnmproxyWifi.cpp index d32f4615..c0086a00 100644 --- a/tests/l2Test/libnm/l2_test_libnmproxyWifi.cpp +++ b/tests/l2Test/libnm/l2_test_libnmproxyWifi.cpp @@ -768,7 +768,7 @@ TEST_F(NetworkManagerWifiTest, StartWiFiScan_with_Frequency) EXPECT_CALL(*p_libnmWrapsImplMock, nm_device_get_state(::testing::_)) .WillOnce(::testing::Return(NM_DEVICE_STATE_UNMANAGED)); - EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("StartWiFiScan"), _T("{\"frequency\":\"5\", \"ssids\":[\"Testssid_1\", \"Testssid_2\"]}"), response)); + EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("StartWiFiScan"), _T("{\"frequencies\":[\"2.4\",\"5\"], \"ssids\":[\"Testssid_1\", \"Testssid_2\"]}"), response)); EXPECT_EQ(response, _T("{\"success\":false}")); g_object_unref(deviceDummy); diff --git a/tests/l2Test/rdk/CMakeLists.txt b/tests/l2Test/rdk/CMakeLists.txt index ed5d4fab..3a94c63d 100644 --- a/tests/l2Test/rdk/CMakeLists.txt +++ b/tests/l2Test/rdk/CMakeLists.txt @@ -38,6 +38,7 @@ add_executable(${NM_RDK_PROXY_L2_TEST} ${CMAKE_SOURCE_DIR}/plugin/NetworkManagerImplementation.cpp ${CMAKE_SOURCE_DIR}/plugin/NetworkManagerConnectivity.cpp ${CMAKE_SOURCE_DIR}/plugin/NetworkManagerStunClient.cpp + ${CMAKE_SOURCE_DIR}/plugin/NetworkManagerPowerClient.cpp ${CMAKE_SOURCE_DIR}/plugin/rdk/NetworkManagerRDKProxy.cpp ${PROXY_STUB_SOURCES} ) diff --git a/tests/l2Test/rdk/l2_test_rdkproxy.cpp b/tests/l2Test/rdk/l2_test_rdkproxy.cpp index 5bcdc3d5..0d13cace 100644 --- a/tests/l2Test/rdk/l2_test_rdkproxy.cpp +++ b/tests/l2Test/rdk/l2_test_rdkproxy.cpp @@ -661,7 +661,7 @@ TEST_F(NetworkManagerTest, StartWiFiScan_Success) )); EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("StartWiFiScan"), - _T("{\"frequency\":\"2.4GHz\"}"), response)); + _T("{\"frequency\":[\"2.4\"]}"), response)); EXPECT_EQ(response, _T("{\"success\":true}")); } @@ -673,7 +673,7 @@ TEST_F(NetworkManagerTest, StartWiFiScan_Failed) .WillOnce(::testing::Return(IARM_RESULT_IPCCORE_FAIL)); EXPECT_EQ(Core::ERROR_NONE, handler.Invoke(connection, _T("StartWiFiScan"), - _T("{\"frequency\":\"2.4GHz\"}"), response)); + _T("{\"frequency\":[\"2.4\"]}"), response)); EXPECT_EQ(response, _T("{\"success\":false}")); } diff --git a/tests/mocks/INetworkManagerMock.h b/tests/mocks/INetworkManagerMock.h index 27a8b287..d9a21719 100644 --- a/tests/mocks/INetworkManagerMock.h +++ b/tests/mocks/INetworkManagerMock.h @@ -36,7 +36,7 @@ class MockINetworkManager : public WPEFramework::Exchange::INetworkManager { MOCK_METHOD(uint32_t, GetPublicIP, (string& interface, string& ipversion, string& ipaddress), (override)); MOCK_METHOD(uint32_t, Ping, (const string ipversion, const string endpoint, const uint32_t count, const uint16_t timeout, const string guid, string& response), (override)); MOCK_METHOD(uint32_t, Trace, (const string ipversion, const string endpoint, const uint32_t nqueries, const string guid, string& response), (override)); - MOCK_METHOD(uint32_t, StartWiFiScan, (const string& frequency, IStringIterator* const ssids), (override)); + MOCK_METHOD(uint32_t, StartWiFiScan, (IStringIterator* const frequencies, IStringIterator* const ssids), (override)); MOCK_METHOD(uint32_t, StopWiFiScan, (), (override)); MOCK_METHOD(uint32_t, GetKnownSSIDs, (IStringIterator*& ssids), (override)); MOCK_METHOD(uint32_t, AddToKnownSSIDs, (const WiFiConnectTo& ssid), (override)); diff --git a/tests/mocks/thunder/IPowerManager.h b/tests/mocks/thunder/IPowerManager.h new file mode 100644 index 00000000..30c1d4d7 --- /dev/null +++ b/tests/mocks/thunder/IPowerManager.h @@ -0,0 +1,85 @@ +/** +* If not stated otherwise in this file or this component's LICENSE +* file the following copyright and licenses apply: +* +* Copyright 2026 RDK Management +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +**/ + +#pragma once + +/** + * Minimal CI stub for IPowerManager — compatible with Thunder R4.4.3. + * + * This file is used only during CI builds (gdbus/libnm L1 proxy tests) where + * the full entservices-apis stack is not available. It defines exactly the + * surface consumed by NetworkManagerPowerClient and nothing more. + * + * DO NOT use this file outside of test/CI contexts. + */ + +#include + +namespace WPEFramework { +namespace Exchange { + + struct EXTERNAL IPowerManager : virtual public Core::IUnknown { + + // Stub ID — not used for COM lookup in L1 unit tests. + enum { ID = 0x8180 }; + + enum PowerState : uint8_t { + POWER_STATE_UNKNOWN = 0, + POWER_STATE_OFF = 1, + POWER_STATE_STANDBY = 2, + POWER_STATE_ON = 3, + POWER_STATE_STANDBY_LIGHT_SLEEP = 4, + POWER_STATE_STANDBY_DEEP_SLEEP = 5, + }; + + // @event + struct EXTERNAL IModePreChangeNotification : virtual public Core::IUnknown { + enum { ID = 0x8182 }; + virtual void OnPowerModePreChange(const PowerState currentState, + const PowerState newState, + const int transactionId, + const int stateChangeAfter) {} + }; + + // @event + struct EXTERNAL IModeChangedNotification : virtual public Core::IUnknown { + enum { ID = 0x8183 }; + virtual void OnPowerModeChanged(const PowerState currentState, + const PowerState newState) {} + }; + + virtual Core::hresult Register(IModePreChangeNotification* notification) {}; + virtual Core::hresult Unregister(const IModePreChangeNotification* notification) {}; + + virtual Core::hresult Register(IModeChangedNotification* notification) {}; + virtual Core::hresult Unregister(const IModeChangedNotification* notification) {}; + + virtual Core::hresult AddPowerModePreChangeClient(const string& clientName, + uint32_t& clientId) {}; + virtual Core::hresult RemovePowerModePreChangeClient(const uint32_t clientId) {}; + virtual Core::hresult PowerModePreChangeComplete(const uint32_t clientId, + const int transactionId) {}; + virtual Core::hresult DelayPowerModeChangeBy(const uint32_t clientId, + const int transactionId, + const int delayPeriod) {}; + virtual Core::hresult GetNetworkStandbyMode(bool& standbyMode) {}; + }; + +} // namespace Exchange +} // namespace WPEFramework diff --git a/tools/plugincli/NetworkManagerLibnmTest.cpp b/tools/plugincli/NetworkManagerLibnmTest.cpp index 650a1ba1..3a358b56 100644 --- a/tools/plugincli/NetworkManagerLibnmTest.cpp +++ b/tools/plugincli/NetworkManagerLibnmTest.cpp @@ -220,7 +220,8 @@ int main() NMLOG_INFO("Sending WiFi scan request%s", ssid.empty() ? " (all SSIDs)" : (" for SSID: " + ssid).c_str()); - if (wifiMgr->wifiScanRequest(ssid)) { + bool scanRequestSent = ssid.empty() ? wifiMgr->wifiScanRequest() : wifiMgr->wifiScanRequest({ssid}); + if (scanRequestSent) { NMLOG_INFO("WiFi scan request sent successfully."); } else { NMLOG_ERROR("Failed to send WiFi scan request.");