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 "dbus/manager.h"
9#include "udisks_debug.h"
10#include "udisksopticaldisc.h"
11#include "udisksutils.h"
12
13#include <QDBusConnection>
14#include <QDBusConnectionInterface>
15#include <QDBusMetaType>
16#include <QDBusObjectPath>
17#include <QDomDocument>
18
19#include <algorithm>
20
21#include "../shared/rootdevice.h"
22#include "solid/genericinterface.h"
23
24using namespace Solid::Backends::UDisks2;
25using namespace Solid::Backends::Shared;
26
27Manager::Manager(QObject *parent)
28 : Solid::Ifaces::DeviceManager(parent)
29 , m_manager(org::freedesktop::DBus::ObjectManager(QStringLiteral(UD2_DBUS_SERVICE), QStringLiteral(UD2_DBUS_PATH), QDBusConnection::systemBus()))
30{
31 m_supportedInterfaces = {
32 Solid::DeviceInterface::GenericInterface,
33 Solid::DeviceInterface::Block,
34 Solid::DeviceInterface::StorageAccess,
35 Solid::DeviceInterface::StorageDrive,
36 Solid::DeviceInterface::OpticalDrive,
37 Solid::DeviceInterface::OpticalDisc,
38 Solid::DeviceInterface::StorageVolume,
39 };
40
41 qDBusRegisterMetaType<QList<QDBusObjectPath>>();
42 qDBusRegisterMetaType<QVariantMap>();
43 qDBusRegisterMetaType<VariantMapMap>();
44 qDBusRegisterMetaType<DBUSManagerStruct>();
45
46 connect(sender: &m_manager, signal: &org::freedesktop::DBus::ObjectManager::InterfacesAdded, context: this, slot: &Manager::slotInterfacesAdded);
47 connect(sender: &m_manager, signal: &org::freedesktop::DBus::ObjectManager::InterfacesRemoved, context: this, slot: &Manager::slotInterfacesRemoved);
48
49 QDBusConnection::systemBus()
50 .connect(QStringLiteral(UD2_DBUS_SERVICE), path: QString() /*any path*/, QStringLiteral(DBUS_INTERFACE_PROPS), QStringLiteral("PropertiesChanged"), receiver: this, SLOT(slotPropertiesChanged(QDBusMessage)));
51}
52
53Manager::~Manager()
54{
55}
56
57bool Manager::hasInterface(const QString &udi, const QString &interface)
58{
59 return deviceCache().value(key: udi).contains(key: interface);
60}
61
62QMap<QString, PropertyMap> Manager::allProperties()
63{
64 return deviceCache();
65}
66
67PropertyMap Manager::deviceProperties(const QString &udi)
68{
69 return deviceCache().value(key: udi);
70}
71
72QVariant Manager::deviceProperty(const QString &udi, const QString &name, Manager::FetchMode fetchMode)
73{
74 const auto props = deviceProperties(udi);
75
76 // Loop through all interfaces looking for a property.
77 for (auto it = props.begin(), end = props.end(); it != end; ++it) {
78 const QString iface = it.key();
79 const auto valueIt = it->constFind(key: name);
80 if (valueIt != it->constEnd()) {
81 if (!valueIt->isValid() && fetchMode == FetchIfNeeded) {
82 QDBusMessage call = QDBusMessage::createMethodCall(QStringLiteral(UD2_DBUS_SERVICE), path: udi, QStringLiteral(DBUS_INTERFACE_PROPS), QStringLiteral("Get"));
83 call.setArguments({iface, name});
84 QDBusReply<QVariant> reply = QDBusConnection::systemBus().call(message: call);
85
86 /* We don't check for error here and store the item in the cache anyway so next time we don't have to
87 * do the DBus call to find out it does not exist but just check whether
88 * prop(key).isValid() */
89 const QVariant value = Utils::sanitizeValue(value: reply.value());
90 m_cache[udi][iface][name] = value;
91 return value;
92 }
93
94 return *valueIt;
95 }
96 }
97
98 return QVariant();
99}
100
101std::unique_ptr<QObject> Manager::createDevice(const QString &udi)
102{
103 if (udi == udiPrefix()) {
104 std::unique_ptr<RootDevice> root = std::make_unique<RootDevice>(args: udi);
105
106 root->setProduct(tr(s: "Storage"));
107 root->setDescription(tr(s: "Storage devices"));
108 root->setIcon(QStringLiteral("server-database")); // Obviously wasn't meant for that, but maps nicely in oxygen icon set :-p
109
110 return root;
111 } else if (deviceCache().contains(key: udi)) {
112 return std::make_unique<Device>(args: this, args: udi);
113 } else {
114 return nullptr;
115 }
116}
117
118QStringList Manager::devicesFromQuery(const QString &parentUdi, Solid::DeviceInterface::Type type)
119{
120 QStringList result;
121
122 const auto devices = deviceCache();
123
124 if (!parentUdi.isEmpty()) {
125 for (auto it = devices.keyBegin(), end = devices.keyEnd(); it != end; ++it) {
126 Device device(this, *it);
127 if (device.queryDeviceInterface(type) && device.parentUdi() == parentUdi) {
128 result << *it;
129 }
130 }
131
132 return result;
133 } else if (type != Solid::DeviceInterface::Unknown) {
134 for (auto it = devices.keyBegin(), end = devices.keyEnd(); it != end; ++it) {
135 Device device(this, *it);
136 if (device.queryDeviceInterface(type)) {
137 result << *it;
138 }
139 }
140
141 return result;
142 }
143
144 return devices.keys();
145}
146
147QStringList Manager::allDevices()
148{
149 m_devices.clear();
150 m_cache.clear();
151
152 org::freedesktop::DBus::ObjectManager manager(QStringLiteral(UD2_DBUS_SERVICE), QStringLiteral(UD2_DBUS_PATH), QDBusConnection::systemBus());
153 QDBusPendingReply<DBUSManagerStruct> reply = manager.GetManagedObjects();
154 reply.waitForFinished();
155 if (reply.isError()) {
156 qCWarning(UDISKS2) << "Failed to fetch all devices" << reply.error().name() << reply.error().message();
157 return m_devices;
158 }
159
160 const auto items = reply.value();
161 for (auto it = items.begin(), end = items.end(); it != end; ++it) {
162 const QString udi = it.key().path();
163
164 if (!udi.startsWith(s: QLatin1String(UD2_DBUS_PATH_BLOCKDEVICES)) && !udi.startsWith(s: QLatin1String(UD2_DBUS_PATH_DRIVES))) {
165 continue;
166 }
167
168 VariantMapMap mapMap = it.value();
169 for (QVariantMap &map : mapMap) {
170 map = Utils::sanitizeValue(map);
171 }
172 m_devices.append(t: udi);
173 m_cache.insert(key: udi, value: mapMap);
174 }
175
176 // Filter out empty optical drives.
177 m_devices.erase(abegin: std::remove_if(first: m_devices.begin(),
178 last: m_devices.end(),
179 pred: [this](const QString &udi) {
180 Device device(this, udi);
181 return device.mightBeOpticalDisc() && !device.isOpticalDisc();
182 }),
183 aend: m_devices.end());
184
185 return m_devices;
186}
187
188QSet<Solid::DeviceInterface::Type> Manager::supportedInterfaces() const
189{
190 return m_supportedInterfaces;
191}
192
193QString Manager::udiPrefix() const
194{
195 return QStringLiteral(UD2_UDI_DISKS_PREFIX);
196}
197
198void Manager::slotInterfacesAdded(const QDBusObjectPath &object_path, const VariantMapMap &interfaces_and_properties)
199{
200 const QString udi = object_path.path();
201
202 /* Ignore jobs */
203 if (udi.startsWith(QStringLiteral(UD2_DBUS_PATH_JOBS))) {
204 return;
205 }
206
207 qCDebug(UDISKS2) << udi << "has new interfaces:" << interfaces_and_properties.keys();
208
209 auto cachedIt = m_cache.find(key: udi);
210 if (cachedIt == m_cache.end()) {
211 cachedIt = m_cache.insert(key: udi, value: VariantMapMap{});
212 }
213
214 // We need to re-fetch all existing interfaces to ensure by the time we emit "add" for FileSystem
215 // the rest is up to date (e.g. if Loop gets updated after we gained FileSystem) some propertes aren't updated yet.
216 // We'll skip Block as every device we are interested in will be a Block device.
217 QStringList oldInterfaces = cachedIt->keys();
218 oldInterfaces.removeOne(QStringLiteral(UD2_DBUS_INTERFACE_BLOCK));
219
220 for (auto it = interfaces_and_properties.begin(), end = interfaces_and_properties.end(); it != end; ++it) {
221 // Filters generic DBus interfaces.
222 if (!it.key().startsWith(s: QLatin1String(UD2_DBUS_SERVICE))) {
223 continue;
224 }
225 cachedIt->insert(key: it.key(), value: Utils::sanitizeValue(map: it.value()));
226 }
227
228 for (const QString &interface : oldInterfaces) {
229 QDBusMessage call = QDBusMessage::createMethodCall(QStringLiteral(UD2_DBUS_SERVICE), path: udi, QStringLiteral(DBUS_INTERFACE_PROPS), QStringLiteral("GetAll"));
230 call.setArguments({interface});
231 QDBusReply<QVariantMap> reply = QDBusConnection::systemBus().call(message: call);
232 if (reply.isValid()) {
233 cachedIt->insert(key: interface, value: Utils::sanitizeValue(map: reply.value()));
234 }
235 }
236
237 bool isNewDevice = !m_devices.contains(str: udi);
238 if (isNewDevice) {
239 // Check if it is an empty optical drive, and if so, ignore it.
240 Device device(this, udi);
241 if (device.mightBeOpticalDisc() && !device.isOpticalDisc()) {
242 qCDebug(UDISKS2) << "\tIt's a new empty optical drive, ignoring";
243 isNewDevice = false;
244 } else {
245 qCDebug(UDISKS2) << "\tIt's a new device, emitting added";
246 m_devices.append(t: udi);
247 }
248 }
249
250 if (isNewDevice) {
251 Q_EMIT deviceAdded(udi);
252 } else if (interfaces_and_properties.contains(key: QLatin1String(UD2_DBUS_INTERFACE_FILESYSTEM))) {
253 // re-emit in case of 2-stage devices like N9 or some Android phones
254 Q_EMIT deviceRemoved(udi);
255 Q_EMIT deviceAdded(udi);
256 }
257
258 // TODO invalidate drive? updateBackend did that
259}
260
261void Manager::slotInterfacesRemoved(const QDBusObjectPath &object_path, const QStringList &interfaces)
262{
263 const QString udi = object_path.path();
264 if (udi.isEmpty()) {
265 return;
266 }
267
268 /* Ignore jobs */
269 if (udi.startsWith(QStringLiteral(UD2_DBUS_PATH_JOBS))) {
270 return;
271 }
272
273 auto cachedIt = m_cache.find(key: udi);
274 if (cachedIt == m_cache.end()) {
275 return;
276 }
277
278 qCDebug(UDISKS2) << udi << "lost interfaces:" << interfaces;
279
280 for (const QString &iface : interfaces) {
281 cachedIt->remove(key: iface);
282 }
283
284 /*
285 * Determine left interfaces. The device backend may have processed the
286 * InterfacesRemoved signal already, but the result set is the same
287 * independent if the backend or the manager processes the signal first.
288 */
289 if (cachedIt->isEmpty()) {
290 qCDebug(UDISKS2) << "\tThere are no more interface, emitting device removal";
291 Q_EMIT deviceRemoved(udi);
292 m_cache.remove(key: udi);
293 m_devices.removeOne(t: udi);
294 } else {
295 /*
296 * Changes in the interface composition may change if a device
297 * matches a Predicate. We have to do a remove-and-readd cycle
298 * as there is no dedicated signal for Predicate reevaluation.
299 */
300 Q_EMIT deviceRemoved(udi);
301 Q_EMIT deviceAdded(udi);
302 }
303}
304
305void Manager::slotPropertiesChanged(const QDBusMessage &msg)
306{
307 const QString udi = msg.path();
308
309 if (udi.isEmpty() || !udi.startsWith(s: QLatin1String(UD2_UDI_DISKS_PREFIX)) || udi.startsWith(s: QLatin1String(UD2_DBUS_PATH_JOBS))) {
310 return;
311 }
312
313 const auto args = msg.arguments();
314 if (Q_UNLIKELY(args.size() != 3)) {
315 return;
316 }
317
318 const QString iface = qdbus_cast<QString>(v: args.at(i: 0));
319 const QVariantMap changed = qdbus_cast<QVariantMap>(v: args.at(i: 1));
320 const QStringList invalidated = qdbus_cast<QStringList>(v: args.at(i: 2));
321
322 auto cachedIt = m_cache.find(key: udi);
323 if (cachedIt == m_cache.end()) {
324 return;
325 }
326
327 const bool knownDevice = m_devices.contains(str: udi);
328
329 // Update cache of internal devices even if we don't advertise them at this time.
330 QMap<QString, int> changeMap;
331
332 for (const QString &prop : invalidated) {
333 // Invalid QVariant() marks property that exists but needs to be fetched first.
334 (*cachedIt)[iface].insert(key: prop, value: QVariant());
335 changeMap.insert(key: prop, value: Solid::GenericInterface::PropertyModified);
336 }
337
338 for (auto it = changed.begin(), end = changed.end(); it != end; ++it) {
339 (*cachedIt)[iface].insert(key: it.key(), value: Utils::sanitizeValue(value: it.value()));
340 changeMap.insert(key: it.key(), value: Solid::GenericInterface::PropertyModified);
341 }
342
343 // Only announce the change if the device is advertised.
344 if (knownDevice && !changeMap.isEmpty()) {
345 Q_EMIT propertyChanged(udi, changes: changeMap);
346 }
347
348 // Special handling for optical media insertion/removal.
349 if (iface == QLatin1String(UD2_DBUS_INTERFACE_BLOCK) && (changed.contains(QStringLiteral("Size")) || invalidated.contains(QStringLiteral("Size")))) {
350 qulonglong size = deviceProperty(udi, QStringLiteral("Size")).toULongLong();
351
352 const bool mediaInserted = !knownDevice && size > 0;
353 const bool mediaRemoved = knownDevice && size == 0;
354
355 if (mediaInserted || mediaRemoved) {
356 Device device(this, udi);
357 if (device.mightBeOpticalDisc()) {
358 if (!knownDevice && size > 0) { // we don't know the optical disc, got inserted.
359 const OpticalDisc disc(&device);
360 // If it is a data disc, wait for its FileSystem interface to be announced instead,
361 // otherwise we'll add this disc twice. But if we don't add it here, we will miss
362 // Audio CDs that will never have a FileSystem interface.
363 if (!disc.availableContent().testFlag(flag: Solid::OpticalDisc::Data)) {
364 m_devices.append(t: udi);
365 Q_EMIT deviceAdded(udi);
366 }
367 } else if (knownDevice && size == 0) { // we know the optical disc, got removed.
368 Q_EMIT deviceRemoved(udi);
369 // Keeping the cache as we never fetch all device properties again after the initial query.
370 m_devices.removeOne(t: udi);
371 }
372 }
373 }
374 }
375
376 // TODO invalidate drive? updateBackend did that
377}
378
379QMap<QString, PropertyMap> Manager::deviceCache()
380{
381 if (m_cache.isEmpty()) {
382 allDevices();
383 }
384
385 return m_cache;
386}
387
388#include "moc_udisksmanager.cpp"
389

source code of solid/src/solid/devices/backends/udisks2/udisksmanager.cpp