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 }