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
@@ -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 {