macOS で USBデバイスの抜き差しを検出する

macOSiOS/Android(ADB) デバイスの接続・切断を検出したくて、この数日ずっと調べていてようやく形にできた。

github.com

備忘録的にどうやっているのかを簡単に書いておこうと思う。

IONotificationPort を CFRunLoop に接続する

まず、IOKit からの通知を受け取る IONotificationPortRef が必要なので IONotificationPortCreate で取得する。ここで発生したイベントがランループで発生するよう、IONotificationPortGetRunLoopSource で CFRunLoopSourceRef を取得して、CFRunLoopAddSource で CFRunLoop に接続する。

このとき、この処理を呼び出すスレッドがどこになるかわからないので、NSThread を専用に作った方がいいと思う。

bself.runLoop = CFRunLoopGetCurrent();
bself.notificationPort = IONotificationPortCreate(kIOMasterPortDefault);
bself.notificationRunLoopSource = IONotificationPortGetRunLoopSource(bself.notificationPort);
CFRunLoopAddSource(bself.runLoop, bself.notificationRunLoopSource, kCFRunLoopDefaultMode);

IOServiceAddMatchingNotification で通知を受け取る

IOServiceAddMatchingNotification で通知を受け取る関数を登録する。

  • 第1引数は先ほど作った IONotificationPortRef
  • 第2引数は受け取りたいイベント
    • kIOPublishNotification: デバイスが接続された
    • kIOMatchedNotification: デバイスの認識が終わって使えるようになった
    • kIOTerminatedNotification: デバイスが終了した(切断された)
    • kIOFirstPublishNotification, kIOFirstMatchNotification: First/Matched と同じだけど、初回の1回だけ通知が発生するらしい。
  • 第3引数はマッチ条件の CFMutableDictionaryRef。
    • IOServiceMatching などの関数で作れる。
    • IOService 系のメソッドの MatchingDictionary の Retain Count を 1 減らす。すぐ作って使ってなくなるなら CFRelease は不要だし、他で使うなら CFRetain をしておく必要がある。
  • 第4引数はコールバック関数
    • 適宜 void Callback(void* refcon, io_iterator_t iterator) というシグネチャで定義しておく。
      • 最初 static void Callback(void* refcon, io_iterator_t iterator) にしてて詰んでた。
  • 第5引数は任意のデータ。
    • コールバック関数の第1引数に渡される。
  • 第6引数はイテレータを受けるポインタ
    • IOServiceAddMatchingNotification を呼び出したタイミングで条件に該当するデータが入っているので、必要があれば中身を処理しておく。
    • 通知の受け取りを終了するまで IOObjectRelease してはいけない。
      • これでめっちゃハマった。
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
CFDictionaryAddValue(matchingDict, CFSTR(kIOPropertyMatchKey),
                     (CFDictionaryRef) @{ @"SupportsIPhoneOS": @YES });

IOServiceAddMatchingNotification(bself.notificationPort,
                                 kIOMatchedNotification,
                                 matchingDict,
                                 EBIMobileDeviceWatcherIOSDeviceMatchedCallback,
                                 (__bridge void *)(bself),
                                 &iosDeviceMatchedIter);
[bself onIOSDeviceMatchedCallback:iosDeviceMatchedIter];

CFRunLoop を回す

NSThread を新規に立てている場合は CFRunLoopRun で RunLoop を回す。

通知の受け取りを止める。

  • NSThread を外から止めるときは、CFRunLoopStop で止めればいい(プロパティなどに CFRunLoopRef を保持しておくといい)。
  • CFRunLoopRemoveSource で RunLoop から Notification の RunLoopSource を取り除く。
  • IOObjectRelease で IOServiceAddMatchingNotification から戻ってきたイテレータを解放する
  • IONotificationPortDestroy で IONotificationPortRef を解放する。

iOS/Androidバイスの接続/切断検知

  • iOSバイスは IOServiceMatching(kIOUSBDeviceClassName) でウォッチできる。かつ SupportsIPhoneOS というプロパティをもっているので、これでマッチさせれば一発でとれる。
  • Androidバイスは、デバイスだけでは Android かどうかを判別できない。また、端末ロック中は ADB インターフェイスが認識されなかったりする。そのため、IOServiceMatching(kIOUSBInterfaceClassName) で ADB インターフェイスをウォッチして、親の USB デバイスを取る、ということをする必要がある。
  • 切断は USB デバイスだけウォッチして、iOS は一致を、Android はシリアルナンバーの一致で切断判定している。

IOKit を使った情報はかなり少なくて苦労したけど、腰も砕けよ 膝も折れよ さんのところがいろいろ詳しく書いてあってすごく助かった。ありがとうございました。