mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 01:59:04 +02:00
ole: replace go-ole with native COM bindings (#2551)
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
@@ -5,7 +5,6 @@ go 1.27.1
|
||||
require (
|
||||
github.com/alecthomas/kingpin/v2 v2.4.0
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2
|
||||
github.com/go-ole/go-ole v1.3.1-0.20250305162226-6867ec158e36
|
||||
github.com/prometheus/client_golang v1.24.1
|
||||
github.com/prometheus/client_model v0.6.3
|
||||
github.com/prometheus/common v0.72.0
|
||||
|
||||
@@ -12,8 +12,6 @@ github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj
|
||||
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.3.1-0.20250305162226-6867ec158e36 h1:PTRdMI7SqYj0FIO+xrw1gWgjB3ygEh1ME45PB0T+npQ=
|
||||
github.com/go-ole/go-ole v1.3.1-0.20250305162226-6867ec158e36/go.mod h1:2ip94f67up8I6Po4OnI0rjMeULNtsGHkQOLwhRZl5qk=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
@@ -53,7 +51,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/xhit/go-str2duration/v2 v2.2.0 h1:SofrD1D+8IuhhkP+OiG1ij4Zq+FiiYSEY/sORBanDG0=
|
||||
@@ -72,7 +69,6 @@ golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
|
||||
@@ -27,9 +27,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/go-ole/go-ole"
|
||||
"github.com/go-ole/go-ole/oleutil"
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/ole"
|
||||
"github.com/prometheus-community/windows_exporter/internal/ole/taskschd"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
@@ -292,232 +292,147 @@ func (c *Collector) collectMetrics(ch chan<- prometheus.Metric, scheduledTasks S
|
||||
}
|
||||
}
|
||||
|
||||
// CLSID_TaskScheduler {0F87369F-A4E5-4CFC-BD3E-73E6154572DD}: https://github.com/microsoft/win32metadata/blob/76c04c2021ef4a831a6f1e06d9566002d746139b/generation/WinSDK/RecompiledIdlHeaders/um/taskschd.h#L10359
|
||||
//
|
||||
//nolint:gochecknoglobals // CLSID, not ProgID: go-ole's CLSIDFromProgID can free the ProgID buffer mid-call
|
||||
var taskSchedulerCLSID = ole.GUID{
|
||||
Data1: 0x0F87369F,
|
||||
Data2: 0xA4E5,
|
||||
Data3: 0x4CFC,
|
||||
Data4: [8]byte{0xBD, 0x3E, 0x73, 0xE6, 0x15, 0x45, 0x72, 0xDD},
|
||||
}
|
||||
|
||||
// S_FALSE is returned by CoInitialize if it was already called on this thread.
|
||||
const S_FALSE = 0x00000001
|
||||
|
||||
// errTasksSkipped is returned with the readable tasks if some tasks or folders could not be read.
|
||||
// errTasksSkipped accompanies readable tasks when some tasks or folders could not be read.
|
||||
var errTasksSkipped = errors.New("tasks skipped")
|
||||
|
||||
func getScheduledTasks() (ScheduledTasks, error) {
|
||||
var scheduledTasks ScheduledTasks
|
||||
|
||||
// The only way to run WMI queries in parallel while being thread-safe is to
|
||||
// ensure the CoInitialize[Ex]() call is bound to its current OS thread.
|
||||
// Otherwise, attempting to initialize and run parallel queries across
|
||||
// goroutines will result in protected memory errors.
|
||||
// COM initialization and every interface call stay on the same OS thread.
|
||||
runtime.LockOSThread()
|
||||
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
if err := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_DISABLE_OLE1DDE); err != nil {
|
||||
if oleCode, ok := errors.AsType[*ole.OleError](err); ok && oleCode.Code() != ole.S_OK && oleCode.Code() != S_FALSE {
|
||||
return nil, err
|
||||
}
|
||||
if err := ole.Initialize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer ole.CoUninitialize()
|
||||
defer ole.Uninitialize()
|
||||
|
||||
taskSchedulerObj, err := ole.CreateInstance(&taskSchedulerCLSID, nil)
|
||||
if err != nil || taskSchedulerObj == nil {
|
||||
return scheduledTasks, err
|
||||
}
|
||||
defer taskSchedulerObj.Release()
|
||||
|
||||
taskServiceObj, err := taskSchedulerObj.QueryInterface(ole.IID_IDispatch)
|
||||
service, err := taskschd.NewTaskService()
|
||||
if err != nil {
|
||||
return scheduledTasks, fmt.Errorf("IID_IDispatch: %w", err)
|
||||
return nil, fmt.Errorf("create Task Scheduler service: %w", err)
|
||||
}
|
||||
defer service.Release()
|
||||
|
||||
if err := service.Connect(); err != nil {
|
||||
return nil, fmt.Errorf("connect Task Scheduler service: %w", err)
|
||||
}
|
||||
|
||||
defer taskServiceObj.Release()
|
||||
|
||||
_, err = oleutil.CallMethod(taskServiceObj, "Connect")
|
||||
root, err := service.Folder(`\`)
|
||||
if err != nil {
|
||||
return scheduledTasks, err
|
||||
return nil, fmt.Errorf("get root task folder: %w", err)
|
||||
}
|
||||
defer root.Release()
|
||||
|
||||
tasks := ScheduledTasks{}
|
||||
if err := fetchTasksRecursively(root, `\`, &tasks); err != nil {
|
||||
return tasks, fmt.Errorf("%w: %w", errTasksSkipped, err)
|
||||
}
|
||||
|
||||
res, err := oleutil.CallMethod(taskServiceObj, "GetFolder", `\`)
|
||||
if err != nil {
|
||||
return scheduledTasks, err
|
||||
}
|
||||
|
||||
rootFolderObj := res.ToIDispatch()
|
||||
defer rootFolderObj.Release()
|
||||
|
||||
if err = fetchTasksRecursively(rootFolderObj, `\`, &scheduledTasks); err != nil {
|
||||
return scheduledTasks, fmt.Errorf("%w: %w", errTasksSkipped, err)
|
||||
}
|
||||
|
||||
return scheduledTasks, nil
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// fetchTasksInFolder appends the tasks of folder to scheduledTasks.
|
||||
// A task that can't be read is skipped and its error is returned after all other tasks are read.
|
||||
func fetchTasksInFolder(folder *ole.IDispatch, scheduledTasks *ScheduledTasks) error {
|
||||
res, err := oleutil.CallMethod(folder, "GetTasks", 1)
|
||||
// fetchTasksInFolder appends readable tasks and reports errors after reading the remaining tasks.
|
||||
func fetchTasksInFolder(folder *taskschd.TaskFolder, scheduledTasks *ScheduledTasks) error {
|
||||
tasks, err := folder.Tasks()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get tasks: %w", err)
|
||||
}
|
||||
|
||||
tasks := res.ToIDispatch()
|
||||
defer tasks.Release()
|
||||
|
||||
errs := make([]error, 0)
|
||||
errs := []error{}
|
||||
|
||||
err = oleutil.ForEach(tasks, func(v *ole.VARIANT) error {
|
||||
task := v.ToIDispatch()
|
||||
defer task.Release()
|
||||
for task, err := range tasks.All() {
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("enumerate tasks: %w", err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
parsedTask, err := parseTask(task)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("parse task: %w", err))
|
||||
|
||||
return nil
|
||||
continue
|
||||
}
|
||||
|
||||
*scheduledTasks = append(*scheduledTasks, parsedTask)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("enumerate tasks: %w", err))
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// fetchTasksRecursively appends the tasks of folder and its sub folders to scheduledTasks.
|
||||
// A folder or task that can't be read is skipped and its error is returned after all other
|
||||
// folders are read.
|
||||
func fetchTasksRecursively(folder *ole.IDispatch, folderPath string, scheduledTasks *ScheduledTasks) error {
|
||||
errs := make([]error, 0)
|
||||
|
||||
// fetchTasksRecursively appends readable tasks, retaining errors from skipped folders or tasks.
|
||||
func fetchTasksRecursively(folder *taskschd.TaskFolder, folderPath string, scheduledTasks *ScheduledTasks) error {
|
||||
errs := []error{}
|
||||
if err := fetchTasksInFolder(folder, scheduledTasks); err != nil {
|
||||
errs = append(errs, fmt.Errorf("folder %s: %w", folderPath, err))
|
||||
}
|
||||
|
||||
res, err := oleutil.CallMethod(folder, "GetFolders", 1)
|
||||
folders, err := folder.Folders()
|
||||
if err != nil {
|
||||
return errors.Join(append(errs, fmt.Errorf("folder %s: get sub folders: %w", folderPath, err))...)
|
||||
}
|
||||
defer folders.Release()
|
||||
|
||||
subFolders := res.ToIDispatch()
|
||||
defer subFolders.Release()
|
||||
for subfolder, err := range folders.All() {
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("folder %s: enumerate sub folders: %w", folderPath, err))
|
||||
|
||||
err = oleutil.ForEach(subFolders, func(v *ole.VARIANT) error {
|
||||
subFolder := v.ToIDispatch()
|
||||
defer subFolder.Release()
|
||||
|
||||
subFolderPath := folderPath
|
||||
|
||||
if pathVar, err := oleutil.GetProperty(subFolder, "Path"); err == nil {
|
||||
subFolderPath = pathVar.ToString()
|
||||
_ = pathVar.Clear()
|
||||
continue
|
||||
}
|
||||
|
||||
if err := fetchTasksRecursively(subFolder, subFolderPath, scheduledTasks); err != nil {
|
||||
subfolderPath := folderPath
|
||||
if path, err := subfolder.Path(); err == nil {
|
||||
subfolderPath = path
|
||||
}
|
||||
|
||||
if err := fetchTasksRecursively(subfolder, subfolderPath, scheduledTasks); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("folder %s: enumerate sub folders: %w", folderPath, err))
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func parseTask(task *ole.IDispatch) (ScheduledTask, error) {
|
||||
var scheduledTask ScheduledTask
|
||||
|
||||
taskNameVar, err := oleutil.GetProperty(task, "Name")
|
||||
func parseTask(task *taskschd.RegisteredTask) (ScheduledTask, error) {
|
||||
name, err := task.Name()
|
||||
if err != nil {
|
||||
return scheduledTask, err
|
||||
return ScheduledTask{}, fmt.Errorf("get task name: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if tempErr := taskNameVar.Clear(); tempErr != nil {
|
||||
err = tempErr
|
||||
}
|
||||
}()
|
||||
|
||||
taskPathVar, err := oleutil.GetProperty(task, "Path")
|
||||
path, err := task.Path()
|
||||
if err != nil {
|
||||
return scheduledTask, err
|
||||
return ScheduledTask{}, fmt.Errorf("get task path: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if tempErr := taskPathVar.Clear(); tempErr != nil {
|
||||
err = tempErr
|
||||
}
|
||||
}()
|
||||
|
||||
taskEnabledVar, err := oleutil.GetProperty(task, "Enabled")
|
||||
enabled, err := task.Enabled()
|
||||
if err != nil {
|
||||
return scheduledTask, err
|
||||
return ScheduledTask{}, fmt.Errorf("get task enabled: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if tempErr := taskEnabledVar.Clear(); tempErr != nil {
|
||||
err = tempErr
|
||||
}
|
||||
}()
|
||||
|
||||
taskStateVar, err := oleutil.GetProperty(task, "State")
|
||||
state, err := task.State()
|
||||
if err != nil {
|
||||
return scheduledTask, err
|
||||
return ScheduledTask{}, fmt.Errorf("get task state: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if tempErr := taskStateVar.Clear(); tempErr != nil {
|
||||
err = tempErr
|
||||
}
|
||||
}()
|
||||
|
||||
taskNumberOfMissedRunsVar, err := oleutil.GetProperty(task, "NumberOfMissedRuns")
|
||||
missed, err := task.NumberOfMissedRuns()
|
||||
if err != nil {
|
||||
return scheduledTask, err
|
||||
return ScheduledTask{}, fmt.Errorf("get task missed runs: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if tempErr := taskNumberOfMissedRunsVar.Clear(); tempErr != nil {
|
||||
err = tempErr
|
||||
}
|
||||
}()
|
||||
|
||||
taskLastTaskResultVar, err := oleutil.GetProperty(task, "LastTaskResult")
|
||||
result, err := task.LastTaskResult()
|
||||
if err != nil {
|
||||
return scheduledTask, err
|
||||
return ScheduledTask{}, fmt.Errorf("get task last result: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if tempErr := taskLastTaskResultVar.Clear(); tempErr != nil {
|
||||
err = tempErr
|
||||
}
|
||||
}()
|
||||
|
||||
scheduledTask.Name = taskNameVar.ToString()
|
||||
scheduledTask.Path = strings.ReplaceAll(taskPathVar.ToString(), "\\", "/")
|
||||
|
||||
if val, ok := taskEnabledVar.Value().(bool); ok {
|
||||
scheduledTask.Enabled = val
|
||||
}
|
||||
|
||||
scheduledTask.State = TaskState(taskStateVar.Val)
|
||||
scheduledTask.MissedRunsCount = float64(taskNumberOfMissedRunsVar.Val)
|
||||
scheduledTask.LastTaskResult = TaskResult(taskLastTaskResultVar.Val)
|
||||
|
||||
return scheduledTask, err
|
||||
return ScheduledTask{
|
||||
Name: name,
|
||||
Path: strings.ReplaceAll(path, "\\", "/"),
|
||||
Enabled: enabled,
|
||||
State: TaskState(state),
|
||||
MissedRunsCount: float64(missed),
|
||||
LastTaskResult: TaskResult(result),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t TaskState) String() string {
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
// 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 scheduled_task
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/ole"
|
||||
"github.com/prometheus-community/windows_exporter/internal/ole/taskschd"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestFetchTasksInFolderPartialCollection(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
countError bool
|
||||
}{
|
||||
{name: "skip unreadable item"},
|
||||
{name: "count failure", countError: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var released, collectionsReleased int
|
||||
|
||||
items := []*ole.Object{fakeScheduledTask(1, &released), nil, fakeScheduledTask(3, &released)}
|
||||
collection, attempted := fakeTaskCollection(items, tc.countError, &collectionsReleased)
|
||||
folder := fakeTaskFolder(collection, nil, nil)
|
||||
tasks := ScheduledTasks{}
|
||||
|
||||
err := fetchTasksInFolder(folder, &tasks)
|
||||
require.ErrorIs(t, err, ole.HRESULT(0x80070005))
|
||||
require.Equal(t, 1, collectionsReleased)
|
||||
|
||||
if tc.countError {
|
||||
require.Empty(t, *attempted)
|
||||
require.Empty(t, tasks)
|
||||
require.Zero(t, released)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(t, []int64{1, 2, 3}, *attempted)
|
||||
require.Len(t, tasks, 2)
|
||||
require.Equal(t, TaskState(1), tasks[0].State)
|
||||
require.Equal(t, TaskState(3), tasks[1].State)
|
||||
require.Equal(t, 2, released)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchTasksRecursivelyPartialFolderCollection(t *testing.T) {
|
||||
var tasksReleased, foldersReleased, collectionsReleased int
|
||||
|
||||
children := make([]*ole.Object, 0, 2)
|
||||
|
||||
for _, state := range []int32{1, 3} {
|
||||
tasks, _ := fakeTaskCollection([]*ole.Object{fakeScheduledTask(state, &tasksReleased)}, false, &collectionsReleased)
|
||||
folders, _ := fakeTaskCollection(nil, false, &collectionsReleased)
|
||||
child := fakeTaskFolder(tasks, folders, &foldersReleased)
|
||||
children = append(children, &child.Object)
|
||||
}
|
||||
|
||||
folders, attempted := fakeTaskCollection([]*ole.Object{children[0], nil, children[1]}, false, &collectionsReleased)
|
||||
emptyTasks, _ := fakeTaskCollection(nil, false, &collectionsReleased)
|
||||
root := fakeTaskFolder(emptyTasks, folders, nil)
|
||||
tasks := ScheduledTasks{}
|
||||
|
||||
err := fetchTasksRecursively(root, `\`, &tasks)
|
||||
require.ErrorIs(t, err, ole.HRESULT(0x80070005))
|
||||
require.Equal(t, []int64{1, 2, 3}, *attempted)
|
||||
require.Len(t, tasks, 2)
|
||||
require.Equal(t, TaskState(1), tasks[0].State)
|
||||
require.Equal(t, TaskState(3), tasks[1].State)
|
||||
require.Equal(t, 2, tasksReleased)
|
||||
require.Equal(t, 2, foldersReleased)
|
||||
require.Equal(t, 6, collectionsReleased)
|
||||
}
|
||||
|
||||
// The fake native collections use the Task Scheduler's one-based VARIANT indices.
|
||||
func fakeTaskCollection(items []*ole.Object, countError bool, released *int) (*ole.Object, *[]int64) {
|
||||
var methods [9]uintptr
|
||||
|
||||
attempted := []int64{}
|
||||
methods[2] = windows.NewCallback(func(uintptr) uintptr {
|
||||
*released++
|
||||
|
||||
return 0
|
||||
})
|
||||
methods[7] = windows.NewCallback(func(_ uintptr, out *int32) uintptr {
|
||||
if countError {
|
||||
return 0x80070005
|
||||
}
|
||||
|
||||
*out = int32(len(items))
|
||||
|
||||
return 0
|
||||
})
|
||||
methods[8] = windows.NewCallback(func(_ uintptr, index *ole.Variant, out **ole.Object) uintptr {
|
||||
attempted = append(attempted, index.Value)
|
||||
|
||||
item := items[index.Value-1]
|
||||
if item == nil {
|
||||
return 0x80070005
|
||||
}
|
||||
|
||||
*out = item
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
return &ole.Object{VTable: &methods[0]}, &attempted
|
||||
}
|
||||
|
||||
func fakeTaskFolder(tasks, folders *ole.Object, released *int) *taskschd.TaskFolder {
|
||||
var methods [15]uintptr
|
||||
|
||||
methods[2] = windows.NewCallback(func(uintptr) uintptr {
|
||||
if released != nil {
|
||||
*released++
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
methods[8] = windows.NewCallback(func(_ uintptr, out *uintptr) uintptr {
|
||||
*out = 0 // A null BSTR is a valid empty string.
|
||||
|
||||
return 0
|
||||
})
|
||||
methods[10] = windows.NewCallback(func(_ uintptr, _ uintptr, out **ole.Object) uintptr {
|
||||
*out = folders
|
||||
|
||||
return 0
|
||||
})
|
||||
methods[14] = windows.NewCallback(func(_ uintptr, _ uintptr, out **ole.Object) uintptr {
|
||||
*out = tasks
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
return &taskschd.TaskFolder{VTable: &methods[0]}
|
||||
}
|
||||
|
||||
func fakeScheduledTask(state int32, released *int) *ole.Object {
|
||||
var methods [18]uintptr
|
||||
|
||||
methods[2] = windows.NewCallback(func(uintptr) uintptr {
|
||||
*released++
|
||||
|
||||
return 0
|
||||
})
|
||||
stringGetter := windows.NewCallback(func(_ uintptr, out *uintptr) uintptr {
|
||||
*out = 0
|
||||
|
||||
return 0
|
||||
})
|
||||
methods[7], methods[8] = stringGetter, stringGetter
|
||||
methods[9] = windows.NewCallback(func(_ uintptr, out *int32) uintptr {
|
||||
*out = state
|
||||
|
||||
return 0
|
||||
})
|
||||
methods[10] = windows.NewCallback(func(_ uintptr, out *int16) uintptr {
|
||||
*out = 0
|
||||
|
||||
return 0
|
||||
})
|
||||
intGetter := windows.NewCallback(func(_ uintptr, out *int32) uintptr {
|
||||
*out = 0
|
||||
|
||||
return 0
|
||||
})
|
||||
methods[16], methods[17] = intGetter, intGetter
|
||||
|
||||
return &ole.Object{VTable: &methods[0]}
|
||||
}
|
||||
@@ -29,9 +29,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/go-ole/go-ole"
|
||||
"github.com/go-ole/go-ole/oleutil"
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/ole"
|
||||
"github.com/prometheus-community/windows_exporter/internal/ole/wuapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/recovery"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -39,16 +39,6 @@ import (
|
||||
|
||||
const Name = "update"
|
||||
|
||||
// CLSID_UpdateSession {4CB43D7F-7EEE-4906-8698-60DA1C38F2FE}: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-uamg/e839e7e0-1795-451b-94ef-abacd6cbecac
|
||||
//
|
||||
//nolint:gochecknoglobals // CLSID, not ProgID: go-ole's CLSIDFromProgID can free the ProgID buffer mid-call
|
||||
var updateSessionCLSID = ole.GUID{
|
||||
Data1: 0x4CB43D7F,
|
||||
Data2: 0x7EEE,
|
||||
Data3: 0x4906,
|
||||
Data4: [8]byte{0x86, 0x98, 0x60, 0xDA, 0x1C, 0x38, 0xF2, 0xFE},
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Online bool `yaml:"online"`
|
||||
ScrapeInterval time.Duration `yaml:"scrape-interval"`
|
||||
@@ -204,93 +194,56 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
|
||||
}
|
||||
|
||||
func (c *Collector) scheduleUpdateStatus(ctx context.Context, logger *slog.Logger, initErrCh chan<- error, online bool) {
|
||||
// The only way to run WMI queries in parallel while being thread-safe is to
|
||||
// ensure the CoInitialize[Ex]() call is bound to its current OS thread.
|
||||
// Otherwise, attempting to initialize and run parallel queries across
|
||||
// goroutines will result in protected memory errors.
|
||||
// COM initialization and every interface call stay on the same OS thread.
|
||||
runtime.LockOSThread()
|
||||
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
if err := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_DISABLE_OLE1DDE); err != nil {
|
||||
var oleCode *ole.OleError
|
||||
if errors.As(err, &oleCode) && oleCode.Code() != ole.S_OK && oleCode.Code() != 0x00000001 {
|
||||
initErrCh <- fmt.Errorf("CoInitializeEx: %w", err)
|
||||
if err := ole.Initialize(); err != nil {
|
||||
initErrCh <- err
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
defer ole.CoUninitialize()
|
||||
defer ole.Uninitialize()
|
||||
|
||||
// Create a new instance of the WMI object
|
||||
sessionObj, err := ole.CreateInstance(&updateSessionCLSID, ole.IID_IUnknown)
|
||||
session, err := wuapi.NewUpdateSession()
|
||||
if err != nil {
|
||||
initErrCh <- fmt.Errorf("create Microsoft.Update.Session: %w", err)
|
||||
|
||||
return
|
||||
}
|
||||
defer session.Release()
|
||||
|
||||
defer sessionObj.Release()
|
||||
|
||||
// Query the IDispatch interface of the object
|
||||
musQueryInterface, err := sessionObj.QueryInterface(ole.IID_IDispatch)
|
||||
if err != nil {
|
||||
initErrCh <- fmt.Errorf("IID_IDispatch: %w", err)
|
||||
if err := session.SetUserLocale(1033); err != nil {
|
||||
initErrCh <- fmt.Errorf("set UserLocale: %w", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer musQueryInterface.Release()
|
||||
|
||||
_, err = oleutil.PutProperty(musQueryInterface, "UserLocale", 1033)
|
||||
if err != nil {
|
||||
initErrCh <- fmt.Errorf("failed to set ClientApplicationID: %w", err)
|
||||
if err := session.SetClientApplicationID("windows_exporter"); err != nil {
|
||||
initErrCh <- fmt.Errorf("set ClientApplicationID: %w", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, err = oleutil.PutProperty(musQueryInterface, "ClientApplicationID", "windows_exporter")
|
||||
if err != nil {
|
||||
initErrCh <- fmt.Errorf("failed to set ClientApplicationID: %w", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdatesession-createupdatesearcher
|
||||
us, err := oleutil.CallMethod(musQueryInterface, "CreateUpdateSearcher")
|
||||
defer func(us *ole.VARIANT) {
|
||||
if us != nil {
|
||||
_ = us.Clear()
|
||||
}
|
||||
}(us)
|
||||
|
||||
searcher, err := session.CreateUpdateSearcher()
|
||||
if err != nil {
|
||||
initErrCh <- fmt.Errorf("create update searcher: %w", err)
|
||||
|
||||
return
|
||||
}
|
||||
defer searcher.Release()
|
||||
|
||||
// ush shares the reference of us, which is released by us.Clear().
|
||||
ush := us.ToIDispatch()
|
||||
|
||||
_, err = oleutil.PutProperty(ush, "Online", online)
|
||||
if err != nil {
|
||||
initErrCh <- fmt.Errorf("put Online: %w", err)
|
||||
if err := searcher.SetOnline(online); err != nil {
|
||||
initErrCh <- fmt.Errorf("set Online: %w", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// lets use the fast local-only query to check if WindowsUpdates service is enabled on the host
|
||||
hc, err := oleutil.CallMethod(ush, "GetTotalHistoryCount")
|
||||
defer func(hc *ole.VARIANT) {
|
||||
if hc != nil {
|
||||
_ = hc.Clear()
|
||||
}
|
||||
}(hc)
|
||||
|
||||
if err != nil {
|
||||
initErrCh <- ErrUpdateServiceDisabled
|
||||
// A local history query checks that the Windows Update service is enabled.
|
||||
if _, err := searcher.GetTotalHistoryCount(); err != nil {
|
||||
initErrCh <- fmt.Errorf("get update history count: %w", errors.Join(ErrUpdateServiceDisabled, err))
|
||||
|
||||
return
|
||||
}
|
||||
@@ -304,7 +257,7 @@ func (c *Collector) scheduleUpdateStatus(ctx context.Context, logger *slog.Logge
|
||||
err = recovery.Run(func() error {
|
||||
var err error
|
||||
|
||||
metricsBuf, err = c.fetchUpdates(logger, ush)
|
||||
metricsBuf, err = c.fetchUpdates(logger, searcher)
|
||||
|
||||
return err
|
||||
})
|
||||
@@ -331,16 +284,18 @@ func (c *Collector) scheduleUpdateStatus(ctx context.Context, logger *slog.Logge
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Collector) fetchUpdates(logger *slog.Logger, usd *ole.IDispatch) ([]prometheus.Metric, error) {
|
||||
func (c *Collector) fetchUpdates(logger *slog.Logger, searcher *wuapi.UpdateSearcher) ([]prometheus.Metric, error) {
|
||||
metricsBuf := make([]prometheus.Metric, 0, len(c.metricsBuf)*2+1)
|
||||
|
||||
timeStart := time.Now()
|
||||
|
||||
usr, err := oleutil.CallMethod(usd, "Search", "IsInstalled=0 and IsHidden=0")
|
||||
result, err := searcher.Search("IsInstalled=0 and IsHidden=0")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search for updates: %w", err)
|
||||
}
|
||||
|
||||
defer result.Release()
|
||||
|
||||
logger.Debug(fmt.Sprintf("search for updates took %s", time.Since(timeStart)))
|
||||
|
||||
metricsBuf = append(metricsBuf, prometheus.MustNewConstMetric(
|
||||
@@ -349,24 +304,24 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, usd *ole.IDispatch) ([]pro
|
||||
time.Since(timeStart).Seconds(),
|
||||
))
|
||||
|
||||
usrd := usr.ToIDispatch()
|
||||
defer usrd.Release()
|
||||
|
||||
upd, err := oleutil.GetProperty(usrd, "Updates")
|
||||
updates, err := result.Updates()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get updates: %w", err)
|
||||
}
|
||||
defer updates.Release()
|
||||
|
||||
updd := upd.ToIDispatch()
|
||||
defer updd.Release()
|
||||
for item, err := range updates.All() {
|
||||
if err != nil {
|
||||
if _, ok := errors.AsType[*ole.CollectionItemError](err); !ok {
|
||||
return nil, fmt.Errorf("enumerate updates: %w", err)
|
||||
}
|
||||
|
||||
countUpdd, err := oleutil.GetProperty(updd, "Count")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get updates count: %w", err)
|
||||
}
|
||||
logger.Error("failed to fetch Windows Update history item", slog.Any("err", err))
|
||||
|
||||
for i := range int(countUpdd.Val) {
|
||||
update, err := c.getUpdateStatus(updd, i)
|
||||
continue
|
||||
}
|
||||
|
||||
update, err := c.getUpdateStatus(item)
|
||||
if err != nil {
|
||||
logger.Error("failed to fetch Windows Update history item",
|
||||
slog.Any("err", err),
|
||||
@@ -418,73 +373,47 @@ type windowsUpdate struct {
|
||||
// getUpdateStatus retrieves the update status of the given item.
|
||||
// other available properties can be found here:
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/aa386114(v=vs.85)
|
||||
func (c *Collector) getUpdateStatus(updd *ole.IDispatch, item int) (windowsUpdate, error) {
|
||||
itemRaw, err := oleutil.GetProperty(updd, "Item", item)
|
||||
if err != nil {
|
||||
return windowsUpdate{}, fmt.Errorf("get update item: %w", err)
|
||||
}
|
||||
|
||||
updateItem := itemRaw.ToIDispatch()
|
||||
defer updateItem.Release()
|
||||
|
||||
severity, err := oleutil.GetProperty(updateItem, "MsrcSeverity")
|
||||
func (c *Collector) getUpdateStatus(item *wuapi.Update) (windowsUpdate, error) {
|
||||
severity, err := item.MsrcSeverity()
|
||||
if err != nil {
|
||||
return windowsUpdate{}, fmt.Errorf("get MsrcSeverity: %w", err)
|
||||
}
|
||||
|
||||
defer clearVariant(severity)
|
||||
|
||||
categoriesRaw, err := oleutil.GetProperty(updateItem, "Categories")
|
||||
categoryName, err := getUpdateCategory(item)
|
||||
if err != nil {
|
||||
return windowsUpdate{}, fmt.Errorf("get Categories: %w", err)
|
||||
return windowsUpdate{}, fmt.Errorf("get category: %w", err)
|
||||
}
|
||||
|
||||
categories := categoriesRaw.ToIDispatch()
|
||||
defer categories.Release()
|
||||
|
||||
categoryName, err := getUpdateCategory(categories)
|
||||
if err != nil {
|
||||
return windowsUpdate{}, fmt.Errorf("get Category: %w", err)
|
||||
}
|
||||
|
||||
title, err := oleutil.GetProperty(updateItem, "Title")
|
||||
title, err := item.Title()
|
||||
if err != nil {
|
||||
return windowsUpdate{}, fmt.Errorf("get Title: %w", err)
|
||||
}
|
||||
|
||||
defer clearVariant(title)
|
||||
|
||||
// Get the Identity object
|
||||
identityVariant, err := oleutil.GetProperty(updateItem, "Identity")
|
||||
identity, err := item.Identity()
|
||||
if err != nil {
|
||||
return windowsUpdate{}, fmt.Errorf("get Identity: %w", err)
|
||||
}
|
||||
|
||||
identity := identityVariant.ToIDispatch()
|
||||
defer identity.Release()
|
||||
|
||||
// Read the UpdateID
|
||||
updateIDVariant, err := oleutil.GetProperty(identity, "UpdateID")
|
||||
updateID, err := identity.UpdateID()
|
||||
if err != nil {
|
||||
return windowsUpdate{}, fmt.Errorf("get UpdateID: %w", err)
|
||||
}
|
||||
|
||||
defer clearVariant(updateIDVariant)
|
||||
|
||||
revisionVariant, err := oleutil.GetProperty(identity, "RevisionNumber")
|
||||
revision, err := identity.RevisionNumber()
|
||||
if err != nil {
|
||||
return windowsUpdate{}, fmt.Errorf("get RevisionNumber: %w", err)
|
||||
}
|
||||
|
||||
lastPublished, err := oleutil.GetProperty(updateItem, "LastDeploymentChangeTime")
|
||||
lastPublished, err := item.LastDeploymentChangeTime()
|
||||
if err != nil {
|
||||
return windowsUpdate{}, fmt.Errorf("get LastDeploymentChangeTime: %w", err)
|
||||
}
|
||||
|
||||
lastPublishedDate, err := ole.GetVariantDate(uint64(lastPublished.Val))
|
||||
lastPublishedDate, err := lastPublished.Time()
|
||||
if err != nil {
|
||||
c.logger.Debug("failed to convert LastDeploymentChangeTime",
|
||||
slog.String("title", title.ToString()),
|
||||
slog.String("title", title),
|
||||
slog.Any("err", err),
|
||||
)
|
||||
|
||||
@@ -492,66 +421,46 @@ func (c *Collector) getUpdateStatus(updd *ole.IDispatch, item int) (windowsUpdat
|
||||
}
|
||||
|
||||
return windowsUpdate{
|
||||
identity: updateIDVariant.ToString(),
|
||||
revision: strconv.FormatInt(revisionVariant.Val, 10),
|
||||
identity: updateID,
|
||||
revision: strconv.FormatInt(int64(revision), 10),
|
||||
category: categoryName,
|
||||
severity: severity.ToString(),
|
||||
title: title.ToString(),
|
||||
severity: severity,
|
||||
title: title,
|
||||
lastPublished: lastPublishedDate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getUpdateCategory(categories *ole.IDispatch) (string, error) {
|
||||
var categoryName string
|
||||
|
||||
categoryCount, err := oleutil.GetProperty(categories, "Count")
|
||||
func getUpdateCategory(update *wuapi.Update) (string, error) {
|
||||
categories, err := update.Categories()
|
||||
if err != nil {
|
||||
return categoryName, fmt.Errorf("get Categories count: %w", err)
|
||||
return "", fmt.Errorf("get Categories: %w", err)
|
||||
}
|
||||
defer categories.Release()
|
||||
|
||||
var categoryName string
|
||||
|
||||
order := int64(math.MaxInt64)
|
||||
|
||||
for i := range categoryCount.Val {
|
||||
err = func(i int64) error {
|
||||
categoryRaw, err := oleutil.GetProperty(categories, "Item", i)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get Category item: %w", err)
|
||||
}
|
||||
|
||||
category := categoryRaw.ToIDispatch()
|
||||
defer category.Release()
|
||||
|
||||
categoryNameRaw, err := oleutil.GetProperty(category, "Name")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get Category item Name: %w", err)
|
||||
}
|
||||
|
||||
defer clearVariant(categoryNameRaw)
|
||||
|
||||
orderRaw, err := oleutil.GetProperty(category, "Order")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get Category item Order: %w", err)
|
||||
}
|
||||
|
||||
if orderRaw.Val < order {
|
||||
order = orderRaw.Val
|
||||
categoryName = categoryNameRaw.ToString()
|
||||
}
|
||||
|
||||
return nil
|
||||
}(i)
|
||||
for category, err := range categories.All() {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get Category item: %w", err)
|
||||
return "", fmt.Errorf("enumerate categories: %w", err)
|
||||
}
|
||||
|
||||
name, err := category.Name()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get category Name: %w", err)
|
||||
}
|
||||
|
||||
categoryOrder, err := category.Order()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get category Order: %w", err)
|
||||
}
|
||||
|
||||
if int64(categoryOrder) < order {
|
||||
order = int64(categoryOrder)
|
||||
categoryName = name
|
||||
}
|
||||
}
|
||||
|
||||
return categoryName, nil
|
||||
}
|
||||
|
||||
// clearVariant frees the value of a VARIANT, for example a BSTR.
|
||||
// Values must be copied, e.g. with [ole.VARIANT.ToString], before.
|
||||
func clearVariant(v *ole.VARIANT) {
|
||||
if v != nil {
|
||||
_ = v.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// 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 item"},
|
||||
{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"}, 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 := 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, []int32{0, 1, 2}, *attempted)
|
||||
require.Len(t, metrics, 6, "two readable updates retain info and publication metrics plus scrape metrics")
|
||||
|
||||
ids := []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() {
|
||||
if label.GetName() == "id" {
|
||||
ids = append(ids, label.GetValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, []string{"update-0", "update-2"}, ids)
|
||||
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.
|
||||
func fakeUpdateSearch(countError bool) (*wuapi.UpdateSearcher, *[]int32) {
|
||||
release := windows.NewCallback(func(uintptr) uintptr { return 0 })
|
||||
attempted := []int32{}
|
||||
|
||||
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), out)
|
||||
})
|
||||
identity := &wuapi.UpdateIdentity{VTable: &identityMethods[0]}
|
||||
|
||||
var updateMethods [35]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)
|
||||
})
|
||||
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 = 3
|
||||
|
||||
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, _ *uint16, out **wuapi.SearchResult) uintptr {
|
||||
*out = result
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
return &wuapi.UpdateSearcher{VTable: &searcherMethods[0]}, &attempted
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -16,8 +16,8 @@
|
||||
package cfgmgr32
|
||||
|
||||
import (
|
||||
"github.com/go-ole/go-ole"
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/win32"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,7 +33,7 @@ const (
|
||||
|
||||
// DEVPROPKEY represents a device property key (GUID + pid)
|
||||
type DEVPROPKEY struct {
|
||||
FmtID ole.GUID
|
||||
FmtID windows.GUID
|
||||
PID uint32
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ type Device struct {
|
||||
var (
|
||||
// https://github.com/Infinidat/infi.devicemanager/blob/8be9ead6b04ff45c63d9e3bc70d82cceafb75c47/src/infi/devicemanager/setupapi/properties.py#L138C1-L143C34
|
||||
DEVPKEYDeviceBusNumber = &DEVPROPKEY{
|
||||
FmtID: ole.GUID{
|
||||
FmtID: windows.GUID{
|
||||
Data1: 0xa45c254e,
|
||||
Data2: 0xdf1c,
|
||||
Data3: 0x4efd,
|
||||
@@ -59,7 +59,7 @@ var (
|
||||
|
||||
// https://github.com/Infinidat/infi.devicemanager/blob/8be9ead6b04ff45c63d9e3bc70d82cceafb75c47/src/infi/devicemanager/setupapi/properties.py#L187-L192
|
||||
DEVPKEYDeviceAddress = &DEVPROPKEY{
|
||||
FmtID: ole.GUID{
|
||||
FmtID: windows.GUID{
|
||||
Data1: 0xa45c254e,
|
||||
Data2: 0xdf1c,
|
||||
Data3: 0x4efd,
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// MIB_TCPROW_OWNER_PID structure for IPv4.
|
||||
@@ -120,7 +120,7 @@ const (
|
||||
type MIB_IF_ROW2 struct {
|
||||
InterfaceLuid uint64
|
||||
InterfaceIndex uint32
|
||||
InterfaceGuid ole.GUID
|
||||
InterfaceGuid windows.GUID
|
||||
Alias [IF_MAX_STRING_SIZE + 1]uint16
|
||||
Description [IF_MAX_STRING_SIZE + 1]uint16
|
||||
PhysicalAddressLength uint32
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
// 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 ole
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
ole32 = windows.NewLazySystemDLL("ole32.dll")
|
||||
oleaut32 = windows.NewLazySystemDLL("oleaut32.dll")
|
||||
coInitializeEx = ole32.NewProc("CoInitializeEx")
|
||||
coUninitialize = ole32.NewProc("CoUninitialize")
|
||||
coCreateInstance = ole32.NewProc("CoCreateInstance")
|
||||
sysAllocStringLen = oleaut32.NewProc("SysAllocStringLen")
|
||||
sysFreeString = oleaut32.NewProc("SysFreeString")
|
||||
sysStringLen = oleaut32.NewProc("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.
|
||||
func Initialize() error {
|
||||
hr, _, _ := coInitializeEx.Call(0, windows.COINIT_MULTITHREADED|windows.COINIT_DISABLE_OLE1DDE)
|
||||
if err := ResultError(hr); err != nil {
|
||||
return fmt.Errorf("initialize COM: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uninitialize balances a successful Initialize on the same locked OS thread.
|
||||
// Release all owned COM interfaces before calling it.
|
||||
func Uninitialize() {
|
||||
// CoUninitialize returns void; GetLastError is irrelevant.
|
||||
_, _, _ = coUninitialize.Call()
|
||||
}
|
||||
|
||||
// Call invokes a native COM method and returns its primary result. COM methods
|
||||
// report HRESULTs or reference counts directly; GetLastError is irrelevant.
|
||||
// Convert pointers to uintptr in the call expression so uintptrescapes moves
|
||||
// their storage to the heap, where reentrant Go callbacks cannot invalidate
|
||||
// native pointers by growing and moving the goroutine stack.
|
||||
//
|
||||
//go:uintptrescapes
|
||||
func Call(fn uintptr, args ...uintptr) uintptr {
|
||||
result, _, _ := syscall.SyscallN(fn, args...)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Object is the common native COM interface layout. VTable points to the native
|
||||
// function table and is owned by COM. Object must be the first field of every
|
||||
// interface wrapper; never copy it or move it between apartments.
|
||||
type Object struct{ VTable *uintptr }
|
||||
|
||||
// nativeScalar includes only output layouts used by these COM bindings. A Go
|
||||
// bool is deliberately excluded: COM VARIANT_BOOL uses a signed 16-bit integer.
|
||||
type nativeScalar interface {
|
||||
~int16 | ~int32 | ~uint32 | ~float64 | bstr
|
||||
}
|
||||
|
||||
// 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))))
|
||||
}
|
||||
|
||||
// Release relinquishes an owned interface reference. Do not call it on borrowed
|
||||
// iterator items. Like IUnknown::Release, it does not return an HRESULT.
|
||||
func (o *Object) Release() {
|
||||
_ = Call(o.Method(2), uintptr(unsafe.Pointer(o)))
|
||||
runtime.KeepAlive(o)
|
||||
}
|
||||
|
||||
// Get calls a native scalar getter. T must exactly match its native
|
||||
// output type. Pointer conversions stay in the Call expression so their storage
|
||||
// remains alive and stable during native calls and reentrant Go callbacks.
|
||||
//
|
||||
//nolint:ireturn // T is a native scalar output, not a Go interface.
|
||||
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)))
|
||||
runtime.KeepAlive(o)
|
||||
|
||||
return value, ResultError(hr)
|
||||
}
|
||||
|
||||
// GetArg calls a native getter with one input argument. T must exactly match
|
||||
// its native output type; arg must use the native argument representation.
|
||||
//
|
||||
//nolint:ireturn // T is a native scalar output, not a Go interface.
|
||||
//go:uintptrescapes
|
||||
func (o *Object) GetArg[T nativeScalar](slot, arg uintptr) (T, error) {
|
||||
var value T
|
||||
|
||||
hr := Call(
|
||||
o.Method(slot),
|
||||
uintptr(unsafe.Pointer(o)),
|
||||
arg,
|
||||
uintptr(unsafe.Pointer(&value)),
|
||||
)
|
||||
runtime.KeepAlive(o)
|
||||
|
||||
return value, ResultError(hr)
|
||||
}
|
||||
|
||||
// GetObject calls a native interface getter and returns an owned reference.
|
||||
// Successful calls must return a non-nil interface pointer.
|
||||
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)))
|
||||
runtime.KeepAlive(o)
|
||||
|
||||
if err := ResultError(hr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
return nil, fmt.Errorf("COM getter at slot %d returned a nil interface", slot)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GetObjectArg calls a native interface getter with one input argument and
|
||||
// returns an owned reference. The argument must use its native representation.
|
||||
// Convert pointer arguments to uintptr in the call expression.
|
||||
//
|
||||
//go:uintptrescapes
|
||||
func (o *Object) GetObjectArg[T nativeObject](slot, arg uintptr) (*T, error) {
|
||||
var value *T
|
||||
|
||||
hr := Call(
|
||||
o.Method(slot),
|
||||
uintptr(unsafe.Pointer(o)),
|
||||
arg,
|
||||
uintptr(unsafe.Pointer(&value)),
|
||||
)
|
||||
runtime.KeepAlive(o)
|
||||
|
||||
if err := ResultError(hr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
return nil, fmt.Errorf("COM getter at slot %d returned a nil interface", slot)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
runtime.KeepAlive(o)
|
||||
|
||||
return ResultError(hr)
|
||||
}
|
||||
|
||||
// Create activates class and returns an owned interface reference for iid.
|
||||
// T must be the matching native interface wrapper, with Object as its first field.
|
||||
func Create[T nativeObject](class, iid windows.GUID) (*T, error) {
|
||||
var value *T
|
||||
|
||||
hr, _, _ := coCreateInstance.Call(
|
||||
uintptr(unsafe.Pointer(&class)),
|
||||
0,
|
||||
0x1|0x4, // CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER
|
||||
uintptr(unsafe.Pointer(&iid)),
|
||||
uintptr(unsafe.Pointer(&value)),
|
||||
)
|
||||
if err := ResultError(hr); err != nil {
|
||||
return nil, fmt.Errorf("create COM instance: %w", err)
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
return nil, errors.New("create COM instance returned a nil interface")
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
type bstr struct{ ptr *uint16 }
|
||||
|
||||
func newBSTR(value string) (bstr, error) {
|
||||
units := utf16.Encode([]rune(value))
|
||||
// Keep a non-nil input buffer even for an empty string.
|
||||
units = append(units, 0)
|
||||
|
||||
p, _, _ := sysAllocStringLen.Call(uintptr(unsafe.Pointer(&units[0])), uintptr(len(units)-1))
|
||||
if p == 0 {
|
||||
return bstr{}, fmt.Errorf("allocate BSTR: %w", HRESULT(0x8007000e))
|
||||
}
|
||||
|
||||
return bstr{ptr: (*uint16)(unsafe.Pointer(p))}, nil
|
||||
}
|
||||
|
||||
func (b bstr) free() {
|
||||
// SysFreeString returns void and accepts a nil BSTR.
|
||||
_, _, _ = sysFreeString.Call(uintptr(unsafe.Pointer(b.ptr)))
|
||||
}
|
||||
|
||||
func (b bstr) string() string {
|
||||
if b.ptr == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
n, _, _ := sysStringLen.Call(uintptr(unsafe.Pointer(b.ptr)))
|
||||
|
||||
return string(utf16.Decode(unsafe.Slice(b.ptr, int(n))))
|
||||
}
|
||||
|
||||
// String calls a BSTR getter, copies its value, and frees the native string.
|
||||
func (o *Object) String(slot uintptr) (string, error) {
|
||||
value, err := o.Get[bstr](slot)
|
||||
defer value.free()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return value.string(), nil
|
||||
}
|
||||
|
||||
// Variant is the 24-byte VARIANT layout on both supported Windows architectures.
|
||||
// Native 64-bit ABIs pass this aggregate by address. Only VT_EMPTY and VT_I4
|
||||
// are needed for local Connect and one-based Task Scheduler collection indices.
|
||||
type Variant struct {
|
||||
Type uint16
|
||||
reserved [3]uint16
|
||||
Value int64
|
||||
padding [8]byte
|
||||
}
|
||||
|
||||
// The x64 ABI requires caller-owned aggregate arguments to be 16-byte aligned.
|
||||
// Go only guarantees eight-byte alignment for VARIANT, so reserve padding and
|
||||
// align explicitly. The same storage is also valid on arm64.
|
||||
type variantStorage [39]byte
|
||||
|
||||
func (s *variantStorage) variant() *Variant {
|
||||
offset := -uintptr(unsafe.Pointer(s)) & 15
|
||||
|
||||
return (*Variant)(unsafe.Add(unsafe.Pointer(s), offset))
|
||||
}
|
||||
|
||||
// GetObjectStringArg calls a native interface getter with one BSTR input and
|
||||
// returns an owned reference. The temporary BSTR is freed before returning.
|
||||
func (o *Object) GetObjectStringArg[T nativeObject](slot uintptr, input string) (*T, error) {
|
||||
value, err := newBSTR(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer value.free()
|
||||
|
||||
return o.GetObjectArg[T](slot, uintptr(unsafe.Pointer(value.ptr)))
|
||||
}
|
||||
|
||||
// PutString calls a setter with a BSTR input, freeing the temporary string.
|
||||
func (o *Object) PutString(slot uintptr, input string) error {
|
||||
value, err := newBSTR(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer value.free()
|
||||
|
||||
return o.Put(slot, uintptr(unsafe.Pointer(value.ptr)))
|
||||
}
|
||||
|
||||
// NewEmptyVariant returns caller-owned, 16-byte-aligned VT_EMPTY storage.
|
||||
// It contains no resources requiring VariantClear.
|
||||
func NewEmptyVariant() *Variant { return new(variantStorage).variant() }
|
||||
|
||||
// NewInt32Variant returns caller-owned, 16-byte-aligned VT_I4 storage.
|
||||
// It contains no resources requiring VariantClear.
|
||||
func NewInt32Variant(value int32) *Variant {
|
||||
v := NewEmptyVariant()
|
||||
v.Type = 3
|
||||
v.Value = int64(value)
|
||||
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// 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 ole
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestInitialize(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
require.NoError(t, Initialize())
|
||||
|
||||
defer Uninitialize()
|
||||
|
||||
// Verify the apartment selected by Initialize, rather than just its status.
|
||||
var apartment, qualifier int32
|
||||
|
||||
hr, _, _ := ole32.NewProc("CoGetApartmentType").Call(
|
||||
uintptr(unsafe.Pointer(&apartment)),
|
||||
uintptr(unsafe.Pointer(&qualifier)),
|
||||
)
|
||||
require.NoError(t, ResultError(hr))
|
||||
require.Equal(t, int32(1), apartment, "APTTYPE_MTA")
|
||||
|
||||
// The second initialization returns S_FALSE; it still needs balancing.
|
||||
require.NoError(t, Initialize())
|
||||
|
||||
defer Uninitialize()
|
||||
}
|
||||
|
||||
func TestBSTR(t *testing.T) {
|
||||
for _, value := range []string{"", "windows_exporter", "before\x00after", "Unicode \U0001F600"} {
|
||||
t.Run(value, func(t *testing.T) {
|
||||
str, err := newBSTR(value)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer str.free()
|
||||
|
||||
require.Equal(t, value, str.string())
|
||||
})
|
||||
}
|
||||
|
||||
require.Empty(t, (bstr{}).string())
|
||||
}
|
||||
|
||||
func TestNativeGetterCallbackStackGrowth(t *testing.T) {
|
||||
for _, name := range []string{"Get", "GetArg"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var methods [8]uintptr
|
||||
|
||||
if name == "Get" {
|
||||
methods[7] = windows.NewCallback(func(_ uintptr, out *int32) uintptr {
|
||||
// A native caller retains the original address even when the
|
||||
// callback grows and relocates the caller's Go stack.
|
||||
growCallbackStack(64)
|
||||
|
||||
*out = 1234
|
||||
|
||||
return 0
|
||||
})
|
||||
} else {
|
||||
methods[7] = windows.NewCallback(func(_ uintptr, arg uintptr, out *int32) uintptr {
|
||||
growCallbackStack(64)
|
||||
|
||||
*out = int32(arg)
|
||||
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
object := &Object{VTable: &methods[0]}
|
||||
|
||||
var (
|
||||
value int32
|
||||
err error
|
||||
)
|
||||
|
||||
if name == "Get" {
|
||||
value, err = object.Get[int32](7)
|
||||
} else {
|
||||
value, err = object.GetArg[int32](7, 1234)
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(1234), value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type nativeTestObject struct{ Object }
|
||||
|
||||
func TestNativeObjectGetters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status uintptr
|
||||
value *nativeTestObject
|
||||
}{
|
||||
{name: "success", value: &nativeTestObject{}},
|
||||
{name: "S_FALSE", status: 1, value: &nativeTestObject{}},
|
||||
{name: "null success"},
|
||||
{name: "null S_FALSE", status: 1},
|
||||
{name: "HRESULT failure", status: 0x80070005},
|
||||
}
|
||||
|
||||
for _, getter := range []string{"GetObject", "GetObjectArg", "GetObjectStringArg"} {
|
||||
t.Run(getter, func(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var methods [8]uintptr
|
||||
|
||||
if getter == "GetObject" {
|
||||
methods[7] = windows.NewCallback(func(_ uintptr, out **nativeTestObject) uintptr {
|
||||
*out = test.value
|
||||
|
||||
return test.status
|
||||
})
|
||||
} else {
|
||||
methods[7] = windows.NewCallback(func(_ uintptr, _ uintptr, out **nativeTestObject) uintptr {
|
||||
*out = test.value
|
||||
|
||||
return test.status
|
||||
})
|
||||
}
|
||||
|
||||
object := &Object{VTable: &methods[0]}
|
||||
|
||||
var (
|
||||
value *nativeTestObject
|
||||
err error
|
||||
)
|
||||
|
||||
switch getter {
|
||||
case "GetObject":
|
||||
value, err = object.GetObject[nativeTestObject](7)
|
||||
case "GetObjectArg":
|
||||
value, err = object.GetObjectArg[nativeTestObject](7, 1)
|
||||
case "GetObjectStringArg":
|
||||
value, err = object.GetObjectStringArg[nativeTestObject](7, "criteria")
|
||||
}
|
||||
|
||||
if int32(test.status) < 0 {
|
||||
require.Nil(t, value)
|
||||
require.ErrorIs(t, err, HRESULT(test.status))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if test.value == nil {
|
||||
require.Nil(t, value)
|
||||
require.ErrorContains(t, err, "returned a nil interface")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Same(t, test.value, value)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeObjectGetterCallbackStackGrowth(t *testing.T) {
|
||||
for _, getter := range []string{"GetObject", "GetObjectArg", "GetObjectStringArg"} {
|
||||
t.Run(getter, func(t *testing.T) {
|
||||
var methods [8]uintptr
|
||||
|
||||
expected := &nativeTestObject{}
|
||||
inputValid := true
|
||||
|
||||
switch getter {
|
||||
case "GetObject":
|
||||
methods[7] = windows.NewCallback(func(_ uintptr, out **nativeTestObject) uintptr {
|
||||
growCallbackStack(64)
|
||||
|
||||
*out = expected
|
||||
|
||||
return 0
|
||||
})
|
||||
case "GetObjectArg":
|
||||
methods[7] = windows.NewCallback(func(_ uintptr, input *int32, out **nativeTestObject) uintptr {
|
||||
growCallbackStack(64)
|
||||
|
||||
inputValid = *input == 1234
|
||||
*out = expected
|
||||
|
||||
return 0
|
||||
})
|
||||
case "GetObjectStringArg":
|
||||
methods[7] = windows.NewCallback(func(_ uintptr, input *uint16, out **nativeTestObject) uintptr {
|
||||
growCallbackStack(64)
|
||||
|
||||
inputValid = (bstr{ptr: input}).string() == "before\x00after"
|
||||
*out = expected
|
||||
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
object := &Object{VTable: &methods[0]}
|
||||
|
||||
var (
|
||||
value *nativeTestObject
|
||||
err error
|
||||
)
|
||||
|
||||
switch getter {
|
||||
case "GetObject":
|
||||
value, err = object.GetObject[nativeTestObject](7)
|
||||
case "GetObjectArg":
|
||||
input := int32(1234)
|
||||
value, err = object.GetObjectArg[nativeTestObject](7, uintptr(unsafe.Pointer(&input)))
|
||||
case "GetObjectStringArg":
|
||||
value, err = object.GetObjectStringArg[nativeTestObject](7, "before\x00after")
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Same(t, expected, value)
|
||||
require.True(t, inputValid)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func growCallbackStack(depth int) byte {
|
||||
var padding [4096]byte
|
||||
for i := range padding {
|
||||
padding[i] = byte(i + depth)
|
||||
}
|
||||
|
||||
if depth > 0 {
|
||||
padding[0] = growCallbackStack(depth - 1)
|
||||
}
|
||||
|
||||
runtime.KeepAlive(&padding)
|
||||
|
||||
return padding[0] + padding[len(padding)-1]
|
||||
}
|
||||
|
||||
func TestVariantLayout(t *testing.T) {
|
||||
require.Equal(t, uintptr(24), unsafe.Sizeof(Variant{}))
|
||||
require.Equal(t, uintptr(8), unsafe.Offsetof(Variant{}.Value))
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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 ole
|
||||
|
||||
import "fmt"
|
||||
|
||||
func formatHRESULT(h HRESULT) string {
|
||||
return fmt.Sprintf("COM HRESULT 0x%08X", uint32(h))
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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.
|
||||
|
||||
package ole
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func formatHRESULT(h HRESULT) string {
|
||||
numeric := fmt.Sprintf("COM HRESULT 0x%08X", uint32(h))
|
||||
|
||||
code := uint32(h)
|
||||
if (code>>16)&0x1fff == 7 { // FACILITY_WIN32 stores the system error in the low word.
|
||||
code &= 0xffff
|
||||
}
|
||||
|
||||
var buffer [1024]uint16
|
||||
|
||||
n, err := windows.FormatMessage(
|
||||
windows.FORMAT_MESSAGE_FROM_SYSTEM|windows.FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
0,
|
||||
code,
|
||||
0,
|
||||
buffer[:],
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return numeric
|
||||
}
|
||||
|
||||
message := strings.TrimSpace(windows.UTF16ToString(buffer[:n]))
|
||||
if message == "" {
|
||||
return numeric
|
||||
}
|
||||
|
||||
return numeric + ": " + message
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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.
|
||||
|
||||
package ole
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestHRESULTFormatting(t *testing.T) {
|
||||
t.Run("Win32 facility", func(t *testing.T) {
|
||||
message := formatHRESULT(HRESULT(0x80070005))
|
||||
require.Contains(t, message, "COM HRESULT 0x80070005")
|
||||
require.Contains(t, message, strings.TrimSpace(windows.ERROR_ACCESS_DENIED.Error()))
|
||||
require.Equal(t, strings.TrimSpace(message), message)
|
||||
})
|
||||
|
||||
t.Run("COM system message", func(t *testing.T) {
|
||||
message := formatHRESULT(HRESULT(0x80004005))
|
||||
require.Contains(t, message, "COM HRESULT 0x80004005: ")
|
||||
})
|
||||
|
||||
t.Run("unknown status", func(t *testing.T) {
|
||||
require.Equal(t, "COM HRESULT 0xDEADBEEF", formatHRESULT(HRESULT(0xdeadbeef)))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// 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.
|
||||
|
||||
// Package ole provides native COM initialization, interface calls, strings,
|
||||
// variants, and iteration shared by the taskschd and wuapi bindings. Native
|
||||
// interfaces and strings have explicit ownership.
|
||||
package ole
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HRESULT preserves the 32-bit COM status code, including when wrapped.
|
||||
type HRESULT uint32
|
||||
|
||||
func (h HRESULT) Error() string { return formatHRESULT(h) }
|
||||
|
||||
// ResultError returns an error only for failed HRESULTs.
|
||||
func ResultError(result uintptr) error {
|
||||
if int32(result) < 0 {
|
||||
return HRESULT(uint32(result))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DATE is an OLE Automation date: days since 1899-12-30, with the absolute
|
||||
// fractional part representing the time of day, including for negative dates.
|
||||
type DATE float64
|
||||
|
||||
func (d DATE) Time() (time.Time, error) {
|
||||
value := float64(d)
|
||||
if math.IsNaN(value) || math.IsInf(value, 0) || value <= -657435 || value >= 2958466 {
|
||||
return time.Time{}, fmt.Errorf("invalid OLE date: %g", value)
|
||||
}
|
||||
|
||||
days, fraction := math.Modf(value)
|
||||
date := time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC).AddDate(0, 0, int(days))
|
||||
millis := int64(math.Round(math.Abs(fraction) * 86400000))
|
||||
|
||||
date = date.Add(time.Duration(millis) * time.Millisecond)
|
||||
if date.Year() > 9999 {
|
||||
return time.Time{}, fmt.Errorf("invalid OLE date: %g", value)
|
||||
}
|
||||
|
||||
return date, nil
|
||||
}
|
||||
|
||||
// CollectionItemError identifies an unreadable index within a COM collection.
|
||||
// The iterator can still read later indices; a count failure is not an item error.
|
||||
type CollectionItemError struct {
|
||||
Index int32
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *CollectionItemError) Error() string {
|
||||
return fmt.Sprintf("get collection item %d: %v", e.Index, e.Err)
|
||||
}
|
||||
|
||||
func (e *CollectionItemError) Unwrap() error { return e.Err }
|
||||
|
||||
// BorrowedItems releases each item even when yield stops early or panics. Items
|
||||
// are valid only during yield; callers must not retain them or release them.
|
||||
// Item errors are yielded as CollectionItemError, and iteration continues if
|
||||
// yield returns true. Count errors stop iteration because there are no indices.
|
||||
func BorrowedItems[T any](count func() (int32, error), item func(int32) (T, error), release func(T)) iter.Seq2[T, error] {
|
||||
return func(yield func(T, error) bool) {
|
||||
var zero T
|
||||
|
||||
n, err := count()
|
||||
if err != nil {
|
||||
yield(zero, fmt.Errorf("get collection count: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if n < 0 {
|
||||
yield(zero, fmt.Errorf("invalid collection count: %d", n))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for i := range n {
|
||||
value, err := item(i)
|
||||
if err != nil {
|
||||
if !yield(zero, &CollectionItemError{Index: i, Err: err}) {
|
||||
return
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if !func() bool {
|
||||
defer release(value)
|
||||
|
||||
return yield(value, nil)
|
||||
}() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// 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.
|
||||
|
||||
package ole
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResultError(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
code uint32
|
||||
failed bool
|
||||
}{
|
||||
{name: "S_OK", code: 0},
|
||||
{name: "S_FALSE", code: 1},
|
||||
{name: "other success", code: 0x7fffffff},
|
||||
{name: "access denied", code: 0x80070005, failed: true},
|
||||
{name: "changed apartment", code: 0x80010106, failed: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := ResultError(uintptr(tc.code))
|
||||
if !tc.failed {
|
||||
require.NoError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.ErrorIs(t, err, HRESULT(tc.code))
|
||||
wrapped := fmt.Errorf("initialize: %w", err)
|
||||
hresult, ok := errors.AsType[HRESULT](wrapped)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, HRESULT(tc.code), hresult)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDATETime(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
date DATE
|
||||
want string
|
||||
}{
|
||||
{name: "epoch", date: 0, want: "1899-12-30T00:00:00Z"},
|
||||
{name: "positive fraction", date: 2.5, want: "1900-01-01T12:00:00Z"},
|
||||
{name: "negative fraction", date: -1.25, want: "1899-12-29T06:00:00Z"},
|
||||
{name: "negative zero day", date: -0.5, want: "1899-12-30T12:00:00Z"},
|
||||
{name: "Unix epoch", date: 25569, want: "1970-01-01T00:00:00Z"},
|
||||
{name: "minimum year", date: -657434, want: "0100-01-01T00:00:00Z"},
|
||||
{name: "maximum year", date: 2958465, want: "9999-12-31T00:00:00Z"},
|
||||
{name: "NaN", date: DATE(math.NaN())},
|
||||
{name: "positive infinity", date: DATE(math.Inf(1))},
|
||||
{name: "negative infinity", date: DATE(math.Inf(-1))},
|
||||
{name: "too early", date: -657435},
|
||||
{name: "too late", date: 2958466},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := tc.date.Time()
|
||||
if tc.want == "" {
|
||||
require.Error(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.want, got.Format(time.RFC3339))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBorrowedItems(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
count int32
|
||||
countErr error
|
||||
itemErrAt int32
|
||||
stopAfter int
|
||||
want []int32
|
||||
wantError bool
|
||||
}{
|
||||
{name: "all", count: 3, itemErrAt: -1, want: []int32{0, 1, 2}},
|
||||
{name: "empty", itemErrAt: -1, want: []int32{}},
|
||||
{name: "early stop", count: 3, itemErrAt: -1, stopAfter: 1, want: []int32{0}},
|
||||
{name: "count failure", countErr: HRESULT(0x80070005), itemErrAt: -1, want: []int32{}, wantError: true},
|
||||
{name: "negative count", count: -1, itemErrAt: -1, want: []int32{}, wantError: true},
|
||||
{name: "first item failure", count: 3, itemErrAt: 0, want: []int32{}, wantError: true},
|
||||
{name: "later item failure", count: 3, itemErrAt: 1, want: []int32{0}, wantError: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
seen := []int32{}
|
||||
released := []int32{}
|
||||
seq := BorrowedItems(
|
||||
func() (int32, error) { return tc.count, tc.countErr },
|
||||
func(index int32) (int32, error) {
|
||||
if index == tc.itemErrAt {
|
||||
return 0, HRESULT(0x80070005)
|
||||
}
|
||||
|
||||
return index, nil
|
||||
},
|
||||
func(value int32) { released = append(released, value) },
|
||||
)
|
||||
|
||||
var gotError error
|
||||
|
||||
for value, err := range seq {
|
||||
if err != nil {
|
||||
gotError = err
|
||||
|
||||
break
|
||||
}
|
||||
// The current value stays alive until the loop body returns.
|
||||
require.Equal(t, seen, released)
|
||||
|
||||
seen = append(seen, value)
|
||||
if tc.stopAfter > 0 && len(seen) == tc.stopAfter {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, tc.want, seen)
|
||||
require.Equal(t, tc.want, released)
|
||||
|
||||
if tc.wantError {
|
||||
require.Error(t, gotError)
|
||||
} else {
|
||||
require.NoError(t, gotError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBorrowedItemsPanic(t *testing.T) {
|
||||
released := 0
|
||||
seq := BorrowedItems(
|
||||
func() (int32, error) { return 2, nil },
|
||||
func(index int32) (int32, error) { return index, nil },
|
||||
func(int32) { released++ },
|
||||
)
|
||||
|
||||
require.Panics(t, func() {
|
||||
for range seq {
|
||||
panic("consumer panic")
|
||||
}
|
||||
})
|
||||
require.Equal(t, 1, released)
|
||||
}
|
||||
|
||||
func TestBorrowedItemsContinuesAfterItemError(t *testing.T) {
|
||||
seen := []int32{}
|
||||
released := []int32{}
|
||||
itemErrors := []int32{}
|
||||
seq := BorrowedItems(
|
||||
func() (int32, error) { return 3, nil },
|
||||
func(index int32) (int32, error) {
|
||||
if index == 1 {
|
||||
return 0, HRESULT(0x80070005)
|
||||
}
|
||||
|
||||
return index, nil
|
||||
},
|
||||
func(value int32) { released = append(released, value) },
|
||||
)
|
||||
|
||||
for value, err := range seq {
|
||||
if err != nil {
|
||||
itemError, ok := errors.AsType[*CollectionItemError](err)
|
||||
require.True(t, ok)
|
||||
require.ErrorIs(t, err, HRESULT(0x80070005))
|
||||
|
||||
itemErrors = append(itemErrors, itemError.Index)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
seen = append(seen, value)
|
||||
}
|
||||
|
||||
require.Equal(t, []int32{0, 2}, seen)
|
||||
require.Equal(t, []int32{0, 2}, released)
|
||||
require.Equal(t, []int32{1}, itemErrors)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// 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 taskschd provides the Task Scheduler COM interfaces needed for task enumeration.
|
||||
package taskschd
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/ole"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Native method slots, including the seven inherited IUnknown/IDispatch slots.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/taskschd/
|
||||
const (
|
||||
taskServiceGetFolder = 7
|
||||
taskServiceConnect = 10
|
||||
taskFolderPath = 8
|
||||
taskFolderGetFolders = 10
|
||||
taskFolderGetTasks = 14
|
||||
taskCollectionCount = 7
|
||||
taskCollectionItem = 8
|
||||
registeredTaskName = 7
|
||||
registeredTaskPath = 8
|
||||
registeredTaskState = 9
|
||||
registeredTaskEnabled = 10
|
||||
registeredTaskLastResult = 16
|
||||
registeredTaskMissedRuns = 17
|
||||
)
|
||||
|
||||
type (
|
||||
TaskService struct{ ole.Object }
|
||||
TaskFolder struct{ ole.Object }
|
||||
RegisteredTask struct{ ole.Object }
|
||||
)
|
||||
|
||||
func NewTaskService() (*TaskService, error) {
|
||||
return ole.Create[TaskService](
|
||||
windows.GUID{Data1: 0x0f87369f, Data2: 0xa4e5, Data3: 0x4cfc, Data4: [8]byte{0xbd, 0x3e, 0x73, 0xe6, 0x15, 0x45, 0x72, 0xdd}},
|
||||
windows.GUID{Data1: 0x2faba4c7, Data2: 0x4da9, Data3: 0x4013, Data4: [8]byte{0x96, 0x97, 0x20, 0xcc, 0x3f, 0xd4, 0x0f, 0x85}},
|
||||
)
|
||||
}
|
||||
|
||||
// Connect connects locally using the current security token. Empty VARIANTs
|
||||
// supply the four optional arguments. The 24-byte values are passed indirectly
|
||||
// on Windows amd64 and arm64, each using distinct caller-owned storage.
|
||||
func (s *TaskService) Connect() error {
|
||||
server := ole.NewEmptyVariant()
|
||||
user := ole.NewEmptyVariant()
|
||||
domain := ole.NewEmptyVariant()
|
||||
password := ole.NewEmptyVariant()
|
||||
|
||||
hr := ole.Call(
|
||||
s.Method(taskServiceConnect),
|
||||
uintptr(unsafe.Pointer(s)),
|
||||
uintptr(unsafe.Pointer(server)),
|
||||
uintptr(unsafe.Pointer(user)),
|
||||
uintptr(unsafe.Pointer(domain)),
|
||||
uintptr(unsafe.Pointer(password)),
|
||||
)
|
||||
runtime.KeepAlive(s)
|
||||
|
||||
return ole.ResultError(hr)
|
||||
}
|
||||
|
||||
func (s *TaskService) Folder(path string) (*TaskFolder, error) {
|
||||
return s.GetObjectStringArg[TaskFolder](taskServiceGetFolder, path)
|
||||
}
|
||||
|
||||
// Folders returns the direct subfolders. Its reserved flags argument must be zero.
|
||||
func (f *TaskFolder) Folders() (*taskCollection[TaskFolder], error) {
|
||||
return f.GetObjectArg[taskCollection[TaskFolder]](taskFolderGetFolders, 0)
|
||||
}
|
||||
|
||||
// Tasks includes hidden tasks (TASK_ENUM_HIDDEN).
|
||||
func (f *TaskFolder) Tasks() (*taskCollection[RegisteredTask], error) {
|
||||
return f.GetObjectArg[taskCollection[RegisteredTask]](taskFolderGetTasks, 1)
|
||||
}
|
||||
|
||||
func (f *TaskFolder) Path() (string, error) { return f.String(taskFolderPath) }
|
||||
|
||||
func (t *RegisteredTask) Name() (string, error) { return t.String(registeredTaskName) }
|
||||
func (t *RegisteredTask) Path() (string, error) { return t.String(registeredTaskPath) }
|
||||
func (t *RegisteredTask) State() (int32, error) { return t.Get[int32](registeredTaskState) }
|
||||
func (t *RegisteredTask) Enabled() (bool, error) {
|
||||
value, err := t.Get[int16](registeredTaskEnabled)
|
||||
|
||||
return value != 0, err
|
||||
}
|
||||
|
||||
func (t *RegisteredTask) LastTaskResult() (int32, error) {
|
||||
return t.Get[int32](registeredTaskLastResult)
|
||||
}
|
||||
|
||||
func (t *RegisteredTask) NumberOfMissedRuns() (int32, error) {
|
||||
return t.Get[int32](registeredTaskMissedRuns)
|
||||
}
|
||||
|
||||
type taskCollection[T TaskFolder | RegisteredTask] struct{ ole.Object }
|
||||
|
||||
func (c *taskCollection[T]) count() (int32, error) { return c.Get[int32](taskCollectionCount) }
|
||||
|
||||
func (c *taskCollection[T]) item(index int32) (*T, error) {
|
||||
value := ole.NewInt32Variant(index + 1) // Task Scheduler uses one-based indices.
|
||||
|
||||
item, err := c.GetObjectArg[T](taskCollectionItem, uintptr(unsafe.Pointer(value)))
|
||||
runtime.KeepAlive(value)
|
||||
|
||||
return item, err
|
||||
}
|
||||
|
||||
// All yields borrowed interfaces, valid only in the loop body. It releases each
|
||||
// item on advance, early exit, or panic; callers must not release the items.
|
||||
// The collection remains owned by the caller and must be released separately.
|
||||
func (c *taskCollection[T]) All() iter.Seq2[*T, error] {
|
||||
return ole.BorrowedItems(c.count, c.item, func(item *T) { (*ole.Object)(unsafe.Pointer(item)).Release() })
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// 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 taskschd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/ole"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestNativeTaskGetters(t *testing.T) {
|
||||
var methods [18]uintptr
|
||||
|
||||
// IRegisteredTask::get_State slot from the Windows SDK.
|
||||
methods[9] = windows.NewCallback(func(_ uintptr, out *int32) uintptr {
|
||||
*out = 4
|
||||
|
||||
return 1 // S_FALSE is a successful HRESULT.
|
||||
})
|
||||
// IRegisteredTask::get_Enabled slot from the Windows SDK.
|
||||
methods[10] = windows.NewCallback(func(_ uintptr, out *int16) uintptr {
|
||||
*out = -1
|
||||
|
||||
return 0
|
||||
})
|
||||
// IRegisteredTask::get_LastTaskResult slot from the Windows SDK.
|
||||
methods[16] = windows.NewCallback(func(_ uintptr, out *int32) uintptr {
|
||||
*out = -2147216629 // 0x8004130B; preserve the native signed LONG.
|
||||
|
||||
return 0
|
||||
})
|
||||
// IRegisteredTask::get_NumberOfMissedRuns slot from the Windows SDK.
|
||||
methods[17] = windows.NewCallback(func(_ uintptr, _ *int32) uintptr {
|
||||
return 0x80070005
|
||||
})
|
||||
task := &RegisteredTask{VTable: &methods[0]}
|
||||
state, err := task.State()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(4), state)
|
||||
|
||||
enabled, err := task.Enabled()
|
||||
require.NoError(t, err)
|
||||
require.True(t, enabled)
|
||||
|
||||
result, err := task.LastTaskResult()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(0x8004130b), uint32(result))
|
||||
|
||||
_, err = task.NumberOfMissedRuns()
|
||||
require.ErrorIs(t, err, ole.HRESULT(0x80070005))
|
||||
}
|
||||
|
||||
func TestNativeConnect(t *testing.T) {
|
||||
var methods [11]uintptr
|
||||
|
||||
valid := false
|
||||
// ITaskService::Connect slot from the Windows SDK.
|
||||
methods[10] = windows.NewCallback(func(_ uintptr, server, user, domain, password *ole.Variant) uintptr {
|
||||
valid = true
|
||||
for _, value := range []*ole.Variant{server, user, domain, password} {
|
||||
valid = valid && *value == (ole.Variant{}) && uintptr(unsafe.Pointer(value))%16 == 0
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
service := &TaskService{VTable: &methods[0]}
|
||||
require.NoError(t, service.Connect())
|
||||
require.True(t, valid, "Connect arguments must be aligned VT_EMPTY variants")
|
||||
}
|
||||
|
||||
func TestNativeTaskCollection(t *testing.T) {
|
||||
var taskMethods [3]uintptr
|
||||
|
||||
released := 0
|
||||
taskMethods[2] = windows.NewCallback(func(uintptr) uintptr {
|
||||
released++
|
||||
|
||||
return 0
|
||||
})
|
||||
task := &RegisteredTask{VTable: &taskMethods[0]}
|
||||
|
||||
var methods [9]uintptr
|
||||
|
||||
// IRegisteredTaskCollection::get_Count slot from the Windows SDK.
|
||||
methods[7] = windows.NewCallback(func(_ uintptr, out *int32) uintptr {
|
||||
*out = 2
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
var (
|
||||
index int64
|
||||
aligned bool
|
||||
)
|
||||
|
||||
// IRegisteredTaskCollection::get_Item slot from the Windows SDK.
|
||||
methods[8] = windows.NewCallback(func(_ uintptr, value *ole.Variant, out **RegisteredTask) uintptr {
|
||||
index = value.Value
|
||||
aligned = value.Type == 3 && uintptr(unsafe.Pointer(value))%16 == 0
|
||||
*out = task
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
collection := &taskCollection[RegisteredTask]{VTable: &methods[0]}
|
||||
for item, err := range collection.All() {
|
||||
require.NoError(t, err)
|
||||
require.Same(t, task, item)
|
||||
require.Zero(t, released)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
require.Equal(t, int64(1), index)
|
||||
require.True(t, aligned)
|
||||
require.Equal(t, 1, released)
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// 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 wuapi provides the Windows Update COM interfaces needed for update searches and metadata.
|
||||
package wuapi
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/ole"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Native method slots from wuapi.h, including inherited IDispatch methods.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/wuapi/
|
||||
const (
|
||||
updateSessionPutApplicationID = 8
|
||||
updateSessionCreateSearcher = 12
|
||||
updateSessionPutLocale = 16 // IUpdateSession2
|
||||
updateSearcherSearch = 19
|
||||
updateSearcherPutOnline = 21
|
||||
updateSearcherHistoryCount = 22
|
||||
searchResultUpdates = 9
|
||||
updateCollectionItem = 7
|
||||
updateCollectionCount = 10
|
||||
updateTitle = 7
|
||||
updateCategories = 11
|
||||
updateIdentity = 19
|
||||
updateDeploymentTime = 30
|
||||
updateSeverity = 34
|
||||
identityRevision = 7
|
||||
identityUpdateID = 8
|
||||
categoryCollectionItem = 7
|
||||
categoryCollectionCount = 9
|
||||
categoryName = 7
|
||||
categoryOrder = 12
|
||||
)
|
||||
|
||||
type (
|
||||
UpdateSession struct{ ole.Object } // IUpdateSession2
|
||||
UpdateSearcher struct{ ole.Object }
|
||||
SearchResult struct{ ole.Object }
|
||||
Update struct{ ole.Object }
|
||||
UpdateIdentity struct{ ole.Object }
|
||||
Category struct{ ole.Object }
|
||||
)
|
||||
|
||||
func NewUpdateSession() (*UpdateSession, error) {
|
||||
return ole.Create[UpdateSession](
|
||||
windows.GUID{Data1: 0x4cb43d7f, Data2: 0x7eee, Data3: 0x4906, Data4: [8]byte{0x86, 0x98, 0x60, 0xda, 0x1c, 0x38, 0xf2, 0xfe}},
|
||||
windows.GUID{Data1: 0x91caf7b0, Data2: 0xeb23, Data3: 0x49ed, Data4: [8]byte{0x99, 0x37, 0xc5, 0x2d, 0x81, 0x7f, 0x46, 0xf7}},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *UpdateSession) SetUserLocale(locale uint32) error {
|
||||
return s.Put(updateSessionPutLocale, uintptr(locale))
|
||||
}
|
||||
|
||||
func (s *UpdateSession) SetClientApplicationID(id string) error {
|
||||
return s.PutString(updateSessionPutApplicationID, id)
|
||||
}
|
||||
|
||||
func (s *UpdateSession) CreateUpdateSearcher() (*UpdateSearcher, error) {
|
||||
return s.GetObject[UpdateSearcher](updateSessionCreateSearcher)
|
||||
}
|
||||
|
||||
func (s *UpdateSearcher) SetOnline(online bool) error {
|
||||
var value uintptr
|
||||
// VARIANT_TRUE is a signed 16-bit -1.
|
||||
if online {
|
||||
value = 0xffff
|
||||
}
|
||||
|
||||
return s.Put(updateSearcherPutOnline, value)
|
||||
}
|
||||
|
||||
func (s *UpdateSearcher) GetTotalHistoryCount() (int32, error) {
|
||||
return s.Get[int32](updateSearcherHistoryCount)
|
||||
}
|
||||
|
||||
func (s *UpdateSearcher) Search(criteria string) (*SearchResult, error) {
|
||||
return s.GetObjectStringArg[SearchResult](updateSearcherSearch, criteria)
|
||||
}
|
||||
|
||||
func (s *SearchResult) Updates() (*UpdateCollection, error) {
|
||||
return s.GetObject[UpdateCollection](searchResultUpdates)
|
||||
}
|
||||
|
||||
func (u *Update) Title() (string, error) { return u.String(updateTitle) }
|
||||
func (u *Update) MsrcSeverity() (string, error) { return u.String(updateSeverity) }
|
||||
func (u *Update) Categories() (*CategoryCollection, error) {
|
||||
return u.GetObject[CategoryCollection](updateCategories)
|
||||
}
|
||||
|
||||
func (u *Update) Identity() (*UpdateIdentity, error) {
|
||||
return u.GetObject[UpdateIdentity](updateIdentity)
|
||||
}
|
||||
|
||||
func (u *Update) LastDeploymentChangeTime() (ole.DATE, error) {
|
||||
return u.Get[ole.DATE](updateDeploymentTime)
|
||||
}
|
||||
func (i *UpdateIdentity) UpdateID() (string, error) { return i.String(identityUpdateID) }
|
||||
func (i *UpdateIdentity) RevisionNumber() (int32, error) { return i.Get[int32](identityRevision) }
|
||||
func (c *Category) Name() (string, error) { return c.String(categoryName) }
|
||||
func (c *Category) Order() (int32, error) { return c.Get[int32](categoryOrder) }
|
||||
|
||||
// UpdateCollection uses IUpdateCollection's native layout and zero-based indices.
|
||||
type UpdateCollection struct{ ole.Object }
|
||||
|
||||
func (c *UpdateCollection) count() (int32, error) {
|
||||
return c.Get[int32](updateCollectionCount)
|
||||
}
|
||||
|
||||
func (c *UpdateCollection) item(index int32) (*Update, error) {
|
||||
return c.GetObjectArg[Update](updateCollectionItem, uintptr(index))
|
||||
}
|
||||
|
||||
// All yields borrowed updates, valid only in the loop body. Callers must not
|
||||
// release or retain them. Item errors can be skipped by continuing the loop.
|
||||
// The collection itself remains owned by the caller and must be released.
|
||||
func (c *UpdateCollection) All() iter.Seq2[*Update, error] {
|
||||
return ole.BorrowedItems(c.count, c.item, func(item *Update) { item.Release() })
|
||||
}
|
||||
|
||||
// CategoryCollection uses ICategoryCollection's distinct native count slot.
|
||||
type CategoryCollection struct{ ole.Object }
|
||||
|
||||
func (c *CategoryCollection) count() (int32, error) {
|
||||
return c.Get[int32](categoryCollectionCount)
|
||||
}
|
||||
|
||||
func (c *CategoryCollection) item(index int32) (*Category, error) {
|
||||
return c.GetObjectArg[Category](categoryCollectionItem, uintptr(index))
|
||||
}
|
||||
|
||||
// All yields borrowed categories, valid only in the loop body. Callers must not
|
||||
// release or retain them. Item errors can be skipped by continuing the loop.
|
||||
// The collection itself remains owned by the caller and must be released.
|
||||
func (c *CategoryCollection) All() iter.Seq2[*Category, error] {
|
||||
return ole.BorrowedItems(c.count, c.item, func(item *Category) { item.Release() })
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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 wuapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/ole"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestNativeUpdateCollection(t *testing.T) {
|
||||
var methods [11]uintptr
|
||||
|
||||
// IUpdateCollection::get_Count slot from the Windows SDK.
|
||||
methods[10] = windows.NewCallback(func(_ uintptr, out *int32) uintptr {
|
||||
*out = 1
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
var index uintptr
|
||||
|
||||
// IUpdateCollection::get_Item slot from the Windows SDK.
|
||||
methods[7] = windows.NewCallback(func(_ uintptr, i uintptr, _ **Update) uintptr {
|
||||
index = i
|
||||
|
||||
return 0x80070005
|
||||
})
|
||||
collection := &UpdateCollection{VTable: &methods[0]}
|
||||
|
||||
errors := 0
|
||||
for item, err := range collection.All() {
|
||||
errors++
|
||||
|
||||
require.Nil(t, item)
|
||||
require.ErrorIs(t, err, ole.HRESULT(0x80070005))
|
||||
}
|
||||
|
||||
require.Equal(t, 1, errors)
|
||||
require.Zero(t, index)
|
||||
}
|
||||
Reference in New Issue
Block a user