File indexing completed on 2024-06-16 05:09:54

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 <QProcess>
0027 #include <QRegularExpression>
0028 #include <QStringList>
0029 #include <QStringView>
0030 
0031 KSolidNotify::KSolidNotify(QObject *parent)
0032     : QObject(parent)
0033 {
0034     Solid::Predicate p(Solid::DeviceInterface::StorageAccess);
0035     p |= Solid::Predicate(Solid::DeviceInterface::OpticalDrive);
0036     p |= Solid::Predicate(Solid::DeviceInterface::PortableMediaPlayer);
0037     for (const QList<Solid::Device> &devices = Solid::Device::listFromQuery(p); const Solid::Device &dev : devices) {
0038         connectSignals(*m_devices.insert(dev.udi(), dev));
0039     }
0040 
0041     connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &KSolidNotify::onDeviceAdded);
0042     connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &KSolidNotify::onDeviceRemoved);
0043 }
0044 
0045 QString KSolidNotify::lastUdi() const
0046 {
0047     return m_lastUdi;
0048 }
0049 
0050 Solid::ErrorType KSolidNotify::lastErrorType() const
0051 {
0052     return m_lastErrorType;
0053 }
0054 
0055 QString KSolidNotify::lastMessage() const
0056 {
0057     return m_lastMessage;
0058 }
0059 
0060 QString KSolidNotify::lastDescription() const
0061 {
0062     return m_lastDescription;
0063 }
0064 
0065 QString KSolidNotify::lastIcon() const
0066 {
0067     return m_lastIcon;
0068 }
0069 
0070 void KSolidNotify::clearMessage()
0071 {
0072     if (m_lastUdi.isEmpty()) {
0073         return;
0074     }
0075 
0076     m_lastUdi.clear();
0077     m_lastErrorType = static_cast<Solid::ErrorType>(0);
0078     m_lastMessage.clear();
0079     m_lastDescription.clear();
0080     m_lastIcon.clear();
0081     Q_EMIT lastUdiChanged();
0082     Q_EMIT lastErrorTypeChanged();
0083     Q_EMIT lastMessageChanged();
0084     Q_EMIT lastDescriptionChanged();
0085     Q_EMIT lastIconChanged();
0086 }
0087 
0088 void KSolidNotify::onDeviceAdded(const QString &udi)
0089 {
0090     // Clear any stale message from a previous instance
0091     clearMessage();
0092 
0093     connectSignals(*m_devices.emplace(udi, udi));
0094 }
0095 
0096 void KSolidNotify::onDeviceRemoved(const QString &udi)
0097 {
0098     if (m_devices[udi].is<Solid::StorageVolume>()) {
0099         Solid::StorageAccess *access = m_devices[udi].as<Solid::StorageAccess>();
0100         if (access) {
0101             access->disconnect(this);
0102         }
0103     }
0104     m_devices.remove(udi);
0105 }
0106 
0107 bool KSolidNotify::isSafelyRemovable(const QString &udi) const
0108 {
0109     Solid::Device parent = m_devices[udi].parent();
0110     if (parent.is<Solid::StorageDrive>()) {
0111         Solid::StorageDrive *drive = parent.as<Solid::StorageDrive>();
0112         return (!drive->isInUse() && (drive->isHotpluggable() || drive->isRemovable()));
0113     }
0114 
0115     const Solid::StorageAccess *access = m_devices[udi].as<Solid::StorageAccess>();
0116     if (access) {
0117         return !m_devices[udi].as<Solid::StorageAccess>()->isAccessible();
0118     } else {
0119         // If this check fails, the device has been already physically
0120         // ejected, so no need to say that it is safe to remove it
0121         return false;
0122     }
0123 }
0124 
0125 void KSolidNotify::connectSignals(Solid::Device &device)
0126 {
0127     Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
0128     if (access) {
0129         connect(access, &Solid::StorageAccess::teardownDone, this, [this](Solid::ErrorType error, const QVariant &errorData, const QString &udi) {
0130             onSolidReply(SolidReplyType::Teardown, error, errorData, udi);
0131         });
0132 
0133         connect(access, &Solid::StorageAccess::setupDone, this, [this](Solid::ErrorType error, const QVariant &errorData, const QString &udi) {
0134             onSolidReply(SolidReplyType::Setup, error, errorData, udi);
0135         });
0136     }
0137     if (device.is<Solid::OpticalDisc>()) {
0138         Solid::OpticalDrive *drive = device.parent().as<Solid::OpticalDrive>();
0139         connect(drive, &Solid::OpticalDrive::ejectDone, this, [this](Solid::ErrorType error, const QVariant &errorData, const QString &udi) {
0140             onSolidReply(SolidReplyType::Eject, error, errorData, udi);
0141         });
0142     }
0143 }
0144 
0145 void KSolidNotify::queryBlockingApps(const QString &devicePath)
0146 {
0147     QProcess *p = new QProcess;
0148     connect(p, static_cast<void (QProcess::*)(QProcess::ProcessError)>(&QProcess::errorOccurred), [=, this](QProcess::ProcessError) {
0149         Q_EMIT blockingAppsReady({});
0150         p->deleteLater();
0151     });
0152     connect(p, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), [p, this](int, QProcess::ExitStatus) {
0153         QStringList blockApps;
0154         const QString out = QString::fromLatin1(p->readAll());
0155         const auto pidList = QStringView(out).split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
0156         KSysGuard::Processes procs;
0157         for (const QStringView pidStr : pidList) {
0158             int pid = pidStr.toInt();
0159             if (!pid) {
0160                 continue;
0161             }
0162             procs.updateOrAddProcess(pid);
0163             KSysGuard::Process *proc = procs.getProcess(pid);
0164             if (!blockApps.contains(proc->name())) {
0165                 blockApps << proc->name();
0166             }
0167         }
0168         Q_EMIT blockingAppsReady(blockApps);
0169         p->deleteLater();
0170     });
0171     p->start(QStringLiteral("lsof"), {QStringLiteral("-t"), devicePath});
0172     //    p.start(QStringLiteral("fuser"), {QStringLiteral("-m"), devicePath});
0173 }
0174 
0175 void KSolidNotify::onSolidReply(SolidReplyType type, Solid::ErrorType error, const QVariant &errorData, const QString &udi)
0176 {
0177     if ((error == Solid::ErrorType::NoError) && (type == SolidReplyType::Setup)) {
0178         if (m_lastUdi == udi) {
0179             clearMessage();
0180         }
0181         return;
0182     }
0183 
0184     QString errorMsg;
0185     QString icon;
0186 
0187     switch (error) {
0188     case Solid::ErrorType::NoError:
0189         if (type != SolidReplyType::Setup && isSafelyRemovable(udi)) {
0190             KNotification::event(QStringLiteral("safelyRemovable"), i18n("Device Status"), i18n("A device can now be safely removed"));
0191             errorMsg = i18n("This device can now be safely removed.");
0192         }
0193         break;
0194 
0195     case Solid::ErrorType::UnauthorizedOperation:
0196         switch (type) {
0197         case SolidReplyType::Setup:
0198             errorMsg = i18n("You are not authorized to mount this device.");
0199             break;
0200         case SolidReplyType::Teardown:
0201             errorMsg = i18nc("Remove is less technical for unmount", "You are not authorized to remove this device.");
0202             break;
0203         case SolidReplyType::Eject:
0204             errorMsg = i18n("You are not authorized to eject this disc.");
0205             break;
0206         }
0207 
0208         break;
0209     case Solid::ErrorType::DeviceBusy: {
0210         if (type == SolidReplyType::Setup) { // can this even happen?
0211             errorMsg = i18n("Could not mount this device as it is busy.");
0212         } else {
0213             Solid::Device device;
0214 
0215             if (type == SolidReplyType::Eject) {
0216                 QString discUdi;
0217                 for (const Solid::Device &device : std::as_const(m_devices)) {
0218                     if (device.parentUdi() == udi) {
0219                         discUdi = device.udi();
0220                     }
0221                 }
0222 
0223                 if (discUdi.isNull()) {
0224                     Q_ASSERT_X(false, Q_FUNC_INFO, "This should not happen, bail out");
0225                     return;
0226                 }
0227 
0228                 device = Solid::Device(discUdi);
0229             } else {
0230                 device = Solid::Device(udi);
0231             }
0232 
0233             icon = device.icon();
0234             Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
0235 
0236             // Without that, our lambda function would capture an uninitialized object, resulting in UB
0237             // and random crashes
0238             QMetaObject::Connection *c = new QMetaObject::Connection();
0239             *c = connect(this, &KSolidNotify::blockingAppsReady, [c, error, errorData, udi, icon, this](const QStringList &blockApps) {
0240                 QString errorMessage;
0241                 if (blockApps.isEmpty()) {
0242                     errorMessage = i18n("One or more files on this device are open within an application.");
0243                 } else {
0244                     errorMessage = i18np("One or more files on this device are opened in application \"%2\".",
0245                                          "One or more files on this device are opened in following applications: %2.",
0246                                          blockApps.size(),
0247                                          blockApps.join(i18nc("separator in list of apps blocking device unmount", ", ")));
0248                 }
0249                 notify(error, errorMessage, errorData.toString(), udi, icon);
0250                 disconnect(*c);
0251                 delete c;
0252             });
0253             queryBlockingApps(access->filePath());
0254             return;
0255         }
0256 
0257         break;
0258     }
0259     case Solid::ErrorType::UserCanceled:
0260         // don't point out the obvious to the user, do nothing here
0261         break;
0262     default:
0263         switch (type) {
0264         case SolidReplyType::Setup:
0265             errorMsg = i18n("Could not mount this device.");
0266             break;
0267         case SolidReplyType::Teardown:
0268             errorMsg = i18nc("Remove is less technical for unmount", "Could not remove this device.");
0269             break;
0270         case SolidReplyType::Eject:
0271             errorMsg = i18n("Could not eject this disc.");
0272             break;
0273         }
0274 
0275         break;
0276     }
0277 
0278     notify(error, errorMsg, errorData.toString(), udi, icon);
0279 }
0280 
0281 void KSolidNotify::notify(Solid::ErrorType error, const QString &errorMessage, const QString &errorData, const QString &udi, const QString &icon)
0282 {
0283     if (m_lastUdi != udi) {
0284         m_lastUdi = udi;
0285         Q_EMIT lastUdiChanged();
0286     }
0287 
0288     if (m_lastErrorType != error) {
0289         m_lastErrorType = error;
0290         Q_EMIT lastErrorTypeChanged();
0291     }
0292 
0293     if (errorMessage != m_lastMessage) {
0294         m_lastMessage = errorMessage;
0295         Q_EMIT lastMessageChanged();
0296     }
0297 
0298     if (m_lastDescription != errorData) {
0299         m_lastDescription = errorData;
0300         Q_EMIT lastDescriptionChanged();
0301     }
0302 
0303     if (m_lastIcon != icon) {
0304         m_lastIcon = icon;
0305         Q_EMIT lastIconChanged();
0306     }
0307 }
0308 
0309 #include "moc_ksolidnotify.cpp"