Skip to content

Commit 7279d98

Browse files
committed
Windows: introduce device info set
1 parent c0eb077 commit 7279d98

File tree

3 files changed

+388
-335
lines changed

3 files changed

+388
-335
lines changed
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
package net.codecrete.usb.windows;
2+
3+
import net.codecrete.usb.common.ScopeCleanup;
4+
import net.codecrete.usb.windows.gen.advapi32.Advapi32;
5+
import net.codecrete.usb.windows.gen.kernel32.Kernel32;
6+
import net.codecrete.usb.windows.gen.kernel32._GUID;
7+
import net.codecrete.usb.windows.gen.ole32.Ole32;
8+
import net.codecrete.usb.windows.gen.setupapi.SetupAPI;
9+
import net.codecrete.usb.windows.gen.setupapi._SP_DEVICE_INTERFACE_DATA;
10+
import net.codecrete.usb.windows.gen.setupapi._SP_DEVICE_INTERFACE_DETAIL_DATA_W;
11+
import net.codecrete.usb.windows.gen.setupapi._SP_DEVINFO_DATA;
12+
import net.codecrete.usb.windows.winsdk.SetupAPI2;
13+
14+
import java.lang.foreign.Arena;
15+
import java.lang.foreign.MemorySegment;
16+
import java.util.List;
17+
18+
import static java.lang.foreign.MemorySegment.NULL;
19+
import static java.lang.foreign.ValueLayout.JAVA_CHAR;
20+
import static java.lang.foreign.ValueLayout.JAVA_INT;
21+
import static net.codecrete.usb.windows.DeviceProperty.DEVPKEY_Device_Service;
22+
import static net.codecrete.usb.windows.Win.allocateErrorState;
23+
import static net.codecrete.usb.windows.WindowsUSBException.throwException;
24+
import static net.codecrete.usb.windows.WindowsUSBException.throwLastError;
25+
26+
/**
27+
* Device information set (of Windows Setup API).
28+
*
29+
* <p>
30+
* An instance of this class represents a device information set ({@code HDEVINFO})
31+
* and a current element within the set.
32+
* </p>
33+
*/
34+
public class DeviceInfoSet implements AutoCloseable {
35+
36+
@FunctionalInterface
37+
interface InfoSetCreator {
38+
MemorySegment create(Arena arena, MemorySegment errorState);
39+
}
40+
41+
private final Arena arena;
42+
private final MemorySegment errorState;
43+
private final MemorySegment devInfoSet;
44+
private final MemorySegment devInfoData;
45+
private MemorySegment devIntfData;
46+
private int iterationIndex = -1;
47+
48+
/**
49+
* Creates a new empty device info set.
50+
*
51+
* @return device info set
52+
*/
53+
static DeviceInfoSet ofEmpty() {
54+
return new DeviceInfoSet((arena, errorState) -> SetupAPI2.SetupDiCreateDeviceInfoList(NULL, NULL, errorState));
55+
}
56+
57+
/**
58+
* Creates a new device info set containing the present devices of the specified device class and
59+
* optionally device instance ID.
60+
*
61+
* <p>
62+
* After creation, there is no current element. {@link #next()} must be called first.
63+
* </p>
64+
*
65+
* @param interfaceGuid device interface class GUID
66+
* @param instanceId device instance ID
67+
* @return device info set
68+
*/
69+
static DeviceInfoSet ofPresentDevices(MemorySegment interfaceGuid, String instanceId) {
70+
return new DeviceInfoSet((arena, errorState) -> {
71+
var instanceIdSegment = instanceId != null ? Win.createSegmentFromString(instanceId, arena) : NULL;
72+
return SetupAPI2.SetupDiGetClassDevsW(interfaceGuid, instanceIdSegment, NULL,
73+
SetupAPI.DIGCF_PRESENT() | SetupAPI.DIGCF_DEVICEINTERFACE(), errorState);
74+
});
75+
}
76+
77+
private DeviceInfoSet(InfoSetCreator creator) {
78+
arena = Arena.ofConfined();
79+
try {
80+
errorState = allocateErrorState(arena);
81+
82+
devInfoSet = creator.create(arena, errorState);
83+
if (Win.isInvalidHandle(devInfoSet))
84+
throwLastError(errorState, "internal error (creating device info set)");
85+
86+
// allocate SP_DEVINFO_DATA (will receive device details)
87+
devInfoData = _SP_DEVINFO_DATA.allocate(arena);
88+
_SP_DEVINFO_DATA.cbSize$set(devInfoData, (int) _SP_DEVINFO_DATA.$LAYOUT().byteSize());
89+
90+
} catch (Exception e) {
91+
arena.close();
92+
throw e;
93+
}
94+
}
95+
96+
@Override
97+
public void close() {
98+
if (devIntfData != null)
99+
SetupAPI.SetupDiDeleteDeviceInterfaceData(devInfoSet, devIntfData);
100+
SetupAPI.SetupDiDestroyDeviceInfoList(devInfoSet);
101+
arena.close();
102+
}
103+
104+
/**
105+
* Iterates to the next element in this set.
106+
*
107+
* @return {@code true} if there is a current element, {@code false} if the iteration moved beyond the last element
108+
*/
109+
boolean next() {
110+
iterationIndex += 1;
111+
if (SetupAPI2.SetupDiEnumDeviceInfo(devInfoSet, iterationIndex, devInfoData, errorState) == 0) {
112+
var err = Win.getLastError(errorState);
113+
if (err == Kernel32.ERROR_NO_MORE_ITEMS())
114+
return false;
115+
throwLastError(errorState, "internal error (SetupDiEnumDeviceInfo)");
116+
}
117+
118+
return true;
119+
}
120+
121+
/**
122+
* Adds the device with the specified instance ID to this device info set.
123+
*
124+
* <p>
125+
* The added device becomes the current element.
126+
* </p>
127+
*
128+
* @param instanceId instance ID
129+
*/
130+
void addInstance(String instanceId) {
131+
var instanceIdSegment = Win.createSegmentFromString(instanceId, arena);
132+
if (SetupAPI2.SetupDiOpenDeviceInfoW(devInfoSet, instanceIdSegment, NULL, 0, devInfoData, errorState) == 0)
133+
throwLastError(errorState, "internal error (SetupDiOpenDeviceInfoW)");
134+
}
135+
136+
/**
137+
* Adds the device with the specified path to this device info set.
138+
*
139+
* <p>
140+
* The added device becomes the current element.
141+
* </p>
142+
*
143+
* @param devicePath device path
144+
*/
145+
void addDevice(String devicePath) {
146+
if (devIntfData != null)
147+
throw new AssertionError("calling addDevice() multiple times is not implemented");
148+
149+
// load device information into dev info set
150+
var intfData = _SP_DEVICE_INTERFACE_DATA.allocate(arena);
151+
_SP_DEVICE_INTERFACE_DATA.cbSize$set(intfData, (int) intfData.byteSize());
152+
var devicePathSegment = Win.createSegmentFromString(devicePath, arena);
153+
if (SetupAPI2.SetupDiOpenDeviceInterfaceW(devInfoSet, devicePathSegment, 0, intfData, errorState) == 0)
154+
throwLastError(errorState, "internal error (SetupDiOpenDeviceInterfaceW)");
155+
156+
devIntfData = intfData; // for later cleanup
157+
158+
if (SetupAPI2.SetupDiGetDeviceInterfaceDetailW(devInfoSet, intfData, NULL, 0, NULL,
159+
devInfoData, errorState) == 0) {
160+
var err = Win.getLastError(errorState);
161+
if (err != Kernel32.ERROR_INSUFFICIENT_BUFFER())
162+
throwException(err, "internal error (SetupDiGetDeviceInterfaceDetailW)");
163+
}
164+
}
165+
166+
167+
/**
168+
* Checks if the current element is a composite USB device
169+
*
170+
* @return {@code true} if it is a composite device
171+
*/
172+
boolean isCompositeDevice() {
173+
var deviceService = getStringProperty(DEVPKEY_Device_Service);
174+
175+
// usbccgp is the USB Generic Parent Driver used for composite devices
176+
return "usbccgp".equalsIgnoreCase(deviceService);
177+
}
178+
179+
/**
180+
* Gets the device path for the device with the given instance ID.
181+
* <p>
182+
* The device path is looked up by checking the GUIDs associated with the current element.
183+
* </p>
184+
*
185+
* @param instanceId device instance ID
186+
* @return the device path, {@code null} if not found
187+
*/
188+
String getDevicePathByGUID(String instanceId) {
189+
var guids = findDeviceInterfaceGUIDs(arena);
190+
191+
for (var guid : guids) {
192+
// check for class GUID
193+
var guidSegment = Win.createSegmentFromString(guid, arena);
194+
var clsid = _GUID.allocate(arena);
195+
if (Ole32.CLSIDFromString(guidSegment, clsid) != 0)
196+
continue;
197+
198+
try {
199+
return getDevicePath(instanceId, clsid);
200+
} catch (Exception e) {
201+
// ignore and try next one
202+
}
203+
}
204+
205+
return null;
206+
}
207+
208+
/**
209+
* Gets a list of {@code DeviceInterfaceGUIDs} from the current element's device configuration information
210+
* in the registry.
211+
*
212+
* @param arena arena for allocating memory
213+
* @return list of GUIDs
214+
*/
215+
private List<String> findDeviceInterfaceGUIDs(Arena arena) {
216+
217+
try (var cleanup = new ScopeCleanup()) {
218+
// open device registry key
219+
var regKey = SetupAPI2.SetupDiOpenDevRegKey(devInfoSet, devInfoData, SetupAPI.DICS_FLAG_GLOBAL(), 0,
220+
SetupAPI.DIREG_DEV(), Advapi32.KEY_READ(), errorState);
221+
if (Win.isInvalidHandle(regKey))
222+
throwLastError(errorState, "internal error (SetupDiOpenDevRegKey)");
223+
cleanup.add(() -> Advapi32.RegCloseKey(regKey));
224+
225+
// read registry value (without buffer, to query length)
226+
var keyNameSegment = Win.createSegmentFromString("DeviceInterfaceGUIDs", arena);
227+
var valueTypeHolder = arena.allocate(JAVA_INT);
228+
var valueSizeHolder = arena.allocate(JAVA_INT);
229+
var res = Advapi32.RegQueryValueExW(regKey, keyNameSegment, NULL, valueTypeHolder, NULL, valueSizeHolder);
230+
if (res == Kernel32.ERROR_FILE_NOT_FOUND())
231+
return List.of(); // no device interface GUIDs
232+
if (res != 0 && res != Kernel32.ERROR_MORE_DATA())
233+
throwException(res, "internal error (RegQueryValueExW)");
234+
235+
// read registry value (with buffer)
236+
var valueSize = valueSizeHolder.get(JAVA_INT, 0);
237+
var value = arena.allocate(valueSize);
238+
res = Advapi32.RegQueryValueExW(regKey, keyNameSegment, NULL, valueTypeHolder, value, valueSizeHolder);
239+
if (res != 0)
240+
throwException(res, "internal error (RegQueryValueExW)");
241+
242+
return Win.createStringListFromSegment(value);
243+
}
244+
}
245+
246+
/**
247+
* Gets the integer device property of the current element.
248+
*
249+
* @param propertyKey property key (of type {@code DEVPKEY})
250+
* @return property value
251+
*/
252+
@SuppressWarnings("SameParameterValue")
253+
int getIntProperty(MemorySegment propertyKey) {
254+
var propertyTypeHolder = arena.allocate(JAVA_INT);
255+
var propertyValueHolder = arena.allocate(JAVA_INT);
256+
if (SetupAPI2.SetupDiGetDevicePropertyW(devInfoSet, devInfoData, propertyKey, propertyTypeHolder,
257+
propertyValueHolder, (int) propertyValueHolder.byteSize(), NULL, 0, errorState) == 0)
258+
throwLastError(errorState, "internal error (SetupDiGetDevicePropertyW - A)");
259+
260+
if (propertyTypeHolder.get(JAVA_INT, 0) != SetupAPI.DEVPROP_TYPE_UINT32())
261+
throwException("internal error (expected property type UINT32)");
262+
263+
return propertyValueHolder.get(JAVA_INT, 0);
264+
}
265+
266+
/**
267+
* Gets the string device property of the current element.
268+
*
269+
* @param propertyKey property key (of type {@code DEVPKEY})
270+
* @return property value
271+
*/
272+
String getStringProperty(MemorySegment propertyKey) {
273+
var propertyValue = getVariableLengthProperty(propertyKey, SetupAPI.DEVPROP_TYPE_STRING(), arena);
274+
if (propertyValue == null)
275+
return null;
276+
return Win.createStringFromSegment(propertyValue);
277+
}
278+
279+
/**
280+
* Gets the string list device property of the current element.
281+
*
282+
* @param propertyKey property key (of type {@code DEVPKEY})
283+
* @return property value
284+
*/
285+
@SuppressWarnings("java:S1168")
286+
List<String> getStringListProperty(MemorySegment propertyKey) {
287+
var propertyValue = getVariableLengthProperty(propertyKey,
288+
SetupAPI.DEVPROP_TYPE_STRING() | SetupAPI.DEVPROP_TYPEMOD_LIST(), arena);
289+
if (propertyValue == null)
290+
return null;
291+
292+
return Win.createStringListFromSegment(propertyValue);
293+
}
294+
295+
private MemorySegment getVariableLengthProperty(MemorySegment propertyKey, int propertyType, Arena arena) {
296+
297+
// query length (thus no buffer)
298+
var propertyTypeHolder = arena.allocate(JAVA_INT);
299+
var requiredSizeHolder = arena.allocate(JAVA_INT);
300+
if (SetupAPI2.SetupDiGetDevicePropertyW(devInfoSet, devInfoData, propertyKey, propertyTypeHolder, NULL, 0,
301+
requiredSizeHolder, 0, errorState) == 0) {
302+
var err = Win.getLastError(errorState);
303+
if (err == Kernel32.ERROR_NOT_FOUND())
304+
return null;
305+
if (err != Kernel32.ERROR_INSUFFICIENT_BUFFER())
306+
throwException(err, "internal error (SetupDiGetDevicePropertyW - B)");
307+
}
308+
309+
if (propertyTypeHolder.get(JAVA_INT, 0) != propertyType)
310+
throwException("internal error (unexpected property type)");
311+
312+
var stringLen = requiredSizeHolder.get(JAVA_INT, 0) / 2 - 1;
313+
314+
// allocate buffer
315+
var propertyValueHolder = arena.allocateArray(JAVA_CHAR, stringLen + 1L);
316+
317+
// get property value
318+
if (SetupAPI2.SetupDiGetDevicePropertyW(devInfoSet, devInfoData, propertyKey, propertyTypeHolder,
319+
propertyValueHolder, (int) propertyValueHolder.byteSize(), NULL, 0, errorState) == 0)
320+
throwLastError(errorState, "internal error (SetupDiGetDevicePropertyW - C)");
321+
322+
return propertyValueHolder;
323+
}
324+
325+
/**
326+
* Gets the device path for the device with the given device instance ID and device interface class.
327+
*
328+
* @param instanceId device instance ID
329+
* @param interfaceGuid device interface class GUID
330+
* @return the device path
331+
*/
332+
static String getDevicePath(String instanceId, MemorySegment interfaceGuid) {
333+
try (var arena = Arena.ofConfined();
334+
var deviceInfoSet = DeviceInfoSet.ofPresentDevices(interfaceGuid, instanceId)) {
335+
336+
// retrieve first element of enumeration
337+
var errorState = allocateErrorState(arena);
338+
var devIntfData = _SP_DEVICE_INTERFACE_DATA.allocate(arena);
339+
_SP_DEVICE_INTERFACE_DATA.cbSize$set(devIntfData, (int) devIntfData.byteSize());
340+
if (SetupAPI2.SetupDiEnumDeviceInterfaces(deviceInfoSet.devInfoSet, NULL, interfaceGuid, 0, devIntfData,
341+
errorState) == 0)
342+
throwLastError(errorState, "internal error (SetupDiEnumDeviceInterfaces)");
343+
344+
// get device path
345+
// (SP_DEVICE_INTERFACE_DETAIL_DATA_W is of variable length and requires a bigger allocation so
346+
// the device path fits)
347+
final var devicePathOffset = 4;
348+
var intfDetailData = arena.allocate(4L + 260 * 2);
349+
_SP_DEVICE_INTERFACE_DETAIL_DATA_W.cbSize$set(intfDetailData,
350+
(int) _SP_DEVICE_INTERFACE_DETAIL_DATA_W.sizeof());
351+
if (SetupAPI2.SetupDiGetDeviceInterfaceDetailW(deviceInfoSet.devInfoSet, devIntfData, intfDetailData,
352+
(int) intfDetailData.byteSize(), NULL, NULL, errorState) == 0)
353+
throwLastError(errorState, "Internal error (SetupDiGetDeviceInterfaceDetailW)");
354+
355+
return Win.createStringFromSegment(intfDetailData.asSlice(devicePathOffset));
356+
}
357+
}
358+
}

0 commit comments

Comments
 (0)