Skip to content

Commit b64c784

Browse files
matthewrankinclaude
andcommitted
Rework hotplug subsystem: per-context storage, timeout events, safe shutdown
Replace single global hotplugCallbackStorage with a per-context registry keyed by *C.libusb_context, so multiple contexts can register hotplug callbacks independently without clobbering each other. Replace libusb_handle_events_completed (which returns immediately and caused busy-spinning) with libusb_handle_events_timeout_completed using a 200ms timeout via a small C helper, so the event loop yields CPU between iterations. Fix done-channel race by replacing send-then-close with just close(done), the idiomatic Go shutdown signal pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 21209f6 commit b64c784

1 file changed

Lines changed: 138 additions & 82 deletions

File tree

hotplug.go

Lines changed: 138 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package libusb
77

88
// #cgo pkg-config: libusb-1.0
99
// #include <libusb.h>
10+
// #include <sys/time.h>
1011
// int libusbHotplugCallback (libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void *user_data);
1112
// typedef struct libusb_device_descriptor libusb_device_descriptor_struct;
1213
// static int libusb_hotplug_register_callback_wrapper (
@@ -18,6 +19,12 @@ package libusb
1819
// {
1920
// return libusb_hotplug_register_callback(ctx, events, flags, vendor_id, product_id, dev_class, cb_fn, user_data, callback_handle);
2021
// }
22+
// static int libusb_handle_events_timeout_ms(libusb_context *ctx, int timeout_ms) {
23+
// struct timeval tv;
24+
// tv.tv_sec = timeout_ms / 1000;
25+
// tv.tv_usec = (timeout_ms % 1000) * 1000;
26+
// return libusb_handle_events_timeout_completed(ctx, &tv, NULL);
27+
// }
2128
import "C"
2229
import (
2330
"fmt"
@@ -26,10 +33,10 @@ import (
2633
"unsafe"
2734
)
2835

29-
// HotPlugEventType ...
36+
// HotPlugEventType represents the type of hotplug event.
3037
type HotPlugEventType uint8
3138

32-
// HotPlugCbFunc callback
39+
// HotPlugCbFunc is the callback function signature for hotplug events.
3340
type HotPlugCbFunc func(vID, pID uint16, eventType HotPlugEventType)
3441

3542
// HotPlug Event Types
@@ -51,34 +58,68 @@ type hotplugCallback struct {
5158
fn HotPlugCbFunc
5259
}
5360

54-
// HotplugCallbackStorage ...
55-
type HotplugCallbackStorage struct {
61+
// hotplugStorage holds the callback map and done channel for a single context.
62+
type hotplugStorage struct {
5663
callbackMap map[uint32]hotplugCallback
5764
done chan struct{}
58-
mu sync.RWMutex // Protects the callbackMap from concurrent access
65+
mu sync.RWMutex
5966
}
6067

61-
var hotplugCallbackStorage HotplugCallbackStorage
68+
// hotplugEventTimeoutMs is the timeout in milliseconds for
69+
// libusb_handle_events_timeout_completed in the event loop. This prevents
70+
// busy-spinning while still being responsive to the done signal.
71+
const hotplugEventTimeoutMs = 200
6272

63-
func (ctx *Context) newHotPlugHandler() {
64-
hotplugCallbackStorage.callbackMap = make(map[uint32]hotplugCallback)
65-
hotplugCallbackStorage.done = make(chan struct{})
73+
// hotplugRegistry maps context pointers to their hotplug storage, allowing
74+
// multiple contexts to register hotplug callbacks independently.
75+
var (
76+
hotplugRegistry = make(map[*C.libusb_context]*hotplugStorage)
77+
hotplugRegistryMu sync.RWMutex
78+
)
6679

67-
go hotplugCallbackStorage.handleEvents(ctx.libusbContext)
80+
func getHotplugStorage(
81+
libCtx *C.libusb_context,
82+
) *hotplugStorage {
83+
hotplugRegistryMu.RLock()
84+
defer hotplugRegistryMu.RUnlock()
85+
return hotplugRegistry[libCtx]
6886
}
6987

70-
func (s *HotplugCallbackStorage) isEmpty() bool {
71-
s.mu.RLock()
72-
defer s.mu.RUnlock()
73-
return s.callbackMap == nil
88+
func (ctx *Context) newHotPlugHandler() *hotplugStorage {
89+
storage := &hotplugStorage{
90+
callbackMap: make(map[uint32]hotplugCallback),
91+
done: make(chan struct{}),
92+
}
93+
94+
hotplugRegistryMu.Lock()
95+
hotplugRegistry[ctx.libusbContext] = storage
96+
hotplugRegistryMu.Unlock()
97+
98+
go storage.handleEvents(ctx.libusbContext)
99+
return storage
74100
}
75101

76-
// HotplugRegisterCallbackEvent ...
77-
func (ctx *Context) HotplugRegisterCallbackEvent(vendorID, productID uint16,
78-
eventType HotPlugEventType, cb HotPlugCbFunc) error {
79-
if hotplugCallbackStorage.isEmpty() {
80-
ctx.newHotPlugHandler()
102+
func (ctx *Context) getOrCreateHotplugStorage() *hotplugStorage {
103+
storage := getHotplugStorage(ctx.libusbContext)
104+
if storage == nil {
105+
storage = ctx.newHotPlugHandler()
81106
}
107+
return storage
108+
}
109+
110+
func removeHotplugStorage(libCtx *C.libusb_context) {
111+
hotplugRegistryMu.Lock()
112+
delete(hotplugRegistry, libCtx)
113+
hotplugRegistryMu.Unlock()
114+
}
115+
116+
// HotplugRegisterCallbackEvent registers a hotplug callback for the given
117+
// vendor/product ID pair and event type.
118+
func (ctx *Context) HotplugRegisterCallbackEvent(
119+
vendorID, productID uint16,
120+
eventType HotPlugEventType, cb HotPlugCbFunc,
121+
) error {
122+
storage := ctx.getOrCreateHotplugStorage()
82123

83124
var event C.int
84125
switch eventType {
@@ -87,7 +128,8 @@ func (ctx *Context) HotplugRegisterCallbackEvent(vendorID, productID uint16,
87128
case HotplugLeft:
88129
event = C.LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT
89130
default:
90-
event = C.LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | C.LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT
131+
event = C.LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED |
132+
C.LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT
91133
}
92134

93135
var vID C.int = C.LIBUSB_HOTPLUG_MATCH_ANY
@@ -109,77 +151,83 @@ func (ctx *Context) HotplugRegisterCallbackEvent(vendorID, productID uint16,
109151
vID,
110152
pID,
111153
C.LIBUSB_HOTPLUG_MATCH_ANY,
112-
C.libusb_hotplug_callback_fn(unsafe.Pointer(C.libusbHotplugCallback)),
154+
C.libusb_hotplug_callback_fn(
155+
unsafe.Pointer(C.libusbHotplugCallback),
156+
),
113157
nil,
114158
&cbHandle,
115159
)
116160
if rc != C.LIBUSB_SUCCESS {
117-
return fmt.Errorf("libusb_hotplug_register_callback error: %s", ErrorCode(rc))
161+
return fmt.Errorf(
162+
"libusb_hotplug_register_callback error: %s", ErrorCode(rc),
163+
)
118164
}
119165

120-
hotplugCallbackStorage.mu.Lock()
121-
hotplugCallbackStorage.callbackMap[vidPidToUint32(vendorID, productID)] = hotplugCallback{
166+
key := vidPidToUint32(vendorID, productID)
167+
storage.mu.Lock()
168+
storage.callbackMap[key] = hotplugCallback{
122169
handler: &cbHandle,
123170
fn: cb,
124171
}
125-
hotplugCallbackStorage.mu.Unlock()
172+
storage.mu.Unlock()
126173

127174
return nil
128175
}
129176

130-
// HotplugDeregisterCallback ...
131-
func (ctx *Context) HotplugDeregisterCallback(vendorID, productID uint16) error {
132-
if hotplugCallbackStorage.isEmpty() {
177+
// HotplugDeregisterCallback deregisters a hotplug callback for the given
178+
// vendor/product ID pair.
179+
func (ctx *Context) HotplugDeregisterCallback(
180+
vendorID, productID uint16,
181+
) error {
182+
storage := getHotplugStorage(ctx.libusbContext)
183+
if storage == nil {
133184
return nil
134185
}
135186

136187
key := vidPidToUint32(vendorID, productID)
137188

138-
hotplugCallbackStorage.mu.RLock()
139-
cb, ok := hotplugCallbackStorage.callbackMap[key]
140-
hotplugCallbackStorage.mu.RUnlock()
189+
storage.mu.RLock()
190+
cb, ok := storage.callbackMap[key]
191+
storage.mu.RUnlock()
141192

142193
if !ok {
143194
return nil
144195
}
145196

146197
C.libusb_hotplug_deregister_callback(ctx.libusbContext, *cb.handler)
147198

148-
hotplugCallbackStorage.mu.Lock()
149-
delete(hotplugCallbackStorage.callbackMap, key)
150-
mapEmpty := len(hotplugCallbackStorage.callbackMap) == 0
151-
hotplugCallbackStorage.mu.Unlock()
199+
storage.mu.Lock()
200+
delete(storage.callbackMap, key)
201+
mapEmpty := len(storage.callbackMap) == 0
202+
storage.mu.Unlock()
152203

153204
if mapEmpty {
154205
ctx.hotplugHandleEventsCompleteAll()
155206
}
156207
return nil
157208
}
158209

159-
// HotplugDeregisterAllCallbacks ...
210+
// HotplugDeregisterAllCallbacks deregisters all hotplug callbacks for this
211+
// context and stops the event handler goroutine.
160212
func (ctx *Context) HotplugDeregisterAllCallbacks() error {
161-
hotplugCallbackStorage.mu.RLock()
162-
mapExists := hotplugCallbackStorage.callbackMap != nil
163-
164-
if mapExists {
165-
// Make a copy of the handlers to avoid holding the lock during C function
166-
// calls
167-
handlers := make(
168-
[]*C.libusb_hotplug_callback_handle,
169-
0,
170-
len(hotplugCallbackStorage.callbackMap),
171-
)
172-
for _, cb := range hotplugCallbackStorage.callbackMap {
173-
handlers = append(handlers, cb.handler)
174-
}
175-
hotplugCallbackStorage.mu.RUnlock()
213+
storage := getHotplugStorage(ctx.libusbContext)
214+
if storage == nil {
215+
return nil
216+
}
176217

177-
// Deregister callbacks without holding the lock
178-
for _, handler := range handlers {
179-
C.libusb_hotplug_deregister_callback(ctx.libusbContext, *handler)
180-
}
181-
} else {
182-
hotplugCallbackStorage.mu.RUnlock()
218+
storage.mu.RLock()
219+
handlers := make(
220+
[]*C.libusb_hotplug_callback_handle,
221+
0,
222+
len(storage.callbackMap),
223+
)
224+
for _, cb := range storage.callbackMap {
225+
handlers = append(handlers, cb.handler)
226+
}
227+
storage.mu.RUnlock()
228+
229+
for _, handler := range handlers {
230+
C.libusb_hotplug_deregister_callback(ctx.libusbContext, *handler)
183231
}
184232

185233
ctx.hotplugHandleEventsCompleteAll()
@@ -188,48 +236,57 @@ func (ctx *Context) HotplugDeregisterAllCallbacks() error {
188236
}
189237

190238
func (ctx *Context) hotplugHandleEventsCompleteAll() {
191-
if hotplugCallbackStorage.isEmpty() {
239+
storage := getHotplugStorage(ctx.libusbContext)
240+
if storage == nil {
192241
return
193242
}
194243

195-
// Signal the event handler to stop
196-
hotplugCallbackStorage.done <- struct{}{}
244+
// Signal the event handler goroutine to stop. Closing the channel
245+
// unblocks all receivers immediately without needing a separate send.
246+
close(storage.done)
197247

198-
// Clear the callbackMap and close the channel
199-
hotplugCallbackStorage.mu.Lock()
200-
hotplugCallbackStorage.callbackMap = nil
201-
hotplugCallbackStorage.mu.Unlock()
248+
// Clean up the storage for this context.
249+
storage.mu.Lock()
250+
storage.callbackMap = nil
251+
storage.mu.Unlock()
202252

203-
close(hotplugCallbackStorage.done)
253+
removeHotplugStorage(ctx.libusbContext)
204254
}
205255

206-
func (storage *HotplugCallbackStorage) handleEvents(libCtx *C.libusb_context) {
256+
func (storage *hotplugStorage) handleEvents(
257+
libCtx *C.libusb_context,
258+
) {
207259
for {
208260
select {
209261
case <-storage.done:
210262
return
211263
default:
212264
}
213-
if errno := C.libusb_handle_events_completed(libCtx, nil); errno < 0 {
265+
errno := C.libusb_handle_events_timeout_ms(
266+
libCtx, C.int(hotplugEventTimeoutMs),
267+
)
268+
if errno < 0 {
214269
if ErrorCode(errno) == errorInterrupted {
215-
continue // ignore harmless EINTR
270+
continue
216271
}
217272
log.Printf("handle_events error: %s", ErrorCode(errno))
218273
}
219274
}
220275
}
221276

222277
//export libusbHotplugCallback
223-
func libusbHotplugCallback(ctx *C.libusb_context, dev *C.libusb_device,
224-
event C.libusb_hotplug_event, p unsafe.Pointer) C.int {
278+
func libusbHotplugCallback(
279+
ctx *C.libusb_context, dev *C.libusb_device,
280+
event C.libusb_hotplug_event, p unsafe.Pointer,
281+
) C.int {
225282
var desc C.libusb_device_descriptor_struct
226283
rc := C.libusb_get_device_descriptor(dev, &desc)
227284
if rc != C.LIBUSB_SUCCESS {
228285
return rc
229286
}
230287

231-
var vendorID = uint16(desc.idVendor)
232-
var productID = uint16(desc.idProduct)
288+
vendorID := uint16(desc.idVendor)
289+
productID := uint16(desc.idProduct)
233290

234291
var e HotPlugEventType
235292
switch event {
@@ -241,28 +298,27 @@ func libusbHotplugCallback(ctx *C.libusb_context, dev *C.libusb_device,
241298
e = HotplugUndefined
242299
}
243300

244-
// Read callback map with a read lock
245-
hotplugCallbackStorage.mu.RLock()
246-
// Get device-specific callback
247-
cb, ok := hotplugCallbackStorage.callbackMap[vidPidToUint32(vendorID, productID)]
301+
storage := getHotplugStorage(ctx)
302+
if storage == nil {
303+
return C.LIBUSB_SUCCESS
304+
}
305+
306+
storage.mu.RLock()
307+
cb, ok := storage.callbackMap[vidPidToUint32(vendorID, productID)]
248308
var deviceCallback HotPlugCbFunc
249309
if ok {
250310
deviceCallback = cb.fn
251311
}
252-
253-
// Get the callback for all devices
254-
cb, ok = hotplugCallbackStorage.callbackMap[0]
312+
cb, ok = storage.callbackMap[0]
255313
var allCallback HotPlugCbFunc
256314
if ok {
257315
allCallback = cb.fn
258316
}
259-
hotplugCallbackStorage.mu.RUnlock()
317+
storage.mu.RUnlock()
260318

261-
// Call callbacks outside the lock
262319
if deviceCallback != nil {
263320
deviceCallback(vendorID, productID, e)
264321
}
265-
266322
if allCallback != nil {
267323
allCallback(vendorID, productID, e)
268324
}

0 commit comments

Comments
 (0)