diff --git a/CMakeLists.txt b/CMakeLists.txt index 7161c4e..d85a2db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) boption(BLUETOOTH "Bluetooth" ON) +boption(NETWORK "Network" ON) include(cmake/install-qml-module.cmake) include(cmake/util.cmake) @@ -117,7 +118,7 @@ if (WAYLAND) list(APPEND QT_FPDEPS WaylandClient) endif() -if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH) +if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH OR NETWORK) set(DBUS ON) endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 52db00a..c95ecf7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,3 +33,7 @@ add_subdirectory(services) if (BLUETOOTH) add_subdirectory(bluetooth) endif() + +if (NETWORK) + add_subdirectory(network) +endif() diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt new file mode 100644 index 0000000..1de97dd --- /dev/null +++ b/src/network/CMakeLists.txt @@ -0,0 +1,67 @@ +# NetworkManager DBus +set_source_files_properties(org.freedesktop.NetworkManager.xml PROPERTIES + CLASSNAME DBusNetworkManagerProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.xml + dbus_nm_backend +) + +set_source_files_properties(org.freedesktop.NetworkManager.Device.xml PROPERTIES + CLASSNAME DBusNMDeviceProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.Device.xml + dbus_nm_device +) + +set_source_files_properties(org.freedesktop.NetworkManager.Device.Wireless.xml PROPERTIES + CLASSNAME DBusNMWirelessProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.Device.Wireless.xml + dbus_nm_wireless +) + +set_source_files_properties(org.freedesktop.NetworkManager.AccessPoint.xml PROPERTIES + CLASSNAME DBusNMAccessPointProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.AccessPoint.xml + dbus_nm_accesspoint +) + +qt_add_library(quickshell-network STATIC + api.cpp + nm_backend.cpp + nm_adapters.cpp + ${NM_DBUS_INTERFACES} +) + +# dbus headers +target_include_directories(quickshell-network PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} +) + +qt_add_qml_module(quickshell-network + URI Quickshell.Network + VERSION 0.1 + DEPENDENCIES QtQml +) + +qs_add_module_deps_light(quickshell-network Quickshell) +install_qml_module(quickshell-network) + +target_link_libraries(quickshell-network PRIVATE Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-network quickshell-dbus) +target_link_libraries(quickshell PRIVATE quickshell-networkplugin) + +qs_module_pch(quickshell-network SET dbus) diff --git a/src/network/api.cpp b/src/network/api.cpp new file mode 100644 index 0000000..2eb7a5c --- /dev/null +++ b/src/network/api.cpp @@ -0,0 +1,152 @@ +#include "api.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "nm_backend.hpp" + +namespace qs::network { + +namespace { +Q_LOGGING_CATEGORY(logNetworkDevice, "quickshell.network.device", QtWarningMsg); +Q_LOGGING_CATEGORY(logNetwork, "quickshell.network", QtWarningMsg); +} // namespace + +// NetworkDevice + +NetworkDevice::NetworkDevice(QObject* parent): QObject(parent) {}; + +QString NetworkDeviceState::toString(NetworkDeviceState::Enum state) { + switch (state) { + case NetworkDeviceState::Unknown: return QStringLiteral("Unknown"); + case NetworkDeviceState::Disconnected: return QStringLiteral("Disconnected"); + case NetworkDeviceState::Connecting: return QStringLiteral("Connecting"); + case NetworkDeviceState::Connected: return QStringLiteral("Connected"); + case NetworkDeviceState::Disconnecting: return QStringLiteral("Disconnecting"); + default: return QStringLiteral("Unknown"); + } +} + +QString NetworkDeviceType::toString(NetworkDeviceType::Enum type) { + switch (type) { + case NetworkDeviceType::Other: return QStringLiteral("Other"); + case NetworkDeviceType::Wireless: return QStringLiteral("Wireless"); + case NetworkDeviceType::Ethernet: return QStringLiteral("Ethernet"); + default: return QStringLiteral("Unknown"); + } +} + +void NetworkDevice::setName(const QString& name) { + if (name != this->bName) { + this->bName = name; + } +} + +void NetworkDevice::setAddress(const QString& address) { + if (address != this->bAddress) { + this->bAddress = address; + } +} + +void NetworkDevice::setState(NetworkDeviceState::Enum state) { + if (state != this->bState) { + this->bState = state; + } +} + +void NetworkDevice::disconnect() { + if (this->bState == NetworkDeviceState::Disconnected) { + qCCritical(logNetworkDevice) << "Device" << this << "is already disconnected"; + return; + } + + if (this->bState == NetworkDeviceState::Disconnecting) { + qCCritical(logNetworkDevice) << "Device" << this << "is already disconnecting"; + return; + } + + qCDebug(logNetworkDevice) << "Disconnecting from device" << this; + + signalDisconnect(); +} + +// WirelessNetworkDevice + +WirelessNetworkDevice::WirelessNetworkDevice(QObject* parent): NetworkDevice(parent) {}; + +void WirelessNetworkDevice::scanComplete(qint64 lastScan) { + this->bLastScan = lastScan; + emit this->lastScanChanged(); + + if (this->bScanning) { + this->bScanning = false; + emit this->scanningChanged(); + } +} + +void WirelessNetworkDevice::scan() { + if (this->bScanning) { + qCCritical(logNetworkDevice) << "Wireless device" << this << "is already scanning"; + return; + } + + qCDebug(logNetworkDevice) << "Requesting scan on wireless device" << this; + this->bScanning = true; + signalScan(); +} + +void WirelessNetworkDevice::addAccessPoint(NetworkAccessPoint* ap) { + mAccessPoints.insertObject(ap); +} + +void WirelessNetworkDevice::removeAccessPoint(NetworkAccessPoint* ap) { + mAccessPoints.removeObject(ap); +} + +// NetworkAccessPoint + +NetworkAccessPoint::NetworkAccessPoint(QObject* parent): QObject(parent) {}; + +void NetworkAccessPoint::setSsid(const QString& ssid) { + if (this->bSsid != ssid) { + this->bSsid = ssid; + emit ssidChanged(); + } +} + +void NetworkAccessPoint::setSignal(quint8 signal) { + if (this->bSignal != signal) { + this->bSignal = signal; + emit signalChanged(); + } +} + +// Network + +Network::Network(QObject* parent): QObject(parent) { + // Try each backend + + // NetworkManager + auto* nm = new NetworkManager(); + if (nm->isAvailable()) { + QObject::connect(nm, &NetworkManager::deviceAdded, this, &Network::addDevice); + QObject::connect(nm, &NetworkManager::deviceRemoved, this, &Network::removeDevice); + this->backend = nm; + return; + } + + // None found + this->backend = nullptr; + qCCritical(logNetwork) << "Network will not work. Could not find an available backend."; +} + +void Network::addDevice(NetworkDevice* device) { this->mDevices.insertObject(device); } + +void Network::removeDevice(NetworkDevice* device) { this->mDevices.removeObject(device); } + +} // namespace qs::network diff --git a/src/network/api.hpp b/src/network/api.hpp new file mode 100644 index 0000000..63eb962 --- /dev/null +++ b/src/network/api.hpp @@ -0,0 +1,223 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/model.hpp" + +namespace qs::network { + +///! A tracked access point on the network +class NetworkAccessPoint: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("WirelessNetwork can only be acquired through Network"); + // clang-format off + /// The service set identifier of the access point. + Q_PROPERTY(QString ssid READ default NOTIFY ssidChanged BINDABLE bindableSsid); + // The current signal quality of the access point, in percent. + Q_PROPERTY(quint8 signal READ default NOTIFY signalChanged BINDABLE bindableSignal); + //clang-format on + +signals: + void ssidChanged(); + void signalChanged(); + +public slots: + void setSsid(const QString& ssid); + void setSignal(quint8 signal); + +public: + explicit NetworkAccessPoint(QObject* parent = nullptr); + + [[nodiscard]] QBindable bindableSsid() const { return &this->bSsid; }; + [[nodiscard]] QBindable bindableSignal() const { return &this->bSignal; }; + +private: + Q_OBJECT_BINDABLE_PROPERTY(NetworkAccessPoint, QString, bSsid, &NetworkAccessPoint::ssidChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkAccessPoint, quint8, bSignal, &NetworkAccessPoint::signalChanged); +}; + +///! Type of network device. +class NetworkDeviceType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + ///! A generic device. + Other = 0, + ///! An 802.11 Wi-Fi device. + Wireless = 1, + ///! A wired ethernet device. + Ethernet = 2 + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NetworkDeviceType::Enum type); +}; + +///! State of a network device. +class NetworkDeviceState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// The device state is unknown. + Unknown = 0, + /// The device is not connected. + Disconnected = 1, + /// The device is connected. + Connected = 2, + /// The device is disconnecting. + Disconnecting = 3, + /// The device is connecting. + Connecting = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NetworkDeviceState::Enum state); +}; + +///! A tracked network device. +class NetworkDevice: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("NetworkDevices can only be acquired through Network"); + + // clang-format off + /// The name of the device's interface. + Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName); + /// The hardware address of the device's interface in the XX:XX:XX:XX:XX:XX format. + Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress); + /// Connection state of the device. + Q_PROPERTY(NetworkDeviceState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); + /// Type of device. + Q_PROPERTY(NetworkDeviceType::Enum type READ type CONSTANT); + // clang-format on + +signals: + void nameChanged(); + void addressChanged(); + void stateChanged(); + + void signalDisconnect(); + +public slots: + void setName(const QString& name); + void setAddress(const QString& address); + void setState(NetworkDeviceState::Enum state); + +public: + explicit NetworkDevice(QObject* parent = nullptr); + + /// Disconnects the device and prevents it from automatically activating further connections. + Q_INVOKABLE void disconnect(); + [[nodiscard]] virtual NetworkDeviceType::Enum type() const { return NetworkDeviceType::Other; }; + + [[nodiscard]] QBindable bindableName() const { return &this->bName; }; + [[nodiscard]] QBindable bindableAddress() const { return &this->bAddress; }; + [[nodiscard]] QBindable bindableState() const { return &this->bState; }; + +private: + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bName, &NetworkDevice::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bAddress, &NetworkDevice::addressChanged); + Q_OBJECT_BINDABLE_PROPERTY( + NetworkDevice, + NetworkDeviceState::Enum, + bState, + &NetworkDevice::stateChanged + ); +}; + +///! Wireless variant of a tracked network device. +class WirelessNetworkDevice: public NetworkDevice { + Q_OBJECT; + + // clang-format off + /// The timestamp (in CLOCK_BOOTTIME milliseconds) for the last finished network scan. + Q_PROPERTY(qint64 lastScan READ default NOTIFY lastScanChanged BINDABLE bindableLastScan); + /// True if the wireless device is currently scanning for available wifi networks. + Q_PROPERTY(bool scanning READ default NOTIFY scanningChanged BINDABLE bindableScanning); + /// A list of all available access points + Q_PROPERTY(UntypedObjectModel* accessPoints READ accessPoints CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*) + //clang-format on + +signals: + void signalScan(); + + void lastScanChanged(); + void scanningChanged(); + +public slots: + void scanComplete(qint64 lastScan); + void addAccessPoint(NetworkAccessPoint* ap); + void removeAccessPoint(NetworkAccessPoint* ap); + +public: + explicit WirelessNetworkDevice(QObject* parent = nullptr); + [[nodiscard]] NetworkDeviceType::Enum type() const override { return NetworkDeviceType::Wireless; }; + + /// Request the wireless device to scan for available WiFi networks. + Q_INVOKABLE void scan(); + + [[nodiscard]] QBindable bindableScanning() { return &this->bScanning; }; + [[nodiscard]] QBindable bindableLastScan() { return &this->bLastScan; }; + + UntypedObjectModel* accessPoints() { return &this->mAccessPoints; }; + +private: + ObjectModel mAccessPoints{this}; + Q_OBJECT_BINDABLE_PROPERTY(WirelessNetworkDevice, bool, bScanning, &WirelessNetworkDevice::scanningChanged); + Q_OBJECT_BINDABLE_PROPERTY(WirelessNetworkDevice, qint64, bLastScan, &WirelessNetworkDevice::lastScanChanged); +}; + +// -- Network -- +class NetworkBackend: public QObject { + Q_OBJECT; + +public: + [[nodiscard]] virtual bool isAvailable() const = 0; + +protected: + explicit NetworkBackend(QObject* parent = nullptr): QObject(parent) {}; +}; + +///! Network manager +/// Provides access to network devices. +class Network: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(Network); + QML_SINGLETON; + + // clang-format off + /// The default wifi device. Usually there is only one. This defaults to the first wifi device registered. + // Q_PROPERTY(WirelessNetworkDevice* defaultWifiDevice READ defaultWifiDevice CONSTANT); + /// A list of all network devices. + Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + // clang-format on + +public slots: + void addDevice(NetworkDevice* device); + void removeDevice(NetworkDevice* device); + +public: + explicit Network(QObject* parent = nullptr); + [[nodiscard]] UntypedObjectModel* devices() { return backend ? &this->mDevices : nullptr; }; + +private: + ObjectModel mDevices {this}; + class NetworkBackend* backend = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm_adapters.cpp b/src/network/nm_adapters.cpp new file mode 100644 index 0000000..1ce02f0 --- /dev/null +++ b/src/network/nm_adapters.cpp @@ -0,0 +1,222 @@ +#include "nm_adapters.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../dbus/properties.hpp" +#include "dbus_nm_accesspoint.h" +#include "dbus_nm_device.h" +#include "dbus_nm_wireless.h" + +using namespace qs::dbus; + +namespace qs::network { + +namespace { +Q_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +// Device + +NMDeviceAdapter::NMDeviceAdapter(QObject* parent): QObject(parent) {} + +void NMDeviceAdapter::init(const QString& path) { + this->proxy = new DBusNMDeviceProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create NMDeviceAdapter for" << path; + return; + } + + this->deviceProperties.setInterface(this->proxy); + this->deviceProperties.updateAllViaGetAll(); +} + +void NMDeviceAdapter::disconnect() { this->proxy->Disconnect(); } +bool NMDeviceAdapter::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMDeviceAdapter::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMDeviceAdapter::path() const { return this->proxy ? this->proxy->path() : QString(); } + +NetworkDeviceState::Enum NMDeviceState::translate(NMDeviceState::Enum state) { + switch (state) { + case 0 ... 20: return NetworkDeviceState::Unknown; + case 30: return NetworkDeviceState::Disconnected; + case 40 ... 90: return NetworkDeviceState::Connecting; + case 100: return NetworkDeviceState::Connected; + case 110 ... 120: return NetworkDeviceState::Disconnecting; + } +} + +// Wireless + +NMWirelessAdapter::NMWirelessAdapter(QObject* parent): QObject(parent) {} + +void NMWirelessAdapter::init(const QString& path) { + this->proxy = new DBusNMWirelessProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create NMWirelessAdapter for" << path; + return; + } + + QObject::connect( + this->proxy, + &DBusNMWirelessProxy::AccessPointAdded, + this, + &NMWirelessAdapter::onAccessPointAdded + ); + + QObject::connect( + this->proxy, + &DBusNMWirelessProxy::AccessPointRemoved, + this, + &NMWirelessAdapter::onAccessPointRemoved + ); + + this->wirelessProperties.setInterface(this->proxy); + this->wirelessProperties.updateAllViaGetAll(); + + this->registerAccessPoints(); +} + +void NMWirelessAdapter::onAccessPointAdded(const QDBusObjectPath& path) { + this->registerAccessPoint(path.path()); +} + +void NMWirelessAdapter::onAccessPointRemoved(const QDBusObjectPath& path) { + auto iter = this->mAPHash.find(path.path()); + if (iter == this->mAPHash.end()) { + qCWarning(logNetworkManager) << "NMWirelessAdapter sent removal signal for" << path.path() + << "which is not registered."; + } else { + auto* ap = iter.value(); + this->mAPHash.erase(iter); + emit accessPointRemoved(ap); + qCDebug(logNetworkManager) << "Access point" << path.path() << "removed."; + } +} + +void NMWirelessAdapter::registerAccessPoints() { + auto pending = this->proxy->GetAllAccessPoints(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to get access points: " << reply.error().message(); + } else { + for (const QDBusObjectPath& devicePath: reply.value()) { + this->registerAccessPoint(devicePath.path()); + } + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NMWirelessAdapter::registerAccessPoint(const QString& path) { + if (this->mAPHash.contains(path)) { + qCDebug(logNetworkManager) << "Skipping duplicate registration of access point" << path; + return; + } + + // Create an access point adapter + auto* apAdapter = new NMAccessPointAdapter(); + apAdapter->init(path); + + if (!apAdapter->isValid()) { + qCWarning(logNetworkManager) << "Cannot create NMAccessPointAdapter for" << path; + delete apAdapter; + return; + } + auto* ap = new NetworkAccessPoint(this); + apAdapter->setParent(ap); + + // NMAccessPointAdapter signal -> NetworkAccessPoint slot + QObject::connect(apAdapter, &NMAccessPointAdapter::ssidChanged, ap, &NetworkAccessPoint::setSsid); + QObject::connect( + apAdapter, + &NMAccessPointAdapter::signalChanged, + ap, + &NetworkAccessPoint::setSignal + ); + + this->mAPHash.insert(path, ap); + emit accessPointAdded(ap); + qCDebug(logNetworkManager) << "Registered access point" << path; +} + +void NMWirelessAdapter::scan() { this->proxy->RequestScan({}); } + +bool NMWirelessAdapter::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMWirelessAdapter::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMWirelessAdapter::path() const { return this->proxy ? this->proxy->path() : QString(); } + +// Access Point + +namespace { +Q_LOGGING_CATEGORY(logNMAccessPoint, "quickshell.network.networkmanager.accesspoint", QtWarningMsg); +} + +NMAccessPointAdapter::NMAccessPointAdapter(QObject* parent): QObject(parent) {} + +void NMAccessPointAdapter::init(const QString& path) { + this->proxy = new DBusNMAccessPointProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNMAccessPoint) << "Cannot create NMWirelessAdapter for" << path; + return; + } + + this->accessPointProperties.setInterface(this->proxy); + this->accessPointProperties.updateAllViaGetAll(); +} + +bool NMAccessPointAdapter::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMAccessPointAdapter::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMAccessPointAdapter::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)); +} + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm_adapters.hpp b/src/network/nm_adapters.hpp new file mode 100644 index 0000000..cea6793 --- /dev/null +++ b/src/network/nm_adapters.hpp @@ -0,0 +1,230 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../dbus/properties.hpp" +#include "api.hpp" +#include "dbus_nm_accesspoint.h" +#include "dbus_nm_device.h" +#include "dbus_nm_wireless.h" + +namespace qs::network { + +class NMDeviceState: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unkown = 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); + + static NetworkDeviceState::Enum translate(NMDeviceState::Enum state); +}; + +class NMDeviceType: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Generic = 14, + Ethernet = 1, + Wifi = 2, + Unused1 = 3, + Unused2 = 4, + Bluetooth = 5, + OlpcMesh = 6, + Wimax = 7, + Modem = 8, + Infiniband = 9, + Bond = 10, + Vlan = 11, + Adsl = 12, + Bridge = 13, + Team = 15, + Tun = 16, + Tunnel = 17, + Macvlan = 18, + Vxlan = 19, + Veth = 20, + Macsec = 21, + Dummy = 22, + Ppp = 23, + OvsInterface = 24, + OvsPort = 25, + OvsBridge = 26, + Wpan = 27, + Lowpan = 28, + WireGuard = 29, + WifiP2P = 30, + Vrf = 31, + Loopback = 32, + Hsr = 33, + Ipvlan = 34, + }; + Q_ENUM(Enum); +}; + +} // namespace qs::network + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMDeviceType::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMDeviceState::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +class NMDeviceAdapter: public QObject { + Q_OBJECT; + +public: + explicit NMDeviceAdapter(QObject* parent = nullptr); + void init(const QString& path); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QString getInterface() { return this->bInterface; }; + [[nodiscard]] QString getHwAddress() { return this->bHwAddress; }; + [[nodiscard]] NMDeviceType::Enum getType() { return this->bType; }; + +public slots: + void disconnect(); + +signals: + void interfaceChanged(const QString& interface); + void hwAddressChanged(const QString& hwAddress); + void typeChanged(NMDeviceType::Enum type); + void stateChanged(NMDeviceState::Enum state); + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMDeviceAdapter, QString, bInterface, &NMDeviceAdapter::interfaceChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDeviceAdapter, QString, bHwAddress, &NMDeviceAdapter::hwAddressChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDeviceAdapter, NMDeviceState::Enum, bState, &NMDeviceAdapter::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDeviceAdapter, NMDeviceType::Enum, bType, &NMDeviceAdapter::typeChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMDeviceAdapter, deviceProperties); + QS_DBUS_PROPERTY_BINDING(NMDeviceAdapter, pName, bInterface, deviceProperties, "Interface"); + QS_DBUS_PROPERTY_BINDING(NMDeviceAdapter, pAddress, bHwAddress, deviceProperties, "HwAddress"); + QS_DBUS_PROPERTY_BINDING(NMDeviceAdapter, pType, bType, deviceProperties, "DeviceType"); + QS_DBUS_PROPERTY_BINDING(NMDeviceAdapter, pState, bState, deviceProperties, "State"); + // clang-format on + + DBusNMDeviceProxy* proxy = nullptr; +}; + +class NMWirelessAdapter: public QObject { + Q_OBJECT; + +public: + explicit NMWirelessAdapter(QObject* parent = nullptr); + void init(const QString& path); + void registerAccessPoint(const QString& path); + void registerAccessPoints(); + QHash mAPHash; + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] qint64 getLastScan() { return this->bLastScan; }; + +public slots: + void scan(); + +signals: + void lastScanChanged(qint64 lastScan); + void accessPointAdded(NetworkAccessPoint* ap); + void accessPointRemoved(NetworkAccessPoint* ap); + +private slots: + void onAccessPointAdded(const QDBusObjectPath& path); + void onAccessPointRemoved(const QDBusObjectPath& path); + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessAdapter, qint64, bLastScan, &NMWirelessAdapter::lastScanChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMWirelessAdapter, wirelessProperties); + QS_DBUS_PROPERTY_BINDING(NMWirelessAdapter, pLastScan, bLastScan, wirelessProperties, "LastScan"); + // clang-format on + + DBusNMWirelessProxy* proxy = nullptr; +}; + +class NMAccessPointAdapter: public QObject { + Q_OBJECT; + +public: + explicit NMAccessPointAdapter(QObject* parent = nullptr); + void init(const QString& path); + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + +signals: + void ssidChanged(QByteArray ssid); + void signalChanged(uchar signal); + +private: + //clang-format off + Q_OBJECT_BINDABLE_PROPERTY( + NMAccessPointAdapter, + QByteArray, + bSsid, + &NMAccessPointAdapter::ssidChanged + ); + Q_OBJECT_BINDABLE_PROPERTY( + NMAccessPointAdapter, + uchar, + bSignal, + &NMAccessPointAdapter::signalChanged + ); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMAccessPointAdapter, accessPointProperties); + QS_DBUS_PROPERTY_BINDING(NMAccessPointAdapter, pSsid, bSsid, accessPointProperties, "Ssid"); + QS_DBUS_PROPERTY_BINDING( + NMAccessPointAdapter, + pSignal, + bSignal, + accessPointProperties, + "Strength" + ); + + DBusNMAccessPointProxy* proxy = nullptr; +}; +} // namespace qs::network diff --git a/src/network/nm_backend.cpp b/src/network/nm_backend.cpp new file mode 100644 index 0000000..22a84cb --- /dev/null +++ b/src/network/nm_backend.cpp @@ -0,0 +1,239 @@ +#include "nm_backend.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../dbus/bus.hpp" +#include "../dbus/properties.hpp" +#include "api.hpp" +#include "dbus_nm_backend.h" +#include "nm_adapters.hpp" + +namespace qs::network { + +namespace { +Q_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +const QString NM_SERVICE = "org.freedesktop.NetworkManager"; +const QString NM_PATH = "/org/freedesktop/NetworkManager"; + +NetworkManager::NetworkManager(QObject* parent): NetworkBackend(parent) { + qCDebug(logNetworkManager) << "Starting NetworkManager Network Backend"; + + auto bus = QDBusConnection::systemBus(); + if (!bus.isConnected()) { + qCWarning(logNetworkManager + ) << "Could not connect to DBus. NetworkManager backend will not work."; + return; + } + + this->proxy = new DBusNetworkManagerProxy(NM_SERVICE, NM_PATH, bus, this); + + if (!this->proxy->isValid()) { + qCDebug(logNetworkManager + ) << "NetworkManager service is not currently running, attempting to start it."; + + dbus::tryLaunchService(this, bus, NM_SERVICE, [this](bool success) { + if (success) { + qCDebug(logNetworkManager) << "Successfully launched NetworkManager backend."; + this->init(); + } else { + qCWarning(logNetworkManager) + << "Could not start NetworkManager. This network backend will not work."; + } + }); + } else { + this->init(); + } +} + +void NetworkManager::init() { + // Proxy signals -> NetworkManager slots + QObject::connect( + this->proxy, + &DBusNetworkManagerProxy::DeviceAdded, + this, + &NetworkManager::onDeviceAdded + ); + QObject::connect( + this->proxy, + &DBusNetworkManagerProxy::DeviceRemoved, + this, + &NetworkManager::onDeviceRemoved + ); + + this->dbusProperties.setInterface(this->proxy); + this->dbusProperties.updateAllViaGetAll(); + + this->registerDevices(); +} + +void NetworkManager::registerDevices() { + auto pending = this->proxy->GetAllDevices(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to get devices: " << reply.error().message(); + } else { + for (const QDBusObjectPath& devicePath: reply.value()) { + this->queueDeviceRegistration(devicePath.path()); + } + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::queueDeviceRegistration(const QString& path) { + if (this->mDeviceHash.contains(path)) { + qCDebug(logNetworkManager) << "Skipping duplicate registration of device" << path; + return; + } + + // Create a device adapter + auto* deviceAdapter = new NMDeviceAdapter(); + deviceAdapter->init(path); + + if (!deviceAdapter->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete deviceAdapter; + return; + } + + // Wait for DBus to send us the device type + QObject::connect( + deviceAdapter, + &NMDeviceAdapter::typeChanged, + this, + [this, deviceAdapter, path](NMDeviceType::Enum type) { + this->registerDevice(deviceAdapter, type, path); + } + ); +} + +// Register the device +void NetworkManager::registerDevice( + NMDeviceAdapter* deviceAdapter, + NMDeviceType::Enum type, + const QString& path +) { + NetworkDevice* device = createDeviceVariant(type, path); + deviceAdapter->setParent(device); + + // NMDeviceAdapter signal -> NetworkDevice slot + QObject::connect( + deviceAdapter, + &NMDeviceAdapter::hwAddressChanged, + device, + &NetworkDevice::setAddress + ); + QObject::connect( + deviceAdapter, + &NMDeviceAdapter::interfaceChanged, + device, + &NetworkDevice::setName + ); + QObject::connect( + deviceAdapter, + &NMDeviceAdapter::stateChanged, + device, + [device](NMDeviceState::Enum state) { device->setState(NMDeviceState::translate(state)); } + ); + + // NetworkDevice signal -> NMDeviceAdapter slot + QObject::connect( + device, + &NetworkDevice::signalDisconnect, + deviceAdapter, + &NMDeviceAdapter::disconnect + ); + + // Track device + this->mDeviceHash.insert(path, device); + emit deviceAdded(device); + + qCDebug(logNetworkManager) << "Registered device at path" << path; +} + +// Create a derived device class based on the NMDeviceType of the NMDeviceAdapter +NetworkDevice* NetworkManager::createDeviceVariant(NMDeviceType::Enum type, const QString& path) { + switch (type) { + case NMDeviceType::Wifi: return this->bindWirelessDevice(path); + default: return new NetworkDevice(); + } +} + +// Create a WirelessNetworkDevice and bind the NMWirelessAdapter +WirelessNetworkDevice* NetworkManager::bindWirelessDevice(const QString& path) { + auto* device = new WirelessNetworkDevice(this); + + auto* wirelessAdapter = new NMWirelessAdapter(device); + wirelessAdapter->init(path); + + // TODO: Check isValid() - throw error + + // NMWirelessAdapter signal -> WirelessNetworkDevice slot + QObject::connect( + wirelessAdapter, + &NMWirelessAdapter::lastScanChanged, + device, + &WirelessNetworkDevice::scanComplete + ); + QObject::connect( + wirelessAdapter, + &NMWirelessAdapter::accessPointAdded, + device, + &WirelessNetworkDevice::addAccessPoint + ); + QObject::connect( + wirelessAdapter, + &NMWirelessAdapter::accessPointRemoved, + device, + &WirelessNetworkDevice::removeAccessPoint + ); + + // WirelessNetworkDevice signal -> NMWirelessAdapter slot + QObject::connect( + device, + &WirelessNetworkDevice::signalScan, + wirelessAdapter, + &NMWirelessAdapter::scan + ); + + return device; +} + +void NetworkManager::onDeviceAdded(const QDBusObjectPath& path) { + this->queueDeviceRegistration(path.path()); +} + +void NetworkManager::onDeviceRemoved(const QDBusObjectPath& path) { + auto iter = this->mDeviceHash.find(path.path()); + + if (iter == this->mDeviceHash.end()) { + qCWarning(logNetworkManager) << "NetworkManager backend sent removal signal for" << path.path() + << "which is not registered."; + } else { + auto* device = iter.value(); + this->mDeviceHash.erase(iter); + emit deviceRemoved(device); + qCDebug(logNetworkManager) << "Device" << path.path() << "removed."; + } +} + +bool NetworkManager::isAvailable() const { return this->proxy && this->proxy->isValid(); } + +} // namespace qs::network diff --git a/src/network/nm_backend.hpp b/src/network/nm_backend.hpp new file mode 100644 index 0000000..5c1c565 --- /dev/null +++ b/src/network/nm_backend.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../dbus/properties.hpp" +#include "api.hpp" +#include "dbus_nm_backend.h" +#include "nm_adapters.hpp" + +namespace qs::network { + +class NetworkManager: public NetworkBackend { + Q_OBJECT; + +signals: + void deviceAdded(NetworkDevice* device); + void deviceRemoved(NetworkDevice* device); + +public: + explicit NetworkManager(QObject* parent = nullptr); + [[nodiscard]] bool isAvailable() const override; + +private slots: + void onDeviceAdded(const QDBusObjectPath& path); + void onDeviceRemoved(const QDBusObjectPath& path); + +private: + void init(); + void registerDevice(NMDeviceAdapter* deviceAdapter, NMDeviceType::Enum type, const QString& path); + void registerDevices(); + void queueDeviceRegistration(const QString& path); + NetworkDevice* createDeviceVariant(NMDeviceType::Enum type, const QString& path); + WirelessNetworkDevice* bindWirelessDevice(const QString& path); + NetworkDevice* bindDevice(NMDeviceAdapter* deviceAdapter); + + QHash mDeviceHash; + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NetworkManager, dbusProperties); + DBusNetworkManagerProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/org.freedesktop.NetworkManager.AccessPoint.xml b/src/network/org.freedesktop.NetworkManager.AccessPoint.xml new file mode 100644 index 0000000..0f5dcf2 --- /dev/null +++ b/src/network/org.freedesktop.NetworkManager.AccessPoint.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/network/org.freedesktop.NetworkManager.Device.Wireless.xml b/src/network/org.freedesktop.NetworkManager.Device.Wireless.xml new file mode 100644 index 0000000..5814185 --- /dev/null +++ b/src/network/org.freedesktop.NetworkManager.Device.Wireless.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/network/org.freedesktop.NetworkManager.Device.xml b/src/network/org.freedesktop.NetworkManager.Device.xml new file mode 100644 index 0000000..f230778 --- /dev/null +++ b/src/network/org.freedesktop.NetworkManager.Device.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/network/org.freedesktop.NetworkManager.xml b/src/network/org.freedesktop.NetworkManager.xml new file mode 100644 index 0000000..4f9ca03 --- /dev/null +++ b/src/network/org.freedesktop.NetworkManager.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/network/test/network.qml b/src/network/test/network.qml new file mode 100644 index 0000000..08c5da7 --- /dev/null +++ b/src/network/test/network.qml @@ -0,0 +1,76 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Network + +FloatingWindow { + color: contentItem.palette.window + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + model: Network.devices + + delegate: WrapperRectangle { + width: parent.width + color: "transparent" + border.color: palette.button + border.width: 1 + margin: 10 + + ColumnLayout { + Label { + text: `Device ${index}: ${modelData.name}` + font.bold: true + } + Label { text: "Hardware Address: " + modelData.address } + Label { text: "Device type: " + NetworkDeviceType.toString(modelData.type) } + + RowLayout { + Label { text: "State: " + NetworkDeviceState.toString(modelData.state) } + Button { + text: "Disconnect" + onClicked: modelData.disconnect() + visible: modelData.state === NetworkDeviceState.Connected + } + } + ColumnLayout { + RowLayout { + Label { text: "Last scan: " + modelData.lastScan } + Button { + text: "Scan" + onClicked: modelData.scan() + visible: modelData.scanning === false; + } + } + Label { text: "Available access points: " } + Repeater { + model: modelData.accessPoints + + delegate: WrapperRectangle { + height: apLabel.implicitHeight + 8 + color: "transparent" + border.color: palette.button + border.width: 1 + + Label { + id: apLabel + anchors.centerIn: parent + text: "SSID: " + (modelData.ssid || "[Hidden]") + ` SIGNAL: ${modelData.signal}` + } + } + } + visible: modelData.type === NetworkDeviceType.Wireless + } + } + } + } + } +}