Files
radiantwave/src/network/nm_device.cpp
T
NeilClack 1c14c0401e Fix: Main Menu footer shows 'WiFi Not Connected'
Updated WiFiDevice::getActiveAccessPoint() to read the AP's SSID so connectedSSID() can report which network is in use.
2026-08-13 16:18:51 -05:00

548 lines
16 KiB
C++

#include "nm_device.h"
#include "nm_connection.h"
#include "logger/logger.h"
#include <cstring>
#include <iostream>
#include <stdexcept>
// Only include sdbus on Linux
#ifdef __linux__
#include <systemd/sd-bus.h>
#endif
namespace radiantwave::network {
// ─── Access Point Helper ───────────────────────────────────────────────
// Build an AccessPointInfo from raw D-Bus AccessPoint property values (see
// nm_device.h). The `ssid` argument carries the raw SSID bytes decoded from
// the AccessPoint.Ssid `ay` property as a std::string.
AccessPointInfo parseAccessPoint(
const std::string& object_path,
const std::string& ssid,
uint8_t signal,
uint32_t flags,
uint32_t wpa_flags,
uint32_t rsn_flags
) {
AccessPointInfo ap;
ap.object_path = object_path;
ap.ssid = ssid;
ap.ssid_bytes = ssidToBytes(ssid);
ap.signal_strength = signal;
ap.flags = flags;
ap.wpa_flags = wpa_flags;
ap.rsn_flags = rsn_flags;
ap.security = detectSecurityType(flags, wpa_flags, rsn_flags);
return ap;
}
// ─── WifiDevice Implementation ─────────────────────────────────────────
#ifdef __linux__
struct WifiDevice::Impl {
std::string object_path;
sd_bus* bus = nullptr;
explicit Impl(const std::string& path) : object_path(path) {
int r = sd_bus_open_system(&bus);
if (r < 0) {
throw std::runtime_error("Failed to open system bus: " + std::string(strerror(-r)));
}
}
~Impl() {
if (bus) {
sd_bus_unref(bus);
}
}
// Helper: call a D-Bus method and get response
sd_bus_message* callMethod(
const char* dest,
const char* path,
const char* iface,
const char* member,
const char* type,
...
) {
sd_bus_message* msg = nullptr;
sd_bus_error error = SD_BUS_ERROR_NULL;
int r = sd_bus_call_method(
bus,
dest,
path,
iface,
member,
&error,
&msg,
type
);
if (r < 0) {
radiantwave::logger::Logger::instance().warning("WiFi",
std::string("D-Bus call failed: ") +
(error.message ? error.message : strerror(-r)));
sd_bus_error_free(&error);
return nullptr;
}
sd_bus_error_free(&error);
return msg;
}
// Helper: get a property value
sd_bus_message* getProperty(
const char* dest,
const char* path,
const char* iface,
const char* prop,
const char* type
) {
sd_bus_message* msg = nullptr;
sd_bus_error error = SD_BUS_ERROR_NULL;
int r = sd_bus_get_property(
bus,
dest,
path,
iface,
prop,
&error,
&msg,
type
);
if (r < 0) {
// Transient D-Bus errors (e.g. a reply raced with a state change
// while NetworkManager churns the device during a failed auth)
// must never throw out of here — callers treat a null reply as
// "unknown", which keeps the UI and wait loops running.
radiantwave::logger::Logger::instance().warning("WiFi",
"D-Bus property get failed on " + std::string(path) + "." +
std::string(prop) + ": " +
(error.message ? error.message : strerror(-r)));
sd_bus_error_free(&error);
return nullptr;
}
sd_bus_error_free(&error);
return msg;
}
};
WifiDevice::WifiDevice(const std::string& object_path)
: impl_(std::make_unique<Impl>(object_path))
{
}
WifiDevice::~WifiDevice() = default;
WifiDevice::WifiDevice(WifiDevice&& other) noexcept = default;
WifiDevice& WifiDevice::operator=(WifiDevice&& other) noexcept = default;
std::string WifiDevice::getInterfaceName() const {
sd_bus_message* msg = impl_->getProperty(
"org.freedesktop.NetworkManager",
impl_->object_path.c_str(),
"org.freedesktop.NetworkManager.Device",
"Interface",
"s"
);
if (!msg) {
return "";
}
const char* iface = nullptr;
sd_bus_message_read(msg, "s", &iface);
std::string result = iface ? iface : "";
sd_bus_message_unref(msg);
return result;
}
uint32_t WifiDevice::getDeviceState() const {
sd_bus_message* msg = impl_->getProperty(
"org.freedesktop.NetworkManager",
impl_->object_path.c_str(),
"org.freedesktop.NetworkManager.Device",
"State",
"u"
);
if (!msg) {
// Unknown state keeps callers polling/falling back gracefully instead
// of aborting on a transient read failure.
return nm_device_state::Unknown;
}
uint32_t state = 0;
sd_bus_message_read(msg, "u", &state);
sd_bus_message_unref(msg);
return state;
}
std::string WifiDevice::getObjectPath() const {
return impl_->object_path;
}
bool WifiDevice::requestScan() {
sd_bus_error error = SD_BUS_ERROR_NULL;
int r = sd_bus_call_method(
impl_->bus,
"org.freedesktop.NetworkManager",
impl_->object_path.c_str(),
"org.freedesktop.NetworkManager.Device.Wireless",
"RequestScan",
&error,
nullptr,
"a{sv}",
0 // Empty options dict
);
if (r < 0) {
radiantwave::logger::Logger::instance().warning("WiFi",
"RequestScan failed on device " + impl_->object_path + ": " +
(error.message ? error.message : strerror(-r)));
}
sd_bus_error_free(&error);
return r >= 0;
}
std::vector<AccessPointInfo> WifiDevice::getAccessPoints() const {
std::vector<AccessPointInfo> result;
// Get access point paths
sd_bus_message* msg = nullptr;
sd_bus_error error = SD_BUS_ERROR_NULL;
int r = sd_bus_get_property(
impl_->bus,
"org.freedesktop.NetworkManager",
impl_->object_path.c_str(),
"org.freedesktop.NetworkManager.Device.Wireless",
"AccessPoints",
&error,
&msg,
"ao"
);
if (r < 0) {
radiantwave::logger::Logger::instance().warning("WiFi",
"Failed to read AccessPoints on device " + impl_->object_path + ": " +
(error.message ? error.message : strerror(-r)));
sd_bus_error_free(&error);
return result;
}
// Read array of object paths
r = sd_bus_message_enter_container(msg, 'a', "o");
if (r < 0) {
sd_bus_message_unref(msg);
return result;
}
// Read a single numeric AccessPoint property. Strength is a byte ("y"),
// the security flag properties are uint32 ("u"). Leaves `out` untouched on
// failure, so callers can rely on zero-initialized defaults.
auto readApProperty = [this](const char* ap_path, const char* prop,
const char* type, void* out) {
sd_bus_message* prop_msg = nullptr;
sd_bus_error prop_error = SD_BUS_ERROR_NULL;
int pr = sd_bus_get_property(
impl_->bus,
"org.freedesktop.NetworkManager",
ap_path,
"org.freedesktop.NetworkManager.AccessPoint",
prop,
&prop_error,
&prop_msg,
type
);
if (pr >= 0 && prop_msg) {
if (strcmp(type, "u") == 0) {
sd_bus_message_read(prop_msg, "u", static_cast<uint32_t*>(out));
} else {
sd_bus_message_read(prop_msg, "y", static_cast<uint8_t*>(out));
}
sd_bus_message_unref(prop_msg);
}
sd_bus_error_free(&prop_error);
return pr >= 0;
};
const char* ap_path = nullptr;
while ((r = sd_bus_message_read(msg, "o", &ap_path)) > 0 && ap_path) {
sd_bus_message* ap_msg = nullptr;
sd_bus_error ap_error = SD_BUS_ERROR_NULL;
r = sd_bus_get_property(
impl_->bus,
"org.freedesktop.NetworkManager",
ap_path,
"org.freedesktop.NetworkManager.AccessPoint",
"Ssid",
&ap_error,
&ap_msg,
"ay"
);
if (r >= 0) {
// Read the SSID byte array. sd_bus_get_property() already leaves
// the message positioned at the array (inside the variant wrapper),
// and sd_bus_message_read_array() enters AND exits the array
// container itself. Do NOT call sd_bus_message_enter_container()
// here first, or the read fails and every network silently drops
// out of the scan list (it only ever shows up as an empty page with
// no error logs).
const uint8_t* ssid_data = nullptr;
size_t ssid_len = 0;
r = sd_bus_message_read_array(ap_msg, 'y', (const void**)&ssid_data, &ssid_len);
std::string ssid;
if (r >= 0 && ssid_data && ssid_len > 0) {
ssid = std::string(reinterpret_cast<const char*>(ssid_data), ssid_len);
}
sd_bus_message_unref(ap_msg);
// Remaining properties: signal strength plus the three flags that
// drive detectSecurityType() (otherwise every network is reported
// as Open and secured connections are built without a security
// section).
uint8_t signal = 0;
uint32_t flags = 0, wpa_flags = 0, rsn_flags = 0;
readApProperty(ap_path, "Strength", "y", &signal);
readApProperty(ap_path, "Flags", "u", &flags);
readApProperty(ap_path, "WpaFlags", "u", &wpa_flags);
readApProperty(ap_path, "RsnFlags", "u", &rsn_flags);
if (!ssid.empty()) {
result.push_back(parseAccessPoint(ap_path, ssid, signal, flags, wpa_flags, rsn_flags));
}
}
sd_bus_error_free(&ap_error);
}
sd_bus_message_exit_container(msg);
sd_bus_message_unref(msg);
return result;
}
AccessPointInfo WifiDevice::getActiveAccessPoint() const {
sd_bus_message* msg = impl_->getProperty(
"org.freedesktop.NetworkManager",
impl_->object_path.c_str(),
"org.freedesktop.NetworkManager.Device.Wireless",
"ActiveAccessPoint",
"o"
);
if (!msg) {
return AccessPointInfo{}; // No active AP
}
const char* ap_path = nullptr;
sd_bus_message_read(msg, "o", &ap_path);
sd_bus_message_unref(msg);
if (!ap_path || std::string(ap_path) == "/") {
return AccessPointInfo{}; // No active AP
}
AccessPointInfo ap;
ap.object_path = ap_path;
// Read the active AP's SSID bytes so connectedSSID() (and the Home footer)
// can report which network is in use.
sd_bus_message* ssid_msg = nullptr;
sd_bus_error ssid_error = SD_BUS_ERROR_NULL;
int r = sd_bus_get_property(
impl_->bus,
"org.freedesktop.NetworkManager",
ap_path,
"org.freedesktop.NetworkManager.AccessPoint",
"Ssid",
&ssid_error,
&ssid_msg,
"ay"
);
if (r >= 0 && ssid_msg) {
const uint8_t* ssid_data = nullptr;
size_t ssid_len = 0;
r = sd_bus_message_read_array(ssid_msg, 'y', (const void**)&ssid_data, &ssid_len);
if (r >= 0 && ssid_data && ssid_len > 0) {
ap.ssid = std::string(reinterpret_cast<const char*>(ssid_data), ssid_len);
}
sd_bus_message_unref(ssid_msg);
}
sd_bus_error_free(&ssid_error);
return ap;
}
bool WifiDevice::isConnected() const {
return getDeviceState() == nm_device_state::Activated;
}
std::string WifiDevice::connectedSSID() const {
if (!isConnected()) {
return "";
}
auto ap = getActiveAccessPoint();
return ap.ssid;
}
std::unique_ptr<WifiDevice> WifiDevice::findFirstDevice() {
sd_bus* bus = nullptr;
int r = sd_bus_open_system(&bus);
if (r < 0) {
return nullptr;
}
sd_bus_message* msg = nullptr;
sd_bus_error error = SD_BUS_ERROR_NULL;
r = sd_bus_call_method(
bus,
"org.freedesktop.NetworkManager",
"/org/freedesktop/NetworkManager",
"org.freedesktop.NetworkManager",
"GetDevices",
&error,
&msg,
""
);
if (r < 0) {
sd_bus_error_free(&error);
sd_bus_unref(bus);
return nullptr;
}
// Read device paths
r = sd_bus_message_enter_container(msg, 'a', "o");
if (r < 0) {
sd_bus_message_unref(msg);
sd_bus_unref(bus);
return nullptr;
}
const char* dev_path = nullptr;
std::string wifi_path;
while ((r = sd_bus_message_read(msg, "o", &dev_path)) > 0 && dev_path) {
// Check device type
sd_bus_message* type_msg = nullptr;
sd_bus_error type_error = SD_BUS_ERROR_NULL;
uint32_t dev_type = 0;
r = sd_bus_get_property(
bus,
"org.freedesktop.NetworkManager",
dev_path,
"org.freedesktop.NetworkManager.Device",
"DeviceType",
&type_error,
&type_msg,
"u"
);
if (r >= 0) {
sd_bus_message_read(type_msg, "u", &dev_type);
sd_bus_message_unref(type_msg);
// NM_DEVICE_TYPE_WIFI = 2
if (dev_type == 2) {
wifi_path = dev_path;
break;
}
}
sd_bus_error_free(&type_error);
}
sd_bus_message_exit_container(msg);
sd_bus_message_unref(msg);
sd_bus_unref(bus);
if (wifi_path.empty()) {
return nullptr;
}
return std::make_unique<WifiDevice>(wifi_path);
}
#else // !__linux__
// ─── Stub implementation for non-Linux platforms ───────────────────────
struct WifiDevice::Impl {
std::string object_path;
explicit Impl(const std::string& path) : object_path(path) {}
};
WifiDevice::WifiDevice(const std::string& object_path)
: impl_(std::make_unique<Impl>(object_path))
{
}
WifiDevice::~WifiDevice() = default;
WifiDevice::WifiDevice(WifiDevice&& other) noexcept = default;
WifiDevice& WifiDevice::operator=(WifiDevice&& other) noexcept = default;
std::string WifiDevice::getInterfaceName() const {
return "wlan0"; // Stub
}
uint32_t WifiDevice::getDeviceState() const {
return nm_device_state::Disconnected; // Stub
}
std::string WifiDevice::getObjectPath() const {
return impl_->object_path;
}
bool WifiDevice::requestScan() {
std::cerr << "[WifiDevice] Stub: requestScan() called on non-Linux platform\n";
return false;
}
std::vector<AccessPointInfo> WifiDevice::getAccessPoints() const {
std::cerr << "[WifiDevice] Stub: getAccessPoints() called on non-Linux platform\n";
return {};
}
AccessPointInfo WifiDevice::getActiveAccessPoint() const {
std::cerr << "[WifiDevice] Stub: getActiveAccessPoint() called on non-Linux platform\n";
return {};
}
bool WifiDevice::isConnected() const {
return false;
}
std::string WifiDevice::connectedSSID() const {
return "";
}
std::unique_ptr<WifiDevice> WifiDevice::findFirstDevice() {
std::cerr << "[WifiDevice] Stub: findFirstDevice() called on non-Linux platform\n";
return nullptr;
}
#endif // __linux__
} // namespace radiantwave::network