hammerspoon/extensions/usb/libusb_watcher.m

324 lines
11 KiB
Objective-C

#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#import <IOKit/IOKitLib.h>
#import <IOKit/IOMessage.h>
#import <IOKit/IOCFPlugIn.h>
#import <IOKit/usb/IOUSBLib.h>
#import <LuaSkin/LuaSkin.h>
/// === hs.usb.watcher ===
///
/// Watch for USB device connection/disconnection events
// Common Code
#define USERDATA_TAG "hs.usb.watcher"
static LSRefTable refTable;
// Not so common code
// userdata object for each watcher
typedef struct _usbwatcher_t {
bool running;
bool isFirstRun;
int fn;
IONotificationPortRef gNotifyPort;
io_iterator_t gAddedIter;
CFRunLoopSourceRef runLoopSource;
LSGCCanary lsCanary;
} usbwatcher_t;
// private data for each USB device
typedef struct _usbprivdata_t {
usbwatcher_t *watcher;
io_object_t notification;
char *productName;
char *vendorName;
int productID;
int vendorID;
} usbprivdata_t;
// Process an IOKit notification, discarding it if it's not about a device being removed
void DeviceNotification(void *refCon, io_service_t service __unused, natural_t messageType, void *messageArgument __unused) {
usbprivdata_t *privateDataRef = (usbprivdata_t *)refCon;
usbwatcher_t *watcher = privateDataRef->watcher;
if (messageType == kIOMessageServiceIsTerminated) {
LuaSkin *skin = [LuaSkin sharedWithState:NULL];
lua_State *L = skin.L;
if (![skin checkGCCanary:watcher->lsCanary]) {
return;
}
_lua_stackguard_entry(L);
[skin pushLuaRef:refTable ref:watcher->fn];
// Prepare the callback's argument table
lua_newtable(L);
lua_pushstring(L, "productName");
lua_pushstring(L, privateDataRef->productName);
lua_settable(L, -3);
lua_pushstring(L, "vendorName");
lua_pushstring(L, privateDataRef->vendorName);
lua_settable(L, -3);
lua_pushstring(L, "productID");
lua_pushinteger(L, privateDataRef->productID);
lua_settable(L, -3);
lua_pushstring(L, "vendorID");
lua_pushinteger(L, privateDataRef->vendorID);
lua_settable(L, -3);
lua_pushstring(L, "eventType");
lua_pushstring(L, "removed");
lua_settable(L, -3);
// Call the callback
[skin protectedCallAndError:@"hs.usb.watcher:removed callback" nargs:1 nresults:0];
// Free the USB private data
if (privateDataRef) {
IOObjectRelease(privateDataRef->notification);
}
if (privateDataRef->productName) {
free(privateDataRef->productName);
privateDataRef->productName = NULL;
}
if (privateDataRef->vendorName) {
free(privateDataRef->vendorName);
privateDataRef->vendorName = NULL;
}
if (privateDataRef) {
free(privateDataRef);
privateDataRef = NULL;
}
_lua_stackguard_exit(L);
}
}
// Iterate over new devices
void DeviceAdded(void *refCon, io_iterator_t iterator) {
LuaSkin *skin = [LuaSkin sharedWithState:NULL];
lua_State *L = skin.L;
_lua_stackguard_entry(L);
usbwatcher_t *watcher = (usbwatcher_t *)refCon;
kern_return_t kr;
io_service_t usbDevice;
CFMutableDictionaryRef deviceData;
NSString *productName;
NSString *vendorName;
int length;
while ((usbDevice = IOIteratorNext(iterator))) {
// Prepare an object to store private data about this USB device
usbprivdata_t *privateDataRef = NULL;
privateDataRef = malloc(sizeof(usbprivdata_t));
bzero(privateDataRef, sizeof(usbprivdata_t));
privateDataRef->watcher = watcher;
// Fetch the IOKit properties for this device
IORegistryEntryCreateCFProperties(usbDevice, &deviceData, kCFAllocatorDefault, kNilOptions);
// Extract the USB device's name
productName = (__bridge NSString *)CFDictionaryGetValue(deviceData, CFSTR(kUSBProductString));
length = (int)[productName length] + 1;
privateDataRef->productName = malloc(length);
if (![productName getCString:privateDataRef->productName maxLength:length encoding:NSUTF8StringEncoding]) {
privateDataRef->productName[0] = '\0';
}
// Extract the USB device's vendor's name
vendorName = (__bridge NSString *)CFDictionaryGetValue(deviceData, CFSTR(kUSBVendorString));
length = (int)[vendorName length] + 1;
privateDataRef->vendorName = malloc(length);
if (![vendorName getCString:privateDataRef->vendorName maxLength:length encoding:NSUTF8StringEncoding]) {
privateDataRef->vendorName[0] = '\0';
}
// Extract the USB device's product/vendor IDs
privateDataRef->productID = [(__bridge NSNumber *)CFDictionaryGetValue(deviceData, CFSTR(kUSBProductID)) intValue];
privateDataRef->vendorID = [(__bridge NSNumber *)CFDictionaryGetValue(deviceData, CFSTR(kUSBVendorID)) intValue];
// Register for notifications relating to this device
kr = IOServiceAddInterestNotification(watcher->gNotifyPort, usbDevice, kIOGeneralInterest, DeviceNotification, privateDataRef, &(privateDataRef->notification));
if (KERN_SUCCESS != kr) {
[skin logBreadcrumb:[NSString stringWithFormat:@"IOServiceAddInterestNotification returned 0x%08x", kr]];
}
// Release data we don't need anymore
CFRelease(deviceData);
IOObjectRelease(usbDevice);
// We don't want to trigger callbacks for every device attached before the watcher starts, but we needed to enumerate them to get private device data cached
if (!watcher->isFirstRun && watcher->fn != LUA_REFNIL && watcher->fn != LUA_NOREF) {
[skin pushLuaRef:refTable ref:watcher->fn];
lua_newtable(L);
lua_pushstring(L, "productName");
lua_pushstring(L, privateDataRef->productName);
lua_settable(L, -3);
lua_pushstring(L, "vendorName");
lua_pushstring(L, privateDataRef->vendorName);
lua_settable(L, -3);
lua_pushstring(L, "productID");
lua_pushinteger(L, privateDataRef->productID);
lua_settable(L, -3);
lua_pushstring(L, "vendorID");
lua_pushinteger(L, privateDataRef->vendorID);
lua_settable(L, -3);
lua_pushstring(L, "eventType");
lua_pushstring(L, "added");
lua_settable(L, -3);
[skin protectedCallAndError:@"hs.usb.watcher:added callback" nargs:1 nresults:0];
}
}
_lua_stackguard_exit(L);
}
/// hs.usb.watcher.new(fn) -> watcher
/// Constructor
/// Creates a new watcher for USB device events
///
/// Parameters:
/// * fn - A function that will be called when a USB device is inserted or removed. The function should accept a single parameter, which is a table containing the following keys:
/// * eventType - A string containing either "added" or "removed" depending on whether the USB device was connected or disconnected
/// * productName - A string containing the name of the device
/// * vendorName - A string containing the name of the device vendor
/// * vendorID - A number containing the Vendor ID of the device
/// * productID - A number containing the Product ID of the device
///
/// Returns:
/// * A `hs.usb.watcher` object
static int usb_watcher_new(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
luaL_checktype(L, 1, LUA_TFUNCTION);
usbwatcher_t* usbwatcher = lua_newuserdata(L, sizeof(usbwatcher_t));
memset(usbwatcher, 0, sizeof(usbwatcher_t));
lua_pushvalue(L, 1);
usbwatcher->fn = [skin luaRef:refTable];
usbwatcher->running = NO;
usbwatcher->gNotifyPort = IONotificationPortCreate(kIOMasterPortDefault);
usbwatcher->runLoopSource = IONotificationPortGetRunLoopSource(usbwatcher->gNotifyPort);
usbwatcher->lsCanary = [skin createGCCanary];
luaL_getmetatable(L, USERDATA_TAG);
lua_setmetatable(L, -2);
return 1;
}
/// hs.usb.watcher:start() -> watcher
/// Method
/// Starts the USB watcher
///
/// Parameters:
/// * None
///
/// Returns:
/// * The `hs.usb.watcher` object
static int usb_watcher_start(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
usbwatcher_t* usbwatcher = luaL_checkudata(L, 1, USERDATA_TAG);
lua_settop(L,1) ;
if (usbwatcher->running) return 1;
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
if (!matchingDict) {
[skin logBreadcrumb:@"Unable to create USB watcher matching dictionary"];
return 1;
}
usbwatcher->running = YES;
usbwatcher->isFirstRun = YES;
CFRunLoopAddSource(CFRunLoopGetCurrent(), usbwatcher->runLoopSource, kCFRunLoopDefaultMode);
if (KERN_SUCCESS == IOServiceAddMatchingNotification(usbwatcher->gNotifyPort,
kIOFirstMatchNotification,
matchingDict,
DeviceAdded,
usbwatcher,
&usbwatcher->gAddedIter)) {
DeviceAdded(usbwatcher, usbwatcher->gAddedIter);
usbwatcher->isFirstRun = NO;
}
return 1;
}
/// hs.usb.watcher:stop() -> watcher
/// Method
/// Stops the USB watcher
///
/// Parameters:
/// * None
///
/// Returns:
/// * The `hs.usb.watcher` object
static int usb_watcher_stop(lua_State* L) {
usbwatcher_t* usbwatcher = luaL_checkudata(L, 1, USERDATA_TAG);
lua_settop(L,1) ;
if (!usbwatcher->running) return 1;
usbwatcher->running = NO;
IOObjectRelease(usbwatcher->gAddedIter);
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), usbwatcher->runLoopSource, kCFRunLoopDefaultMode);
return 1;
}
static int usb_watcher_gc(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
usbwatcher_t* usbwatcher = luaL_checkudata(L, 1, USERDATA_TAG);
lua_pushcfunction(L, usb_watcher_stop) ; lua_pushvalue(L,1); lua_call(L, 1, 1);
usbwatcher->fn = [skin luaUnref:refTable ref:usbwatcher->fn];
[skin destroyGCCanary:&(usbwatcher->lsCanary)];
IONotificationPortDestroy(usbwatcher->gNotifyPort);
return 0;
}
static int meta_gc(lua_State* __unused L) {
return 0;
}
static int userdata_tostring(lua_State* L) {
lua_pushstring(L, [[NSString stringWithFormat:@"%s: (%p)", USERDATA_TAG, lua_topointer(L, 1)] UTF8String]) ;
return 1 ;
}
// Metatable for created objects when _new invoked
static const luaL_Reg usb_metalib[] = {
{"start", usb_watcher_start},
{"stop", usb_watcher_stop},
{"__tostring", userdata_tostring},
{"__gc", usb_watcher_gc},
{NULL, NULL}
};
// Functions for returned object when module loads
static const luaL_Reg usbLib[] = {
{"new", usb_watcher_new},
{NULL, NULL}
};
// Metatable for returned object when module loads
static const luaL_Reg meta_gcLib[] = {
{"__gc", meta_gc},
{NULL, NULL}
};
int luaopen_hs_libusbwatcher(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
refTable = [skin registerLibraryWithObject:USERDATA_TAG functions:usbLib metaFunctions:meta_gcLib objectFunctions:usb_metalib];
return 1;
}