1 /*
2     SPDX-FileCopyrightText: 2017 René J.V. Bertin <rjvbertin@gmail.com>
3 
4     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5 */
6 
7 #include "iokitopticaldrive.h"
8 
9 #include <QDebug>
10 #include <QProcess>
11 
12 #ifdef EJECT_USING_DISKARBITRATION
13 // for QCFType:
14 #include <private/qcore_mac_p.h>
15 #else
16 #include <QStandardPaths>
17 #endif
18 
19 #include <CoreFoundation/CoreFoundation.h>
20 #include <DiskArbitration/DiskArbitration.h>
21 #include <IOKit/scsi/IOSCSIMultimediaCommandsDevice.h>
22 
23 using namespace Solid::Backends::IOKit;
24 
25 class IOKitOpticalDrive::Private
26 {
27 public:
Private(const IOKitDevice * device,const QVariantMap & devCharMap)28     Private(const IOKitDevice *device, const QVariantMap &devCharMap)
29         : m_device(device)
30         , m_deviceCharacteristics(devCharMap)
31     {
32     }
~Private()33     virtual ~Private()
34     {
35     }
36 
property(const QString & key) const37     QVariant property(const QString &key) const
38     {
39         return m_deviceCharacteristics.value(key);
40     }
41 
42     const IOKitDevice *m_device;
43     const QVariantMap m_deviceCharacteristics;
44 
45     static const QMap<Solid::OpticalDrive::MediumType, uint32_t> cdTypeMap;
46     static const QMap<Solid::OpticalDrive::MediumType, uint32_t> dvdTypeMap;
47     static const QMap<Solid::OpticalDrive::MediumType, uint32_t> bdTypeMap;
48 
49 #ifdef EJECT_USING_DISKARBITRATION
50     // DiskArbitration-based ejection based on the implementation in libcdio's osx.c
51     // in turn based on VideoLAN (VLC) code.
52     // Not activated by default ATM because I have only been able to test it with the
53     // solid-hardware5 utility and that one remains stuck after a successful return
54     // from IOKitOpticalDrive::eject(). It does so too when using the hdiutil external
55     // utility which cannot be due to using QProcess (to the best of my knowledge).
56     // NB: the full-fledged approach using a cancel sourc ref (cancel_signal) etc. may
57     // well be too complicated.
58 
59     typedef struct DAContext {
60         const IOKitDevice *device;
61         int success;
62         bool completed;
63         DASessionRef session;
64         CFRunLoopRef runloop;
65         CFRunLoopSourceRef cancel_signal;
66     } DAContext;
67 
cancelEjectRunloop(void *)68     static void cancelEjectRunloop(void *){};
69 
daEjectCallback(DADiskRef disk,DADissenterRef dissenter,void * context)70     static void daEjectCallback(DADiskRef disk, DADissenterRef dissenter, void *context)
71     {
72         Q_UNUSED(disk);
73         DAContext *daContext = static_cast<DAContext *>(context);
74 
75         if (dissenter) {
76             CFStringRef status = DADissenterGetStatusString(dissenter);
77             if (status) {
78                 qWarning() << "Warning while ejecting" << daContext->device->property("BSD Name").toString() << ":" << QString::fromCFString(status);
79                 CFRelease(status);
80             }
81         }
82 
83         daContext->success = dissenter ? false : true;
84         daContext->completed = TRUE;
85         CFRunLoopSourceSignal(daContext->cancel_signal);
86         CFRunLoopWakeUp(daContext->runloop);
87     }
88 
daUnmountCallback(DADiskRef disk,DADissenterRef dissenter,void * context)89     static void daUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void *context)
90     {
91         DAContext *daContext = (DAContext *)context;
92 
93         if (!dissenter) {
94             DADiskEject(disk, kDADiskEjectOptionDefault, daEjectCallback, context);
95             daContext->success = (daContext->success == -1 ? true : daContext->success);
96         } else {
97             daContext->success = false;
98             daContext->completed = true;
99             CFRunLoopSourceSignal(daContext->cancel_signal);
100             CFRunLoopWakeUp(daContext->runloop);
101         }
102     }
103 
eject(double timeoutSeconds)104     bool eject(double timeoutSeconds)
105     {
106         CFDictionaryRef description = nullptr;
107         CFRunLoopSourceContext cancelRunLoopSourceContext = {.perform = cancelEjectRunloop};
108         DAContext daContext = {m_device, -1, false, 0, CFRunLoopGetCurrent(), 0};
109         QCFType<CFRunLoopSourceRef> cancel = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &cancelRunLoopSourceContext);
110         if (!(daContext.cancel_signal = cancel)) {
111             qWarning() << Q_FUNC_INFO << "failed to create cancel runloop source";
112             return false;
113         }
114         QCFType<DASessionRef> session = DASessionCreate(kCFAllocatorDefault);
115         if (!(daContext.session = session)) {
116             qWarning() << Q_FUNC_INFO << "failed to create DiskArbitration session";
117             return false;
118         }
119         const QString devName = m_device->property(QStringLiteral("BSD Name")).toString();
120         QCFType<DADiskRef> daRef = DADiskCreateFromBSDName(kCFAllocatorDefault, daContext.session, devName.toStdString().c_str());
121         if (!daRef) {
122             qWarning() << Q_FUNC_INFO << "failed to create DiskArbitration reference for" << devName;
123             return false;
124         }
125         description = DADiskCopyDescription(daRef);
126         if (description) {
127             DASessionScheduleWithRunLoop(daContext.session, daContext.runloop, kCFRunLoopDefaultMode);
128             CFRunLoopAddSource(daContext.runloop, daContext.cancel_signal, kCFRunLoopDefaultMode);
129             if (CFDictionaryGetValueIfPresent(description, kDADiskDescriptionVolumePathKey, nullptr)) {
130                 DADiskUnmount(daRef, kDADiskUnmountOptionWhole, daUnmountCallback, &daContext);
131             }
132             DADiskEject(daRef, kDADiskEjectOptionDefault, daEjectCallback, &daContext);
133             daContext.success = (daContext.success == -1 ? true : daContext.success);
134             while (!daContext.completed) {
135                 if (CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeoutSeconds, true) == kCFRunLoopRunTimedOut) {
136                     break;
137                 }
138             }
139             if (daContext.completed) {
140                 qWarning() << Q_FUNC_INFO << "ejected" << devName;
141             } else {
142                 qWarning() << Q_FUNC_INFO << "timeout ejecting" << devName;
143             }
144             CFRunLoopRemoveSource(daContext.runloop, daContext.cancel_signal, kCFRunLoopDefaultMode);
145             DASessionSetDispatchQueue(daContext.session, 0);
146             DASessionUnscheduleFromRunLoop(daContext.session, daContext.runloop, kCFRunLoopDefaultMode);
147             CFRelease(description);
148         } else {
149             qWarning() << Q_FUNC_INFO << "failed to fetch DiskArbitration description for" << devName;
150         }
151         return daContext.success == -1 ? false : daContext.success;
152     }
153 #endif // EJECT_USING_DISKARBITRATION
154 };
155 
156 const QMap<Solid::OpticalDrive::MediumType, uint32_t> IOKitOpticalDrive::Private::cdTypeMap = {
157     {Solid::OpticalDrive::Cdr, kCDFeaturesWriteOnceMask},
158     {Solid::OpticalDrive::Cdrw, kCDFeaturesReWriteableMask},
159 };
160 const QMap<Solid::OpticalDrive::MediumType, uint32_t> IOKitOpticalDrive::Private::dvdTypeMap = {
161     {Solid::OpticalDrive::Dvd, kDVDFeaturesReadStructuresMask},
162     {Solid::OpticalDrive::Dvdr, kDVDFeaturesWriteOnceMask},
163     {Solid::OpticalDrive::Dvdrw, kDVDFeaturesReWriteableMask},
164     {Solid::OpticalDrive::Dvdram, kDVDFeaturesRandomWriteableMask},
165     {Solid::OpticalDrive::Dvdplusr, kDVDFeaturesPlusRMask},
166     {Solid::OpticalDrive::Dvdplusrw, kDVDFeaturesPlusRWMask},
167     // not supported:
168     //         {Solid::OpticalDrive::Dvdplusdl, "dvdplusrdl"}
169     //         {Solid::OpticalDrive::Dvdplusdlrw, "dvdplusrwdl"}
170     {Solid::OpticalDrive::HdDvd, kDVDFeaturesHDReadMask},
171     {Solid::OpticalDrive::HdDvdr, kDVDFeaturesHDRMask},
172     {Solid::OpticalDrive::HdDvdrw, kDVDFeaturesHDRWMask},
173 };
174 const QMap<Solid::OpticalDrive::MediumType, uint32_t> IOKitOpticalDrive::Private::bdTypeMap = {
175     {Solid::OpticalDrive::Bd, kBDFeaturesReadMask},
176     {Solid::OpticalDrive::Bdr, kBDFeaturesWriteMask},
177 }; // also Solid::OpticalDrive::Bdre
178 
IOKitOpticalDrive(IOKitDevice * device)179 IOKitOpticalDrive::IOKitOpticalDrive(IOKitDevice *device)
180     : IOKitStorage(device)
181 {
182     // walk up the IOKit chain to find the parent that has the "Device Characteristics" property
183     // In the examples I've seen this is always the 2nd parent but if ever that turns out
184     // to be non-guaranteed we'll need to do a true walk.
185     IOKitDevice ioDVDServices(IOKitDevice(device->parentUdi()).parentUdi());
186     QVariantMap devCharMap;
187     if (!ioDVDServices.iOKitPropertyExists(QStringLiteral("Device Characteristics"))) {
188         qWarning() << Q_FUNC_INFO << "Grandparent of" << m_device->udi() << "doesn't have the \"Device Characteristics\" but is" << ioDVDServices.udi();
189     } else {
190         const QVariant devCharVar = ioDVDServices.property(QStringLiteral("Device Characteristics"));
191         devCharMap = devCharVar.toMap();
192     }
193     d = new Private(device, devCharMap);
194 }
195 
~IOKitOpticalDrive()196 IOKitOpticalDrive::~IOKitOpticalDrive()
197 {
198 }
199 
200 /* clang-format off */
201 // // Example properties: QMap(("BSD Major", QVariant(int, 1))
202 //     ("BSD Minor", QVariant(int, 12))
203 //     ("BSD Name", QVariant(QString, "disk3"))
204 //     ("BSD Unit", QVariant(int, 3))
205 //     ("Content", QVariant(QString, "CD_partition_scheme"))
206 //     ("Content Hint", QVariant(QString, ""))
207 //     ("Ejectable", QVariant(bool, true))
208 //     ("IOBusyInterest", QVariant(QString, "IOCommand is not serializable"))
209 //     ("IOGeneralInterest", QVariant(QString, "IOCommand is not serializable"))
210 //     ("IOMediaIcon", QVariant(QVariantMap, QMap(("CFBundleIdentifier", QVariant(QString, "com.apple.iokit.IOCDStorageFamily"))
211 //         ("IOBundleResourceFile", QVariant(QString, "CD.icns")))))
212 //     ("Leaf", QVariant(bool, false))
213 //     ("Open", QVariant(bool, true))
214 //     ("Preferred Block Size", QVariant(qlonglong, 2352))
215 //     ("Removable", QVariant(bool, true))
216 //     ("Size", QVariant(qlonglong, 750932448))
217 //     ("TOC", QVariant(QByteArray, "\x00\xA7\x01\x01\x01\x10\x00\xA0\x00\x00\x00\x00\x01\x00\x00\x01\x12\x00\xA1\x00\x00\x00\x00\f\x00\x00\x01\x12\x00\xA2\x00\x00\x00\x00""F:J\x01\x12\x00\x01\x00\x00\x00\x00\x00\x02\x00\x01\x12\x00\x02\x00\x00\x00\x00\x07/\b\x01\x12\x00\x03\x00\x00\x00\x00\x12\b\x0E\x01\x12\x00\x04\x00\x00\x00\x00\x17\x12""0\x01\x12\x00\x05\x00\x00\x00\x00\x1B+ \x01\x12\x00\x06\x00\x00\x00\x00 \x11\n\x01\x12\x00\x07\x00\x00\x00\x00!-\n\x01\x12\x00\b\x00\x00\x00\x00'\f\x1F\x01\x12\x00\t\x00\x00\x00\x00-\x13;\x01\x12\x00\n\x00\x00\x00\x00""4%\x1E\x01\x12\x00\x0B\x00\x00\x00\x00""62 \x01\x12\x00\f\x00\x00\x00\x00""C\x06""E"))
218 //     ("Type", QVariant(QString, "CD-ROM"))
219 //     ("Whole", QVariant(bool, true))
220 //     ("Writable", QVariant(bool, false))
221 //     ("className", QVariant(QString, "IOCDMedia")))
222 // // related useful entry: QMap(("Device Characteristics", QVariant(QVariantMap, QMap(("Async Notification", QVariant(bool, false))
223 //         ("BD Features", QVariant(int, 0))
224 //         ("CD Features", QVariant(int, 2047))
225 //         ("DVD Features", QVariant(int, 503))
226 //         ("Fast Spindown", QVariant(bool, true))
227 //         ("Loading Mechanism", QVariant(QString, "Slot"))
228 //         ("Low Power Polling", QVariant(bool, false))
229 //         ("Power Off", QVariant(bool, true))
230 //         ("Product Name", QVariant(QString, "DVD-R   UJ-8A8"))
231 //         ("Product Revision Level", QVariant(QString, "HA13"))
232 //         ("Vendor Name", QVariant(QString, "MATSHITA")))))
233 //     ("IOCFPlugInTypes", QVariant(QVariantMap, QMap(("97ABCF2C-23CC-11D5-A0E8-003065704866", QVariant(QString,     "IOSCSIArchitectureModelFamily.kext/Contents/PlugIns/SCSITaskUserClient.kext/Contents/PlugIns/SCSITaskLib.plugin")))))
234 //     ("IOGeneralInterest", QVariant(QString, "IOCommand is not serializable"))
235 //     ("IOMatchCategory", QVariant(QString, "SCSITaskUserClientIniter"))
236 //     ("IOMinimumSegmentAlignmentByteCount", QVariant(qlonglong, 4))
237 //     ("IOUserClientClass", QVariant(QString, "SCSITaskUserClient"))
238 //     ("Protocol Characteristics", QVariant(QVariantMap, QMap(("AHCI Port Number", QVariant(qlonglong, 0))
239 //         ("ATAPI", QVariant(bool, true))
240 //         ("Physical Interconnect", QVariant(QString, "SATA"))
241 //         ("Physical Interconnect Location", QVariant(QString, "Internal"))
242 //         ("Port Speed", QVariant(QString, "1.5 Gigabit"))
243 //         ("Read Time Out Duration", QVariant(qlonglong, 15000))
244 //         ("Retry Count", QVariant(qlonglong, 1))
245 //         ("Write Time Out Duration", QVariant(qlonglong, 15000)))))
246 //     ("SCSITaskDeviceCategory", QVariant(QString, "SCSITaskAuthoringDevice"))
247 //     ("SCSITaskUserClient GUID", QVariant(QByteArray, "\x00]\x0F""F\x80\xFF\xFF\xFFg\xB6\xAB\x1B\x00\x00\x00\x00"))
248 //     ("className", QVariant(QString, "IODVDServices"))
249 //     ("device-type", QVariant(QString, "DVD")))
250 // //                       QMap(("CFBundleIdentifier", QVariant(QString, "com.apple.iokit.IODVDStorageFamily"))
251 //     ("IOClass", QVariant(QString, "IODVDBlockStorageDriver"))
252 //     ("IOGeneralInterest", QVariant(QString, "IOCommand is not serializable"))
253 //     ("IOMatchCategory", QVariant(QString, "IODefaultMatchCategory"))
254 //     ("IOProbeScore", QVariant(int, 0))
255 //     ("IOPropertyMatch", QVariant(QVariantMap, QMap(("device-type", QVariant(QString, "DVD")))))
256 //     ("IOProviderClass", QVariant(QString, "IODVDBlockStorageDevice"))
257 //     ("Statistics", QVariant(QVariantMap, QMap(("Bytes (Read)", QVariant(qlonglong, 578020608))
258 //         ("Bytes (Write)", QVariant(qlonglong, 0))
259 //         ("Errors (Read)", QVariant(qlonglong, 0))
260 //         ("Errors (Write)", QVariant(qlonglong, 0))
261 //         ("Latency Time (Read)", QVariant(qlonglong, 0))
262 //         ("Latency Time (Write)", QVariant(qlonglong, 0))
263 //         ("Operations (Read)", QVariant(qlonglong, 18475))
264 //         ("Operations (Write)", QVariant(qlonglong, 0))
265 //         ("Retries (Read)", QVariant(qlonglong, 0))
266 //         ("Retries (Write)", QVariant(qlonglong, 0))
267 //         ("Total Time (Read)", QVariant(qlonglong, 219944025102))
268 //         ("Total Time (Write)", QVariant(qlonglong, 0)))))
269 //     ("className", QVariant(QString, "IODVDBlockStorageDriver")))
270 /* clang-format on */
271 
supportedMedia() const272 Solid::OpticalDrive::MediumTypes IOKitOpticalDrive::supportedMedia() const
273 {
274     Solid::OpticalDrive::MediumTypes supported;
275 
276     uint32_t cdFeatures = d->property(QStringLiteral("CD Features")).toInt();
277     uint32_t dvdFeatures = d->property(QStringLiteral("DVD Features")).toInt();
278     uint32_t bdFeatures = d->property(QStringLiteral("BD Features")).toInt();
279 
280     qDebug() << Q_FUNC_INFO << "cdFeatures" << cdFeatures << "dvdFeatures" << dvdFeatures << "bdFeatures" << bdFeatures;
281 
282     for (auto it = d->cdTypeMap.cbegin(); it != d->cdTypeMap.cend(); ++it) {
283         if (cdFeatures & it.value()) {
284             supported |= it.key();
285         }
286     }
287     for (auto it = d->dvdTypeMap.cbegin(); it != d->dvdTypeMap.cend(); ++it) {
288         if (dvdFeatures & it.value()) {
289             supported |= it.key();
290         }
291     }
292     for (auto it = d->bdTypeMap.cbegin(); it != d->bdTypeMap.cend(); ++it) {
293         const uint32_t value = it.value();
294         if (bdFeatures & value) {
295             supported |= it.key();
296             if (value == kBDFeaturesWriteMask) {
297                 supported |= Solid::OpticalDrive::Bdre;
298             }
299         }
300     }
301 
302     return supported;
303 }
304 
readSpeed() const305 int IOKitOpticalDrive::readSpeed() const
306 {
307     return 0;
308 }
309 
writeSpeed() const310 int IOKitOpticalDrive::writeSpeed() const
311 {
312     return 0;
313 }
314 
writeSpeeds() const315 QList<int> IOKitOpticalDrive::writeSpeeds() const
316 {
317     return {};
318 }
319 
eject()320 bool IOKitOpticalDrive::eject()
321 {
322 #ifdef EJECT_USING_DISKARBITRATION
323     // give the devices 30 seconds to eject
324     int error = !d->eject(30.0);
325 #else
326     QProcess ejectJob;
327     int error = ejectJob.execute(
328         QStandardPaths::findExecutable(QStringLiteral("hdiutil")), //
329         {QStringLiteral("detach"), QStringLiteral("-verbose"), QStringLiteral("/dev/") + m_device->property(QStringLiteral("BSD Name")).toString()});
330     if (error) {
331         qWarning() << "hdiutil returned" << error << "trying to eject" << m_device->product();
332     }
333 #endif // EJECT_USING_DISKARBITRATION
334     if (error) {
335         Q_EMIT ejectDone(Solid::ErrorType::OperationFailed, QVariant(), m_device->udi());
336         return false;
337     } else {
338         Q_EMIT ejectDone(Solid::ErrorType::NoError, QVariant(), m_device->udi());
339         return true;
340     }
341 }
342