| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2012 Lukáš Tinkl <ltinkl@redhat.com> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
| 5 | */ |
| 6 | |
| 7 | #include "udisksmanager.h" |
| 8 | #include "udisks_debug.h" |
| 9 | #include "udisksdevicebackend.h" |
| 10 | |
| 11 | #include <QDBusConnection> |
| 12 | #include <QDBusConnectionInterface> |
| 13 | #include <QDBusMetaType> |
| 14 | #include <QDBusObjectPath> |
| 15 | #include <QDomDocument> |
| 16 | |
| 17 | #include "../shared/rootdevice.h" |
| 18 | |
| 19 | using namespace Solid::Backends::UDisks2; |
| 20 | using namespace Solid::Backends::Shared; |
| 21 | |
| 22 | Manager::Manager(QObject *parent) |
| 23 | : Solid::Ifaces::DeviceManager(parent) |
| 24 | , m_manager(QStringLiteral(UD2_DBUS_SERVICE), QStringLiteral(UD2_DBUS_PATH), QDBusConnection::systemBus()) |
| 25 | { |
| 26 | m_supportedInterfaces = { |
| 27 | Solid::DeviceInterface::GenericInterface, |
| 28 | Solid::DeviceInterface::Block, |
| 29 | Solid::DeviceInterface::StorageAccess, |
| 30 | Solid::DeviceInterface::StorageDrive, |
| 31 | Solid::DeviceInterface::OpticalDrive, |
| 32 | Solid::DeviceInterface::OpticalDisc, |
| 33 | Solid::DeviceInterface::StorageVolume, |
| 34 | }; |
| 35 | |
| 36 | qDBusRegisterMetaType<QList<QDBusObjectPath>>(); |
| 37 | qDBusRegisterMetaType<QVariantMap>(); |
| 38 | qDBusRegisterMetaType<VariantMapMap>(); |
| 39 | qDBusRegisterMetaType<DBUSManagerStruct>(); |
| 40 | |
| 41 | bool serviceFound = m_manager.isValid(); |
| 42 | if (!serviceFound) { |
| 43 | // find out whether it will be activated automatically |
| 44 | QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.DBus" ), // |
| 45 | QStringLiteral("/org/freedesktop/DBus" ), |
| 46 | QStringLiteral("org.freedesktop.DBus" ), |
| 47 | QStringLiteral("ListActivatableNames" )); |
| 48 | |
| 49 | QDBusReply<QStringList> reply = QDBusConnection::systemBus().call(message); |
| 50 | if (reply.isValid() && reply.value().contains(QStringLiteral(UD2_DBUS_SERVICE))) { |
| 51 | QDBusConnection::systemBus().interface()->startService(QStringLiteral(UD2_DBUS_SERVICE)); |
| 52 | serviceFound = true; |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | if (serviceFound) { |
| 57 | connect(sender: &m_manager, SIGNAL(InterfacesAdded(QDBusObjectPath, VariantMapMap)), receiver: this, SLOT(slotInterfacesAdded(QDBusObjectPath, VariantMapMap))); |
| 58 | connect(sender: &m_manager, SIGNAL(InterfacesRemoved(QDBusObjectPath, QStringList)), receiver: this, SLOT(slotInterfacesRemoved(QDBusObjectPath, QStringList))); |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | Manager::~Manager() |
| 63 | { |
| 64 | while (!m_deviceCache.isEmpty()) { |
| 65 | QString udi = m_deviceCache.takeFirst(); |
| 66 | DeviceBackend::destroyBackend(udi); |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | QObject *Manager::createDevice(const QString &udi) |
| 71 | { |
| 72 | if (udi == udiPrefix()) { |
| 73 | RootDevice *root = new RootDevice(udi); |
| 74 | |
| 75 | root->setProduct(tr(s: "Storage" )); |
| 76 | root->setDescription(tr(s: "Storage devices" )); |
| 77 | root->setIcon(QStringLiteral("server-database" )); // Obviously wasn't meant for that, but maps nicely in oxygen icon set :-p |
| 78 | |
| 79 | return root; |
| 80 | } else if (deviceCache().contains(str: udi)) { |
| 81 | return new Device(udi); |
| 82 | } else { |
| 83 | return nullptr; |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | QStringList Manager::devicesFromQuery(const QString &parentUdi, Solid::DeviceInterface::Type type) |
| 88 | { |
| 89 | QStringList result; |
| 90 | const QStringList deviceList = deviceCache(); |
| 91 | |
| 92 | if (!parentUdi.isEmpty()) { |
| 93 | for (const QString &udi : deviceList) { |
| 94 | Device device(udi); |
| 95 | if (device.queryDeviceInterface(type) && device.parentUdi() == parentUdi) { |
| 96 | result << udi; |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | return result; |
| 101 | } else if (type != Solid::DeviceInterface::Unknown) { |
| 102 | for (const QString &udi : deviceList) { |
| 103 | Device device(udi); |
| 104 | if (device.queryDeviceInterface(type)) { |
| 105 | result << udi; |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | return result; |
| 110 | } |
| 111 | |
| 112 | return deviceCache(); |
| 113 | } |
| 114 | |
| 115 | QStringList Manager::allDevices() |
| 116 | { |
| 117 | m_deviceCache.clear(); |
| 118 | |
| 119 | introspect(QStringLiteral(UD2_DBUS_PATH_BLOCKDEVICES), checkOptical: true /*checkOptical*/); |
| 120 | introspect(QStringLiteral(UD2_DBUS_PATH_DRIVES)); |
| 121 | |
| 122 | return m_deviceCache; |
| 123 | } |
| 124 | |
| 125 | void Manager::introspect(const QString &path, bool checkOptical) |
| 126 | { |
| 127 | QDBusMessage call = |
| 128 | QDBusMessage::createMethodCall(QStringLiteral(UD2_DBUS_SERVICE), path, QStringLiteral(DBUS_INTERFACE_INTROSPECT), QStringLiteral("Introspect" )); |
| 129 | QDBusPendingReply<QString> reply = QDBusConnection::systemBus().call(message: call); |
| 130 | |
| 131 | if (reply.isValid()) { |
| 132 | QDomDocument dom; |
| 133 | dom.setContent(data: reply.value()); |
| 134 | QDomNodeList nodeList = dom.documentElement().elementsByTagName(QStringLiteral("node" )); |
| 135 | for (int i = 0; i < nodeList.count(); i++) { |
| 136 | QDomElement nodeElem = nodeList.item(index: i).toElement(); |
| 137 | if (!nodeElem.isNull() && nodeElem.hasAttribute(QStringLiteral("name" ))) { |
| 138 | const QString name = nodeElem.attribute(QStringLiteral("name" )); |
| 139 | const QString udi = path + QStringLiteral("/" ) + name; |
| 140 | |
| 141 | // Optimization, a loop device cannot really have a physical drive associated with it |
| 142 | if (checkOptical && !name.startsWith(s: QLatin1String("loop" ))) { |
| 143 | Device device(udi); |
| 144 | if (device.mightBeOpticalDisc()) { |
| 145 | QDBusConnection::systemBus().connect(QStringLiteral(UD2_DBUS_SERVICE), // |
| 146 | path: udi, |
| 147 | QStringLiteral(DBUS_INTERFACE_PROPS), |
| 148 | QStringLiteral("PropertiesChanged" ), |
| 149 | receiver: this, |
| 150 | SLOT(slotMediaChanged(QDBusMessage))); |
| 151 | if (!device.isOpticalDisc()) { // skip empty CD disc |
| 152 | continue; |
| 153 | } |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | m_deviceCache.append(t: udi); |
| 158 | } |
| 159 | } |
| 160 | } else { |
| 161 | qCWarning(UDISKS2) << "Failed enumerating UDisks2 objects:" << reply.error().name() << "\n" << reply.error().message(); |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | QSet<Solid::DeviceInterface::Type> Manager::supportedInterfaces() const |
| 166 | { |
| 167 | return m_supportedInterfaces; |
| 168 | } |
| 169 | |
| 170 | QString Manager::udiPrefix() const |
| 171 | { |
| 172 | return QStringLiteral(UD2_UDI_DISKS_PREFIX); |
| 173 | } |
| 174 | |
| 175 | void Manager::slotInterfacesAdded(const QDBusObjectPath &object_path, const VariantMapMap &interfaces_and_properties) |
| 176 | { |
| 177 | const QString udi = object_path.path(); |
| 178 | |
| 179 | /* Ignore jobs */ |
| 180 | if (udi.startsWith(QStringLiteral(UD2_DBUS_PATH_JOBS))) { |
| 181 | return; |
| 182 | } |
| 183 | |
| 184 | qCDebug(UDISKS2) << udi << "has new interfaces:" << interfaces_and_properties.keys(); |
| 185 | |
| 186 | // If device gained an org.freedesktop.UDisks2.Block interface, we |
| 187 | // should check if it is an optical drive, in order to properly |
| 188 | // register mediaChanged event handler with newly-plugged external |
| 189 | // drives |
| 190 | if (interfaces_and_properties.contains(QStringLiteral("org.freedesktop.UDisks2.Block" ))) { |
| 191 | Device device(udi); |
| 192 | if (device.mightBeOpticalDisc()) { |
| 193 | QDBusConnection::systemBus().connect(QStringLiteral(UD2_DBUS_SERVICE), // |
| 194 | path: udi, |
| 195 | QStringLiteral(DBUS_INTERFACE_PROPS), |
| 196 | QStringLiteral("PropertiesChanged" ), |
| 197 | receiver: this, |
| 198 | SLOT(slotMediaChanged(QDBusMessage))); |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | updateBackend(udi); |
| 203 | |
| 204 | // new device, we don't know it yet |
| 205 | if (!m_deviceCache.contains(str: udi)) { |
| 206 | m_deviceCache.append(t: udi); |
| 207 | Q_EMIT deviceAdded(udi); |
| 208 | } |
| 209 | // re-emit in case of 2-stage devices like N9 or some Android phones |
| 210 | else if (m_deviceCache.contains(str: udi) && interfaces_and_properties.keys().contains(QStringLiteral(UD2_DBUS_INTERFACE_FILESYSTEM))) { |
| 211 | Q_EMIT deviceAdded(udi); |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | void Manager::slotInterfacesRemoved(const QDBusObjectPath &object_path, const QStringList &interfaces) |
| 216 | { |
| 217 | const QString udi = object_path.path(); |
| 218 | if (udi.isEmpty()) { |
| 219 | return; |
| 220 | } |
| 221 | |
| 222 | /* Ignore jobs */ |
| 223 | if (udi.startsWith(QStringLiteral(UD2_DBUS_PATH_JOBS))) { |
| 224 | return; |
| 225 | } |
| 226 | |
| 227 | qCDebug(UDISKS2) << udi << "lost interfaces:" << interfaces; |
| 228 | |
| 229 | /* |
| 230 | * Determine left interfaces. The device backend may have processed the |
| 231 | * InterfacesRemoved signal already, but the result set is the same |
| 232 | * independent if the backend or the manager processes the signal first. |
| 233 | */ |
| 234 | Device device(udi); |
| 235 | const QStringList ifaceList = device.interfaces(); |
| 236 | QSet<QString> leftInterfaces(ifaceList.begin(), ifaceList.end()); |
| 237 | leftInterfaces.subtract(other: QSet<QString>(interfaces.begin(), interfaces.end())); |
| 238 | |
| 239 | if (leftInterfaces.isEmpty()) { |
| 240 | // remove the device if the last interface is removed |
| 241 | Q_EMIT deviceRemoved(udi); |
| 242 | m_deviceCache.removeAll(t: udi); |
| 243 | DeviceBackend::destroyBackend(udi); |
| 244 | } else { |
| 245 | /* |
| 246 | * Changes in the interface composition may change if a device |
| 247 | * matches a Predicate. We have to do a remove-and-readd cycle |
| 248 | * as there is no dedicated signal for Predicate reevaluation. |
| 249 | */ |
| 250 | Q_EMIT deviceRemoved(udi); |
| 251 | Q_EMIT deviceAdded(udi); |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | void Manager::slotMediaChanged(const QDBusMessage &msg) |
| 256 | { |
| 257 | const QVariantMap properties = qdbus_cast<QVariantMap>(v: msg.arguments().at(i: 1)); |
| 258 | |
| 259 | if (!properties.contains(QStringLiteral("Size" ))) { // react only on Size changes |
| 260 | return; |
| 261 | } |
| 262 | |
| 263 | const QString udi = msg.path(); |
| 264 | updateBackend(udi); |
| 265 | qulonglong size = properties.value(QStringLiteral("Size" )).toULongLong(); |
| 266 | qCDebug(UDISKS2) << "MEDIA CHANGED in" << udi << "; size is:" << size; |
| 267 | |
| 268 | Device device(udi); |
| 269 | if (!device.interfaces().contains(t: u"org.freedesktop.UDisks2.Filesystem" )) { |
| 270 | if (!m_deviceCache.contains(str: udi) && size > 0) { // we don't know the optdisc, got inserted |
| 271 | m_deviceCache.append(t: udi); |
| 272 | Q_EMIT deviceAdded(udi); |
| 273 | } |
| 274 | |
| 275 | if (m_deviceCache.contains(str: udi) && size == 0) { // we know the optdisc, got removed |
| 276 | Q_EMIT deviceRemoved(udi); |
| 277 | m_deviceCache.removeAll(t: udi); |
| 278 | DeviceBackend::destroyBackend(udi); |
| 279 | } |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | const QStringList &Manager::deviceCache() |
| 284 | { |
| 285 | if (m_deviceCache.isEmpty()) { |
| 286 | allDevices(); |
| 287 | } |
| 288 | |
| 289 | return m_deviceCache; |
| 290 | } |
| 291 | |
| 292 | void Manager::updateBackend(const QString &udi) |
| 293 | { |
| 294 | DeviceBackend *backend = DeviceBackend::backendForUDI(udi); |
| 295 | if (!backend) { |
| 296 | return; |
| 297 | } |
| 298 | |
| 299 | // This doesn't emit "changed" signals. Signals are emitted later by DeviceBackend's slots |
| 300 | backend->allProperties(); |
| 301 | |
| 302 | QVariant driveProp = backend->prop(QStringLiteral("Drive" )); |
| 303 | if (!driveProp.isValid()) { |
| 304 | return; |
| 305 | } |
| 306 | |
| 307 | QDBusObjectPath drivePath = qdbus_cast<QDBusObjectPath>(v: driveProp); |
| 308 | DeviceBackend *driveBackend = DeviceBackend::backendForUDI(udi: drivePath.path(), create: false); |
| 309 | if (!driveBackend) { |
| 310 | return; |
| 311 | } |
| 312 | |
| 313 | driveBackend->invalidateProperties(); |
| 314 | } |
| 315 | |
| 316 | #include "moc_udisksmanager.cpp" |
| 317 | |