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
19using namespace Solid::Backends::UDisks2;
20using namespace Solid::Backends::Shared;
21
22Manager::Manager(QObject *parent)
23 : Solid::Ifaces::DeviceManager(parent)
24 , m_manager(UD2_DBUS_SERVICE, 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(destination: "org.freedesktop.DBus", //
45 path: "/org/freedesktop/DBus",
46 interface: "org.freedesktop.DBus",
47 method: "ListActivatableNames");
48
49 QDBusReply<QStringList> reply = QDBusConnection::systemBus().call(message);
50 if (reply.isValid() && reply.value().contains(UD2_DBUS_SERVICE)) {
51 QDBusConnection::systemBus().interface()->startService(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
62Manager::~Manager()
63{
64 while (!m_deviceCache.isEmpty()) {
65 QString udi = m_deviceCache.takeFirst();
66 DeviceBackend::destroyBackend(udi);
67 }
68}
69
70QObject *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("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
87QStringList 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
115QStringList Manager::allDevices()
116{
117 m_deviceCache.clear();
118
119 introspect(UD2_DBUS_PATH_BLOCKDEVICES, checkOptical: true /*checkOptical*/);
120 introspect(UD2_DBUS_PATH_DRIVES);
121
122 return m_deviceCache;
123}
124
125void Manager::introspect(const QString &path, bool checkOptical)
126{
127 QDBusMessage call = QDBusMessage::createMethodCall(UD2_DBUS_SERVICE, path, DBUS_INTERFACE_INTROSPECT, method: "Introspect");
128 QDBusPendingReply<QString> reply = QDBusConnection::systemBus().call(message: call);
129
130 if (reply.isValid()) {
131 QDomDocument dom;
132 dom.setContent(data: reply.value());
133 QDomNodeList nodeList = dom.documentElement().elementsByTagName(tagname: "node");
134 for (int i = 0; i < nodeList.count(); i++) {
135 QDomElement nodeElem = nodeList.item(index: i).toElement();
136 if (!nodeElem.isNull() && nodeElem.hasAttribute(name: "name")) {
137 const QString name = nodeElem.attribute(name: "name");
138 const QString udi = path + "/" + name;
139
140 // Optimization, a loop device cannot really have a physical drive associated with it
141 if (checkOptical && !name.startsWith(s: QLatin1String("loop"))) {
142 Device device(udi);
143 if (device.mightBeOpticalDisc()) {
144 QDBusConnection::systemBus().connect(UD2_DBUS_SERVICE, //
145 path: udi,
146 DBUS_INTERFACE_PROPS,
147 name: "PropertiesChanged",
148 receiver: this,
149 SLOT(slotMediaChanged(QDBusMessage)));
150 if (!device.isOpticalDisc()) { // skip empty CD disc
151 continue;
152 }
153 }
154 }
155
156 m_deviceCache.append(t: udi);
157 }
158 }
159 } else {
160 qCWarning(UDISKS2) << "Failed enumerating UDisks2 objects:" << reply.error().name() << "\n" << reply.error().message();
161 }
162}
163
164QSet<Solid::DeviceInterface::Type> Manager::supportedInterfaces() const
165{
166 return m_supportedInterfaces;
167}
168
169QString Manager::udiPrefix() const
170{
171 return UD2_UDI_DISKS_PREFIX;
172}
173
174void Manager::slotInterfacesAdded(const QDBusObjectPath &object_path, const VariantMapMap &interfaces_and_properties)
175{
176 const QString udi = object_path.path();
177
178 /* Ignore jobs */
179 if (udi.startsWith(UD2_DBUS_PATH_JOBS)) {
180 return;
181 }
182
183 qCDebug(UDISKS2) << udi << "has new interfaces:" << interfaces_and_properties.keys();
184
185 // If device gained an org.freedesktop.UDisks2.Block interface, we
186 // should check if it is an optical drive, in order to properly
187 // register mediaChanged event handler with newly-plugged external
188 // drives
189 if (interfaces_and_properties.contains(key: "org.freedesktop.UDisks2.Block")) {
190 Device device(udi);
191 if (device.mightBeOpticalDisc()) {
192 QDBusConnection::systemBus().connect(UD2_DBUS_SERVICE, //
193 path: udi,
194 DBUS_INTERFACE_PROPS,
195 name: "PropertiesChanged",
196 receiver: this,
197 SLOT(slotMediaChanged(QDBusMessage)));
198 }
199 }
200
201 updateBackend(udi);
202
203 // new device, we don't know it yet
204 if (!m_deviceCache.contains(str: udi)) {
205 m_deviceCache.append(t: udi);
206 Q_EMIT deviceAdded(udi);
207 }
208 // re-emit in case of 2-stage devices like N9 or some Android phones
209 else if (m_deviceCache.contains(str: udi) && interfaces_and_properties.keys().contains(UD2_DBUS_INTERFACE_FILESYSTEM)) {
210 Q_EMIT deviceAdded(udi);
211 }
212}
213
214void Manager::slotInterfacesRemoved(const QDBusObjectPath &object_path, const QStringList &interfaces)
215{
216 const QString udi = object_path.path();
217 if (udi.isEmpty()) {
218 return;
219 }
220
221 /* Ignore jobs */
222 if (udi.startsWith(UD2_DBUS_PATH_JOBS)) {
223 return;
224 }
225
226 qCDebug(UDISKS2) << udi << "lost interfaces:" << interfaces;
227
228 /*
229 * Determine left interfaces. The device backend may have processed the
230 * InterfacesRemoved signal already, but the result set is the same
231 * independent if the backend or the manager processes the signal first.
232 */
233 Device device(udi);
234 const QStringList ifaceList = device.interfaces();
235 QSet<QString> leftInterfaces(ifaceList.begin(), ifaceList.end());
236 leftInterfaces.subtract(other: QSet<QString>(interfaces.begin(), interfaces.end()));
237
238 if (leftInterfaces.isEmpty()) {
239 // remove the device if the last interface is removed
240 Q_EMIT deviceRemoved(udi);
241 m_deviceCache.removeAll(t: udi);
242 DeviceBackend::destroyBackend(udi);
243 } else {
244 /*
245 * Changes in the interface composition may change if a device
246 * matches a Predicate. We have to do a remove-and-readd cycle
247 * as there is no dedicated signal for Predicate reevaluation.
248 */
249 Q_EMIT deviceRemoved(udi);
250 Q_EMIT deviceAdded(udi);
251 }
252}
253
254void Manager::slotMediaChanged(const QDBusMessage &msg)
255{
256 const QVariantMap properties = qdbus_cast<QVariantMap>(v: msg.arguments().at(i: 1));
257
258 if (!properties.contains(key: "Size")) { // react only on Size changes
259 return;
260 }
261
262 const QString udi = msg.path();
263 updateBackend(udi);
264 qulonglong size = properties.value(key: "Size").toULongLong();
265 qCDebug(UDISKS2) << "MEDIA CHANGED in" << udi << "; size is:" << size;
266
267 if (!m_deviceCache.contains(str: udi) && size > 0) { // we don't know the optdisc, got inserted
268 m_deviceCache.append(t: udi);
269 Q_EMIT deviceAdded(udi);
270 }
271
272 if (m_deviceCache.contains(str: udi) && size == 0) { // we know the optdisc, got removed
273 Q_EMIT deviceRemoved(udi);
274 m_deviceCache.removeAll(t: udi);
275 DeviceBackend::destroyBackend(udi);
276 }
277}
278
279const QStringList &Manager::deviceCache()
280{
281 if (m_deviceCache.isEmpty()) {
282 allDevices();
283 }
284
285 return m_deviceCache;
286}
287
288void Manager::updateBackend(const QString &udi)
289{
290 DeviceBackend *backend = DeviceBackend::backendForUDI(udi);
291 if (!backend) {
292 return;
293 }
294
295 // This doesn't emit "changed" signals. Signals are emitted later by DeviceBackend's slots
296 backend->allProperties();
297
298 QVariant driveProp = backend->prop(key: "Drive");
299 if (!driveProp.isValid()) {
300 return;
301 }
302
303 QDBusObjectPath drivePath = qdbus_cast<QDBusObjectPath>(v: driveProp);
304 DeviceBackend *driveBackend = DeviceBackend::backendForUDI(udi: drivePath.path(), create: false);
305 if (!driveBackend) {
306 return;
307 }
308
309 driveBackend->invalidateProperties();
310}
311
312#include "moc_udisksmanager.cpp"
313

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