diff --git a/constants.go b/constants.go index c2061c3..760640d 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..4c6cf82 --- /dev/null +++ b/hotplug.go @@ -0,0 +1,121 @@ +// 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 + +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() +} + +// 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 +} + +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). +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 +} + +// 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 { + 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 +} + +// 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 2c533a1..b6becf0 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) 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, // 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 { + delete(m, id) + C.free(id) + } + } + delete(hotplugCallbackMap.m, c) + hotplugCallbackMap.Unlock() return nil } @@ -509,3 +537,90 @@ 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 + } + dereg := fn((*libusbContext)(ctx), (*libusbDevice)(device), HotplugEventType(event)) + + if dereg { + hotplugCallbackMap.Lock() + m, ok := hotplugCallbackMap.m[(*libusbContext)(ctx)] + if ok { + delete(m, userData) + C.free(userData) + } + hotplugCallbackMap.Unlock() + return 1 + } + 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 + 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) + C.free(id) + } + hotplugCallbackMap.Unlock() + }, nil +}