gpu: add device id label (#2186)

This commit is contained in:
Jan-Otto Kröpke
2025-08-28 06:36:10 +02:00
committed by GitHub
parent 71cedbc4d0
commit 0b8a257b31
11 changed files with 490 additions and 193 deletions

View File

@@ -0,0 +1,92 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// 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 cfgmgr32
import (
"fmt"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/win32"
"golang.org/x/sys/windows"
)
func GetDevicesInstanceIDs(deviceID string) ([]Device, error) {
var (
err error
listSize uint32
)
deviceIDLWStr := win32.NewLPWSTR(deviceID)
err = CMGetDeviceIDListSize(deviceIDLWStr, &listSize)
if err != nil {
return nil, err
}
listBuffer := make([]uint16, listSize)
err = CMGetDeviceIDList(deviceIDLWStr, listBuffer)
if err != nil {
return nil, err
}
deviceInstanceIDs := win32.ParseMultiSz(listBuffer)
devices := make([]Device, 0, len(deviceInstanceIDs))
for _, deviceInstanceID := range deviceInstanceIDs {
var devNode *windows.Handle
err = CMLocateDevNode(&devNode, deviceInstanceID)
if err != nil {
return nil, err
}
var (
busNumber uint32
deviceAddress uint32
propType uint32
)
propLen := uint32(4)
err = CMGetDevNodeProperty(devNode, DEVPKEYDeviceBusNumber, &propType, unsafe.Pointer(&busNumber), &propLen)
if err != nil {
return nil, err
}
if propType != DEVPROP_TYPE_UINT32 {
return nil, fmt.Errorf("unexpected property type: 0x%08X", propType)
}
err = CMGetDevNodeProperty(devNode, DEVPKEYDeviceAddress, &propType, unsafe.Pointer(&deviceAddress), &propLen)
if err != nil {
return nil, err
}
if propType != DEVPROP_TYPE_UINT32 {
return nil, fmt.Errorf("unexpected property type: 0x%08X", propType)
}
devices = append(devices, Device{
InstanceID: windows.UTF16ToString(deviceInstanceID),
BusNumber: win32.UINT(busNumber),
DeviceNumber: win32.UINT(deviceAddress >> 16),
FunctionNumber: win32.UINT(deviceAddress & 0xFFFF),
})
}
return devices, nil
}

View File

@@ -0,0 +1,94 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// 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 cfgmgr32
import (
"fmt"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/win32"
"golang.org/x/sys/windows"
)
//nolint:gochecknoglobals
var (
cfgmgr32 = windows.NewLazySystemDLL("cfgmgr32.dll")
procCMGetDeviceIDListW = cfgmgr32.NewProc("CM_Get_Device_ID_ListW")
procCMGetDeviceIDListSize = cfgmgr32.NewProc("CM_Get_Device_ID_List_SizeW")
procCMGetDevNodePropertyW = cfgmgr32.NewProc("CM_Get_DevNode_PropertyW")
procCMLocateDevNodeW = cfgmgr32.NewProc("CM_Locate_DevNodeW")
)
func CMGetDeviceIDListSize(filter *win32.LPWSTR, size *uint32) error {
ret, _, _ := procCMGetDeviceIDListSize.Call(
uintptr(unsafe.Pointer(size)),
filter.Pointer(),
uintptr(CM_GETIDLIST_FILTER_PRESENT|CM_GETIDLIST_FILTER_ENUMERATOR),
)
if ret != CR_SUCCESS {
return fmt.Errorf("CMGetDeviceIDListSize failed: 0x%02X", ret)
}
return nil
}
func CMGetDeviceIDList(filter *win32.LPWSTR, buf []uint16) error {
ret, _, _ := procCMGetDeviceIDListW.Call(
filter.Pointer(),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(len(buf)),
uintptr(CM_GETIDLIST_FILTER_PRESENT|CM_GETIDLIST_FILTER_ENUMERATOR),
)
if ret != CR_SUCCESS {
return fmt.Errorf("CMGetDeviceIDList failed: 0x%02X", ret)
}
return nil
}
func CMLocateDevNode(devInst **windows.Handle, deviceID []uint16) error {
ret, _, _ := procCMLocateDevNodeW.Call(
uintptr(unsafe.Pointer(devInst)),
uintptr(unsafe.Pointer(&deviceID[0])),
0,
)
if ret != CR_SUCCESS {
return fmt.Errorf("CMLocateDevNode failed: 0x%02X", ret)
}
return nil
}
func CMGetDevNodeProperty(devInst *windows.Handle, propKey *DEVPROPKEY, propType *uint32, buf unsafe.Pointer, bufLen *uint32) error {
ret, _, _ := procCMGetDevNodePropertyW.Call(
uintptr(unsafe.Pointer(devInst)),
uintptr(unsafe.Pointer(propKey)),
uintptr(unsafe.Pointer(propType)),
uintptr(buf),
uintptr(unsafe.Pointer(bufLen)),
0,
)
if ret != CR_SUCCESS {
return fmt.Errorf("CMGetDevNodeProperty failed: 0x%02X", ret)
}
return nil
}

View File

@@ -0,0 +1,70 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// 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 cfgmgr32
import (
"github.com/go-ole/go-ole"
"github.com/prometheus-community/windows_exporter/internal/headers/win32"
)
const (
// Configuration Manager return codes
CR_SUCCESS = 0x00
// Filter flags
CM_GETIDLIST_FILTER_ENUMERATOR = 0x00000001
CM_GETIDLIST_FILTER_PRESENT = 0x00000100
DEVPROP_TYPE_UINT32 uint32 = 0x00000007
)
// DEVPROPKEY represents a device property key (GUID + pid)
type DEVPROPKEY struct {
FmtID ole.GUID
PID uint32
}
type Device struct {
InstanceID string
BusNumber win32.UINT
DeviceNumber win32.UINT
FunctionNumber win32.UINT
}
//nolint:gochecknoglobals
var (
// https://github.com/Infinidat/infi.devicemanager/blob/8be9ead6b04ff45c63d9e3bc70d82cceafb75c47/src/infi/devicemanager/setupapi/properties.py#L138C1-L143C34
DEVPKEYDeviceBusNumber = &DEVPROPKEY{
FmtID: ole.GUID{
Data1: 0xa45c254e,
Data2: 0xdf1c,
Data3: 0x4efd,
Data4: [8]byte{0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0},
},
PID: 23, // DEVPROP_TYPE_UINT32
}
// https://github.com/Infinidat/infi.devicemanager/blob/8be9ead6b04ff45c63d9e3bc70d82cceafb75c47/src/infi/devicemanager/setupapi/properties.py#L187-L192
DEVPKEYDeviceAddress = &DEVPROPKEY{
FmtID: ole.GUID{
Data1: 0xa45c254e,
Data2: 0xdf1c,
Data3: 0x4efd,
Data4: [8]byte{0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0},
},
PID: 30, // DEVPROP_TYPE_UINT32
}
)

View File

@@ -34,41 +34,12 @@ const (
KMTQAITYPE_ADAPTERADDRESS = 6
// KMTQAITYPE_ADAPTERREGISTRYINFO pPrivateDriverData points to a D3DKMT_ADAPTERREGISTRYINFO structure that contains registry information about the graphics adapter.
KMTQAITYPE_ADAPTERREGISTRYINFO = 8
// KMTQAITYPE_PHYSICALADAPTERDEVICEIDS pPrivateDriverData points to a D3DKMT_QUERY_DEVICE_IDS structure that specifies the device ID(s) of the physical adapters. Supported starting with Windows 10 (WDDM 2.0).
KMTQAITYPE_PHYSICALADAPTERDEVICEIDS = 31
)
var ErrNoGPUDevices = errors.New("no GPU devices found")
func GetGPUDeviceByLUID(adapterLUID windows.LUID) (GPUDevice, error) {
open := D3DKMT_OPENADAPTERFROMLUID{
AdapterLUID: adapterLUID,
}
if err := D3DKMTOpenAdapterFromLuid(&open); err != nil {
return GPUDevice{}, fmt.Errorf("D3DKMTOpenAdapterFromLuid failed: %w", err)
}
errs := make([]error, 0)
gpuDevice, err := GetGPUDevice(open.HAdapter)
if err != nil {
errs = append(errs, fmt.Errorf("GetGPUDevice failed: %w", err))
}
if err := D3DKMTCloseAdapter(&D3DKMT_CLOSEADAPTER{
HAdapter: open.HAdapter,
}); err != nil {
errs = append(errs, fmt.Errorf("D3DKMTCloseAdapter failed: %w", err))
}
if len(errs) > 0 {
return gpuDevice, fmt.Errorf("errors occurred while getting GPU device: %w", errors.Join(errs...))
}
gpuDevice.LUID = adapterLUID
return gpuDevice, nil
}
func GetGPUDevice(hAdapter D3DKMT_HANDLE) (GPUDevice, error) {
var gpuDevice GPUDevice
@@ -118,6 +89,18 @@ func GetGPUDevice(hAdapter D3DKMT_HANDLE) (GPUDevice, error) {
gpuDevice.AdapterString = windows.UTF16ToString(info.AdapterString[:])
var deviceIDs D3DKMT_QUERY_DEVICE_IDS
query.queryType = KMTQAITYPE_PHYSICALADAPTERDEVICEIDS
query.pPrivateDriverData = unsafe.Pointer(&deviceIDs)
query.privateDriverDataSize = uint32(unsafe.Sizeof(deviceIDs))
if err := D3DKMTQueryAdapterInfo(&query); err != nil && !errors.Is(err, windows.ERROR_FILE_NOT_FOUND) {
return gpuDevice, fmt.Errorf("D3DKMTQueryAdapterInfo (Device IDs) failed: %w", err)
}
gpuDevice.DeviceID = formatPNPDeviceID(deviceIDs)
return gpuDevice, nil
}
@@ -151,7 +134,7 @@ func GetGPUDevices() ([]GPUDevice, error) {
// Process each adapter
for i := range enumAdapters.NumAdapters {
adapter := pAdapters[i]
// Validate handle before using it
// Validate the handle before using it.
if adapter.HAdapter == 0 {
errs = append(errs, fmt.Errorf("adapter %d has null handle", i))
@@ -190,3 +173,13 @@ func GetGPUDevices() ([]GPUDevice, error) {
return gpuDevices, nil
}
func formatPNPDeviceID(deviceIDs D3DKMT_QUERY_DEVICE_IDS) string {
return fmt.Sprintf("PCI\\VEN_%04X&DEV_%04X&SUBSYS_%04X%04X&REV_%02X",
uint16(deviceIDs.DeviceIds.VendorID),
uint16(deviceIDs.DeviceIds.DeviceID),
uint16(deviceIDs.DeviceIds.SubSystemID),
uint16(deviceIDs.DeviceIds.SubVendorID),
uint8(deviceIDs.DeviceIds.RevisionID),
)
}

View File

@@ -73,9 +73,22 @@ type D3DKMT_ADAPTERADDRESS struct {
FunctionNumber win32.UINT
}
type D3DKMT_QUERY_DEVICE_IDS struct {
PhysicalAdapterIndex win32.UINT
DeviceIds struct {
VendorID win32.UINT
DeviceID win32.UINT
SubVendorID win32.UINT
SubSystemID win32.UINT
RevisionID win32.UINT
BusType win32.UINT
}
}
type GPUDevice struct {
AdapterString string
LUID windows.LUID
DeviceID string
DedicatedVideoMemorySize uint64
DedicatedSystemMemorySize uint64
SharedSystemMemorySize uint64

View File

@@ -18,6 +18,7 @@
package win32
import (
"strconv"
"unsafe"
"golang.org/x/sys/windows"
@@ -32,8 +33,8 @@ type (
LPWSTR struct {
*uint16
}
ULONG = uint32 // ULONG is a 32-bit unsigned int in Win32
UINT = uint32 // UINT is a 32-bit unsigned int in Win32
ULONG uint32 // ULONG is a 32-bit unsigned int in Win32
UINT uint32 // UINT is a 32-bit unsigned int in Win32
)
// NewLPWSTR creates a new LPWSTR from a string.
@@ -60,3 +61,7 @@ func (s *LPWSTR) Pointer() uintptr {
func (s *LPWSTR) String() string {
return windows.UTF16PtrToString(s.uint16)
}
func (u *UINT) String() string {
return strconv.FormatUint(uint64(*u), 10)
}

View File

@@ -0,0 +1,55 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// 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 win32
// ParseMultiSz splits a UTF-16 encoded MULTI_SZ buffer (Windows style) into
// individual UTF-16 string slices.
//
// A MULTI_SZ buffer is a sequence of UTF-16 strings separated by single null
// terminators (0x0000) and terminated by an extra null (i.e., two consecutive
// nulls) to mark the end of the list.
//
// Example layout in memory (UTF-16):
//
// "foo\0bar\0baz\0\0"
//
// Given such a []uint16, this function returns a [][]uint16 where each inner
// slice is one null-terminated string segment without the trailing null.
//
// The returned slices reference the original buffer (no copying).
func ParseMultiSz(buf []uint16) [][]uint16 {
var (
result [][]uint16
start int
)
for i := range buf {
if buf[i] == 0 {
// Found a null terminator.
if i == start {
// Two consecutive nulls → end of list.
break
}
// Append current string slice (excluding null).
result = append(result, buf[start:i])
// Move start to next character after null.
start = i + 1
}
}
return result
}