Fix/57 macos iosetelement not found#61
Merged
Merged
Conversation
- Move setDevices(...) before MacOS.runEventLoop(...) so the devices are observable to the caller of init() the moment discovery completes (previously, setDevices was unreachable because CFRunLoopRun blocks indefinitely). - Replace the volatile boolean + Thread.sleep(100) polling wait with a CountDownLatch awaited with a 3 second timeout, removing the happens-before ambiguity from the previous approach. - Decrement the latch from the catch block too so callers do not block forever on init failures.
…ion objects - Demote the 'IOHIDElement not found' message in hidInputValueCallback from WARNING to FINE. Reporting elements that belong to unregistered HID devices (keyboard, trackpad, etc.) is informational, not an error, and was flooding the user's log output. - Release the CFSet returned by IOHIDManagerCopyDevices and the CFArray returned by IOHIDDeviceCopyMatchingElements via CFRelease in a finally block. Both follow the Create Rule and were leaked on every discovery, causing slow but real growth across long sessions.
…CFIndex width - Build a CFDictionary matching criteria for IOHIDManager that restricts enumeration to HID devices on the Generic Desktop usage page whose primary usage is joystick (0x04) or gamepad (0x05). Without this, the HID manager opens with a NULL matching dict, which means 'all devices', causing the input value callback to fire for keyboards, mice, trackpads and other unrelated HID hardware. - Add the necessary CoreFoundation bindings: CFDictionaryCreateMutable, CFDictionarySetValue, CFArrayCreateMutable, CFArrayAppendValue and CFNumberCreate. - Resolve the addresses of kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks and kCFTypeArrayCallBacks at static init time via dlsym on the CoreFoundation framework, so we do not depend on hard-coded addresses that change between macOS releases. - Change the CFArrayGetCount, CFSetGetCount (return) and CFArrayGetValueAtIndex (second arg) function descriptors from JAVA_INT to JAVA_LONG, because CFIndex is typedef long on 64-bit macOS. The previous signatures would silently truncate any count over 2^31 and on arrays over 2^31 elements.
Pull the InputDevice / component assembly logic out of internalInitDevices into a package-private populateDevices(Collection) method. The discovery call (getSupportedHIDDevices) still requires native libs to run, but the assembly logic is pure Java and can now be unit-tested by passing in hand-built IOHIDDevice fixtures on any platform.
…ointer Adds IOHIDElementGetCookie binding and stores the cookie on IOHIDElement during discovery. The hidInputValueCallback now resolves the originating element by (deviceAddress, cookie) instead of dereferencing the sender pointer, which is unsafe across the FFM boundary. - MacOS: IOHIDElementGetCookie downcall handle - IOHIDElement: int cookie field with javadoc - IOKitPlugin: Map<Long, Map<Integer, IOHIDElement>> indexed by device address and cookie; populateDevices builds the index, findElement queries it, close() clears it. - hidInputValueCallback: resolve element via findElement(sender.address(), MacOS.IOHIDElementGetCookie(element)). If the element cannot be resolved, log at FINE and return (no WARNING flood). - IOKitPluginTests: covers populateDevices (registers devices/components, skips UNDEFINED for components but still indexes them), findElement (cookie+device lookup, isolation across devices, null for unknown cookie/device, cleared on close).
… fold setDevices into populateDevices - MacOS: the static initializer looked up kCFType*CallBacks via a confined Arena that closed at the end of the block, leaving the three stored MemorySegments backed by a closed scope. They worked only by accident (raw pointer pass-through and framework refcount > 0). Replace with a static Arena.global() so the segments live for the JVM lifetime. - IOKitPlugin: move the setDevices call from internalInitDevices into populateDevices so the helper is a complete discover+publish unit. - IOKitPluginTests: drop the publishDevices reflection helper now that populateDevices itself publishes the devices.
- IOKitPlugin: remove the unused MacOS.IOHIDValueGetTimeStamp call in hidInputValueCallback. - MacOS: fix IOHIDElementGetCookie Javadoc - the method throws on failure, it does not return 0 as a sentinel. - IOKitPluginTests: reorder imports to match AGENTS.md (java.*, external, de.gurkenlabs.*, static).
- MacOS: include kHIDUsage_GD_MultiAxisController (0x08) in the IOHIDManager matching dict so flight sticks, 3D mice, and some racing wheels are picked up alongside joysticks and gamepads. - MacOS: build the matching dictionary once at class init and reuse it across initHIDManager invocations. Removes the per-init CFArray / CFDictionary / CFNumber allocations and the try/finally release dance. - IOKitPlugin: move the initLatch CountDownLatch from a field to a local in internalInitDevices. The field was never read by anything other than the initialization flow itself.
… join CFRunLoopRun is a native C function that blocks on a Mach port and is not responsive to Thread.interrupt(). The previous close() called eventLoopThread.interrupt() and returned, leaving the run-loop thread blocked indefinitely. JVM shutdown would hang waiting for the thread; re-initializing the plugin would leak a thread with a live HID manager. - MacOS: add CFRunLoopStop downcall handle and helper. - MacOS: add CFRunLoopGetCurrent helper to expose the calling thread's CFRunLoopRef through the same try/catch wrapper as the other helpers. - IOKitPlugin: capture this thread's CFRunLoopGetCurrent() into a volatile eventLoopRunLoop field immediately before initLatch.countDown so close() can find it. The CountDownLatch's happens-before edge guarantees the main thread sees the captured ref. - IOKitPlugin.close: call CFRunLoopStop on the captured run loop, join the event-loop thread with a 3 s timeout, then super.close() and clear the maps. The event-loop thread is guaranteed not to write to IOHIDElement.currentValue or the cookie index by the time super.close() runs.
The CFRunLoop thread writes currentValue in hidInputValueCallback and the polling thread reads it in pollIOHIDDevice. Per JLS 17.4 the read is not guaranteed to see the most recent write without a happens- before edge. The fix is the volatile keyword on the field.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Fixes #57: macOS IOKit plugin throws
IllegalStateExceptionand floods log with "IOHIDElement not found" warnings.Root cause
setDevices(...)was called afterCFRunLoopRun()blocked, so callers ofinit()could observeIllegalStateExceptionfromgetAll().IOHIDElementRefpointer, which is unreliable when the FFM downcall layer hands back aMemorySegmentcarrier — causing the WARNING flood.Changes
volatile boolean+Thread.sleep(100)polling withCountDownLatch; count down incatchso failures don't hang.IOHIDElementGetCookie, store onIOHIDElement, resolve values via(deviceAddress, cookie)index inhidInputValueCallback. Demote unknown-element log to FINE.kIOHIDPrimaryUsagePageto0x01andkIOHIDPrimaryUsageto[joystick, gamepad, multi-axis controller].CFArrayGetCount/CFSetGetCount/CFArrayGetValueAtIndexuseJAVA_LONG, notJAVA_INT.CFReleasetheIOHIDManagerCopyDevicesset and the per-deviceIOHIDDeviceCopyMatchingElementsarray.Arena(closed at end of static block) withArena.global()for the CoreFoundation callback-struct lookups, so the storedMemorySegments live for the JVM lifetime.close()now callsCFRunLoopStopon the captured run loop, joins the event-loop thread (3 s timeout), thensuper.close()— no more JVM hangs on a blockedCFRunLoopRun.populateDevices(...)for cross-platform unit testing; foldsetDevicesinto it. Singleton-ify the matching dict.initLatch→ local.IOHIDElement.currentValueis nowvolatile(was a JLS 17.4 data race between CFRunLoop writer and polling reader).