File indexing completed on 2025-01-26 05:09:00

0001 /*
0002     SPDX-FileCopyrightText: 2010 Jacopo De Simoi <wilderkde@gmail.com>
0003     SPDX-FileCopyrightText: 2014 Lukáš Tinkl <ltinkl@redhat.com>
0004     SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "ksolidnotify.h"
0010 
0011 #include <Solid/DeviceInterface>
0012 #include <Solid/DeviceNotifier>
0013 #include <Solid/OpticalDisc>
0014 #include <Solid/OpticalDrive>
0015 #include <Solid/PortableMediaPlayer>
0016 #include <Solid/Predicate>
0017 #include <Solid/StorageAccess>
0018 #include <Solid/StorageDrive>
0019 #include <Solid/StorageVolume>
0020 
0021 #include <KLocalizedString>
0022 #include <KNotification>
0023 #include <processcore/process.h>
0024 #include <processcore/processes.h>
0025 
0026 #include <QList>
0027 #include <QProcess>
0028 #include <QRegularExpression>
0029 #include <QStringList>
0030 #include <QStringView>
0031 
0032 KSolidNotify::KSolidNotify(QObject *parent)
0033     : QObject(parent)
0034 {
0035     Solid::Predicate p(Solid::DeviceInterface::StorageAccess);
0036     p |= Solid::Predicate(Solid::DeviceInterface::OpticalDrive);
0037     p |= Solid::Predicate(Solid::DeviceInterface::PortableMediaPlayer);
0038     const QList<Solid::Device> &devices = Solid::Device::listFromQuery(p);
0039     for (const Solid::Device &dev : devices) {
0040         m_devices.insert(dev.udi(), dev);
0041         connectSignals(&m_devices[dev.udi()]);
0042     }
0043 
0044     connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &KSolidNotify::onDeviceAdded);
0045     connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &KSolidNotify::onDeviceRemoved);
0046 }
0047 
0048 void KSolidNotify::onDeviceAdded(const QString &udi)
0049 {
0050     // Clear any stale message from a previous instance
0051     Q_EMIT clearNotification(udi);
0052     Solid::Device device(udi);
0053     m_devices.insert(udi, device);
0054     connectSignals(&m_devices[udi]);
0055 }
0056 
0057 void KSolidNotify::onDeviceRemoved(const QString &udi)
0058 {
0059     if (m_devices[udi].is<Solid::StorageVolume>()) {
0060         Solid::StorageAccess *access = m_devices[udi].as<Solid::StorageAccess>();
0061         if (access) {
0062             disconnect(access, nullptr, this, nullptr);
0063         }
0064     }
0065     m_devices.remove(udi);
0066 }
0067 
0068 bool KSolidNotify::isSafelyRemovable(const QString &udi) const
0069 {
0070     Solid::Device parent = m_devices[udi].parent();
0071     if (parent.is<Solid::StorageDrive>()) {
0072         Solid::StorageDrive *drive = parent.as<Solid::StorageDrive>();
0073         return (!drive->isInUse() && (drive->isHotpluggable() || drive->isRemovable()));
0074     }
0075 
0076     const Solid::StorageAccess *access = m_devices[udi].as<Solid::StorageAccess>();
0077     if (access) {
0078         return !m_devices[udi].as<Solid::StorageAccess>()->isAccessible();
0079     } else {
0080         // If this check fails, the device has been already physically
0081         // ejected, so no need to say that it is safe to remove it
0082         return false;
0083     }
0084 }
0085 
0086 void KSolidNotify::connectSignals(Solid::Device *device)
0087 {
0088     Solid::StorageAccess *access = device->as<Solid::StorageAccess>();
0089     if (access) {
0090         connect(access, &Solid::StorageAccess::teardownDone, this, [this](Solid::ErrorType error, const QVariant &errorData, const QString &udi) {
0091             onSolidReply(SolidReplyType::Teardown, error, errorData, udi);
0092         });
0093 
0094         connect(access, &Solid::StorageAccess::setupDone, this, [this](Solid::ErrorType error, const QVariant &errorData, const QString &udi) {
0095             onSolidReply(SolidReplyType::Setup, error, errorData, udi);
0096         });
0097     }
0098     if (device->is<Solid::OpticalDisc>()) {
0099         Solid::OpticalDrive *drive = device->parent().as<Solid::OpticalDrive>();
0100         connect(drive, &Solid::OpticalDrive::ejectDone, this, [this](Solid::ErrorType error, const QVariant &errorData, const QString &udi) {
0101             onSolidReply(SolidReplyType::Eject, error, errorData, udi);
0102         });
0103     }
0104 }
0105 
0106 void KSolidNotify::queryBlockingApps(const QString &devicePath)
0107 {
0108     QProcess *p = new QProcess;
0109     connect(p, static_cast<void (QProcess::*)(QProcess::ProcessError)>(&QProcess::errorOccurred), [=, this](QProcess::ProcessError) {
0110         Q_EMIT blockingAppsReady({});
0111         p->deleteLater();
0112     });
0113     connect(p, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), [=, this](int, QProcess::ExitStatus) {
0114         QStringList blockApps;
0115         QString out = QString::fromUtf8(p->readAll());
0116         const auto pidList = QStringView(out).split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
0117         KSysGuard::Processes procs;
0118         for (const QStringView &pidStr : pidList) {
0119             int pid = pidStr.toInt();
0120             if (!pid) {
0121                 continue;
0122             }
0123             procs.updateOrAddProcess(pid);
0124             KSysGuard::Process *proc = procs.getProcess(pid);
0125             if (!blockApps.contains(proc->name())) {
0126                 blockApps << proc->name();
0127             }
0128         }
0129         blockApps.removeDuplicates();
0130         Q_EMIT blockingAppsReady(blockApps);
0131         p->deleteLater();
0132     });
0133     p->start(QStringLiteral("lsof"), {QStringLiteral("-t"), devicePath});
0134     //    p.start(QStringLiteral("fuser"), {QStringLiteral("-m"), devicePath});
0135 }
0136 
0137 void KSolidNotify::onSolidReply(SolidReplyType type, Solid::ErrorType error, const QVariant &errorData, const QString &udi)
0138 {
0139     if ((error == Solid::ErrorType::NoError) && (type == SolidReplyType::Setup)) {
0140         Q_EMIT clearNotification(udi);
0141         return;
0142     }
0143 
0144     QString errorMsg;
0145 
0146     switch (error) {
0147     case Solid::ErrorType::NoError:
0148         if (type != SolidReplyType::Setup && isSafelyRemovable(udi)) {
0149             KNotification::event(QStringLiteral("safelyRemovable"), i18n("Device Status"), i18n("A device can now be safely removed"));
0150             errorMsg = i18n("This device can now be safely removed.");
0151         }
0152         break;
0153 
0154     case Solid::ErrorType::UnauthorizedOperation:
0155         switch (type) {
0156         case SolidReplyType::Setup:
0157             errorMsg = i18n("You are not authorized to mount this device.");
0158             break;
0159         case SolidReplyType::Teardown:
0160             errorMsg = i18nc("Remove is less technical for unmount", "You are not authorized to remove this device.");
0161             break;
0162         case SolidReplyType::Eject:
0163             errorMsg = i18n("You are not authorized to eject this disc.");
0164             break;
0165         }
0166 
0167         break;
0168     case Solid::ErrorType::DeviceBusy: {
0169         if (type == SolidReplyType::Setup) { // can this even happen?
0170             errorMsg = i18n("Could not mount this device as it is busy.");
0171         } else {
0172             Solid::Device device;
0173 
0174             if (type == SolidReplyType::Eject) {
0175                 QString discUdi;
0176                 for (const Solid::Device &device : std::as_const(m_devices)) {
0177                     if (device.parentUdi() == udi) {
0178                         discUdi = device.udi();
0179                     }
0180                 }
0181 
0182                 if (discUdi.isNull()) {
0183                     // This should not happen, bail out
0184                     return;
0185                 }
0186 
0187                 device = Solid::Device(discUdi);
0188             } else {
0189                 device = Solid::Device(udi);
0190             }
0191 
0192             Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
0193 
0194             // Without that, our lambda function would capture an uninitialized object, resulting in UB
0195             // and random crashes
0196             QMetaObject::Connection *c = new QMetaObject::Connection();
0197             *c = connect(this, &KSolidNotify::blockingAppsReady, [=, this](const QStringList &blockApps) {
0198                 QString errorMessage;
0199                 if (blockApps.isEmpty()) {
0200                     errorMessage = i18n("One or more files on this device are open within an application.");
0201                 } else {
0202                     errorMessage = i18np("One or more files on this device are opened in application \"%2\".",
0203                                          "One or more files on this device are opened in following applications: %2.",
0204                                          blockApps.count(),
0205                                          blockApps.join(i18nc("separator in list of apps blocking device unmount", ", ")));
0206                 }
0207                 Q_EMIT notify(error, errorMessage, errorData.toString(), udi);
0208                 disconnect(*c);
0209                 delete c;
0210             });
0211             queryBlockingApps(access->filePath());
0212         }
0213 
0214         break;
0215     }
0216     case Solid::ErrorType::UserCanceled:
0217         // don't point out the obvious to the user, do nothing here
0218         break;
0219     default:
0220         switch (type) {
0221         case SolidReplyType::Setup:
0222             errorMsg = i18n("Could not mount this device.");
0223             break;
0224         case SolidReplyType::Teardown:
0225             errorMsg = i18nc("Remove is less technical for unmount", "Could not remove this device.");
0226             break;
0227         case SolidReplyType::Eject:
0228             errorMsg = i18n("Could not eject this disc.");
0229             break;
0230         }
0231 
0232         break;
0233     }
0234 
0235     if (!errorMsg.isEmpty()) {
0236         Q_EMIT notify(error, errorMsg, errorData.toString(), udi);
0237     }
0238 }