/* Copyright (C) 2010 by Jacopo De Simoi Copyright (C) 2014 by Lukáš Tinkl This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ksolidnotify.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include KSolidNotify::KSolidNotify(QObject* parent): QObject(parent) { Solid::Predicate p(Solid::DeviceInterface::StorageAccess); p |= Solid::Predicate(Solid::DeviceInterface::OpticalDrive); p |= Solid::Predicate(Solid::DeviceInterface::PortableMediaPlayer); QList devices = Solid::Device::listFromQuery(p); foreach (const Solid::Device &dev, devices) { m_devices.insert(dev.udi(), dev); connectSignals(&m_devices[dev.udi()]); } connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &KSolidNotify::onDeviceAdded); connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &KSolidNotify::onDeviceRemoved); } void KSolidNotify::onDeviceAdded(const QString &udi) { Solid::Device device(udi); m_devices.insert(udi, device); connectSignals(&m_devices[udi]); } void KSolidNotify::onDeviceRemoved(const QString &udi) { if (m_devices[udi].is()) { Solid::StorageAccess *access = m_devices[udi].as(); if (access) disconnect(access, 0, this, 0); } m_devices.remove(udi); } bool KSolidNotify::isSafelyRemovable(const QString &udi) { Solid::Device parent = m_devices[udi].parent(); if (parent.is()) { Solid::StorageDrive *drive = parent.as(); return (!drive->isInUse() && (drive->isHotpluggable() || drive->isRemovable())); } Solid::StorageAccess* access = m_devices[udi].as(); if (access) { return !m_devices[udi].as()->isAccessible(); } else { // If this check fails, the device has been already physically // ejected, so no need to say that it is safe to remove it return false; } } void KSolidNotify::connectSignals(Solid::Device* device) { Solid::StorageAccess *access = device->as(); if (access) { connect(access, &Solid::StorageAccess::teardownDone, this, &KSolidNotify::storageTeardownDone); connect(access, &Solid::StorageAccess::setupDone, this, &KSolidNotify::storageSetupDone); } if (device->is()) { Solid::OpticalDrive *drive = device->parent().as(); connect(drive, &Solid::OpticalDrive::ejectDone, this, &KSolidNotify::storageEjectDone); } } void KSolidNotify::storageSetupDone(Solid::ErrorType error, QVariant errorData, const QString &udi) { if (error) { Solid::Device device(udi); const QString errorMessage = i18n("Could not mount the following device: %1", device.description()); emit notify(error, errorMessage, errorData.toString(), udi); } } void KSolidNotify::queryBlockingApps(const QString &devicePath) { QProcess *p = new QProcess; connect(p, static_cast(&QProcess::error), [=](QProcess::ProcessError) { emit blockingAppsReady({}); p->deleteLater(); }); connect(p, static_cast(&QProcess::finished), [=](int,QProcess::ExitStatus) { QStringList blockApps; QString out(p->readAll()); const QStringList &pidList = out.split(QRegExp(QStringLiteral("\\s+")), QString::SkipEmptyParts); KSysGuard::Processes procs; Q_FOREACH (const QString &pidStr, pidList) { int pid = pidStr.toInt(); if (!pid) { continue; } procs.updateOrAddProcess(pid); KSysGuard::Process *proc = procs.getProcess(pid); if (!blockApps.contains(proc->name())) { blockApps << proc->name(); } } blockApps.removeDuplicates(); emit blockingAppsReady(blockApps); p->deleteLater(); }); p->start(QStringLiteral("lsof"), {QStringLiteral("-t"), devicePath}); // p.start(QStringLiteral("fuser"), {QStringLiteral("-m"), devicePath}); } void KSolidNotify::storageTeardownDone(Solid::ErrorType error, QVariant errorData, const QString &udi) { if (error) { Solid::Device device(udi); Solid::StorageAccess *access = device.as(); // Without that, our lambda function would capture an uninitialized object, resulting in UB // and random crashes QMetaObject::Connection *c = new QMetaObject::Connection(); *c = connect(this, &KSolidNotify::blockingAppsReady, [=] (const QStringList &blockApps) { QString errorMessage; if (blockApps.isEmpty()) { errorMessage = i18n("Could not safely remove the following device: %1\nOne or more files on this device are open within an application", device.description()); } else { errorMessage = i18np("Could not safely remove the following device: %1\nOne or more files on this device are opened in application \"%3\"\nPlease close it and try again", "Could not safely remove the following device: %1\nOne or more files on this device are opened in following applications: %3\nPlease close it and try again", device.description(), blockApps.size(), blockApps.join(i18nc("separator in list of apps blocking device unmount", ", "))); } emit notify(error, errorMessage, errorData.toString(), udi); disconnect(*c); delete c; }); queryBlockingApps(access->filePath()); } else if (isSafelyRemovable(udi)) { Solid::Device device(udi); const QString errorMessage = i18nc("The term \"remove\" here means \"physically disconnect the device from the computer\", whereas \"safely\" means \"without risk of data loss\"", "The following device can now be safely removed: %1", device.description()); emit notify(error, errorMessage, errorData.toString(), udi); } } void KSolidNotify::storageEjectDone(Solid::ErrorType error, QVariant errorData, const QString &udi) { if (error) { QString discUdi; foreach (Solid::Device device, m_devices) { if (device.parentUdi() == udi) { discUdi = device.udi(); } } if (discUdi.isNull()) { //This should not happen, bail out return; } Solid::Device discDevice(discUdi); Solid::StorageAccess *access = discDevice.as(); // Without that, our lambda function would capture an uninitialized object, resulting in UB // and random crashes QMetaObject::Connection *c = new QMetaObject::Connection(); *c = connect(this, &KSolidNotify::blockingAppsReady, [=] (const QStringList &blockApps) { QString errorMessage; if (blockApps.isEmpty()) { errorMessage = i18n("Could not eject the following device: %1\nOne or more files on this device are open within an application", discDevice.description()); } else { errorMessage = i18np("Could not eject the following device: %1\nOne or more files on this device are opened in application \"%3\"\nPlease close it and try again", "Could not eject the following device: %1\nOne or more files on this device are opened in following applications: %3\nPlease close it and try again", discDevice.description(), blockApps.size(), blockApps.join(i18nc("separator in list of apps blocking device unmount", ", "))); } emit notify(error, errorMessage, errorData.toString(), udi); disconnect(*c); delete c; }); queryBlockingApps(access->filePath()); } else if (isSafelyRemovable(udi)) { Solid::Device device(udi); const QString errorMessage = i18n("The following device can now be safely removed: %1", device.description()); emit notify(error, errorMessage, errorData.toString(), udi); } }