From ac2cec84dbbd4f8a3fa8b3ad0c28869fb5c0f7f9 Mon Sep 17 00:00:00 2001 From: Nikola Kovacs Date: Wed, 7 Mar 2018 18:36:37 +0100 Subject: [PATCH 1/3] WIP: hotplug support --- constants.go | 12 ++++ fakelibusb_test.go | 6 ++ hotplug.c | 32 +++++++++ hotplug.go | 166 +++++++++++++++++++++++++++++++++++++++++++++ libusb.go | 106 +++++++++++++++++++++++++++++ 5 files changed, 322 insertions(+) create mode 100644 hotplug.c create mode 100644 hotplug.go diff --git a/constants.go b/constants.go index c2061c3..fdcca59 100644 --- a/constants.go +++ b/constants.go @@ -256,3 +256,15 @@ const ( // Milliamperes is a unit of electric current consumption. type Milliamperes uint + +// HotplugEventType identifies the type of the hotplug event. +type HotplugEventType uint + +// Hotplug events. +const ( + HotplugEventDeviceArrived HotplugEventType = C.LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED + HotplugEventDeviceLeft HotplugEventType = C.LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT + HotplugEventAny HotplugEventType = HotplugEventDeviceArrived | HotplugEventDeviceLeft +) + +const HotplugMatchAny = C.LIBUSB_HOTPLUG_MATCH_ANY diff --git a/fakelibusb_test.go b/fakelibusb_test.go index 5774d90..378b025 100644 --- a/fakelibusb_test.go +++ b/fakelibusb_test.go @@ -209,6 +209,12 @@ func (f *fakeLibusb) empty() bool { return len(f.submitted) == 0 } +func (f *fakeLibusb) registerHotplugCallback(ctx *libusbContext, events HotplugEventType, enumerate bool, vendorId int32, productId int32, devClass int32, fn libusbHotplugCallback) (func(), error) { + // TODO: implement + return func() { + }, nil +} + func newFakeLibusb() *fakeLibusb { fl := &fakeLibusb{ fakeDevices: make(map[*libusbDevice]*fakeDevice), diff --git a/hotplug.c b/hotplug.c new file mode 100644 index 0000000..13aff73 --- /dev/null +++ b/hotplug.c @@ -0,0 +1,32 @@ +// Copyright 2013 Google Inc. All rights reserved. +// Copyright 2016 the gousb Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "_cgo_export.h" + +int gousb_hotplug_register_callback( + libusb_context* ctx, + libusb_hotplug_event events, + libusb_hotplug_flag flags, + int vid, + int pid, + int dev_class, + void *user_data, + libusb_hotplug_callback_handle *handle +) { + return libusb_hotplug_register_callback( + ctx, events, flags, vid, pid, dev_class, (libusb_hotplug_callback_fn)(goHotplugCallback), user_data, handle + ); +} diff --git a/hotplug.go b/hotplug.go new file mode 100644 index 0000000..cca3cd3 --- /dev/null +++ b/hotplug.go @@ -0,0 +1,166 @@ +// Copyright 2013 Google Inc. All rights reserved. +// Copyright 2016 the gousb Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gousb + +// HotplugOptions can be used to configure and register a hotplug event callback. +// By default, the callback will be called when a new device arrives +// or a device leaves. +// Call Arrived to only receive HotplugEventDeviceArrived events, +// and Left to only receive HotplugEventDeviceLeft events. +// Call Enumerate to also receive HotplugEventDeviceArrived events for +// devices that are already plugged in. +// Use ProductID, VendorID and DeviceClass to filter by product id, vendor id +// or device class. +// Call Register to register the callback function. The options cannot +// be changed after registering. The returned HotplugCallback +// can be used to deregister the callback. +// Callbacks are automatically deregistered when the Context is closed. +type HotplugOptions interface { + Arrived() HotplugOptions + Left() HotplugOptions + Enumerate() HotplugOptions + ProductID(ID) HotplugOptions + VendorID(ID) HotplugOptions + DeviceClass(Class) HotplugOptions + Register(func(HotplugEvent)) (HotplugCallback, error) +} + +// HotplugCallback is a registered hotplug callback. +// It only method is Deregister, which can be used to deregister the callback. +// It is safe to call deregister multiple times. +// Callbacks are also automatically deregistered when the Context is closed. +type HotplugCallback interface { + Deregister() +} + +// HotplugEvent is a hotplug event. +// Its methods should only be called during the event callback. +type HotplugEvent interface { + // Type returns the event's type (HotplugEventDeviceArrived or HotplugEventDeviceLeft). + Type() HotplugEventType + // DeviceDesc returns the device's descriptor. + DeviceDesc() (*DeviceDesc, error) + // Open opens the device. + Open() (*Device, error) +} + +type hotplugOptions struct { + ctx *Context + events HotplugEventType + enumerate bool + productID int32 + vendorID int32 + devClass int32 +} + +type hotplugEvent struct { + eventType HotplugEventType + ctx *Context + dev *libusbDevice + desc *DeviceDesc + err error +} + +type hotplugCallback struct { + fn func() +} + +func (c *hotplugCallback) Deregister() { + c.fn() +} + +// Hotplug sets up a hotplug event callback. +// It returns a HotplugOptions that can be used to configure and register +// the callback. +func (c *Context) Hotplug() HotplugOptions { + return &hotplugOptions{ + ctx: c, + productID: HotplugMatchAny, + vendorID: HotplugMatchAny, + devClass: HotplugMatchAny, + } +} + +func (h *hotplugOptions) Arrived() HotplugOptions { + h.events |= HotplugEventDeviceArrived + return h +} + +func (h *hotplugOptions) Left() HotplugOptions { + h.events |= HotplugEventDeviceLeft + return h +} + +func (h *hotplugOptions) Enumerate() HotplugOptions { + h.enumerate = true + return h +} + +func (h *hotplugOptions) ProductID(pid ID) HotplugOptions { + h.productID = int32(pid) + return h +} + +func (h *hotplugOptions) VendorID(vid ID) HotplugOptions { + h.vendorID = int32(vid) + return h +} + +func (h *hotplugOptions) DeviceClass(devClass Class) HotplugOptions { + h.devClass = int32(devClass) + return h +} + +func (h *hotplugOptions) Register(fn func(HotplugEvent)) (HotplugCallback, error) { + if h.events == 0 { + h.events = HotplugEventAny + } + dereg, err := h.ctx.libusb.registerHotplugCallback(h.ctx.ctx, h.events, h.enumerate, h.vendorID, h.productID, h.devClass, func(ctx *libusbContext, dev *libusbDevice, eventType HotplugEventType) { + desc, err := h.ctx.libusb.getDeviceDesc(dev) + fn(&hotplugEvent{ + eventType: eventType, + ctx: h.ctx, + dev: dev, + desc: desc, + err: err, + }) + }) + return &hotplugCallback{ + fn: dereg, + }, err +} + +// Type returns the event's type (HotplugEventDeviceArrived or HotplugEventDeviceLeft). +func (e *hotplugEvent) Type() HotplugEventType { + return e.eventType +} + +// DeviceDesc returns the device's descriptor. +func (e *hotplugEvent) DeviceDesc() (*DeviceDesc, error) { + return e.desc, e.err +} + +// Open opens the device. +func (e *hotplugEvent) Open() (*Device, error) { + if e.err != nil { + return nil, e.err + } + handle, err := e.ctx.libusb.open(e.dev) + if err != nil { + return nil, err + } + return &Device{handle: handle, ctx: e.ctx, Desc: e.desc}, nil +} diff --git a/libusb.go b/libusb.go index 2c533a1..378c27d 100644 --- a/libusb.go +++ b/libusb.go @@ -15,6 +15,7 @@ package gousb import ( + "errors" "fmt" "log" "reflect" @@ -26,11 +27,22 @@ import ( /* #cgo pkg-config: libusb-1.0 #include +#include int gousb_compact_iso_data(struct libusb_transfer *xfer, unsigned char *status); struct libusb_transfer *gousb_alloc_transfer_and_buffer(int bufLen, int numIsoPackets); void gousb_free_transfer_and_buffer(struct libusb_transfer *xfer); int submit(struct libusb_transfer *xfer); +int gousb_hotplug_register_callback( + libusb_context* ctx, + libusb_hotplug_event events, + libusb_hotplug_flag flags, + int vid, + int pid, + int dev_class, + void *user_data, + libusb_hotplug_callback_handle *handle +); */ import "C" @@ -124,6 +136,8 @@ func (ep libusbEndpoint) endpointDesc(dev *DeviceDesc) EndpointDesc { return ei } +type libusbHotplugCallback func(*libusbContext, *libusbDevice, HotplugEventType) + // libusbIntf is a set of trivial idiomatic Go wrappers around libusb C functions. // The underlying code is generally not testable or difficult to test, // since libusb interacts directly with the host USB stack. @@ -165,6 +179,9 @@ type libusbIntf interface { data(*libusbTransfer) (int, TransferStatus) free(*libusbTransfer) setIsoPacketLengths(*libusbTransfer, uint32) + + // hotplug + registerHotplugCallback(ctx *libusbContext, events HotplugEventType, enumerate bool, vendorID int32, productID int32, devClass int32, fn libusbHotplugCallback) (func(), error) } // libusbImpl is an implementation of libusbIntf using real CGo-wrapped libusb. @@ -215,6 +232,17 @@ func (libusbImpl) getDevices(ctx *libusbContext) ([]*libusbDevice, error) { func (libusbImpl) exit(c *libusbContext) error { C.libusb_exit((*C.libusb_context)(c)) + // libusb_exit automatically deregisters hotplug callbacks, + // but we need to free the callback map. + hotplugCallbackMap.Lock() + if m, ok := hotplugCallbackMap.m[c]; ok { + for id := range m { + C.free(id) + delete(m, id) + } + } + delete(hotplugCallbackMap.m, c) + hotplugCallbackMap.Unlock() return nil } @@ -509,3 +537,81 @@ func newDevicePointer() *libusbDevice { func newFakeTransferPointer() *libusbTransfer { return (*libusbTransfer)(unsafe.Pointer(C.malloc(1))) } + +// hotplugCallbackMap keeps a map of go callback functions for libusb hotplug callbacks +// for each context. +// When a context is closed with libusb_exit, its callbacks are automatically deregistered +// by libusb, and they are removed from this map too. +var hotplugCallbackMap = struct { + m map[*libusbContext]map[unsafe.Pointer]libusbHotplugCallback + sync.RWMutex +}{ + m: make(map[*libusbContext]map[unsafe.Pointer]libusbHotplugCallback), +} + +//export goHotplugCallback +func goHotplugCallback(ctx *C.libusb_context, device *C.libusb_device, event C.libusb_hotplug_event, userData unsafe.Pointer) C.int { + var fn libusbHotplugCallback + hotplugCallbackMap.RLock() + m, ok := hotplugCallbackMap.m[(*libusbContext)(ctx)] + if ok { + fn, ok = m[userData] + } + hotplugCallbackMap.RUnlock() + if !ok { + // This shouldn't happen. Deregister the callback. + return 1 + } + fn((*libusbContext)(ctx), (*libusbDevice)(device), HotplugEventType(event)) + + // We don't support deregistering by returning 1 in the callback. + // The callback function can just call deregister directly. + return 0 +} + +func (libusbImpl) registerHotplugCallback(ctx *libusbContext, events HotplugEventType, enumerate bool, vendorID int32, productID int32, devClass int32, fn libusbHotplugCallback) (func(), error) { + // We must allocate memory to pass to C, since we can't pass a go pointer. + // We can use the resulting pointer as a map key instead of + // storing the map key inside the memory allocated. + id := C.malloc(1) + if id == nil { + panic(errors.New("Failed to allocate memory during callback registration")) + } + hotplugCallbackMap.Lock() + m, ok := hotplugCallbackMap.m[ctx] + if !ok { + hotplugCallbackMap.m[ctx] = make(map[unsafe.Pointer]libusbHotplugCallback) + m = hotplugCallbackMap.m[ctx] + } + m[id] = fn + hotplugCallbackMap.Unlock() + + var flags C.libusb_hotplug_flag = C.LIBUSB_HOTPLUG_NO_FLAGS + if enumerate { + flags = C.LIBUSB_HOTPLUG_ENUMERATE + } + var handle C.libusb_hotplug_callback_handle + + // TODO: figure out how to run deregister in callback. + // There's a race condition here, because the callback may be called before + // gousb_hotplug_register_callback returns, depending on libusb's implementation. + res := C.gousb_hotplug_register_callback((*C.libusb_context)(ctx), C.libusb_hotplug_event(events), flags, C.int(vendorID), C.int(productID), C.int(devClass), id, &handle) + err := fromErrNo(res) + if err != nil { + hotplugCallbackMap.Lock() + delete(hotplugCallbackMap.m[ctx], id) + hotplugCallbackMap.Unlock() + C.free(id) + return nil, err + } + + return func() { + C.libusb_hotplug_deregister_callback((*C.libusb_context)(ctx), handle) + hotplugCallbackMap.Lock() + m, ok := hotplugCallbackMap.m[ctx] + if ok { + delete(m, id) + } + hotplugCallbackMap.Unlock() + }, nil +} From a624d6a9ca3898fdb11dfb543127e78c86d67501 Mon Sep 17 00:00:00 2001 From: Nikola Kovacs Date: Sun, 18 Mar 2018 09:38:11 +0100 Subject: [PATCH 2/3] Hotplug support, take two Remove filtering, it's better to do it in the callback with simple ifs. Enumerate in go code to properly support cancellation and distinguish enumerated devices. Add support for deregistering from callback. Fix memory leak. --- constants.go | 2 +- hotplug.go | 183 +++++++++++++++++++-------------------------------- libusb.go | 19 ++++-- 3 files changed, 84 insertions(+), 120 deletions(-) diff --git a/constants.go b/constants.go index fdcca59..760640d 100644 --- a/constants.go +++ b/constants.go @@ -267,4 +267,4 @@ const ( HotplugEventAny HotplugEventType = HotplugEventDeviceArrived | HotplugEventDeviceLeft ) -const HotplugMatchAny = C.LIBUSB_HOTPLUG_MATCH_ANY +const hotplugMatchAny = C.LIBUSB_HOTPLUG_MATCH_ANY diff --git a/hotplug.go b/hotplug.go index cca3cd3..4c6cf82 100644 --- a/hotplug.go +++ b/hotplug.go @@ -15,132 +15,77 @@ package gousb -// HotplugOptions can be used to configure and register a hotplug event callback. -// By default, the callback will be called when a new device arrives -// or a device leaves. -// Call Arrived to only receive HotplugEventDeviceArrived events, -// and Left to only receive HotplugEventDeviceLeft events. -// Call Enumerate to also receive HotplugEventDeviceArrived events for -// devices that are already plugged in. -// Use ProductID, VendorID and DeviceClass to filter by product id, vendor id -// or device class. -// Call Register to register the callback function. The options cannot -// be changed after registering. The returned HotplugCallback -// can be used to deregister the callback. -// Callbacks are automatically deregistered when the Context is closed. -type HotplugOptions interface { - Arrived() HotplugOptions - Left() HotplugOptions - Enumerate() HotplugOptions - ProductID(ID) HotplugOptions - VendorID(ID) HotplugOptions - DeviceClass(Class) HotplugOptions - Register(func(HotplugEvent)) (HotplugCallback, error) -} - -// HotplugCallback is a registered hotplug callback. -// It only method is Deregister, which can be used to deregister the callback. -// It is safe to call deregister multiple times. -// Callbacks are also automatically deregistered when the Context is closed. -type HotplugCallback interface { - Deregister() -} - -// HotplugEvent is a hotplug event. -// Its methods should only be called during the event callback. type HotplugEvent interface { // Type returns the event's type (HotplugEventDeviceArrived or HotplugEventDeviceLeft). Type() HotplugEventType + // IsEnumerated returns true if the device was already plugged in when the callback was registered. + IsEnumerated() bool // DeviceDesc returns the device's descriptor. DeviceDesc() (*DeviceDesc, error) // Open opens the device. Open() (*Device, error) + // Deregister deregisters the callback registration after the callback function returns. + Deregister() } -type hotplugOptions struct { - ctx *Context - events HotplugEventType - enumerate bool - productID int32 - vendorID int32 - devClass int32 -} - -type hotplugEvent struct { - eventType HotplugEventType - ctx *Context - dev *libusbDevice - desc *DeviceDesc - err error -} - -type hotplugCallback struct { - fn func() -} - -func (c *hotplugCallback) Deregister() { - c.fn() -} - -// Hotplug sets up a hotplug event callback. -// It returns a HotplugOptions that can be used to configure and register -// the callback. -func (c *Context) Hotplug() HotplugOptions { - return &hotplugOptions{ - ctx: c, - productID: HotplugMatchAny, - vendorID: HotplugMatchAny, - devClass: HotplugMatchAny, +// RegisterHotplug registers a hotplug callback function. +// The callback will receive arrive events for all currently plugged in devices. +// These events will return true from IsEnumerated(). +// Note that events are delivered concurrently. You may receive arrive and leave events +// concurrently with enumerated arrive events. You may also receive arrive events twice +// for the same device, and you may receive a leave event for a device for which +// you never received an arrive event. +func (c *Context) RegisterHotplug(fn func(HotplugEvent)) (func(), error) { + dereg, err := c.libusb.registerHotplugCallback(c.ctx, HotplugEventAny, false, hotplugMatchAny, hotplugMatchAny, hotplugMatchAny, func(ctx *libusbContext, dev *libusbDevice, eventType HotplugEventType) bool { + desc, err := c.libusb.getDeviceDesc(dev) + e := &hotplugEvent{ + eventType: eventType, + ctx: c, + dev: dev, + desc: desc, + err: err, + enumerated: false, + } + fn(e) + return e.deregister + }) + if err != nil { + return nil, err } + // enumerate devices + // this is done in gousb to properly support cancellation and to distinguish enumerated devices + list, err := c.libusb.getDevices(c.ctx) + if err != nil { + dereg() + return nil, err + } + for _, dev := range list { + desc, err := c.libusb.getDeviceDesc(dev) + e := &hotplugEvent{ + eventType: HotplugEventDeviceArrived, + ctx: c, + dev: dev, + desc: desc, + err: err, + enumerated: true, + } + fn(e) + if e.deregister { + dereg() + break + } + } + return dereg, nil } -func (h *hotplugOptions) Arrived() HotplugOptions { - h.events |= HotplugEventDeviceArrived - return h -} - -func (h *hotplugOptions) Left() HotplugOptions { - h.events |= HotplugEventDeviceLeft - return h -} - -func (h *hotplugOptions) Enumerate() HotplugOptions { - h.enumerate = true - return h -} - -func (h *hotplugOptions) ProductID(pid ID) HotplugOptions { - h.productID = int32(pid) - return h -} - -func (h *hotplugOptions) VendorID(vid ID) HotplugOptions { - h.vendorID = int32(vid) - return h -} - -func (h *hotplugOptions) DeviceClass(devClass Class) HotplugOptions { - h.devClass = int32(devClass) - return h -} - -func (h *hotplugOptions) Register(fn func(HotplugEvent)) (HotplugCallback, error) { - if h.events == 0 { - h.events = HotplugEventAny - } - dereg, err := h.ctx.libusb.registerHotplugCallback(h.ctx.ctx, h.events, h.enumerate, h.vendorID, h.productID, h.devClass, func(ctx *libusbContext, dev *libusbDevice, eventType HotplugEventType) { - desc, err := h.ctx.libusb.getDeviceDesc(dev) - fn(&hotplugEvent{ - eventType: eventType, - ctx: h.ctx, - dev: dev, - desc: desc, - err: err, - }) - }) - return &hotplugCallback{ - fn: dereg, - }, err +type hotplugEvent struct { + eventType HotplugEventType + ctx *Context + dev *libusbDevice + desc *DeviceDesc + err error + enumerated bool + deregister bool } // Type returns the event's type (HotplugEventDeviceArrived or HotplugEventDeviceLeft). @@ -153,6 +98,11 @@ func (e *hotplugEvent) DeviceDesc() (*DeviceDesc, error) { return e.desc, e.err } +// IsEnumerated returns true if the device was already plugged in when the callback was registered. +func (e *hotplugEvent) IsEnumerated() bool { + return e.enumerated +} + // Open opens the device. func (e *hotplugEvent) Open() (*Device, error) { if e.err != nil { @@ -164,3 +114,8 @@ func (e *hotplugEvent) Open() (*Device, error) { } return &Device{handle: handle, ctx: e.ctx, Desc: e.desc}, nil } + +// Deregister deregisters the callback registration after the callback function returns. +func (e *hotplugEvent) Deregister() { + e.deregister = true +} diff --git a/libusb.go b/libusb.go index 378c27d..9d337da 100644 --- a/libusb.go +++ b/libusb.go @@ -136,7 +136,7 @@ func (ep libusbEndpoint) endpointDesc(dev *DeviceDesc) EndpointDesc { return ei } -type libusbHotplugCallback func(*libusbContext, *libusbDevice, HotplugEventType) +type libusbHotplugCallback func(*libusbContext, *libusbDevice, HotplugEventType) bool // libusbIntf is a set of trivial idiomatic Go wrappers around libusb C functions. // The underlying code is generally not testable or difficult to test, @@ -237,8 +237,8 @@ func (libusbImpl) exit(c *libusbContext) error { hotplugCallbackMap.Lock() if m, ok := hotplugCallbackMap.m[c]; ok { for id := range m { - C.free(id) delete(m, id) + C.free(id) } } delete(hotplugCallbackMap.m, c) @@ -562,10 +562,18 @@ func goHotplugCallback(ctx *C.libusb_context, device *C.libusb_device, event C.l // This shouldn't happen. Deregister the callback. return 1 } - fn((*libusbContext)(ctx), (*libusbDevice)(device), HotplugEventType(event)) + dereg := fn((*libusbContext)(ctx), (*libusbDevice)(device), HotplugEventType(event)) - // We don't support deregistering by returning 1 in the callback. - // The callback function can just call deregister directly. + if dereg { + hotplugCallbackMap.Lock() + m, ok := hotplugCallbackMap.m[(*libusbContext)(ctx)] + if ok { + delete(m, userData) + C.free(userData) + } + hotplugCallbackMap.Unlock() + return 1 + } return 0 } @@ -611,6 +619,7 @@ func (libusbImpl) registerHotplugCallback(ctx *libusbContext, events HotplugEven m, ok := hotplugCallbackMap.m[ctx] if ok { delete(m, id) + C.free(id) } hotplugCallbackMap.Unlock() }, nil From 26ac0676799874fb322998e96e39c51e57bf8fca Mon Sep 17 00:00:00 2001 From: Nikola Kovacs Date: Sun, 18 Mar 2018 11:21:47 +0100 Subject: [PATCH 3/3] Don't use LIBUSB_HOTPLUG_NO_FLAGS, use zero value LIBUSB_HOTPLUG_NO_FLAGS was added later, it does not exist in libusb 1.0.16. Its value is zero, so just use the zero value. --- libusb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libusb.go b/libusb.go index 9d337da..b6becf0 100644 --- a/libusb.go +++ b/libusb.go @@ -594,7 +594,7 @@ func (libusbImpl) registerHotplugCallback(ctx *libusbContext, events HotplugEven m[id] = fn hotplugCallbackMap.Unlock() - var flags C.libusb_hotplug_flag = C.LIBUSB_HOTPLUG_NO_FLAGS + var flags C.libusb_hotplug_flag if enumerate { flags = C.LIBUSB_HOTPLUG_ENUMERATE }