diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ed8374..b02b3d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.20) project(quickshell VERSION "0.2.1" LANGUAGES CXX C) -set(UNRELEASED_FEATURES) +set(UNRELEASED_FEATURES "network.2") set(QT_MIN_VERSION "6.6.0") set(CMAKE_CXX_STANDARD 20) diff --git a/changelog/next.md b/changelog/next.md index fc6d79e..024ab10 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -20,7 +20,7 @@ set shell id. - Added the ability to handle move and resize events to FloatingWindow. - Pipewire service now reconnects if pipewire dies or a protocol error occurs. - Added pipewire audio peak detection. -- Added initial support for network management. +- Added network management support. - Added support for grabbing focus from popup windows. - Added support for IPC signal listeners. - Added Quickshell version checking and version gated preprocessing. diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index 6075040..03ef86a 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -4,6 +4,7 @@ qt_add_library(quickshell-network STATIC network.cpp device.cpp wifi.cpp + enums.cpp ) target_include_directories(quickshell-network PRIVATE diff --git a/src/network/device.cpp b/src/network/device.cpp index 22e3949..5679e8d 100644 --- a/src/network/device.cpp +++ b/src/network/device.cpp @@ -8,6 +8,7 @@ #include #include "../core/logcat.hpp" +#include "enums.hpp" namespace qs::network { @@ -15,49 +16,9 @@ namespace { QS_LOGGING_CATEGORY(logNetworkDevice, "quickshell.network.device", QtWarningMsg); } // namespace -QString DeviceConnectionState::toString(DeviceConnectionState::Enum state) { - switch (state) { - case Unknown: return QStringLiteral("Unknown"); - case Connecting: return QStringLiteral("Connecting"); - case Connected: return QStringLiteral("Connected"); - case Disconnecting: return QStringLiteral("Disconnecting"); - case Disconnected: return QStringLiteral("Disconnected"); - default: return QStringLiteral("Unknown"); - } -} - -QString DeviceType::toString(DeviceType::Enum type) { - switch (type) { - case None: return QStringLiteral("None"); - case Wifi: return QStringLiteral("Wifi"); - default: return QStringLiteral("Unknown"); - } -} - -QString NMDeviceState::toString(NMDeviceState::Enum state) { - switch (state) { - case Unknown: return QStringLiteral("Unknown"); - case Unmanaged: return QStringLiteral("Not managed by NetworkManager"); - case Unavailable: return QStringLiteral("Unavailable"); - case Disconnected: return QStringLiteral("Disconnected"); - case Prepare: return QStringLiteral("Preparing to connect"); - case Config: return QStringLiteral("Connecting to a network"); - case NeedAuth: return QStringLiteral("Waiting for authentication"); - case IPConfig: return QStringLiteral("Requesting IPv4 and/or IPv6 addresses from the network"); - case IPCheck: - return QStringLiteral("Checking if further action is required for the requested connection"); - case Secondaries: - return QStringLiteral("Waiting for a required secondary connection to activate"); - case Activated: return QStringLiteral("Connected"); - case Deactivating: return QStringLiteral("Disconnecting"); - case Failed: return QStringLiteral("Failed to connect"); - default: return QStringLiteral("Unknown"); - }; -} - NetworkDevice::NetworkDevice(DeviceType::Enum type, QObject* parent): QObject(parent), mType(type) { this->bindableConnected().setBinding([this]() { - return this->bState == DeviceConnectionState::Connected; + return this->bState == ConnectionState::Connected; }); }; @@ -66,12 +27,17 @@ void NetworkDevice::setAutoconnect(bool autoconnect) { emit this->requestSetAutoconnect(autoconnect); } +void NetworkDevice::setNmManaged(bool managed) { + if (this->bNmManaged == managed) return; + emit this->requestSetNmManaged(managed); +} + void NetworkDevice::disconnect() { - if (this->bState == DeviceConnectionState::Disconnected) { + if (this->bState == ConnectionState::Disconnected) { qCCritical(logNetworkDevice) << "Device" << this << "is already disconnected"; return; } - if (this->bState == DeviceConnectionState::Disconnecting) { + if (this->bState == ConnectionState::Disconnecting) { qCCritical(logNetworkDevice) << "Device" << this << "is already disconnecting"; return; } diff --git a/src/network/device.hpp b/src/network/device.hpp index f3807c2..8d914a1 100644 --- a/src/network/device.hpp +++ b/src/network/device.hpp @@ -6,76 +6,22 @@ #include #include +#include "../core/doc.hpp" +#include "enums.hpp" + namespace qs::network { -///! Connection state of a NetworkDevice. -class DeviceConnectionState: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - Unknown = 0, - Connecting = 1, - Connected = 2, - Disconnecting = 3, - Disconnected = 4, - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(DeviceConnectionState::Enum state); -}; - -///! Type of network device. -class DeviceType: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - None = 0, - Wifi = 1, - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(DeviceType::Enum type); -}; - -///! NetworkManager-specific device state. -/// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState. -class NMDeviceState: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - Unknown = 0, - Unmanaged = 10, - Unavailable = 20, - Disconnected = 30, - Prepare = 40, - Config = 50, - NeedAuth = 60, - IPConfig = 70, - IPCheck = 80, - Secondaries = 90, - Activated = 100, - Deactivating = 110, - Failed = 120, - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(NMDeviceState::Enum state); -}; - ///! A network device. -/// When @@type is `Wifi`, the device is a @@WifiDevice, which can be used to scan for and connect to access points. +/// The @@type property may be used to determine if this device is a @@WifiDevice. class NetworkDevice: public QObject { Q_OBJECT; QML_ELEMENT; QML_UNCREATABLE("Devices can only be acquired through Network"); // clang-format off /// The device type. + /// + /// When the device type is `Wifi`, the device object is a @@WifiDevice which exposes wifi network + /// connection and scanning. Q_PROPERTY(DeviceType::Enum type READ type CONSTANT); /// The name of the device's control interface. Q_PROPERTY(QString name READ name NOTIFY nameChanged BINDABLE bindableName); @@ -84,10 +30,12 @@ class NetworkDevice: public QObject { /// True if the device is connected. Q_PROPERTY(bool connected READ default NOTIFY connectedChanged BINDABLE bindableConnected); /// Connection state of the device. - Q_PROPERTY(qs::network::DeviceConnectionState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); - /// A more specific device state when the backend is NetworkManager. - Q_PROPERTY(qs::network::NMDeviceState::Enum nmState READ default NOTIFY nmStateChanged BINDABLE bindableNmState); - /// True if the device is allowed to autoconnect. + Q_PROPERTY(qs::network::ConnectionState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); + /// True if the device is managed by NetworkManager. + /// + /// > [!WARNING] Only valid for the NetworkManager backend. + Q_PROPERTY(bool nmManaged READ nmManaged WRITE setNmManaged NOTIFY nmManagedChanged) + /// True if the device is allowed to autoconnect to a network. Q_PROPERTY(bool autoconnect READ autoconnect WRITE setAutoconnect NOTIFY autoconnectChanged); // clang-format on @@ -97,25 +45,28 @@ public: /// Disconnects the device and prevents it from automatically activating further connections. Q_INVOKABLE void disconnect(); - [[nodiscard]] DeviceType::Enum type() const { return this->mType; }; - QBindable bindableName() { return &this->bName; }; - [[nodiscard]] QString name() const { return this->bName; }; - QBindable bindableAddress() { return &this->bAddress; }; - QBindable bindableConnected() { return &this->bConnected; }; - QBindable bindableState() { return &this->bState; }; - QBindable bindableNmState() { return &this->bNmState; }; - [[nodiscard]] bool autoconnect() const { return this->bAutoconnect; }; - QBindable bindableAutoconnect() { return &this->bAutoconnect; }; + [[nodiscard]] DeviceType::Enum type() const { return this->mType; } + QBindable bindableName() { return &this->bName; } + [[nodiscard]] QString name() const { return this->bName; } + QBindable bindableAddress() { return &this->bAddress; } + QBindable bindableConnected() { return &this->bConnected; } + QBindable bindableState() { return &this->bState; } + QBindable bindableNmManaged() { return &this->bNmManaged; } + [[nodiscard]] bool nmManaged() { return this->bNmManaged; } + void setNmManaged(bool managed); + QBindable bindableAutoconnect() { return &this->bAutoconnect; } + [[nodiscard]] bool autoconnect() { return this->bAutoconnect; } void setAutoconnect(bool autoconnect); signals: - void requestDisconnect(); - void requestSetAutoconnect(bool autoconnect); + QSDOC_HIDE void requestDisconnect(); + QSDOC_HIDE void requestSetAutoconnect(bool autoconnect); + QSDOC_HIDE void requestSetNmManaged(bool managed); void nameChanged(); void addressChanged(); void connectedChanged(); void stateChanged(); - void nmStateChanged(); + void nmManagedChanged(); void autoconnectChanged(); private: @@ -124,8 +75,8 @@ private: Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bName, &NetworkDevice::nameChanged); Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bAddress, &NetworkDevice::addressChanged); Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, bool, bConnected, &NetworkDevice::connectedChanged); - Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, DeviceConnectionState::Enum, bState, &NetworkDevice::stateChanged); - Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, NMDeviceState::Enum, bNmState, &NetworkDevice::nmStateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, ConnectionState::Enum, bState, &NetworkDevice::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, bool, bNmManaged, &NetworkDevice::nmManagedChanged); Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, bool, bAutoconnect, &NetworkDevice::autoconnectChanged); // clang-format on }; diff --git a/src/network/enums.cpp b/src/network/enums.cpp new file mode 100644 index 0000000..2cf36c1 --- /dev/null +++ b/src/network/enums.cpp @@ -0,0 +1,86 @@ +#include "enums.hpp" + +#include + +namespace qs::network { + +QString NetworkConnectivity::toString(NetworkConnectivity::Enum conn) { + switch (conn) { + case Unknown: return QStringLiteral("Unknown"); + case None: return QStringLiteral("Not connected to a network"); + case Portal: return QStringLiteral("Connection intercepted by a captive portal"); + case Limited: return QStringLiteral("Partial internet connectivity"); + case Full: return QStringLiteral("Full internet connectivity"); + default: return QStringLiteral("Unknown"); + } +} + +QString NetworkBackendType::toString(NetworkBackendType::Enum type) { + switch (type) { + case NetworkBackendType::None: return "None"; + case NetworkBackendType::NetworkManager: return "NetworkManager"; + default: return "Unknown"; + } +} + +QString ConnectionState::toString(ConnectionState::Enum state) { + switch (state) { + case Unknown: return QStringLiteral("Unknown"); + case Connecting: return QStringLiteral("Connecting"); + case Connected: return QStringLiteral("Connected"); + case Disconnecting: return QStringLiteral("Disconnecting"); + case Disconnected: return QStringLiteral("Disconnected"); + default: return QStringLiteral("Unknown"); + } +} + +QString ConnectionFailReason::toString(ConnectionFailReason::Enum reason) { + switch (reason) { + case Unknown: return QStringLiteral("Unknown"); + case NoSecrets: return QStringLiteral("Secrets were required but not provided"); + case WifiClientDisconnected: return QStringLiteral("Wi-Fi supplicant diconnected"); + case WifiClientFailed: return QStringLiteral("Wi-Fi supplicant failed"); + case WifiAuthTimeout: return QStringLiteral("Wi-Fi connection took too long to authenticate"); + case WifiNetworkLost: return QStringLiteral("Wi-Fi network could not be found"); + default: return QStringLiteral("Unknown"); + } +} + +QString DeviceType::toString(DeviceType::Enum type) { + switch (type) { + case None: return QStringLiteral("None"); + case Wifi: return QStringLiteral("Wifi"); + default: return QStringLiteral("Unknown"); + } +} + +QString WifiSecurityType::toString(WifiSecurityType::Enum type) { + switch (type) { + case Unknown: return QStringLiteral("Unknown"); + case Wpa3SuiteB192: return QStringLiteral("WPA3 Suite B 192-bit"); + case Sae: return QStringLiteral("WPA3"); + case Wpa2Eap: return QStringLiteral("WPA2 Enterprise"); + case Wpa2Psk: return QStringLiteral("WPA2"); + case WpaEap: return QStringLiteral("WPA Enterprise"); + case WpaPsk: return QStringLiteral("WPA"); + case StaticWep: return QStringLiteral("WEP"); + case DynamicWep: return QStringLiteral("Dynamic WEP"); + case Leap: return QStringLiteral("LEAP"); + case Owe: return QStringLiteral("OWE"); + case Open: return QStringLiteral("Open"); + default: return QStringLiteral("Unknown"); + } +} + +QString WifiDeviceMode::toString(WifiDeviceMode::Enum mode) { + switch (mode) { + case Unknown: return QStringLiteral("Unknown"); + case AdHoc: return QStringLiteral("Ad-Hoc"); + case Station: return QStringLiteral("Station"); + case AccessPoint: return QStringLiteral("Access Point"); + case Mesh: return QStringLiteral("Mesh"); + default: return QStringLiteral("Unknown"); + }; +} + +} // namespace qs::network diff --git a/src/network/enums.hpp b/src/network/enums.hpp new file mode 100644 index 0000000..49c28ce --- /dev/null +++ b/src/network/enums.hpp @@ -0,0 +1,154 @@ +#pragma once + +#include +#include +#include + +namespace qs::network { + +///! The degree to which the host can reach the internet. +class NetworkConnectivity: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// Network connectivity is unknown. This means the connectivity checks are disabled or have not run yet. + Unknown = 0, + /// The host is not connected to any network. + None = 1, + /// The internet connection is hijacked by a captive portal gateway. + /// This indicates the shell should open a sandboxed web browser window for the purpose of authenticating to a gateway. + Portal = 2, + /// The host is connected to a network but does not appear to be able to reach the full internet. + Limited = 3, + /// The host is connected to a network and appears to be able to reach the full internet. + Full = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NetworkConnectivity::Enum conn); +}; + +///! The backend supplying the Network service. +class NetworkBackendType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + None = 0, + NetworkManager = 1, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NetworkBackendType::Enum type); +}; + +///! The connection state of a device or network. +class ConnectionState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Connecting = 1, + Connected = 2, + Disconnecting = 3, + Disconnected = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(ConnectionState::Enum state); +}; + +///! The reason a connection failed. +class ConnectionFailReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// The connection failed for an unknown reason. + Unknown = 0, + /// Secrets were required, but not provided. + NoSecrets = 1, + /// The Wi-Fi supplicant disconnected. + WifiClientDisconnected = 2, + /// The Wi-Fi supplicant failed. + WifiClientFailed = 3, + /// The Wi-Fi connection took too long to authenticate. + WifiAuthTimeout = 4, + /// The Wi-Fi network could not be found. + WifiNetworkLost = 5, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(ConnectionFailReason::Enum reason); +}; + +///! Type of a @@NetworkDevice. +class DeviceType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + None = 0, + Wifi = 1, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(DeviceType::Enum type); +}; + +///! The security type of a @@WifiNetwork. +class WifiSecurityType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Wpa3SuiteB192 = 0, + Sae = 1, + Wpa2Eap = 2, + Wpa2Psk = 3, + WpaEap = 4, + WpaPsk = 5, + StaticWep = 6, + DynamicWep = 7, + Leap = 8, + Owe = 9, + Open = 10, + Unknown = 11, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(WifiSecurityType::Enum type); +}; + +///! The 802.11 mode of a @@WifiDevice. +class WifiDeviceMode: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// The device is part of an Ad-Hoc network without a central access point. + AdHoc = 0, + /// The device is a station that can connect to networks. + Station = 1, + /// The device is a local hotspot/access point. + AccessPoint = 2, + /// The device is an 802.11s mesh point. + Mesh = 3, + /// The device mode is unknown. + Unknown = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(WifiDeviceMode::Enum mode); +}; + +} // namespace qs::network diff --git a/src/network/module.md b/src/network/module.md index a0c8e64..91ff2f1 100644 --- a/src/network/module.md +++ b/src/network/module.md @@ -4,6 +4,8 @@ headers = [ "network.hpp", "device.hpp", "wifi.hpp", + "enums.hpp", + "nm/settings.hpp", ] ----- This module exposes Network management APIs provided by a supported network backend. diff --git a/src/network/network.cpp b/src/network/network.cpp index e325b05..e66ffa6 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -1,6 +1,7 @@ #include "network.hpp" #include +#include #include #include #include @@ -9,7 +10,9 @@ #include "../core/logcat.hpp" #include "device.hpp" +#include "enums.hpp" #include "nm/backend.hpp" +#include "nm/settings.hpp" namespace qs::network { @@ -17,25 +20,22 @@ namespace { QS_LOGGING_CATEGORY(logNetwork, "quickshell.network", QtWarningMsg); } // namespace -QString NetworkState::toString(NetworkState::Enum state) { - switch (state) { - case NetworkState::Connecting: return QStringLiteral("Connecting"); - case NetworkState::Connected: return QStringLiteral("Connected"); - case NetworkState::Disconnecting: return QStringLiteral("Disconnecting"); - case NetworkState::Disconnected: return QStringLiteral("Disconnected"); - default: return QStringLiteral("Unknown"); - } -} - Networking::Networking(QObject* parent): QObject(parent) { // Try to create the NetworkManager backend and bind to it. auto* nm = new NetworkManager(this); if (nm->isAvailable()) { + // clang-format off QObject::connect(nm, &NetworkManager::deviceAdded, this, &Networking::deviceAdded); QObject::connect(nm, &NetworkManager::deviceRemoved, this, &Networking::deviceRemoved); QObject::connect(this, &Networking::requestSetWifiEnabled, nm, &NetworkManager::setWifiEnabled); + QObject::connect(this, &Networking::requestSetConnectivityCheckEnabled, nm, &NetworkManager::setConnectivityCheckEnabled); + QObject::connect(this, &Networking::requestCheckConnectivity, nm, &NetworkManager::checkConnectivity); this->bindableWifiEnabled().setBinding([nm]() { return nm->wifiEnabled(); }); this->bindableWifiHardwareEnabled().setBinding([nm]() { return nm->wifiHardwareEnabled(); }); + this->bindableCanCheckConnectivity().setBinding([nm]() { return nm->connectivityCheckAvailable(); }); + this->bindableConnectivityCheckEnabled().setBinding([nm]() { return nm->connectivityCheckEnabled(); }); + this->bindableConnectivity().setBinding([nm]() { return static_cast(nm->connectivity()); }); + // clang-format on this->mBackend = nm; this->mBackendType = NetworkBackendType::NetworkManager; @@ -43,23 +43,89 @@ Networking::Networking(QObject* parent): QObject(parent) { } else { delete nm; } - qCCritical(logNetwork) << "Network will not work. Could not find an available backend."; } +Networking* Networking::instance() { + static Networking* instance = new Networking(); // NOLINT + return instance; +} + void Networking::deviceAdded(NetworkDevice* dev) { this->mDevices.insertObject(dev); } void Networking::deviceRemoved(NetworkDevice* dev) { this->mDevices.removeObject(dev); } +void Networking::checkConnectivity() { + if (!this->bConnectivityCheckEnabled || !this->bCanCheckConnectivity) return; + emit this->requestCheckConnectivity(); +} + void Networking::setWifiEnabled(bool enabled) { if (this->bWifiEnabled == enabled) return; emit this->requestSetWifiEnabled(enabled); } +void Networking::setConnectivityCheckEnabled(bool enabled) { + if (this->bConnectivityCheckEnabled == enabled) return; + emit this->requestSetConnectivityCheckEnabled(enabled); +} + +NetworkingQml::NetworkingQml(QObject* parent): QObject(parent) { + // clang-format off + QObject::connect(Networking::instance(), &Networking::wifiEnabledChanged, this, &NetworkingQml::wifiEnabledChanged); + QObject::connect(Networking::instance(), &Networking::wifiHardwareEnabledChanged, this, &NetworkingQml::wifiHardwareEnabledChanged); + QObject::connect(Networking::instance(), &Networking::canCheckConnectivityChanged, this, &NetworkingQml::canCheckConnectivityChanged); + QObject::connect(Networking::instance(), &Networking::connectivityCheckEnabledChanged, this, &NetworkingQml::connectivityCheckEnabledChanged); + QObject::connect(Networking::instance(), &Networking::connectivityChanged, this, &NetworkingQml::connectivityChanged); + // clang-format on +} + +void NetworkingQml::checkConnectivity() { Networking::instance()->checkConnectivity(); } + Network::Network(QString name, QObject* parent): QObject(parent), mName(std::move(name)) { this->bStateChanging.setBinding([this] { auto state = this->bState.value(); - return state == NetworkState::Connecting || state == NetworkState::Disconnecting; + return state == ConnectionState::Connecting || state == ConnectionState::Disconnecting; }); }; +void Network::connect() { + if (this->bConnected) { + qCCritical(logNetwork) << this << "is already connected."; + return; + } + this->requestConnect(); +} + +void Network::connectWithSettings(NMSettings* settings) { + if (this->bConnected) { + qCCritical(logNetwork) << this << "is already connected."; + return; + } + if (this->bNmSettings.value().indexOf(settings) == -1) return; + this->requestConnectWithSettings(settings); +} + +void Network::disconnect() { + if (!this->bConnected) { + qCCritical(logNetwork) << this << "is not currently connected"; + return; + } + this->requestDisconnect(); +} + +void Network::forget() { this->requestForget(); } + +void Network::settingsAdded(NMSettings* settings) { + auto list = this->bNmSettings.value(); + if (list.contains(settings)) return; + list.append(settings); + this->bNmSettings = list; +} + +void Network::settingsRemoved(NMSettings* settings) { + auto list = this->bNmSettings.value(); + list.removeOne(settings); + this->bNmSettings = list; +} + } // namespace qs::network diff --git a/src/network/network.hpp b/src/network/network.hpp index 8af7c9d..f7734a2 100644 --- a/src/network/network.hpp +++ b/src/network/network.hpp @@ -6,43 +6,14 @@ #include #include +#include "../core/doc.hpp" #include "../core/model.hpp" #include "device.hpp" +#include "enums.hpp" +#include "nm/settings.hpp" namespace qs::network { -///! The connection state of a Network. -class NetworkState: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - Unknown = 0, - Connecting = 1, - Connected = 2, - Disconnecting = 3, - Disconnected = 4, - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(NetworkState::Enum state); -}; - -///! The backend supplying the Network service. -class NetworkBackendType: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - None = 0, - NetworkManager = 1, - }; - Q_ENUM(Enum); -}; - class NetworkBackend: public QObject { Q_OBJECT; @@ -53,15 +24,65 @@ protected: explicit NetworkBackend(QObject* parent = nullptr): QObject(parent) {}; }; +class Networking: public QObject { + Q_OBJECT; + +public: + static Networking* instance(); + + void checkConnectivity(); + + [[nodiscard]] ObjectModel* devices() { return &this->mDevices; } + [[nodiscard]] NetworkBackendType::Enum backend() const { return this->mBackendType; } + QBindable bindableWifiEnabled() { return &this->bWifiEnabled; } + [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; } + void setWifiEnabled(bool enabled); + QBindable bindableWifiHardwareEnabled() { return &this->bWifiHardwareEnabled; } + QBindable bindableCanCheckConnectivity() { return &this->bCanCheckConnectivity; } + QBindable bindableConnectivityCheckEnabled() { return &this->bConnectivityCheckEnabled; } + [[nodiscard]] bool connectivityCheckEnabled() const { return this->bConnectivityCheckEnabled; } + void setConnectivityCheckEnabled(bool enabled); + QBindable bindableConnectivity() { return &this->bConnectivity; } + +signals: + void requestSetWifiEnabled(bool enabled); + void requestSetConnectivityCheckEnabled(bool enabled); + void requestCheckConnectivity(); + + void wifiEnabledChanged(); + void wifiHardwareEnabledChanged(); + void canCheckConnectivityChanged(); + void connectivityCheckEnabledChanged(); + void connectivityChanged(); + +private slots: + void deviceAdded(NetworkDevice* dev); + void deviceRemoved(NetworkDevice* dev); + +private: + explicit Networking(QObject* parent = nullptr); + + ObjectModel mDevices {this}; + NetworkBackend* mBackend = nullptr; + NetworkBackendType::Enum mBackendType = NetworkBackendType::None; + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiEnabled, &Networking::wifiEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiHardwareEnabled, &Networking::wifiHardwareEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bCanCheckConnectivity, &Networking::canCheckConnectivityChanged); + Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bConnectivityCheckEnabled, &Networking::connectivityCheckEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(Networking, NetworkConnectivity::Enum, bConnectivity, &Networking::connectivityChanged); + // clang-format on +}; + ///! The Network service. /// An interface to a network backend (currently only NetworkManager), /// which can be used to view, configure, and connect to various networks. -class Networking: public QObject { +class NetworkingQml: public QObject { Q_OBJECT; + QML_NAMED_ELEMENT(Networking); QML_SINGLETON; - QML_ELEMENT; // clang-format off - /// A list of all network devices. + /// A list of all network devices. Networks are exposed through their respective devices. QSDOC_TYPE_OVERRIDE(ObjectModel*); Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); /// The backend being used to power the Network service. @@ -70,73 +91,143 @@ class Networking: public QObject { Q_PROPERTY(bool wifiEnabled READ wifiEnabled WRITE setWifiEnabled NOTIFY wifiEnabledChanged); /// State of the rfkill hardware block of all wireless devices. Q_PROPERTY(bool wifiHardwareEnabled READ default NOTIFY wifiHardwareEnabledChanged BINDABLE bindableWifiHardwareEnabled); + /// True if the @@backend supports connectivity checks. + Q_PROPERTY(bool canCheckConnectivity READ default NOTIFY canCheckConnectivityChanged BINDABLE bindableCanCheckConnectivity); + /// True if connectivity checking is enabled. + Q_PROPERTY(bool connectivityCheckEnabled READ connectivityCheckEnabled WRITE setConnectivityCheckEnabled NOTIFY connectivityCheckEnabledChanged); + /// The result of the last connectivity check. + /// + /// Connectivity checks may require additional configuration depending on your distro. + /// + /// > [!NOTE] This property can be used to determine if network access is restricted + /// > or gated behind a captive portal. + /// > + /// > If checking for captive portals, @@checkConnectivity() should be called after + /// > the portal is dismissed to update this property. + Q_PROPERTY(qs::network::NetworkConnectivity::Enum connectivity READ default NOTIFY connectivityChanged BINDABLE bindableConnectivity); // clang-format on public: - explicit Networking(QObject* parent = nullptr); + explicit NetworkingQml(QObject* parent = nullptr); - [[nodiscard]] ObjectModel* devices() { return &this->mDevices; }; - [[nodiscard]] NetworkBackendType::Enum backend() const { return this->mBackendType; }; - QBindable bindableWifiEnabled() { return &this->bWifiEnabled; }; - [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; }; - void setWifiEnabled(bool enabled); - QBindable bindableWifiHardwareEnabled() { return &this->bWifiHardwareEnabled; }; + /// Re-check the network connectivity state immediately. + /// > [!NOTE] This should be invoked after a user dismisses a web browser that was opened to authenticate via a captive portal. + Q_INVOKABLE static void checkConnectivity(); + + [[nodiscard]] static ObjectModel* devices() { + return Networking::instance()->devices(); + } + [[nodiscard]] static NetworkBackendType::Enum backend() { + return Networking::instance()->backend(); + } + [[nodiscard]] static bool wifiEnabled() { return Networking::instance()->wifiEnabled(); } + static void setWifiEnabled(bool enabled) { Networking::instance()->setWifiEnabled(enabled); } + [[nodiscard]] static QBindable bindableWifiHardwareEnabled() { + return Networking::instance()->bindableWifiHardwareEnabled(); + } + [[nodiscard]] static QBindable bindableWifiEnabled() { + return Networking::instance()->bindableWifiEnabled(); + } + [[nodiscard]] static QBindable bindableCanCheckConnectivity() { + return Networking::instance()->bindableCanCheckConnectivity(); + } + [[nodiscard]] static bool connectivityCheckEnabled() { + return Networking::instance()->connectivityCheckEnabled(); + } + static void setConnectivityCheckEnabled(bool enabled) { + Networking::instance()->setConnectivityCheckEnabled(enabled); + } + [[nodiscard]] static QBindable bindableConnectivity() { + return Networking::instance()->bindableConnectivity(); + } signals: - void requestSetWifiEnabled(bool enabled); void wifiEnabledChanged(); void wifiHardwareEnabledChanged(); - -private slots: - void deviceAdded(NetworkDevice* dev); - void deviceRemoved(NetworkDevice* dev); - -private: - ObjectModel mDevices {this}; - NetworkBackend* mBackend = nullptr; - NetworkBackendType::Enum mBackendType = NetworkBackendType::None; - // clang-format off - Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiEnabled, &Networking::wifiEnabledChanged); - Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiHardwareEnabled, &Networking::wifiHardwareEnabledChanged); - // clang-format on + void canCheckConnectivityChanged(); + void connectivityCheckEnabledChanged(); + void connectivityChanged(); }; ///! A network. +/// A network. Networks derived from a @@WifiDevice are @@WifiNetwork instances. class Network: public QObject { Q_OBJECT; QML_ELEMENT; - QML_UNCREATABLE("BaseNetwork can only be aqcuired through network devices"); + QML_UNCREATABLE("Network can only be aqcuired through networking devices"); // clang-format off /// The name of the network. Q_PROPERTY(QString name READ name CONSTANT); + /// A list of NetworkManager connnection settings profiles for this network. + /// + /// > [!WARNING] Only valid for the NetworkManager backend. + Q_PROPERTY(QList nmSettings READ nmSettings NOTIFY nmSettingsChanged BINDABLE bindableNmSettings); /// True if the network is connected. Q_PROPERTY(bool connected READ default NOTIFY connectedChanged BINDABLE bindableConnected); + /// True if the wifi network has known connection settings saved. + Q_PROPERTY(bool known READ default NOTIFY knownChanged BINDABLE bindableKnown); /// The connectivity state of the network. - Q_PROPERTY(NetworkState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); + Q_PROPERTY(ConnectionState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); /// If the network is currently connecting or disconnecting. Shorthand for checking @@state. Q_PROPERTY(bool stateChanging READ default NOTIFY stateChangingChanged BINDABLE bindableStateChanging); // clang-format on public: explicit Network(QString name, QObject* parent = nullptr); + /// Attempt to connect to the network. + /// + /// > [!NOTE] If the network is a @@WifiNetwork and requires secrets, a @@connectionFailed(s) + /// > signal will be emitted with `NoSecrets`. + /// > @@WifiNetwork.connectWithPsk() can be used to provide secrets. + Q_INVOKABLE void connect(); + /// Attempt to connect to the network with a specific @@nmSettings entry. + /// + /// > [!WARNING] Only valid for the NetworkManager backend. + Q_INVOKABLE void connectWithSettings(NMSettings* settings); + /// Disconnect from the network. + Q_INVOKABLE void disconnect(); + /// Forget all connection settings for this network. + Q_INVOKABLE void forget(); - [[nodiscard]] QString name() const { return this->mName; }; + void settingsAdded(NMSettings* settings); + void settingsRemoved(NMSettings* settings); + + // clang-format off + [[nodiscard]] QString name() const { return this->mName; } + [[nodiscard]] const QList& nmSettings() const { return this->bNmSettings; } + QBindable> bindableNmSettings() const { return &this->bNmSettings; } QBindable bindableConnected() { return &this->bConnected; } - QBindable bindableState() { return &this->bState; } + QBindable bindableKnown() { return &this->bKnown; } + [[nodiscard]] ConnectionState::Enum state() const { return this->bState; } + QBindable bindableState() { return &this->bState; } QBindable bindableStateChanging() { return &this->bStateChanging; } + // clang-format on signals: + /// Signals that a connection to the network has failed because of the given @@ConnectionFailReason. + void connectionFailed(ConnectionFailReason::Enum reason); + void connectedChanged(); + void knownChanged(); void stateChanged(); void stateChangingChanged(); + void nmSettingsChanged(); + QSDOC_HIDE void requestConnect(); + QSDOC_HIDE void requestConnectWithSettings(NMSettings* settings); + QSDOC_HIDE void requestDisconnect(); + QSDOC_HIDE void requestForget(); protected: QString mName; + // clang-format off Q_OBJECT_BINDABLE_PROPERTY(Network, bool, bConnected, &Network::connectedChanged); - Q_OBJECT_BINDABLE_PROPERTY(Network, NetworkState::Enum, bState, &Network::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Network, bool, bKnown, &Network::knownChanged); + Q_OBJECT_BINDABLE_PROPERTY(Network, ConnectionState::Enum, bState, &Network::stateChanged); Q_OBJECT_BINDABLE_PROPERTY(Network, bool, bStateChanging, &Network::stateChangingChanged); + Q_OBJECT_BINDABLE_PROPERTY(Network, QList, bNmSettings, &Network::nmSettingsChanged); + // clang-format on }; } // namespace qs::network diff --git a/src/network/nm/CMakeLists.txt b/src/network/nm/CMakeLists.txt index bb8635e..61f7e66 100644 --- a/src/network/nm/CMakeLists.txt +++ b/src/network/nm/CMakeLists.txt @@ -63,10 +63,12 @@ qt_add_dbus_interface(NM_DBUS_INTERFACES qt_add_library(quickshell-network-nm STATIC backend.cpp device.cpp - connection.cpp + active_connection.cpp + settings.cpp accesspoint.cpp wireless.cpp utils.cpp + dbus_types.cpp enums.hpp ${NM_DBUS_INTERFACES} ) diff --git a/src/network/nm/accesspoint.hpp b/src/network/nm/accesspoint.hpp index 8409089..63e35ee 100644 --- a/src/network/nm/accesspoint.hpp +++ b/src/network/nm/accesspoint.hpp @@ -48,14 +48,14 @@ public: [[nodiscard]] bool isValid() const; [[nodiscard]] QString path() const; [[nodiscard]] QString address() const; - [[nodiscard]] QByteArray ssid() const { return this->bSsid; }; - [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; - [[nodiscard]] NM80211ApFlags::Enum flags() const { return this->bFlags; }; - [[nodiscard]] NM80211ApSecurityFlags::Enum wpaFlags() const { return this->bWpaFlags; }; - [[nodiscard]] NM80211ApSecurityFlags::Enum rsnFlags() const { return this->bRsnFlags; }; - [[nodiscard]] NM80211Mode::Enum mode() const { return this->bMode; }; - [[nodiscard]] QBindable bindableSecurity() { return &this->bSecurity; }; - [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; }; + [[nodiscard]] QByteArray ssid() const { return this->bSsid; } + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; } + [[nodiscard]] NM80211ApFlags::Enum flags() const { return this->bFlags; } + [[nodiscard]] NM80211ApSecurityFlags::Enum wpaFlags() const { return this->bWpaFlags; } + [[nodiscard]] NM80211ApSecurityFlags::Enum rsnFlags() const { return this->bRsnFlags; } + [[nodiscard]] NM80211Mode::Enum mode() const { return this->bMode; } + [[nodiscard]] QBindable bindableSecurity() { return &this->bSecurity; } + [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; } signals: void loaded(); diff --git a/src/network/nm/active_connection.cpp b/src/network/nm/active_connection.cpp new file mode 100644 index 0000000..cab0e52 --- /dev/null +++ b/src/network/nm/active_connection.cpp @@ -0,0 +1,68 @@ +#include "active_connection.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "dbus_nm_active_connection.h" +#include "enums.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMActiveConnection::NMActiveConnection(const QString& path, QObject* parent): QObject(parent) { + this->proxy = new DBusNMActiveConnectionProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; + return; + } + + // clang-format off + QObject::connect(&this->activeConnectionProperties, &DBusPropertyGroup::getAllFinished, this, &NMActiveConnection::loaded, Qt::SingleShotConnection); + QObject::connect(this->proxy, &DBusNMActiveConnectionProxy::StateChanged, this, &NMActiveConnection::onStateChanged); + // clang-format on + + this->activeConnectionProperties.setInterface(this->proxy); + this->activeConnectionProperties.updateAllViaGetAll(); +} + +void NMActiveConnection::onStateChanged(quint32 /*state*/, quint32 reason) { + auto enumReason = static_cast(reason); + if (this->bStateReason == enumReason) return; + this->bStateReason = enumReason; +} + +bool NMActiveConnection::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMActiveConnection::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMActiveConnection::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/connection.hpp b/src/network/nm/active_connection.hpp similarity index 51% rename from src/network/nm/connection.hpp rename to src/network/nm/active_connection.hpp index 4f126c8..33426a1 100644 --- a/src/network/nm/connection.hpp +++ b/src/network/nm/active_connection.hpp @@ -1,18 +1,16 @@ #pragma once +#include #include #include -#include #include #include -#include +#include +#include #include #include "../../dbus/properties.hpp" -#include "../wifi.hpp" #include "dbus_nm_active_connection.h" -#include "dbus_nm_connection_settings.h" -#include "dbus_types.hpp" #include "enums.hpp" namespace qs::dbus { @@ -28,40 +26,6 @@ struct DBusDataTransform { namespace qs::network { -// Proxy of a /org/freedesktop/NetworkManager/Settings/Connection/* object. -class NMConnectionSettings: public QObject { - Q_OBJECT; - -public: - explicit NMConnectionSettings(const QString& path, QObject* parent = nullptr); - - void forget(); - - [[nodiscard]] bool isValid() const; - [[nodiscard]] QString path() const; - [[nodiscard]] QString address() const; - [[nodiscard]] ConnectionSettingsMap settings() const { return this->bSettings; }; - [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; }; - [[nodiscard]] QBindable bindableSecurity() { return &this->bSecurity; }; - -signals: - void loaded(); - void settingsChanged(ConnectionSettingsMap settings); - void securityChanged(WifiSecurityType::Enum security); - void ssidChanged(QString ssid); - -private: - bool mLoaded = false; - void updateSettings(); - // clang-format off - Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, ConnectionSettingsMap, bSettings, &NMConnectionSettings::settingsChanged); - Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, WifiSecurityType::Enum, bSecurity, &NMConnectionSettings::securityChanged); - QS_DBUS_BINDABLE_PROPERTY_GROUP(NMConnectionSettings, connectionSettingsProperties); - // clang-format on - - DBusNMConnectionSettingsProxy* proxy = nullptr; -}; - // Proxy of a /org/freedesktop/NetworkManager/ActiveConnection/* object. class NMActiveConnection: public QObject { Q_OBJECT; @@ -72,31 +36,27 @@ public: [[nodiscard]] bool isValid() const; [[nodiscard]] QString path() const; [[nodiscard]] QString address() const; - [[nodiscard]] QDBusObjectPath connection() const { return this->bConnection; }; - [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; }; - [[nodiscard]] NMConnectionStateReason::Enum stateReason() const { return this->mStateReason; }; + [[nodiscard]] QDBusObjectPath connection() const { return this->bConnection; } + [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; } + [[nodiscard]] NMConnectionStateReason::Enum stateReason() const { return this->bStateReason; } signals: void loaded(); void connectionChanged(QDBusObjectPath path); void stateChanged(NMConnectionState::Enum state); void stateReasonChanged(NMConnectionStateReason::Enum reason); - void uuidChanged(const QString& uuid); private slots: void onStateChanged(quint32 state, quint32 reason); private: - NMConnectionStateReason::Enum mStateReason = NMConnectionStateReason::Unknown; - // clang-format off Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QDBusObjectPath, bConnection, &NMActiveConnection::connectionChanged); - Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QString, bUuid, &NMActiveConnection::uuidChanged); Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, NMConnectionState::Enum, bState, &NMActiveConnection::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, NMConnectionStateReason::Enum, bStateReason, &NMActiveConnection::stateReasonChanged); QS_DBUS_BINDABLE_PROPERTY_GROUP(NMActiveConnection, activeConnectionProperties); QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pConnection, bConnection, activeConnectionProperties, "Connection"); - QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pUuid, bUuid, activeConnectionProperties, "Uuid"); QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pState, bState, activeConnectionProperties, "State"); // clang-format on DBusNMActiveConnectionProxy* proxy = nullptr; diff --git a/src/network/nm/backend.cpp b/src/network/nm/backend.cpp index 4b61e33..a46ccb2 100644 --- a/src/network/nm/backend.cpp +++ b/src/network/nm/backend.cpp @@ -1,5 +1,6 @@ #include "backend.hpp" +#include #include #include #include @@ -15,6 +16,7 @@ #include "../../core/logcat.hpp" #include "../../dbus/properties.hpp" #include "../device.hpp" +#include "../enums.hpp" #include "../network.hpp" #include "../wifi.hpp" #include "dbus_nm_backend.h" @@ -31,7 +33,8 @@ QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWa } NetworkManager::NetworkManager(QObject* parent): NetworkBackend(parent) { - qDBusRegisterMetaType(); + qCDebug(logNetworkManager) << "Connecting to NetworkManager"; + qDBusRegisterMetaType(); auto bus = QDBusConnection::systemBus(); if (!bus.isConnected()) { @@ -69,6 +72,23 @@ void NetworkManager::init() { this->registerDevices(); } +void NetworkManager::checkConnectivity() { + auto pending = this->proxy->CheckConnectivity(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCInfo(logNetworkManager) << "Failed to check connectivity: " << reply.error().message(); + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + void NetworkManager::registerDevices() { auto pending = this->proxy->GetAllDevices(); auto* call = new QDBusPendingCallWatcher(pending, this); @@ -117,23 +137,21 @@ void NetworkManager::registerDevice(const QString& path) { } if (dev) { + qCDebug(logNetworkManager) << "Device added:" << path; if (!dev->isValid()) { qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; delete dev; } else { this->mDevices[path] = dev; - // Only register a frontend device while it's managed by NM. - auto onManagedChanged = [this, dev, type](bool managed) { - managed ? this->registerFrontendDevice(type, dev) : this->removeFrontendDevice(dev); - }; // clang-format off QObject::connect(dev, &NMDevice::addAndActivateConnection, this, &NetworkManager::addAndActivateConnection); QObject::connect(dev, &NMDevice::activateConnection, this, &NetworkManager::activateConnection); - QObject::connect(dev, &NMDevice::managedChanged, this, onManagedChanged); // clang-format on - if (dev->managed()) this->registerFrontendDevice(type, dev); + this->registerFrontendDevice(type, dev); } + } else { + qCDebug(logNetworkManager) << "Ignoring registration of unsupported device:" << path; } temp->deleteLater(); } @@ -173,21 +191,22 @@ void NetworkManager::registerFrontendDevice(NMDeviceType::Enum type, NMDevice* d // Bind generic NetworkDevice properties auto translateState = [dev]() { switch (dev->state()) { - case 0 ... 20: return DeviceConnectionState::Unknown; - case 30: return DeviceConnectionState::Disconnected; - case 40 ... 90: return DeviceConnectionState::Connecting; - case 100: return DeviceConnectionState::Connected; - case 110 ... 120: return DeviceConnectionState::Disconnecting; + case 0 ... 20: return ConnectionState::Unknown; + case 30: return ConnectionState::Disconnected; + case 40 ... 90: return ConnectionState::Connecting; + case 100: return ConnectionState::Connected; + case 110 ... 120: return ConnectionState::Disconnecting; } }; // clang-format off frontendDev->bindableName().setBinding([dev]() { return dev->interface(); }); frontendDev->bindableAddress().setBinding([dev]() { return dev->hwAddress(); }); - frontendDev->bindableNmState().setBinding([dev]() { return dev->state(); }); frontendDev->bindableState().setBinding(translateState); frontendDev->bindableAutoconnect().setBinding([dev]() { return dev->autoconnect(); }); + frontendDev->bindableNmManaged().setBinding([dev]() { return dev->managed(); }); QObject::connect(frontendDev, &WifiDevice::requestDisconnect, dev, &NMDevice::disconnect); QObject::connect(frontendDev, &NetworkDevice::requestSetAutoconnect, dev, &NMDevice::setAutoconnect); + QObject::connect(frontendDev, &NetworkDevice::requestSetNmManaged, dev, &NMDevice::setManaged); // clang-format on this->mFrontendDevices.insert(dev->path(), frontendDev); @@ -215,6 +234,7 @@ void NetworkManager::onDevicePathRemoved(const QDBusObjectPath& path) { auto* dev = iter.value(); this->mDevices.erase(iter); if (dev) { + qCDebug(logNetworkManager) << "Device removed:" << path.path(); this->removeFrontendDevice(dev); delete dev; } @@ -240,7 +260,7 @@ void NetworkManager::activateConnection( } void NetworkManager::addAndActivateConnection( - const ConnectionSettingsMap& settings, + const NMSettingsMap& settings, const QDBusObjectPath& devPath, const QDBusObjectPath& specificObjectPath ) { @@ -259,6 +279,12 @@ void NetworkManager::addAndActivateConnection( QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); } +void NetworkManager::setConnectivityCheckEnabled(bool enabled) { + if (enabled == this->bConnectivityCheckEnabled) return; + this->bConnectivityCheckEnabled = enabled; + this->pConnectivityCheckEnabled.write(); +} + void NetworkManager::setWifiEnabled(bool enabled) { if (enabled == this->bWifiEnabled) return; this->bWifiEnabled = enabled; @@ -268,3 +294,12 @@ void NetworkManager::setWifiEnabled(bool enabled) { bool NetworkManager::isAvailable() const { return this->proxy && this->proxy->isValid(); }; } // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/backend.hpp b/src/network/nm/backend.hpp index 471f57a..2825a17 100644 --- a/src/network/nm/backend.hpp +++ b/src/network/nm/backend.hpp @@ -10,7 +10,20 @@ #include "../../dbus/properties.hpp" #include "../network.hpp" #include "dbus_nm_backend.h" +#include "dbus_types.hpp" #include "device.hpp" +#include "enums.hpp" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMConnectivityState::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus namespace qs::network { @@ -21,24 +34,34 @@ public: explicit NetworkManager(QObject* parent = nullptr); [[nodiscard]] bool isAvailable() const override; - [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; }; - [[nodiscard]] bool wifiHardwareEnabled() const { return this->bWifiHardwareEnabled; }; + [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; } + [[nodiscard]] bool wifiHardwareEnabled() const { return this->bWifiHardwareEnabled; } + [[nodiscard]] bool connectivityCheckAvailable() const { + return this->bConnectivityCheckAvailable; + }; + [[nodiscard]] bool connectivityCheckEnabled() const { return this->bConnectivityCheckEnabled; } + [[nodiscard]] NMConnectivityState::Enum connectivity() const { return this->bConnectivity; } signals: void deviceAdded(NetworkDevice* device); void deviceRemoved(NetworkDevice* device); void wifiEnabledChanged(bool enabled); void wifiHardwareEnabledChanged(bool enabled); + void connectivityStateChanged(NMConnectivityState::Enum state); + void connectivityCheckAvailableChanged(bool available); + void connectivityCheckEnabledChanged(bool enabled); public slots: void setWifiEnabled(bool enabled); + void setConnectivityCheckEnabled(bool enabled); + void checkConnectivity(); private slots: void onDevicePathAdded(const QDBusObjectPath& path); void onDevicePathRemoved(const QDBusObjectPath& path); void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath); void addAndActivateConnection( - const ConnectionSettingsMap& settings, + const NMSettingsMap& settings, const QDBusObjectPath& devPath, const QDBusObjectPath& specificObjectPath ); @@ -56,10 +79,16 @@ private: // clang-format off Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiEnabled, &NetworkManager::wifiEnabledChanged); Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiHardwareEnabled, &NetworkManager::wifiHardwareEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, NMConnectivityState::Enum, bConnectivity, &NetworkManager::connectivityStateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bConnectivityCheckAvailable, &NetworkManager::connectivityCheckAvailableChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bConnectivityCheckEnabled, &NetworkManager::connectivityCheckEnabledChanged); QS_DBUS_BINDABLE_PROPERTY_GROUP(NetworkManager, dbusProperties); QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiEnabled, bWifiEnabled, dbusProperties, "WirelessEnabled"); QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiHardwareEnabled, bWifiHardwareEnabled, dbusProperties, "WirelessHardwareEnabled"); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pConnectivity, bConnectivity, dbusProperties, "Connectivity"); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pConnectivityCheckAvailable, bConnectivityCheckAvailable, dbusProperties, "ConnectivityCheckAvailable"); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pConnectivityCheckEnabled, bConnectivityCheckEnabled, dbusProperties, "ConnectivityCheckEnabled"); // clang-format on DBusNetworkManagerProxy* proxy = nullptr; }; diff --git a/src/network/nm/connection.cpp b/src/network/nm/connection.cpp deleted file mode 100644 index 39b6f66..0000000 --- a/src/network/nm/connection.cpp +++ /dev/null @@ -1,151 +0,0 @@ -#include "connection.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../../core/logcat.hpp" -#include "../../dbus/properties.hpp" -#include "../wifi.hpp" -#include "dbus_nm_active_connection.h" -#include "dbus_nm_connection_settings.h" -#include "dbus_types.hpp" -#include "enums.hpp" -#include "utils.hpp" - -namespace qs::network { -using namespace qs::dbus; - -namespace { -QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); -} - -NMConnectionSettings::NMConnectionSettings(const QString& path, QObject* parent): QObject(parent) { - qDBusRegisterMetaType(); - - this->proxy = new DBusNMConnectionSettingsProxy( - "org.freedesktop.NetworkManager", - path, - QDBusConnection::systemBus(), - this - ); - - if (!this->proxy->isValid()) { - qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; - return; - } - - QObject::connect( - this->proxy, - &DBusNMConnectionSettingsProxy::Updated, - this, - &NMConnectionSettings::updateSettings - ); - this->bSecurity.setBinding([this]() { return securityFromConnectionSettings(this->bSettings); }); - - this->connectionSettingsProperties.setInterface(this->proxy); - this->connectionSettingsProperties.updateAllViaGetAll(); - - this->updateSettings(); -} - -void NMConnectionSettings::updateSettings() { - auto pending = this->proxy->GetSettings(); - auto* call = new QDBusPendingCallWatcher(pending, this); - - auto responseCallback = [this](QDBusPendingCallWatcher* call) { - const QDBusPendingReply reply = *call; - - if (reply.isError()) { - qCWarning(logNetworkManager) - << "Failed to get" << this->path() << "settings:" << reply.error().message(); - } else { - this->bSettings = reply.value(); - } - - if (!this->mLoaded) { - emit this->loaded(); - this->mLoaded = true; - } - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); -} - -void NMConnectionSettings::forget() { - auto pending = this->proxy->Delete(); - auto* call = new QDBusPendingCallWatcher(pending, this); - - auto responseCallback = [this](QDBusPendingCallWatcher* call) { - const QDBusPendingReply<> reply = *call; - - if (reply.isError()) { - qCWarning(logNetworkManager) - << "Failed to forget" << this->path() << ":" << reply.error().message(); - } - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); -} - -bool NMConnectionSettings::isValid() const { return this->proxy && this->proxy->isValid(); } -QString NMConnectionSettings::address() const { - return this->proxy ? this->proxy->service() : QString(); -} -QString NMConnectionSettings::path() const { return this->proxy ? this->proxy->path() : QString(); } - -NMActiveConnection::NMActiveConnection(const QString& path, QObject* parent): QObject(parent) { - this->proxy = new DBusNMActiveConnectionProxy( - "org.freedesktop.NetworkManager", - path, - QDBusConnection::systemBus(), - this - ); - - if (!this->proxy->isValid()) { - qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; - return; - } - - // clang-format off - QObject::connect(&this->activeConnectionProperties, &DBusPropertyGroup::getAllFinished, this, &NMActiveConnection::loaded, Qt::SingleShotConnection); - QObject::connect(this->proxy, &DBusNMActiveConnectionProxy::StateChanged, this, &NMActiveConnection::onStateChanged); - // clang-format on - - this->activeConnectionProperties.setInterface(this->proxy); - this->activeConnectionProperties.updateAllViaGetAll(); -} - -void NMActiveConnection::onStateChanged(quint32 /*state*/, quint32 reason) { - auto enumReason = static_cast(reason); - if (this->mStateReason == enumReason) return; - this->mStateReason = enumReason; - emit this->stateReasonChanged(enumReason); -} - -bool NMActiveConnection::isValid() const { return this->proxy && this->proxy->isValid(); } -QString NMActiveConnection::address() const { - return this->proxy ? this->proxy->service() : QString(); -} -QString NMActiveConnection::path() const { return this->proxy ? this->proxy->path() : QString(); } - -} // namespace qs::network - -namespace qs::dbus { - -DBusResult -DBusDataTransform::fromWire(quint32 wire) { - return DBusResult(static_cast(wire)); -} - -} // namespace qs::dbus diff --git a/src/network/nm/dbus_types.cpp b/src/network/nm/dbus_types.cpp new file mode 100644 index 0000000..e161f11 --- /dev/null +++ b/src/network/nm/dbus_types.cpp @@ -0,0 +1,69 @@ +#include "dbus_types.hpp" + +#include +#include +#include +#include +#include +#include + +namespace qs::network { + +const QDBusArgument& operator>>(const QDBusArgument& argument, NMSettingsMap& map) { + argument.beginMap(); + while (!argument.atEnd()) { + argument.beginMapEntry(); + QString groupName; + argument >> groupName; + + QVariantMap group; + argument >> group; + + map.insert(groupName, group); + argument.endMapEntry(); + } + argument.endMap(); + return argument; +} + +const QDBusArgument& operator<<(QDBusArgument& argument, const NMSettingsMap& map) { + argument.beginMap(qMetaTypeId(), qMetaTypeId()); + for (auto it = map.constBegin(); it != map.constEnd(); ++it) { + argument.beginMapEntry(); + argument << it.key(); + argument << it.value(); + argument.endMapEntry(); + } + argument.endMap(); + return argument; +} + +const QDBusArgument& operator>>(const QDBusArgument& argument, NMIPv6Address& addr) { + argument.beginStructure(); + argument >> addr.address >> addr.prefix >> addr.gateway; + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator<<(QDBusArgument& argument, const NMIPv6Address& addr) { + argument.beginStructure(); + argument << addr.address << addr.prefix << addr.gateway; + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator>>(const QDBusArgument& argument, NMIPv6Route& route) { + argument.beginStructure(); + argument >> route.destination >> route.prefix >> route.nexthop >> route.metric; + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator<<(QDBusArgument& argument, const NMIPv6Route& route) { + argument.beginStructure(); + argument << route.destination << route.prefix << route.nexthop << route.metric; + argument.endStructure(); + return argument; +} + +} // namespace qs::network diff --git a/src/network/nm/dbus_types.hpp b/src/network/nm/dbus_types.hpp index dadbcf3..bf428e5 100644 --- a/src/network/nm/dbus_types.hpp +++ b/src/network/nm/dbus_types.hpp @@ -1,9 +1,40 @@ #pragma once -#include +#include +#include #include #include +#include #include -using ConnectionSettingsMap = QMap; -Q_DECLARE_METATYPE(ConnectionSettingsMap); +namespace qs::network { + +using NMSettingsMap = QMap; + +const QDBusArgument& operator>>(const QDBusArgument& argument, NMSettingsMap& map); +const QDBusArgument& operator<<(QDBusArgument& argument, const NMSettingsMap& map); + +struct NMIPv6Address { + QByteArray address; + quint32 prefix = 0; + QByteArray gateway; +}; + +const QDBusArgument& operator>>(const QDBusArgument& argument, qs::network::NMIPv6Address& addr); +const QDBusArgument& operator<<(QDBusArgument& argument, const qs::network::NMIPv6Address& addr); + +struct NMIPv6Route { + QByteArray destination; + quint32 prefix = 0; + QByteArray nexthop; + quint32 metric = 0; +}; + +const QDBusArgument& operator>>(const QDBusArgument& argument, qs::network::NMIPv6Route& route); +const QDBusArgument& operator<<(QDBusArgument& argument, const qs::network::NMIPv6Route& route); + +} // namespace qs::network + +Q_DECLARE_METATYPE(qs::network::NMSettingsMap); +Q_DECLARE_METATYPE(qs::network::NMIPv6Address); +Q_DECLARE_METATYPE(qs::network::NMIPv6Route); diff --git a/src/network/nm/device.cpp b/src/network/nm/device.cpp index aad565d..1f229c8 100644 --- a/src/network/nm/device.cpp +++ b/src/network/nm/device.cpp @@ -14,9 +14,10 @@ #include "../../core/logcat.hpp" #include "../../dbus/properties.hpp" -#include "../device.hpp" -#include "connection.hpp" +#include "active_connection.hpp" #include "dbus_nm_device.h" +#include "enums.hpp" +#include "settings.hpp" namespace qs::network { using namespace qs::dbus; @@ -39,19 +40,29 @@ NMDevice::NMDevice(const QString& path, QObject* parent): QObject(parent) { } // clang-format off - QObject::connect(this, &NMDevice::availableConnectionPathsChanged, this, &NMDevice::onAvailableConnectionPathsChanged); + QObject::connect(this, &NMDevice::availableSettingsPathsChanged, this, &NMDevice::onAvailableSettingsPathsChanged); QObject::connect(this, &NMDevice::activeConnectionPathChanged, this, &NMDevice::onActiveConnectionPathChanged); + QObject::connect(this->deviceProxy, &DBusNMDeviceProxy::StateChanged, this, &NMDevice::onStateChanged); // clang-format on this->deviceProperties.setInterface(this->deviceProxy); this->deviceProperties.updateAllViaGetAll(); } +void NMDevice::onStateChanged(quint32 newState, quint32 /*oldState*/, quint32 reason) { + auto enumReason = static_cast(reason); + auto enumNewState = static_cast(newState); + if (enumNewState == NMDeviceState::Failed) this->bLastFailReason = enumReason; + if (this->bStateReason == enumReason) return; + this->bStateReason = enumReason; +} + void NMDevice::onActiveConnectionPathChanged(const QDBusObjectPath& path) { const QString stringPath = path.path(); // Remove old active connection if (this->mActiveConnection) { + qCDebug(logNetworkManager) << "Active connection removed:" << this->mActiveConnection->path(); QObject::disconnect(this->mActiveConnection, nullptr, this, nullptr); delete this->mActiveConnection; this->mActiveConnection = nullptr; @@ -64,6 +75,7 @@ void NMDevice::onActiveConnectionPathChanged(const QDBusObjectPath& path) { qCWarning(logNetworkManager) << "Ignoring invalid registration of" << stringPath; delete active; } else { + qCDebug(logNetworkManager) << "Active connection added:" << stringPath; this->mActiveConnection = active; QObject::connect( active, @@ -76,42 +88,44 @@ void NMDevice::onActiveConnectionPathChanged(const QDBusObjectPath& path) { } } -void NMDevice::onAvailableConnectionPathsChanged(const QList& paths) { +void NMDevice::onAvailableSettingsPathsChanged(const QList& paths) { QSet newPathSet; for (const QDBusObjectPath& path: paths) { newPathSet.insert(path.path()); } - const auto existingPaths = this->mConnections.keys(); + const auto existingPaths = this->mSettings.keys(); const QSet existingPathSet(existingPaths.begin(), existingPaths.end()); - const auto addedConnections = newPathSet - existingPathSet; - const auto removedConnections = existingPathSet - newPathSet; + const auto addedSettings = newPathSet - existingPathSet; + const auto removedSettings = existingPathSet - newPathSet; - for (const QString& path: addedConnections) { - this->registerConnection(path); + for (const QString& path: addedSettings) { + this->registerSettings(path); } - for (const QString& path: removedConnections) { - auto* connection = this->mConnections.take(path); + for (const QString& path: removedSettings) { + auto* connection = this->mSettings.take(path); if (!connection) { qCDebug(logNetworkManager) << "Sent removal signal for" << path << "which is not registered."; } else { + qCDebug(logNetworkManager) << "Connection settings removed:" << path; delete connection; } }; } -void NMDevice::registerConnection(const QString& path) { - auto* connection = new NMConnectionSettings(path, this); - if (!connection->isValid()) { +void NMDevice::registerSettings(const QString& path) { + auto* settings = new NMSettings(path, this); + if (!settings->isValid()) { qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; - delete connection; + delete settings; } else { - this->mConnections.insert(path, connection); + qCDebug(logNetworkManager) << "Connection settings added:" << path; + this->mSettings.insert(path, settings); QObject::connect( - connection, - &NMConnectionSettings::loaded, + settings, + &NMSettings::loaded, this, - [this, connection]() { emit this->connectionLoaded(connection); }, + [this, settings]() { emit this->settingsLoaded(settings); }, Qt::SingleShotConnection ); } @@ -125,6 +139,12 @@ void NMDevice::setAutoconnect(bool autoconnect) { this->pAutoconnect.write(); } +void NMDevice::setManaged(bool managed) { + if (managed == this->bManaged) return; + this->bManaged = managed; + this->pManaged.write(); +} + bool NMDevice::isValid() const { return this->deviceProxy && this->deviceProxy->isValid(); } QString NMDevice::address() const { return this->deviceProxy ? this->deviceProxy->service() : QString(); diff --git a/src/network/nm/device.hpp b/src/network/nm/device.hpp index e3ff4b9..963f574 100644 --- a/src/network/nm/device.hpp +++ b/src/network/nm/device.hpp @@ -8,8 +8,10 @@ #include #include "../../dbus/properties.hpp" -#include "connection.hpp" +#include "../enums.hpp" +#include "active_connection.hpp" #include "dbus_nm_device.h" +#include "settings.hpp" namespace qs::dbus { @@ -36,43 +38,49 @@ public: [[nodiscard]] virtual bool isValid() const; [[nodiscard]] QString path() const; [[nodiscard]] QString address() const; - [[nodiscard]] QString interface() const { return this->bInterface; }; - [[nodiscard]] QString hwAddress() const { return this->bHwAddress; }; - [[nodiscard]] bool managed() const { return this->bManaged; }; - [[nodiscard]] NMDeviceState::Enum state() const { return this->bState; }; - [[nodiscard]] bool autoconnect() const { return this->bAutoconnect; }; - [[nodiscard]] NMActiveConnection* activeConnection() const { return this->mActiveConnection; }; + [[nodiscard]] QString interface() const { return this->bInterface; } + [[nodiscard]] QString hwAddress() const { return this->bHwAddress; } + [[nodiscard]] bool managed() const { return this->bManaged; } + [[nodiscard]] NMDeviceState::Enum state() const { return this->bState; } + [[nodiscard]] NMDeviceStateReason::Enum stateReason() const { return this->bStateReason; } + [[nodiscard]] NMDeviceStateReason::Enum lastFailReason() const { return this->bLastFailReason; } + [[nodiscard]] bool autoconnect() const { return this->bAutoconnect; } + [[nodiscard]] NMActiveConnection* activeConnection() const { return this->mActiveConnection; } signals: void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath); void addAndActivateConnection( - const ConnectionSettingsMap& settings, + const NMSettingsMap& settings, const QDBusObjectPath& devPath, const QDBusObjectPath& apPath ); - void connectionLoaded(NMConnectionSettings* connection); - void connectionRemoved(NMConnectionSettings* connection); - void availableConnectionPathsChanged(QList paths); + void settingsLoaded(NMSettings* settings); + void settingsRemoved(NMSettings* settings); + void availableSettingsPathsChanged(QList paths); void activeConnectionPathChanged(const QDBusObjectPath& connection); void activeConnectionLoaded(NMActiveConnection* active); void interfaceChanged(const QString& interface); void hwAddressChanged(const QString& hwAddress); void managedChanged(bool managed); void stateChanged(NMDeviceState::Enum state); + void stateReasonChanged(NMDeviceStateReason::Enum reason); + void lastFailReasonChanged(NMDeviceStateReason::Enum reason); void autoconnectChanged(bool autoconnect); public slots: void disconnect(); void setAutoconnect(bool autoconnect); + void setManaged(bool managed); private slots: - void onAvailableConnectionPathsChanged(const QList& paths); + void onStateChanged(quint32 newState, quint32 oldState, quint32 reason); + void onAvailableSettingsPathsChanged(const QList& paths); void onActiveConnectionPathChanged(const QDBusObjectPath& path); private: - void registerConnection(const QString& path); + void registerSettings(const QString& path); - QHash mConnections; + QHash mSettings; NMActiveConnection* mActiveConnection = nullptr; // clang-format off @@ -80,8 +88,10 @@ private: Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QString, bHwAddress, &NMDevice::hwAddressChanged); Q_OBJECT_BINDABLE_PROPERTY(NMDevice, bool, bManaged, &NMDevice::managedChanged); Q_OBJECT_BINDABLE_PROPERTY(NMDevice, NMDeviceState::Enum, bState, &NMDevice::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, NMDeviceStateReason::Enum, bStateReason, &NMDevice::stateReasonChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, NMDeviceStateReason::Enum, bLastFailReason, &NMDevice::lastFailReasonChanged); Q_OBJECT_BINDABLE_PROPERTY(NMDevice, bool, bAutoconnect, &NMDevice::autoconnectChanged); - Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QList, bAvailableConnections, &NMDevice::availableConnectionPathsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QList, bAvailableConnections, &NMDevice::availableSettingsPathsChanged); Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QDBusObjectPath, bActiveConnection, &NMDevice::activeConnectionPathChanged); QS_DBUS_BINDABLE_PROPERTY_GROUP(NMDeviceAdapter, deviceProperties); diff --git a/src/network/nm/enums.hpp b/src/network/nm/enums.hpp index 34e5b65..18b1b8b 100644 --- a/src/network/nm/enums.hpp +++ b/src/network/nm/enums.hpp @@ -7,6 +7,20 @@ namespace qs::network { +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMConnectivityState +class NMConnectivityState: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + None = 1, + Portal = 2, + Limited = 3, + Full = 4, + }; +}; + // Indicates the type of hardware represented by a device object. class NMDeviceType: public QObject { Q_OBJECT; @@ -52,6 +66,123 @@ public: Q_ENUM(Enum); }; +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState. +class NMDeviceState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Unmanaged = 10, + Unavailable = 20, + Disconnected = 30, + Prepare = 40, + Config = 50, + NeedAuth = 60, + IPConfig = 70, + IPCheck = 80, + Secondaries = 90, + Activated = 100, + Deactivating = 110, + Failed = 120, + }; + Q_ENUM(Enum); +}; + +// Device state change reason codes. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceStateReason. +class NMDeviceStateReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + None = 0, + Unknown = 1, + NowManaged = 2, + NowUnmanaged = 3, + ConfigFailed = 4, + IpConfigUnavailable = 5, + IpConfigExpired = 6, + NoSecrets = 7, + SupplicantDisconnect = 8, + SupplicantConfigFailed = 9, + SupplicantFailed = 10, + SupplicantTimeout = 11, + PppStartFailed = 12, + PppDisconnect = 13, + PppFailed = 14, + DhcpStartFailed = 15, + DhcpError = 16, + DhcpFailed = 17, + SharedStartFailed = 18, + SharedFailed = 19, + AutoIpStartFailed = 20, + AutoIpError = 21, + AutoIpFailed = 22, + ModemBusy = 23, + ModemNoDialTone = 24, + ModemNoCarrier = 25, + ModemDialTimeout = 26, + ModemDialFailed = 27, + ModemInitFailed = 28, + GsmApnFailed = 29, + GsmRegistrationNotSearching = 30, + GsmRegistrationDenied = 31, + GsmRegistrationTimeout = 32, + GsmRegistrationFailed = 33, + GsmPinCheckFailed = 34, + FirmwareMissing = 35, + Removed = 36, + Sleeping = 37, + ConnectionRemoved = 38, + UserRequested = 39, + Carrier = 40, + ConnectionAssumed = 41, + SupplicantAvailable = 42, + ModemNotFound = 43, + BtFailed = 44, + GsmSimNotInserted = 45, + GsmSimPinRequired = 46, + GsmSimPukRequired = 47, + GsmSimWrong = 48, + InfinibandMode = 49, + DependencyFailed = 50, + Br2684Failed = 51, + ModemManagerUnavailable = 52, + SsidNotFound = 53, + SecondaryConnectionFailed = 54, + DcbFcoeFailed = 55, + TeamdControlFailed = 56, + ModemFailed = 57, + ModemAvailable = 58, + SimPinIncorrect = 59, + NewActivation = 60, + ParentChanged = 61, + ParentManagedChanged = 62, + OvsdbFailed = 63, + IpAddressDuplicate = 64, + IpMethodUnsupported = 65, + SriovConfigurationFailed = 66, + PeerNotFound = 67, + DeviceHandlerFailed = 68, + UnmanagedByDefault = 69, + UnmanagedExternalDown = 70, + UnmanagedLinkNotInit = 71, + UnmanagedQuitting = 72, + UnmanagedManagerDisabled = 73, + UnmanagedUserConf = 74, + UnmanagedUserExplicit = 75, + UnmanagedUserSettings = 76, + UnmanagedUserUdev = 77, + NetworkingOff = 78, + }; + Q_ENUM(Enum); +}; + // 802.11 specific device encryption and authentication capabilities. // In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceWifiCapabilities. class NMWirelessCapabilities: public QObject { @@ -153,4 +284,31 @@ public: Q_ENUM(Enum); }; +/// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionStateReason. +class NMConnectionStateReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + None = 1, + UserDisconnected = 2, + DeviceDisconnected = 3, + ServiceStopped = 4, + IpConfigInvalid = 5, + ConnectTimeout = 6, + ServiceStartTimeout = 7, + ServiceStartFailed = 8, + NoSecrets = 9, + LoginFailed = 10, + ConnectionRemoved = 11, + DependencyFailed = 12, + DeviceRealizeFailed = 13, + DeviceRemoved = 14 + }; + Q_ENUM(Enum); +}; + } // namespace qs::network diff --git a/src/network/nm/org.freedesktop.NetworkManager.Device.xml b/src/network/nm/org.freedesktop.NetworkManager.Device.xml index 322635f..414d24f 100644 --- a/src/network/nm/org.freedesktop.NetworkManager.Device.xml +++ b/src/network/nm/org.freedesktop.NetworkManager.Device.xml @@ -1,5 +1,10 @@ + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml index 0283847..81419b9 100644 --- a/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml +++ b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml @@ -2,8 +2,18 @@ - + + + + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.xml b/src/network/nm/org.freedesktop.NetworkManager.xml index d4470ea..75c314a 100644 --- a/src/network/nm/org.freedesktop.NetworkManager.xml +++ b/src/network/nm/org.freedesktop.NetworkManager.xml @@ -1,5 +1,8 @@ + + + @@ -11,7 +14,7 @@ - + diff --git a/src/network/nm/settings.cpp b/src/network/nm/settings.cpp new file mode 100644 index 0000000..af36dae --- /dev/null +++ b/src/network/nm/settings.cpp @@ -0,0 +1,227 @@ +#include "settings.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "dbus_nm_connection_settings.h" +#include "dbus_types.hpp" +#include "utils.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network", QtWarningMsg); +QS_LOGGING_CATEGORY(logNMSettings, "quickshell.network.nm_settings", QtWarningMsg); +} // namespace + +NMSettings::NMSettings(const QString& path, QObject* parent): QObject(parent) { + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>>(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + + this->proxy = new DBusNMConnectionSettingsProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection settings at" + << path; + return; + } + + QObject::connect( + this->proxy, + &DBusNMConnectionSettingsProxy::Updated, + this, + &NMSettings::getSettings + ); + + this->bId.setBinding([this]() { return this->bSettings.value()["connection"]["id"].toString(); }); + this->bUuid.setBinding([this]() { + return this->bSettings.value()["connection"]["uuid"].toString(); + }); + + this->settingsProperties.setInterface(this->proxy); + this->settingsProperties.updateAllViaGetAll(); + + this->getSettings(); +} + +void NMSettings::getSettings() { + auto pending = this->proxy->GetSettings(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to get settings for" << this->path() << ":" << reply.error().message(); + } else { + auto settings = reply.value(); + manualSettingDemarshall(settings); + this->bSettings = settings; + qCDebug(logNetworkManager) << "Settings map updated for" << this->path(); + + if (!this->mLoaded) { + emit this->loaded(); + this->mLoaded = true; + } + }; + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +QDBusPendingCallWatcher* NMSettings::updateSettings( + const NMSettingsMap& settingsToChange, + const NMSettingsMap& settingsToRemove +) { + auto settings = removeSettingsInMap(this->bSettings, settingsToRemove); + settings = mergeSettingsMaps(settings, settingsToChange); + auto pending = this->proxy->Update(settings); + auto* call = new QDBusPendingCallWatcher(pending, this); + + return call; +} + +void NMSettings::clearSecrets() { + auto pending = this->proxy->ClearSecrets(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to clear secrets for" << this->path() << ":" << reply.error().message(); + } else { + qCDebug(logNetworkManager) << "Cleared secrets for" << this->path(); + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NMSettings::forget() { + auto pending = this->proxy->Delete(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to forget" << this->path() << ":" << reply.error().message(); + } else { + qCDebug(logNetworkManager) << "Successfully deletion of" << this->path(); + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +QVariantMap NMSettings::read() { + QVariantMap result; + const auto& settings = this->bSettings.value(); + for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) { + QVariantMap group; + for (auto jt = it.value().constBegin(); jt != it.value().constEnd(); ++jt) { + group.insert(jt.key(), settingTypeToQml(jt.value())); + } + result.insert(it.key(), group); + } + return result; +} + +void NMSettings::write(const QVariantMap& settings) { + NMSettingsMap changedSettings; + NMSettingsMap removedSettings; + QStringList failedSettings; + + for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) { + if (!it.value().canConvert()) continue; + + auto group = it.value().toMap(); + QVariantMap toChange; + QVariantMap toRemove; + for (auto jt = group.constBegin(); jt != group.constEnd(); ++jt) { + if (jt.value().isNull()) { + toRemove.insert(jt.key(), QVariant()); + } else { + auto converted = settingTypeFromQml(it.key(), jt.key(), jt.value()); + if (!converted.isValid()) failedSettings.append(it.key() + "." + jt.key()); + else toChange.insert(jt.key(), converted); + } + } + if (!toChange.isEmpty()) changedSettings.insert(it.key(), toChange); + if (!toRemove.isEmpty()) removedSettings.insert(it.key(), toRemove); + } + + if (!failedSettings.isEmpty()) { + qCWarning(logNMSettings) << "A write to" << this + << "has received bad types for the following settings:" + << failedSettings.join(", "); + } + + auto* call = this->updateSettings(changedSettings, removedSettings); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to update settings for" << this->path() << ":" << reply.error().message(); + } else { + qCDebug(logNMSettings) << "Successful write to" << this; + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +bool NMSettings::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMSettings::address() const { return this->proxy ? this->proxy->service() : QString(); } +QString NMSettings::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network + +QDebug operator<<(QDebug debug, const qs::network::NMSettings* settings) { + auto saver = QDebugStateSaver(debug); + + if (settings) { + debug.nospace() << "NMSettings(" << static_cast(settings) + << ", uuid=" << settings->uuid() << ")"; + } else { + debug << "WifiNetwork(nullptr)"; + } + + return debug; +} diff --git a/src/network/nm/settings.hpp b/src/network/nm/settings.hpp new file mode 100644 index 0000000..3a76c61 --- /dev/null +++ b/src/network/nm/settings.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "dbus_nm_connection_settings.h" +#include "dbus_types.hpp" + +namespace qs::network { + +// Proxy of a /org/freedesktop/NetworkManager/Settings/Connection/* object. +///! A NetworkManager connection settings profile. +class NMSettings: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + + /// The human-readable unique identifier for the connection. + Q_PROPERTY(QString id READ default NOTIFY idChanged BINDABLE bindableId); + /// A universally unique identifier for the connection. + Q_PROPERTY(QString uuid READ uuid NOTIFY uuidChanged BINDABLE bindableUuid); + +public: + explicit NMSettings(const QString& path, QObject* parent = nullptr); + + /// Clear all of the secrets belonging to the settings. + Q_INVOKABLE void clearSecrets(); + /// Delete the settings. + Q_INVOKABLE void forget(); + /// Update the connection with new settings and save the connection to disk. + /// Only changed fields need to be included. + /// Writing a setting to `null` will remove the setting or reset it to its default. + /// + /// > [!NOTE] Secrets may be part of the update request, + /// > and will be either stored in persistent storage or sent to a Secret Agent for storage, + /// > depending on the flags associated with each secret. + Q_INVOKABLE void write(const QVariantMap& settings); + /// Get the settings map describing this network configuration. + /// + /// > [!NOTE] This will never include any secrets required for connection to the network, as those are often protected. + Q_INVOKABLE QVariantMap read(); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] NMSettingsMap map() { return this->bSettings; } + QDBusPendingCallWatcher* + updateSettings(const NMSettingsMap& settingsToChange, const NMSettingsMap& settingsToRemove = {}); + QBindable bindableId() { return &this->bId; } + [[nodiscard]] QString uuid() const { return this->bUuid; } + QBindable bindableUuid() { return &this->bUuid; } + +signals: + void loaded(); + void settingsChanged(NMSettingsMap settings); + void idChanged(QString id); + void uuidChanged(QString uuid); + +private: + bool mLoaded = false; + + void getSettings(); + + Q_OBJECT_BINDABLE_PROPERTY(NMSettings, NMSettingsMap, bSettings, &NMSettings::settingsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMSettings, QString, bId, &NMSettings::idChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMSettings, QString, bUuid, &NMSettings::uuidChanged); + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMSettings, settingsProperties); + DBusNMConnectionSettingsProxy* proxy = nullptr; +}; + +} // namespace qs::network + +QDebug operator<<(QDebug debug, const qs::network::NMSettings* settings); diff --git a/src/network/nm/utils.cpp b/src/network/nm/utils.cpp index 0be29e5..afdc796 100644 --- a/src/network/nm/utils.cpp +++ b/src/network/nm/utils.cpp @@ -1,27 +1,28 @@ #include "utils.hpp" - +#include // We depend on non-std Linux extensions that ctime doesn't put in the global namespace // NOLINTNEXTLINE(modernize-deprecated-headers) #include #include #include +#include #include #include #include #include -#include "../wifi.hpp" +#include "../enums.hpp" #include "dbus_types.hpp" #include "enums.hpp" namespace qs::network { -WifiSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings) { - const QVariantMap& security = settings.value("802-11-wireless-security"); - if (security.isEmpty()) { - return WifiSecurityType::Open; - }; +WifiSecurityType::Enum securityFromSettingsMap(const NMSettingsMap& settings) { + const QString mapName = "802-11-wireless-security"; + if (!settings.contains(mapName)) return WifiSecurityType::Unknown; + const QVariantMap& security = settings.value(mapName); + if (security.isEmpty()) return WifiSecurityType::Open; const QString keyMgmt = security["key-mgmt"].toString(); const QString authAlg = security["auth-alg"].toString(); @@ -45,6 +46,8 @@ WifiSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMa return WifiSecurityType::Sae; } else if (keyMgmt == "wpa-eap-suite-b-192") { return WifiSecurityType::Wpa3SuiteB192; + } else if (keyMgmt == "owe") { + return WifiSecurityType::Owe; } return WifiSecurityType::Open; } @@ -224,6 +227,280 @@ WifiSecurityType::Enum findBestWirelessSecurity( return WifiSecurityType::Unknown; } +NMSettingsMap mergeSettingsMaps(const NMSettingsMap& target, const NMSettingsMap& source) { + NMSettingsMap result = target; + for (auto iter = source.constBegin(); iter != source.constEnd(); ++iter) { + result[iter.key()].insert(iter.value()); + } + return result; +} + +NMSettingsMap removeSettingsInMap(const NMSettingsMap& target, const NMSettingsMap& toRemove) { + NMSettingsMap result = target; + for (auto iter = toRemove.constBegin(); iter != toRemove.constEnd(); ++iter) { + const QString& group = iter.key(); + const QVariantMap& keysToRemove = iter.value(); + + if (!result.contains(group)) continue; + + for (auto jt = keysToRemove.constBegin(); jt != keysToRemove.constEnd(); ++jt) { + result[group].remove(jt.key()); + } + + // Remove the group entirely if it's now empty + if (result[group].isEmpty()) { + result.remove(group); + } + } + return result; +} + +// Some NMSettingsMap settings remain QDBusArguments after autodemarshalling. +// Manually demarshall these for any complex signature we have registered. +void manualSettingDemarshall(NMSettingsMap& map) { + auto demarshallValue = [](const QVariant& value) -> QVariant { + if (value.userType() != qMetaTypeId()) { + return value; + } + + auto arg = value.value(); + auto signature = arg.currentSignature(); + + if (signature == "ay") return QVariant::fromValue(qdbus_cast(arg)); + if (signature == "aay") return QVariant::fromValue(qdbus_cast>(arg)); + if (signature == "au") return QVariant::fromValue(qdbus_cast>(arg)); + if (signature == "aau") return QVariant::fromValue(qdbus_cast>>(arg)); + if (signature == "aa{sv}") return QVariant::fromValue(qdbus_cast>(arg)); + if (signature == "a(ayuay)") return QVariant::fromValue(qdbus_cast>(arg)); + if (signature == "a(ayuayu)") return QVariant::fromValue(qdbus_cast>(arg)); + + return value; + }; + + for (auto it = map.begin(); it != map.end(); ++it) + for (auto jt = it.value().begin(); jt != it.value().end(); ++jt) + jt.value() = demarshallValue(jt.value()); +} + +// Some NMSettingsMap setting types can't be expressed in QML. +// Convert these settings to their correct type or return an invalid QVariant. +QVariant settingTypeFromQml(const QString& group, const QString& key, const QVariant& value) { + auto s = group + "." + key; + + // QString -> QByteArray + if (s == "802-1x.ca-cert" || s == "802-1x.client-cert" || s == "802-1x.private-key" + || s == "802-1x.password-raw" || s == "802-1x.phase2-ca-cert" + || s == "802-1x.phase2-client-cert" || s == "802-1x.phase2-private-key" + || s == "802-11-wireless.ssid") + { + if (value.typeId() == QMetaType::QString) { + return value.toString().toUtf8(); + } + if (value.typeId() == QMetaType::QByteArray) { + return value; + } + return QVariant(); + } + + // QVariantList -> QList + if (s == "ipv6.dns") { + if (value.typeId() == QMetaType::QVariantList) { + QList r; + for (const auto& v: value.toList()) { + if (v.typeId() == QMetaType::QString) { + r.append(v.toString().toUtf8()); + } else { + r.append(v.toByteArray()); + } + } + return QVariant::fromValue(r); + } + return QVariant(); + } + + // QVariantList -> QList + if (s == "ipv4.dns") { + if (value.typeId() == QMetaType::QVariantList) { + QList r; + for (const auto& v: value.toList()) { + r.append(v.value()); + } + return QVariant::fromValue(r); + } + return QVariant(); + } + + // QVariantList -> QList> + if (s == "ipv4.addresses" || s == "ipv4.routes") { + if (value.typeId() == QMetaType::QVariantList) { + QList> r; + for (const auto& v: value.toList()) { + if (v.typeId() != QMetaType::QVariantList) { + continue; + } + QList inner; + for (const auto& u: v.toList()) { + inner.append(u.value()); + } + r.append(inner); + } + return QVariant::fromValue(r); + } + return QVariant(); + } + + // QVariantList -> QList + if (s == "ipv4.address-data" || s == "ipv4.route-data" || s == "ipv4.routing-rules" + || s == "ipv6.address-data" || s == "ipv6.route-data" || s == "ipv6.routing-rules") + { + if (value.typeId() == QMetaType::QVariantList) { + QList r; + for (const auto& v: value.toList()) { + if (!v.canConvert()) { + continue; + } + r.append(v.toMap()); + } + return QVariant::fromValue(r); + } + return QVariant(); + } + + // QVariantList -> QList + if (s == "ipv6.addresses") { + if (value.typeId() == QMetaType::QVariantList) { + QList r; + for (const auto& v: value.toList()) { + if (v.typeId() != QMetaType::QVariantList) { + continue; + } + auto fields = v.toList(); + if (fields.size() != 3) { + continue; + } + const QByteArray address = fields[0].typeId() == QMetaType::QString + ? fields[0].toString().toUtf8() + : fields[0].toByteArray(); + const QByteArray gateway = fields[2].typeId() == QMetaType::QString + ? fields[2].toString().toUtf8() + : fields[2].toByteArray(); + r.append({.address = address, .prefix = fields[1].value(), .gateway = gateway}); + } + return QVariant::fromValue(r); + } + return QVariant(); + } + + // QVariantList -> QList + if (s == "ipv6.routes") { + if (value.typeId() == QMetaType::QVariantList) { + QList r; + for (const auto& v: value.toList()) { + if (v.typeId() != QMetaType::QVariantList) { + continue; + } + auto fields = v.toList(); + if (fields.size() != 4) { + continue; + } + const QByteArray destination = fields[0].typeId() == QMetaType::QString + ? fields[0].toString().toUtf8() + : fields[0].toByteArray(); + const QByteArray nexthop = fields[2].typeId() == QMetaType::QString + ? fields[2].toString().toUtf8() + : fields[2].toByteArray(); + r.append( + {.destination = destination, + .prefix = fields[1].value(), + .nexthop = nexthop, + .metric = fields[3].value()} + ); + } + return QVariant::fromValue(r); + } + return QVariant(); + } + + // QVariantList -> QStringList + if (s == "connection.permissions" || s == "ipv4.dns-search" || s == "ipv6.dns-search" + || s == "802-11-wireless.seen-bssids") + { + if (value.typeId() == QMetaType::QVariantList) { + QStringList stringList; + for (const auto& item: value.toList()) { + stringList.append(item.toString()); + } + return stringList; + } + return QVariant(); + } + + // double (whole number) -> qint32 + if (value.typeId() == QMetaType::Double) { + auto num = value.toDouble(); + if (std::isfinite(num) && num == std::trunc(num)) { + return QVariant::fromValue(static_cast(num)); + } + } + + return value; +} + +// Some NMSettingsMap setting types must be converted to a type that is supported by QML. +// Although QByteArrays can be represented in QML, we convert them to strings for convenience. +QVariant settingTypeToQml(const QVariant& value) { + // QByteArray -> QString + if (value.typeId() == QMetaType::QByteArray) { + return QString::fromUtf8(value.toByteArray()); + } + + // QList -> QVariantList + if (value.userType() == qMetaTypeId>()) { + QVariantList out; + for (const auto& ba: value.value>()) { + out.append(QString::fromUtf8(ba)); + } + return out; + } + + // QList -> QVariantList + if (value.userType() == qMetaTypeId>()) { + QVariantList out; + for (const auto& addr: value.value>()) { + out.append( + QVariant::fromValue( + QVariantList { + QString::fromUtf8(addr.address), + addr.prefix, + QString::fromUtf8(addr.gateway), + } + ) + ); + } + return out; + } + + // QList -> QVariantList + if (value.userType() == qMetaTypeId>()) { + QVariantList out; + for (const auto& route: value.value>()) { + out.append( + QVariant::fromValue( + QVariantList { + QString::fromUtf8(route.destination), + route.prefix, + QString::fromUtf8(route.nexthop), + route.metric, + } + ) + ); + } + return out; + } + + return value; +} + // NOLINTBEGIN QDateTime clockBootTimeToDateTime(qint64 clockBootTime) { clockid_t clkId = CLOCK_BOOTTIME; diff --git a/src/network/nm/utils.hpp b/src/network/nm/utils.hpp index ce8b784..8c51423 100644 --- a/src/network/nm/utils.hpp +++ b/src/network/nm/utils.hpp @@ -3,15 +3,14 @@ #include #include #include -#include -#include "../wifi.hpp" +#include "../enums.hpp" #include "dbus_types.hpp" #include "enums.hpp" namespace qs::network { -WifiSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings); +WifiSecurityType::Enum securityFromSettingsMap(const NMSettingsMap& settings); bool deviceSupportsApCiphers( NMWirelessCapabilities::Enum caps, @@ -40,6 +39,16 @@ WifiSecurityType::Enum findBestWirelessSecurity( NM80211ApSecurityFlags::Enum apRsn ); +NMSettingsMap mergeSettingsMaps(const NMSettingsMap& target, const NMSettingsMap& source); + +NMSettingsMap removeSettingsInMap(const NMSettingsMap& target, const NMSettingsMap& toRemove); + +void manualSettingDemarshall(NMSettingsMap& map); + +QVariant settingTypeFromQml(const QString& group, const QString& key, const QVariant& value); + +QVariant settingTypeToQml(const QVariant& value); + QDateTime clockBootTimeToDateTime(qint64 clockBootTime); } // namespace qs::network diff --git a/src/network/nm/wireless.cpp b/src/network/nm/wireless.cpp index 9dff14b..5f55bed 100644 --- a/src/network/nm/wireless.cpp +++ b/src/network/nm/wireless.cpp @@ -1,6 +1,7 @@ #include "wireless.hpp" #include +#include #include #include #include @@ -11,20 +12,23 @@ #include #include #include +#include #include #include #include +#include #include "../../core/logcat.hpp" #include "../../dbus/properties.hpp" -#include "../network.hpp" +#include "../enums.hpp" #include "../wifi.hpp" #include "accesspoint.hpp" -#include "connection.hpp" +#include "active_connection.hpp" #include "dbus_nm_wireless.h" #include "dbus_types.hpp" #include "device.hpp" #include "enums.hpp" +#include "settings.hpp" #include "utils.hpp" namespace qs::network { @@ -42,38 +46,43 @@ NMWirelessNetwork::NMWirelessNetwork(QString ssid, QObject* parent) , bReason(NMConnectionStateReason::None) , bState(NMConnectionState::Deactivated) {} -void NMWirelessNetwork::updateReferenceConnection() { +void NMWirelessNetwork::updateReferenceSettings() { // If the network has no connections, the reference is nullptr. - if (this->mConnections.isEmpty()) { - this->mReferenceConn = nullptr; + if (this->mSettings.isEmpty()) { + this->mReferenceSettings = nullptr; this->bSecurity = WifiSecurityType::Unknown; - // Set security back to reference AP. if (this->mReferenceAp) { this->bSecurity.setBinding([this]() { return this->mReferenceAp->security(); }); } return; }; - // If the network has an active connection, use it as the reference. + // If the network has an active connection, use its settings as the reference. if (this->mActiveConnection) { - auto* conn = this->mConnections.value(this->mActiveConnection->connection().path()); - if (conn && conn != this->mReferenceConn) { - this->mReferenceConn = conn; - this->bSecurity.setBinding([conn]() { return conn->security(); }); + auto* settings = this->mSettings.value(this->mActiveConnection->connection().path()); + if (settings && settings != this->mReferenceSettings) { + this->mReferenceSettings = settings; + this->bSecurity.setBinding([settings]() { return securityFromSettingsMap(settings->map()); }); } return; } - // Otherwise, choose the connection with the strongest security settings. - NMConnectionSettings* selectedConn = nullptr; - for (auto* conn: this->mConnections.values()) { - if (!selectedConn || conn->security() > selectedConn->security()) { - selectedConn = conn; + // Otherwise, choose the settings responsible for the last successful connection. + NMSettings* selectedSettings = nullptr; + quint64 selectedTimestamp = 0; + for (auto* settings: this->mSettings.values()) { + const quint64 timestamp = settings->map()["connection"]["timestamp"].toULongLong(); + if (!selectedSettings || timestamp > selectedTimestamp) { + selectedSettings = settings; + selectedTimestamp = timestamp; } } - if (this->mReferenceConn != selectedConn) { - this->mReferenceConn = selectedConn; - this->bSecurity.setBinding([selectedConn]() { return selectedConn->security(); }); + + if (this->mReferenceSettings != selectedSettings) { + this->mReferenceSettings = selectedSettings; + this->bSecurity.setBinding([selectedSettings]() { + return securityFromSettingsMap(selectedSettings->map()); + }); } } @@ -101,7 +110,7 @@ void NMWirelessNetwork::updateReferenceAp() { this->mReferenceAp = selectedAp; this->bSignalStrength.setBinding([selectedAp]() { return selectedAp->signalStrength(); }); // Reference AP is used for security when there's no connection settings. - if (!this->mReferenceConn) { + if (!this->mReferenceSettings) { this->bSecurity.setBinding([selectedAp]() { return selectedAp->security(); }); } } @@ -113,7 +122,7 @@ void NMWirelessNetwork::addAccessPoint(NMAccessPoint* ap) { auto onDestroyed = [this, ap]() { if (this->mAccessPoints.take(ap->path())) { this->updateReferenceAp(); - if (this->mAccessPoints.isEmpty() && this->mConnections.isEmpty()) emit this->disappeared(); + if (this->mAccessPoints.isEmpty() && this->mSettings.isEmpty()) emit this->disappeared(); } }; // clang-format off @@ -123,44 +132,45 @@ void NMWirelessNetwork::addAccessPoint(NMAccessPoint* ap) { this->updateReferenceAp(); }; -void NMWirelessNetwork::addConnection(NMConnectionSettings* conn) { - if (this->mConnections.contains(conn->path())) return; - this->mConnections.insert(conn->path(), conn); - auto onDestroyed = [this, conn]() { - if (this->mConnections.take(conn->path())) { - this->updateReferenceConnection(); - if (this->mConnections.isEmpty()) this->bKnown = false; - if (this->mAccessPoints.isEmpty() && this->mConnections.isEmpty()) emit this->disappeared(); +void NMWirelessNetwork::addSettings(NMSettings* settings) { + if (this->mSettings.contains(settings->path())) return; + this->mSettings.insert(settings->path(), settings); + + auto onDestroyed = [this, settings]() { + if (this->mSettings.take(settings->path())) { + emit this->settingsRemoved(settings); + this->updateReferenceSettings(); + if (this->mSettings.isEmpty()) this->bKnown = false; + if (this->mAccessPoints.isEmpty() && this->mSettings.isEmpty()) emit this->disappeared(); } }; - // clang-format off - QObject::connect(conn, &NMConnectionSettings::securityChanged, this, &NMWirelessNetwork::updateReferenceConnection); - QObject::connect(conn, &NMConnectionSettings::destroyed, this, onDestroyed); - // clang-format on + QObject::connect(settings, &NMSettings::destroyed, this, onDestroyed); this->bKnown = true; - this->updateReferenceConnection(); + this->updateReferenceSettings(); + emit this->settingsAdded(settings); }; void NMWirelessNetwork::addActiveConnection(NMActiveConnection* active) { if (this->mActiveConnection) return; this->mActiveConnection = active; + this->bState.setBinding([active]() { return active->state(); }); this->bReason.setBinding([active]() { return active->stateReason(); }); auto onDestroyed = [this, active]() { if (this->mActiveConnection && this->mActiveConnection == active) { this->mActiveConnection = nullptr; - this->updateReferenceConnection(); + this->updateReferenceSettings(); this->bState = NMConnectionState::Deactivated; this->bReason = NMConnectionStateReason::None; } }; QObject::connect(active, &NMActiveConnection::destroyed, this, onDestroyed); - this->updateReferenceConnection(); + this->updateReferenceSettings(); }; void NMWirelessNetwork::forget() { - if (this->mConnections.isEmpty()) return; - for (auto* conn: this->mConnections.values()) { + if (this->mSettings.isEmpty()) return; + for (auto* conn: this->mSettings.values()) { conn->forget(); } } @@ -200,7 +210,7 @@ void NMWirelessDevice::initWireless() { QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointAdded, this, &NMWirelessDevice::onAccessPointAdded); QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointRemoved, this, &NMWirelessDevice::onAccessPointRemoved); QObject::connect(this, &NMWirelessDevice::accessPointLoaded, this, &NMWirelessDevice::onAccessPointLoaded); - QObject::connect(this, &NMWirelessDevice::connectionLoaded, this, &NMWirelessDevice::onConnectionLoaded); + QObject::connect(this, &NMWirelessDevice::settingsLoaded, this, &NMWirelessDevice::onSettingsLoaded); QObject::connect(this, &NMWirelessDevice::activeConnectionLoaded, this, &NMWirelessDevice::onActiveConnectionLoaded); QObject::connect(this, &NMWirelessDevice::scanningChanged, this, &NMWirelessDevice::onScanningChanged); // clang-format on @@ -218,6 +228,7 @@ void NMWirelessDevice::onAccessPointRemoved(const QDBusObjectPath& path) { << "which is not registered."; return; } + qCDebug(logNetworkManager) << "Access point removed:" << path.path(); delete ap; } @@ -233,28 +244,26 @@ void NMWirelessDevice::onAccessPointLoaded(NMAccessPoint* ap) { } } -void NMWirelessDevice::onConnectionLoaded(NMConnectionSettings* conn) { - const ConnectionSettingsMap& settings = conn->settings(); +void NMWirelessDevice::onSettingsLoaded(NMSettings* settings) { + const NMSettingsMap& map = settings->map(); // Filter connections that aren't wireless or have missing settings - if (settings["connection"]["id"].toString().isEmpty() - || settings["connection"]["uuid"].toString().isEmpty() - || !settings.contains("802-11-wireless") - || settings["802-11-wireless"]["ssid"].toString().isEmpty()) + if (map["connection"]["id"].toString().isEmpty() || map["connection"]["uuid"].toString().isEmpty() + || !map.contains("802-11-wireless") || map["802-11-wireless"]["ssid"].toString().isEmpty()) { return; } - const auto ssid = settings["802-11-wireless"]["ssid"].toString(); - const auto mode = settings["802-11-wireless"]["mode"].toString(); + const auto ssid = map["802-11-wireless"]["ssid"].toString(); + const auto mode = map["802-11-wireless"]["mode"].toString(); if (mode == "infrastructure") { auto* net = this->mNetworks.value(ssid); if (!net) net = this->registerNetwork(ssid); - net->addConnection(conn); + net->addSettings(settings); // Check for active connections that loaded before their respective connection settings auto* active = this->activeConnection(); - if (active && conn->path() == active->connection().path()) { + if (active && settings->path() == active->connection().path()) { net->addActiveConnection(active); } } @@ -265,8 +274,8 @@ void NMWirelessDevice::onActiveConnectionLoaded(NMActiveConnection* active) { // Find an exisiting network with connection settings that matches the active const QString activeConnPath = active->connection().path(); for (const auto& net: this->mNetworks.values()) { - for (auto* conn: net->connections()) { - if (activeConnPath == conn->path()) { + for (auto* settings: net->settings()) { + if (activeConnPath == settings->path()) { net->addActiveConnection(active); return; } @@ -334,6 +343,7 @@ void NMWirelessDevice::registerAccessPoint(const QString& path) { return; } + qCDebug(logNetworkManager) << "Access point added:" << path; this->mAccessPoints.insert(path, ap); QObject::connect( ap, @@ -356,22 +366,18 @@ void NMWirelessDevice::registerAccessPoint(const QString& path) { NMWirelessNetwork* NMWirelessDevice::registerNetwork(const QString& ssid) { auto* net = new NMWirelessNetwork(ssid, this); - // To avoid exposing outdated state to the frontend, filter the backend networks to only show - // the known or currently connected networks when the scanner is off. auto visible = [this, net]() { return this->bScanning || net->state() == NMConnectionState::Activated || net->known(); }; - auto onVisibilityChanged = [this, net](bool visible) { - visible ? this->registerFrontendNetwork(net) : this->removeFrontendNetwork(net); - }; net->bindableVisible().setBinding(visible); net->bindableActiveApPath().setBinding([this]() { return this->activeApPath().path(); }); + net->bindableDeviceFailReason().setBinding([this]() { return this->lastFailReason(); }); QObject::connect(net, &NMWirelessNetwork::disappeared, this, &NMWirelessDevice::removeNetwork); - QObject::connect(net, &NMWirelessNetwork::visibilityChanged, this, onVisibilityChanged); + qCDebug(logNetworkManager) << "Registered network for SSID" << ssid; this->mNetworks.insert(ssid, net); - if (net->visible()) this->registerFrontendNetwork(net); + this->registerFrontendNetwork(net); return net; } @@ -385,46 +391,137 @@ void NMWirelessDevice::registerFrontendNetwork(NMWirelessNetwork* net) { frontendNet->bindableSignalStrength().setBinding(translateSignal); frontendNet->bindableConnected().setBinding(translateState); frontendNet->bindableKnown().setBinding([net]() { return net->known(); }); - frontendNet->bindableNmReason().setBinding([net]() { return net->reason(); }); frontendNet->bindableSecurity().setBinding([net]() { return net->security(); }); frontendNet->bindableState().setBinding([net]() { - return static_cast(net->state()); + return static_cast(net->state()); + }); + + QObject::connect(net, &NMWirelessNetwork::reasonChanged, this, [net, frontendNet]() { + if (net->reason() == NMConnectionStateReason::DeviceDisconnected) { + auto deviceReason = net->deviceFailReason(); + if (deviceReason == NMDeviceStateReason::NoSecrets) + emit frontendNet->connectionFailed(ConnectionFailReason::NoSecrets); + if (deviceReason == NMDeviceStateReason::SupplicantDisconnect) + emit frontendNet->connectionFailed(ConnectionFailReason::WifiClientDisconnected); + if (deviceReason == NMDeviceStateReason::SupplicantFailed) + emit frontendNet->connectionFailed(ConnectionFailReason::WifiClientFailed); + if (deviceReason == NMDeviceStateReason::SupplicantTimeout) + emit frontendNet->connectionFailed(ConnectionFailReason::WifiAuthTimeout); + if (deviceReason == NMDeviceStateReason::SsidNotFound) + emit frontendNet->connectionFailed(ConnectionFailReason::WifiNetworkLost); + } }); QObject::connect(frontendNet, &WifiNetwork::requestConnect, this, [this, net]() { - if (net->referenceConnection()) { + if (net->referenceSettings()) { emit this->activateConnection( - QDBusObjectPath(net->referenceConnection()->path()), + QDBusObjectPath(net->referenceSettings()->path()), QDBusObjectPath(this->path()) ); return; } if (net->referenceAp()) { emit this->addAndActivateConnection( - ConnectionSettingsMap(), + NMSettingsMap(), QDBusObjectPath(this->path()), QDBusObjectPath(net->referenceAp()->path()) ); + return; } + qCInfo(logNetworkManager) << "Failed to connect to" + << this->path() + ": The network disappeared."; }); QObject::connect( frontendNet, - &WifiNetwork::requestDisconnect, + &WifiNetwork::requestConnectWithPsk, this, - &NMWirelessDevice::disconnect + [this, net](const QString& psk) { + NMSettingsMap settings; + settings["802-11-wireless-security"]["psk"] = psk; + if (const QPointer ref = net->referenceSettings()) { + auto* call = ref->updateSettings(settings); + QObject::connect( + call, + &QDBusPendingCallWatcher::finished, + this, + [this, ref](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCInfo(logNetworkManager) + << "Failed to write PSK for" << this->path() + ":" << reply.error().message(); + } else { + if (!ref) { + qCInfo(logNetworkManager) << "Failed to connectWithPsk to" + << this->path() + ": The settings disappeared."; + } else { + emit this->activateConnection( + QDBusObjectPath(ref->path()), + QDBusObjectPath(this->path()) + ); + } + } + delete call; + } + ); + return; + } + if (net->referenceAp()) { + emit this->addAndActivateConnection( + settings, + QDBusObjectPath(this->path()), + QDBusObjectPath(net->referenceAp()->path()) + ); + return; + } + qCInfo(logNetworkManager) << "Failed to connectWithPsk to" + << this->path() + ": The network disappeared."; + } ); + QObject::connect( + frontendNet, + &WifiNetwork::requestConnectWithSettings, + this, + [this](NMSettings* settings) { + if (settings) { + emit this->activateConnection( + QDBusObjectPath(settings->path()), + QDBusObjectPath(this->path()) + ); + return; + } + qCInfo(logNetworkManager) << "Failed to connectWithSettings to" + << this->path() + ": The provided settings no longer exist."; + } + ); + + QObject::connect( + net, + &NMWirelessNetwork::visibilityChanged, + this, + [this, frontendNet](bool visible) { + if (visible) this->networkAdded(frontendNet); + else this->networkRemoved(frontendNet); + } + ); + + // clang-format off + QObject::connect(frontendNet, &WifiNetwork::requestDisconnect, this, &NMWirelessDevice::disconnect); QObject::connect(frontendNet, &WifiNetwork::requestForget, net, &NMWirelessNetwork::forget); + QObject::connect(net, &NMWirelessNetwork::settingsAdded, frontendNet, &WifiNetwork::settingsAdded); + QObject::connect(net, &NMWirelessNetwork::settingsRemoved, frontendNet, &WifiNetwork::settingsRemoved); + // clang-format on this->mFrontendNetworks.insert(ssid, frontendNet); - emit this->networkAdded(frontendNet); + if (net->visible()) emit this->networkAdded(frontendNet); } void NMWirelessDevice::removeFrontendNetwork(NMWirelessNetwork* net) { auto* frontendNet = this->mFrontendNetworks.take(net->ssid()); if (frontendNet) { - emit this->networkRemoved(frontendNet); + if (net->visible()) emit this->networkRemoved(frontendNet); frontendNet->deleteLater(); } } diff --git a/src/network/nm/wireless.hpp b/src/network/nm/wireless.hpp index fe4010e..94ce754 100644 --- a/src/network/nm/wireless.hpp +++ b/src/network/nm/wireless.hpp @@ -9,10 +9,11 @@ #include "../wifi.hpp" #include "accesspoint.hpp" -#include "connection.hpp" +#include "active_connection.hpp" #include "dbus_nm_wireless.h" #include "device.hpp" #include "enums.hpp" +#include "settings.hpp" namespace qs::dbus { template <> @@ -32,7 +33,7 @@ struct DBusDataTransform { } // namespace qs::dbus namespace qs::network { -// NMWirelessNetwork aggregates all related NMActiveConnection, NMAccessPoint, and NMConnectionSetting objects. +// NMWirelessNetwork aggregates all related NMActiveConnection, NMAccessPoint, and NMSettings objects. class NMWirelessNetwork: public QObject { Q_OBJECT; @@ -40,46 +41,51 @@ public: explicit NMWirelessNetwork(QString ssid, QObject* parent = nullptr); void addAccessPoint(NMAccessPoint* ap); - void addConnection(NMConnectionSettings* conn); + void addSettings(NMSettings* settings); void addActiveConnection(NMActiveConnection* active); void forget(); - [[nodiscard]] QString ssid() const { return this->mSsid; }; - [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; - [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; }; - [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; }; - [[nodiscard]] bool known() const { return this->bKnown; }; - [[nodiscard]] NMConnectionStateReason::Enum reason() const { return this->bReason; }; - [[nodiscard]] NMAccessPoint* referenceAp() const { return this->mReferenceAp; }; - [[nodiscard]] NMConnectionSettings* referenceConnection() const { return this->mReferenceConn; }; - [[nodiscard]] QList accessPoints() const { return this->mAccessPoints.values(); }; - [[nodiscard]] QList connections() const { - return this->mConnections.values(); - } - [[nodiscard]] QBindable bindableActiveApPath() { return &this->bActiveApPath; }; - [[nodiscard]] QBindable bindableVisible() { return &this->bVisible; }; - [[nodiscard]] bool visible() const { return this->bVisible; }; + // clang-format off + [[nodiscard]] QString ssid() const { return this->mSsid; } + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; } + [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; } + [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; } + [[nodiscard]] bool known() const { return this->bKnown; } + [[nodiscard]] NMConnectionStateReason::Enum reason() const { return this->bReason; } + QBindable bindableDeviceFailReason() { return &this->bDeviceFailReason; } + [[nodiscard]] NMDeviceStateReason::Enum deviceFailReason() const { return this->bDeviceFailReason; } + [[nodiscard]] NMAccessPoint* referenceAp() const { return this->mReferenceAp; } + [[nodiscard]] QList accessPoints() const { return this->mAccessPoints.values(); } + [[nodiscard]] QList settings() const { return this->mSettings.values(); } + [[nodiscard]] NMSettings* referenceSettings() const { return this->mReferenceSettings; } + QBindable bindableActiveApPath() { return &this->bActiveApPath; } + QBindable bindableVisible() { return &this->bVisible; } + bool visible() const { return this->bVisible; } + // clang-format on signals: void disappeared(); + void settingsAdded(NMSettings* settings); + void settingsRemoved(NMSettings* settings); void visibilityChanged(bool visible); void signalStrengthChanged(quint8 signal); void stateChanged(NMConnectionState::Enum state); void knownChanged(bool known); void securityChanged(WifiSecurityType::Enum security); void reasonChanged(NMConnectionStateReason::Enum reason); + void deviceFailReasonChanged(NMDeviceStateReason::Enum reason); void capabilitiesChanged(NMWirelessCapabilities::Enum caps); void activeApPathChanged(QString path); private: void updateReferenceAp(); - void updateReferenceConnection(); + void updateReferenceSettings(); QString mSsid; QHash mAccessPoints; - QHash mConnections; + QHash mSettings; NMAccessPoint* mReferenceAp = nullptr; - NMConnectionSettings* mReferenceConn = nullptr; + NMSettings* mReferenceSettings = nullptr; NMActiveConnection* mActiveConnection = nullptr; // clang-format off @@ -88,6 +94,7 @@ private: Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, WifiSecurityType::Enum, bSecurity, &NMWirelessNetwork::securityChanged); Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMConnectionStateReason::Enum, bReason, &NMWirelessNetwork::reasonChanged); Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMConnectionState::Enum, bState, &NMWirelessNetwork::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMDeviceStateReason::Enum, bDeviceFailReason, &NMWirelessNetwork::deviceFailReasonChanged); Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, quint8, bSignalStrength, &NMWirelessNetwork::signalStrengthChanged); Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, QString, bActiveApPath, &NMWirelessNetwork::activeApPathChanged); // clang-format on @@ -103,10 +110,10 @@ public: explicit NMWirelessDevice(const QString& path, QObject* parent = nullptr); [[nodiscard]] bool isValid() const override; - [[nodiscard]] NMWirelessCapabilities::Enum capabilities() { return this->bCapabilities; }; - [[nodiscard]] const QDBusObjectPath& activeApPath() { return this->bActiveAccessPoint; }; - [[nodiscard]] NM80211Mode::Enum mode() { return this->bMode; }; - [[nodiscard]] QBindable bindableScanning() { return &this->bScanning; }; + [[nodiscard]] NMWirelessCapabilities::Enum capabilities() { return this->bCapabilities; } + [[nodiscard]] const QDBusObjectPath& activeApPath() { return this->bActiveAccessPoint; } + [[nodiscard]] NM80211Mode::Enum mode() { return this->bMode; } + [[nodiscard]] QBindable bindableScanning() { return &this->bScanning; } signals: void accessPointLoaded(NMAccessPoint* ap); @@ -123,7 +130,7 @@ private slots: void onAccessPointAdded(const QDBusObjectPath& path); void onAccessPointRemoved(const QDBusObjectPath& path); void onAccessPointLoaded(NMAccessPoint* ap); - void onConnectionLoaded(NMConnectionSettings* conn); + void onSettingsLoaded(NMSettings* settings); void onActiveConnectionLoaded(NMActiveConnection* active); void onScanTimeout(); void onScanningChanged(bool scanning); diff --git a/src/network/test/manual/network.qml b/src/network/test/manual/network.qml index 0fd0f72..eadc159 100644 --- a/src/network/test/manual/network.qml +++ b/src/network/test/manual/network.qml @@ -5,151 +5,356 @@ import Quickshell import Quickshell.Widgets import Quickshell.Networking -FloatingWindow { - color: contentItem.palette.window +Scope { + Component { + id: editorComponent + FloatingWindow { + id: editorWindow + required property var nmSettings + color: contentItem.palette.window - ColumnLayout { - anchors.fill: parent - anchors.margins: 5 + Component.onCompleted: editorArea.text = JSON.stringify(nmSettings.read(), null, 2) - Column { - Layout.fillWidth: true - RowLayout { - Label { - text: "WiFi" - font.bold: true - font.pointSize: 12 - } - CheckBox { - text: "Software" - checked: Networking.wifiEnabled - onClicked: Networking.wifiEnabled = !Networking.wifiEnabled - } - CheckBox { - enabled: false - text: "Hardware" - checked: Networking.wifiHardwareEnabled - } - } - } + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 - ListView { - clip: true - Layout.fillWidth: true - Layout.fillHeight: true - model: Networking.devices + Label { + text: "Editing " + nmSettings?.id + " (" + nmSettings?.uuid + ")" + font.bold: true + font.pointSize: 12 + } - delegate: WrapperRectangle { - width: parent.width - color: "transparent" - border.color: palette.button - border.width: 1 - margin: 5 + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + TextArea { + id: editorArea + wrapMode: TextEdit.Wrap + selectByMouse: true + } + } - ColumnLayout { - RowLayout { - Label { text: modelData.name; font.bold: true } - Label { text: modelData.address } - Label { text: `(Type: ${DeviceType.toString(modelData.type)})` } - } - RowLayout { - Label { - text: DeviceConnectionState.toString(modelData.state) - color: modelData.connected ? palette.link : palette.placeholderText - } - Label { - visible: Networking.backend == NetworkBackendType.NetworkManager && (modelData.state == DeviceConnectionState.Connecting || modelData.state == DeviceConnectionState.Disconnecting) - text: `(${NMDeviceState.toString(modelData.nmState)})` - } - Button { - visible: modelData.state == DeviceConnectionState.Connected - text: "Disconnect" - onClicked: modelData.disconnect() - } - CheckBox { - text: "Autoconnect" - checked: modelData.autoconnect - onClicked: modelData.autoconnect = !modelData.autoconnect - } - Label { - text: `Mode: ${WifiDeviceMode.toString(modelData.mode)}` - visible: modelData.type == DeviceType.Wifi - } - CheckBox { - text: "Scanner" - checked: modelData.scannerEnabled - onClicked: modelData.scannerEnabled = !modelData.scannerEnabled - visible: modelData.type === DeviceType.Wifi - } - } + RowLayout { + Layout.fillWidth: true + Label { + id: statusLabel + Layout.fillWidth: true + color: palette.placeholderText + } + Button { + text: "Reload" + onClicked: { + editorArea.text = JSON.stringify(editorWindow.nmSettings.read(), null, 2); + statusLabel.text = "Reloaded"; + } + } + Button { + text: "Save" + onClicked: { + try { + const parsed = JSON.parse(editorArea.text); + nmSettings.write(parsed); + statusLabel.text = "Saved"; + } catch (e) { + statusLabel.text = "Parse error: " + e.message; + } + } + } + Button { + text: "Close" + onClicked: { + editorArea.focus = false; + editorWindow.destroy(); + } + } + } + } + } + } - Repeater { - Layout.fillWidth: true - model: { - if (modelData.type !== DeviceType.Wifi) return [] - return [...modelData.networks.values].sort((a, b) => { - if (a.connected !== b.connected) { - return b.connected - a.connected - } - return b.signalStrength - a.signalStrength - }) - } + FloatingWindow { + color: contentItem.palette.window - WrapperRectangle { - Layout.fillWidth: true - color: modelData.connected ? palette.highlight : palette.button - border.color: palette.mid - border.width: 1 - margin: 5 + ColumnLayout { + anchors.fill: parent + anchors.margins: 5 - RowLayout { - ColumnLayout { - Layout.fillWidth: true - RowLayout { - Label { text: modelData.name; font.bold: true } - Label { - text: modelData.known ? "Known" : "" - color: palette.placeholderText - } - } - RowLayout { - Label { - text: `Security: ${WifiSecurityType.toString(modelData.security)}` - color: palette.placeholderText - } - Label { - text: `| Signal strength: ${Math.round(modelData.signalStrength*100)}%` - color: palette.placeholderText - } - } - Label { - visible: Networking.backend == NetworkBackendType.NetworkManager && (modelData.nmReason != NMConnectionStateReason.Unknown && modelData.nmReason != NMConnectionStateReason.None) - text: `Connection change reason: ${NMConnectionStateReason.toString(modelData.nmReason)}` - } - } - RowLayout { - Layout.alignment: Qt.AlignRight - Button { - text: "Connect" - onClicked: modelData.connect() - visible: !modelData.connected - } - Button { - text: "Disconnect" - onClicked: modelData.disconnect() - visible: modelData.connected - } - Button { - text: "Forget" - onClicked: modelData.forget() - visible: modelData.known - } - } - } - } - } - } - } - } - } + ColumnLayout { + Label { + text: `Networking (${NetworkBackendType.toString(Networking.backend)} backend)` + font.bold: true + font.pointSize: 12 + } + RowLayout { + Label { + text: `Connectivity` + font.bold: true + } + Label { + text: `${NetworkConnectivity.toString(Networking.connectivity)}` + visible: Networking.canCheckConnectivity + } + Button { + text: "Re-check" + visible: Networking.canCheckConnectivity && Networking.connectivityCheckEnabled + onClicked: Networking.checkConnectivity() + } + CheckBox { + text: "Checking enabled" + checked: Networking.connectivityCheckEnabled + onClicked: Networking.connectivityCheckEnabled = !Networking.connectivityCheckEnabled + visible: Networking.canCheckConnectivity + } + CheckBox { + enabled: false + text: "Supported" + checked: Networking.canCheckConnectivity + } + } + } + + Column { + Layout.fillWidth: true + RowLayout { + Label { + text: "WiFi" + font.bold: true + } + CheckBox { + text: "Software" + checked: Networking.wifiEnabled + onClicked: Networking.wifiEnabled = !Networking.wifiEnabled + } + CheckBox { + enabled: false + text: "Hardware" + checked: Networking.wifiHardwareEnabled + } + } + } + + ListView { + clip: true + Layout.fillWidth: true + Layout.fillHeight: true + model: Networking.devices + + delegate: WrapperRectangle { + width: parent.width + color: "transparent" + border.color: palette.button + border.width: 1 + margin: 5 + + ColumnLayout { + RowLayout { + Label { + text: modelData.name + font.bold: true + } + Label { + text: modelData.address + } + Label { + text: `(Type: ${DeviceType.toString(modelData.type)})` + } + CheckBox { + text: `Managed` + checked: modelData.nmManaged + onClicked: modelData.nmManaged = !modelData.nmManaged + } + } + RowLayout { + Label { + text: ConnectionState.toString(modelData.state) + color: modelData.connected ? palette.link : palette.placeholderText + } + Button { + visible: modelData.state == ConnectionState.Connected + text: "Disconnect" + onClicked: modelData.disconnect() + } + CheckBox { + text: "Autoconnect" + checked: modelData.autoconnect + onClicked: modelData.autoconnect = !modelData.autoconnect + } + Label { + text: `Mode: ${WifiDeviceMode.toString(modelData.mode)}` + visible: modelData.type == DeviceType.Wifi + } + CheckBox { + text: "Scanner" + checked: modelData.scannerEnabled + onClicked: modelData.scannerEnabled = !modelData.scannerEnabled + visible: modelData.type === DeviceType.Wifi + } + } + + Repeater { + Layout.fillWidth: true + model: ScriptModel { + values: [...modelData.networks.values].sort((a, b) => { + if (a.connected !== b.connected) { + return b.connected - a.connected; + } + return b.signalStrength - a.signalStrength; + }) + } + + WrapperRectangle { + property var chosenSettings: { + const settings = modelData.nmSettings; + if (!settings || settings.length === 0) { + return null; + } + if (settings.length === 1) { + return settings[0]; + } + return settings[settingsComboBox.currentIndex]; + } + + Connections { + target: modelData + function onConnectionFailed(reason) { + failLoader.sourceComponent = failComponent; + failLoader.item.failReason = reason; + } + function onStateChanged() { + if (modelData.state == ConnectionState.Connecting) { + failLoader.sourceComponent = null; + } + } + } + + Component { + id: failComponent + RowLayout { + property var failReason + Label { + text: ConnectionFailReason.toString(failReason) + } + RowLayout { + TextField { + id: pskField + placeholderText: "PSK" + } + Button { + text: "Set" + visible: pskField.visible + onClicked: { + modelData.connectWithPsk(pskField.text); + failLoader.sourceComponent = null; + } + } + visible: modelData.security === WifiSecurityType.WpaPsk || modelData.security === WifiSecurityType.Wpa2Psk || modelData.security === WifiSecurityType.Sae + } + Button { + text: "Close" + onClicked: failLoader.sourceComponent = null + } + } + } + + Layout.fillWidth: true + color: modelData.connected ? palette.highlight : palette.button + border.color: palette.mid + border.width: 1 + margin: 5 + + RowLayout { + ColumnLayout { + Layout.fillWidth: true + RowLayout { + Label { + text: modelData.name + font.bold: true + } + Label { + text: modelData.known ? "Known" : "" + color: palette.placeholderText + } + } + RowLayout { + Label { + text: `Security: ${WifiSecurityType.toString(modelData.security)}` + color: palette.placeholderText + } + Label { + text: `| Signal strength: ${Math.round(modelData.signalStrength * 100)}%` + color: palette.placeholderText + } + } + } + ColumnLayout { + Layout.alignment: Qt.AlignRight + RowLayout { + Layout.alignment: Qt.AlignRight + BusyIndicator { + implicitHeight: 30 + implicitWidth: 30 + running: modelData.stateChanging + visible: modelData.stateChanging + } + Label { + text: ConnectionState.toString(modelData.state) + color: modelData.connected ? palette.link : palette.placeholderText + } + RowLayout { + Label { + text: "Choose settings:" + } + ComboBox { + id: settingsComboBox + model: modelData.nmSettings.map(s => s?.read()?.connection?.id) + currentIndex: 0 + } + visible: modelData.nmSettings.length > 1 + } + Button { + text: "Connect" + onClicked: { + if (chosenSettings) + modelData.connectWithSettings(chosenSettings); + else + modelData.connect(); + } + visible: !modelData.connected + } + Button { + text: "Disconnect" + onClicked: modelData.disconnect() + visible: modelData.connected + } + Button { + text: "Forget" + onClicked: modelData.forget() + visible: modelData.known + } + Button { + text: "Edit" + visible: modelData.known + onClicked: { + if (chosenSettings) + editorComponent.createObject(null, { + nmSettings: chosenSettings + }); + } + } + } + Loader { + id: failLoader + Layout.alignment: Qt.AlignRight + visible: sourceComponent !== null + } + } + } + } + } + } + } + } + } + } } diff --git a/src/network/wifi.cpp b/src/network/wifi.cpp index 57fb8ea..e9939c2 100644 --- a/src/network/wifi.cpp +++ b/src/network/wifi.cpp @@ -6,99 +6,35 @@ #include #include #include +#include #include "../core/logcat.hpp" #include "device.hpp" +#include "enums.hpp" #include "network.hpp" namespace qs::network { namespace { -QS_LOGGING_CATEGORY(logWifi, "quickshell.network.wifi", QtWarningMsg); -} // namespace - -QString WifiSecurityType::toString(WifiSecurityType::Enum type) { - switch (type) { - case Unknown: return QStringLiteral("Unknown"); - case Wpa3SuiteB192: return QStringLiteral("WPA3 Suite B 192-bit"); - case Sae: return QStringLiteral("WPA3"); - case Wpa2Eap: return QStringLiteral("WPA2 Enterprise"); - case Wpa2Psk: return QStringLiteral("WPA2"); - case WpaEap: return QStringLiteral("WPA Enterprise"); - case WpaPsk: return QStringLiteral("WPA"); - case StaticWep: return QStringLiteral("WEP"); - case DynamicWep: return QStringLiteral("Dynamic WEP"); - case Leap: return QStringLiteral("LEAP"); - case Owe: return QStringLiteral("OWE"); - case Open: return QStringLiteral("Open"); - default: return QStringLiteral("Unknown"); - } +QS_LOGGING_CATEGORY(logWifiNetwork, "quickshell.wifinetwork", QtWarningMsg); } -QString WifiDeviceMode::toString(WifiDeviceMode::Enum mode) { - switch (mode) { - case Unknown: return QStringLiteral("Unknown"); - case AdHoc: return QStringLiteral("Ad-Hoc"); - case Station: return QStringLiteral("Station"); - case AccessPoint: return QStringLiteral("Access Point"); - case Mesh: return QStringLiteral("Mesh"); - default: return QStringLiteral("Unknown"); - }; -} - -QString NMConnectionStateReason::toString(NMConnectionStateReason::Enum reason) { - switch (reason) { - case Unknown: return QStringLiteral("Unknown"); - case None: return QStringLiteral("No reason"); - case UserDisconnected: return QStringLiteral("User disconnection"); - case DeviceDisconnected: - return QStringLiteral("The device the connection was using was disconnected."); - case ServiceStopped: - return QStringLiteral("The service providing the VPN connection was stopped."); - case IpConfigInvalid: - return QStringLiteral("The IP config of the active connection was invalid."); - case ConnectTimeout: - return QStringLiteral("The connection attempt to the VPN service timed out."); - case ServiceStartTimeout: - return QStringLiteral( - "A timeout occurred while starting the service providing the VPN connection." - ); - case ServiceStartFailed: - return QStringLiteral("Starting the service providing the VPN connection failed."); - case NoSecrets: return QStringLiteral("Necessary secrets for the connection were not provided."); - case LoginFailed: return QStringLiteral("Authentication to the server failed."); - case ConnectionRemoved: - return QStringLiteral("Necessary secrets for the connection were not provided."); - case DependencyFailed: - return QStringLiteral("Master connection of this connection failed to activate."); - case DeviceRealizeFailed: return QStringLiteral("Could not create the software device link."); - case DeviceRemoved: return QStringLiteral("The device this connection depended on disappeared."); - default: return QStringLiteral("Unknown"); - }; -}; - WifiNetwork::WifiNetwork(QString ssid, QObject* parent): Network(std::move(ssid), parent) {}; -void WifiNetwork::connect() { +void WifiNetwork::connectWithPsk(const QString& psk) { if (this->bConnected) { - qCCritical(logWifi) << this << "is already connected."; + qCCritical(logWifiNetwork) << this << "is already connected."; return; } - - this->requestConnect(); -} - -void WifiNetwork::disconnect() { - if (!this->bConnected) { - qCCritical(logWifi) << this << "is not currently connected"; + if (this->bSecurity != WifiSecurityType::WpaPsk && this->bSecurity != WifiSecurityType::Wpa2Psk + && this->bSecurity != WifiSecurityType::Sae) + { + qCCritical(logWifiNetwork) << this << "has the wrong security type for a PSK."; return; } - - this->requestDisconnect(); + emit this->requestConnectWithPsk(psk); } -void WifiNetwork::forget() { this->requestForget(); } - WifiDevice::WifiDevice(QObject* parent): NetworkDevice(DeviceType::Wifi, parent) {}; void WifiDevice::setScannerEnabled(bool enabled) { diff --git a/src/network/wifi.hpp b/src/network/wifi.hpp index 15b093d..c0091f7 100644 --- a/src/network/wifi.hpp +++ b/src/network/wifi.hpp @@ -6,90 +6,15 @@ #include #include +#include "../core/doc.hpp" #include "../core/model.hpp" #include "device.hpp" +#include "enums.hpp" #include "network.hpp" namespace qs::network { -///! The security type of a wifi network. -class WifiSecurityType: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - Wpa3SuiteB192 = 0, - Sae = 1, - Wpa2Eap = 2, - Wpa2Psk = 3, - WpaEap = 4, - WpaPsk = 5, - StaticWep = 6, - DynamicWep = 7, - Leap = 8, - Owe = 9, - Open = 10, - Unknown = 11, - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(WifiSecurityType::Enum type); -}; - -///! The 802.11 mode of a wifi device. -class WifiDeviceMode: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - /// The device is part of an Ad-Hoc network without a central access point. - AdHoc = 0, - /// The device is a station that can connect to networks. - Station = 1, - /// The device is a local hotspot/access point. - AccessPoint = 2, - /// The device is an 802.11s mesh point. - Mesh = 3, - /// The device mode is unknown. - Unknown = 4, - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(WifiDeviceMode::Enum mode); -}; - -///! NetworkManager-specific reason for a WifiNetworks connection state. -/// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionStateReason. -class NMConnectionStateReason: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - Unknown = 0, - None = 1, - UserDisconnected = 2, - DeviceDisconnected = 3, - ServiceStopped = 4, - IpConfigInvalid = 5, - ConnectTimeout = 6, - ServiceStartTimeout = 7, - ServiceStartFailed = 8, - NoSecrets = 9, - LoginFailed = 10, - ConnectionRemoved = 11, - DependencyFailed = 12, - DeviceRealizeFailed = 13, - DeviceRemoved = 14 - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(NMConnectionStateReason::Enum reason); -}; - -///! An available wifi network. +///! WiFi subtype of @@Network. class WifiNetwork: public Network { Q_OBJECT; QML_ELEMENT; @@ -97,58 +22,46 @@ class WifiNetwork: public Network { // clang-format off /// The current signal strength of the network, from 0.0 to 1.0. Q_PROPERTY(qreal signalStrength READ default NOTIFY signalStrengthChanged BINDABLE bindableSignalStrength); - /// True if the wifi network has known connection settings saved. - Q_PROPERTY(bool known READ default NOTIFY knownChanged BINDABLE bindableKnown); /// The security type of the wifi network. Q_PROPERTY(WifiSecurityType::Enum security READ default NOTIFY securityChanged BINDABLE bindableSecurity); - /// A specific reason for the connection state when the backend is NetworkManager. - Q_PROPERTY(NMConnectionStateReason::Enum nmReason READ default NOTIFY nmReasonChanged BINDABLE bindableNmReason); // clang-format on public: explicit WifiNetwork(QString ssid, QObject* parent = nullptr); - - /// Attempt to connect to the wifi network. + /// Attempt to connect to the network with the given PSK. If the PSK is wrong, + /// a @@Network.connectionFailed(s) signal will be emitted with `NoSecrets`. /// - /// > [!WARNING] Quickshell does not yet provide a NetworkManager authentication agent, - /// > meaning another agent will need to be active to enter passwords for unsaved networks. - Q_INVOKABLE void connect(); - /// Disconnect from the wifi network. - Q_INVOKABLE void disconnect(); - /// Forget all connection settings for this wifi network. - Q_INVOKABLE void forget(); + /// The networking backend may store the PSK for future use with @@Network.connect(). + /// As such, calling that function first is recommended to avoid having to show a + /// prompt if not required. + /// + /// > [!NOTE] PSKs should only be provided when the @@security is one of + /// > `WpaPsk`, `Wpa2Psk`, or `Sae`. + Q_INVOKABLE void connectWithPsk(const QString& psk); QBindable bindableSignalStrength() { return &this->bSignalStrength; } - QBindable bindableKnown() { return &this->bKnown; } - QBindable bindableNmReason() { return &this->bNmReason; } QBindable bindableSecurity() { return &this->bSecurity; } signals: - void requestConnect(); - void requestDisconnect(); - void requestForget(); + QSDOC_HIDE void requestConnectWithPsk(QString psk); void signalStrengthChanged(); - void knownChanged(); void securityChanged(); - void nmReasonChanged(); private: // clang-format off Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, qreal, bSignalStrength, &WifiNetwork::signalStrengthChanged); - Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, bool, bKnown, &WifiNetwork::knownChanged); - Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, NMConnectionStateReason::Enum, bNmReason, &WifiNetwork::nmReasonChanged); Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, WifiSecurityType::Enum, bSecurity, &WifiNetwork::securityChanged); // clang-format on }; -///! Wireless variant of a NetworkDevice. +///! WiFi variant of a @@NetworkDevice. class WifiDevice: public NetworkDevice { Q_OBJECT; QML_ELEMENT; QML_UNCREATABLE(""); // clang-format off - /// A list of this available and connected wifi networks. + /// A list of this available or connected wifi networks. QSDOC_TYPE_OVERRIDE(ObjectModel*); Q_PROPERTY(UntypedObjectModel* networks READ networks CONSTANT); /// True when currently scanning for networks. @@ -164,9 +77,9 @@ public: void networkAdded(WifiNetwork* net); void networkRemoved(WifiNetwork* net); - [[nodiscard]] ObjectModel* networks() { return &this->mNetworks; }; - QBindable bindableScannerEnabled() { return &this->bScannerEnabled; }; - [[nodiscard]] bool scannerEnabled() const { return this->bScannerEnabled; }; + [[nodiscard]] ObjectModel* networks() { return &this->mNetworks; } + QBindable bindableScannerEnabled() { return &this->bScannerEnabled; } + [[nodiscard]] bool scannerEnabled() const { return this->bScannerEnabled; } void setScannerEnabled(bool enabled); QBindable bindableMode() { return &this->bMode; }