headers: fail Build on missing native exports and assert MI/COM layouts (#2739)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-16 02:32:29 +02:00
committed by GitHub
parent af2a7c3f6c
commit 2f5781ff13
43 changed files with 866 additions and 16 deletions
+6 -1
View File
@@ -54,7 +54,12 @@ in [`Makefile`](Makefile). Do not change versions to work around a local tooling
problem. Follow [`.golangci.yaml`](.golangci.yaml), [`.editorconfig`](.editorconfig)
and [`.gitattributes`](.gitattributes); keep LF line endings and the existing
license headers. Use structured `slog` attributes and the existing Windows API
wrappers (`golang.org/x/sys/windows`, `NewLazySystemDLL`).
wrappers (`golang.org/x/sys/windows`, `NewLazySystemDLL`). `LazyProc.Call`
panics for a missing export: add every proc a binding calls without its own
`Find` check to the package's `lazyproc.Once` list, and call the package's
`Load` in the `Build` of each collector that uses it. Mirrors of native structs
that MI or COM read by offset need `unsafe.Sizeof`/`Offsetof` checks, see
[`internal/mi/layout.go`](internal/mi/layout.go).
Before committing, run the repository checks from its root:
+3 -1
View File
@@ -121,7 +121,9 @@ The suffix only keeps the series unique and does not identify the VM across scra
Metrics about the host's CPU allocation. Processor counts per virtual machine are
read from the Host Compute Service API; the logical processor count comes from the
`Hyper-V Hypervisor Logical Processor` performance counter object.
`Hyper-V Hypervisor Logical Processor` performance counter object. The Host Compute
Service API requires Windows Server 2019 or later; on older versions the sub-collector
is skipped with a warning.
| Metric Name | Description | Type | Labels |
|-------------------------------------------------|------------------------------------------------------------------------|-------|--------------------|
@@ -247,7 +247,16 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
// Without the Containers feature, the Host Compute Service is missing and
// every scrape would fail. Report the collector as unsupported instead, so
// it is skipped once at startup rather than logging a warning per scrape.
if err := kernel32.Load(); err != nil {
return err
}
if slices.Contains(c.config.CollectorsEnabled, subCollectorHCS) {
// hcn.Load also checks the hcs functions.
if err := hcn.Load(); err != nil {
return fmt.Errorf("host compute APIs not available, is the Containers feature installed? %w", err)
}
if _, err := hcs.GetContainers(); errors.Is(err, hcs.ErrServiceNotAvailable) {
return fmt.Errorf("host compute service not available, is the Containers feature installed? %w: %w",
errors.ErrUnsupported, err)
+23
View File
@@ -46,6 +46,11 @@ type Config struct {
CollectorsEnabled []string `yaml:"enabled"`
}
// loadDHCPServerAPI checks the DHCP server API. Tests replace it.
//
//nolint:gochecknoglobals
var loadDHCPServerAPI = dhcpsapi.Load
//nolint:gochecknoglobals
var ConfigDefaults = Config{
CollectorsEnabled: []string{
@@ -164,6 +169,24 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
}
}
// Without the DHCP server API only the scope metrics are unavailable; the
// server metrics come from performance counters.
if slices.Contains(c.config.CollectorsEnabled, subCollectorScopeMetrics) {
if err := loadDHCPServerAPI(); err != nil {
if !slices.Contains(c.config.CollectorsEnabled, subCollectorServerMetrics) {
return fmt.Errorf("DHCP server API not available: %w", err)
}
c.logger.Warn("DHCP server API not available, skipping the scope_metrics sub-collector",
slog.Any("err", err),
)
c.config.CollectorsEnabled = slices.DeleteFunc(slices.Clone(c.config.CollectorsEnabled), func(name string) bool {
return name == subCollectorScopeMetrics
})
}
}
var err error
if slices.Contains(c.config.CollectorsEnabled, subCollectorScopeMetrics) {
@@ -0,0 +1,61 @@
// 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.
//go:build windows
package dhcp
import (
"errors"
"log/slog"
"testing"
"github.com/stretchr/testify/require"
)
// TestBuildWithoutDHCPServerAPI checks that a missing DHCP server API only
// disables the scope metrics, unless they are the only sub-collector.
//
//nolint:paralleltest // replaces loadDHCPServerAPI.
func TestBuildWithoutDHCPServerAPI(t *testing.T) {
original := loadDHCPServerAPI
t.Cleanup(func() { loadDHCPServerAPI = original })
loadDHCPServerAPI = func() error { return errors.ErrUnsupported }
t.Run("server and scope metrics", func(t *testing.T) {
enabled := []string{subCollectorServerMetrics, subCollectorScopeMetrics}
c := New(&Config{CollectorsEnabled: enabled})
// The DHCP Server counters may be missing on this host; only the
// scope metrics decision is checked.
_ = c.Build(slog.New(slog.DiscardHandler), nil)
t.Cleanup(func() { _ = c.Close() })
require.Equal(t, []string{subCollectorServerMetrics}, c.config.CollectorsEnabled)
require.Equal(t, []string{subCollectorServerMetrics, subCollectorScopeMetrics}, enabled, "the caller's slice must not change")
require.Nil(t, c.scopeInfo)
})
t.Run("scope metrics only", func(t *testing.T) {
c := New(&Config{CollectorsEnabled: []string{subCollectorScopeMetrics}})
err := c.Build(slog.New(slog.DiscardHandler), nil)
require.ErrorIs(t, err, errors.ErrUnsupported)
})
}
+4
View File
@@ -69,6 +69,10 @@ func (c *Collector) Close() error {
}
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
if err := sysinfoapi.Load(); err != nil {
return err
}
info, err := sysinfoapi.GetSMBIOSInfo()
if err != nil {
return fmt.Errorf("SMBIOS info unavailable: %w", err)
+4
View File
@@ -159,6 +159,10 @@ func (c *Collector) Close() error {
}
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
if err := errors.Join(gdi32.Load(), cfgmgr32.Load()); err != nil {
return fmt.Errorf("GPU APIs not available: %w", err)
}
var err error
c.logger = logger.With(slog.String("collector", Name))
+5 -3
View File
@@ -233,9 +233,11 @@ func (c *Collector) Build(logger *slog.Logger, miSession *mi.Session) error {
close: func() { c.perfDataCollectorDynamicMemoryVM.Close() },
},
subCollectorHost: {
build: c.buildHost,
collect: c.collectHost,
close: func() { c.perfDataCollectorLogicalProcessor.Close() },
// The Host Compute Service API (computecore.dll) exists since 1809.
minBuildNumber: osversion.LTSC2019,
build: c.buildHost,
collect: c.collectHost,
close: func() { c.perfDataCollectorLogicalProcessor.Close() },
},
subCollectorHypervisorLogicalProcessor: {
build: c.buildHypervisorLogicalProcessor,
+4
View File
@@ -70,6 +70,10 @@ type vmVirtualNode struct {
}
func (c *Collector) buildHost() error {
if err := hcs.Load(); err != nil {
return err
}
var err error
c.perfDataCollectorLogicalProcessor, err = pdh.NewCollector[perfDataCounterValuesHost](c.logger, pdh.CounterTypeRaw, "Hyper-V Hypervisor Logical Processor", pdh.InstancesAll)
@@ -52,6 +52,10 @@ func (c *Collector) buildWMIHealth() error {
return mi.ErrNotInitialized
}
if err := sysinfoapi.Load(); err != nil {
return err
}
// The host's Msvm_ComputerSystem is named after the physical NetBIOS name.
// Caption is localized, so it can't be used to select the host.
hostname, err := sysinfoapi.GetComputerName(sysinfoapi.ComputerNamePhysicalNetBIOS)
+4
View File
@@ -76,6 +76,10 @@ func (c *Collector) Close() error {
}
func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) error {
if err := slc.Load(); err != nil {
return err
}
c.licenseStatus = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "status"),
"Status of windows license",
+4
View File
@@ -115,6 +115,10 @@ func (c *Collector) Close() error {
}
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
if err := sysinfoapi.Load(); err != nil {
return err
}
c.availableBytes = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "available_bytes"),
"The amount of physical memory immediately available for allocation to a process or for system use. It is equal to the sum of memory assigned to"+
+4
View File
@@ -77,6 +77,10 @@ func (c *Collector) Close() error {
}
func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) error {
if err := sysinfoapi.Load(); err != nil {
return err
}
productName, revision, installationType, err := c.getWindowsVersion()
if err != nil {
return fmt.Errorf("failed to get Windows version: %w", err)
+4
View File
@@ -79,6 +79,10 @@ func (c *Collector) Close() error {
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.logger = logger.With(slog.String("collector", Name))
if err := psapi.Load(); err != nil {
return err
}
c.pagingLimitBytes = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "limit_bytes"),
"Current size of the paging file in bytes",
+4
View File
@@ -134,6 +134,10 @@ func (c *Collector) Close() error {
}
func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) error {
if err := winspool.Load(); err != nil {
return err
}
c.printerJobStatus = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "job_status"),
"A counter of printer jobs by status",
+4
View File
@@ -82,6 +82,10 @@ func (c *Collector) Close() error {
}
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
if err := ntdll.Load(); err != nil {
return err
}
c.bootTime = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "boot_time_timestamp"),
"Unix timestamp of last system boot. Follows clock corrections.",
+6
View File
@@ -143,6 +143,12 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
}
}
if slices.Contains(c.config.CollectorsEnabled, subCollectorConnectionsState) {
if err := iphlpapi.Load(); err != nil {
return err
}
}
labels := []string{"af"}
c.connectionFailures = prometheus.NewDesc(
@@ -289,6 +289,10 @@ func (c *Collector) Build(logger *slog.Logger, miSession *mi.Session) error {
c.logger.Debug("host is not a connection broker skipping Connection Broker performance metrics.")
}
if err = wtsapi32.Load(); err != nil {
return err
}
c.hServer, err = wtsapi32.WTSOpenServer("")
if err != nil {
return fmt.Errorf("failed to open WTS server: %w", err)
+4
View File
@@ -139,6 +139,10 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
}
}
if err := kernel32.Load(); err != nil {
return err
}
// https://github.com/prometheus-community/windows_exporter/issues/1891
c.ppbCounterPresent = osversion.Build() >= osversion.LTSC2019
+7
View File
@@ -19,6 +19,7 @@ import (
"fmt"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"github.com/prometheus-community/windows_exporter/internal/headers/win32"
"golang.org/x/sys/windows"
)
@@ -31,8 +32,14 @@ var (
procCMGetDeviceIDListSize = cfgmgr32.NewProc("CM_Get_Device_ID_List_SizeW")
procCMGetDevNodePropertyW = cfgmgr32.NewProc("CM_Get_DevNode_PropertyW")
procCMLocateDevNodeW = cfgmgr32.NewProc("CM_Locate_DevNodeW")
load = lazyproc.Once(cfgmgr32, procCMGetDeviceIDListW, procCMGetDeviceIDListSize, procCMGetDevNodePropertyW, procCMLocateDevNodeW)
)
// Load checks that cfgmgr32.dll exports every function of this package. Call
// it in Build: a missing export panics on the first call otherwise.
func Load() error { return load() }
func CMGetDeviceIDListSize(filter *win32.LPWSTR, size *uint32) error {
ret, _, _ := procCMGetDeviceIDListSize.Call(
uintptr(unsafe.Pointer(size)),
+14
View File
@@ -23,6 +23,7 @@ import (
"net"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"golang.org/x/sys/windows"
)
@@ -35,8 +36,21 @@ var (
procDhcpV4EnumSubnetReservations = modDhcpServer.NewProc("DhcpV4EnumSubnetReservations")
procDhcpV4FailoverGetScopeStatistics = modDhcpServer.NewProc("DhcpV4FailoverGetScopeStatistics")
procDhcpGetMibInfoV5 = modDhcpServer.NewProc("DhcpGetMibInfoV5")
load = lazyproc.Once(modDhcpServer,
procDhcpGetSubnetInfo,
procDhcpGetSuperScopeInfoV4,
procDhcpRpcFreeMemory,
procDhcpV4EnumSubnetReservations,
procDhcpV4FailoverGetScopeStatistics,
procDhcpGetMibInfoV5,
)
)
// Load checks that dhcpsapi.dll exports every function of this package. Call
// it in Build: a missing export panics on the first call otherwise.
func Load() error { return load() }
func GetDHCPV4ScopeStatistics() ([]DHCPV4Scope, error) {
var mibInfo *DHCP_MIB_INFO_V5
+19
View File
@@ -19,6 +19,7 @@ import (
"fmt"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"github.com/prometheus-community/windows_exporter/internal/headers/ntdll"
"golang.org/x/sys/windows"
)
@@ -30,8 +31,26 @@ var (
procD3DKMTQueryAdapterInfo = modGdi32.NewProc("D3DKMTQueryAdapterInfo")
procD3DKMTCloseAdapter = modGdi32.NewProc("D3DKMTCloseAdapter")
procD3DKMTEnumAdapters2 = modGdi32.NewProc("D3DKMTEnumAdapters2")
load = lazyproc.Once(modGdi32,
procD3DKMTOpenAdapterFromLuid,
procD3DKMTQueryAdapterInfo,
procD3DKMTCloseAdapter,
procD3DKMTEnumAdapters2,
)
)
// Load checks that gdi32.dll, and the ntdll functions this package uses,
// export every function of this package. Call it in Build: a missing export
// panics on the first call otherwise.
func Load() error {
if err := load(); err != nil {
return err
}
return ntdll.Load()
}
func D3DKMTOpenAdapterFromLuid(ptr *D3DKMT_OPENADAPTERFROMLUID) error {
ret, _, _ := procD3DKMTOpenAdapterFromLuid.Call(
uintptr(unsafe.Pointer(ptr)),
+13
View File
@@ -23,14 +23,27 @@ import (
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/hcs"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"github.com/prometheus-community/windows_exporter/internal/utils"
"golang.org/x/sys/windows"
)
// Load checks that vmcompute.dll, and the hcs functions this package uses,
// export every function of this package. Call it in Build: a missing export
// panics on the first call otherwise.
func Load() error {
if err := load(); err != nil {
return err
}
return hcs.Load()
}
//nolint:gochecknoglobals
var (
modvmcompute = windows.NewLazySystemDLL("vmcompute.dll")
procHNSCall = modvmcompute.NewProc("HNSCall")
load = lazyproc.Once(modvmcompute, procHNSCall)
hcnBodyEmpty = utils.Must(windows.UTF16PtrFromString(""))
hcnMethodGet = utils.Must(windows.UTF16PtrFromString("GET"))
+15
View File
@@ -22,6 +22,7 @@ import (
"fmt"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"golang.org/x/sys/windows"
)
@@ -36,8 +37,22 @@ var (
procHcsOpenComputeSystem = modComputeCore.NewProc("HcsOpenComputeSystem")
procHcsGetComputeSystemProperties = modComputeCore.NewProc("HcsGetComputeSystemProperties")
procHcsCloseComputeSystem = modComputeCore.NewProc("HcsCloseComputeSystem")
load = lazyproc.Once(modComputeCore,
procHcsCreateOperation,
procHcsWaitForOperationResult,
procHcsCloseOperation,
procHcsEnumerateComputeSystems,
procHcsOpenComputeSystem,
procHcsGetComputeSystemProperties,
procHcsCloseComputeSystem,
)
)
// Load checks that computecore.dll exports every function of this package.
// Call it in Build: a missing export panics on the first call otherwise.
func Load() error { return load() }
// CreateOperation creates a new operation.
//
// HcsCreateOperation returns the operation handle, or NULL on failure.
+7
View File
@@ -22,6 +22,7 @@ import (
"fmt"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"golang.org/x/sys/windows"
)
@@ -29,8 +30,14 @@ import (
var (
modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
procGetExtendedTcpTable = modiphlpapi.NewProc("GetExtendedTcpTable")
load = lazyproc.Once(modiphlpapi, procGetExtendedTcpTable)
)
// Load checks that iphlpapi.dll exports every function of this package. Call
// it in Build: a missing export panics on the first call otherwise.
func Load() error { return load() }
func GetTCPConnectionStates(family uint32) (map[MIB_TCP_STATE]uint32, error) {
stateCounts := make(map[MIB_TCP_STATE]uint32)
+14
View File
@@ -20,6 +20,7 @@ package kernel32
import (
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"golang.org/x/sys/windows"
)
@@ -31,8 +32,21 @@ var (
procKernelLocalFileTimeToFileTime = modkernel32.NewProc("LocalFileTimeToFileTime")
procOpenJobObject = modkernel32.NewProc("OpenJobObjectW")
procIsProcessInJob = modkernel32.NewProc("IsProcessInJob")
// GetDiskSpaceInformationW is optional and checked by GetDiskSpaceInformation.
load = lazyproc.Once(modkernel32,
procGetDynamicTimeZoneInformationSys,
procKernelLocalFileTimeToFileTime,
procOpenJobObject,
procIsProcessInJob,
)
)
// Load checks that kernel32.dll exports every function this package calls
// without a check of its own. Call it in Build: a missing export panics on
// the first call otherwise.
func Load() error { return load() }
// SYSTEMTIME contains a date and time.
// 📑 https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime
type SYSTEMTIME struct {
@@ -0,0 +1,85 @@
// 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.
//go:build windows
package lazyproc_test
import (
"os"
"path/filepath"
"testing"
"github.com/prometheus-community/windows_exporter/internal/headers/cfgmgr32"
"github.com/prometheus-community/windows_exporter/internal/headers/dhcpsapi"
"github.com/prometheus-community/windows_exporter/internal/headers/gdi32"
"github.com/prometheus-community/windows_exporter/internal/headers/hcn"
"github.com/prometheus-community/windows_exporter/internal/headers/hcs"
"github.com/prometheus-community/windows_exporter/internal/headers/iphlpapi"
"github.com/prometheus-community/windows_exporter/internal/headers/kernel32"
"github.com/prometheus-community/windows_exporter/internal/headers/ntdll"
"github.com/prometheus-community/windows_exporter/internal/headers/psapi"
"github.com/prometheus-community/windows_exporter/internal/headers/slc"
"github.com/prometheus-community/windows_exporter/internal/headers/sysinfoapi"
"github.com/prometheus-community/windows_exporter/internal/headers/winspool"
"github.com/prometheus-community/windows_exporter/internal/headers/wtsapi32"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows"
)
// TestBindingsLoad checks the export names of every binding package against
// the DLLs of this host. A DLL that belongs to an optional Windows feature
// skips its binding when the DLL is not installed.
func TestBindingsLoad(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
load func() error
// optionalDLL is set for DLLs of optional Windows features.
optionalDLL string
}{
{name: "cfgmgr32", load: cfgmgr32.Load},
{name: "dhcpsapi", load: dhcpsapi.Load, optionalDLL: "dhcpsapi.dll"},
{name: "gdi32", load: gdi32.Load},
{name: "hcn", load: hcn.Load, optionalDLL: "vmcompute.dll"},
{name: "hcs", load: hcs.Load, optionalDLL: "computecore.dll"},
{name: "iphlpapi", load: iphlpapi.Load},
{name: "kernel32", load: kernel32.Load},
{name: "ntdll", load: ntdll.Load},
{name: "psapi", load: psapi.Load},
{name: "slc", load: slc.Load},
{name: "sysinfoapi", load: sysinfoapi.Load},
{name: "winspool", load: winspool.Load},
{name: "wtsapi32", load: wtsapi32.Load},
{name: "pdh", load: pdh.Load},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if tc.optionalDLL != "" {
systemDirectory, err := windows.GetSystemDirectory()
require.NoError(t, err)
if _, err := os.Stat(filepath.Join(systemDirectory, tc.optionalDLL)); err != nil {
t.Skipf("%s is not installed: %v", tc.optionalDLL, err)
}
}
require.NoError(t, tc.load())
})
}
}
+95
View File
@@ -0,0 +1,95 @@
// 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.
//go:build windows
// Package lazyproc checks that lazily loaded native functions exist.
//
// windows.LazyProc.Call panics when its DLL or export is missing, e.g. on a
// Windows version or edition without the subsystem. A binding package lists
// the functions it calls without a check of its own in a Load function built
// with [Once], and collectors call it in Build, so a missing export fails
// Build instead of panicking on the first scrape.
package lazyproc
import (
"errors"
"fmt"
"strings"
"sync"
"golang.org/x/sys/windows"
)
// Error reports a missing DLL or missing exports. It matches
// [errors.ErrUnsupported] and has no other cause, so a collector that fails
// Build with it is skipped as an unavailable subsystem: the loader errors,
// e.g. ERROR_MOD_NOT_FOUND, are only kept as text and in Missing.
type Error struct {
// DLL is the name of the DLL.
DLL string
// Missing holds the error of the DLL, if it could not be loaded, or of
// each missing export.
Missing []error
}
func (e *Error) Error() string {
var message strings.Builder
fmt.Fprintf(&message, "%s: %s", errors.ErrUnsupported, e.DLL)
for _, err := range e.Missing {
message.WriteString(": ")
message.WriteString(err.Error())
}
return message.String()
}
// Is reports whether target is [errors.ErrUnsupported].
func (e *Error) Is(target error) bool {
return target == errors.ErrUnsupported
}
// Find loads dll and every proc, which must belong to dll. It returns nil if
// all of them exist, and otherwise an [*Error] that names the missing DLL or
// each missing export.
func Find(dll *windows.LazyDLL, procs ...*windows.LazyProc) error {
if err := dll.Load(); err != nil {
return &Error{DLL: dll.Name, Missing: []error{err}}
}
var missing []error
for _, proc := range procs {
if err := proc.Find(); err != nil {
missing = append(missing, err)
}
}
if len(missing) == 0 {
return nil
}
return &Error{DLL: dll.Name, Missing: missing}
}
// Once returns a function that runs [Find] once and then returns its result.
// A DLL or export that is missing stays missing for the life of the process.
func Once(dll *windows.LazyDLL, procs ...*windows.LazyProc) func() error {
return sync.OnceValue(func() error {
return Find(dll, procs...)
})
}
@@ -0,0 +1,78 @@
// 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.
//go:build windows
package lazyproc_test
import (
"errors"
"strings"
"testing"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows"
)
func TestFind(t *testing.T) {
t.Parallel()
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
missingDLL := windows.NewLazySystemDLL("windows_exporter_missing.dll")
t.Run("all present", func(t *testing.T) {
t.Parallel()
require.NoError(t, lazyproc.Find(kernel32, kernel32.NewProc("GetTickCount64"), kernel32.NewProc("GetSystemInfo")))
require.NoError(t, lazyproc.Find(kernel32))
})
t.Run("missing export", func(t *testing.T) {
t.Parallel()
err := lazyproc.Find(kernel32, kernel32.NewProc("GetTickCount64"), kernel32.NewProc("WindowsExporterMissingExport"))
require.ErrorIs(t, err, errors.ErrUnsupported)
require.ErrorContains(t, err, "WindowsExporterMissingExport")
})
t.Run("missing DLL is reported once", func(t *testing.T) {
t.Parallel()
err := lazyproc.Find(missingDLL, missingDLL.NewProc("A"), missingDLL.NewProc("B"))
require.ErrorIs(t, err, errors.ErrUnsupported)
require.Equal(t, 1, strings.Count(err.Error(), "windows_exporter_missing.dll"), err.Error())
// The loader error stays text only, so the error has a single cause.
require.NotErrorIs(t, err, windows.ERROR_MOD_NOT_FOUND)
var lazyErr *lazyproc.Error
require.ErrorAs(t, err, &lazyErr)
require.Equal(t, "windows_exporter_missing.dll", lazyErr.DLL)
require.Len(t, lazyErr.Missing, 1)
})
t.Run("Once", func(t *testing.T) {
t.Parallel()
load := lazyproc.Once(kernel32, kernel32.NewProc("WindowsExporterMissingExport"))
first := load()
require.ErrorIs(t, first, errors.ErrUnsupported)
// The cached error is returned again.
require.Same(t, first, load())
})
}
+7
View File
@@ -18,6 +18,7 @@
package ntdll
import (
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"golang.org/x/sys/windows"
)
@@ -25,8 +26,14 @@ import (
var (
modNtdll = windows.NewLazySystemDLL("ntdll.dll")
procRtlNtStatusToDosError = modNtdll.NewProc("RtlNtStatusToDosError")
load = lazyproc.Once(modNtdll, procRtlNtStatusToDosError)
)
// Load checks that ntdll.dll exports every function of this package. Call it
// in Build: a missing export panics on the first call otherwise.
func Load() error { return load() }
func RtlNtStatusToDosError(status uintptr) error {
ret, _, _ := procRtlNtStatusToDosError.Call(status)
if ret == 0 {
+7
View File
@@ -20,6 +20,7 @@ package psapi
import (
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"golang.org/x/sys/windows"
)
@@ -46,8 +47,14 @@ type PerformanceInformation struct {
var (
psapi = windows.NewLazySystemDLL("psapi.dll")
procGetPerformanceInfo = psapi.NewProc("GetPerformanceInfo")
load = lazyproc.Once(psapi, procGetPerformanceInfo)
)
// Load checks that psapi.dll exports every function of this package. Call it
// in Build: a missing export panics on the first call otherwise.
func Load() error { return load() }
// GetPerformanceInfo returns the dereferenced version of GetLPPerformanceInfo.
func GetPerformanceInfo() (PerformanceInformation, error) {
var lppi PerformanceInformation
+7
View File
@@ -21,6 +21,7 @@ import (
"errors"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"golang.org/x/sys/windows"
)
@@ -28,8 +29,14 @@ import (
var (
slc = windows.NewLazySystemDLL("slc.dll")
procSLIsWindowsGenuineLocal = slc.NewProc("SLIsWindowsGenuineLocal")
load = lazyproc.Once(slc, procSLIsWindowsGenuineLocal)
)
// Load checks that slc.dll exports every function of this package. Call it in
// Build: a missing export panics on the first call otherwise.
func Load() error { return load() }
// SL_GENUINE_STATE enumeration
//
// https://learn.microsoft.com/en-us/windows/win32/api/slpublic/ne-slpublic-sl_genuine_state
+12
View File
@@ -21,6 +21,7 @@ import (
"unicode/utf16"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"golang.org/x/sys/windows"
)
@@ -121,8 +122,19 @@ var (
procGetSystemInfo = kernel32.NewProc("GetSystemInfo")
procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
procGetComputerNameExW = kernel32.NewProc("GetComputerNameExW")
load = lazyproc.Once(kernel32,
procGetSystemInfo,
procGlobalMemoryStatusEx,
procGetComputerNameExW,
procGetSystemFirmwareTable,
)
)
// Load checks that kernel32.dll exports every function of this package. Call
// it in Build: a missing export panics on the first call otherwise.
func Load() error { return load() }
// GlobalMemoryStatusEx retrieves information about the system's current usage of both physical and virtual memory.
// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-globalmemorystatusex
func GlobalMemoryStatusEx() (MemoryStatus, error) {
+7
View File
@@ -27,6 +27,7 @@ import (
"fmt"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"golang.org/x/sys/windows"
)
@@ -38,8 +39,14 @@ var (
procOpenPrinterW = modWinspool.NewProc("OpenPrinterW")
procClosePrinter = modWinspool.NewProc("ClosePrinter")
procEnumJobsW = modWinspool.NewProc("EnumJobsW")
load = lazyproc.Once(modWinspool, procEnumPrintersW, procOpenPrinterW, procClosePrinter, procEnumJobsW)
)
// Load checks that winspool.drv exports every function of this package. Call
// it in Build: a missing export panics on the first call otherwise.
func Load() error { return load() }
// EnumPrinters returns the printers selected by flags, a combination of PRINTER_ENUM_* values.
func EnumPrinters(flags uint32) ([]Printer, error) {
buf, count, err := enum(func(buf *byte, size uint32, needed, returned *uint32) (uintptr, error) {
+7
View File
@@ -22,9 +22,14 @@ import (
"log/slog"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"golang.org/x/sys/windows"
)
// Load checks that wtsapi32.dll exports every function of this package. Call
// it in Build: a missing export panics on the first call otherwise.
func Load() error { return load() }
type WTSTypeClass int
// The valid values for the WTSTypeClass enumeration.
@@ -107,6 +112,8 @@ var (
procWTSFreeMemoryEx = wtsapi32.NewProc("WTSFreeMemoryExW")
procWTSCloseServer = wtsapi32.NewProc("WTSCloseServer")
load = lazyproc.Once(wtsapi32, procWTSOpenServerEx, procWTSEnumerateSessionsEx, procWTSFreeMemoryEx, procWTSCloseServer)
WTSSessionStates = map[WTSConnectState]string{
wtsActive: "active",
wtsConnected: "connected",
+80
View File
@@ -0,0 +1,80 @@
// 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.
//go:build windows
package mi
import "unsafe"
// Compile-time checks of the Go mirrors of native MI structures against
// mi.h. MI reads and writes these structures by offset, so a missing or
// reordered field corrupts memory instead of failing.
//
// Each check indexes a one-element array with the difference of two
// constants: it compiles only when both are equal. A smaller left side
// overflows uintptr, a larger one is out of bounds.
const ptrSize = unsafe.Sizeof(uintptr(0))
// handleSize is the size of the MI handle structures MI_Application,
// MI_Session, MI_Operation, MI_OperationOptions and MI_DestinationOptions:
// MI_Uint64 reserved1, ptrdiff_t reserved2 and a function table pointer.
const handleSize = 8 + 2*ptrSize
// miValueSize is the size of the MI_Value union. Its largest member is
// MI_Datetime: an MI_Boolean, padded to 4 bytes, and a 32-byte MI_Timestamp or
// MI_Interval union, 36 bytes, rounded up to the 8-byte alignment of the
// 64-bit members. Instance.GetElementByName passes a buffer of this size.
const miValueSize = 40
var (
// Handles without Go-only fields.
_ = [1]struct{}{}[unsafe.Sizeof(Application{})-handleSize]
_ = [1]struct{}{}[unsafe.Sizeof(OperationOptions{})-handleSize]
_ = [1]struct{}{}[unsafe.Sizeof(DestinationOptions{})-handleSize]
// Session and Operation append Go-only fields after the native layout,
// which MI never writes past. The first Go-only field must start right
// after the native fields, with the function table at the native offset.
_ = [1]struct{}{}[unsafe.Offsetof(Session{}.ft)-(8+ptrSize)]
_ = [1]struct{}{}[unsafe.Offsetof(Session{}.application)-handleSize]
_ = [1]struct{}{}[unsafe.Offsetof(Operation{}.ft)-(8+ptrSize)]
_ = [1]struct{}{}[unsafe.Offsetof(Operation{}.completed)-handleSize]
// MI_Instance: four pointers and ptrdiff_t reserved[4].
_ = [1]struct{}{}[unsafe.Sizeof(Instance{})-8*ptrSize]
// MI_OperationCallbacks: the callback context and eight callbacks.
_ = [1]struct{}{}[unsafe.Sizeof(operationCallbacks{})-9*ptrSize]
_ = [1]struct{}{}[unsafe.Offsetof(operationCallbacks{}.instanceResult)-5*ptrSize]
// MI_Timestamp and MI_Interval: eight 32-bit fields each.
_ = [1]struct{}{}[unsafe.Sizeof(Timestamp{})-32]
_ = [1]struct{}{}[unsafe.Sizeof(Interval{})-32]
// The MI_Value buffer of Instance.GetElementByName, kept in Element.raw.
_ = [1]struct{}{}[unsafe.Sizeof(Element{}.raw)-miValueSize]
_ = [1]struct{}{}[unsafe.Sizeof(ValueType(0))-4]
// Function tables, by the number of entries in mi.h. Calls use the slot
// offsets, so a missing entry would call the wrong function.
_ = [1]struct{}{}[unsafe.Sizeof(ApplicationFT{})-11*ptrSize]
_ = [1]struct{}{}[unsafe.Sizeof(SessionFT{})-15*ptrSize]
_ = [1]struct{}{}[unsafe.Sizeof(OperationFT{})-6*ptrSize]
_ = [1]struct{}{}[unsafe.Sizeof(OperationOptionsFT{})-13*ptrSize]
_ = [1]struct{}{}[unsafe.Sizeof(DestinationOptionsFT{})-15*ptrSize]
_ = [1]struct{}{}[unsafe.Sizeof(InstanceFT{})-18*ptrSize]
)
+67 -10
View File
@@ -27,6 +27,7 @@ import (
"unicode/utf8"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"golang.org/x/sys/windows"
)
@@ -40,12 +41,21 @@ var (
sysAllocStringLen = oleaut32.NewProc("SysAllocStringLen")
sysFreeString = oleaut32.NewProc("SysFreeString")
sysStringLen = oleaut32.NewProc("SysStringLen")
loadOle32 = lazyproc.Once(ole32, coInitializeEx, coUninitialize, coCreateInstance)
loadOleaut32 = lazyproc.Once(oleaut32, sysAllocStringLen, sysFreeString, sysStringLen)
)
// Initialize initializes the MTA and disables legacy OLE 1.0 DDE. The caller
// must lock its OS thread before calling and release all objects before
// Uninitialize on that same thread. S_FALSE is successful initialization.
// A missing ole32 or oleaut32 export fails Initialize instead of panicking
// in a later call.
func Initialize() error {
if err := errors.Join(loadOle32(), loadOleaut32()); err != nil {
return fmt.Errorf("initialize COM: %w", err)
}
hr, _, _ := coInitializeEx.Call(0, windows.COINIT_MULTITHREADED|windows.COINIT_DISABLE_OLE1DDE)
if err := ResultError(hr); err != nil {
return fmt.Errorf("initialize COM: %w", err)
@@ -88,15 +98,37 @@ type nativeScalar interface {
// nativeObject requires the common COM interface layout without extra Go data.
type nativeObject interface{ ~struct{ Object } }
// Method returns the address of the zero-based native vtable slot.
func (o *Object) Method(slot uintptr) uintptr {
return *(*uintptr)(unsafe.Add(unsafe.Pointer(o.VTable), slot*unsafe.Sizeof(uintptr(0))))
// ErrNilObject reports a call on a nil interface pointer, an interface without
// a vtable, or an empty vtable slot. Dereferencing them would crash the process
// instead of failing the call.
var ErrNilObject = errors.New("COM interface or vtable slot is nil")
// Method returns the address of the zero-based native vtable slot. It returns
// ErrNilObject instead of dereferencing a nil interface, a nil vtable or
// calling an empty slot.
func (o *Object) Method(slot uintptr) (uintptr, error) {
if o == nil || o.VTable == nil {
return 0, ErrNilObject
}
fn := *(*uintptr)(unsafe.Add(unsafe.Pointer(o.VTable), slot*unsafe.Sizeof(uintptr(0))))
if fn == 0 {
return 0, fmt.Errorf("COM vtable slot %d: %w", slot, ErrNilObject)
}
return fn, nil
}
// Release relinquishes an owned interface reference. Do not call it on borrowed
// iterator items. Like IUnknown::Release, it does not return an HRESULT.
// iterator items. Like IUnknown::Release, it does not return an HRESULT, and
// releasing a nil interface does nothing.
func (o *Object) Release() {
_ = Call(o.Method(2), uintptr(unsafe.Pointer(o)))
fn, err := o.Method(2)
if err != nil {
return
}
_ = Call(fn, uintptr(unsafe.Pointer(o)))
runtime.KeepAlive(o)
}
@@ -108,7 +140,12 @@ func (o *Object) Release() {
func (o *Object) Get[T nativeScalar](slot uintptr) (T, error) {
var value T
hr := Call(o.Method(slot), uintptr(unsafe.Pointer(o)), uintptr(unsafe.Pointer(&value)))
fn, err := o.Method(slot)
if err != nil {
return value, err
}
hr := Call(fn, uintptr(unsafe.Pointer(o)), uintptr(unsafe.Pointer(&value)))
runtime.KeepAlive(o)
return value, ResultError(hr)
@@ -122,8 +159,13 @@ func (o *Object) Get[T nativeScalar](slot uintptr) (T, error) {
func (o *Object) GetArg[T nativeScalar](slot, arg uintptr) (T, error) {
var value T
fn, err := o.Method(slot)
if err != nil {
return value, err
}
hr := Call(
o.Method(slot),
fn,
uintptr(unsafe.Pointer(o)),
arg,
uintptr(unsafe.Pointer(&value)),
@@ -138,7 +180,12 @@ func (o *Object) GetArg[T nativeScalar](slot, arg uintptr) (T, error) {
func (o *Object) GetObject[T nativeObject](slot uintptr) (*T, error) {
var value *T
hr := Call(o.Method(slot), uintptr(unsafe.Pointer(o)), uintptr(unsafe.Pointer(&value)))
fn, err := o.Method(slot)
if err != nil {
return nil, err
}
hr := Call(fn, uintptr(unsafe.Pointer(o)), uintptr(unsafe.Pointer(&value)))
runtime.KeepAlive(o)
if err := ResultError(hr); err != nil {
@@ -160,8 +207,13 @@ func (o *Object) GetObject[T nativeObject](slot uintptr) (*T, error) {
func (o *Object) GetObjectArg[T nativeObject](slot, arg uintptr) (*T, error) {
var value *T
fn, err := o.Method(slot)
if err != nil {
return nil, err
}
hr := Call(
o.Method(slot),
fn,
uintptr(unsafe.Pointer(o)),
arg,
uintptr(unsafe.Pointer(&value)),
@@ -181,7 +233,12 @@ func (o *Object) GetObjectArg[T nativeObject](slot, arg uintptr) (*T, error) {
// Put calls a native setter with a scalar argument.
func (o *Object) Put(slot, arg uintptr) error {
hr := Call(o.Method(slot), uintptr(unsafe.Pointer(o)), arg)
fn, err := o.Method(slot)
if err != nil {
return err
}
hr := Call(fn, uintptr(unsafe.Pointer(o)), arg)
runtime.KeepAlive(o)
return ResultError(hr)
+36
View File
@@ -264,3 +264,39 @@ func TestVariantLayout(t *testing.T) {
require.Equal(t, uintptr(24), unsafe.Sizeof(Variant{}))
require.Equal(t, uintptr(8), unsafe.Offsetof(Variant{}.Value))
}
// TestNilObject checks that calls on a nil interface, a nil vtable or an empty
// vtable slot fail instead of crashing the process.
func TestNilObject(t *testing.T) {
var methods [8]uintptr
for name, object := range map[string]*Object{
"nil interface": nil,
"nil vtable": {},
"empty slot": {VTable: &methods[0]},
} {
t.Run(name, func(t *testing.T) {
_, err := object.Method(7)
require.ErrorIs(t, err, ErrNilObject)
_, err = object.Get[int32](7)
require.ErrorIs(t, err, ErrNilObject)
_, err = object.GetArg[int32](7, 0)
require.ErrorIs(t, err, ErrNilObject)
_, err = object.GetObject[nativeTestObject](7)
require.ErrorIs(t, err, ErrNilObject)
_, err = object.GetObjectArg[nativeTestObject](7, 0)
require.ErrorIs(t, err, ErrNilObject)
require.ErrorIs(t, object.Put(7, 0), ErrNilObject)
_, err = object.String(7)
require.ErrorIs(t, err, ErrNilObject)
require.NotPanics(t, object.Release)
})
}
}
+6 -1
View File
@@ -67,8 +67,13 @@ func (s *TaskService) Connect() error {
domain := ole.NewEmptyVariant()
password := ole.NewEmptyVariant()
fn, err := s.Method(taskServiceConnect)
if err != nil {
return err
}
hr := ole.Call(
s.Method(taskServiceConnect),
fn,
uintptr(unsafe.Pointer(s)),
uintptr(unsafe.Pointer(server)),
uintptr(unsafe.Pointer(user)),
+4
View File
@@ -321,6 +321,10 @@ func newCollector[T any](logger *slog.Logger, query *Query, resultType CounterTy
return nil, fmt.Errorf("invalid result type: %v", resultType)
}
if err := Load(); err != nil {
return nil, err
}
var handle pdhQueryHandle
if query != nil {
+32
View File
@@ -55,6 +55,7 @@ import (
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/kernel32"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"golang.org/x/sys/windows"
)
@@ -292,8 +293,39 @@ var (
pdhGetRawCounterArrayW = libPdhDll.NewProc("PdhGetRawCounterArrayW")
pdhPdhGetCounterTimeBase = libPdhDll.NewProc("PdhGetCounterTimeBase")
pdhRemoveCounter = libPdhDll.NewProc("PdhRemoveCounter")
// The localized-name procs are optional and checked where they are used.
loadPdh = lazyproc.Once(libPdhDll,
pdhAddCounterW,
pdhAddEnglishCounterW,
pdhCloseQuery,
pdhCollectQueryData,
pdhCollectQueryDataWithTime,
pdhGetFormattedCounterValue,
pdhGetFormattedCounterArrayW,
pdhOpenQuery,
pdhValidatePathW,
pdhExpandWildCardPathW,
pdhGetCounterInfoW,
pdhGetRawCounterValue,
pdhGetRawCounterArrayW,
pdhPdhGetCounterTimeBase,
pdhRemoveCounter,
)
)
// Load checks that pdh.dll, and the kernel32 functions this package uses,
// export every function this package calls without a check of its own.
// [NewQuery] and the collector constructors call it, so a missing export
// fails Build instead of panicking on the first scrape.
func Load() error {
if err := loadPdh(); err != nil {
return err
}
return kernel32.Load()
}
// AddCounter adds the specified counter to the query. This is the internationalized version. Preferably, use the
// function AddEnglishCounter instead. hQuery is the query handle, which has been fetched by OpenQuery.
// szFullCounterPath is a full, internationalized counter path (this will differ per Windows language version).
+4
View File
@@ -39,6 +39,10 @@ type Query struct {
// NewQuery opens a query for [NewQueryCollector]. The caller must Close it
// after the collectors in it.
func NewQuery() (*Query, error) {
if err := Load(); err != nil {
return nil, err
}
var handle pdhQueryHandle
if ret := OpenQuery(0, 0, &handle); ret != ErrorSuccess {
@@ -0,0 +1,82 @@
// 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.
//go:build windows
package collector
import (
"errors"
"fmt"
"testing"
"github.com/prometheus-community/windows_exporter/internal/headers/lazyproc"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows"
)
// TestExpectedBuildError_MissingNativeAPI checks that a collector whose
// binding Load fails because of a missing DLL or export is skipped, also
// inside the joined errors of collectors with sub-collectors.
func TestExpectedBuildError_MissingNativeAPI(t *testing.T) {
t.Parallel()
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
missingDLL := windows.NewLazySystemDLL("windows_exporter_missing.dll")
missingExport := lazyproc.Find(kernel32, kernel32.NewProc("GetTickCount64"), kernel32.NewProc("WindowsExporterMissingExport"))
require.Error(t, missingExport)
missingLibrary := lazyproc.Find(missingDLL, missingDLL.NewProc("A"), missingDLL.NewProc("B"))
require.Error(t, missingLibrary)
for name, tc := range map[string]struct {
err error
expected bool
}{
"missing export": {
err: fmt.Errorf("error build collector printer: %w", missingExport),
expected: true,
},
"missing DLL": {
err: fmt.Errorf("error build collector dhcp: DHCP server API not available: %w", missingLibrary),
expected: true,
},
"joined bindings": {
err: fmt.Errorf("error build collector gpu: GPU APIs not available: %w", errors.Join(missingExport, missingLibrary)),
expected: true,
},
"hyperv sub-collectors": {
err: fmt.Errorf("error build collector hyperv: %w", errors.Join(
fmt.Errorf("failed to build host collector: %w", missingLibrary),
fmt.Errorf("failed to build wmi_health collector: %w", missingExport),
)),
expected: true,
},
"hyperv sub-collectors with an unexpected failure": {
err: fmt.Errorf("error build collector hyperv: %w", errors.Join(
fmt.Errorf("failed to build host collector: %w", missingLibrary),
fmt.Errorf("failed to build datastore collector: %w", windows.ERROR_ACCESS_DENIED),
)),
expected: false,
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tc.expected, expectedBuildError(tc.err), tc.err)
})
}
}