Files
windows_exporter/internal/mi/session.go
T

663 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 (
"context"
"errors"
"fmt"
"math"
"reflect"
"sync"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
// BuildQueryTimeout bounds the probe queries collectors run in Build. Build
// has no scrape budget, and the session default, [DefaultQueryTimeout], is
// too short for slow providers. A timed-out query fails with
// MI_RESULT_INVALID_OPERATION_TIMEOUT.
const BuildQueryTimeout = 30 * time.Second
// DefaultQueryTimeout is the MI timeout of a query that is started with a
// non-positive timeout or with nil operation options. Every query started
// through [Session.Query] or [Session.QueryFunc] has an MI timeout: MI treats
// a zero timeout as unset, which would let a stalled provider block the query
// forever.
const DefaultQueryTimeout = 5 * time.Second
// minQueryTimeout is the shortest MI timeout. MI ignores timeouts below 1ms,
// so shorter positive timeouts are raised to it instead of disabling the
// timeout.
const minQueryTimeout = time.Millisecond
// maxCachedOperationOptions bounds the number of fixed query timeouts whose
// operation options a session keeps.
const maxCachedOperationOptions = 16
// Session represents a session.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/ns-mi-mi_session
type Session struct {
reserved1 uint64
reserved2 uintptr
ft *SessionFT
// Go-only state after the MI_Session fields, which MI never writes past.
//
// application created the session and outlives it, so per-query
// operation options are created without MI_Session_GetApplication.
application *Application
// defaultOperationOptions carry DefaultQueryTimeout. They are used for
// nil operation options and non-positive query timeouts.
defaultOperationOptions *OperationOptions
// lifecycle orders starting queries against Close. startQuery holds a
// read lock from checking closed until MI_Session_QueryInstances has
// returned, so Close, which holds the write lock, never deletes options
// that a query is about to start with. closed is guarded by it.
lifecycle sync.RWMutex
closed bool
// optionsMu guards cachedOperationOptions, which concurrent queries fill
// under the read lock of lifecycle. The cached options carry fixed
// timeouts, see operationOptions. Like the default options, they are
// shared by concurrent operations and deleted by Close after
// MI_Session_Close has closed every operation.
optionsMu sync.Mutex
cachedOperationOptions map[time.Duration]*OperationOptions
}
// SessionFT represents the function table for Session.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/ns-mi-mi_session
type SessionFT struct {
Close uintptr
GetApplication uintptr
GetInstance uintptr
ModifyInstance uintptr
CreateInstance uintptr
DeleteInstance uintptr
Invoke uintptr
EnumerateInstances uintptr
QueryInstances uintptr
AssociatorInstances uintptr
ReferenceInstances uintptr
Subscribe uintptr
GetClass uintptr
EnumerateClasses uintptr
TestConnection uintptr
}
// Close closes a session and releases all associated memory.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_session_close
func (s *Session) Close() error {
if s == nil || s.ft == nil {
return ErrNotInitialized
}
// The write lock waits for queries that are being started, see
// startQuery, and makes queries started afterwards fail instead of using
// options that are about to be deleted.
s.lifecycle.Lock()
defer s.lifecycle.Unlock()
if s.closed {
return ErrNotInitialized
}
s.closed = true
// MI_Session_Close cancels running operations and blocks until their
// handles are closed. The default and cached options are deleted
// afterwards, so an operation that is still open never uses freed options.
// The goroutines that close the operations do not take lifecycle.
r0, _, _ := syscall.SyscallN(s.ft.Close,
uintptr(unsafe.Pointer(s)),
0,
0,
)
s.optionsMu.Lock()
for _, operationOptions := range s.cachedOperationOptions {
_ = operationOptions.Delete()
}
s.cachedOperationOptions = nil
if s.defaultOperationOptions != nil {
_ = s.defaultOperationOptions.Delete()
s.defaultOperationOptions = nil
}
s.optionsMu.Unlock()
if result := ResultError(r0); !errors.Is(result, MI_RESULT_OK) {
return result
}
return nil
}
// TestConnection queries instances. It is used to test the connection.
// The function returns an operation that can be used to retrieve the result with [Operation.GetInstance]. The operation must be closed with [Operation.Close].
// The instance returned by [Operation.GetInstance] is always nil.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_session_testconnection
func (s *Session) TestConnection() error {
if s == nil || s.ft == nil {
return ErrNotInitialized
}
operation := &Operation{}
// MI_Session_TestConnection returns void. The outcome, e.g. a timeout, is
// reported by GetInstance, and the operation must be closed either way.
//
// ref: https://github.com/KurtDeGreeff/omi/blob/9caa55032a1070a665e14fd282a091f6247d13c3/Unix/scriptext/py/PMI_Session.c#L92-L105
_, _, _ = syscall.SyscallN(
s.ft.TestConnection,
uintptr(unsafe.Pointer(s)),
0,
0,
uintptr(unsafe.Pointer(operation)),
)
_, _, err := operation.GetInstance()
if closeErr := operation.Close(); err == nil && closeErr != nil {
return fmt.Errorf("failed to close operation: %w", closeErr)
}
if err != nil {
return fmt.Errorf("failed to get instance: %w", err)
}
return nil
}
// GetApplication gets the Application handle that was used to create the specified session.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_session_getapplication
func (s *Session) GetApplication() (*Application, error) {
if s == nil || s.ft == nil {
return nil, ErrNotInitialized
}
application := &Application{}
r0, _, _ := syscall.SyscallN(
s.ft.GetApplication,
uintptr(unsafe.Pointer(s)),
uintptr(unsafe.Pointer(application)),
)
if result := ResultError(r0); !errors.Is(result, MI_RESULT_OK) {
return nil, result
}
return application, nil
}
// QueryInstances queries for a set of instances based on a query expression.
// The results are read synchronously with [Operation.GetInstance]. Prefer
// [Session.Query] or [Session.QueryFunc], which receive them through callbacks;
// the synchronous path leaks WMI client handles under parallel load.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_session_queryinstances
func (s *Session) QueryInstances(flags OperationFlags, operationOptions *OperationOptions, namespaceName Namespace,
queryDialect QueryDialect, queryExpression string,
) (*Operation, error) {
if s == nil || s.ft == nil {
return nil, ErrNotInitialized
}
queryExpressionUTF16, err := windows.UTF16PtrFromString(queryExpression)
if err != nil {
return nil, err
}
return s.queryInstances(flags, operationOptions, namespaceName, queryDialect, queryExpressionUTF16), nil
}
// queryInstances starts an MI_Session_QueryInstances operation. Nil operationOptions
// fall back to the session defaults. The caller must close the returned operation.
//
// MI_Session_QueryInstances returns void: a failed query still yields an
// operation, and its status, e.g. a timeout or an invalid class, is reported
// by [Operation.GetInstance].
func (s *Session) queryInstances(flags OperationFlags, operationOptions *OperationOptions, namespaceName Namespace,
queryDialect QueryDialect, queryExpression Query,
) *Operation {
operation := &Operation{}
if operationOptions == nil {
operationOptions = s.defaultOperationOptions
}
_, _, _ = syscall.SyscallN(
s.ft.QueryInstances,
uintptr(unsafe.Pointer(s)),
uintptr(flags),
uintptr(unsafe.Pointer(operationOptions)),
uintptr(unsafe.Pointer(namespaceName)),
uintptr(unsafe.Pointer(queryDialect)),
uintptr(unsafe.Pointer(queryExpression)),
0,
uintptr(unsafe.Pointer(operation)),
)
return operation
}
// unmarshalInstance populates structValue from instance using the `mi` struct tags.
//
// skipMissing controls what happens when the instance has no element for a tagged field.
// QueryUnmarshal passes true, tolerating classes whose MOF gained fields the query did not
// select. Operation.Unmarshal passes false, so a missing element is reported as an error
// instead of being silently left at its zero value.
func unmarshalInstance(instance *Instance, fields []miField, structValue reflect.Value, skipMissing bool) error {
for _, f := range fields {
element, err := instance.GetElementByName(f.name)
if err != nil {
if skipMissing && errors.Is(err, MI_RESULT_NO_SUCH_PROPERTY) {
continue
}
return fmt.Errorf("failed to get element %s: %w", f.tag, err)
}
if err := setField(f.tag, structValue.Field(f.index), &element); err != nil {
return err
}
}
return nil
}
// setField assigns the value of element to field, rejecting Go field types that
// cannot hold the MI value instead of letting the reflect package panic.
func setField(miTag string, field reflect.Value, element *Element) error {
if element.IsNull() {
// MI makes no promise about the contents of a NULL element's union.
// Check the Go type against a zeroed copy, so a mismatched field fails
// whether or not the property is NULL, but leave the field untouched.
return setField(miTag, reflect.New(field.Type()).Elem(), &Element{valueType: element.valueType})
}
switch element.valueType {
case ValueTypeBOOLEAN:
if field.Kind() != reflect.Bool {
return fieldTypeError(miTag, field, "boolean")
}
field.SetBool(element.value == 1)
case ValueTypeUINT8, ValueTypeUINT16, ValueTypeUINT32, ValueTypeUINT64:
return setUintField(miTag, field, uint64(element.value))
case ValueTypeSINT8:
return setIntField(miTag, field, int64(int8(element.value)))
case ValueTypeSINT16:
return setIntField(miTag, field, int64(int16(element.value)))
case ValueTypeSINT32:
return setIntField(miTag, field, int64(int32(element.value)))
case ValueTypeSINT64:
return setIntField(miTag, field, int64(element.value))
case ValueTypeSTRING:
if field.Kind() != reflect.String {
return fieldTypeError(miTag, field, "string")
}
if element.value == 0 {
return nil
}
field.SetString(windows.UTF16PtrToString((*uint16)(unsafe.Pointer(element.value))))
case ValueTypeCHAR16:
// A CHAR16 is a single UTF-16 code unit, not a pointer to a string.
// Note: lone UTF-16 surrogates (0xD800–0xDFFF) are replaced with U+FFFD
// by string(rune(...)). Use a uint16 Go field when the raw code unit is needed.
if field.Kind() == reflect.String {
field.SetString(string(rune(uint16(element.value))))
return nil
}
return setUintField(miTag, field, uint64(uint16(element.value)))
case ValueTypeREAL32:
if field.Kind() != reflect.Float32 && field.Kind() != reflect.Float64 {
return fieldTypeError(miTag, field, "float")
}
field.SetFloat(float64(math.Float32frombits(uint32(element.value))))
case ValueTypeREAL64:
if field.Kind() != reflect.Float64 {
return fieldTypeError(miTag, field, "float")
}
field.SetFloat(math.Float64frombits(uint64(element.value)))
case ValueTypeUINT16A:
if field.Type() != reflect.TypeFor[[]uint16]() {
return fmt.Errorf("cannot unmarshal UINT16A into field of type %s, expected []uint16", field.Type())
}
field.Set(reflect.ValueOf(element.getUint16Array()))
case ValueTypeSTRINGA:
if field.Type() != reflect.TypeFor[[]string]() {
return fmt.Errorf("cannot unmarshal STRINGA into field of type %s, expected []string", field.Type())
}
field.Set(reflect.ValueOf(element.getStringArray()))
default:
return fmt.Errorf("unsupported value type: %d", element.valueType)
}
return nil
}
// setUintField assigns value to a numeric field, rejecting fields that cannot hold it
// instead of letting the reflect package panic or silently wrap.
func setUintField(miTag string, field reflect.Value, value uint64) error {
if !isUnsignedKind(field.Kind()) {
return fieldTypeError(miTag, field, "unsigned integer")
}
if field.OverflowUint(value) {
return fmt.Errorf("field %s of Go type %s cannot hold the MI unsigned value %d", miTag, field.Type(), value)
}
field.SetUint(value)
return nil
}
// setIntField assigns value to a numeric field, rejecting fields that cannot hold it.
// If the Go field is unsigned, non-negative values that fit are accepted; negative
// values are rejected with an error rather than silently wrapping.
func setIntField(miTag string, field reflect.Value, value int64) error {
if !isNumericKind(field.Kind()) {
return fieldTypeError(miTag, field, "signed integer")
}
if isUnsignedKind(field.Kind()) {
if value < 0 || field.OverflowUint(uint64(value)) {
return fmt.Errorf("field %s of Go type %s cannot hold the MI signed value %d", miTag, field.Type(), value)
}
field.SetUint(uint64(value))
return nil
}
if field.OverflowInt(value) {
return fmt.Errorf("field %s of Go type %s cannot hold the MI signed value %d", miTag, field.Type(), value)
}
field.SetInt(value)
return nil
}
func isNumericKind(k reflect.Kind) bool {
//nolint:exhaustive // We only care about fixed-width integer kinds.
switch k {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return true
default:
return false
}
}
func isUnsignedKind(k reflect.Kind) bool {
//nolint:exhaustive // We only care about unsigned integer kinds.
switch k {
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return true
default:
return false
}
}
func fieldTypeError(miTag string, field reflect.Value, miType string) error {
return fmt.Errorf("field %s is of Go type %s but the MI value is a %s", miTag, field.Type(), miType)
}
// QueryUnmarshal queries for a set of instances based on a query expression.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_session_queryinstances
func (s *Session) QueryUnmarshal[T any](dst *[]T,
flags OperationFlags, operationOptions *OperationOptions,
namespaceName Namespace, queryDialect QueryDialect, queryExpression Query,
) error {
if s == nil || s.ft == nil {
return ErrNotInitialized
}
fields, err := prepareUnmarshal(dst)
if err != nil {
return err
}
return s.query(flags, operationOptions, namespaceName, queryDialect, queryExpression, func(instance *Instance) error {
return appendInstance(dst, instance, fields, true)
})
}
// Query queries for a set of instances based on a query expression and
// unmarshals them into dst. It is [Session.QueryContext] without
// cancellation.
func (s *Session) Query[T any](dst *[]T, namespaceName Namespace, queryExpression Query, queryTimeout time.Duration) error {
return s.QueryContext(context.Background(), dst, namespaceName, queryExpression, queryTimeout)
}
// QueryContext queries for a set of instances based on a query expression and
// unmarshals them into dst.
//
// queryTimeout is the MI timeout of the query. A non-positive timeout selects
// [DefaultQueryTimeout], so MI never runs the query without a timeout. A
// positive timeout below 1ms is raised to 1ms, the shortest one MI honors.
//
// If ctx is done before the query starts, the query is not started. If ctx is
// done while the query runs, the operation is cancelled. Either way the
// context's cause is returned, unless the query failed before. QueryContext
// returns only after MI has delivered the final result of a started query.
func (s *Session) QueryContext[T any](ctx context.Context, dst *[]T, namespaceName Namespace, queryExpression Query,
queryTimeout time.Duration,
) error {
if s == nil || s.ft == nil {
return ErrNotInitialized
}
fields, err := prepareUnmarshal(dst)
if err != nil {
return err
}
if ctx.Err() != nil {
return context.Cause(ctx)
}
query := newAsyncQuery(func(instance *Instance) error {
return appendInstance(dst, instance, fields, true)
})
return s.runQuery(ctx, query, OperationFlagsStandardRTTI, nil, queryTimeout, namespaceName, QueryDialectWQL, queryExpression)
}
// QueryFunc runs a WQL query and calls fn for every returned instance. It is
// [Session.QueryFuncContext] without cancellation.
func (s *Session) QueryFunc(namespaceName Namespace, queryExpression Query, queryTimeout time.Duration, fn func(*Instance) error) error {
return s.QueryFuncContext(context.Background(), namespaceName, queryExpression, queryTimeout, fn)
}
// QueryFuncContext runs a WQL query and calls fn for every returned instance,
// in order. It is meant for queries whose result shape is only known at
// runtime, where [Session.QueryContext] cannot unmarshal into a struct.
//
// queryTimeout and ctx behave as for [Session.QueryContext]. Once ctx is
// done, fn is not called any more.
//
// The instance passed to fn, and every element read from it, is only valid
// until fn returns. If fn returns an error, the query is cancelled and that
// error is returned.
//
// fn runs on the calling goroutine, for a copy of each instance: MI delivers
// the instances to a callback on one of its threads, which copies them and
// returns right away. A panic in fn cancels the query and propagates to the
// caller once MI has finished the operation.
//
// MI does not wait for fn. With a slow fn, copies of the remaining result set
// queue up in native memory, and the query timeout does not stop fn; check a
// deadline in fn where that matters.
func (s *Session) QueryFuncContext(ctx context.Context, namespaceName Namespace, queryExpression Query,
queryTimeout time.Duration, fn func(*Instance) error,
) error {
if s == nil || s.ft == nil {
return ErrNotInitialized
}
if ctx.Err() != nil {
return context.Cause(ctx)
}
return s.runQuery(ctx, newCopyQuery(fn), OperationFlagsStandardRTTI, nil, queryTimeout, namespaceName, QueryDialectWQL, queryExpression)
}
// startQuery starts query with operationOptions or, if they are nil, with
// the options for queryTimeout, see operationOptions. It returns a function
// that releases the options once the operation is closed.
//
// The read lock of lifecycle is held from checking closed until
// MI_Session_QueryInstances has returned and the operation exists. Close
// takes the write lock, so it either fails this query first or waits until
// the operation exists, which MI_Session_Close then cancels and waits for,
// before it deletes the options.
func (s *Session) startQuery(query *asyncQuery, flags OperationFlags, operationOptions *OperationOptions, queryTimeout time.Duration,
namespaceName Namespace, queryDialect QueryDialect, queryExpression Query,
) (func(), error) {
s.lifecycle.RLock()
defer s.lifecycle.RUnlock()
if s.closed {
return nil, ErrNotInitialized
}
release := func() {}
if operationOptions == nil {
var err error
operationOptions, release, err = s.operationOptions(queryTimeout)
if err != nil {
return nil, err
}
}
query.start(s, flags, operationOptions, namespaceName, queryDialect, queryExpression)
return release, nil
}
// operationOptions returns the operation options for queryTimeout and a
// function that releases them once the operation that uses them is closed.
//
// - A non-positive timeout, or DefaultQueryTimeout itself, returns the
// session defaults.
// - A timeout of whole seconds is taken as a fixed, configured timeout, e.g.
// BuildQueryTimeout. Its options are created once and kept until Close,
// for up to maxCachedOperationOptions distinct timeouts. Every query with
// such a timeout saves three native calls.
// - Any other timeout, typically the remaining scrape budget, gets options
// of its own, which release deletes.
//
// The caller holds the read lock of lifecycle and has checked closed, see
// startQuery. After Close it fails with ErrNotInitialized.
func (s *Session) operationOptions(queryTimeout time.Duration) (*OperationOptions, func(), error) {
s.optionsMu.Lock()
defer s.optionsMu.Unlock()
if s.defaultOperationOptions == nil {
return nil, nil, ErrNotInitialized
}
if queryTimeout <= 0 || queryTimeout == DefaultQueryTimeout {
return s.defaultOperationOptions, func() {}, nil
}
queryTimeout = max(queryTimeout, minQueryTimeout)
if queryTimeout%time.Second != 0 {
return s.ownOperationOptions(queryTimeout)
}
if operationOptions, ok := s.cachedOperationOptions[queryTimeout]; ok {
return operationOptions, func() {}, nil
}
if len(s.cachedOperationOptions) >= maxCachedOperationOptions {
return s.ownOperationOptions(queryTimeout)
}
operationOptions, err := s.newOperationOptions(queryTimeout)
if err != nil {
return nil, nil, err
}
if s.cachedOperationOptions == nil {
s.cachedOperationOptions = make(map[time.Duration]*OperationOptions, 1)
}
s.cachedOperationOptions[queryTimeout] = operationOptions
return operationOptions, func() {}, nil
}
// ownOperationOptions returns new operation options for queryTimeout and a
// function that deletes them.
func (s *Session) ownOperationOptions(queryTimeout time.Duration) (*OperationOptions, func(), error) {
operationOptions, err := s.newOperationOptions(queryTimeout)
if err != nil {
return nil, nil, err
}
return operationOptions, func() { _ = operationOptions.Delete() }, nil
}
// newOperationOptions creates operation options carrying queryTimeout, which
// must be at least minQueryTimeout. The caller must delete them once the
// operation that uses them is closed.
func (s *Session) newOperationOptions(queryTimeout time.Duration) (*OperationOptions, error) {
operationOptions, err := s.application.NewOperationOptions()
if err != nil {
return nil, fmt.Errorf("failed to create operation options: %w", err)
}
if err = operationOptions.SetTimeout(queryTimeout); err != nil {
_ = operationOptions.Delete()
return nil, fmt.Errorf("failed to set timeout: %w", err)
}
return operationOptions, nil
}