diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ X11Extras Svg Concurrent + Test ) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS @@ -111,6 +112,12 @@ XKB XINPUT ) +find_package(Bolt) +set_package_properties(Bolt PROPERTIES DESCRIPTION "Thunderbolt device manager" + URL "https://gitlab.freedesktop.org/bolt/bolt" + PURPOSE "Runtime-only dependency for Thunderbolt KCM" + TYPE RUNTIME) + set_package_properties(XCB PROPERTIES TYPE REQUIRED) add_feature_info("XCB-XKB" XCB_XKB_FOUND "Required for building kcm/keyboard") add_feature_info("libxft" X11_Xft_FOUND "X FreeType interface library required for font installation") @@ -161,20 +168,27 @@ add_subdirectory(layout-templates) +add_subdirectory(libs) add_subdirectory(doc) add_subdirectory(runners) add_subdirectory(containments) add_subdirectory(toolboxes) add_subdirectory(applets) add_subdirectory(dataengines) add_subdirectory(kcms) +add_subdirectory(kded) add_subdirectory(knetattach) add_subdirectory(attica-kde) add_subdirectory(imports/activitymanager/) add_subdirectory(solid-device-automounter) if(X11_Xkb_FOUND AND XCB_XKB_FOUND) add_subdirectory(kaccess) endif() + +if (BUILD_TESTING) + add_subdirectory(autotests) +endif() + install(FILES org.kde.plasmashell.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES plasma-desktop.categories DESTINATION ${KDE_INSTALL_CONFDIR}) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/autotests/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(bolt) diff --git a/autotests/bolt/CMakeLists.txt b/autotests/bolt/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/autotests/bolt/CMakeLists.txt @@ -0,0 +1,5 @@ +kde_enable_exceptions() + +add_subdirectory(fakeserver) +add_subdirectory(lib) +add_subdirectory(kded) diff --git a/autotests/bolt/data/default.json b/autotests/bolt/data/default.json new file mode 100644 --- /dev/null +++ b/autotests/bolt/data/default.json @@ -0,0 +1,60 @@ +{ + "AuthMode": "enabled", + "DefaultPolicy": "auto", + "Probing": false, + "SecurityLevel": "none", + "Version": 1, + "Devices": [ + { + "AuthFlags": "none", + "AuthorizeTime": 1539182360, + "ConnectTime": 1539182360, + "Key": "missing", + "Label": "Dell Thunderbolt Cable", + "Name": "Dell Thunderbolt Cable", + "Parent": "d1030000-0040-3f18-23fe-13029072721e", + "Policy": "default", + "Status": "connected", + "StoreTime": 0, + "Stored": false, + "SysfsPath": "/sys/devices/pci0000:00/0000:00:1c.4/0000:03:00.0/0000:04:00.0/0000:05:00.0/domain0/0-0/0-1", + "Type": "peripheral", + "Uid": "00e74979_e33d_d400_ffff_ffffffffffff", + "Vendor": "Dell" + }, + { + "AuthFlags": "none", + "AuthorizeTime": 1539182360, + "ConnectTime": 1539182360, + "Key": "missing", + "Label": "Dell Thunderbold Dock", + "Name": "Dell Thunderbolt Dock", + "Parent": "00e74979-e33d-d400-ffff-ffffffffffff", + "Policy": "default", + "Status": "authorized", + "StoreTime": 0, + "Stored": false, + "SysfsPath": "/sys/devices/pci0000:00/0000:00:1c.4/0000:03:00.0/0000:04:00.0/0000:05:00.0/domain0/0-0/0-1/0-301", + "Type": "peripheral", + "Uid": "10c78865_633d_8680_ffff_ffffffffffff", + "Vendor": "Dell" + }, + { + "AuthFlags": "none", + "AuthorizeTime": 1539182359, + "ConnectTime": 1539182359, + "Key": "missing", + "Label": "Dell Latitude 7480", + "Name": "Dell Latitude 7480", + "Parent": "", + "Policy": "default", + "Status": "authorized", + "StoreTime": 0, + "Stored": false, + "SysfsPath": "/sys/devices/pci0000:00/0000:00:1c.4/0000:03:00.0/0000:04:00.0/0000:05:00.0/domain0/0-0", + "Type": "host", + "Uid": "d1030000_0040_3f18_23fe_13029072721e", + "Vendor": "Dell" + } + ] +} diff --git a/autotests/bolt/fakeserver/CMakeLists.txt b/autotests/bolt/fakeserver/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/autotests/bolt/fakeserver/CMakeLists.txt @@ -0,0 +1,35 @@ +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +set(libboltfakeserver_SRCS + fakedevice.cpp + fakemanager.cpp + fakeserver.cpp +) + +qt5_add_dbus_adaptor( + libboltfakeserver_SRCS + ${CMAKE_SOURCE_DIR}/libs/bolt/interfaces/org.freedesktop.bolt1.device.xml + fakedevice.h FakeDevice fakedeviceadaptor FakeDeviceAdaptor +) +qt5_add_dbus_adaptor( + libboltfakeserver_SRCS + ${CMAKE_SOURCE_DIR}/libs/bolt/interfaces/org.freedesktop.bolt1.manager.xml + fakemanager.h FakeManager fakemanageradaptor FakeManagerAdaptor +) + +add_library(libboltfakeserver STATIC ${libboltfakeserver_SRCS}) +target_link_libraries(libboltfakeserver Qt5::Core Qt5::DBus Qt5::Test) +set_target_properties(libboltfakeserver PROPERTIES OUTPUT_NAME boltfakeserver CXX_STANDARD 14) +target_include_directories(libboltfakeserver PUBLIC "$;$") + +####################################################################### + +set(boltfakeserver_SRCS + main.cpp +) + +add_executable(boltfakeserver ${boltfakeserver_SRCS}) +target_link_libraries(boltfakeserver libboltfakeserver Qt5::Core Qt5::DBus) diff --git a/autotests/bolt/fakeserver/fakedevice.h b/autotests/bolt/fakeserver/fakedevice.h new file mode 100644 --- /dev/null +++ b/autotests/bolt/fakeserver/fakedevice.h @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FAKEDEVICE_H +#define FAKEDEVICE_H + +#include +#include +#include + +class FakeDeviceException : public std::runtime_error +{ +public: + FakeDeviceException(const QString &what) + : std::runtime_error(what.toStdString()) {} +}; + +class FakeDevice : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.bolt1.Device") + + Q_PROPERTY(QString Uid READ uid CONSTANT) + Q_PROPERTY(QString Name READ name CONSTANT) + Q_PROPERTY(QString Vendor READ vendor CONSTANT) + Q_PROPERTY(QString Type READ type CONSTANT) + Q_PROPERTY(QString Status READ status CONSTANT) + Q_PROPERTY(QString AuthFlags READ authFlags CONSTANT) + Q_PROPERTY(QString Parent READ parent CONSTANT) + Q_PROPERTY(QString SysfsPath READ sysfsPath CONSTANT) + Q_PROPERTY(bool Stored READ stored CONSTANT) + Q_PROPERTY(QString Policy READ policy CONSTANT) + Q_PROPERTY(QString Key READ key CONSTANT) + Q_PROPERTY(QString Label READ label WRITE setLabel) + Q_PROPERTY(quint64 ConnectTime READ connectTime CONSTANT) + Q_PROPERTY(quint64 AuthorizeTime READ authorizeTime CONSTANT) + Q_PROPERTY(quint64 StoreTime READ storeTime CONSTANT) +public: + explicit FakeDevice(const QString &uid, QObject *parent = nullptr); + explicit FakeDevice(const QJsonObject &json, QObject *parent = nullptr); + ~FakeDevice() override; + + QDBusObjectPath dbusPath() const; + + QString uid() const; + QString name() const; + void setName(const QString &name); + QString vendor() const; + void setVendor(const QString &vendor); + QString type() const; + void setType(const QString &type); + QString status() const; + void setStatus(const QString &status); + QString authFlags() const; + void setAuthFlags(const QString &authFlags); + QString parent() const; + void setParent(const QString &parent); + QString sysfsPath() const; + void setSysfsPath(const QString &sysfsPath); + bool stored() const; + void setStored(bool stored); + QString policy() const; + void setPolicy(const QString &policy); + QString key() const; + void setKey(const QString &key); + QString label() const; + void setLabel(const QString &label); + quint64 connectTime() const; + void setConnectTime(quint64 connectTime); + quint64 authorizeTime() const; + void setAuthorizeTime(quint64 authorizeTime); + quint64 storeTime() const; + void setStoreTime(quint64 storeTime); + +public Q_SLOTS: + void Authorize(const QString &flags); + +private: + QDBusObjectPath mDBusPath; + + QString mUid; + QString mName; + QString mVendor; + QString mType; + QString mStatus = QStringLiteral("unknown"); + QString mAuthFlags = QStringLiteral("none"); + QString mParent; + QString mSysfsPath; + QString mPolicy = QStringLiteral("unknown"); + QString mKey; + QString mLabel; + quint64 mConnectTime = 0; + quint64 mAuthorizeTime = 0; + quint64 mStoreTime = 0; + bool mStored = false; +}; + +#endif + diff --git a/autotests/bolt/fakeserver/fakedevice.cpp b/autotests/bolt/fakeserver/fakedevice.cpp new file mode 100644 --- /dev/null +++ b/autotests/bolt/fakeserver/fakedevice.cpp @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "fakedevice.h" +#include "fakedeviceadaptor.h" + +#include +#include + +#include + +using namespace std::chrono_literals; + +FakeDevice::FakeDevice(const QString &uid, QObject *parent) + : QObject(parent) + , mUid(uid) +{ + new FakeDeviceAdaptor(this); + auto bus = QDBusConnection::sessionBus(); + if (!bus.registerObject(dbusPath().path(), this)) { + throw FakeDeviceException(QStringLiteral("Failed to register device %1 to DBus: %2") + .arg(mUid, bus.lastError().message())); + } +} + +FakeDevice::FakeDevice(const QJsonObject &json, QObject *parent) + : QObject(parent) + , mUid(json[QStringLiteral("Uid")].toString()) + , mName(json[QStringLiteral("Name")].toString()) + , mVendor(json[QStringLiteral("Vendor")].toString()) + , mType(json[QStringLiteral("Type")].toString()) + , mStatus(json[QStringLiteral("Status")].toString()) + , mAuthFlags(json[QStringLiteral("AuthFlags")].toString()) + , mParent(json[QStringLiteral("Parent")].toString()) + , mSysfsPath(json[QStringLiteral("SysfsPath")].toString()) + , mPolicy(json[QStringLiteral("Policy")].toString()) + , mKey(json[QStringLiteral("Key")].toString()) + , mLabel(json[QStringLiteral("Label")].toString()) + , mConnectTime(json[QStringLiteral("ConnectTime")].toVariant().value()) + , mAuthorizeTime(json[QStringLiteral("AuthorizeTime")].toVariant().value()) + , mStoreTime(json[QStringLiteral("StoreTime")].toVariant().value()) + , mStored(json[QStringLiteral("Stored")].toBool()) +{ + new FakeDeviceAdaptor(this); + auto bus = QDBusConnection::sessionBus(); + if (!bus.registerObject(dbusPath().path(), this)) { + throw FakeDeviceException(QStringLiteral("Failed to register device %1 to DBus: %2") + .arg(mUid, bus.lastError().message())); + } +} + +FakeDevice::~FakeDevice() +{ + QDBusConnection::sessionBus().unregisterObject(dbusPath().path()); +} + +QDBusObjectPath FakeDevice::dbusPath() const +{ + return QDBusObjectPath(QStringLiteral("/org/freedesktop/bolt/devices/%1") + .arg(QString(mUid).replace(QLatin1Char('-'), QLatin1Char('_')))); +} + +QString FakeDevice::uid() const +{ + return mUid; +} + +QString FakeDevice::name() const +{ + return mName; +} + +void FakeDevice::setName(const QString &name) +{ + mName = name; +} + +QString FakeDevice::vendor() const +{ + return mVendor; +} + +void FakeDevice::setVendor(const QString &vendor) +{ + mVendor = vendor; +} + +QString FakeDevice::type() const +{ + return mType; +} + +void FakeDevice::setType(const QString &type) +{ + mType = type; +} + +QString FakeDevice::status() const +{ + return mStatus; +} + +void FakeDevice::setStatus(const QString &status) +{ + mStatus = status; +} + +QString FakeDevice::authFlags() const +{ + return mAuthFlags; +} + +void FakeDevice::setAuthFlags(const QString &authFlags) +{ + mAuthFlags = authFlags; +} + +QString FakeDevice::parent() const +{ + return mParent; +} + +void FakeDevice::setParent(const QString &parent) +{ + mParent = parent; +} + +QString FakeDevice::sysfsPath() const +{ + return mSysfsPath; +} + +void FakeDevice::setSysfsPath(const QString &sysfsPath) +{ + mSysfsPath = sysfsPath; +} + +bool FakeDevice::stored() const +{ + return mStored; +} + +void FakeDevice::setStored(bool stored) +{ + mStored = stored; +} + +QString FakeDevice::policy() const +{ + return mPolicy; +} + +void FakeDevice::setPolicy(const QString &policy) +{ + mPolicy = policy; +} + +QString FakeDevice::key() const +{ + return mKey; +} + +void FakeDevice::setKey(const QString &key) +{ + mKey = key; +} + +QString FakeDevice::label() const +{ + return mLabel; +} + +void FakeDevice::setLabel(const QString &label) +{ + mLabel = label; +} + +quint64 FakeDevice::connectTime() const +{ + return mConnectTime; +} + +void FakeDevice::setConnectTime(quint64 connectTime) +{ + mConnectTime = connectTime; +} + +quint64 FakeDevice::authorizeTime() const +{ + return mAuthorizeTime; +} + +void FakeDevice::setAuthorizeTime(quint64 authorizeTime) +{ + mAuthorizeTime = authorizeTime; +} + +quint64 FakeDevice::storeTime() const +{ + return mStoreTime; +} + +void FakeDevice::setStoreTime(quint64 storeTime) +{ + mStoreTime = storeTime; +} + +void FakeDevice::Authorize(const QString &flags) +{ + std::this_thread::sleep_for(1s); // simulate this operation taking time + mAuthFlags = flags; + mStatus = QLatin1Literal("authorized"); +} diff --git a/autotests/bolt/fakeserver/fakemanager.h b/autotests/bolt/fakeserver/fakemanager.h new file mode 100644 --- /dev/null +++ b/autotests/bolt/fakeserver/fakemanager.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FAKEMANAGER_H_ +#define FAKEMANAGER_H_ + +#include +#include +#include +#include +#include + +#include + +class FakeManagerException : public std::runtime_error +{ +public: + FakeManagerException(const QString &what) + : std::runtime_error(what.toStdString()) + {} +}; + +class FakeDevice; +class FakeManager : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.bolt1.Manager") + + Q_PROPERTY(unsigned int Version READ version CONSTANT) + Q_PROPERTY(bool Probing READ isProbing CONSTANT) + Q_PROPERTY(QString DefaultPolicy READ defaultPolicy CONSTANT) + Q_PROPERTY(QString SecurityLevel READ securityLevel CONSTANT) + Q_PROPERTY(QString AuthMode READ authMode WRITE setAuthMode) +public: + explicit FakeManager(const QJsonObject &json, QObject *parent = nullptr); + explicit FakeManager(QObject *parent = nullptr); + ~FakeManager() override; + + unsigned int version() const; + bool isProbing() const; + QString defaultPolicy() const; + QString securityLevel() const; + QString authMode() const; + void setAuthMode(const QString &authMode); + + FakeDevice *addDevice(std::unique_ptr device); + void removeDevice(const QString &uid); + QList devices() const; + +public Q_SLOTS: + QList ListDevices() const; + QDBusObjectPath DeviceByUid(const QString &uid) const; + QDBusObjectPath EnrollDevice(const QString &uid, const QString &policy, const QString &flags); + void ForgetDevice(const QString &uid); + +Q_SIGNALS: + void DeviceAdded(const QDBusObjectPath &device); + void DeviceRemoved(const QDBusObjectPath &device); + +private: + bool mProbing = false; + QString mDefaultPolicy = QStringLiteral("auto"); + QString mSecurityLevel = QStringLiteral("none"); + QString mAuthMode = QStringLiteral("enabled"); + + QMap mDevices; +}; + + +#endif diff --git a/autotests/bolt/fakeserver/fakemanager.cpp b/autotests/bolt/fakeserver/fakemanager.cpp new file mode 100644 --- /dev/null +++ b/autotests/bolt/fakeserver/fakemanager.cpp @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "fakemanager.h" +#include "fakemanageradaptor.h" +#include "fakedevice.h" + +#include +#include +#include + +#include + +#include + +using namespace std::chrono_literals; + +namespace { +static const QString kManagerDBusPath = QStringLiteral("/org/freedesktop/bolt"); +} + +FakeManager::FakeManager(const QJsonObject &json, QObject *parent) + : QObject(parent) + , mProbing(json[QStringLiteral("Probing")].toBool()) + , mDefaultPolicy(json[QStringLiteral("DefaultPolicy")].toString()) + , mSecurityLevel(json[QStringLiteral("SecurityLevel")].toString()) + , mAuthMode(json[QStringLiteral("AuthMode")].toString()) +{ + new FakeManagerAdaptor(this); + if (!QDBusConnection::sessionBus().registerObject(kManagerDBusPath, this)) { + throw FakeManagerException(QStringLiteral("Failed to register FakeManager to DBus: %1") + .arg(QDBusConnection::sessionBus().lastError().message())); + } + + const auto jsonDevices = json[QStringLiteral("Devices")].toArray(); + for (const auto &jsonDevice : jsonDevices) { + auto device = new FakeDevice(jsonDevice.toObject(), this); + mDevices.insert(device->uid(), device); + } +} + +FakeManager::FakeManager(QObject *parent) + : QObject(parent) +{ + new FakeManagerAdaptor(this); + if (!QDBusConnection::sessionBus().registerObject(kManagerDBusPath, this)) { + throw FakeManagerException(QStringLiteral("Failed to register FakeManager to DBus: %1") + .arg(QDBusConnection::sessionBus().lastError().message())); + } +} + +FakeManager::~FakeManager() +{ + QDBusConnection::sessionBus().unregisterObject(kManagerDBusPath); + qDeleteAll(mDevices); +} + +FakeDevice *FakeManager::addDevice(std::unique_ptr device) +{ + auto ptr = device.release(); + mDevices.insert(ptr->uid(), ptr); + Q_EMIT DeviceAdded(ptr->dbusPath()); + return ptr; +} + +void FakeManager::removeDevice(const QString &uid) +{ + auto deviceIt = mDevices.find(uid); + if (deviceIt == mDevices.end()) { + return; + } + auto device = *deviceIt; + mDevices.erase(deviceIt); + Q_EMIT DeviceRemoved(device->dbusPath()); + device->deleteLater(); +} + +QList FakeManager::devices() const +{ + return mDevices.values(); +} + +unsigned int FakeManager::version() const +{ + return 1; +} + +bool FakeManager::isProbing() const +{ + return mProbing; +} + +QString FakeManager::defaultPolicy() const +{ + return mDefaultPolicy; +} + +QString FakeManager::securityLevel() const +{ + return mSecurityLevel; +} + +QString FakeManager::authMode() const +{ + return mAuthMode; +} + +void FakeManager::setAuthMode(const QString &authMode) +{ + qDebug("Manager: authMode changed to %s", qUtf8Printable(authMode)); + mAuthMode = authMode; +} + +QList FakeManager::ListDevices() const +{ + QList rv; + rv.reserve(mDevices.size()); + for (const auto device : mDevices) { + rv.push_back(device->dbusPath()); + } + return rv; +} + +QDBusObjectPath FakeManager::DeviceByUid(const QString &uid) const +{ + auto device = mDevices.value(uid); + return device ? device->dbusPath() : QDBusObjectPath(); +} + +QDBusObjectPath FakeManager::EnrollDevice(const QString &uid, + const QString &policy, + const QString &flags) +{ + std::this_thread::sleep_for(1s); // simulate this operation taking time + + auto device = mDevices.value(uid); + if (policy == QLatin1Literal("default")) { + device->setPolicy(defaultPolicy()); + } else { + device->setPolicy(policy); + } + device->setAuthFlags(flags); + device->setStored(true); + device->setStatus(QLatin1Literal("authorized")); + + return device->dbusPath(); +} + +void FakeManager::ForgetDevice(const QString &uid) +{ + std::this_thread::sleep_for(1s); // simulate this operation taking time + + auto device = mDevices.value(uid); + device->setStored(false); + device->setStatus(QLatin1Literal("connected")); +} + diff --git a/autotests/bolt/fakeserver/fakeserver.h b/autotests/bolt/fakeserver/fakeserver.h new file mode 100644 --- /dev/null +++ b/autotests/bolt/fakeserver/fakeserver.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FAKESERVER_H +#define FAKESERVER_H + +#include +#include + +class FakeServerException : public std::runtime_error +{ +public: + FakeServerException(const char *what) + : std::runtime_error(what) {} + FakeServerException(const QString &what) + : std::runtime_error(what.toStdString()) {} +}; + +class FakeManager; +class FakeServer +{ +public: + explicit FakeServer(const QString &file); + explicit FakeServer(); + ~FakeServer(); + + static void enableFakeEnv(); + + FakeManager *manager() const; +private: + QScopedPointer mManager; +}; + +#endif diff --git a/autotests/bolt/fakeserver/fakeserver.cpp b/autotests/bolt/fakeserver/fakeserver.cpp new file mode 100644 --- /dev/null +++ b/autotests/bolt/fakeserver/fakeserver.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "fakeserver.h" +#include "fakemanager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +static const QString OrgKdeFakebolt = QStringLiteral("org.kde.fakebolt"); + +} + +FakeServer::FakeServer(const QString &filename) +{ + QFile jsonFile(filename); + if (!jsonFile.open(QIODevice::ReadOnly)) { + qCritical("Failed to open file %s: %s", qUtf8Printable(filename), qUtf8Printable(jsonFile.errorString())); + throw FakeServerException( + QStringLiteral("Failed to open file %1: %2").arg(filename, jsonFile.errorString())); + } + + const auto doc = QJsonDocument::fromJson(jsonFile.readAll()); + + if (!QDBusConnection::sessionBus().registerService(OrgKdeFakebolt)) { + throw FakeServerException( + QStringLiteral("Failed to register org.kde.fakebolt service: %1") + .arg(QDBusConnection::sessionBus().lastError().message())); + } + + try { + mManager.reset(new FakeManager(doc.object())); + } catch (const FakeManagerException &e) { + throw FakeServerException(e.what()); + } +} + +FakeServer::FakeServer() +{ + if (!QDBusConnection::sessionBus().registerService(OrgKdeFakebolt)) { + throw FakeServerException( + QStringLiteral("Failed to register org.kde.fakebolt service: %1") + .arg(QDBusConnection::sessionBus().lastError().message())); + } + + try { + mManager.reset(new FakeManager()); + } catch (FakeManagerException &e) { + throw FakeServerException(e.what()); + } +} + +FakeServer::~FakeServer() +{ +} + +void FakeServer::enableFakeEnv() +{ + qputenv("KBOLT_FAKE", "1"); +} + +FakeManager *FakeServer::manager() const +{ + return mManager.get(); +} diff --git a/autotests/bolt/fakeserver/main.cpp b/autotests/bolt/fakeserver/main.cpp new file mode 100644 --- /dev/null +++ b/autotests/bolt/fakeserver/main.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "fakeserver.h" + +int main(int argc, char **argv) +{ + QCoreApplication::setOrganizationName(QStringLiteral("KDE")); + QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); + QCoreApplication::setApplicationName(QStringLiteral("fakeserver")); + + QCoreApplication app(argc, argv); + + QCommandLineParser parser; + QCommandLineOption cfgOption(QStringLiteral("cfg"), QStringLiteral("Config file"), QStringLiteral("FILE")); + parser.addOption(cfgOption); + parser.addHelpOption(); + parser.process(app); + if (!parser.isSet(cfgOption)) { + std::cout << "Missing option --cfg" << std::endl; + parser.showHelp(); + return 0; + } + + try { + FakeServer server(parser.value(cfgOption)); + return app.exec(); + } catch (const FakeServerException &e) { + std::cerr << e.what() << std::endl; + return -1; + } +} diff --git a/autotests/bolt/kded/CMakeLists.txt b/autotests/bolt/kded/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/autotests/bolt/kded/CMakeLists.txt @@ -0,0 +1,17 @@ +include(ECMAddTests) + +include_directories( + ${CMAKE_SOURCE_DIR}/kded/bolt + ${CMAKE_BINARY_DIR}/kded/bolt +) + +ecm_add_test( + kdedtest.cpp + ${CMAKE_SOURCE_DIR}/kded/bolt/kded_bolt.cpp + ${CMAKE_BINARY_DIR}/kded/bolt/kded_bolt_debug.cpp + TEST_NAME kdedtest + LINK_LIBRARIES kbolt libboltfakeserver Qt5::Test Qt5::Core Qt5::DBus KF5::CoreAddons KF5::DBusAddons KF5::I18n KF5::Notifications + NAME_PREFIX kbolt-kded- +) +set_target_properties(kdedtest PROPERTIES CXX_STANDARD 14) + diff --git a/autotests/bolt/kded/kdedtest.cpp b/autotests/bolt/kded/kdedtest.cpp new file mode 100644 --- /dev/null +++ b/autotests/bolt/kded/kdedtest.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "fakeserver.h" +#include "fakemanager.h" +#include "fakedevice.h" + +#include "device.h" + +#include "kded_bolt.h" + +#include + +class TestableKDEDBolt : public KDEDBolt +{ + Q_OBJECT +public: + using KDEDBolt::KDEDBolt; + +Q_SIGNALS: + void deviceNotify(const QVector> &device); + +protected: + void notify() override + { + Q_EMIT deviceNotify(sortDevices(mPendingDevices)); + } +}; + + +class KDEDTest : public QObject +{ + Q_OBJECT +public: + KDEDTest() + : QObject() + { + FakeServer::enableFakeEnv(); + qRegisterMetaType>(); + } + +private Q_SLOTS: + void testShouldNotify() + { + QScopedPointer fakeServer; + try { + fakeServer.reset(new FakeServer); + } catch (const FakeServerException &e) { + qWarning("Fake server exception: %s", e.what()); + QFAIL("Caught server exception"); + } + + TestableKDEDBolt kded(nullptr, {}); + QSignalSpy notifySpy(&kded, &TestableKDEDBolt::deviceNotify); + QVERIFY(notifySpy.isValid()); + + // Add unauthorized device + auto fakeManager = fakeServer->manager(); + try { + auto fakeDevice = std::make_unique(QStringLiteral("Device1")); + fakeDevice->setStatus(QStringLiteral("connected")); + fakeDevice->setAuthFlags(QStringLiteral("none")); + fakeManager->addDevice(std::move(fakeDevice)); + + QTRY_COMPARE(notifySpy.size(), 1); + const auto devices = notifySpy[0][0].value>>(); + QCOMPARE(devices.size(), 1); + const auto device = devices.front(); + QCOMPARE(device->uid(), QStringLiteral("Device1")); + QCOMPARE(device->authFlags(), Bolt::Auth::None); + QCOMPARE(device->status(), Bolt::Status::Connected); + } catch (const FakeDeviceException &e) { + qWarning("Fake device exception: %s", e.what()); + QFAIL("Caught device exception"); + } + + // Add authorized device + notifySpy.clear(); + try { + auto fakeDevice = std::make_unique(QStringLiteral("Device2")); + fakeDevice->setStatus(QStringLiteral("authorized")); + fakeDevice->setAuthFlags(QStringLiteral("nokey | boot")); + fakeManager->addDevice(std::move(fakeDevice)); + + QTest::qWait(200); + QVERIFY(notifySpy.empty()); + } catch (const FakeDeviceException &e) { + qWarning("Fake device exception: %s", e.what()); + QFAIL("Caught device exception"); + } + } +}; + +QTEST_MAIN(KDEDTest) + +#include "kdedtest.moc" diff --git a/autotests/bolt/lib/CMakeLists.txt b/autotests/bolt/lib/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/autotests/bolt/lib/CMakeLists.txt @@ -0,0 +1,14 @@ +include(ECMAddTests) + +macro(add_libkbolt_test name) + ecm_add_test( + ${name}.cpp + LINK_LIBRARIES kbolt libboltfakeserver Qt5::Test Qt5::Core Qt5::DBus + NAME_PREFIX libkbolt- + ) + set_target_properties(${name} PROPERTIES CXX_STANDARD 14) +endmacro() + +add_libkbolt_test(managertest) +add_libkbolt_test(devicetest) + diff --git a/autotests/bolt/lib/devicetest.cpp b/autotests/bolt/lib/devicetest.cpp new file mode 100644 --- /dev/null +++ b/autotests/bolt/lib/devicetest.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "fakeserver.h" +#include "fakemanager.h" +#include "fakedevice.h" + +#include "device.h" +#include "manager.h" + +#include + +class DeviceTest : public QObject +{ + Q_OBJECT +public: + DeviceTest() + : QObject() + { + FakeServer::enableFakeEnv(); + qRegisterMetaType>(); + } + +private Q_SLOTS: + void testAuthorize() + { + QScopedPointer fakeServer; + try { + fakeServer.reset(new FakeServer); + } catch (const FakeServerException &e) { + qWarning("Fake server exception: %s", e.what()); + QFAIL("Caught server exception"); + } + + auto fakeManager = fakeServer->manager(); + FakeDevice *fakeDevice = nullptr; + try { + fakeDevice = fakeManager->addDevice( + std::make_unique(QStringLiteral("Device1"))); + } catch (const FakeDeviceException &e) { + qWarning("Fake device exception: %s", e.what()); + QFAIL("Caught device exception"); + } + fakeDevice->setAuthFlags(QStringLiteral("none")); + + Bolt::Manager manager; + QVERIFY(manager.isAvailable()); + + auto device = manager.device(fakeDevice->uid()); + QVERIFY(device); + QCOMPARE(device->authFlags(), Bolt::Auth::None); + device->authorize(Bolt::Auth::NoKey | Bolt::Auth::Boot); + + QTRY_COMPARE(fakeDevice->authFlags(), QStringLiteral("nokey | boot")); + } +}; + +#include "devicetest.moc" + +QTEST_GUILESS_MAIN(DeviceTest) diff --git a/autotests/bolt/lib/managertest.cpp b/autotests/bolt/lib/managertest.cpp new file mode 100644 --- /dev/null +++ b/autotests/bolt/lib/managertest.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "fakeserver.h" +#include "fakemanager.h" +#include "fakedevice.h" + +#include "manager.h" +#include "device.h" + +#include + +Q_DECLARE_METATYPE(QSharedPointer) + +class ManagerTest : public QObject +{ + Q_OBJECT +public: + ManagerTest() + : QObject() + { + FakeServer::enableFakeEnv(); + qRegisterMetaType>(); + } + +private Q_SLOTS: + void testDeviceAddedRemoved() + { + QScopedPointer server; + try { + server.reset(new FakeServer); + } catch (const FakeServerException &e) { + qWarning("Fake server exception: %s", e.what()); + QFAIL("Exception server caught"); + } + + auto fakeManager = server->manager(); + + Bolt::Manager manager; + QVERIFY(manager.isAvailable()); + + QSignalSpy addSpy(&manager, &Bolt::Manager::deviceAdded); + QVERIFY(addSpy.isValid()); + + FakeDevice *fakeDevice = nullptr; + try { + fakeDevice = fakeManager->addDevice( + std::make_unique(QStringLiteral("device1"))); + } catch (const FakeDeviceException &e) { + qWarning("Fake device exception: %s", e.what()); + QFAIL("Caught device exception"); + } + QTRY_COMPARE(addSpy.size(), 1); + auto device = addSpy.first().first().value>(); + QCOMPARE(device->uid(), fakeDevice->uid()); + + QSignalSpy removeSpy(&manager, &Bolt::Manager::deviceRemoved); + QVERIFY(removeSpy.isValid()); + fakeManager->removeDevice(fakeDevice->uid()); + QTRY_COMPARE(removeSpy.size(), 1); + QCOMPARE(removeSpy.first().first().value>(), device); + } +}; + +QTEST_GUILESS_MAIN(ManagerTest) + +#include "managertest.moc" diff --git a/cmake/modules/FindBolt.cmake b/cmake/modules/FindBolt.cmake new file mode 100644 --- /dev/null +++ b/cmake/modules/FindBolt.cmake @@ -0,0 +1,37 @@ +#============================================================================= +# Copyright 2019 Daniel Vrátil +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= +find_program(boltd_EXECUTABLE + NAMES boltd + PATH_SUFFIXES bin libexec +) +find_package_handle_standard_args(bolt + FOUND_VAR + bolt_FOUND + REQUIRED_VARS + boltd_EXECUTABLE +) +mark_as_advanced(boltd_EXECUTABLE) diff --git a/kcms/CMakeLists.txt b/kcms/CMakeLists.txt --- a/kcms/CMakeLists.txt +++ b/kcms/CMakeLists.txt @@ -19,6 +19,7 @@ endif() add_subdirectory( access ) +add_subdirectory( bolt ) add_subdirectory( dateandtime ) add_subdirectory( autostart ) add_subdirectory( ksplash ) diff --git a/kcms/bolt/CMakeLists.txt b/kcms/bolt/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/kcms/bolt/CMakeLists.txt @@ -0,0 +1,23 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_bolt\") +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +set(KCM_BOLT_SRCS + kcm_bolt.cpp +) + +add_library(kcm_bolt MODULE ${KCM_BOLT_SRCS}) +set_target_properties(kcm_bolt PROPERTIES CXX_STANDARD 14) +target_link_libraries(kcm_bolt + KF5::QuickAddons + KF5::I18n + KF5::Declarative + kbolt +) + +kcoreaddons_desktop_to_json(kcm_bolt "kcm_bolt.desktop" SERVICE_TYPES kcmodule.desktop) + +install(FILES kcm_bolt.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) +install(TARGETS kcm_bolt DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) + +kpackage_install_package(package kcm_bolt kcms) + diff --git a/kcms/bolt/Messages.sh b/kcms/bolt/Messages.sh new file mode 100755 --- /dev/null +++ b/kcms/bolt/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name "*.cpp" -o -name "*.qml" -o $podir/kcm_bolt.pot + diff --git a/kcms/bolt/kcm_bolt.h b/kcms/bolt/kcm_bolt.h new file mode 100644 --- /dev/null +++ b/kcms/bolt/kcm_bolt.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KCMBOLT_H +#define KCMBOLT_H + +#include + +class KCMBolt : public KQuickAddons::ConfigModule +{ + Q_OBJECT + +public: + KCMBolt(QObject *parent, const QVariantList &args); + ~KCMBolt() override = default; + +}; + +#endif diff --git a/kcms/bolt/kcm_bolt.cpp b/kcms/bolt/kcm_bolt.cpp new file mode 100644 --- /dev/null +++ b/kcms/bolt/kcm_bolt.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "kcm_bolt.h" + +#include +#include +#include + +#include "device.h" +#include "devicemodel.h" +#include "manager.h" +#include "enum.h" + +#include + +K_PLUGIN_FACTORY_WITH_JSON(KCMBoltFactory, "kcm_bolt.json", registerPlugin();) + +class QMLHelper : public QObject +{ + Q_OBJECT +public: + explicit QMLHelper(QObject *parent = nullptr) + : QObject(parent) + {} + +public Q_SLOTS: + void authorizeDevice(Bolt::Device *device, Bolt::AuthFlags authFlags, + QJSValue successCb = {}, QJSValue errorCb = {}) + { + device->authorize(authFlags, invoke(successCb), invoke(errorCb)); + } + + void enrollDevice(Bolt::Manager *manager, const QString &uid, Bolt::Policy policy, + Bolt::AuthFlags authFlags, QJSValue successCb = {}, QJSValue errorCb = {}) + { + manager->enrollDevice(uid, policy, authFlags, invoke(successCb), invoke(errorCb)); + } + + void forgetDevice(Bolt::Manager *manager, const QString &uid, QJSValue successCb, QJSValue errorCb) + { + manager->forgetDevice(uid, invoke(successCb), invoke(errorCb)); + } + +private: + template + std::function invoke(QJSValue cb_) + { + return [cb = std::move(cb_)](Args && ... args) mutable { + Q_ASSERT(cb.isCallable()); + cb.call({std::forward(args) ...}); + }; + } +}; + +KCMBolt::KCMBolt(QObject *parent, const QVariantList &args) + : KQuickAddons::ConfigModule(parent, args) +{ + qmlRegisterType("org.kde.bolt", 0, 1, "DeviceModel"); + qmlRegisterType("org.kde.bolt", 0, 1, "Manager"); + qmlRegisterUncreatableType("org.kde.bolt", 0, 1, "Device", + QStringLiteral("Use DeviceModel")); + qmlRegisterUncreatableMetaObject(Bolt::staticMetaObject, "org.kde.bolt", 0, 1, "Bolt", + QStringLiteral("For enums and flags only")); + qmlRegisterSingletonType("org.kde.bolt", 0, 1, "QMLHelper", + [](auto, auto) -> QObject* { return new QMLHelper(); }); + + auto about = new KAboutData(QStringLiteral("kcm_bolt"), + i18n("Thunderbolt Device Management"), + QStringLiteral("0.1"), + i18n("System Settings module for managing Thunderbolt devices."), + KAboutLicense::GPL); + about->addAuthor(i18n("Daniel Vrátil"), {}, QStringLiteral("dvratil@kde.org")); + setAboutData(about); +} + +#include "kcm_bolt.moc" diff --git a/kcms/bolt/kcm_bolt.desktop b/kcms/bolt/kcm_bolt.desktop new file mode 100644 --- /dev/null +++ b/kcms/bolt/kcm_bolt.desktop @@ -0,0 +1,15 @@ +[Desktop Entry] +Icon= +Exec=kcmshell5 kcm_bolt +Type=Service +X-KDE-ServiceTypes=KCModule +X-KDE-Library=kcm_bolt +X-KDE-ParentApp=kcontrol + +X-KDE-System-Settings-Parent-Category=Hardware +X-KDE-Weight=120 + +Name=Thunderbolt +Comment=Thunderbolt Device Management +X-KDE-Keywords=thunderbolt,hardware,device +Categories=Qt;KDE;X-KDE-settings-system diff --git a/kcms/bolt/package/contents/ui/DeviceList.qml b/kcms/bolt/package/contents/ui/DeviceList.qml new file mode 100644 --- /dev/null +++ b/kcms/bolt/package/contents/ui/DeviceList.qml @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.7 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 + +import org.kde.kirigami 2.4 as Kirigami +import org.kde.kquickcontrolsaddons 2.0 as KQCAddons +import org.kde.kcm 1.2 as KCM +import org.kde.bolt 0.1 as Bolt +import "utils.js" as Utils + +KCM.ScrollViewKCM { + id: page + + property Bolt.DeviceModel deviceModel: null + + signal itemClicked(Bolt.Device device) + + header: RowLayout { + CheckBox { + id: enableBox + text: i18n("Enable Thunderbolt devices") + + checked: deviceModel.manager.authMode == Bolt.Bolt.AuthMode.Enabled + + onToggled: { + deviceModel.manager.authMode = enableBox.checked + ? Bolt.Bolt.AuthMode.Enabled + : Bolt.Bolt.AuthMode.Disabled + } + } + } + + view: ListView { + id: view + model: deviceModel + enabled: enableBox.checked + + property int _evalTrigger: 0 + + Timer { + interval: 2000 + running: view.visible + repeat: true + onTriggered: view._evalTrigger++; + } + + delegate: Kirigami.AbstractListItem { + id: item + width: view.width + + RowLayout { + id: layout + spacing: Kirigami.Units.smallSpacing * 2 + property bool indicateActiveFocus: item.pressed || Kirigami.Settings.tabletMode || item.activeFocus || (item.ListView.view ? item.ListView.view.activeFocus : false) + + BusyIndicator { + id: busyIndicator + visible: model.device.status == Bolt.Bolt.Status.Authorizing + running: visible + Layout.minimumHeight: Kirigami.Units.iconSizes.smallMedium + Layout.maximumHeight: Layout.minimumHeight + Layout.minimumWidth: height + } + + Label { + id: label + text: model.device.label + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.smallSpacing + Layout.bottomMargin: Kirigami.Units.smallSpacing + color: parent.indicateActiveFocus && (item.highlighted || item.checked || item.pressed) ? item.activeTextColor : item.textColor + elide: Text.ElideRight + font: item.font + } + + Label { + id: statusLabel + + Layout.alignment: Qt.AlignRight + + text: view._evalTrigger, Utils.deviceStatus(model.device, true) + } + } + + onClicked: { + page.itemClicked(model.device) + } + } + } +} diff --git a/kcms/bolt/package/contents/ui/DeviceView.qml b/kcms/bolt/package/contents/ui/DeviceView.qml new file mode 100644 --- /dev/null +++ b/kcms/bolt/package/contents/ui/DeviceView.qml @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.7 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 + +import org.kde.kirigami 2.4 as Kirigami +import org.kde.kquickcontrolsaddons 2.0 as KQCAddons +import org.kde.bolt 0.1 as Bolt +import "utils.js" as Utils + +Kirigami.ScrollablePage { + id: page + + property Bolt.Manager manager: null + property Bolt.Device device: null + + property int _evalTrigger: 0 + + Timer { + interval: 2000 + running: device != null + repeat: true + onTriggered: page._evalTrigger++; + } + + ColumnLayout { + spacing: Kirigami.Units.smallSpacing * 5 + + RowLayout { + Button { + icon.name: "draw-arrow-back" + visible: !pageRow.wideMode + onClicked: pageRow.pop() + } + + Kirigami.Heading { + level: 2 + text: _evalTrigger, device ? device.name : "" + } + } + + Kirigami.InlineMessage { + id: errorMessage + + Layout.fillWidth: true + + type: Kirigami.MessageType.Error + showCloseButton: true + + function show(msg) { + text = msg; + visible = true; + } + } + + Kirigami.FormLayout { + Label { + text: _evalTrigger, device ? device.vendor : "" + Kirigami.FormData.label: i18n("Vendor:") + } + Label { + text: _evalTrigger, device ? device.uid : "" + Kirigami.FormData.label: i18n("UID:") + } + Label { + text: _evalTrigger, device ? Utils.deviceStatus(device, false) : "" + Kirigami.FormData.label: i18n("Status:") + } + Label { + visible: device && device.status == Bolt.Bolt.Status.Authorized + text: _evalTrigger, device ? Qt.formatDateTime(device.authorizeTime) : "" + Kirigami.FormData.label: i18n("Authorized at:") + } + Label { + visible: device && device.status == Bolt.Bolt.Status.Connected + text: _evalTrigger, device ? Qt.formatDateTime(device.connectTime) : "" + Kirigami.FormData.label: i18n("Connected at:") + } + Label { + visible: device && device.status == Bolt.Bolt.Status.Disconnected + text: _evalTrigger, device ? Qt.formatDateTime(device.storeTime) : "" + Kirigami.FormData.label: i18n("Enrolled at:") + } + Label { + visible: device && (device.status == Bolt.Bolt.Status.Authorized || device.status == Bolt.Bolt.Status.Disconnected) + text: _evalTrigger, device && device.stored ? i18n("Yes") : i18n("No") + Kirigami.FormData.label: i18n("Trusted:") + } + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + + Button { + id: authorizeBtn + text: device && device.status == Bolt.Bolt.Status.Authorizing ? i18n("Authorizing...") : i18n("Authorize") + enabled: device && device.status != Bolt.Bolt.Status.Authorizing + visible: device && (device.status == Bolt.Bolt.Status.Connected || device.status == Bolt.Bolt.Status.AuthError) + onClicked: { + Bolt.QMLHelper.enrollDevice( + manager, device.uid, Bolt.Bolt.Policy.Default, + Bolt.Bolt.Auth.Boot | Bolt.Bolt.Auth.NoKey, + function() { + console.log("Thunderbolt device " + device.uid + " (" + device.name + ") enrolled successfully"); + }, + function(error) { + errorMessage.show(i18n("Failed to enroll device %1: %2", device.name, error)); + } + ); + } + } + Button { + id: storeBtn + text: i18n("Trust this Device") + visible: device && device.status == Bolt.Bolt.Status.Authorized && device.stored == false + onClicked: { + enabled = false; + Bolt.QMLHelper.enrollDevice( + manager, device.uid, Bolt.Bolt.Policy.Default, + Bolt.Bolt.Auth.Boot | Bolt.Bolt.Auth.NoKey, + function() { + enabled = true; + console.log("Thunderbolt Device " + device.uid + " (" + device.name + ") enrolled successfully"); + }, + function(error) { + enabled = true; + errorMessage.show(i18n("Failed to enroll device %1: %2", device.name, error)); + } + ); + } + } + + Button { + id: forgetBtn + text: i18n("Revoke Trust") + visible: device && device.stored + onClicked: { + enabled = false + Bolt.QMLHelper.forgetDevice( + manager, device.uid, + function() { + enabled = true; + console.log("Device " + device.uid + " successfully forgotten."); + }, + function(error) { + enabled = true; + errorMessage.show(i18n("Error changing device trust: %1: %2", device.name, error)); + } + ); + // If the device is not connected it will cease to exist + // once forgotten, so we should pop this view + if (device.status == Bolt.Bolt.Status.Disconnected) { + pageRow.pop(); + } + } + } + } + + Label { + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + + text: device && !device.stored + ? qsTr("Hint: trusted device will be automatically authorized the next time it is connected to the computer.") + : qsTr("Hint: an untrusted device needs to be manually authorized each time it is connected to the computer.") + visible: storeBtn.visible || forgetBtn.visible + wrapMode: Text.WordWrap + horizontalAlignment: Qt.AlignHCenter + } + } +} diff --git a/kcms/bolt/package/contents/ui/main.qml b/kcms/bolt/package/contents/ui/main.qml new file mode 100644 --- /dev/null +++ b/kcms/bolt/package/contents/ui/main.qml @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.7 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 + +import org.kde.kirigami 2.4 as Kirigami +import org.kde.kquickcontrolsaddons 2.0 as KQCAddons +import org.kde.kcm 1.2 as KCM +import org.kde.bolt 0.1 as Bolt +import "utils.js" as Utils + +Kirigami.Page { + KCM.ConfigModule.quickHelp: i18n("This module allows you to manage Thunderbolt devices connected to your computer."); + id: root + + title: kcm.name + implicitWidth: Kirigami.Units.gridUnit * 20 + implicitHeight: pageRow.contentHeight > 0 ? Math.min(pageRow.contentHeight, Kirigami.Units.gridUnit * 20) + : Kirigami.Units.gridUnit * 20 + + Bolt.Manager { + id: boltManager + } + + Kirigami.PageRow { + id: pageRow + clip: true + anchors.fill: parent + + Component.onCompleted: { + if (boltManager.isAvailable) { + if (boltManager.securityLevel == Bolt.Bolt.Security.DPOnly + || boltManager.securityLevel == Bolt.Bolt.Security.USBOnly) { + pageRow.push(noBoltPage, { text: i18n("Thunderbolt support has been disabled in BIOS") }) + } else { + pageRow.push(deviceList, { manager: boltManager }) + } + } else { + pageRow.push(noBoltPage, { text: i18n("Thunderbolt subsystem is not available") }) + } + } + } + + Component { + id: noBoltPage + Kirigami.Page { + property alias text: label.text + Label { + id: label + + anchors.fill: parent + verticalAlignment: Qt.AlignVCenter + horizontalAlignment: Qt.AlignHCenter + } + } + } + + Component { + id: deviceList + DeviceList { + property alias manager: model.manager + deviceModel: Bolt.DeviceModel { + id: model + showHosts: false + } + + anchors.fill: parent + + onItemClicked: function(device) { + pageRow.push(deviceView, { manager: manager, device: device }) + } + } + } + + Component { + id: deviceView + DeviceView { + anchors.fill: parent + } + } +} diff --git a/kcms/bolt/package/contents/ui/utils.js b/kcms/bolt/package/contents/ui/utils.js new file mode 100644 --- /dev/null +++ b/kcms/bolt/package/contents/ui/utils.js @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +function deviceStatus(device, withStored) +{ + var status = device.status; + var str = ""; + if (status == Bolt.Bolt.Status.Disconnected) { + str = i18n("Disconnected"); + } else if (status == Bolt.Bolt.Status.Connecting) { + str = i18n("Connecting"); + } else if (status == Bolt.Bolt.Status.Connected) { + str = i18n("Connected"); + } else if (status == Bolt.Bolt.Status.AuthError) { + str = i18n("Authorization Error"); + } else if (status == Bolt.Bolt.Status.Authorizing) { + str = i18n("Authorizing"); + } else if (status == Bolt.Bolt.Status.Authorized) { + if (device.authFlags & Bolt.Bolt.Auth.NoPCIE) { + str = i18n("Reduced Functionality"); + } else { + str = i18n("Connected & Authorized"); + } + } + if (withStored && device.stored) { + if (str != "") { + str += ", "; + } + str += i18n("Trusted"); + } + + return str; +} diff --git a/kcms/bolt/package/metadata.desktop b/kcms/bolt/package/metadata.desktop new file mode 100644 --- /dev/null +++ b/kcms/bolt/package/metadata.desktop @@ -0,0 +1,16 @@ +[Desktop Entry] +Name=Thunderbolt +Comment=Thunderbolt Device Management +Icon= +Keywords= +Type=Service +X-KDE-ParentApp= +X-KDE-PluginInfo-Author=Daniel Vrátil +X-KDE-PluginInfo-Email=dvratil@kde.org +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=kcm_bolt +X-KDE-PluginInfo-Version= +X-KDE-PluginInfo-Website= +X-KDE-ServiceTypes=Plasma/Generic +X-Plasma-API=declarativeappletscript +X-Plasma-MainScript=ui/main.qml diff --git a/kded/CMakeLists.txt b/kded/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/kded/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(bolt) diff --git a/kded/bolt/CMakeLists.txt b/kded/bolt/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/kded/bolt/CMakeLists.txt @@ -0,0 +1,30 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kded_bolt\") + +include_directories( + ${CMAKE_CURRENT_BINARY_DIR} +) + +set(KDED_BOLT_SRCS + main.cpp + kded_bolt.cpp +) + +ecm_qt_declare_logging_category(KDED_BOLT_SRCS + HEADER kded_bolt_debug.h + IDENTIFIER log_kded_bolt + CATEGORY_NAME org.kde.bolt.kded +) + +add_library(kded_bolt MODULE ${KDED_BOLT_SRCS}) +set_target_properties(kded_bolt PROPERTIES CXX_STANDARD 14) +kcoreaddons_desktop_to_json(kded_bolt kded_bolt.desktop) +target_link_libraries(kded_bolt + KF5::DBusAddons + KF5::I18n + KF5::Notifications + KF5::CoreAddons + kbolt +) + +install(TARGETS kded_bolt DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf5/kded) +install(FILES kded_bolt.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR}) diff --git a/kded/bolt/kded_bolt.h b/kded/bolt/kded_bolt.h new file mode 100644 --- /dev/null +++ b/kded/bolt/kded_bolt.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KDED_BOLT_H +#define KDED_BOLT_H + +#include "manager.h" + +#include + +#include +#include +#include +#include + +class KNotification; +namespace Bolt { +class Device; +} + +class Q_DECL_EXPORT KDEDBolt : public KDEDModule +{ + Q_OBJECT + +public: + using BoltDeviceList = QVector>; + + KDEDBolt(QObject *parent, const QVariantList &args); + ~KDEDBolt() override; + +protected: + virtual void notify(); + + BoltDeviceList sortDevices(const BoltDeviceList &devices); + +private: + enum AuthMode { + Enroll, + Authorize + }; + void authorizeDevices(BoltDeviceList devices, AuthMode mode); + +protected: + Bolt::Manager mManager; + BoltDeviceList mPendingDevices; + QMap mNotifiedDevices; + QTimer mPendingDeviceTimer; +}; + +#endif // KDED_BOLT_H diff --git a/kded/bolt/kded_bolt.cpp b/kded/bolt/kded_bolt.cpp new file mode 100644 --- /dev/null +++ b/kded/bolt/kded_bolt.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "kded_bolt.h" +#include "kded_bolt_debug.h" + +#include "device.h" + +#include + +#include +#include + +#include + +using namespace std::chrono_literals; + +KDEDBolt::KDEDBolt(QObject *parent, const QVariantList &) + : KDEDModule(parent) +{ + if (!mManager.isAvailable()) { + qCInfo(log_kded_bolt, "Couldn't connect to Bolt DBus daemon"); + return; + } + + mPendingDeviceTimer.setSingleShot(true); + mPendingDeviceTimer.setInterval(500ms); + connect(&mPendingDeviceTimer, &QTimer::timeout, this, &KDEDBolt::notify); + + connect(&mManager, &Bolt::Manager::deviceAdded, + this, [this](const QSharedPointer &device) { + // Already authorized, nothing else to do here + if (device->status() == Bolt::Status::Authorized) { + return; + } + + mPendingDevices.append(device); + mPendingDeviceTimer.start(); + }); + connect(&mManager, &Bolt::Manager::deviceRemoved, + this, [this](const QSharedPointer &device) { + // Check if maybe the device is in pending or currently active + // notification, remove it if so. + mPendingDevices.removeOne(device); + Q_ASSERT(!mPendingDevices.removeOne(device)); + + for (auto it = mNotifiedDevices.begin(), end = mNotifiedDevices.end(); it != end; ++it) { + if (it->contains(device)) { + auto devices = *it; + devices.removeOne(device); + mPendingDevices += devices; + mPendingDeviceTimer.start(); + } + it.key()->close(); + } + }); +} + +KDEDBolt::~KDEDBolt() +{ +} + +void KDEDBolt::notify() +{ + auto ntf = KNotification::event( + QStringLiteral("unauthorizedDeviceConnected"), + i18n("New Thunderbolt Device Detected"), + mPendingDevices.size() == 1 + ? i18n("Unauthorized Thunderbolt device %1 was detected. Do you want to authorize it?", mPendingDevices.front()->name()) + : i18np("%1 unauthorized Thunderbolt device was detected. Do you want to authorize it?", + "%1 unauthorized Thunderbolt devices were detected. Do you want to authorize them?", + mPendingDevices.size()), + /*icon*/ QPixmap{}, /* widget */ nullptr, + KNotification::Persistent, + QStringLiteral("kded_bolt")); + ntf->setActions({ + i18n("Authorize Now"), + i18n("Authorize Permanently") + }); + mNotifiedDevices.insert(ntf, mPendingDevices); + connect(ntf, &KNotification::action1Activated, + this, [this, ntf, devices = mPendingDevices]() { + authorizeDevices(sortDevices(devices), Authorize); + }); + connect(ntf, &KNotification::action2Activated, + this, [this, ntf, devices = mPendingDevices]() { + authorizeDevices(sortDevices(devices), Enroll); + }); + connect(ntf, &KNotification::closed, + this, [this, ntf]() { + mNotifiedDevices.remove(ntf); + }); + + mPendingDevices.clear(); +} + +KDEDBolt::BoltDeviceList KDEDBolt::sortDevices(const BoltDeviceList &devices) +{ + QVector> sorted; + sorted.reserve(devices.size()); + + // Sort the devices so that parents go before their children. Probably + // fairly inefficient but there's rarely more than a couple of items. + for (const auto &device : devices) { + auto child = std::find_if(sorted.begin(), sorted.end(), [device](const auto &d) { return d->parent() == device->uid(); }); + auto parent = std::find_if(sorted.begin(), child, [device](const auto &d) { return device->parent() == d->uid(); }); + if (parent != sorted.end()) { + ++parent; + } + sorted.insert(parent, device); + } + + return sorted; +} + +void KDEDBolt::authorizeDevices(BoltDeviceList devices, AuthMode mode) +{ + if (devices.empty()) { + return; + } + + const auto device = devices.takeFirst(); + + const auto okCb = [this, devices, mode]() { + authorizeDevices(std::move(devices), mode); + }; + const auto errCb = [device](const QString &error) { + KNotification::event( + QStringLiteral("deviceAuthError"), + i18n("Thunderbolt Device Authorization Error"), + i18n("Failed to authorize Thunderbolt device %1: %2", device->name().toHtmlEscaped(), error), + /* icon */ QPixmap{}, /* parent */ nullptr, + KNotification::CloseOnTimeout, + QStringLiteral("kded_bolt")); + }; + if (mode == Enroll) { + mManager.enrollDevice(device->uid(), Bolt::Policy::Auto, Bolt::Auth::Boot | Bolt::Auth::NoKey, okCb, errCb); + } else { + device->authorize(Bolt::Auth::Boot | Bolt::Auth::NoKey, okCb, errCb); + } +} + diff --git a/kded/bolt/kded_bolt.desktop b/kded/bolt/kded_bolt.desktop new file mode 100644 --- /dev/null +++ b/kded/bolt/kded_bolt.desktop @@ -0,0 +1,18 @@ +[Desktop Entry] +Name=Thunderbolt Device Monitor +Comment=Thunderbolt Device Monitor + +Type=Service +Icon= +X-KDE-ServiceTypes=KDEDModule +X-KDE-Library=kded_bolt +X-KDE-Kded-autoload=true +X-KDE-Kded-load-on-demand=false +X-KDE-Kded-phase=1 + +X-KDE-PluginInfo-Author=Daniel Vrátil +X-KDE-PluginInfo-Email=dvratil@kde.org +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=kded_bolt +X-KDE-PluginInfo-Version=0.1 + diff --git a/kded/bolt/kded_bolt.notifyrc b/kded/bolt/kded_bolt.notifyrc new file mode 100644 --- /dev/null +++ b/kded/bolt/kded_bolt.notifyrc @@ -0,0 +1,25 @@ +[Global] +IconName=device-notifier +Name=Thunderbolt Device Monitor +Comment=Thunderbolt Device Monitor + +[Context/warningnot] +Name=Warning +Comment=Used for warning notifications + +[Content/errornot] +Name=Error +Comment=Used for error notifications + + +[Event/unauthorizedDeviceConnected] +Name=Unauthorized device is plugged in +Comment=An unauthorized Thunderbolt device has been plugged into the computer +Contexts=warningnot +Action=Popup + +[Event/deviceAuthError] +Name=Error during device authorization +Comment=An error occurred while authorizing or blocking a Thunderbolt device +Contexts=errornot +Action=Popup diff --git a/kded/bolt/main.cpp b/kded/bolt/main.cpp new file mode 100644 --- /dev/null +++ b/kded/bolt/main.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "kded_bolt.h" + +#include + +K_PLUGIN_CLASS_WITH_JSON(KDEDBolt, "kded_bolt.json") + +#include "main.moc" diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/libs/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(bolt) diff --git a/libs/bolt/CMakeLists.txt b/libs/bolt/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/libs/bolt/CMakeLists.txt @@ -0,0 +1,40 @@ +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +kde_enable_exceptions() + +set(LIBKBOLT_SRCS + dbushelper.cpp + device.cpp + devicemodel.cpp + enum.cpp + manager.cpp +) + +ecm_qt_declare_logging_category(LIBKBOLT_SRCS + HEADER libkbolt_debug.h + IDENTIFIER log_libkbolt + CATEGORY_NAME org.kde.libkbolt +) + +qt5_add_dbus_interfaces( + LIBKBOLT_SRCS + + interfaces/org.freedesktop.bolt1.manager.xml + interfaces/org.freedesktop.bolt1.device.xml +) + +add_library(kbolt SHARED ${LIBKBOLT_SRCS}) +set_target_properties(kbolt PROPERTIES CXX_STANDARD 14) +generate_export_header(kbolt) +target_link_libraries(kbolt + Qt5::Core + Qt5::DBus + KF5::I18n +) + +target_include_directories(kbolt PUBLIC + $ + $ +) + +install(TARGETS kbolt DESTINATION ${KDE_INSTALL_LIBDIR}) diff --git a/libs/bolt/dbushelper.h b/libs/bolt/dbushelper.h new file mode 100644 --- /dev/null +++ b/libs/bolt/dbushelper.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef DBUSHELPER_H_ +#define DBUSHELPER_H_ + +#include +#include + +namespace KBolt { +class Device; +} + +namespace DBusHelper +{ + +QDBusConnection connection(); +QString serviceName(); + +using CallErrorCallback = std::function; +using CallOkCallback = std::function; +void handleCall(QDBusPendingCall call, CallOkCallback &&okCb, + CallErrorCallback &&errCb, QObject *parent); + +template +void call(QDBusAbstractInterface *iface, const QString &method, const V & ... args, + CallOkCallback &&okCb, CallErrorCallback &&errCb, QObject *parent = nullptr) +{ + handleCall(iface->asyncCall(method, args ...), + std::move(okCb), std::move(errCb), parent); +} + + + +} // namespace + +#endif diff --git a/libs/bolt/dbushelper.cpp b/libs/bolt/dbushelper.cpp new file mode 100644 --- /dev/null +++ b/libs/bolt/dbushelper.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "dbushelper.h" + +#include +#include +#include +#include + +namespace { + +bool isFakeEnv() +{ + return qEnvironmentVariableIsSet("KBOLT_FAKE"); +} + +} // namespace + +QDBusConnection DBusHelper::connection() +{ + if (isFakeEnv()) { + return QDBusConnection::sessionBus(); + } else { + return QDBusConnection::systemBus(); + } +} + +QString DBusHelper::serviceName() +{ + if (isFakeEnv()) { + return QStringLiteral("org.kde.fakebolt"); + } else { + return QStringLiteral("org.freedesktop.bolt"); + } +} + +void DBusHelper::handleCall(QDBusPendingCall call, CallOkCallback &&okCb, + CallErrorCallback &&errCb, QObject *parent) +{ + auto watcher = new QDBusPendingCallWatcher(call); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, + parent, [okCb = std::move(okCb), errCb = std::move(errCb)] + (QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + const QDBusPendingReply reply(*watcher); + if (reply.isError()) { + if (errCb) { + errCb(reply.error().message()); + } + } else if (okCb) { + okCb(); + } + }); +} diff --git a/libs/bolt/device.h b/libs/bolt/device.h new file mode 100644 --- /dev/null +++ b/libs/bolt/device.h @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef DEVICE_H_ +#define DEVICE_H_ + +#include +#include +#include +#include +#include + +#include + +#include "enum.h" +#include "kbolt_export.h" + +class OrgFreedesktopBolt1DeviceInterface; +namespace Bolt { + +class Manager; +class KBOLT_EXPORT Device : public QObject + , public QEnableSharedFromThis +{ + Q_OBJECT + + Q_PROPERTY(QString uid READ uid CONSTANT) + Q_PROPERTY(QString name READ name CONSTANT STORED false) + Q_PROPERTY(QString vendor READ vendor CONSTANT STORED false) + Q_PROPERTY(Bolt::Type type READ type CONSTANT STORED false) + Q_PROPERTY(Bolt::Status status READ status NOTIFY statusChanged STORED false) + Q_PROPERTY(Bolt::AuthFlags authFlags READ authFlags NOTIFY authFlagsChanged STORED false) + Q_PROPERTY(QString parent READ parent CONSTANT STORED false) + Q_PROPERTY(QString sysfsPath READ sysfsPath CONSTANT STORED false) + Q_PROPERTY(QDateTime connectTime READ connectTime CONSTANT STORED false) + Q_PROPERTY(QDateTime authorizeTime READ authorizeTime CONSTANT STORED false) + Q_PROPERTY(bool stored READ stored NOTIFY storedChanged STORED false) + Q_PROPERTY(Bolt::Policy policy READ policy NOTIFY policyChanged STORED false) + Q_PROPERTY(Bolt::KeyState keyState READ keyState CONSTANT STORED false) + Q_PROPERTY(QDateTime storeTime READ storeTime CONSTANT STORED false) + Q_PROPERTY(QString label READ label CONSTANT STORED false) + + friend class Manager; +public: + static QSharedPointer create(const QDBusObjectPath &path, + QObject *parent = nullptr); + explicit Device(QObject *parent = nullptr); + ~Device() override; + + QString uid() const; + QString name() const; + QString vendor() const; + Type type() const; + Status status() const; + AuthFlags authFlags() const; + QString parent() const; + QString sysfsPath() const; + QDateTime connectTime() const; + QDateTime authorizeTime() const; + bool stored() const; + Policy policy() const; + KeyState keyState() const; + QDateTime storeTime() const; + QString label() const; + + QDBusObjectPath dbusPath() const; + + void authorize(Bolt::AuthFlags authFlags, + std::function successCb = {}, + std::function errorCb = {}); + +Q_SIGNALS: + void statusChanged(Bolt::Status); + void storedChanged(bool stored); + void policyChanged(Bolt::Policy policy); + void authFlagsChanged(Bolt::AuthFlags authFlags); + +private: + template + friend QSharedPointer QSharedPointer::create(Args&& ...); + + Device(const QDBusObjectPath &path, QObject *parent = nullptr); + + void setStatusOverride(Status status); + void clearStatusOverride(); + + std::unique_ptr mInterface; + QDBusObjectPath mDBusPath; + QString mUid; + Status mStatusOverride = Status::Unknown; +}; + +} // namespace + +#endif diff --git a/libs/bolt/device.cpp b/libs/bolt/device.cpp new file mode 100644 --- /dev/null +++ b/libs/bolt/device.cpp @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "device.h" +#include "deviceinterface.h" +#include "dbushelper.h" +#include "libkbolt_debug.h" + +#include + +#include + +using namespace Bolt; + +using DeviceInterface = org::freedesktop::bolt1::Device; + +class DBusException : public std::runtime_error +{ +public: + DBusException(const QString &what) + : std::runtime_error(what.toStdString()) {} +}; + +Device::Device(QObject *parent) + : QObject(parent) +{} + +Device::Device(const QDBusObjectPath &path, QObject *parent) + : QObject(parent) + , mInterface(std::make_unique( + DBusHelper::serviceName(), path.path(), DBusHelper::connection())) + , mDBusPath(path) +{ + if (!mInterface->isValid()) { + throw DBusException(QStringLiteral("Failed to obtain DBus interface for device %1: %2") + .arg(path.path(), mInterface->lastError().message())); + } + + // cache UID in case the we still need to identify the device, even if it's + // gone on DBus + mUid = mInterface->uid(); +} + +Device::~Device() = default; + +QSharedPointer Device::create(const QDBusObjectPath &path, QObject *parent) +{ + try { + return QSharedPointer::create(path, parent); + } catch (const DBusException &e) { + qCWarning(log_libkbolt, "%s", e.what()); + return {}; + } +} + +QDBusObjectPath Device::dbusPath() const +{ + return mDBusPath; +} + +QString Device::uid() const +{ + return mUid; +} + +QString Device::name() const +{ + return mInterface->name(); +} + +QString Device::vendor() const +{ + return mInterface->vendor(); +} + +Type Device::type() const +{ + const auto val = mInterface->type(); + return val.isEmpty() ? Type::Unknown : typeFromString(val); +} + +Status Device::status() const +{ + if (mStatusOverride == Status::Unknown) { + const auto val = mInterface->status(); + return val.isEmpty() ? Status::Unknown : statusFromString(val); + } else { + return mStatusOverride; + } +} + +void Device::setStatusOverride(Status status) +{ + if (mStatusOverride != status) { + mStatusOverride = status; + Q_EMIT statusChanged(status); + } +} + +void Device::clearStatusOverride() +{ + setStatusOverride(Status::Unknown); +} + +AuthFlags Device::authFlags() const +{ + const auto val = mInterface->authFlags(); + return val.isEmpty() ? Auth::None : authFlagsFromString(val); +} + +QString Device::parent() const +{ + return mInterface->parentUid(); +} + +QString Device::sysfsPath() const +{ + return mInterface->sysfsPath(); +} + +QDateTime Device::connectTime() const +{ + const auto val = mInterface->connectTime(); + return val == 0 ? QDateTime() : QDateTime::fromTime_t(val); +} + +QDateTime Device::authorizeTime() const +{ + const auto val = mInterface->authorizeTime(); + return val == 0 ? QDateTime() : QDateTime::fromTime_t(val); +} + +bool Device::stored() const +{ + return mInterface ? mInterface->stored() : false; +} + +Policy Device::policy() const +{ + const auto val = mInterface->policy(); + return val.isEmpty() ? Policy::Unknown : policyFromString(val); +} + +KeyState Device::keyState() const +{ + const auto val = mInterface->key(); + return val.isEmpty() ? KeyState::Unknown : keyStateFromString(val); +} + +QDateTime Device::storeTime() const +{ + const auto val = mInterface->storeTime(); + return val == 0 ? QDateTime() : QDateTime::fromTime_t(val); +} + +QString Device::label() const +{ + return mInterface->label(); +} + +void Device::authorize(AuthFlags authFlags, + std::function successCb, + std::function errorCb) +{ + qCDebug(log_libkbolt, "Authorizing device %s with auth flags %s", + qUtf8Printable(mUid), qUtf8Printable(authFlagsToString(authFlags))); + + setStatusOverride(Status::Authorizing); + DBusHelper::call(mInterface.get(), QStringLiteral("Authorize"), + authFlagsToString(authFlags), + [this, cb = std::move(successCb)]() { + qCDebug(log_libkbolt, "Device %s was successfully authorized", + qUtf8Printable(mUid)); + clearStatusOverride(); + if (cb) { + cb(); + } + }, + [this, cb = std::move(errorCb)](const QString &error) { + qCWarning(log_libkbolt, "Failed to authorize device %s: %s", + qUtf8Printable(mUid), qUtf8Printable(error)); + setStatusOverride(Status::AuthError); + if (cb) { + cb(error); + } + }, + this); +} + diff --git a/libs/bolt/devicemodel.h b/libs/bolt/devicemodel.h new file mode 100644 --- /dev/null +++ b/libs/bolt/devicemodel.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef DEVICEMODEL_H_ +#define DEVICEMODEL_H_ + +#include + +#include "kbolt_export.h" + +namespace Bolt +{ + +class Manager; +class Device; +class KBOLT_EXPORT DeviceModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(Bolt::Manager *manager READ manager WRITE setManager NOTIFY managerChanged) + + /** Whether to show only peripherals or display hosts as well */ + Q_PROPERTY(bool showHosts READ showHosts WRITE setShowHosts NOTIFY showHostsChanged) +public: + enum Role { + DeviceRole = Qt::UserRole + }; + + using QAbstractListModel::QAbstractListModel; + ~DeviceModel() override = default; + + Manager *manager() const; + void setManager(Manager *manager); + + bool showHosts() const; + void setShowHosts(bool showHosts); + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + +Q_SIGNALS: + void managerChanged(Bolt::Manager *manager); + void showHostsChanged(bool showHosts); + +private: + void populateWithoutReset(); + + Manager *mManager = nullptr; + QVector> mDevices; + bool mShowHosts = true; +}; + +} // namespace Bolt + +#endif diff --git a/libs/bolt/devicemodel.cpp b/libs/bolt/devicemodel.cpp new file mode 100644 --- /dev/null +++ b/libs/bolt/devicemodel.cpp @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "devicemodel.h" +#include "device.h" +#include "manager.h" + +using namespace Bolt; + +Q_DECLARE_METATYPE(QSharedPointer) + +void DeviceModel::setManager(Manager *manager) +{ + if (mManager == manager) { + return; + } + + if (mManager) { + mManager->disconnect(this); + } + + beginResetModel(); + mManager = manager; + mDevices.clear(); + if (mManager) { + connect(mManager, &Manager::deviceAdded, + this, [this](const QSharedPointer &device) { + if (mShowHosts || device->type() == Type::Peripheral) { + beginInsertRows({}, mDevices.count(), mDevices.count()); + mDevices.push_back(device); + endInsertRows(); + } + }); + connect(mManager, &Manager::deviceRemoved, + this, [this](const QSharedPointer &device) { + const int idx = mDevices.indexOf(device); + if (idx == -1) { + return; + } + beginRemoveRows({}, idx, idx); + mDevices.removeAt(idx); + endRemoveRows(); + }); + + populateWithoutReset(); + } + endResetModel(); + + Q_EMIT managerChanged(mManager); +} + +Manager *DeviceModel::manager() const +{ + return mManager; +} + +bool DeviceModel::showHosts() const +{ + return mShowHosts; +} + +void DeviceModel::setShowHosts(bool showHosts) +{ + if (mShowHosts != showHosts) { + mShowHosts = showHosts; + Q_EMIT showHostsChanged(mShowHosts); + if (mManager) { + beginResetModel(); + populateWithoutReset(); + endResetModel(); + } + } +} + +QHash DeviceModel::roleNames() const +{ + auto roles = QAbstractListModel::roleNames(); + roles[DeviceRole] = "device"; + return roles; +} + +int DeviceModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return mDevices.count(); +} + +QVariant DeviceModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + if (index.row() >= mDevices.size()) { + return {}; + } + + if (role == DeviceRole) { + return QVariant::fromValue(mDevices.at(index.row()).data()); + } + + return {}; +} + +void DeviceModel::populateWithoutReset() +{ + Q_ASSERT(mManager); + + mDevices.clear(); + const auto all = mManager->devices(); + std::copy_if(all.cbegin(), all.cend(), std::back_inserter(mDevices), + [this](const auto &device) { + return mShowHosts || device->type() == Type::Peripheral; + }); +} diff --git a/libs/bolt/enum.h b/libs/bolt/enum.h new file mode 100644 --- /dev/null +++ b/libs/bolt/enum.h @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef BOLT_ENUM_H_ +#define BOLT_ENUM_H_ + +#include "kbolt_export.h" + +#include +#include +#include + +namespace Bolt +{ +// NOTE: Keep this split over two lines otherwise MOC may fail to see +// the Q_NAMESPACE macro if KBOLT_EXPORT is not expanded correctly. +KBOLT_EXPORT +Q_NAMESPACE + +enum class Status { + Unknown = -1, + Disconnected, + Connecting, + Connected, + Authorizing, + AuthError, + Authorized +}; + +Q_ENUM_NS(Status) + +Status statusFromString(const QString &str); +QString statusToString(Status status); + +enum class Auth { + None = 0, + NoPCIE = 1 << 0, + Secure = 1 << 1, + NoKey = 1 << 2, + Boot = 1 << 3 +}; +Q_ENUM_NS(Auth) +Q_DECLARE_FLAGS(AuthFlags, Auth) + +AuthFlags authFlagsFromString(const QString &str); +QString authFlagsToString(AuthFlags flags); + +enum class KeyState { + Unknown = -1, + Missing, + Have, + New +}; +Q_ENUM_NS(KeyState) + +KeyState keyStateFromString(const QString &str); + +enum class Policy { + Unknown = -1, + Default, + Manual, + Auto +}; +Q_ENUM_NS(Policy) + +Policy policyFromString(const QString &str); +QString policyToString(Policy policy); + +enum class Type { + Unknown = -1, + Host, + Peripheral +}; +Q_ENUM_NS(Type) + +Type typeFromString(const QString &str); + + +enum class AuthMode { + Disabled = 0, + Enabled +}; +Q_ENUM_NS(AuthMode) + +AuthMode authModeFromString(const QString &str); +QString authModeToString(AuthMode authMode); + +enum class Security { + Unknown = -1, + None, + DPOnly, + User = '1', + Secure = '2', + USBOnly = 4 +}; +Q_ENUM_NS(Security) + +Security securityFromString(const QString &str); + +} // namespace + +Q_DECLARE_METATYPE(Bolt::Status) +Q_DECLARE_METATYPE(Bolt::AuthFlags) +Q_DECLARE_METATYPE(Bolt::KeyState) +Q_DECLARE_METATYPE(Bolt::Policy) +Q_DECLARE_METATYPE(Bolt::Type) +Q_DECLARE_METATYPE(Bolt::AuthMode) +Q_DECLARE_METATYPE(Bolt::Security) +Q_DECLARE_OPERATORS_FOR_FLAGS(Bolt::AuthFlags) + + +#endif diff --git a/libs/bolt/enum.cpp b/libs/bolt/enum.cpp new file mode 100644 --- /dev/null +++ b/libs/bolt/enum.cpp @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "enum.h" +#include "libkbolt_debug.h" + +#include +#include +#include + +Bolt::Status Bolt::statusFromString(const QString &str) +{ + if (str == QLatin1String("unknown")) { + return Bolt::Status::Unknown; + } else if (str == QLatin1String("disconnected")) { + return Bolt::Status::Disconnected; + } else if (str == QLatin1String("connecting")) { + return Bolt::Status::Connecting; + } else if (str == QLatin1String("connected")) { + return Bolt::Status::Connected; + } else if (str == QLatin1String("authorizing")) { + return Bolt::Status::Authorizing; + } else if (str == QLatin1String("authorized")) { + return Bolt::Status::Authorized; + } else if (str == QLatin1String("auth-error")) { + return Bolt::Status::AuthError; + } else { + qCCritical(log_libkbolt, "Unknown Status enum value '%s'", qUtf8Printable(str)); + Q_ASSERT(false); + return Bolt::Status::Unknown; + } +} + +QString Bolt::statusToString(Bolt::Status status) +{ + switch (status) { + case Bolt::Status::Unknown: + return QStringLiteral("unknown"); + case Bolt::Status::Disconnected: + return QStringLiteral("disconnected"); + case Bolt::Status::Connecting: + return QStringLiteral("connecting"); + case Bolt::Status::Connected: + return QStringLiteral("connected"); + case Bolt::Status::Authorizing: + return QStringLiteral("authorizing"); + case Bolt::Status::Authorized: + return QStringLiteral("authorized"); + case Bolt::Status::AuthError: + return QStringLiteral("auth-error"); + } + Q_UNREACHABLE(); + return {}; +} + +Bolt::AuthFlags Bolt::authFlagsFromString(const QString &str) +{ + const auto splitRef = str.splitRef(QStringLiteral("|")); + Bolt::AuthFlags outFlags = Bolt::Auth::None; + for (const auto &flag : splitRef) { + const auto f = flag.trimmed(); + if (f == QLatin1String("none")) { + outFlags |= Bolt::Auth::None; + } else if (f == QLatin1String("nopcie")) { + outFlags |= Bolt::Auth::NoPCIE; + } else if (f == QLatin1String("secure")) { + outFlags |= Bolt::Auth::Secure; + } else if (f == QLatin1String("nokey")) { + outFlags |= Bolt::Auth::NoKey; + } else if (f == QLatin1String("boot")) { + outFlags |= Bolt::Auth::Boot; + } else { + qCCritical(log_libkbolt, "Unknown AuthFlags enum value '%s'", qUtf8Printable(str)); + Q_ASSERT(false); + return Bolt::Auth::None; + } + } + return outFlags; +} + +QString Bolt::authFlagsToString(AuthFlags flags) +{ + QStringList str; + if (flags == AuthFlags(Bolt::Auth::None)) { + return QStringLiteral("none"); + } + if (flags & Bolt::Auth::NoPCIE) { + str.push_back(QStringLiteral("nopcie")); + } + if (flags & Bolt::Auth::Secure) { + str.push_back(QStringLiteral("secure")); + } + if (flags & Bolt::Auth::NoKey) { + str.push_back(QStringLiteral("nokey")); + } + if (flags & Bolt::Auth::Boot) { + str.push_back(QStringLiteral("boot")); + } + + return str.join(QStringLiteral(" | ")); +} + + +Bolt::KeyState Bolt::keyStateFromString(const QString &str) +{ + if (str == QLatin1String("unknown")) { + return Bolt::KeyState::Unknown; + } else if (str == QLatin1String("missing")) { + return Bolt::KeyState::Missing; + } else if (str == QLatin1String("have")) { + return Bolt::KeyState::Have; + } else if (str == QLatin1String("new")) { + return Bolt::KeyState::New; + } else { + qCCritical(log_libkbolt, "Unknown KeyState enum value '%s'", qUtf8Printable(str)); + Q_ASSERT(false); + return Bolt::KeyState::Unknown; + } +} + +Bolt::Policy Bolt::policyFromString(const QString &str) +{ + if (str == QLatin1String("unknown")) { + return Bolt::Policy::Unknown; + } else if (str == QLatin1String("default")) { + return Bolt::Policy::Default; + } else if (str == QLatin1String("manual")) { + return Bolt::Policy::Manual; + } else if (str == QLatin1String("auto")) { + return Bolt::Policy::Auto; + } else { + qCCritical(log_libkbolt, "Unknown Policy enum value '%s'", qUtf8Printable(str)); + Q_ASSERT(false); + return Bolt::Policy::Unknown; + } +} + +QString Bolt::policyToString(Bolt::Policy policy) +{ + switch (policy) { + case Policy::Unknown: + return QStringLiteral("unknown"); + case Policy::Auto: + return QStringLiteral("auto"); + case Policy::Default: + return QStringLiteral("default"); + case Policy::Manual: + return QStringLiteral("manual"); + } + + Q_UNREACHABLE(); + return {}; +} + +Bolt::Type Bolt::typeFromString(const QString &str) +{ + if (str == QLatin1String("unknown")) { + return Bolt::Type::Unknown; + } else if (str == QLatin1String("host")) { + return Bolt::Type::Host; + } else if (str == QLatin1String("peripheral")) { + return Bolt::Type::Peripheral; + } else { + qCCritical(log_libkbolt, "Unknown Type enum value '%s'", qUtf8Printable(str)); + Q_ASSERT(false); + return Bolt::Type::Unknown; + } +} + +Bolt::AuthMode Bolt::authModeFromString(const QString &str) +{ + if (str == QLatin1String("disabled")) { + return Bolt::AuthMode::Disabled; + } else if (str == QLatin1String("enabled")) { + return Bolt::AuthMode::Enabled; + } else { + qCCritical(log_libkbolt, "Unknown AuthMode enum value '%s'", qUtf8Printable(str)); + Q_ASSERT(false); + return Bolt::AuthMode::Disabled; + } +} + +QString Bolt::authModeToString(Bolt::AuthMode authMode) +{ + switch (authMode) { + case Bolt::AuthMode::Enabled: + return QStringLiteral("enabled"); + case Bolt::AuthMode::Disabled: + return QStringLiteral("disabled"); + } + + Q_UNREACHABLE(); + return {}; +} + +Bolt::Security Bolt::securityFromString(const QString &str) +{ + if (str == QLatin1String("unknown")) { + return Bolt::Security::Unknown; + } else if (str == QLatin1String("none")) { + return Bolt::Security::None; + } else if (str == QLatin1String("dponly")) { + return Bolt::Security::DPOnly; + } else if (str == QLatin1String("user")) { + return Bolt::Security::User; + } else if (str == QLatin1String("secure")) { + return Bolt::Security::Secure; + } else if (str == QLatin1String("usbonly")) { + return Bolt::Security::USBOnly; + } else { + qCCritical(log_libkbolt, "Unknown Security enum value '%s'", qUtf8Printable(str)); + Q_ASSERT(false); + return Bolt::Security::Unknown; + } +} + diff --git a/libs/bolt/interfaces/org.freedesktop.bolt1.device.xml b/libs/bolt/interfaces/org.freedesktop.bolt1.device.xml new file mode 100644 --- /dev/null +++ b/libs/bolt/interfaces/org.freedesktop.bolt1.device.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/bolt/interfaces/org.freedesktop.bolt1.manager.xml b/libs/bolt/interfaces/org.freedesktop.bolt1.manager.xml new file mode 100644 --- /dev/null +++ b/libs/bolt/interfaces/org.freedesktop.bolt1.manager.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/bolt/manager.h b/libs/bolt/manager.h new file mode 100644 --- /dev/null +++ b/libs/bolt/manager.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MANAGER_H_ +#define MANAGER_H_ + +#include +#include + +#include +#include + +#include "enum.h" +#include "kbolt_export.h" + +class QDBusObjectPath; +class OrgFreedesktopBolt1ManagerInterface; +namespace Bolt +{ + +class Device; +class KBOLT_EXPORT Manager : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool isAvailable READ isAvailable CONSTANT) + Q_PROPERTY(uint version READ version CONSTANT STORED false) + Q_PROPERTY(bool isProbing READ isProbing CONSTANT STORED false) + Q_PROPERTY(Bolt::Policy defaultPolicy READ defaultPolicy CONSTANT STORED false) + Q_PROPERTY(Bolt::Security securityLevel READ securityLevel CONSTANT STORED false) + Q_PROPERTY(Bolt::AuthMode authMode READ authMode WRITE setAuthMode STORED false) + +public: + explicit Manager(QObject *parent = nullptr); + ~Manager() override; + + bool isAvailable() const; + + uint version() const; + bool isProbing() const; + Policy defaultPolicy() const; + Security securityLevel() const; + AuthMode authMode() const; + void setAuthMode(AuthMode mode); + + /** + * Updates device authorization and stores it persistently. + */ + void enrollDevice(const QString &uid, Bolt::Policy policy, Bolt::AuthFlags flags, + std::function successCallback = {}, + std::function errorCallback = {}); + /** + * Keeps device authorized but removes it from persistent store. + * + * Next time the device is plugged in, it will not be authorized. + */ + void forgetDevice(const QString &uid, + std::function successCallback = {}, + std::function errorCallback = {}); + +public Q_SLOTS: + QSharedPointer device(const QString &uid) const; + QSharedPointer device(const QDBusObjectPath &path) const; + QList> devices() const; + +Q_SIGNALS: + void deviceAdded(const QSharedPointer &device); + void deviceRemoved(const QSharedPointer &device); + +private: + QSharedPointer device(std::function &)> &&match) const; + std::unique_ptr mInterface; + + uint mVersion = 0; + Policy mPolicy = Policy::Unknown; + Security mSecurity = Security::Unknown; + AuthMode mAuthMode = AuthMode::Disabled; + bool mIsProbing = false; + + QList> mDevices; +}; + +} // namespace + +#endif diff --git a/libs/bolt/manager.cpp b/libs/bolt/manager.cpp new file mode 100644 --- /dev/null +++ b/libs/bolt/manager.cpp @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2018 - 2019 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "manager.h" +#include "device.h" +#include "managerinterface.h" +#include "dbushelper.h" +#include "enum.h" +#include "libkbolt_debug.h" + +using namespace Bolt; + +using ManagerInterface = org::freedesktop::bolt1::Manager; + +Manager::Manager(QObject *parent) + : QObject(parent) + , mInterface(std::make_unique( + DBusHelper::serviceName(), QStringLiteral("/org/freedesktop/bolt"), + DBusHelper::connection())) +{ + if (!mInterface->isValid()) { + qCWarning(log_libkbolt, "Failed to connect to Bolt manager DBus interface: %s", + qUtf8Printable(mInterface->lastError().message())); + return; + } + + connect(mInterface.get(), &ManagerInterface::DeviceAdded, + this, [this](const QDBusObjectPath &path) { + if (auto device = Device::create(path, this)) { + mDevices.push_back(device); + qCDebug(log_libkbolt, "New Thunderbolt device %s (%s) added, status=%s", + qUtf8Printable(device->uid()), qUtf8Printable(device->name()), + qUtf8Printable(statusToString(device->status()))); + Q_EMIT deviceAdded(device); + } + }); + connect(mInterface.get(), &ManagerInterface::DeviceRemoved, + this, [this](const QDBusObjectPath &path) { + if (auto device = this->device(path)) { + mDevices.removeOne(device); + qCDebug(log_libkbolt, "Thunderbolt Device %s (%s) removed", + qUtf8Printable(device->uid()), qUtf8Printable(device->name())); + Q_EMIT deviceRemoved(device); + } + }); + + const auto devicePaths = mInterface->ListDevices().argumentAt<0>(); + for (const auto &devicePath : devicePaths) { + if (auto device = Device::create(devicePath, this)) { + qCDebug(log_libkbolt, "Discovered Thunderbolt device %s (%s), status=%s", + qUtf8Printable(device->uid()), qUtf8Printable(device->name()), + qUtf8Printable(statusToString(device->status()))); + mDevices.push_back(device); + } + } +} + +Manager::~Manager() = default; + +bool Manager::isAvailable() const +{ + return mInterface.get() && mInterface->isValid(); +} + +uint Manager::version() const +{ + return mInterface->version(); +} + +bool Manager::isProbing() const +{ + return mInterface->probing(); +} + +Policy Manager::defaultPolicy() const +{ + const auto policy = mInterface->defaultPolicy(); + if (!mInterface->isValid() || policy.isEmpty()) { + return Policy::Unknown; + } + return policyFromString(policy); +} + +Security Manager::securityLevel() const +{ + const auto level = mInterface->securityLevel(); + if (!mInterface->isValid() || level.isEmpty()) { + return Security::Unknown; + } + return securityFromString(level); +} + +AuthMode Manager::authMode() const +{ + const auto mode = mInterface->authMode(); + if (!mInterface->isValid() || mode.isEmpty()) { + return AuthMode::Disabled; + } + return authModeFromString(mode); +} + +void Manager::setAuthMode(AuthMode mode) +{ + mInterface->setAuthMode(authModeToString(mode)); +} + +QSharedPointer Manager::device(std::function &)> &&match) const +{ + auto device = std::find_if(mDevices.cbegin(), mDevices.cend(), std::move(match)); + return device == mDevices.cend() ? QSharedPointer() : *device; +} + +QSharedPointer Manager::device(const QString &uid) const +{ + return device([uid](const auto &device) { return device->uid() == uid; }); +} + +QSharedPointer Manager::device(const QDBusObjectPath &path) const +{ + return device([path](const auto &device) { return device->dbusPath() == path; }); +} + +QList> Manager::devices() const +{ + return mDevices; +} + +void Manager::enrollDevice(const QString &uid, Policy policy, AuthFlags authFlags, + std::function successCallback, + std::function errorCallback) +{ + qCDebug(log_libkbolt, "Enrolling Thunderbolt device %s with policy %s and flags %s", + qUtf8Printable(uid), qUtf8Printable(policyToString(policy)), + qUtf8Printable(authFlagsToString(authFlags))); + + auto device = this->device(uid); + if (device) { + device->setStatusOverride(Status::Authorizing); + } else { + qCWarning(log_libkbolt, "Found no matching Thunderbolt device object for uid %s", + qUtf8Printable(uid)); + } + + DBusHelper::call(mInterface.get(), + QStringLiteral("EnrollDevice"), uid, + policyToString(policy), authFlagsToString(authFlags), + [uid, device, policy, authFlags, cb = std::move(successCallback)]() { + qCDebug(log_libkbolt, "Thunderbolt device %s was successfully enrolled", qUtf8Printable(uid)); + if (device) { + device->clearStatusOverride(); + Q_EMIT device->storedChanged(true); + Q_EMIT device->policyChanged(policy); + Q_EMIT device->authFlagsChanged(authFlags); + } + if (cb) { + cb(); + } + }, + [this, uid, device, cb = std::move(errorCallback)](const QString &error) { + qCWarning(log_libkbolt, "Failed to enroll Thunderbolt device %s: %s", + qUtf8Printable(uid), qUtf8Printable(error)); + if (device) { + device->setStatusOverride(Status::AuthError); + } + if (cb) { + cb(error); + } + }, + this); +} + +void Manager::forgetDevice(const QString &uid, + std::function successCallback, + std::function errorCallback) +{ + qCDebug(log_libkbolt, "Forgetting Thunderbolt device %s", qUtf8Printable(uid)); + + DBusHelper::call(mInterface.get(), + QStringLiteral("ForgetDevice"), uid, + [this, uid, cb = std::move(successCallback)]() { + qCDebug(log_libkbolt, "Thunderbolt device %s was successfully forgotten", qUtf8Printable(uid)); + if (auto device = this->device(uid)) { + device->clearStatusOverride(); + Q_EMIT device->storedChanged(false); + Q_EMIT device->authFlagsChanged(Auth::None); + Q_EMIT device->policyChanged(Policy::Auto); + } + if (cb) { + cb(); + } + }, + [this, uid, cb = std::move(errorCallback)](const QString &error) { + qCWarning(log_libkbolt, "Failed to forget Thunderbolt device %s: %s", + qUtf8Printable(uid), qUtf8Printable(error)); + if (auto device = this->device(uid)) { + device->setStatusOverride(Status::AuthError); + } + if (cb) { + cb(error); + } + }, + this); +}