Empty-tray optical drives couldn't be opened because the macOS backend
resolved drives by media-child `BSD Name`, which doesn't exist without a
disc loaded. Result: `list_drives()` returned nothing for a drive with no
media, and any attempt to `open("disc://")` on that state failed.
Fix: enumerate `IOBDServices` directly. With media present, the returned
selector is the familiar `/dev/diskN` path (unchanged). With no media,
the selector is an opaque `ioreg:<registry-id>` string that resolves to
the same IOBDServices entry — Disk Arbitration and unmount are skipped
because there's no media node.
- `src/drive/macos.rs`, `src/drive/mod.rs`, `src/scsi/mod.rs`: threads
the selector through the transport so empty-tray drives are openable
without punning it as a filesystem path.
- `src/scsi/macos.rs`, `src/scsi/macos_shim.c`: the C shim now accepts
both `/dev/diskN` and `ioreg:<id>` in `shim_open_exclusive`, and
`shim_list_drives` reports the correct selector for each drive.
- `tests/macos_drive_tests.rs` (new): covers empty-tray discovery.
- `docs/drive-access.md`, `docs/scsi-macos.md`: document the two-path
selector model.
[skip-media]
4.0 KiB
macOS SCSI transport (src/scsi/macos.rs)
Open sequence (C shim, macos_shim.c)
MacScsiTransport::open drives all CDBs (INQUIRY, READ, REPORT KEY, etc.)
through SCSITaskDeviceInterface::ExecuteTaskSync — 1:1 with the Linux
SG_IO backend. Getting there involves the shim doing:
diskutil unmountDisk forceon the target device only- Find
IOBDServicesmatching the requested BSD name (walks IOKit registry: IOBDServices -> IOBDBlockStorageDriver -> IOMedia -> BSD Name) - Create
MMCDeviceInterface->SCSITaskDeviceInterface ObtainExclusiveAccess- Raw CDB dispatch via
CreateSCSITask+ExecuteTaskSync
Single-instance ownership (OPEN flag)
The C shim uses one global IOKit handle (g_handle), so only one
MacScsiTransport may exist at a time: a second open() would share
that handle, and the first drop() would tear it down out from under
the other. The OPEN AtomicBool enforces single-instance ownership,
independent of the shim's own state.
map_shim_open_error
Maps a shim_open_exclusive failure sentinel (a negative return code,
NOT an IOReturn) to its typed Error variant. Pulled out of open
as its own callable predicate so the mapping — otherwise reachable only
through a real IOKit FFI call — can be pinned by a test: collapsing
-4..=-2 or -5 into the DeviceNotFound catch-all would silently
turn "another process holds the drive" into "no such drive", or "the
IOKit plugin chain failed" into the same.
Drive enumeration and the media-presence probe
list_drives uses the IOKit registry directly via shim_list_drives —
no exclusive access, no SCSI commands, no unmounts. It enumerates
IOBDServices even when no IOMedia child exists. With media, the
returned path remains /dev/diskN; with an empty tray it uses the opaque
ioreg:<registry-id> selector so the service can still be opened directly.
That selector is an internal list-then-open handle, not a filesystem path.
drive_has_disc is documented (crate::scsi::drive_has_disc) as the
cheap, side-effect-free "is there a disc?" question, suitable for a
poll-loop tick. The Linux backend honours that: open(O_RDWR|O_NONBLOCK)
- one TEST UNIT READY, no exclusive access, no unmount. The Windows backend likewise opens a shared handle and issues one TUR.
macOS could not do the same: MacScsiTransport::open is the FULL
exclusive-transport path above, whose first act is diskutil unmountDisk force on the target device. So a probe implemented via open would
force-unmount the user's disc — and on every poll tick, taking (and
dropping) exclusive access each time.
Instead drive_has_disc answers via shim_media_present, the IOKit
registry only: steps 1-5 above are the transport open path, and this
probe must not run any of them. For a BSD disk path, the registry looks
for its matching IOMedia; for an ioreg:<id> selector, it resolves the
drive service and checks whether that service currently has media.
No SCSI command is issued, which is why no timeout parameter is
involved.
Trade-off, stated plainly: this reports what the OS has enumerated,
so a disc that is inserted but still spinning up (no IOMedia published
yet) reads as absent for the moment the enumeration takes — the same
window in which a TUR would answer "not ready" and this function's
contract already maps to Ok(false).
Regression test: presence_probe_does_not_open_a_transport
drive_has_disc used to be implemented by constructing a FULL exclusive
transport, whose first act is diskutil unmountDisk force on the target
device followed by an unconditional usleep(500000) — so the probe
force-unmounted the user's disc, on every poll tick.
Two observables separate the registry probe from the transport open, neither of which needs an optical drive to be attached:
- It ANSWERS. The transport path returned
Err(DeviceNotFound)here; the registry path reports "no media" asOk(false). - It is FAST. The transport path's
usleep(500000)after the spawn is unconditional, so it could not complete inside this budget even when the spawn itself failed.