diff --git a/internal/mi/application.go b/internal/mi/application.go index 248f58af..0b9ef16d 100644 --- a/internal/mi/application.go +++ b/internal/mi/application.go @@ -53,7 +53,7 @@ var ( ) // Application represents the MI application. -// https://learn.microsoft.com/de-de/windows/win32/api/mi/ns-mi-mi_application +// https://learn.microsoft.com/en-us/windows/win32/api/mi/ns-mi-mi_application type Application struct { reserved1 uint64 reserved2 uintptr @@ -61,7 +61,7 @@ type Application struct { } // ApplicationFT represents the function table of the MI application. -// https://learn.microsoft.com/de-de/windows/win32/api/mi/ns-mi-mi_applicationft +// https://learn.microsoft.com/en-us/windows/win32/api/mi/ns-mi-mi_applicationft type ApplicationFT struct { Close uintptr NewSession uintptr @@ -214,16 +214,6 @@ func (application *Application) NewOperationOptions() (*OperationOptions, error) return operationOptions, nil } -// MustNewOperationOptions is the panicking version of NewOperationOptions. -func (application *Application) MustNewOperationOptions() *OperationOptions { - operationOptions, err := application.NewOperationOptions() - if err != nil { - panic(fmt.Sprintf("failed to create operation options: %v", err)) - } - - return operationOptions -} - // NewDestinationOptions creates an DestinationOptions object that can be used with the Application.NewSession function. // // https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_application_newdestinationoptions diff --git a/internal/mi/doc.go b/internal/mi/doc.go index 58cf9dff..38893c84 100644 --- a/internal/mi/doc.go +++ b/internal/mi/doc.go @@ -15,8 +15,8 @@ //go:build windows -// mi is a package that provides a Go API for Windows Management Infrastructure (MI) functions. +// mi is a package that provides a Go API for Windows Management Infrastructure (MI) functions. // It requires Windows Management Framework 3.0 or later. // -// https://learn.microsoft.com/de-de/previous-versions/windows/desktop/wmi_v2/why-use-mi- +// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/wmi_v2/why-use-mi- package mi diff --git a/internal/mi/operation.go b/internal/mi/operation.go index 9af09ee5..e2f348a3 100644 --- a/internal/mi/operation.go +++ b/internal/mi/operation.go @@ -92,18 +92,6 @@ type OperationOptionsFT struct { GetInterval uintptr } -type OperationCallbacks[T any] struct { - CallbackContext *T - PromptUser uintptr - WriteError uintptr - WriteMessage uintptr - WriteProgress uintptr - InstanceResult uintptr - IndicationResult uintptr - ClassResult uintptr - StreamedParameterResult uintptr -} - // Close closes an operation handle. // // https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_operation_close diff --git a/internal/mi/result.go b/internal/mi/result.go index 745058bd..cb66aa78 100644 --- a/internal/mi/result.go +++ b/internal/mi/result.go @@ -17,7 +17,7 @@ package mi -import "errors" +import "strconv" type ResultError uint32 @@ -40,6 +40,11 @@ const ( MI_RESULT_INVALID_QUERY MI_RESULT_METHOD_NOT_AVAILABLE MI_RESULT_METHOD_NOT_FOUND + // MI_Result has no values 18 and 19. + // + // https://learn.microsoft.com/en-us/windows/win32/api/mi/ne-mi-mi_result + _ + _ MI_RESULT_NAMESPACE_NOT_EMPTY MI_RESULT_INVALID_ENUMERATION_CONTEXT MI_RESULT_INVALID_OPERATION_TIMEOUT @@ -55,63 +60,43 @@ func (r ResultError) Error() string { return r.String() } +//nolint:gochecknoglobals +var resultNames = [...]string{ + MI_RESULT_OK: "MI_RESULT_OK", + MI_RESULT_FAILED: "MI_RESULT_FAILED", + MI_RESULT_ACCESS_DENIED: "MI_RESULT_ACCESS_DENIED", + MI_RESULT_INVALID_NAMESPACE: "MI_RESULT_INVALID_NAMESPACE", + MI_RESULT_INVALID_PARAMETER: "MI_RESULT_INVALID_PARAMETER", + MI_RESULT_INVALID_CLASS: "MI_RESULT_INVALID_CLASS", + MI_RESULT_NOT_FOUND: "MI_RESULT_NOT_FOUND", + MI_RESULT_NOT_SUPPORTED: "MI_RESULT_NOT_SUPPORTED", + MI_RESULT_CLASS_HAS_CHILDREN: "MI_RESULT_CLASS_HAS_CHILDREN", + MI_RESULT_CLASS_HAS_INSTANCES: "MI_RESULT_CLASS_HAS_INSTANCES", + MI_RESULT_INVALID_SUPERCLASS: "MI_RESULT_INVALID_SUPERCLASS", + MI_RESULT_ALREADY_EXISTS: "MI_RESULT_ALREADY_EXISTS", + MI_RESULT_NO_SUCH_PROPERTY: "MI_RESULT_NO_SUCH_PROPERTY", + MI_RESULT_TYPE_MISMATCH: "MI_RESULT_TYPE_MISMATCH", + MI_RESULT_QUERY_LANGUAGE_NOT_SUPPORTED: "MI_RESULT_QUERY_LANGUAGE_NOT_SUPPORTED", + MI_RESULT_INVALID_QUERY: "MI_RESULT_INVALID_QUERY", + MI_RESULT_METHOD_NOT_AVAILABLE: "MI_RESULT_METHOD_NOT_AVAILABLE", + MI_RESULT_METHOD_NOT_FOUND: "MI_RESULT_METHOD_NOT_FOUND", + MI_RESULT_NAMESPACE_NOT_EMPTY: "MI_RESULT_NAMESPACE_NOT_EMPTY", + MI_RESULT_INVALID_ENUMERATION_CONTEXT: "MI_RESULT_INVALID_ENUMERATION_CONTEXT", + MI_RESULT_INVALID_OPERATION_TIMEOUT: "MI_RESULT_INVALID_OPERATION_TIMEOUT", + MI_RESULT_PULL_HAS_BEEN_ABANDONED: "MI_RESULT_PULL_HAS_BEEN_ABANDONED", + MI_RESULT_PULL_CANNOT_BE_ABANDONED: "MI_RESULT_PULL_CANNOT_BE_ABANDONED", + MI_RESULT_FILTERED_ENUMERATION_NOT_SUPPORTED: "MI_RESULT_FILTERED_ENUMERATION_NOT_SUPPORTED", + MI_RESULT_CONTINUATION_ON_ERROR_NOT_SUPPORTED: "MI_RESULT_CONTINUATION_ON_ERROR_NOT_SUPPORTED", + MI_RESULT_SERVER_LIMITS_EXCEEDED: "MI_RESULT_SERVER_LIMITS_EXCEEDED", + MI_RESULT_SERVER_IS_SHUTTING_DOWN: "MI_RESULT_SERVER_IS_SHUTTING_DOWN", +} + +// String returns the name of the MI_Result. Codes without a known name keep +// their numeric value, so an unexpected native result stays identifiable. func (r ResultError) String() string { - switch { - case errors.Is(r, MI_RESULT_OK): - return "MI_RESULT_OK" - case errors.Is(r, MI_RESULT_FAILED): - return "MI_RESULT_FAILED" - case errors.Is(r, MI_RESULT_ACCESS_DENIED): - return "MI_RESULT_ACCESS_DENIED" - case errors.Is(r, MI_RESULT_INVALID_NAMESPACE): - return "MI_RESULT_INVALID_NAMESPACE" - case errors.Is(r, MI_RESULT_INVALID_PARAMETER): - return "MI_RESULT_INVALID_PARAMETER" - case errors.Is(r, MI_RESULT_INVALID_CLASS): - return "MI_RESULT_INVALID_CLASS" - case errors.Is(r, MI_RESULT_NOT_FOUND): - return "MI_RESULT_NOT_FOUND" - case errors.Is(r, MI_RESULT_NOT_SUPPORTED): - return "MI_RESULT_NOT_SUPPORTED" - case errors.Is(r, MI_RESULT_CLASS_HAS_CHILDREN): - return "MI_RESULT_CLASS_HAS_CHILDREN" - case errors.Is(r, MI_RESULT_CLASS_HAS_INSTANCES): - return "MI_RESULT_CLASS_HAS_INSTANCES" - case errors.Is(r, MI_RESULT_INVALID_SUPERCLASS): - return "MI_RESULT_INVALID_SUPERCLASS" - case errors.Is(r, MI_RESULT_ALREADY_EXISTS): - return "MI_RESULT_ALREADY_EXISTS" - case errors.Is(r, MI_RESULT_NO_SUCH_PROPERTY): - return "MI_RESULT_NO_SUCH_PROPERTY" - case errors.Is(r, MI_RESULT_TYPE_MISMATCH): - return "MI_RESULT_TYPE_MISMATCH" - case errors.Is(r, MI_RESULT_QUERY_LANGUAGE_NOT_SUPPORTED): - return "MI_RESULT_QUERY_LANGUAGE_NOT_SUPPORTED" - case errors.Is(r, MI_RESULT_INVALID_QUERY): - return "MI_RESULT_INVALID_QUERY" - case errors.Is(r, MI_RESULT_METHOD_NOT_AVAILABLE): - return "MI_RESULT_METHOD_NOT_AVAILABLE" - case errors.Is(r, MI_RESULT_METHOD_NOT_FOUND): - return "MI_RESULT_METHOD_NOT_FOUND" - case errors.Is(r, MI_RESULT_NAMESPACE_NOT_EMPTY): - return "MI_RESULT_NAMESPACE_NOT_EMPTY" - case errors.Is(r, MI_RESULT_INVALID_ENUMERATION_CONTEXT): - return "MI_RESULT_INVALID_ENUMERATION_CONTEXT" - case errors.Is(r, MI_RESULT_INVALID_OPERATION_TIMEOUT): - return "MI_RESULT_INVALID_OPERATION_TIMEOUT" - case errors.Is(r, MI_RESULT_PULL_HAS_BEEN_ABANDONED): - return "MI_RESULT_PULL_HAS_BEEN_ABANDONED" - case errors.Is(r, MI_RESULT_PULL_CANNOT_BE_ABANDONED): - return "MI_RESULT_PULL_CANNOT_BE_ABANDONED" - case errors.Is(r, MI_RESULT_FILTERED_ENUMERATION_NOT_SUPPORTED): - return "MI_RESULT_FILTERED_ENUMERATION_NOT_SUPPORTED" - case errors.Is(r, MI_RESULT_CONTINUATION_ON_ERROR_NOT_SUPPORTED): - return "MI_RESULT_CONTINUATION_ON_ERROR_NOT_SUPPORTED" - case errors.Is(r, MI_RESULT_SERVER_LIMITS_EXCEEDED): - return "MI_RESULT_SERVER_LIMITS_EXCEEDED" - case errors.Is(r, MI_RESULT_SERVER_IS_SHUTTING_DOWN): - return "MI_RESULT_SERVER_IS_SHUTTING_DOWN" - default: - return "MI_RESULT_UNKNOWN" + if int(r) < len(resultNames) && resultNames[r] != "" { + return resultNames[r] } + + return "MI_RESULT_UNKNOWN(" + strconv.FormatUint(uint64(r), 10) + ")" } diff --git a/internal/mi/result_internal_test.go b/internal/mi/result_internal_test.go new file mode 100644 index 00000000..01705f83 --- /dev/null +++ b/internal/mi/result_internal_test.go @@ -0,0 +1,46 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestResultError_Values pins the constants to the native MI_Result values, +// which have no 18 and 19. +func TestResultError_Values(t *testing.T) { + t.Parallel() + + require.EqualValues(t, 17, MI_RESULT_METHOD_NOT_FOUND) + require.EqualValues(t, 20, MI_RESULT_NAMESPACE_NOT_EMPTY) + require.EqualValues(t, 22, MI_RESULT_INVALID_OPERATION_TIMEOUT) + require.EqualValues(t, 28, MI_RESULT_SERVER_IS_SHUTTING_DOWN) +} + +func TestResultError_String(t *testing.T) { + t.Parallel() + + require.Equal(t, "MI_RESULT_OK", MI_RESULT_OK.String()) + require.Equal(t, "MI_RESULT_NO_SUCH_PROPERTY", MI_RESULT_NO_SUCH_PROPERTY.Error()) + require.Equal(t, "MI_RESULT_INVALID_OPERATION_TIMEOUT", ResultError(22).String()) + require.Equal(t, "MI_RESULT_SERVER_IS_SHUTTING_DOWN", ResultError(28).String()) + require.Equal(t, "MI_RESULT_UNKNOWN(18)", ResultError(18).String()) + require.Equal(t, "MI_RESULT_UNKNOWN(1000)", ResultError(1000).String()) +} diff --git a/internal/mi/session.go b/internal/mi/session.go index e6a1455d..ee41fa4d 100644 --- a/internal/mi/session.go +++ b/internal/mi/session.go @@ -74,16 +74,20 @@ func (s *Session) Close() error { return ErrNotInitialized } - if s.defaultOperationOptions != nil { - _ = s.defaultOperationOptions.Delete() - } - + // MI_Session_Close cancels running operations and blocks until their + // handles are closed. The default options are deleted afterwards, so an + // operation that is still open never uses freed options. r0, _, _ := syscall.SyscallN(s.ft.Close, uintptr(unsafe.Pointer(s)), 0, 0, ) + if s.defaultOperationOptions != nil { + _ = s.defaultOperationOptions.Delete() + s.defaultOperationOptions = nil + } + if result := ResultError(r0); !errors.Is(result, MI_RESULT_OK) { return result } diff --git a/internal/mi/types.go b/internal/mi/types.go index 1bddf1a1..846b9d74 100644 --- a/internal/mi/types.go +++ b/internal/mi/types.go @@ -19,7 +19,6 @@ package mi import ( "time" - "unsafe" "github.com/prometheus-community/windows_exporter/internal/utils" "golang.org/x/sys/windows" @@ -115,38 +114,3 @@ type Datetime struct { Timestamp *Timestamp // Used when IsTimestamp is true Interval *Interval // Used when IsTimestamp is false } - -type PropertyDecl struct { - Flags uint32 - Code uint32 - Name *uint16 - Mqualifiers uintptr - NumQualifiers uint32 - PropertyType ValueType - ClassName *uint16 - Subscript uint32 - Offset uint32 - Origin *uint16 - Propagator *uint16 - Value uintptr -} - -func (c *ClassDecl) Properties() []*PropertyDecl { - // Create a slice to hold the properties - properties := make([]*PropertyDecl, c.NumProperties) - - // Mproperties is a pointer to an array of pointers to PropertyDecl - propertiesArray := (**PropertyDecl)(unsafe.Pointer(c.Mproperties)) - - // Iterate over the number of properties and fetch each property - for i := range c.NumProperties { - // Get the property pointer at index i - propertyPtr := *(**PropertyDecl)(unsafe.Add(unsafe.Pointer(propertiesArray), uintptr(i)*unsafe.Sizeof(uintptr(0)))) - - // Append the property to the slice - properties[i] = propertyPtr - } - - // Return the slice of properties - return properties -} diff --git a/internal/mi/value.go b/internal/mi/value.go index 00059563..2ea217cb 100644 --- a/internal/mi/value.go +++ b/internal/mi/value.go @@ -28,7 +28,7 @@ import ( "golang.org/x/sys/windows" ) -type ValueType int +type ValueType uint32 //nolint:iotamixing const ( @@ -229,7 +229,14 @@ func (i *Interval) Duration() time.Duration { time.Duration(i.Microseconds)*time.Microsecond } +// GetValue returns the element's value as the matching Go type. A NULL +// element returns nil without an error, since MI makes no promise about the +// contents of its union. func (e *Element) GetValue() (any, error) { + if e.IsNull() { + return nil, nil //nolint:nilnil + } + switch e.valueType { case ValueTypeBOOLEAN: return e.value == 1, nil diff --git a/internal/mi/value_internal_test.go b/internal/mi/value_internal_test.go index 10613d0b..f9a5178d 100644 --- a/internal/mi/value_internal_test.go +++ b/internal/mi/value_internal_test.go @@ -221,3 +221,21 @@ func TestElementGetValueStringArray(t *testing.T) { require.NoError(t, err) require.Nil(t, got) } + +// TestValueType_Size guards the MI_Type out-parameter of MI_Instance_GetElement, +// which the native side writes as a 32-bit enum. +func TestValueType_Size(t *testing.T) { + t.Parallel() + + require.Equal(t, uintptr(4), unsafe.Sizeof(ValueType(0))) +} + +func TestElement_GetValue_Null(t *testing.T) { + t.Parallel() + + for _, valueType := range []ValueType{ValueTypeUINT32, ValueTypeSTRING, ValueTypeDATETIME} { + value, err := newElement(valueType, [5]uint64{42}, flagNull).GetValue() + require.NoError(t, err) + require.Nil(t, value) + } +}