Skip to content

Commit 547950d

Browse files
committed
Fix #11 (race condition in Linux)
1 parent 2da89b1 commit 547950d

File tree

7 files changed

+86
-78
lines changed

7 files changed

+86
-78
lines changed

java-does-usb/jextract/README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ SymbolLookup loaderLookup = SymbolLookup.libraryLookup("libudev.so.1", MemorySes
6868

6969
## MacOS
7070

71-
Most of the required native functions on macOS are part of a framework. Frameworks internally have a more complex file organization of header and binary files than appears from the outside. Thus they require a special logic to locate framework header files. *clang* supports it with the `-F`. *jextract* allows to specify the options via `compiler_flags.txt` file. Since the file must be in the local directory and since it does not apply to Linux and Windows, separate directories must be used for the operating systems.
71+
Most of the required native functions on macOS are part of a framework. Frameworks internally have a more complex file organization of header and binary files than appears from the outside. Thus, they require a special logic to locate framework header files. *clang* supports it with the `-F`. *jextract* allows to specify the options via `compiler_flags.txt` file. Since the file must be in the local directory and since it does not apply to Linux and Windows, separate directories must be used for the operating systems.
7272

7373
The generated code has the same problem as the Linux code for *udev*. It must be manually changed to use `SymbolLookup.libraryLookup()` for the libraries `CoreFoundation.framework/CoreFoundation` and `IOKit.framework/IOKit` respectively.
7474

@@ -102,12 +102,12 @@ The worst example is [`IOUSBInterfaceStruct100`](https://github.com/manuelbl/Jav
102102

103103
The table below shows statictics for version 0.2.0 of the library:
104104

105-
| Operating Systems | Manually Created | % | Generated | % | Total | % |
106-
| - | -:| -:| -:| -:| -:| -:|
107-
| Linux | 24,140 | 1.96% | 182,774 | 14.84% | 206,914 | 16.80% |
108-
| macOS | 57,788 | 4.69% | 666,037 | 54.08% | 723,825 | 58.77% |
109-
| Windows | 46,085 | 3.74% | 201,000 | 16.32% | 247,085 | 20.06% |
110-
| Common | 53,774 | 4.37% | 53,774 | 0.00% | | 4.37% |
111-
| Grand Total | 181,787 | 14.76% | 1,049,811 | 85.24% | 1,231,598 | 100.00% |
105+
| Operating Systems | Manually Created | % | Generated | % | Total | % |
106+
|-------------------|-----------------:|-------:|----------:|-------:|----------:|--------:|
107+
| Linux | 24,140 | 1.96% | 182,774 | 14.84% | 206,914 | 16.80% |
108+
| macOS | 57,788 | 4.69% | 666,037 | 54.08% | 723,825 | 58.77% |
109+
| Windows | 46,085 | 3.74% | 201,000 | 16.32% | 247,085 | 20.06% |
110+
| Common | 53,774 | 4.37% | 53,774 | 0.00% | | 4.37% |
111+
| Grand Total | 181,787 | 14.76% | 1,049,811 | 85.24% | 1,231,598 | 100.00% |
112112

113113
*Code Size (compiled), in bytes and percentage of total size*

java-does-usb/jextract/linux/gen_linux.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ JEXTRACT=../../../../jextract/build/jextract/bin/jextract
66
$JEXTRACT --source --output ../../src/main/java \
77
--header-class-name errno \
88
--target-package net.codecrete.usb.linux.gen.errno \
9-
--include-constant ETIMEDOUT \
109
--include-constant EPIPE \
1110
--include-constant EBADF \
1211
--include-constant EAGAIN \
12+
--include-constant EINVAL \
1313
/usr/include/errno.h
1414

1515
# string.h

java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxAsyncTask.java

Lines changed: 62 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@
1515

1616
import java.lang.foreign.Arena;
1717
import java.lang.foreign.MemorySegment;
18-
import java.lang.foreign.SegmentAllocator;
19-
import java.lang.foreign.SegmentScope;
2018
import java.util.ArrayList;
21-
import java.util.HashMap;
19+
import java.util.LinkedHashMap;
2220
import java.util.List;
2321
import java.util.Map;
2422

@@ -58,15 +56,15 @@ static synchronized LinuxAsyncTask instance() {
5856
return singletonInstance;
5957
}
6058

61-
private final SegmentAllocator GLOBAL_ALLOCATOR = SegmentAllocator.nativeAllocator(SegmentScope.global());
59+
private final Arena urbArena = Arena.openShared();
6260
/// available URBs
6361
private final List<MemorySegment> availableURBs = new ArrayList<>();
6462
/// map of URB addresses to transfer (for outstanding transfers)
65-
private final Map<Long, LinuxTransfer> transfersByURB = new HashMap<>();
63+
private final Map<MemorySegment, LinuxTransfer> transfersByURB = new LinkedHashMap<>();
6664
/// array of file descriptors using asynchronous completion
6765
private int[] asyncFds;
6866
/// file descriptor to notify async IO background thread about an update
69-
private int asyncIOUpdateEventFd;
67+
private int asyncIOWakeUpEventFd;
7068

7169
/**
7270
* Background task for handling asynchronous IO completions.
@@ -103,7 +101,8 @@ private void asyncCompletionTask() {
103101
pollfd.revents$set(asyncPolls, i, (short) 0);
104102
}
105103

106-
pollfd.fd$set(asyncPolls, n, asyncIOUpdateEventFd);
104+
// entry n is the wake-up event file descriptor
105+
pollfd.fd$set(asyncPolls, n, asyncIOWakeUpEventFd);
107106
pollfd.events$set(asyncPolls, n, (short) poll.POLLIN());
108107
pollfd.revents$set(asyncPolls, n, (short) 0);
109108

@@ -112,74 +111,79 @@ private void asyncCompletionTask() {
112111
if (res < 0)
113112
throwException("internal error (poll)");
114113

115-
// check for events
116-
for (int i = 0; i < n + 1; i++) {
117-
var revent = pollfd.revents$get(asyncPolls, i);
118-
if (revent == 0)
119-
continue;
114+
// acquire lock
115+
synchronized (this) {
120116

121-
if ((revent & poll.POLLERR()) != 0) {
122-
// most likely the device has been disconnected;
123-
// remove from polled FD list to prevent further problems
124-
int fd = pollfd.fd$get(asyncPolls, i);
125-
removeFdFromAsyncIOCompletion(fd);
117+
if ((pollfd.revents$get(asyncPolls, n) & poll.POLLIN()) != 0) {
118+
// wakeup to refresh list of file descriptors
119+
res = IO.eventfd_read(asyncIOWakeUpEventFd, eventfdValueHolder, errnoState);
120+
if (res < 0)
121+
throwLastError(errnoState, "internal error (eventfd_read)");
126122
continue;
127123
}
128124

129-
if (i != n) {
125+
// check for events
126+
for (int i = 0; i < n + 1; i++) {
127+
var revent = pollfd.revents$get(asyncPolls, i);
128+
if (revent == 0)
129+
continue;
130+
131+
if ((revent & poll.POLLERR()) != 0) {
132+
// most likely the device has been disconnected;
133+
// remove from polled FD list to prevent further problems
134+
int fd = pollfd.fd$get(asyncPolls, i);
135+
removeFdFromAsyncIOCompletion(fd);
136+
continue;
137+
}
138+
130139
// reap URB
131140
int fd = pollfd.fd$get(asyncPolls, i);
132-
reapURB(fd, urbPointerHolder, errnoState);
133-
134-
} else {
135-
// wakeup to refresh list of file descriptors
136-
res = IO.eventfd_read(asyncIOUpdateEventFd, eventfdValueHolder, errnoState);
137-
if (res < 0)
138-
throwLastError(errnoState, "internal error (eventfd_read)");
141+
reapURBs(fd, urbPointerHolder, errnoState);
139142
}
140143
}
141144
}
142145
}
143146
}
144147

145148
/**
146-
* Reap URB and handle the completed transfer.
149+
* Reap all pending URBs and handle the completed transfers.
147150
*
148151
* @param fd file descriptor
149152
* @param urbPointerHolder native memory to receive the URB pointer
150153
* @param errnoState native memory to receive the errno
151154
*/
152-
private void reapURB(int fd, MemorySegment urbPointerHolder, MemorySegment errnoState) {
153-
int res;
154-
res = IO.ioctl(fd, REAPURBNDELAY, urbPointerHolder, errnoState);
155-
if (res < 0) {
156-
var err = Linux.getErrno(errnoState);
157-
if (err == errno.EAGAIN())
158-
return; // retry
159-
if (err == errno.EBADF())
160-
return; // ignore, device might have been closed
161-
throwException(err, "internal error (reap URB)");
162-
}
155+
private void reapURBs(int fd, MemorySegment urbPointerHolder, MemorySegment errnoState) {
156+
while (true) {
157+
int res = IO.ioctl(fd, REAPURBNDELAY, urbPointerHolder, errnoState);
158+
if (res < 0) {
159+
var err = Linux.getErrno(errnoState);
160+
if (err == errno.EAGAIN())
161+
return; // no more pending URBs
162+
if (err == errno.EBADF())
163+
return; // ignore, device might have been closed
164+
throwException(err, "internal error (reap URB)");
165+
}
163166

164-
// call completion handler
165-
var urbAddr = urbPointerHolder.get(JAVA_LONG, 0);
166-
var transfer = getTransferResult(urbAddr);
167-
transfer.completion.completed(transfer);
167+
// call completion handler
168+
var urb = urbPointerHolder.get(ADDRESS, 0);
169+
var transfer = getTransferResult(urb);
170+
transfer.completion.completed(transfer);
171+
}
168172
}
169173

170174
/**
171175
* Notifies background process about changed FD list
172176
*/
173177
private void notifyAsyncIOTask() {
174178
// start background process if needed
175-
if (asyncIOUpdateEventFd == 0) {
179+
if (asyncIOWakeUpEventFd == 0) {
176180
startAsyncIOTask();
177181
return;
178182
}
179183

180184
try (var arena = Arena.openConfined()) {
181185
var errnoState = arena.allocate(Linux.ERRNO_STATE.layout());
182-
if (IO.eventfd_write(asyncIOUpdateEventFd, 1, errnoState) < 0)
186+
if (IO.eventfd_write(asyncIOWakeUpEventFd, 1, errnoState) < 0)
183187
throwLastError(errnoState, "internal error (eventfd_write)");
184188
}
185189
}
@@ -268,15 +272,15 @@ private void addURB(LinuxTransfer transfer) {
268272
if (size > 0) {
269273
urb = availableURBs.remove(size - 1);
270274
} else {
271-
urb = usbdevfs_urb.allocate(GLOBAL_ALLOCATOR);
275+
urb = usbdevfs_urb.allocate(urbArena);
272276
}
273277

274278
transfer.urb = urb;
275-
transfersByURB.put(urb.address(), transfer);
279+
transfersByURB.put(urb, transfer);
276280
}
277281

278-
private synchronized LinuxTransfer getTransferResult(long urbAddr) {
279-
var transfer = transfersByURB.remove(urbAddr);
282+
private synchronized LinuxTransfer getTransferResult(MemorySegment urb) {
283+
var transfer = transfersByURB.remove(urb);
280284
if (transfer == null)
281285
throwException("internal error (unknown URB)");
282286

@@ -291,27 +295,31 @@ private synchronized LinuxTransfer getTransferResult(long urbAddr) {
291295
synchronized void abortTransfers(LinuxUSBDevice device, byte endpointAddress) {
292296
int fd = device.fileDescriptor();
293297
try (var arena = Arena.openConfined()) {
298+
294299
var errnoState = arena.allocate(Linux.ERRNO_STATE.layout());
295300

296-
for (var urbAddress : transfersByURB.keySet()) {
297-
var urb = usbdevfs_urb.ofAddress(MemorySegment.ofAddress(urbAddress), SegmentScope.global());
301+
// iterate all URBs and discard the ones for the specified endpoint
302+
for (var urb : transfersByURB.keySet()) {
298303
if (fd != (int) usbdevfs_urb.usercontext$get(urb).address())
299304
continue;
300305
if (endpointAddress != usbdevfs_urb.endpoint$get(urb))
301306
continue;
302307

303-
if (IO.ioctl(fd, DISCARDURB, urb, errnoState) < 0)
304-
throwLastError(errnoState, "failed to cancel transfer");
308+
if (IO.ioctl(fd, DISCARDURB, urb, errnoState) < 0) {
309+
// ignore EINVAL; it occurs if the URB has completed at the same time
310+
if (Linux.getErrno(errnoState) != errno.EINVAL())
311+
throwLastError(errnoState, "failed to abort transfer");
312+
}
305313
}
306314
}
307315
}
308316

309317
private void startAsyncIOTask() {
310318
try (var arena = Arena.openConfined()) {
311319
var errnoState = arena.allocate(Linux.ERRNO_STATE.layout());
312-
asyncIOUpdateEventFd = IO.eventfd(0, 0, errnoState);
313-
if (asyncIOUpdateEventFd == -1) {
314-
asyncIOUpdateEventFd = 0;
320+
asyncIOWakeUpEventFd = IO.eventfd(0, 0, errnoState);
321+
if (asyncIOWakeUpEventFd == -1) {
322+
asyncIOWakeUpEventFd = 0;
315323
throwLastError(errnoState, "internal error (eventfd)");
316324
}
317325
}

java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUSBDevice.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ public synchronized void claimInterface(int interfaceNumber) {
111111
throwException("Interface %d has already been claimed", interfaceNumber);
112112

113113
try (var arena = Arena.openConfined()) {
114-
var intfNumSegment = arena.allocate(JAVA_INT, interfaceNumber);
115114
var errnoState = arena.allocate(Linux.ERRNO_STATE.layout());
116115

117116
// claim interface (possibly disconnecting kernel driver)
@@ -324,15 +323,15 @@ public synchronized void abortTransfers(USBDirection direction, int endpointNumb
324323
}
325324

326325
@Override
327-
public InputStream openInputStream(int endpointNumber, int bufferSize) {
326+
public synchronized InputStream openInputStream(int endpointNumber, int bufferSize) {
328327
// check that endpoint number is valid
329328
getEndpoint(USBDirection.IN, endpointNumber, USBTransferType.BULK, null);
330329

331330
return new LinuxEndpointInputStream(this, endpointNumber, bufferSize);
332331
}
333332

334333
@Override
335-
public OutputStream openOutputStream(int endpointNumber, int bufferSize) {
334+
public synchronized OutputStream openOutputStream(int endpointNumber, int bufferSize) {
336335
// check that endpoint number is valid
337336
getEndpoint(USBDirection.OUT, endpointNumber, USBTransferType.BULK, null);
338337

java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/errno.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,19 @@ public static int EAGAIN() {
3131
}
3232
/**
3333
* {@snippet :
34-
* #define EPIPE 32
34+
* #define EINVAL 22
3535
* }
3636
*/
37-
public static int EPIPE() {
38-
return (int)32L;
37+
public static int EINVAL() {
38+
return (int)22L;
3939
}
4040
/**
4141
* {@snippet :
42-
* #define ETIMEDOUT 110
42+
* #define EPIPE 32
4343
* }
4444
*/
45-
public static int ETIMEDOUT() {
46-
return (int)110L;
45+
public static int EPIPE() {
46+
return (int)32L;
4747
}
4848
}
4949

java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUSBDevice.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ public synchronized void close() {
9797
if (!isOpen())
9898
return;
9999

100-
for (InterfaceInfo interfaceInfo : claimedInterfaces) {
101-
IoKitUSB.USBInterfaceClose(interfaceInfo.iokitInterface());
102-
IoKitUSB.Release(interfaceInfo.iokitInterface());
103-
setClaimed(interfaceInfo.interfaceNumber, false);
100+
for (InterfaceInfo(MemorySegment iokitInterface, int interfaceNumber) : claimedInterfaces) {
101+
IoKitUSB.USBInterfaceClose(iokitInterface);
102+
IoKitUSB.Release(iokitInterface);
103+
setClaimed(interfaceNumber, false);
104104
}
105105

106106
claimedInterfaces = null;

java-does-usb/src/test/java/net/codecrete/usb/sample/LogicAnalyzer.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ void sampleData(int sampleRate, int duration, String filename) {
118118
this.filename = filename;
119119
prepareSampling(sampleRate, duration);
120120
var acquirer = CompletableFuture.runAsync(this::saveSamples);
121-
sleep(20); // give thread time to start
121+
sleep(50); // give thread time to start
122122
startAcquisition();
123123
var watchdog = CompletableFuture.runAsync(this::detectBufferOverrun);
124124
CompletableFuture.allOf(acquirer, watchdog).join();
@@ -232,11 +232,12 @@ void stopAcquisition() {
232232
}
233233

234234
void detectBufferOverrun() {
235+
sleep(10);
235236
// if the 'activityValue' hasn't changed within 20ms, the logic analyzer
236237
// has stopped sending data (likely due to a buffer overrun)
237238
long lastValue = 0;
238239
while (true) {
239-
sleep(20);
240+
sleep(5);
240241

241242
if (stopped)
242243
break;

0 commit comments

Comments
 (0)