mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 01:59:04 +02:00
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
329 lines
8.8 KiB
Go
329 lines
8.8 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// Copyright The Prometheus Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//go:build windows
|
|
|
|
package process
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// maxInfoWorkers bounds the goroutines that open processes for new cache entries.
|
|
const maxInfoWorkers = 8
|
|
|
|
// errProcessGone reports that the PID no longer belongs to the snapshot process.
|
|
var errProcessGone = errors.New("process exited")
|
|
|
|
// processInfo holds the values of windows_process_info that are fixed for the lifetime of a process.
|
|
type processInfo struct {
|
|
// createTime identifies the process together with the PID, because Windows reuses PIDs.
|
|
createTime int64
|
|
|
|
cmdLine string
|
|
owner string
|
|
processGroupID uint32
|
|
}
|
|
|
|
// resolveProcessInfo returns the info of each process, in the order of processes.
|
|
// Cached entries are reused while the PID and creation time match. The others are
|
|
// looked up in parallel. The cache keeps only the processes passed in.
|
|
func (c *Collector) resolveProcessInfo(processes []*processSnapshot) []processInfo {
|
|
infos := make([]processInfo, len(processes))
|
|
cached := make([]bool, len(processes))
|
|
misses := make([]int, 0)
|
|
|
|
for i, process := range processes {
|
|
if info, ok := c.infoCache[process.pid]; ok && info.createTime == process.createTime {
|
|
infos[i] = info
|
|
cached[i] = true
|
|
|
|
continue
|
|
}
|
|
|
|
misses = append(misses, i)
|
|
}
|
|
|
|
if len(misses) > 0 {
|
|
var next atomic.Int64
|
|
|
|
wg := sync.WaitGroup{}
|
|
|
|
for range min(len(misses), runtime.GOMAXPROCS(0), maxInfoWorkers) {
|
|
wg.Go(func() {
|
|
for {
|
|
n := int(next.Add(1)) - 1
|
|
if n >= len(misses) {
|
|
return
|
|
}
|
|
|
|
i := misses[n]
|
|
c.lookupProcessInfo(processes[i], &infos[i], &cached[i])
|
|
}
|
|
})
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
cache := make(map[uint32]processInfo, len(processes))
|
|
|
|
for i, process := range processes {
|
|
if cached[i] {
|
|
cache[process.pid] = infos[i]
|
|
}
|
|
}
|
|
|
|
c.infoCache = cache
|
|
|
|
return infos
|
|
}
|
|
|
|
// lookupProcessInfo opens the process and reads its owner, command line and process group ID.
|
|
// cacheable is set only if the lookup succeeded. A panic leaves both values unset.
|
|
func (c *Collector) lookupProcessInfo(process *processSnapshot, info *processInfo, cacheable *bool) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
c.logger.LogAttrs(context.Background(), slog.LevelError, "Panic while reading process information",
|
|
slog.Uint64("pid", uint64(process.pid)),
|
|
slog.Any("panic", r),
|
|
slog.String("stack", string(debug.Stack())),
|
|
)
|
|
}
|
|
}()
|
|
|
|
cmdLine, owner, processGroupID, err := c.getProcessInformation(process.pid, process.createTime)
|
|
if err != nil {
|
|
if !errors.Is(err, errProcessGone) {
|
|
c.logger.LogAttrs(context.Background(), slog.LevelDebug, "Failed to get process information",
|
|
slog.Uint64("pid", uint64(process.pid)),
|
|
slog.String("process", process.name),
|
|
slog.Any("err", err),
|
|
)
|
|
}
|
|
}
|
|
|
|
*info = processInfo{
|
|
createTime: process.createTime,
|
|
cmdLine: cmdLine,
|
|
owner: owner,
|
|
processGroupID: processGroupID,
|
|
}
|
|
*cacheable = err == nil
|
|
}
|
|
|
|
// getProcessInformation returns the command line, owner and process group ID of a process.
|
|
// createTime is checked after opening the process, so a reused PID is not attributed to the old process.
|
|
//
|
|
// ref: https://github.com/microsoft/hcsshim/blob/8beabacfc2d21767a07c20f8dd5f9f3932dbf305/internal/uvm/stats.go#L25
|
|
func (c *Collector) getProcessInformation(pid uint32, createTime int64) (string, string, uint32, error) {
|
|
if pid == 0 {
|
|
return "", "", 0, nil
|
|
}
|
|
|
|
hProcess, vmReadAccess, err := c.openProcess(pid)
|
|
if err != nil {
|
|
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
|
return "", "", 0, nil
|
|
}
|
|
|
|
return "", "", 0, err
|
|
}
|
|
|
|
defer func(hProcess windows.Handle) {
|
|
if err := windows.CloseHandle(hProcess); err != nil {
|
|
c.logger.Warn("CloseHandle failed",
|
|
slog.Any("err", err),
|
|
)
|
|
}
|
|
}(hProcess)
|
|
|
|
var creationTime, exitTime, kernelTime, userTime windows.Filetime
|
|
|
|
if err := windows.GetProcessTimes(hProcess, &creationTime, &exitTime, &kernelTime, &userTime); err == nil &&
|
|
createTime != 0 && int64(creationTime.HighDateTime)<<32|int64(creationTime.LowDateTime) != createTime {
|
|
return "", "", 0, errProcessGone
|
|
}
|
|
|
|
owner, err := c.getProcessOwner(c.logger, hProcess)
|
|
if err != nil {
|
|
return "", "", 0, err
|
|
}
|
|
|
|
var (
|
|
cmdLine string
|
|
processGroupID uint32
|
|
)
|
|
|
|
if vmReadAccess {
|
|
cmdLine, processGroupID, err = c.getExtendedProcessInformation(hProcess)
|
|
if err != nil {
|
|
return "", owner, processGroupID, err
|
|
}
|
|
}
|
|
|
|
return cmdLine, owner, processGroupID, nil
|
|
}
|
|
|
|
func (c *Collector) getExtendedProcessInformation(hProcess windows.Handle) (string, uint32, error) {
|
|
// Get the process environment block (PEB) address
|
|
var pbi windows.PROCESS_BASIC_INFORMATION
|
|
|
|
retLen := uint32(unsafe.Sizeof(pbi))
|
|
if err := windows.NtQueryInformationProcess(hProcess, windows.ProcessBasicInformation, unsafe.Pointer(&pbi), retLen, &retLen); err != nil {
|
|
return "", 0, fmt.Errorf("failed to query process basic information: %w", err)
|
|
}
|
|
|
|
// Minimal processes like Registry and Memory Compression have no PEB.
|
|
if pbi.PebBaseAddress == nil {
|
|
return "", 0, nil
|
|
}
|
|
|
|
peb := windows.PEB{}
|
|
|
|
err := windows.ReadProcessMemory(hProcess,
|
|
uintptr(unsafe.Pointer(pbi.PebBaseAddress)),
|
|
(*byte)(unsafe.Pointer(&peb)),
|
|
unsafe.Sizeof(peb),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return "", 0, fmt.Errorf("failed to read process memory: %w", err)
|
|
}
|
|
|
|
if peb.ProcessParameters == nil {
|
|
return "", 0, nil
|
|
}
|
|
|
|
processParameters := windows.RTL_USER_PROCESS_PARAMETERS{}
|
|
|
|
err = windows.ReadProcessMemory(hProcess,
|
|
uintptr(unsafe.Pointer(peb.ProcessParameters)),
|
|
(*byte)(unsafe.Pointer(&processParameters)),
|
|
unsafe.Sizeof(processParameters),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return "", 0, fmt.Errorf("failed to read process memory: %w", err)
|
|
}
|
|
|
|
var cmdLine string
|
|
|
|
// CommandLine.Length is in bytes.
|
|
if c.config.EnableCMDLine && processParameters.CommandLine.Length >= 2 && processParameters.CommandLine.Buffer != nil {
|
|
cmdLineUTF16 := make([]uint16, processParameters.CommandLine.Length/2)
|
|
|
|
err = windows.ReadProcessMemory(hProcess,
|
|
uintptr(unsafe.Pointer(processParameters.CommandLine.Buffer)),
|
|
(*byte)(unsafe.Pointer(&cmdLineUTF16[0])),
|
|
uintptr(len(cmdLineUTF16)*2),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return "", processParameters.ProcessGroupId, fmt.Errorf("failed to read process memory: %w", err)
|
|
}
|
|
|
|
cmdLine = strings.TrimSpace(windows.UTF16ToString(cmdLineUTF16))
|
|
}
|
|
|
|
return cmdLine, processParameters.ProcessGroupId, nil
|
|
}
|
|
|
|
func (c *Collector) getProcessOwner(logger *slog.Logger, hProcess windows.Handle) (string, error) {
|
|
var tok windows.Token
|
|
|
|
if err := windows.OpenProcessToken(hProcess, windows.TOKEN_QUERY, &tok); err != nil {
|
|
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
|
return "", nil
|
|
}
|
|
|
|
return "", fmt.Errorf("failed to open process token: %w", err)
|
|
}
|
|
|
|
defer func(tok windows.Token) {
|
|
if err := tok.Close(); err != nil {
|
|
logger.Warn("Token close failed",
|
|
slog.Any("err", err),
|
|
)
|
|
}
|
|
}(tok)
|
|
|
|
tokenUser, err := tok.GetTokenUser()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get token user: %w", err)
|
|
}
|
|
|
|
sid := tokenUser.User.Sid.String()
|
|
|
|
var owner string
|
|
|
|
ownerVal, ok := c.lookupCache.Load(sid)
|
|
|
|
if ok {
|
|
owner, ok = ownerVal.(string)
|
|
}
|
|
|
|
if !ok {
|
|
account, domain, _, err := tokenUser.User.Sid.LookupAccount("")
|
|
if err != nil {
|
|
owner = sid
|
|
} else {
|
|
owner = fmt.Sprintf(`%s\%s`, domain, account)
|
|
}
|
|
|
|
c.lookupCache.Store(sid, owner)
|
|
}
|
|
|
|
return owner, nil
|
|
}
|
|
|
|
func (c *Collector) openProcess(pid uint32) (windows.Handle, bool, error) {
|
|
// Open the process with QUERY_INFORMATION and VM_READ permissions.
|
|
hProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, pid)
|
|
if err == nil {
|
|
return hProcess, true, nil
|
|
}
|
|
|
|
if errors.Is(err, windows.ERROR_INVALID_PARAMETER) { // The PID does not exist anymore.
|
|
return 0, false, errProcessGone
|
|
}
|
|
|
|
if !errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
|
return 0, false, fmt.Errorf("failed to open process: %w", err)
|
|
}
|
|
|
|
hProcess, err = windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
|
if err != nil {
|
|
if errors.Is(err, windows.ERROR_INVALID_PARAMETER) {
|
|
return 0, false, errProcessGone
|
|
}
|
|
|
|
return 0, false, fmt.Errorf("failed to open process with limited permissions: %w", err)
|
|
}
|
|
|
|
return hProcess, false, nil
|
|
}
|