mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 18:19:04 +02:00
372 lines
11 KiB
Go
372 lines
11 KiB
Go
// 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_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/prometheus-community/windows_exporter/internal/mi"
|
|
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
type msftStoragePool struct {
|
|
FriendlyName string `mi:"FriendlyName"`
|
|
}
|
|
|
|
// newTestSession returns a session that is closed with the test.
|
|
func newTestSession(tb testing.TB) *mi.Session {
|
|
tb.Helper()
|
|
|
|
application, err := mi.ApplicationInitialize()
|
|
require.NoError(tb, err)
|
|
|
|
tb.Cleanup(func() { require.NoError(tb, application.Close()) })
|
|
|
|
session, err := application.NewSession(nil)
|
|
require.NoError(tb, err)
|
|
|
|
tb.Cleanup(func() { require.NoError(tb, session.Close()) })
|
|
|
|
return session
|
|
}
|
|
|
|
// storagePoolQueryTimeout is generous: the storage provider is slow, and CI
|
|
// runs several test binaries at once. The tests check handles, not latency.
|
|
const storagePoolQueryTimeout = 30 * time.Second
|
|
|
|
// storagePoolQuery returns the MSFT_StoragePool query, or skips if the storage
|
|
// provider is not installed or not accessible, e.g. without elevation. Other
|
|
// errors fail the test.
|
|
func storagePoolQuery(tb testing.TB, session *mi.Session) mi.Query {
|
|
tb.Helper()
|
|
|
|
query, err := mi.NewQuery("SELECT FriendlyName FROM MSFT_StoragePool")
|
|
require.NoError(tb, err)
|
|
|
|
var pools []msftStoragePool
|
|
|
|
err = session.Query(&pools, mi.NamespaceRootStorage, query, storagePoolQueryTimeout)
|
|
if errors.Is(err, mi.MI_RESULT_INVALID_NAMESPACE) || errors.Is(err, mi.MI_RESULT_INVALID_CLASS) ||
|
|
errors.Is(err, mi.MI_RESULT_NOT_SUPPORTED) || errors.Is(err, mi.MI_RESULT_ACCESS_DENIED) {
|
|
tb.Skipf("MSFT_StoragePool is not available: %v", err)
|
|
}
|
|
|
|
require.NoError(tb, err)
|
|
|
|
return query
|
|
}
|
|
|
|
// Test_MI_ParallelQuery_HandleGrowth runs parallel queries against the slow
|
|
// MSFT_StoragePool provider. With MI_Operation_GetInstance, the WMI client
|
|
// leaked Event handles whenever its result hand-over had to wait, which
|
|
// parallel queries against a slow provider do: about one per three queries.
|
|
func Test_MI_ParallelQuery_HandleGrowth(t *testing.T) {
|
|
const (
|
|
workers = 8
|
|
queriesPerWorker = 50
|
|
rounds = 4
|
|
maxHandleIncrease = 100
|
|
)
|
|
|
|
session := newTestSession(t)
|
|
query := storagePoolQuery(t, session)
|
|
|
|
round := func() {
|
|
var wg sync.WaitGroup
|
|
|
|
for range workers {
|
|
wg.Go(func() {
|
|
for i := range queriesPerWorker {
|
|
// Both query paths deliver results through the callback.
|
|
if i%2 == 0 {
|
|
var pools []msftStoragePool
|
|
|
|
if err := session.Query(&pools, mi.NamespaceRootStorage, query, storagePoolQueryTimeout); err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
err := session.QueryFunc(mi.NamespaceRootStorage, query, storagePoolQueryTimeout, func(*mi.Instance) error {
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
handleCount := func() int64 {
|
|
count, err := testutils.GetProcessHandleCount(windows.CurrentProcess())
|
|
require.NoError(t, err)
|
|
|
|
return int64(count)
|
|
}
|
|
|
|
// The first parallel round grows MI and Go thread pools.
|
|
round()
|
|
|
|
baseline := handleCount()
|
|
|
|
for range rounds {
|
|
round()
|
|
}
|
|
|
|
current := handleCount()
|
|
|
|
t.Logf("handle count after warm-up %d, after %d queries %d", baseline, workers*queriesPerWorker*rounds, current)
|
|
|
|
require.LessOrEqual(t, current, baseline+maxHandleIncrease,
|
|
"handle count grew from %d to %d after %d parallel queries", baseline, current, workers*queriesPerWorker*rounds)
|
|
}
|
|
|
|
// Test_MI_QueryFunc_Panic checks that a panic in fn reaches the caller, after
|
|
// the query has been cancelled and closed, and leaves the session usable.
|
|
func Test_MI_QueryFunc_Panic(t *testing.T) {
|
|
session := newTestSession(t)
|
|
|
|
query, err := mi.NewQuery("SELECT Name FROM Win32_Process")
|
|
require.NoError(t, err)
|
|
|
|
for range 3 {
|
|
var calls int
|
|
|
|
require.PanicsWithValue(t, "fn panicked", func() {
|
|
_ = session.QueryFunc(mi.NamespaceRootCIMv2, query, 5*time.Second, func(*mi.Instance) error {
|
|
calls++
|
|
|
|
panic("fn panicked")
|
|
})
|
|
})
|
|
require.Equal(t, 1, calls)
|
|
}
|
|
|
|
var processes []win32Process
|
|
|
|
require.NoError(t, session.Query(&processes, mi.NamespaceRootCIMv2, query, 5*time.Second))
|
|
require.NotEmpty(t, processes)
|
|
}
|
|
|
|
// Test_MI_QueryFunc_Timeout checks that a timeout reaches QueryFunc as the
|
|
// native MI result.
|
|
func Test_MI_QueryFunc_Timeout(t *testing.T) {
|
|
session := newTestSession(t)
|
|
|
|
query, err := mi.NewQuery("SELECT * FROM Win32_Process")
|
|
require.NoError(t, err)
|
|
|
|
err = session.QueryFunc(mi.NamespaceRootCIMv2, query, time.Millisecond, func(*mi.Instance) error {
|
|
return nil
|
|
})
|
|
require.ErrorIs(t, err, mi.MI_RESULT_INVALID_OPERATION_TIMEOUT)
|
|
}
|
|
|
|
// Test_MI_QueryFunc_Nested runs a query from within fn, which runs on the
|
|
// calling goroutine. The instance passed to fn stays valid across it.
|
|
func Test_MI_QueryFunc_Nested(t *testing.T) {
|
|
session := newTestSession(t)
|
|
|
|
outer, err := mi.NewQuery("SELECT Name FROM Win32_Process WHERE Handle = 0 OR Handle = 4")
|
|
require.NoError(t, err)
|
|
|
|
inner, err := mi.NewQuery("SELECT Name FROM Win32_Process WHERE Handle = 4")
|
|
require.NoError(t, err)
|
|
|
|
var calls int
|
|
|
|
err = session.QueryFunc(mi.NamespaceRootCIMv2, outer, 5*time.Second, func(instance *mi.Instance) error {
|
|
calls++
|
|
|
|
var processes []win32Process
|
|
|
|
if err := session.Query(&processes, mi.NamespaceRootCIMv2, inner, 5*time.Second); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(processes) != 1 || processes[0].Name != "System" {
|
|
return errors.New("unexpected inner result")
|
|
}
|
|
|
|
// The outer instance is still valid.
|
|
element, err := instance.GetElement("Name")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = element.String()
|
|
|
|
return err
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, calls)
|
|
}
|
|
|
|
// Test_MI_Query_InvalidParameter checks a failure MI reports from within
|
|
// MI_Session_QueryInstances, on the calling goroutine.
|
|
func Test_MI_Query_InvalidParameter(t *testing.T) {
|
|
session := newTestSession(t)
|
|
|
|
var processes []win32Process
|
|
|
|
require.ErrorIs(t, session.Query(&processes, mi.NamespaceRootCIMv2, nil, 5*time.Second), mi.MI_RESULT_INVALID_PARAMETER)
|
|
|
|
err := session.QueryFunc(nil, nil, 5*time.Second, func(*mi.Instance) error {
|
|
return errors.New("unexpected instance")
|
|
})
|
|
require.ErrorIs(t, err, mi.MI_RESULT_INVALID_PARAMETER)
|
|
}
|
|
|
|
// Test_MI_Query_DeadlockDetector runs a query in a test binary without the
|
|
// test timeout, so no timer is pending while the only goroutine waits for MI
|
|
// callbacks. The runtime must not declare a deadlock
|
|
// (https://github.com/golang/go/issues/55015).
|
|
func Test_MI_Query_DeadlockDetector(t *testing.T) {
|
|
if os.Getenv("MI_TEST_DEADLOCK_DETECTOR") == "1" {
|
|
session := newTestSession(t)
|
|
|
|
query, err := mi.NewQuery("SELECT * FROM Win32_Process")
|
|
require.NoError(t, err)
|
|
|
|
for range 5 {
|
|
var processes []win32Process
|
|
|
|
require.NoError(t, session.Query(&processes, mi.NamespaceRootCIMv2, query, -1))
|
|
require.NoError(t, session.QueryFunc(mi.NamespaceRootCIMv2, query, -1, func(*mi.Instance) error { return nil }))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
executable, err := os.Executable()
|
|
require.NoError(t, err)
|
|
|
|
cmd := exec.CommandContext(t.Context(), executable, "-test.run=^Test_MI_Query_DeadlockDetector$", "-test.timeout=0", "-test.count=1")
|
|
|
|
cmd.Env = append(os.Environ(), "MI_TEST_DEADLOCK_DETECTOR=1")
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, "%s", output)
|
|
}
|
|
|
|
// Test_MI_QueryContext_CancelBeforeStart checks that a query with a done
|
|
// context is not started.
|
|
func Test_MI_QueryContext_CancelBeforeStart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
session := newTestSession(t)
|
|
|
|
query, err := mi.NewQuery("SELECT Name FROM Win32_Process")
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
cancel()
|
|
|
|
processes := []win32Process{{Name: "stale"}}
|
|
|
|
require.ErrorIs(t, session.QueryContext(ctx, &processes, mi.NamespaceRootCIMv2, query, 5*time.Second), context.Canceled)
|
|
require.Empty(t, processes)
|
|
|
|
err = session.QueryFuncContext(ctx, mi.NamespaceRootCIMv2, query, 5*time.Second, func(*mi.Instance) error {
|
|
return errors.New("unexpected instance")
|
|
})
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
}
|
|
|
|
// Test_MI_QueryContext_CancelDuringQuery checks that a context that is done
|
|
// while the query runs cancels the operation and is reported, and that the
|
|
// session stays usable.
|
|
func Test_MI_QueryContext_CancelDuringQuery(t *testing.T) {
|
|
session := newTestSession(t)
|
|
|
|
query, err := mi.NewQuery("SELECT Name FROM Win32_Process")
|
|
require.NoError(t, err)
|
|
|
|
t.Run("QueryFuncContext", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
|
|
var handled int
|
|
|
|
err := session.QueryFuncContext(ctx, mi.NamespaceRootCIMv2, query, 5*time.Second, func(*mi.Instance) error {
|
|
handled++
|
|
|
|
cancel()
|
|
|
|
return nil
|
|
})
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
require.Equal(t, 1, handled)
|
|
})
|
|
|
|
t.Run("QueryContext", func(t *testing.T) {
|
|
// CIM_DataFile enumerates every file of the drive, which takes far
|
|
// longer than the context.
|
|
slowQuery, err := mi.NewQuery("SELECT Name FROM CIM_DataFile WHERE Drive = 'C:'")
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
type cimDataFile struct {
|
|
Name string `mi:"Name"`
|
|
}
|
|
|
|
var files []cimDataFile
|
|
|
|
start := time.Now()
|
|
|
|
err = session.QueryContext(ctx, &files, mi.NamespaceRootCIMv2, slowQuery, time.Minute)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
require.Less(t, time.Since(start), 30*time.Second)
|
|
})
|
|
|
|
var processes []win32Process
|
|
|
|
require.NoError(t, session.Query(&processes, mi.NamespaceRootCIMv2, query, 5*time.Second))
|
|
require.NotEmpty(t, processes)
|
|
}
|
|
|
|
// Test_MI_Query_ZeroTimeout checks that a zero timeout runs the query with
|
|
// mi.DefaultQueryTimeout instead of failing.
|
|
func Test_MI_Query_ZeroTimeout(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
session := newTestSession(t)
|
|
|
|
query, err := mi.NewQuery("SELECT Name FROM Win32_Process")
|
|
require.NoError(t, err)
|
|
|
|
var processes []win32Process
|
|
|
|
require.NoError(t, session.Query(&processes, mi.NamespaceRootCIMv2, query, 0))
|
|
require.NotEmpty(t, processes)
|
|
}
|