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