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 | |