1+ package com .shimmerresearch .usb ;
2+
3+ import com .sun .jna .*;
4+ import com .sun .jna .ptr .IntByReference ;
5+
6+ public class UsbDebugListenerMacOS extends UsbDebugListener {
7+
8+ private static final String kIOUSBDeviceClassName = "IOUSBDevice" ;
9+ private static final String kIOFirstMatchNotification = "IOServiceFirstMatch" ;
10+ private static final String kIOTerminatedNotification = "IOServiceTerminate" ;
11+
12+ private volatile boolean running = false ;
13+ private Thread listenerThread ;
14+ private final UsbDockChangeListener listener ;
15+
16+ private IOKit .IONotificationPortRef notificationPort ;
17+ private CoreFoundation .CFRunLoopRef runLoop ;
18+
19+ private int addedIterator = 0 ;
20+ private int removedIterator = 0 ;
21+ private boolean suppressInitialDeviceScan = true ;
22+
23+ public UsbDebugListenerMacOS (UsbDockChangeListener listener ) {
24+ this .listener = listener ;
25+ }
26+
27+ public synchronized void start () {
28+ if (running ) {
29+ System .out .println ("[DEBUG] macOS USB listener already running." );
30+ return ;
31+ }
32+
33+ running = true ;
34+
35+ listenerThread = new Thread (() -> {
36+ try {
37+ runNotificationLoop ();
38+ } catch (Exception e ) {
39+ e .printStackTrace ();
40+ } finally {
41+ running = false ;
42+ cleanup ();
43+ System .out .println ("[DEBUG] macOS USB listener thread finished." );
44+ }
45+ }, "UsbDebugListenerMacOS" );
46+
47+ listenerThread .setDaemon (true );
48+ listenerThread .start ();
49+ }
50+
51+ public synchronized void stop () {
52+ if (!running ) {
53+ return ;
54+ }
55+
56+ System .out .println ("[DEBUG] Stopping macOS USB listener..." );
57+ running = false ;
58+
59+ if (runLoop != null ) {
60+ CoreFoundation .INSTANCE .CFRunLoopStop (runLoop );
61+ }
62+ }
63+
64+ public boolean isRunning () {
65+ return running ;
66+ }
67+
68+ private void runNotificationLoop () {
69+ System .out .println ("[DEBUG] Starting macOS USB debug listener..." );
70+
71+ runLoop = CoreFoundation .INSTANCE .CFRunLoopGetCurrent ();
72+ if (runLoop == null ) {
73+ throw new RuntimeException ("CFRunLoopGetCurrent returned null" );
74+ }
75+
76+ int mainPort = IOKit .INSTANCE .IOMainPort (0 , new IntByReference ());
77+ System .out .println ("[DEBUG] IOMainPort result = " + mainPort );
78+
79+ notificationPort = IOKit .INSTANCE .IONotificationPortCreate (mainPort );
80+ if (notificationPort == null ) {
81+ throw new RuntimeException ("IONotificationPortCreate failed" );
82+ }
83+
84+ Pointer runLoopSource = IOKit .INSTANCE .IONotificationPortGetRunLoopSource (notificationPort );
85+ if (runLoopSource == null ) {
86+ throw new RuntimeException ("IONotificationPortGetRunLoopSource returned null" );
87+ }
88+
89+ if (CoreFoundation .kCFRunLoopDefaultMode == null ) {
90+ throw new RuntimeException ("kCFRunLoopDefaultMode is null" );
91+ }
92+
93+ CoreFoundation .INSTANCE .CFRunLoopAddSource (
94+ runLoop ,
95+ runLoopSource ,
96+ CoreFoundation .kCFRunLoopDefaultMode
97+ );
98+
99+ Pointer matchingDictAdd = IOKit .INSTANCE .IOServiceMatching (kIOUSBDeviceClassName );
100+ if (matchingDictAdd == null ) {
101+ throw new RuntimeException ("IOServiceMatching(IOUSBDevice) failed for add" );
102+ }
103+
104+ Pointer matchingDictRemove = IOKit .INSTANCE .IOServiceMatching (kIOUSBDeviceClassName );
105+ if (matchingDictRemove == null ) {
106+ throw new RuntimeException ("IOServiceMatching(IOUSBDevice) failed for remove" );
107+ }
108+
109+ IOKit .IOServiceMatchingCallback addCallback = (refCon , iterator ) -> {
110+ System .out .println ("[DEBUG] Device arrival callback triggered" );
111+ drainIterator (iterator , true , false );
112+ };
113+
114+ IOKit .IOServiceMatchingCallback removeCallback = (refCon , iterator ) -> {
115+ System .out .println ("[DEBUG] Device removal callback triggered" );
116+ drainIterator (iterator , false , false );
117+ };
118+
119+ IntByReference addIterRef = new IntByReference ();
120+ int kr = IOKit .INSTANCE .IOServiceAddMatchingNotification (
121+ notificationPort ,
122+ kIOFirstMatchNotification ,
123+ matchingDictAdd ,
124+ addCallback ,
125+ null ,
126+ addIterRef
127+ );
128+ System .out .println ("[DEBUG] IOServiceAddMatchingNotification(add) = " + kr );
129+ if (kr != 0 ) {
130+ throw new RuntimeException ("IOServiceAddMatchingNotification(add) failed: " + kr );
131+ }
132+ addedIterator = addIterRef .getValue ();
133+
134+ IntByReference removeIterRef = new IntByReference ();
135+ kr = IOKit .INSTANCE .IOServiceAddMatchingNotification (
136+ notificationPort ,
137+ kIOTerminatedNotification ,
138+ matchingDictRemove ,
139+ removeCallback ,
140+ null ,
141+ removeIterRef
142+ );
143+ System .out .println ("[DEBUG] IOServiceAddMatchingNotification(remove) = " + kr );
144+ if (kr != 0 ) {
145+ throw new RuntimeException ("IOServiceAddMatchingNotification(remove) failed: " + kr );
146+ }
147+ removedIterator = removeIterRef .getValue ();
148+
149+ // Drain existing devices once to arm notifications.
150+ // Usually you do not want currently-connected devices to be treated as "new arrivals".
151+ drainIterator (addedIterator , true , suppressInitialDeviceScan );
152+ drainIterator (removedIterator , false , true );
153+
154+ System .out .println ("[DEBUG] Entering CFRunLoop..." );
155+ CoreFoundation .INSTANCE .CFRunLoopRun ();
156+ System .out .println ("[DEBUG] CFRunLoop exited." );
157+ }
158+
159+ private void drainIterator (int iterator , boolean connected , boolean suppressEvents ) {
160+ while (true ) {
161+ int service = IOKit .INSTANCE .IOIteratorNext (iterator );
162+ if (service == 0 ) {
163+ break ;
164+ }
165+
166+ try {
167+ if (!suppressEvents ) {
168+ if (connected ) {
169+ System .out .println ("[EVENT] Device connected" );
170+ if (listener != null ) {
171+ listener .onUsbDeviceConnected ();
172+ }
173+ } else {
174+ System .out .println ("[EVENT] Device disconnected" );
175+ if (listener != null ) {
176+ listener .onUsbDeviceDisconnected ();
177+ }
178+ }
179+ } else {
180+ System .out .println ("[DEBUG] Suppressed initial device event" );
181+ }
182+ } finally {
183+ IOKit .INSTANCE .IOObjectRelease (service );
184+ }
185+ }
186+ }
187+
188+ private void cleanup () {
189+ if (addedIterator != 0 ) {
190+ IOKit .INSTANCE .IOObjectRelease (addedIterator );
191+ addedIterator = 0 ;
192+ }
193+
194+ if (removedIterator != 0 ) {
195+ IOKit .INSTANCE .IOObjectRelease (removedIterator );
196+ removedIterator = 0 ;
197+ }
198+
199+ if (notificationPort != null ) {
200+ IOKit .INSTANCE .IONotificationPortDestroy (notificationPort );
201+ notificationPort = null ;
202+ }
203+
204+ runLoop = null ;
205+ System .out .println ("[DEBUG] macOS USB listener cleaned up." );
206+ }
207+
208+ interface CoreFoundation extends Library {
209+ CoreFoundation INSTANCE = Native .load ("CoreFoundation" , CoreFoundation .class );
210+
211+ class CFRunLoopRef extends PointerType {}
212+
213+ Pointer kCFRunLoopDefaultMode = NativeLibrary
214+ .getInstance ("CoreFoundation" )
215+ .getGlobalVariableAddress ("kCFRunLoopDefaultMode" )
216+ .getPointer (0 );
217+
218+ CFRunLoopRef CFRunLoopGetCurrent ();
219+ void CFRunLoopRun ();
220+ void CFRunLoopStop (CFRunLoopRef rl );
221+ void CFRunLoopAddSource (CFRunLoopRef rl , Pointer source , Pointer mode );
222+ }
223+
224+ interface IOKit extends Library {
225+ IOKit INSTANCE = Native .load ("IOKit" , IOKit .class );
226+
227+ class IONotificationPortRef extends PointerType {}
228+
229+ interface IOServiceMatchingCallback extends Callback {
230+ void invoke (Pointer refCon , int iterator );
231+ }
232+
233+ int IOMainPort (int bootstrapPort , IntByReference mainPort );
234+
235+ IONotificationPortRef IONotificationPortCreate (int masterPort );
236+ Pointer IONotificationPortGetRunLoopSource (IONotificationPortRef notifyPort );
237+ void IONotificationPortDestroy (IONotificationPortRef notifyPort );
238+
239+ Pointer IOServiceMatching (String name );
240+
241+ int IOServiceAddMatchingNotification (
242+ IONotificationPortRef notifyPort ,
243+ String notificationType ,
244+ Pointer matchingDictionary ,
245+ IOServiceMatchingCallback callback ,
246+ Pointer refCon ,
247+ IntByReference notificationIterator
248+ );
249+
250+ int IOIteratorNext (int iterator );
251+ int IOObjectRelease (int object );
252+ }
253+ }
0 commit comments