Skip to content

Commit 71016aa

Browse files
committed
Stop holding refs in hotplug
1 parent 6fbfc4a commit 71016aa

4 files changed

Lines changed: 133 additions & 70 deletions

File tree

‎Cargo.lock‎

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ futures-lite = "2.6.1"
1313
napi = { version = "3.0.0", features = ["tokio_rt"] }
1414
napi-derive = "3.0.0"
1515
nusb = "=0.2.1"
16-
tokio = { version = "1", features = ["rt", "time", "fs"] }
16+
tokio = { version = "1", features = ["rt", "time", "fs", "macros"] }
1717

1818
[build-dependencies]
1919
napi-build = "2"

‎src/lib.rs‎

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,63 +6,106 @@ use futures_lite::StreamExt;
66
use napi::{bindgen_prelude::*, threadsafe_function::ThreadsafeFunction, threadsafe_function::ThreadsafeFunctionCallMode};
77
use napi_derive::napi;
88
use nusb::{hotplug::HotplugEvent, MaybeFuture};
9+
use std::sync::Arc;
10+
use tokio::sync::{watch, RwLock};
911
use webusb_device::UsbDevice;
1012

13+
struct Callbacks {
14+
attach: Option<ThreadsafeFunction<UsbDevice, (), UsbDevice, napi::Status, false>>,
15+
detach: Option<ThreadsafeFunction<String, (), String, napi::Status, false>>,
16+
}
17+
1118
#[napi]
1219
pub struct Emitter {
13-
attachCallback: Option<ThreadsafeFunction<UsbDevice, (), UsbDevice, napi::Status, false>>,
14-
detachCallback: Option<ThreadsafeFunction<String, (), String, napi::Status, false>>,
20+
callbacks: Arc<RwLock<Callbacks>>,
21+
listeners_tx: watch::Sender<bool>,
1522
}
1623

1724
#[napi]
1825
impl Emitter {
1926
#[napi(constructor)]
2027
pub fn new() -> Self {
28+
let callbacks = Arc::new(RwLock::new(Callbacks { attach: None, detach: None }));
29+
let (listeners_tx, _listeners_rx) = watch::channel(false);
2130
Self {
22-
attachCallback: None,
23-
detachCallback: None,
31+
callbacks,
32+
listeners_tx,
2433
}
2534
}
2635

2736
#[napi]
28-
pub async unsafe fn start(&self) {
29-
let mut watch = nusb::watch_devices().unwrap();
30-
while let Some(event) = watch.next().await {
31-
match event {
32-
HotplugEvent::Connected(info) => {
33-
if let Some(callback) = &self.attachCallback {
34-
let device = UsbDevice::new(info);
35-
callback.call(device, ThreadsafeFunctionCallMode::NonBlocking);
36-
}
37+
pub async fn init(&self) {
38+
let callbacks = self.callbacks.clone();
39+
let mut listeners_rx = self.listeners_tx.subscribe();
40+
41+
tokio::spawn(async move {
42+
loop {
43+
while !*listeners_rx.borrow() {
44+
// Wait for an event to be attached
3745
}
38-
HotplugEvent::Disconnected(id) => {
39-
if let Some(callback) = &self.detachCallback {
40-
let handle = format!("{:?}", id);
41-
callback.call(handle, ThreadsafeFunctionCallMode::NonBlocking);
46+
47+
let mut watch_stream = nusb::watch_devices().unwrap();
48+
loop {
49+
tokio::select! {
50+
_ = listeners_rx.changed() => {
51+
if !*listeners_rx.borrow() {
52+
// No listeners attached, stop watching for device events
53+
break;
54+
}
55+
}
56+
ev = watch_stream.next() => {
57+
match ev {
58+
Some(HotplugEvent::Connected(info)) => {
59+
let guard = callbacks.read().await;
60+
if let Some(cb) = guard.attach.as_ref() {
61+
cb.call(UsbDevice::new(info), ThreadsafeFunctionCallMode::NonBlocking);
62+
}
63+
}
64+
Some(HotplugEvent::Disconnected(id)) => {
65+
let guard = callbacks.read().await;
66+
if let Some(cb) = guard.detach.as_ref() {
67+
cb.call(format!("{:?}", id), ThreadsafeFunctionCallMode::NonBlocking);
68+
}
69+
}
70+
None => break,
71+
}
72+
}
4273
}
4374
}
4475
}
45-
}
76+
});
4677
}
4778

4879
#[napi]
49-
pub fn addAttach(&mut self, callback: ThreadsafeFunction<UsbDevice, (), UsbDevice, napi::Status, false>) {
50-
self.attachCallback = Some(callback);
80+
pub async unsafe fn addAttach(&mut self, callback: ThreadsafeFunction<UsbDevice, (), UsbDevice, napi::Status, false>) {
81+
{ self.callbacks.write().await.attach = Some(callback); }
82+
self.publishState().await;
5183
}
5284

5385
#[napi]
54-
pub fn removeAttach(&mut self, _callback: ThreadsafeFunction<UsbDevice, (), UsbDevice, napi::Status, false>) {
55-
self.attachCallback = None;
86+
pub async unsafe fn removeAttach(&mut self) {
87+
{ self.callbacks.write().await.attach = None; }
88+
self.publishState().await;
5689
}
5790

5891
#[napi]
59-
pub fn addDetach(&mut self, callback: ThreadsafeFunction<String, (), String, napi::Status, false>) {
60-
self.detachCallback = Some(callback);
92+
pub async unsafe fn addDetach(&mut self, callback: ThreadsafeFunction<String, (), String, napi::Status, false>) {
93+
{ self.callbacks.write().await.detach = Some(callback); }
94+
self.publishState().await;
6195
}
6296

6397
#[napi]
64-
pub fn removeDetach(&mut self, _callback: ThreadsafeFunction<String, (), String, napi::Status, false>) {
65-
self.detachCallback = None;
98+
pub async unsafe fn removeDetach(&mut self) {
99+
{ self.callbacks.write().await.detach = None; }
100+
self.publishState().await;
101+
}
102+
103+
async fn publishState(&self) {
104+
let listeners = {
105+
let cb = self.callbacks.read().await;
106+
cb.attach.is_some() || cb.detach.is_some()
107+
};
108+
let _ = self.listeners_tx.send(listeners);
66109
}
67110
}
68111

‎tsc/index.ts‎

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -122,29 +122,6 @@ class ConnectionEvent extends Event implements USBConnectionEvent {
122122
}
123123
}
124124

125-
interface WebUSB {
126-
addEventListener(
127-
type: "connect" | "disconnect",
128-
listener: (this: this, ev: USBConnectionEvent) => any,
129-
useCapture?: boolean,
130-
): void;
131-
addEventListener(
132-
type: string,
133-
listener: EventListenerOrEventListenerObject | null,
134-
options?: boolean | AddEventListenerOptions,
135-
): void;
136-
removeEventListener(
137-
type: "connect" | "disconnect",
138-
callback: (this: this, ev: USBConnectionEvent) => any,
139-
useCapture?: boolean,
140-
): void;
141-
removeEventListener(
142-
type: string,
143-
callback: EventListenerOrEventListenerObject | null,
144-
options?: EventListenerOptions | boolean,
145-
): void;
146-
}
147-
148125
/**
149126
* WebUSB class
150127
*
@@ -159,35 +136,66 @@ class WebUSB extends EventTarget implements USB {
159136

160137
protected nativeEmitter = new Emitter();
161138
protected authorisedDevices = new Set<USBDeviceFilter>();
139+
protected listenerCount = 0;
162140
protected knownDevices: Map<string, UsbDevice> = new Map();
163141

164142
constructor(private options: USBOptions = {}) {
165143
super();
144+
this.nativeEmitter.init();
145+
}
166146

167-
const deviceConnectCallback = async (device: UsbDevice) => {
168-
this.knownDevices.set(device.handle, device);
147+
private deviceConnectCallback = async (device: UsbDevice) => {
148+
this.knownDevices.set(device.handle, device);
169149

170-
// When connected, emit an event if it is an allowed device
150+
// When connected, emit an event if it is an allowed device
151+
if (device && this.isAuthorisedDevice(device)) {
152+
this.dispatchEvent(new ConnectionEvent('connect', { device }));
153+
}
154+
};
155+
156+
private deviceDisconnectCallback = async (handle: string) => {
157+
// When disconnected, emit an event if the device was a known allowed device
158+
if (this.knownDevices.has(handle)) {
159+
const device = this.knownDevices.get(handle)!;
171160
if (device && this.isAuthorisedDevice(device)) {
172-
this.dispatchEvent(new ConnectionEvent('connect', { device }));
173-
}
174-
};
175-
176-
const deviceDisconnectCallback = async (handle: string) => {
177-
// When disconnected, emit an event if the device was a known allowed device
178-
if (this.knownDevices.has(handle)) {
179-
const device = this.knownDevices.get(handle)!;
180-
if (device && this.isAuthorisedDevice(device)) {
181-
this.dispatchEvent(new ConnectionEvent('disconnect', { device }));
182-
}
183-
this.knownDevices.delete(handle);
161+
this.dispatchEvent(new ConnectionEvent('disconnect', { device }));
184162
}
185-
};
163+
this.knownDevices.delete(handle);
164+
}
165+
};
166+
167+
public addEventListener(type: 'connect' | 'disconnect', listener: (this: this, ev: USBConnectionEvent) => void): void;
168+
public addEventListener(type: 'connect' | 'disconnect', listener: EventListener): void;
169+
public addEventListener(type: string, listener: (ev: USBConnectionEvent) => void): void {
170+
if (type !== 'connect' && type !== 'disconnect') {
171+
return;
172+
}
186173

187-
this.nativeEmitter.start();
188-
this.nativeEmitter.addAttach(deviceConnectCallback);
189-
this.nativeEmitter.addDetach(deviceDisconnectCallback);
190-
nativeGetDevices().then(devices => devices.forEach(device => this.knownDevices.set(device.handle, device)));
174+
super.addEventListener(type, listener as any);
175+
this.listenerCount++;
176+
177+
if (this.listenerCount === 1) {
178+
this.nativeEmitter.addAttach(this.deviceConnectCallback);
179+
this.nativeEmitter.addDetach(this.deviceDisconnectCallback);
180+
nativeGetDevices().then(devices => devices.forEach(device => this.knownDevices.set(device.handle, device)));
181+
}
182+
}
183+
184+
public removeEventListener(type: 'connect' | 'disconnect', callback: (this: this, ev: USBConnectionEvent) => void): void;
185+
public removeEventListener(type: 'connect' | 'disconnect', callback: EventListener): void;
186+
public removeEventListener(type: string, callback: (this: this, ev: USBConnectionEvent) => void): void {
187+
if (type !== 'connect' && type !== 'disconnect') {
188+
return;
189+
}
190+
191+
super.removeEventListener(type, callback as any);
192+
this.listenerCount--;
193+
194+
if (this.listenerCount === 0) {
195+
this.nativeEmitter.removeAttach();
196+
this.nativeEmitter.removeDetach();
197+
this.knownDevices.clear();
198+
}
191199
}
192200

193201
private _onconnect: ((ev: USBConnectionEvent) => void) | undefined;

0 commit comments

Comments
 (0)