1 /*
2 SPDX-FileCopyrightText: 2010 Jacopo De Simoi <wilderkde@gmail.com>
3 SPDX-FileCopyrightText: 2014 Lukáš Tinkl <ltinkl@redhat.com>
4 SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
5
6 SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9 #include "ksolidnotify.h"
10
11 #include <Solid/DeviceInterface>
12 #include <Solid/DeviceNotifier>
13 #include <Solid/OpticalDisc>
14 #include <Solid/OpticalDrive>
15 #include <Solid/PortableMediaPlayer>
16 #include <Solid/Predicate>
17 #include <Solid/StorageAccess>
18 #include <Solid/StorageDrive>
19 #include <Solid/StorageVolume>
20
21 #include <KLocalizedString>
22 #include <KNotification>
23 #include <processcore/process.h>
24 #include <processcore/processes.h>
25
26 #include <QProcess>
27 #include <QRegularExpression>
28 #include <QStringList>
29 #include <QStringRef>
30 #include <QVector>
31
KSolidNotify(QObject * parent)32 KSolidNotify::KSolidNotify(QObject *parent)
33 : QObject(parent)
34 {
35 Solid::Predicate p(Solid::DeviceInterface::StorageAccess);
36 p |= Solid::Predicate(Solid::DeviceInterface::OpticalDrive);
37 p |= Solid::Predicate(Solid::DeviceInterface::PortableMediaPlayer);
38 const QList<Solid::Device> &devices = Solid::Device::listFromQuery(p);
39 for (const Solid::Device &dev : devices) {
40 m_devices.insert(dev.udi(), dev);
41 connectSignals(&m_devices[dev.udi()]);
42 }
43
44 connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &KSolidNotify::onDeviceAdded);
45 connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &KSolidNotify::onDeviceRemoved);
46 }
47
onDeviceAdded(const QString & udi)48 void KSolidNotify::onDeviceAdded(const QString &udi)
49 {
50 // Clear any stale message from a previous instance
51 emit clearNotification(udi);
52 Solid::Device device(udi);
53 m_devices.insert(udi, device);
54 connectSignals(&m_devices[udi]);
55 }
56
onDeviceRemoved(const QString & udi)57 void KSolidNotify::onDeviceRemoved(const QString &udi)
58 {
59 if (m_devices[udi].is<Solid::StorageVolume>()) {
60 Solid::StorageAccess *access = m_devices[udi].as<Solid::StorageAccess>();
61 if (access) {
62 disconnect(access, nullptr, this, nullptr);
63 }
64 }
65 m_devices.remove(udi);
66 }
67
isSafelyRemovable(const QString & udi) const68 bool KSolidNotify::isSafelyRemovable(const QString &udi) const
69 {
70 Solid::Device parent = m_devices[udi].parent();
71 if (parent.is<Solid::StorageDrive>()) {
72 Solid::StorageDrive *drive = parent.as<Solid::StorageDrive>();
73 return (!drive->isInUse() && (drive->isHotpluggable() || drive->isRemovable()));
74 }
75
76 const Solid::StorageAccess *access = m_devices[udi].as<Solid::StorageAccess>();
77 if (access) {
78 return !m_devices[udi].as<Solid::StorageAccess>()->isAccessible();
79 } else {
80 // If this check fails, the device has been already physically
81 // ejected, so no need to say that it is safe to remove it
82 return false;
83 }
84 }
85
connectSignals(Solid::Device * device)86 void KSolidNotify::connectSignals(Solid::Device *device)
87 {
88 Solid::StorageAccess *access = device->as<Solid::StorageAccess>();
89 if (access) {
90 connect(access, &Solid::StorageAccess::teardownDone, this, [=](Solid::ErrorType error, const QVariant &errorData, const QString &udi) {
91 onSolidReply(SolidReplyType::Teardown, error, errorData, udi);
92 });
93
94 connect(access, &Solid::StorageAccess::setupDone, this, [=](Solid::ErrorType error, const QVariant &errorData, const QString &udi) {
95 onSolidReply(SolidReplyType::Setup, error, errorData, udi);
96 });
97 }
98 if (device->is<Solid::OpticalDisc>()) {
99 Solid::OpticalDrive *drive = device->parent().as<Solid::OpticalDrive>();
100 connect(drive, &Solid::OpticalDrive::ejectDone, this, [=](Solid::ErrorType error, const QVariant &errorData, const QString &udi) {
101 onSolidReply(SolidReplyType::Eject, error, errorData, udi);
102 });
103 }
104 }
105
queryBlockingApps(const QString & devicePath)106 void KSolidNotify::queryBlockingApps(const QString &devicePath)
107 {
108 QProcess *p = new QProcess;
109 connect(p, static_cast<void (QProcess::*)(QProcess::ProcessError)>(&QProcess::errorOccurred), [=](QProcess::ProcessError) {
110 emit blockingAppsReady({});
111 p->deleteLater();
112 });
113 connect(p, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), [=](int, QProcess::ExitStatus) {
114 QStringList blockApps;
115 QString out(p->readAll());
116 const QVector<QStringRef> pidList = out.splitRef(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
117 KSysGuard::Processes procs;
118 for (const QStringRef &pidStr : pidList) {
119 int pid = pidStr.toInt();
120 if (!pid) {
121 continue;
122 }
123 procs.updateOrAddProcess(pid);
124 KSysGuard::Process *proc = procs.getProcess(pid);
125 if (!blockApps.contains(proc->name())) {
126 blockApps << proc->name();
127 }
128 }
129 blockApps.removeDuplicates();
130 emit blockingAppsReady(blockApps);
131 p->deleteLater();
132 });
133 p->start(QStringLiteral("lsof"), {QStringLiteral("-t"), devicePath});
134 // p.start(QStringLiteral("fuser"), {QStringLiteral("-m"), devicePath});
135 }
136
onSolidReply(SolidReplyType type,Solid::ErrorType error,const QVariant & errorData,const QString & udi)137 void KSolidNotify::onSolidReply(SolidReplyType type, Solid::ErrorType error, const QVariant &errorData, const QString &udi)
138 {
139 if ((error == Solid::ErrorType::NoError) && (type == SolidReplyType::Setup)) {
140 emit clearNotification(udi);
141 return;
142 }
143
144 QString errorMsg;
145
146 switch (error) {
147 case Solid::ErrorType::NoError:
148 if (type != SolidReplyType::Setup && isSafelyRemovable(udi)) {
149 KNotification::event(QStringLiteral("safelyRemovable"), i18n("Device Status"), i18n("A device can now be safely removed"));
150 errorMsg = i18n("This device can now be safely removed.");
151 }
152 break;
153
154 case Solid::ErrorType::UnauthorizedOperation:
155 switch (type) {
156 case SolidReplyType::Setup:
157 errorMsg = i18n("You are not authorized to mount this device.");
158 break;
159 case SolidReplyType::Teardown:
160 errorMsg = i18nc("Remove is less technical for unmount", "You are not authorized to remove this device.");
161 break;
162 case SolidReplyType::Eject:
163 errorMsg = i18n("You are not authorized to eject this disc.");
164 break;
165 }
166
167 break;
168 case Solid::ErrorType::DeviceBusy: {
169 if (type == SolidReplyType::Setup) { // can this even happen?
170 errorMsg = i18n("Could not mount this device as it is busy.");
171 } else {
172 Solid::Device device;
173
174 if (type == SolidReplyType::Eject) {
175 QString discUdi;
176 for (Solid::Device device : qAsConst(m_devices)) {
177 if (device.parentUdi() == udi) {
178 discUdi = device.udi();
179 }
180 }
181
182 if (discUdi.isNull()) {
183 // This should not happen, bail out
184 return;
185 }
186
187 device = Solid::Device(discUdi);
188 } else {
189 device = Solid::Device(udi);
190 }
191
192 Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
193
194 // Without that, our lambda function would capture an uninitialized object, resulting in UB
195 // and random crashes
196 QMetaObject::Connection *c = new QMetaObject::Connection();
197 *c = connect(this, &KSolidNotify::blockingAppsReady, [=](const QStringList &blockApps) {
198 QString errorMessage;
199 if (blockApps.isEmpty()) {
200 errorMessage = i18n("One or more files on this device are open within an application.");
201 } else {
202 errorMessage = i18np("One or more files on this device are opened in application \"%2\".",
203 "One or more files on this device are opened in following applications: %2.",
204 blockApps.count(),
205 blockApps.join(i18nc("separator in list of apps blocking device unmount", ", ")));
206 }
207 emit notify(error, errorMessage, errorData.toString(), udi);
208 disconnect(*c);
209 delete c;
210 });
211 queryBlockingApps(access->filePath());
212 }
213
214 break;
215 }
216 case Solid::ErrorType::UserCanceled:
217 // don't point out the obvious to the user, do nothing here
218 break;
219 default:
220 switch (type) {
221 case SolidReplyType::Setup:
222 errorMsg = i18n("Could not mount this device.");
223 break;
224 case SolidReplyType::Teardown:
225 errorMsg = i18nc("Remove is less technical for unmount", "Could not remove this device.");
226 break;
227 case SolidReplyType::Eject:
228 errorMsg = i18n("Could not eject this disc.");
229 break;
230 }
231
232 break;
233 }
234
235 if (!errorMsg.isEmpty()) {
236 emit notify(error, errorMsg, errorData.toString(), udi);
237 }
238 }
239