mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
234 lines
6.6 KiB
Go
234 lines
6.6 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 && (amd64 || arm64)
|
|
|
|
package update
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log/slog"
|
|
"testing"
|
|
"unicode/utf16"
|
|
"unsafe"
|
|
|
|
"github.com/prometheus-community/windows_exporter/internal/ole"
|
|
"github.com/prometheus-community/windows_exporter/internal/ole/wuapi"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
dto "github.com/prometheus/client_model/go"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
func TestFetchUpdatesPartialCollection(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
countError bool
|
|
}{
|
|
{name: "skip unreadable and duplicate items"},
|
|
{name: "count failure", countError: true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var logs bytes.Buffer
|
|
|
|
logger := slog.New(slog.NewTextHandler(&logs, nil))
|
|
c := &Collector{
|
|
logger: logger,
|
|
pendingUpdate: prometheus.NewDesc("test_pending", "", []string{"id", "revision", "category", "severity", "title", "deployment_action"}, nil),
|
|
pendingUpdateLastPublished: prometheus.NewDesc("test_published", "", []string{"id", "revision"}, nil),
|
|
queryDurationSeconds: prometheus.NewDesc("test_duration", "", nil, nil),
|
|
lastScrapeMetric: prometheus.NewDesc("test_timestamp", "", nil, nil),
|
|
}
|
|
|
|
searcher, attempted, criteria := fakeUpdateSearch(tc.countError)
|
|
|
|
metrics, err := c.fetchUpdates(logger, searcher)
|
|
if tc.countError {
|
|
require.ErrorIs(t, err, ole.HRESULT(0x80070005))
|
|
require.Nil(t, metrics)
|
|
require.Empty(t, *attempted)
|
|
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, searchCriteria, *criteria)
|
|
require.Equal(t, []int32{0, 1, 2, 3}, *attempted)
|
|
require.Len(t, metrics, 6, "two distinct readable updates retain info and publication metrics plus scrape metrics")
|
|
|
|
ids := []string{}
|
|
actions := []string{}
|
|
|
|
for _, metric := range metrics {
|
|
if metric.Desc() != c.pendingUpdate {
|
|
continue
|
|
}
|
|
|
|
var sample dto.Metric
|
|
require.NoError(t, metric.Write(&sample))
|
|
|
|
for _, label := range sample.GetLabel() {
|
|
switch label.GetName() {
|
|
case "id":
|
|
ids = append(ids, label.GetValue())
|
|
case "deployment_action":
|
|
actions = append(actions, label.GetValue())
|
|
}
|
|
}
|
|
}
|
|
|
|
require.Equal(t, []string{"update-0", "update-2"}, ids)
|
|
require.Equal(t, []string{"installation", "optional_installation"}, actions)
|
|
require.Contains(t, logs.String(), "failed to fetch Windows Update history item")
|
|
require.Contains(t, logs.String(), "get collection item 1")
|
|
})
|
|
}
|
|
}
|
|
|
|
// fakeUpdateSearch implements only the native slots exercised by a scrape.
|
|
// Item 1 cannot be read; both surrounding items remain available. Item 0 is a
|
|
// regular update and item 2 an optional one. Item 3 repeats the identity of
|
|
// item 0. The search criteria are recorded.
|
|
func fakeUpdateSearch(countError bool) (*wuapi.UpdateSearcher, *[]int32, *string) {
|
|
release := windows.NewCallback(func(uintptr) uintptr { return 0 })
|
|
attempted := []int32{}
|
|
|
|
var criteria string
|
|
|
|
var selected int32
|
|
|
|
var categoryMethods [10]uintptr
|
|
|
|
categoryMethods[2] = release
|
|
categoryMethods[9] = windows.NewCallback(func(_ uintptr, out *int32) uintptr {
|
|
*out = 0
|
|
|
|
return 0
|
|
})
|
|
categories := &wuapi.CategoryCollection{VTable: &categoryMethods[0]}
|
|
|
|
var identityMethods [9]uintptr
|
|
|
|
identityMethods[2] = release
|
|
identityMethods[7] = windows.NewCallback(func(_ uintptr, out *int32) uintptr {
|
|
*out = 1
|
|
|
|
return 0
|
|
})
|
|
identityMethods[8] = windows.NewCallback(func(_ uintptr, out **uint16) uintptr {
|
|
return fakeBSTR(fmt.Sprintf("update-%d", selected%3), out)
|
|
})
|
|
identity := &wuapi.UpdateIdentity{VTable: &identityMethods[0]}
|
|
|
|
var updateMethods [49]uintptr
|
|
|
|
updateMethods[2] = release
|
|
updateMethods[7] = windows.NewCallback(func(_ uintptr, out **uint16) uintptr {
|
|
return fakeBSTR("readable update", out)
|
|
})
|
|
updateMethods[11] = windows.NewCallback(func(_ uintptr, out **wuapi.CategoryCollection) uintptr {
|
|
*out = categories
|
|
|
|
return 0
|
|
})
|
|
updateMethods[19] = windows.NewCallback(func(_ uintptr, out **wuapi.UpdateIdentity) uintptr {
|
|
*out = identity
|
|
|
|
return 0
|
|
})
|
|
updateMethods[30] = windows.NewCallback(func(_ uintptr, out *ole.DATE) uintptr {
|
|
*out = 25569 // Unix epoch.
|
|
|
|
return 0
|
|
})
|
|
updateMethods[34] = windows.NewCallback(func(_ uintptr, out **uint16) uintptr {
|
|
return fakeBSTR("Critical", out)
|
|
})
|
|
updateMethods[48] = windows.NewCallback(func(_ uintptr, out *wuapi.DeploymentAction) uintptr {
|
|
*out = wuapi.DeploymentActionInstallation
|
|
if selected == 2 {
|
|
*out = wuapi.DeploymentActionOptionalInstallation
|
|
}
|
|
|
|
return 0
|
|
})
|
|
update := &wuapi.Update{VTable: &updateMethods[0]}
|
|
|
|
var collectionMethods [11]uintptr
|
|
|
|
collectionMethods[2] = release
|
|
collectionMethods[7] = windows.NewCallback(func(_ uintptr, index uintptr, out **wuapi.Update) uintptr {
|
|
selected = int32(index)
|
|
|
|
attempted = append(attempted, selected)
|
|
if selected == 1 {
|
|
return 0x80070005
|
|
}
|
|
|
|
*out = update
|
|
|
|
return 0
|
|
})
|
|
collectionMethods[10] = windows.NewCallback(func(_ uintptr, out *int32) uintptr {
|
|
if countError {
|
|
return 0x80070005
|
|
}
|
|
|
|
*out = 4
|
|
|
|
return 0
|
|
})
|
|
updates := &wuapi.UpdateCollection{VTable: &collectionMethods[0]}
|
|
|
|
var resultMethods [10]uintptr
|
|
|
|
resultMethods[2] = release
|
|
resultMethods[9] = windows.NewCallback(func(_ uintptr, out **wuapi.UpdateCollection) uintptr {
|
|
*out = updates
|
|
|
|
return 0
|
|
})
|
|
result := &wuapi.SearchResult{VTable: &resultMethods[0]}
|
|
|
|
var searcherMethods [20]uintptr
|
|
|
|
searcherMethods[2] = release
|
|
searcherMethods[19] = windows.NewCallback(func(_ uintptr, input *uint16, out **wuapi.SearchResult) uintptr {
|
|
criteria = windows.UTF16PtrToString(input)
|
|
*out = result
|
|
|
|
return 0
|
|
})
|
|
|
|
return &wuapi.UpdateSearcher{VTable: &searcherMethods[0]}, &attempted, &criteria
|
|
}
|
|
|
|
func fakeBSTR(value string, out **uint16) uintptr {
|
|
units := append(utf16.Encode([]rune(value)), 0)
|
|
|
|
ptr, _, _ := windows.NewLazySystemDLL("oleaut32.dll").NewProc("SysAllocStringLen").Call(
|
|
uintptr(unsafe.Pointer(&units[0])),
|
|
uintptr(len(units)-1),
|
|
)
|
|
if ptr == 0 {
|
|
return 0x8007000e
|
|
}
|
|
|
|
*out = (*uint16)(unsafe.Pointer(ptr))
|
|
|
|
return 0
|
|
}
|