IOKitでMACアドレスなどMacの情報を取得

## 概要
- IOKitを使ってMacのMACアドレスや機体のシリアル、バッテリー温度を取得してみる
- また使いやすいようにいくつか一般化をおこなってみた
- コード補足などはコメントを参照

## 参考
- [How to get MAC address from OS X with Swift](https://stackoverflow.com/questions/31835418/how-to-get-mac-address-from-os-x-with-swift)
- [atr000/osxMacAddress\.m](https://gist.github.com/atr000/537946)
- [GetPrimaryMACAddress](https://developer.apple.com/library/archive/samplecode/GetPrimaryMACAddress/Introduction/Intro.html)
- [Swift: macのバッテリー温度を取得する](https://zenn.dev/kyome/articles/7334ac32e213e9fce709)
- Apple公式ドキュメントもあるので、本格的に利用する際はこちらを参照すると良さそう
    - [IOKit](https://developer.apple.com/documentation/iokit)
    - [Introduction to I/O Kit Fundamentals](https://developer.apple.com/library/archive/documentation/DeviceDrivers/Conceptual/IOKitFundamentals/Introduction/Introduction.html#//apple_ref/doc/uid/TP0000011)

## 実行例

```swift
// MARK: - Entry Point

print("### MAC Addresses ###")
let macAddresses = IOServiceManager.getMacAddresses()
for macAddress in macAddresses {
    print("\(macAddress.hexadecimalString) / \(macAddress.isPrivate ? "private" : "public")")
}
print("")

print("### MAC Addresses (Primary Interface) ###")
let macAddressesOfPrimaryInterface = IOServiceManager.getMacAddressesOfPrimaryInterface()
for macAddress in macAddressesOfPrimaryInterface {
    print("\(macAddress.hexadecimalString) / \(macAddress.isPrivate ? "private" : "public")")
}
print("")


print("### Serial Number ###")
if let value = IOServiceManager.getPlatformSerialNumber() {
    print(value)
}
print("")

print("### Battery Temperature ###")
print(IOServiceManager.getTemperature())
print("")
```

- 出力例は以下の通り

```sh
### MAC Addresses ###
XX:XX:XX:XX:XX:48 / private
XX:XX:XX:XX:XX:f0 / public
...
XX:XX:XX:XX:XX:1f / public

### MAC Addresses (Primary Interface) ###
XX:XX:XX:XX:XX:1f / public

### Serial Number ###
XXXXXXXA-XXXX-XXXX-XXXX-XXXXXXXXXXXX

### Battery Temperature ###
30.09
```

## IOKit部分のコード
- IOServiceの取得するのに下記2種類のメソッドがそれぞれ単体・複数別にあるので、メソッドをそれぞれに対して用意している。`IOServiceIterator`と`IOService`の型を用意しているのもそのため
    - [IOServiceGetMatchingService\(\_:\_:\)](https://developer.apple.com/documentation/iokit/1514535-ioservicegetmatchingservice)
    - [IOServiceGetMatchingServices\(\_:\_:\_:\)](https://developer.apple.com/documentation/iokit/1514494-ioservicegetmatchingservices)


```swift
import Foundation

// MARK: - Extension

// [How to read a single character from a string](https://www.hackingwithswift.com/example-code/strings/how-to-read-a-single-character-from-a-string)
extension String {
    subscript(i: Int) -> String {
        return String(self[index(startIndex, offsetBy: i)])
    }
}

// MARK: - MACAddress

struct MACAddress {
    
    let rawValue: [UInt8]
    
    /// 16進数表記のMACアドレス
    /// e.g. bc:d0:74:2e:c2:7f
    var hexadecimalString: String {
        return rawValue
            .map { String(format:"%02x", $0) }
            .joined(separator: ":")
    }
    
    /// プライベートMACアドレスかどうか
    /// [グローバル・プライベート（ローカル）MACアドレスの見分け方](https://qiita.com/IKEH/items/d2c47b9d336a5500bb63)
    var isPrivate: Bool {
        return ["2", "6", "A", "E"].contains(hexadecimalString[1])
    }
}

// MARK: - IOServiceManager

struct IOServiceManager {
}

// MARK: Define Type

private extension IOServiceManager {
    // typealiasだと型の制約がつけられなかったため新しく型を定義する
    struct IOServiceIterator {
        let rawValue: io_iterator_t
    }
    
    struct IOService {
        let rawValue: io_iterator_t
    }
}

// MARK: General Methods

private extension IOServiceManager {
    static func findMatchingServices(
        matchingKey: String,
        propertyMatchDict: Dictionary<String, Any>? = nil
    ) -> IOServiceIterator? {
        // IOEthernetInterfaceにマッチする辞書を作成します。
        guard let matchingDictUM = IOServiceMatching(matchingKey) else {
            return nil
        }
        let matchingDict = matchingDictUM as NSMutableDictionary
        if let propertyMatchDict {
            matchingDict["IOPropertyMatch"] = propertyMatchDict
        }
        // マッチするサービスを格納するための変数。
        var matchingServices = io_iterator_t.zero
        // マッチングサービスの取得に失敗した場合、nilを返します。
        if #available(macOS 12.0, *) {
            if IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &matchingServices) != KERN_SUCCESS {
                return nil
            }
        } else {
            if IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &matchingServices) != KERN_SUCCESS {
                return nil
            }
        }
        
        // 成功した場合、マッチしたサービスのイテレータを返します。
        return IOServiceIterator(rawValue: matchingServices)
    }
    
    static func findMatchingService(
        matchingKey: String,
        propertyMatchDict: Dictionary<String, Any>? = nil
    ) -> IOService? {
        guard let matchingDictUM = IOServiceMatching(matchingKey) else {
            return nil
        }
        let matchingDict = matchingDictUM as NSMutableDictionary
        if let propertyMatchDict {
            matchingDict["IOPropertyMatch"] = propertyMatchDict
        }
        if #available(macOS 12.0, *) {
            return IOService(rawValue: IOServiceGetMatchingService(kIOMainPortDefault, matchingDict))
        } else {
            return IOService(rawValue: IOServiceGetMatchingService(kIOMasterPortDefault, matchingDict))
        }
    }
    
    static func getPropertyValue<T: CFTypeRef>(from service: IOService, propertyKey: String) -> T? {
        if let dataUM = IORegistryEntryCreateCFProperty(
            service.rawValue,
            propertyKey as CFString,
            kCFAllocatorDefault,
            0 // no options
        ) {
            return dataUM.takeRetainedValue() as? T
        }
        return nil
    }
            
    static func getPropertyValues<T: CFTypeRef>(from intfIterator: IOServiceIterator, propertyKey: String) -> [T] {
        var values: [T] = []
        // イテレータからインターフェースサービスを取得します。
        var intfService = IOIteratorNext(intfIterator.rawValue)
        while intfService != 0 {
            // コントローラーサービスを格納する変数。
            var controllerService: io_object_t = 0
            // 親エントリーを取得し、MACアドレスを含むコントローラーサービスを取得します。
            if IORegistryEntryGetParentEntry(intfService, kIOServicePlane, &controllerService) == KERN_SUCCESS {
                // コントローラーサービスからIOMACAddressプロパティを取得します。
                if let dataUM = IORegistryEntryCreateCFProperty(
                    controllerService,
                    propertyKey as CFString,
                    kCFAllocatorDefault,
                    0 // no options
                ) {
                    guard let value = dataUM.takeRetainedValue() as? T else {
                        continue
                    }
                    values.append(value)
                }
                // コントローラーサービスの解放。
                IOObjectRelease(controllerService)
            }
            // インターフェースサービスの解放。
            IOObjectRelease(intfService)
            // 次のインターフェースサービスを取得。
            intfService = IOIteratorNext(intfIterator.rawValue)
        }
        return values
    }
}

// MARK: Getting Info Methods

extension IOServiceManager {
    
    /// MACアドレスの一覧を取得
    static func getMacAddresses() -> [MACAddress] {
        // イーサネットインターフェースのイテレータを取得します。
        guard let serviceIterator = IOServiceManager.findMatchingServices(matchingKey: "IOEthernetInterface") else {
            return []
        }
        let macAddressValues: [CFData] = IOServiceManager.getPropertyValues(from: serviceIterator, propertyKey: "IOMACAddress")
        let macAddresses = macAddressValues
            .map { $0 as Data }
            .map { data in
                // MACアドレスのための6バイトの配列を初期化し、データをコピーします。
                var macAddress = [UInt8](repeating: 0, count: 6)
                data.copyBytes(to: &macAddress, count: macAddress.count)
                return MACAddress(rawValue: macAddress)
            }
        // イテレータの解放はServiceを呼び出し元で行う必要がある
        // https://developer.apple.com/documentation/iokit/1514494-ioservicegetmatchingservices
        IOObjectRelease(serviceIterator.rawValue)
        return macAddresses
    }
    
    // MACアドレス(IOPrimaryInterface)の一覧を取得
    static func getMacAddressesOfPrimaryInterface() -> [MACAddress] {
        guard let serviceIterator = IOServiceManager.findMatchingServices(
            matchingKey: "IOEthernetInterface",
            // IOPrimaryInterfaceがtrueのインターフェースのみをマッチさせるためのフィルタを設定します。
            propertyMatchDict: ["IOPrimaryInterface" : true]
        ) else {
            return []
        }
        let macAddressValues: [CFData] = IOServiceManager.getPropertyValues(from: serviceIterator, propertyKey: "IOMACAddress")
        let macAddresses = macAddressValues
            .map { $0 as Data }
            .map { data in
                var macAddress = [UInt8](repeating: 0, count: 6)
                data.copyBytes(to: &macAddress, count: macAddress.count)
                return MACAddress(rawValue: macAddress)
            }
        IOObjectRelease(serviceIterator.rawValue)
        return macAddresses
    }
    
    static func getPlatformSerialNumber() -> String? {
        var serialNumber: String?
        if
            let service = IOServiceManager.findMatchingService(matchingKey: "IOPlatformExpertDevice"),
            let _serialNumber = (IOServiceManager.getPropertyValue(from: service, propertyKey: "IOPlatformUUID") as CFString?) as? String
        {
            serialNumber = _serialNumber
            IOObjectRelease(service.rawValue)
        }
        return serialNumber
    }
    
    static func getTemperature() -> Double {
        var temperature = Double.zero
        if
            let service = IOServiceManager.findMatchingService(matchingKey: "AppleSmartBattery"),
            let _temperature = (IOServiceManager.getPropertyValue(from: service, propertyKey: "Temperature") as CFNumber?) as? Double
        {
            temperature = _temperature
            IOObjectRelease(service.rawValue)
        }
        
        // 温度値の補正
        if temperature == 0 {
            return 0
        }
        return temperature / 100
    }
}
```