What is the best way to get BSD drive names on macOS (Swift)? - swift

What's the best way I can get a list of BSD names of all USB devices (and maybe including internal Mac drives) without using a diskutil CLI wrapper?
I don't want to use any wrappers that interact with the CLI interface, as this way of interacting is quite slow and unreliable:
This is an example of why I'm not happy with using CLI wrappers
(Compare 'Time elapsed for DiskUtil CLI Wrapper.' and 'Time elapsed for Disk Arbitration')
What is the best way to implement the solution for my problem?
Use the data from IOReg?
If yes, how can I get a list of BSD names of connected devices using it?
Here is an example what I want to get:
["disk0", "disk0s1", "disk0s2", "disk0s3", "disk1", "disk1s1", "disk1s2", "disk1s3", "disk1s4", "disk2", "disk2s1", "disk2s2", "disk3", "disk3s1", "disk3s1s1", "disk3s2", "disk3s3", "disk3s4", "disk3s5", "disk3s6", "disk4", "disk4s1", "disk4s2", "disk5", "disk5s1", "disk5s2", "disk6", "disk6s1", "disk6s2", "disk10", "disk10s1", "disk10s2", "disk11", "disk11s1"]
At the moment, I have the following:
static func getMountedBSDNames() -> [String] {
guard let session = DASessionCreate(nil) else { return [] }
guard let mountedVolumeURLs = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: nil) else { return [] }
var BSDNames: [String] = []
for volumeURL in mountedVolumeURLs {
if let disk = DADiskCreateFromVolumePath(kCFAllocatorDefault, session, volumeURL as CFURL), let BSDName = DADiskGetBSDName(disk) {
BSDNames.append(
String(cString: BSDName)
)
}
}
return BSDNames
}
But in this case, only mounted are returning.
I want there to have even those, that were ejected

I achieved the desired result using the IOReg lookup method:
Elapsed Time
func getDriveBSDNames() -> [String] {
var iterator: io_iterator_t = 0
let matching: CFDictionary = IOServiceMatching(kIOServicePlane)
// Use 'kIOMasterPortDefault' for macOS older than 12.0 Monterey
IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator)
var child: io_object_t = IOIteratorNext(iterator)
var BSDNames: [String] = []
while child > 0 {
if let BSDNameAnyObject = IORegistryEntryCreateCFProperty(child, "BSD Name" as CFString, kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively)) {
if let BSDNameString = (BSDNameAnyObject.takeRetainedValue() as? String), BSDNameString.starts(with: "disk") {
BSDNames.append(
BSDNameString
)
}
}
child = IOIteratorNext(iterator)
}
return BSDNames
}
In this case, it was also necessary to filter the output of the results using:
BSDNameString.starts(with: "disk")
(otherwise, some unnecessary devices were added, such as en0, anpi0, llw0, etc.)

Note that while the Disk Arbitration framework doesn't have a function for synchronously enumerating all disks, it does effectively support asynchronous enumeration by registering a callback for disk appearance. This may or may not be useful depending on your use case - when providing the user with an interactive list of devices, this is usually exactly what you want though, as you'll automatically be notified of newly added devices.
I don't do Swift, sorry, but the following C code should be easy enough to understand to come up with something similar in other languages.
#include <DiskArbitration/DiskArbitration.h>
#include <stdio.h>
static void disk_appeared(DADiskRef disk, void* context)
{
printf("%s\n", DADiskGetBSDName(disk) ?: "(null)");
}
int main()
{
DASessionRef session = DASessionCreate(kCFAllocatorDefault);
DASessionSetDispatchQueue(session, dispatch_get_main_queue());
DARegisterDiskAppearedCallback(session, NULL, disk_appeared, NULL /*context*/);
dispatch_main();
}
Note that the callback will also be called for APFS snapshots, which don't have a BSD name, so DADiskGetBSDName returns NULL and you'll have to do a little bit of filtering.

Related

Get the battery voltage of Logitech Lightspeed device with IOKit?

I'd like to write a simple tool in Swift to read battery status of my mouse (Logitech G Pro Wireless). Since it's my first dive into both IOKit and managing HID devices, I am struggling with getting it done.
Here's my current approach:
I used this library to skip for now messing with Obj-C https://github.com/Arti3DPlayer/USBDeviceSwift
On launch of the app I create HID Device with hardcoded ids and start listening for its presence:
struct MyApp: App {
let rfDeviceMonitor = HIDDeviceMonitor([
HIDMonitorData(vendorId: 0x046d, productId: 0xC539)//0xc088)
], reportSize: 64)
var body: some Scene {
Window()
.onAppear {
let rfDeviceDaemon = Thread(target: self.rfDeviceMonitor, selector:#selector(self.rfDeviceMonitor.start), object: nil)
rfDeviceDaemon.start()
}
}
}
Another class is listening for connection and device's data.
func configure() {
NotificationCenter.default.addObserver(self, selector: #selector(self.usbConnected), name: .HIDDeviceConnected, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.hidReadData), name: .HIDDeviceDataReceived, object: nil)
}
#objc func usbConnected(notification: NSNotification) {
guard let nobj = notification.object as? NSDictionary else {
return
}
guard let deviceInfo: HIDDevice = nobj["device"] as? HIDDevice else {
return
}
self.deviceInfo = deviceInfo
write()
}
#objc func hidReadData(notification: Notification) {
let obj = notification.object as! NSDictionary
let data = obj["data"] as! Data
print([UInt8](data))
}
func write() {
let payload: [UInt8] = [
0x10, // short message
0xff, // receiver's index
0x06, // feature index - battery voltage for g pro wireless
0x00,
0x00,
0x00
]
var correctData = Data(payload)
var count = correctData.count
let ret: Int32 = IOHIDDeviceGetReport(
self.deviceInfo.device,
kIOHIDReportTypeFeature,
CFIndex(0x10),
&correctData,
&count
)
print(ret) // 0xe0005000
print([UInt8](correctData)) // [16, 255, 6, 0, 0, 0]
}
The issue is that IOKit always returns a value (0xe0005000) after calling IOHIDDeviceGetReport that is not a success. I have no idea what this means, since kIOReturn header doesn't mention this value at all.
Links that I found useful:
receiver's properties: https://github.com/pwr-Solaar/Solaar/blob/78341f87e969fcdb657d912953f919e7bdd7c491/docs/devices/Lightspeed%20Receiver%20C539.txt
Mouse's properties: https://github.com/pwr-Solaar/Solaar/blob/78341f87e969fcdb657d912953f919e7bdd7c491/docs/devices/G%20Pro%20Wireless%20Gaming%20Mouse%204079.txt
feature's and implementation of reading features via hidpp 2.0 https://github.com/pwr-Solaar/Solaar/blob/eac916b57c78b23a40bcded1a9c89e2cc30e06d4/lib/logitech_receiver/hidpp20.py
Logitech's specification's draft for hidpp 2.0 https://drive.google.com/drive/folders/0BxbRzx7vEV7eWmgwazJ3NUFfQ28?resourcekey=0-dQ-Lx1FORQl0KAdOHQaE1A
The most important informations I got from these sites:
My mouse has only a feature of passing current voltage. It's available on index 6. Also, there's mentioned hex value of 0x1001 next to that feature, but I am not sure what it could mean, maybe it's some Logitech's identifier for this feature.
buffer size has 7 or 20 bytes (this call should be 7B), where:
first means message size (short - 7B - 0x10, long - 20B - 0x11)
second is device index (0xFF is for receiver, yet to find which is meant for device connected via a wire)
Third means feature index (which is 6 in this case)
Fourth is divided on function and software identifier (in this order). The second one people say that is used to recognize responses meant for us from the rest of the output.
rest should be filled by report that I'd like to get.
https://github.com/pwr-Solaar/Solaar/blob/d41c60718876957158f2ef7ce51648cab78c72ad/lib/logitech_receiver/base.py#L437 - here's Python's implementation of sending such a request. This line particularly looks like it sends a request, then it waits for a message back? This is the point where I am struggling the most.
There's a line in documentation that describes bytes meaning, says:
"The device index, feature index, function identifier, and software identifier are always returned unchanged.", so I suppose I should use getReport instead of setReport, but again, my knowledge is very limited here, so I tried setReport as well, but with no luck. Same error has been thrown.

How to properly use CFStringGetCString in swift?

I am looking at the docs for
CFStringGetCString
and AXUIElementCopyAttributeValue.
CFStringGetCString takes the param buffer: UnsafeMutablePointer<Int8>!
AXUIElementCopyAttributeValue takes the param value: UnsafeMutablePointer<CFTypeRef?>
For the latter, I can do a call like this:
var value: CFTypeRef?
let err = AXUIElementCopyAttributeValue(element, attribute as CFString, &value);
This satisfies the doc asking for an UnsafeMutablePointer of type CFTypeRef.
However I can't get the same logic to apply by doing
let buffer: Int8!
CFStringGetCString(attribute as! CFString, &buffer, 2048, CFStringBuiltInEncodings.UTF8.rawValue)
I also tried
let buffer: Int8?
CFStringGetCString(attribute as! CFString, &buffer!, 2048, CFStringBuiltInEncodings.UTF8.rawValue)
Either way, it complains about using buffer before it's initialized, even though it never complained about value in the working method with similar param requirements.
All the working examples I've seen for CFStringGetCString are using objective-c like syntax with *. Not sure what the proper swift way is here.
I also tried this way to get the value I wanted:
let app = AXUIElementCreateSystemWide();
var valueString = "";
var value: CFTypeRef?
// An exception on execution happens here when passing app
// Passing in the element I clicked instead of app
// yields error -25205 (attributeunsupported)
let err = AXUIElementCopyAttributeValue(app, "AXFocusedApplication" as CFString, &value);
if (err == AXError.success) {
valueString = value! as! NSString as String;
} else {
print("ERROR!");
print(err.rawValue);
}
return valueString;
Why are you torturing yourself with CFStringGetCString? If you have a CFString in Swift, you can cast it to a String and get a C string from that:
let cString: [Int8] = (cfString as String).cString(using: .utf8)!
Note also that the value of the kAXFocusedApplicationAttribute is not a CFString. It is an AXUIElement.
Here's my playground test:
import Foundation
import CoreFoundation
let axSystem = AXUIElementCreateSystemWide()
var cfValue: CFTypeRef?
AXUIElementCopyAttributeValue(axSystem, kAXFocusedApplicationAttribute as CFString, &cfValue)
if let cfValue = cfValue, CFGetTypeID(cfValue) == AXUIElementGetTypeID() {
let axFocusedApplication = cfValue
print(axFocusedApplication)
}
The first time I executed this playground, I got a system dialog box telling me that I need to give Xcode permission to control my computer. I went to System Preferences > Security & Privacy > Privacy > Accessibility, found Xcode at the bottom of the list, and turned on its checkbox.
Here's the output of the playground:
<AXUIElement Application 0x7fb2d60001c0> {pid=30253}
I assume you're on macOS since the AXUI API is only available on macOS. If you just want the name of the front application as a string, you can do this:
if let frontAppName = NSWorkspace.shared.frontmostApplication?.localizedName {
print(frontAppName)
}

Reading data from file handle leaks memory on Linux

I am experiencing a memory leak when reading data from files. This code creates the leak:
func read() throws {
let url = URL(fileURLWithPath: "content.pdf")
let fileHandle = try FileHandle(forReadingFrom: url)
while true {
let chunk = fileHandle.readData(ofLength: 256)
guard !chunk.isEmpty else {
break
}
}
print("read")
}
do {
for _ in 0 ..< 10000 {
try read()
}
}
catch {
print("Error: \(error)")
}
*FYI: to run this code you will have to have a "content.pdf" file in your working directory.
If I run this on linux with Swift 3.1.1 (or 3.1), it does a number of iterations of the loop consuming more and more memory until the process is killed.
On Mac this also happens because the data is put into the Autorelease pool and I can fix the memory issue by wrapping each iteration in an autorelease pool but that does not exist on Linux so I don't know how I can free up that memory. Does anyone have an idea?
I found the problem which is within the standard library. There is actually already a bug report open for it. Basically the problem is that the readData(ofLength:) method is returning a Data object that is not cleaning up after itself when deallocated.
For now, I am using this workaround:
extension FileHandle {
public func safelyReadData(ofLength length: Int) -> Data {
#if os(Linux)
var leakingData = self.readData(ofLength: length)
var data: Data = Data()
if leakingData.count > 0 {
leakingData.withUnsafeMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) -> Void in
data = Data(bytesNoCopy: bytes, count: leakingData.count, deallocator: .free)
})
}
return data
#else
return self.readData(ofLength: length)
#endif
}
}
Anywhere I was previously using readData(ofLength:) I am now using my safelyReadData(ofLength:) method. On all platforms other than Linux it simply calls the original because those implementations are fine. On Linux I am creating a copy of the data that will actually free the underlying data when deallocated.
Instead of how to work around the missing autorelease pool, a better question is how to prevent the leak. Maybe creating (and not deallocating) 10,000 FileHandles are the problem. Try this.
func read() throws {
let url = URL(fileURLWithPath: "content.pdf")
let fileHandle = try FileHandle(forReadingFrom: url)
while true {
let chunk = fileHandle.readData(ofLength: 256)
guard !chunk.isEmpty else {
break
}
}
fileHandle.closeFile()
print("read")
}
This may not be the problem, but it is still good code hygiene. How many loops are made before the crash?

MIDIThruConnectionCreate always creates persistent MIDI thru connection?

Xcode 8 beta 2 / Swift 3:
According to Apple's CoreMIDI API documentation, a MIDI thru connection can be established as persistent (stays in place forever, even after your app quits and your system reboots) or non-persistent/transitory (owned by your application and automatically destroys it on app quit).
The trouble I'm running into is that I can't seem to create a non-persistent connection, even though I am following Apple's guidelines.
It comes down to this API:
func MIDIThruConnectionCreate(_ inPersistentOwnerID: CFString?,
_ inConnectionParams: CFData,
_ outConnection: UnsafeMutablePointer<MIDIThruConnectionRef>) -> OSStatus
If you pass null (nil) to inPersistentOwnerID which is a Swift optional, the connection should be created as transitory. However, regardless of whether I pass nil or a String, connections are always created as persistent. (I can verify this by checking CoreMIDI's persistent thru connections.)
A summation of my code:
public class OTMIDIConnectedThru {
var connectionRef = MIDIThruConnectionRef()
init?(sourceEndpoints: [MIDIEndpointRef], destinationEndpoints: [MIDIEndpointRef], persistentOwnerID: String? = nil) {
var params = MIDIThruConnectionParams()
MIDIThruConnectionParamsInitialize(&params) // fill with defaults
// (... snip: code to prepare parameters here ...)
let paramsData = withUnsafePointer(&params) { p in
NSData(bytes: p, length: MIDIThruConnectionParamsSize(&params))
}
result = MIDIThruConnectionCreate(persistentOwnerID, paramsData, &connectionRef)
guard result == noErr else { return nil }
}
}
Any idea what I'm doing wrong? This couldn't possibly be a bug in the API?
I had the same issue, and yes I think it always does create persistent connections. Probably an ID of NULL is the same as an empty string, because MIDIThruConnectionFind with an empty string returns all those persistent connections. So, a bug in the API or the docs!
I would recommend using a real persistentID, and remove all existing/stale connections when you initialize your MIDI stuff:
CFDataRef data;
MIDIThruConnectionFind(CFSTR("com.yourcompany.yourapp"), &data);
unsigned long n = CFDataGetLength(data) / sizeof(MIDIThruConnectionRef);
MIDIThruConnectionRef * con = (MIDIThruConnectionRef*)CFDataGetBytePtr(data);
for(int i=0;i<n;i++) {
MIDIThruConnectionDispose(*con);
con++;
}

RxSwift Using Variables Correctly

I am trying to convert a project to use RxSwift and MVVM. I have a service that syncs a list of data from Parse on every app launch and I basically want to make sure I am taking the correct approach.
What I have done is made a Variable subject and then allow my models to listen to this.
ParseService:
let rx_parseMushrooms = Variable<[ParseMushroom]>([])
MushroomLibraryModel:
_ = parseService.rx_parseMushrooms
.asObservable()
.map { (parseMushrooms:[ParseMushroom]) -> [Mushroom] in
let mushrooms = parseMushrooms.map { (parseMushroom:ParseMushroom) -> Mushroom in
let mushroom = Mapper<Mushroom>().map(parseMushroom.dictionaryWithValuesForKeys(parseMushroom.allKeys()))
return mushroom!
}
return mushrooms
}
.subscribeNext({ (mushrooms:[Mushroom]) -> Void in
self.mushrooms = mushrooms
print(mushrooms)
})
I do the same for expressing the sync state.
ParseService:
struct SyncState {
enum State {
case Unsynced, ConnectingToServer, SyncingInfo, FetchingImageList, SyncingImages, SyncComplete, SyncCompleteWithError
}
var infoToSync = 0
var imagesToSync = 0
var imagesSynced = 0
var state = State.Unsynced
}
let rx_syncState = Variable(SyncState())
I then update the variable a la
self.rx_syncState.value = self.syncState
SyncViewModel:
_ = parseService.rx_syncState
.asObservable()
.subscribeNext { [weak self] (syncState:ParseService.SyncState) -> Void in
switch syncState.state {
//show stuff based on state struct
}
}
Anyways, I would greatly appreciate if someone can tell me if this is a good way of going about it or if I am misusing RxSwift (and guide me on how I should be doing this).
Cheers!
Hmm... Here is an article about using Variable (note that Variable is a wrapper around BehaviorSubject.)
http://davesexton.com/blog/post/To-Use-Subject-Or-Not-To-Use-Subject.aspx
In your case, you already have a cold observable (the network call,) so you don't need a Subject/Variable. All you need to do is publish the observable you already have and use replay(1) to cache the value. I would expect a class named something like ParseServer that contains a computed property named something like mushrooms.
To help get the mushrooms out of parse, you could use this (this will create the cold observable you need):
extension PFQuery {
var rx_findObjects: Observable<[PFObject]> {
return Observable.create { observer in
self.findObjectsInBackgroundWithBlock({ results, error in
if let results = results {
observer.on(.Next(results))
observer.on(.Completed)
}
else {
observer.on(.Error(error ?? RxError.Unknown))
}
})
return AnonymousDisposable({ self.cancel() })
}
}
}
And then you would have something like:
class ParseServer {
var mushrooms: Observable<[Mushroom]> {
return PFQuery(className: "Mushroom").rx_findObjects
.map { $0.map { Mushroom(pfObject: $0) } }
.publish()
.replay(1)
}
}
I think the above is correct. I didn't run it through a compiler though, much less test it. It might need editing.
The idea though is that the first time you call myParseServer.mushrooms the system will call Parse to get the mushrooms out and cache them. From then on, it will just return the previous cashed mushrooms.