diff --git a/AGENTS.md b/AGENTS.md index fa352783..e600dc2f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,8 +16,8 @@ their runtime behavior. - [`internal/collector`](internal/collector): built-in collectors. Each implements `GetName`, `Build`, `Collect` and `Close` from [`pkg/collector/types.go`](pkg/collector/types.go). -- [`internal/pdh`](internal/pdh) and [`internal/pdh/registry`](internal/pdh/registry): - performance counter queries and perflib decoding. [`internal/mi`](internal/mi) +- [`internal/pdh`](internal/pdh): + performance counter queries. [`internal/mi`](internal/mi) provides Management Infrastructure queries; [`internal/ole`](internal/ole) and [`internal/headers`](internal/headers) contain native Windows bindings. - [`internal/config`](internal/config), [`internal/httphandler`](internal/httphandler) @@ -86,7 +86,7 @@ exercised a live Windows role. even when the Windows counter name says average, percentage or seconds. Convert units explicitly; expose the base/operation count needed for ratios. Never publish an invalid sample as zero: that can look like a counter reset. -- Keep the typed PDH/registry collectors and `perfdata` tags. Preserve duplicate +- Keep the typed PDH collectors and `perfdata` tags. Preserve duplicate instance occurrences within a sample; their suffixes are not stable identities. Do not remove every name ending in `_Total`: legitimate process and database names can use that suffix. Use the shared aggregate-instance policy. diff --git a/docs/collector.process.md b/docs/collector.process.md index 305b102f..20920e68 100644 --- a/docs/collector.process.md +++ b/docs/collector.process.md @@ -2,20 +2,21 @@ The process collector exposes metrics about processes. -Note, on Windows Server 2022, the `Process` counter set is disabled by default. To enable it, run the following command in an elevated PowerShell session: +The collector reads all processes with one `NtQuerySystemInformation(SystemProcessInformation)` call. +The perflib `Process` counter set reads the same data, so the values and process names are the same. +The collector does not use performance counters. It works when the `Process` counter set is disabled, +for example on Windows Server 2022, and it does not require administrator rights. -```powershell -lodctr.exe /E:Lsa -lodctr.exe /E:PerfProc -lodctr.exe /R -``` +The `owner`, `cmdline` and `process_group_id` labels of `windows_process_info` require opening the process. +Without administrator rights, they are empty for processes of other users and for protected processes. +The collector reads them once per process and caches them while the process is running and matches the filters. +A failed read is retried on the next scrape. -| | | -|---------------------|-----------| -| Metric name prefix | `process` | -| Data source | Perflib | -| Counters | `Process` | -| Enabled by default? | No | +| | | +|---------------------|----------------------------| +| Metric name prefix | `process` | +| Data source | `NtQuerySystemInformation` | +| Enabled by default? | No | ## Flags @@ -38,12 +39,6 @@ See [IIS worker processes](#iis-worker-processes). Disabled by default, and can be enabled with `--collector.process.iis`. NOTE: Just plain parameter without `true`. -### `--collector.process.counter-version` - -Version of the process collector to use. 1 for Process V1, 2 for Process V2. -0 uses Process V2 if it is available and falls back to Process V1 otherwise. -Defaults to 1 (Process V1). - ### `--collector.process.cmdline` Enables the `cmdline` label for the process metrics. @@ -52,9 +47,9 @@ Enabled by default, and can be turned off with `--no-collector.process.cmdline`. ### Example To match all firefox processes: `--collector.process.include="firefox.*"`. -Note that multiple processes with the same name will be disambiguated by -Windows by adding a number suffix, such as `firefox#2`. Your [regular expression](https://en.wikipedia.org/wiki/Regular_expression) must take -these suffixes into consideration. +The process name is the image name without the `.exe` extension, like the instance names of the `Process` counter set. +Processes with the same name have the same `process` label and differ in the `process_id` label. +A `#` in the image name is kept: `app#2.exe` is `app#2`. Earlier versions cut the name at the first `#`. :warning: The regular expression is case-sensitive, so `--collector.process.include="FIREFOX.*"` will **NOT** match a process named `firefox` . diff --git a/internal/collector/process/AGENTS.md b/internal/collector/process/AGENTS.md index da739611..079706bf 100644 --- a/internal/collector/process/AGENTS.md +++ b/internal/collector/process/AGENTS.md @@ -1,4 +1,31 @@ -# process collector +# Process collector + +The collector reads all processes with one +`NtQuerySystemInformation(SystemProcessInformation)` call, not with the perflib +`Process` or PDH `Process V2` counter sets. The perflib provider (perfproc) reads +the same kernel data, so the metric values must stay identical to the `Process` +counter set. There is no counter-set selection; `--collector.process.counter-version` +and `Config.CounterVersion` were removed. + +- Process names follow the perflib instance names: the image name without a + case-insensitive `.exe` suffix, other extensions such as `.scr` are kept, and + PID 0 is `Idle`. Users' include and exclude expressions depend on these names. +- The `windows_process_info` labels (owner, command line, process group ID) need + `OpenProcess` and are cached by PID and creation time, because Windows reuses + PIDs. The creation time of the opened handle is compared with the snapshot, + so a PID reused between the snapshot and the lookup is not attributed to the + old process. Failed lookups are not cached and retry on the next scrape; + access-denied results are cached as empty labels. +- Read the snapshot buffer with bounds checks against the returned length, and + keep it 8-byte aligned (`[]uint64` backing array). + +To check value parity after changes, compare the snapshot against the `Process` +counter set by PID in a temporary test, for example with +`pdh.NewCollector(..., pdh.CounterTypeRaw, "Process", pdh.InstancesAll)`, including +processes with names like `a.b.exe` and `tool.scr`. Run it as a standard user +too: neither data source needs administrator rights. + +## IIS application pools IIS application pool names (`--collector.process.iis`) come from two sources. diff --git a/internal/collector/process/apppool.go b/internal/collector/process/apppool.go index 8e1f73c8..697cbca8 100644 --- a/internal/collector/process/apppool.go +++ b/internal/collector/process/apppool.go @@ -27,13 +27,13 @@ import ( "golang.org/x/sys/windows" ) -// workerProcessName is the perflib instance name of IIS worker processes (w3wp.exe). +// workerProcessName is the process name of IIS worker processes (w3wp.exe). const workerProcessName = "w3wp" // errNoAppPoolArgument reports a w3wp command line without a non-empty -ap argument. var errNoAppPoolArgument = errors.New("command line has no -ap argument") -// isWorkerProcess reports whether the perflib process name (without instance suffix) is an IIS worker process. +// isWorkerProcess reports whether the process name is an IIS worker process. func isWorkerProcess(name string) bool { return strings.EqualFold(name, workerProcessName) } diff --git a/internal/collector/process/apppool_test.go b/internal/collector/process/apppool_test.go index 0475ace9..1648c099 100644 --- a/internal/collector/process/apppool_test.go +++ b/internal/collector/process/apppool_test.go @@ -370,7 +370,6 @@ func TestBuildCloseWorkerProcess(t *testing.T) { ProcessInclude: regexp.MustCompile(`^(?:w3wp)$`), ProcessExclude: types.RegExpEmpty, EnableWorkerProcess: true, - CounterVersion: 1, }) logger := slog.New(slog.DiscardHandler) diff --git a/internal/collector/process/process.go b/internal/collector/process/process.go index 7e208d5e..62cd288d 100644 --- a/internal/collector/process/process.go +++ b/internal/collector/process/process.go @@ -25,16 +25,11 @@ import ( "regexp" "slices" "strconv" - "strings" "sync" "time" - "unsafe" "github.com/alecthomas/kingpin/v2" "github.com/prometheus-community/windows_exporter/internal/mi" - "github.com/prometheus-community/windows_exporter/internal/pdh" - "github.com/prometheus-community/windows_exporter/internal/pdh/registry" - pdhtypes "github.com/prometheus-community/windows_exporter/internal/pdh/types" "github.com/prometheus-community/windows_exporter/internal/types" "github.com/prometheus/client_golang/prometheus" "golang.org/x/sys/windows" @@ -42,12 +37,21 @@ import ( const Name = "process" +const ( + // windowsEpoch is the FILETIME of the Unix epoch, in 100ns intervals since 1601. + windowsEpoch int64 = 116444736000000000 + // ticksPerSecond is the number of 100ns intervals per second. + ticksPerSecond = 1e7 + // secondsPerTick converts 100ns intervals to seconds. Multiplying by it, like the perflib + // reader did, keeps the CPU time values bit-identical to the Process counter set. + secondsPerTick = 1 / ticksPerSecond +) + type Config struct { ProcessInclude *regexp.Regexp `yaml:"include"` ProcessExclude *regexp.Regexp `yaml:"exclude"` EnableWorkerProcess bool `yaml:"iis"` EnableCMDLine bool `yaml:"cmdline"` - CounterVersion uint8 `yaml:"counter-version"` } //nolint:gochecknoglobals @@ -56,7 +60,6 @@ var ConfigDefaults = Config{ ProcessExclude: types.RegExpEmpty, EnableWorkerProcess: false, EnableCMDLine: true, - CounterVersion: 1, } type Collector struct { @@ -69,10 +72,10 @@ type Collector struct { miSession *mi.Session workerProcessMIQueryQuery mi.Query - perfDataCollector pdhtypes.Collector[perfDataCounterValues] - perfDataObject []perfDataCounterValues - workerCh chan processWorkerRequest - workerWG sync.WaitGroup + snapshot snapshotBuffer + processes []processSnapshot + // infoCache holds the windows_process_info values of the processes of the last scrape, keyed by PID. + infoCache map[uint32]processInfo lookupCache sync.Map @@ -143,11 +146,6 @@ func NewWithFlags(app *kingpin.Application) *Collector { "If enabled, the full cmdline is exposed to the windows_process_info metrics.", ).Default(strconv.FormatBool(c.config.EnableCMDLine)).BoolVar(&c.config.EnableCMDLine) - app.Flag( - "collector.process.counter-version", - "Version of the process collector to use. 1 for Process V1, 2 for Process V2, 0 for Process V2 with fallback to Process V1 if V2 is not available. Defaults to 1.", - ).Default(strconv.FormatUint(uint64(c.config.CounterVersion), 10)).Uint8Var(&c.config.CounterVersion) - app.Action(func(*kingpin.ParseContext) error { var err error @@ -175,15 +173,9 @@ func (c *Collector) Close() error { c.mu.Lock() defer c.mu.Unlock() - if c.perfDataCollector != nil { - c.perfDataCollector.Close() - } - - if c.workerCh != nil { - close(c.workerCh) - c.workerCh = nil - c.workerWG.Wait() - } + c.snapshot = snapshotBuffer{} + c.processes = nil + c.infoCache = nil // The session belongs to the caller of Build. c.miSession = nil @@ -194,40 +186,7 @@ func (c *Collector) Close() error { func (c *Collector) Build(logger *slog.Logger, miSession *mi.Session) error { c.logger = logger.With(slog.String("collector", Name)) - var err error - - switch c.config.CounterVersion { - case 2: - c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](c.logger, pdh.CounterTypeRaw, "Process V2", pdh.InstancesAll) - case 1: - c.perfDataCollector, err = registry.NewCollector[perfDataCounterValues]("Process", pdh.InstancesAll) - default: - c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](c.logger, pdh.CounterTypeRaw, "Process V2", pdh.InstancesAll) - c.config.CounterVersion = 2 - - if errors.Is(err, pdh.NewPdhError(pdh.CstatusNoObject)) { - // NewCollector returns a partially built collector on errors. Release it before falling back to V1. - c.perfDataCollector.Close() - - c.perfDataCollector, err = registry.NewCollector[perfDataCounterValues]("Process", pdh.InstancesAll) - c.config.CounterVersion = 1 - } - - c.logger.LogAttrs(context.Background(), slog.LevelDebug, fmt.Sprintf("Using process collector V%d", c.config.CounterVersion)) - } - - if err != nil { - return fmt.Errorf("failed to create Process V%d collector: %w", c.config.CounterVersion, err) - } - - c.workerCh = make(chan processWorkerRequest, 32) - - for range 4 { - c.workerWG.Add(1) - go c.collectWorker(c.workerCh) - } - - c.mu = sync.RWMutex{} + c.infoCache = nil c.lookupCache = sync.Map{} // The flags wrap the expressions in ^(?:...)$, the defaults of [New] are [types.RegExpAny] and [types.RegExpEmpty]. @@ -379,173 +338,286 @@ func (c *Collector) buildWorkerProcessWMI(miSession *mi.Session) error { } func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error { - return c.collect(ch, maxScrapeDuration) + c.mu.Lock() + defer c.mu.Unlock() + + var err error + + c.processes, err = c.snapshot.query(c.processes) + if err != nil { + return fmt.Errorf("failed to query processes: %w", err) + } + + processes := make([]*processSnapshot, 0, len(c.processes)) + + var workerProcessIDs []uint32 + + for i := range c.processes { + process := &c.processes[i] + + if c.config.ProcessExclude.MatchString(process.name) || !c.config.ProcessInclude.MatchString(process.name) { + continue + } + + if c.config.EnableWorkerProcess && isWorkerProcess(process.name) { + workerProcessIDs = append(workerProcessIDs, process.pid) + } + + processes = append(processes, process) + } + + var appPools map[uint32]string + + if len(workerProcessIDs) > 0 { + appPools, err = resolveAppPools(c.logger, workerProcessIDs, workerProcessAppPool, c.workerProcessWMIQuery(maxScrapeDuration)) + } + + infos := c.resolveProcessInfo(processes) + + for i, process := range processes { + name := process.name + if appPoolName := appPools[process.pid]; appPoolName != "" { + name = name + "_" + appPoolName + } + + c.collectProcess(ch, name, process, &infos[i]) + } + + return err } -// ref: https://github.com/microsoft/hcsshim/blob/8beabacfc2d21767a07c20f8dd5f9f3932dbf305/internal/uvm/stats.go#L25 -func (c *Collector) getProcessInformation(pid uint32) (string, string, uint32, error) { - if pid == 0 { - return "", "", 0, nil +// workerProcessWMIQuery returns the root\WebAdministration query, or nil if it was unavailable during Build. +func (c *Collector) workerProcessWMIQuery(maxScrapeDuration time.Duration) func() ([]WorkerProcess, error) { + miSession, miQuery := c.miSession, c.workerProcessMIQueryQuery + if miSession == nil { + return nil } - hProcess, vmReadAccess, err := c.openProcess(pid) - if err != nil { - if errors.Is(err, windows.ERROR_ACCESS_DENIED) { - return "", "", 0, nil - } + return func() ([]WorkerProcess, error) { + var workerProcesses []WorkerProcess - return "", "", 0, err + err := miSession.Query(&workerProcesses, mi.NamespaceRootWebAdministration, miQuery, maxScrapeDuration) + + return workerProcesses, err } - - defer func(hProcess windows.Handle) { - if err := windows.CloseHandle(hProcess); err != nil { - c.logger.Warn("CloseHandle failed", - slog.Any("err", err), - ) - } - }(hProcess) - - 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 +// resolveAppPools maps IIS worker process IDs to application pool names. +// +// The command line of the worker process is the primary source: it needs no +// optional feature and no WMI round trip. WMI is queried at most once, for the +// worker processes whose command line couldn't be read or has no -ap argument, +// and only if queryWMI isn't nil. Unresolved processes have no entry. +func resolveAppPools( + logger *slog.Logger, + pids []uint32, + readCommandLine func(pid uint32) (string, error), + queryWMI func() ([]WorkerProcess, error), +) (map[uint32]string, error) { + appPools := make(map[uint32]string, len(pids)) - 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) - } + var unresolved []uint32 - peb := windows.PEB{} + for _, pid := range pids { + appPool, err := readCommandLine(pid) + if err == nil { + appPools[pid] = appPool - 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) - } + continue + } - 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 - - if c.config.EnableCMDLine { - cmdLineUTF16 := make([]uint16, processParameters.CommandLine.Length) - - err = windows.ReadProcessMemory(hProcess, - uintptr(unsafe.Pointer(processParameters.CommandLine.Buffer)), - (*byte)(unsafe.Pointer(&cmdLineUTF16[0])), - uintptr(processParameters.CommandLine.Length), - nil, + logger.LogAttrs(context.Background(), slog.LevelDebug, "Failed to read the IIS application pool from the worker process command line", + slog.Uint64("pid", uint64(pid)), + slog.Any("err", err), ) - if err != nil { - return "", processParameters.ProcessGroupId, fmt.Errorf("failed to read process memory: %w", err) - } - cmdLine = strings.TrimSpace(windows.UTF16ToString(cmdLineUTF16)) + // OpenProcess fails with ERROR_INVALID_PARAMETER for processes that exited after the + // process snapshot. WAS no longer reports them either. + if !errors.Is(err, windows.ERROR_INVALID_PARAMETER) { + unresolved = append(unresolved, pid) + } } - 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) + if len(unresolved) == 0 || queryWMI == nil { + return appPools, nil } - 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() + workerProcesses, err := queryWMI() if err != nil { - return "", fmt.Errorf("failed to get token user: %w", err) + return appPools, fmt.Errorf("WMI query for collector.process.iis failed: %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) + byPID := make(map[uint64]string, len(workerProcesses)) + for _, wp := range workerProcesses { + if _, ok := byPID[wp.ProcessId]; !ok && wp.AppPoolName != "" { + byPID[wp.ProcessId] = wp.AppPoolName } - - c.lookupCache.Store(sid, owner) } - return owner, nil + for _, pid := range unresolved { + if appPool, ok := byPID[uint64(pid)]; ok { + appPools[pid] = appPool + } + } + + return appPools, 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 - } +func (c *Collector) collectProcess(ch chan<- prometheus.Metric, name string, process *processSnapshot, info *processInfo) { + pid := strconv.FormatUint(uint64(process.pid), 10) - if !errors.Is(err, windows.ERROR_ACCESS_DENIED) { - return 0, false, fmt.Errorf("failed to open process: %w", err) - } + ch <- prometheus.MustNewConstMetric( + c.info, + prometheus.GaugeValue, + 1.0, + name, pid, strconv.FormatUint(uint64(process.parentPID), 10), strconv.FormatUint(uint64(info.processGroupID), 10), info.owner, info.cmdLine, + ) - if errors.Is(err, windows.Errno(0x57)) { // invalid parameter, for PIDs that don't exist - return 0, false, errors.New("process not found") - } + // Perflib reports whole seconds for the Elapsed Time counter of the Process counter set. + ch <- prometheus.MustNewConstMetric( + c.startTime, + prometheus.GaugeValue, + float64((process.createTime-windowsEpoch)/ticksPerSecond), + name, pid, + ) - hProcess, err = windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid) - if err != nil { - return 0, false, fmt.Errorf("failed to open process with limited permissions: %w", err) - } + ch <- prometheus.MustNewConstMetric( + c.handleCount, + prometheus.GaugeValue, + float64(process.handleCount), + name, pid, + ) - return hProcess, false, nil + ch <- prometheus.MustNewConstMetric( + c.cpuTimeTotal, + prometheus.CounterValue, + float64(process.kernelTime)*secondsPerTick, + name, pid, "privileged", + ) + + ch <- prometheus.MustNewConstMetric( + c.cpuTimeTotal, + prometheus.CounterValue, + float64(process.userTime)*secondsPerTick, + name, pid, "user", + ) + + ch <- prometheus.MustNewConstMetric( + c.ioBytesTotal, + prometheus.CounterValue, + float64(process.otherTransferCount), + name, pid, "other", + ) + + ch <- prometheus.MustNewConstMetric( + c.ioOperationsTotal, + prometheus.CounterValue, + float64(process.otherOperationCount), + name, pid, "other", + ) + + ch <- prometheus.MustNewConstMetric( + c.ioBytesTotal, + prometheus.CounterValue, + float64(process.readTransferCount), + name, pid, "read", + ) + + ch <- prometheus.MustNewConstMetric( + c.ioOperationsTotal, + prometheus.CounterValue, + float64(process.readOperationCount), + name, pid, "read", + ) + + ch <- prometheus.MustNewConstMetric( + c.ioBytesTotal, + prometheus.CounterValue, + float64(process.writeTransferCount), + name, pid, "write", + ) + + ch <- prometheus.MustNewConstMetric( + c.ioOperationsTotal, + prometheus.CounterValue, + float64(process.writeOperationCount), + name, pid, "write", + ) + + ch <- prometheus.MustNewConstMetric( + c.pageFaultsTotal, + prometheus.CounterValue, + float64(process.pageFaultCount), + name, pid, + ) + + ch <- prometheus.MustNewConstMetric( + c.pageFileBytes, + prometheus.GaugeValue, + float64(process.pageFileUsage), + name, pid, + ) + + ch <- prometheus.MustNewConstMetric( + c.poolBytes, + prometheus.GaugeValue, + float64(process.nonPagedPoolUsage), + name, pid, "nonpaged", + ) + + ch <- prometheus.MustNewConstMetric( + c.poolBytes, + prometheus.GaugeValue, + float64(process.pagedPoolUsage), + name, pid, "paged", + ) + + ch <- prometheus.MustNewConstMetric( + c.priorityBase, + prometheus.GaugeValue, + float64(process.basePriority), + name, pid, + ) + + ch <- prometheus.MustNewConstMetric( + c.privateBytes, + prometheus.GaugeValue, + float64(process.privatePageCount), + name, pid, + ) + + ch <- prometheus.MustNewConstMetric( + c.threadCount, + prometheus.GaugeValue, + float64(process.threadCount), + name, pid, + ) + + ch <- prometheus.MustNewConstMetric( + c.virtualBytes, + prometheus.GaugeValue, + float64(process.virtualSize), + name, pid, + ) + + ch <- prometheus.MustNewConstMetric( + c.workingSetPrivate, + prometheus.GaugeValue, + float64(process.workingSetPrivate), + name, pid, + ) + + ch <- prometheus.MustNewConstMetric( + c.workingSetPeak, + prometheus.GaugeValue, + float64(process.workingSetPeak), + name, pid, + ) + + ch <- prometheus.MustNewConstMetric( + c.workingSet, + prometheus.GaugeValue, + float64(process.workingSetSize), + name, pid, + ) } diff --git a/internal/collector/process/process_iis_test.go b/internal/collector/process/process_iis_test.go index b72b4aa5..9b438d98 100644 --- a/internal/collector/process/process_iis_test.go +++ b/internal/collector/process/process_iis_test.go @@ -93,7 +93,6 @@ func TestCollectorIISAppPool(t *testing.T) { ProcessInclude: regexp.MustCompile(`^(?:w3wp)$`), ProcessExclude: types.RegExpEmpty, EnableWorkerProcess: true, - CounterVersion: 1, }) require.Contains(t, families, "windows_process_info") @@ -142,12 +141,10 @@ func TestCollectorWorkerProcessCommandLine(t *testing.T) { ProcessInclude: regexp.MustCompile(`^(?:w3wp)$`), ProcessExclude: types.RegExpEmpty, EnableWorkerProcess: true, - CounterVersion: 1, }) - if _, ok := families["windows_process_info"]; !ok { - t.Skip("the Process counter set has no w3wp instance") - } + // The process snapshot includes suspended processes. + require.Contains(t, families, "windows_process_info", "no w3wp process in the process snapshot") for _, metric := range families["windows_process_info"].GetMetric() { labels := make(map[string]string) diff --git a/internal/collector/process/process_info.go b/internal/collector/process/process_info.go new file mode 100644 index 00000000..1c4c379b --- /dev/null +++ b/internal/collector/process/process_info.go @@ -0,0 +1,328 @@ +// 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 +} diff --git a/internal/collector/process/process_test.go b/internal/collector/process/process_test.go index 027d7bc6..0f6d09d7 100644 --- a/internal/collector/process/process_test.go +++ b/internal/collector/process/process_test.go @@ -18,11 +18,15 @@ package process_test import ( + "os" + "regexp" + "strconv" "testing" "github.com/alecthomas/kingpin/v2" "github.com/prometheus-community/windows_exporter/internal/collector/process" "github.com/prometheus-community/windows_exporter/internal/utils/testutils" + "github.com/stretchr/testify/require" ) func BenchmarkProcessCollector(b *testing.B) { @@ -37,3 +41,40 @@ func BenchmarkProcessCollector(b *testing.B) { func TestCollector(t *testing.T) { testutils.TestCollector(t, process.New, nil) } + +func TestCollectorCurrentProcess(t *testing.T) { + config := process.ConfigDefaults + config.ProcessInclude = regexp.MustCompile(`^process\.test$`) + + families := testutils.TestCollector(t, process.New, &config) + pid := strconv.Itoa(os.Getpid()) + + infos := families["windows_process_info"].GetMetric() + require.Len(t, infos, 1) + + labels := map[string]string{} + for _, label := range infos[0].GetLabel() { + labels[label.GetName()] = label.GetValue() + } + + require.Equal(t, "process.test", labels["process"]) + require.Equal(t, pid, labels["process_id"]) + require.Equal(t, strconv.Itoa(os.Getppid()), labels["creating_process_id"]) + require.NotEmpty(t, labels["owner"]) + require.Contains(t, labels["cmdline"], "process.test") + + for _, name := range []string{ + "windows_process_start_time_seconds_timestamp", + "windows_process_handles", + "windows_process_threads", + "windows_process_virtual_bytes", + "windows_process_working_set_bytes", + "windows_process_private_bytes", + } { + require.Positive(t, testutils.MetricValuesByLabel(families, name, "process_id")[pid], name) + } + + cpu := families["windows_process_cpu_time_total"].GetMetric() + require.Len(t, cpu, 2) + require.NotNil(t, cpu[0].GetCounter()) +} diff --git a/internal/collector/process/process_worker.go b/internal/collector/process/process_worker.go deleted file mode 100644 index 1c46fa52..00000000 --- a/internal/collector/process/process_worker.go +++ /dev/null @@ -1,380 +0,0 @@ -// 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/debug" - "strconv" - "strings" - "sync" - "time" - - "github.com/prometheus-community/windows_exporter/internal/mi" - "github.com/prometheus/client_golang/prometheus" - "golang.org/x/sys/windows" -) - -type processWorkerRequest struct { - ch chan<- prometheus.Metric - name string - appPoolName string - performanceCounterValues perfDataCounterValues - waitGroup *sync.WaitGroup -} - -func (c *Collector) collect(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error { - err := c.perfDataCollector.Collect(&c.perfDataObject) - if err != nil { - return fmt.Errorf("failed to collect metrics: %w", err) - } - - requests := make([]processWorkerRequest, 0, len(c.perfDataObject)) - - var workerProcessIDs []uint32 - - for _, process := range c.perfDataObject { - // Duplicate processes are suffixed #, and an index number. Remove those. - name, _, _ := strings.Cut(process.Name, ":") // Process V2 - - // Duplicate processes are suffixed #, and an index number. Remove those. - name, _, _ = strings.Cut(name, "#") // Process V1 - - if c.config.ProcessExclude.MatchString(name) || !c.config.ProcessInclude.MatchString(name) { - continue - } - - if process.ProcessID == 0 && name != "Idle" { - c.logger.LogAttrs(context.Background(), slog.LevelDebug, "Skipping process with PID 0", - slog.String("name", name), - slog.String("process_name", process.Name), - slog.Any("process", fmt.Sprintf("%+v", process)), - ) - - continue - } - - if c.config.EnableWorkerProcess && isWorkerProcess(name) { - workerProcessIDs = append(workerProcessIDs, uint32(process.ProcessID)) - } - - requests = append(requests, processWorkerRequest{ - ch: ch, - name: name, - performanceCounterValues: process, - }) - } - - var appPools map[uint32]string - - if len(workerProcessIDs) > 0 { - appPools, err = resolveAppPools(c.logger, workerProcessIDs, workerProcessAppPool, c.workerProcessWMIQuery(maxScrapeDuration)) - } - - wg := &sync.WaitGroup{} - - for _, req := range requests { - req.appPoolName = appPools[uint32(req.performanceCounterValues.ProcessID)] - req.waitGroup = wg - - wg.Add(1) - - c.workerCh <- req - } - - wg.Wait() - - return err -} - -// workerProcessWMIQuery returns the root\WebAdministration query, or nil if it was unavailable during Build. -func (c *Collector) workerProcessWMIQuery(maxScrapeDuration time.Duration) func() ([]WorkerProcess, error) { - miSession, miQuery := c.miSession, c.workerProcessMIQueryQuery - if miSession == nil { - return nil - } - - return func() ([]WorkerProcess, error) { - var workerProcesses []WorkerProcess - - err := miSession.Query(&workerProcesses, mi.NamespaceRootWebAdministration, miQuery, maxScrapeDuration) - - return workerProcesses, err - } -} - -// resolveAppPools maps IIS worker process IDs to application pool names. -// -// The command line of the worker process is the primary source: it needs no -// optional feature and no WMI round trip. WMI is queried at most once, for the -// worker processes whose command line couldn't be read or has no -ap argument, -// and only if queryWMI isn't nil. Unresolved processes have no entry. -func resolveAppPools( - logger *slog.Logger, - pids []uint32, - readCommandLine func(pid uint32) (string, error), - queryWMI func() ([]WorkerProcess, error), -) (map[uint32]string, error) { - appPools := make(map[uint32]string, len(pids)) - - var unresolved []uint32 - - for _, pid := range pids { - appPool, err := readCommandLine(pid) - if err == nil { - appPools[pid] = appPool - - continue - } - - logger.LogAttrs(context.Background(), slog.LevelDebug, "Failed to read the IIS application pool from the worker process command line", - slog.Uint64("pid", uint64(pid)), - slog.Any("err", err), - ) - - // OpenProcess fails with ERROR_INVALID_PARAMETER for processes that exited after the - // performance counter snapshot. WAS no longer reports them either. - if !errors.Is(err, windows.ERROR_INVALID_PARAMETER) { - unresolved = append(unresolved, pid) - } - } - - if len(unresolved) == 0 || queryWMI == nil { - return appPools, nil - } - - workerProcesses, err := queryWMI() - if err != nil { - return appPools, fmt.Errorf("WMI query for collector.process.iis failed: %w", err) - } - - for _, pid := range unresolved { - for _, wp := range workerProcesses { - if wp.ProcessId == uint64(pid) && wp.AppPoolName != "" { - appPools[pid] = wp.AppPoolName - - break - } - } - } - - return appPools, nil -} - -func (c *Collector) collectWorker(requests <-chan processWorkerRequest) { - defer c.workerWG.Done() - - for req := range requests { - (func() { - defer req.waitGroup.Done() - defer func() { - if r := recover(); r != nil { - c.logger.Error("Worker panic", - slog.Any("panic", r), - slog.String("stack", string(debug.Stack())), - ) - } - }() - - ch := req.ch - name := req.name - data := req.performanceCounterValues - - pid := uint64(data.ProcessID) - parentPID := strconv.FormatUint(uint64(data.CreatingProcessID), 10) - - if req.appPoolName != "" { - name = name + "_" + req.appPoolName - } - - cmdLine, processOwner, processGroupID, err := c.getProcessInformation(uint32(pid)) - if err != nil { - c.logger.LogAttrs(context.Background(), slog.LevelDebug, "Failed to get process information", - slog.Uint64("pid", pid), - slog.Any("err", err), - ) - } - - pidString := strconv.FormatUint(pid, 10) - - ch <- prometheus.MustNewConstMetric( - c.info, - prometheus.GaugeValue, - 1.0, - name, pidString, parentPID, strconv.Itoa(int(processGroupID)), processOwner, cmdLine, - ) - - startTime := data.ElapsedTime - if c.config.CounterVersion == 2 { - // For V2, the ElapsedTime is in seconds, so we need to convert it to a timestamp. - // The start time is the current time minus the elapsed time. - startTime = float64(time.Now().UnixMicro())/1e6 - data.ElapsedTime - } - - ch <- prometheus.MustNewConstMetric( - c.startTime, - prometheus.GaugeValue, - startTime, - name, pidString, - ) - - ch <- prometheus.MustNewConstMetric( - c.handleCount, - prometheus.GaugeValue, - data.HandleCount, - name, pidString, - ) - - ch <- prometheus.MustNewConstMetric( - c.cpuTimeTotal, - prometheus.CounterValue, - data.PercentPrivilegedTime, - name, pidString, "privileged", - ) - - ch <- prometheus.MustNewConstMetric( - c.cpuTimeTotal, - prometheus.CounterValue, - data.PercentUserTime, - name, pidString, "user", - ) - - ch <- prometheus.MustNewConstMetric( - c.ioBytesTotal, - prometheus.CounterValue, - data.IoOtherBytesPerSec, - name, pidString, "other", - ) - - ch <- prometheus.MustNewConstMetric( - c.ioOperationsTotal, - prometheus.CounterValue, - data.IoOtherOperationsPerSec, - name, pidString, "other", - ) - - ch <- prometheus.MustNewConstMetric( - c.ioBytesTotal, - prometheus.CounterValue, - data.IoReadBytesPerSec, - name, pidString, "read", - ) - - ch <- prometheus.MustNewConstMetric( - c.ioOperationsTotal, - prometheus.CounterValue, - data.IoReadOperationsPerSec, - name, pidString, "read", - ) - - ch <- prometheus.MustNewConstMetric( - c.ioBytesTotal, - prometheus.CounterValue, - data.IoWriteBytesPerSec, - name, pidString, "write", - ) - - ch <- prometheus.MustNewConstMetric( - c.ioOperationsTotal, - prometheus.CounterValue, - data.IoWriteOperationsPerSec, - name, pidString, "write", - ) - - ch <- prometheus.MustNewConstMetric( - c.pageFaultsTotal, - prometheus.CounterValue, - data.PageFaultsPerSec, - name, pidString, - ) - - ch <- prometheus.MustNewConstMetric( - c.pageFileBytes, - prometheus.GaugeValue, - data.PageFileBytes, - name, pidString, - ) - - ch <- prometheus.MustNewConstMetric( - c.poolBytes, - prometheus.GaugeValue, - data.PoolNonPagedBytes, - name, pidString, "nonpaged", - ) - - ch <- prometheus.MustNewConstMetric( - c.poolBytes, - prometheus.GaugeValue, - data.PoolPagedBytes, - name, pidString, "paged", - ) - - ch <- prometheus.MustNewConstMetric( - c.priorityBase, - prometheus.GaugeValue, - data.PriorityBase, - name, pidString, - ) - - ch <- prometheus.MustNewConstMetric( - c.privateBytes, - prometheus.GaugeValue, - data.PrivateBytes, - name, pidString, - ) - - ch <- prometheus.MustNewConstMetric( - c.threadCount, - prometheus.GaugeValue, - data.ThreadCount, - name, pidString, - ) - - ch <- prometheus.MustNewConstMetric( - c.virtualBytes, - prometheus.GaugeValue, - data.VirtualBytes, - name, pidString, - ) - - ch <- prometheus.MustNewConstMetric( - c.workingSetPrivate, - prometheus.GaugeValue, - data.WorkingSetPrivate, - name, pidString, - ) - - ch <- prometheus.MustNewConstMetric( - c.workingSetPeak, - prometheus.GaugeValue, - data.WorkingSetPeak, - name, pidString, - ) - - ch <- prometheus.MustNewConstMetric( - c.workingSet, - prometheus.GaugeValue, - data.WorkingSet, - name, pidString, - ) - })() - } -} diff --git a/internal/collector/process/process_worker_test.go b/internal/collector/process/process_worker_test.go deleted file mode 100644 index 58f78f4a..00000000 --- a/internal/collector/process/process_worker_test.go +++ /dev/null @@ -1,89 +0,0 @@ -// 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 ( - "log/slog" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestCloseBeforeWorkerStarts(t *testing.T) { - t.Parallel() - - c := New(nil) - requests := make(chan processWorkerRequest) - c.workerCh = requests - c.workerWG.Add(1) - - closed := make(chan error, 1) - go func() { closed <- c.Close() }() - - select { - case _, ok := <-requests: - require.False(t, ok) - case <-time.After(time.Second): - t.Fatal("Close did not close the worker channel") - } - - // The worker starts after Close has set c.workerCh to nil. - go c.collectWorker(requests) - - select { - case err := <-closed: - require.NoError(t, err) - case <-time.After(time.Second): - t.Fatal("Close did not wait for the delayed worker to exit") - } - - require.NoError(t, c.Close()) -} - -func TestWorkerContinuesAfterRequestPanic(t *testing.T) { - t.Parallel() - - c := New(nil) - c.logger = slog.New(slog.DiscardHandler) - c.workerCh = make(chan processWorkerRequest, 2) - - c.workerWG.Add(1) - go c.collectWorker(c.workerCh) - - var requests sync.WaitGroup - requests.Add(2) - - for range 2 { - // Missing descriptors make metric construction panic in both requests. - c.workerCh <- processWorkerRequest{waitGroup: &requests} - } - - processed := make(chan struct{}) - - go func() { requests.Wait(); close(processed) }() - - select { - case <-processed: - case <-time.After(time.Second): - t.Fatal("worker did not recover and process the next request") - } - - require.NoError(t, c.Close()) -} diff --git a/internal/collector/process/system_process_information.go b/internal/collector/process/system_process_information.go new file mode 100644 index 00000000..02824807 --- /dev/null +++ b/internal/collector/process/system_process_information.go @@ -0,0 +1,179 @@ +// 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 ( + "errors" + "fmt" + "strings" + "unsafe" + + "golang.org/x/sys/windows" +) + +// maxSnapshotAttempts bounds the retries when processes are created +// between the size query and the snapshot. +const maxSnapshotAttempts = 5 + +// processSnapshot is one process from SystemProcessInformation. +// The values are the same that the perflib Process counter set reads. +type processSnapshot struct { + name string + + pid uint32 + parentPID uint32 + + // createTime is the FILETIME of the process creation, in 100ns intervals since 1601. + createTime int64 + // userTime and kernelTime are in 100ns intervals. + userTime int64 + kernelTime int64 + + basePriority int32 + handleCount uint32 + threadCount uint32 + + pageFaultCount uint32 + virtualSize uint64 + workingSetSize uint64 + workingSetPeak uint64 + workingSetPrivate uint64 + pagedPoolUsage uint64 + nonPagedPoolUsage uint64 + pageFileUsage uint64 + privatePageCount uint64 + + readOperationCount uint64 + writeOperationCount uint64 + otherOperationCount uint64 + readTransferCount uint64 + writeTransferCount uint64 + otherTransferCount uint64 +} + +// snapshotBuffer holds the SystemProcessInformation buffer across scrapes. +// The backing array is []uint64, so the process entries are 8-byte aligned. +type snapshotBuffer struct { + buf []uint64 +} + +// query returns all processes of the system with one NtQuerySystemInformation call. +func (s *snapshotBuffer) query(dst []processSnapshot) ([]processSnapshot, error) { + if len(s.buf) == 0 { + s.buf = make([]uint64, 256<<10/8) + } + + var returnLength uint32 + + for attempt := 0; ; attempt++ { + err := windows.NtQuerySystemInformation( + windows.SystemProcessInformation, + unsafe.Pointer(&s.buf[0]), + uint32(len(s.buf)*8), + &returnLength, + ) + if err == nil { + break + } + + if !errors.Is(err, windows.STATUS_INFO_LENGTH_MISMATCH) || attempt >= maxSnapshotAttempts { + return dst, fmt.Errorf("NtQuerySystemInformation(SystemProcessInformation): %w", err) + } + + // Processes can start before the next call. Leave room for them. + size := max(uint64(returnLength), uint64(len(s.buf))*8) + s.buf = make([]uint64, (size+size/4)/8+1) + } + + return parseSystemProcessInformation(dst[:0], unsafe.Slice((*byte)(unsafe.Pointer(&s.buf[0])), len(s.buf)*8), returnLength) +} + +// parseSystemProcessInformation decodes the entries of a SystemProcessInformation buffer. +// The buffer must be 8-byte aligned. The image names are copied, so dst does not reference buf. +func parseSystemProcessInformation(dst []processSnapshot, buf []byte, length uint32) ([]processSnapshot, error) { + end := min(uint64(length), uint64(len(buf))) + entrySize := uint64(unsafe.Sizeof(windows.SYSTEM_PROCESS_INFORMATION{})) + + for offset := uint64(0); ; { + if offset+entrySize > end { + return dst, fmt.Errorf("SystemProcessInformation entry at offset %d exceeds the buffer length %d", offset, end) + } + + entry := (*windows.SYSTEM_PROCESS_INFORMATION)(unsafe.Pointer(&buf[offset])) + + var imageName string + + if entry.ImageName.Length > 0 && entry.ImageName.Buffer != nil { + // The kernel stores the image name inside the buffer. Check it, so a bad entry cannot read outside of it. + nameOffset := uint64(uintptr(unsafe.Pointer(entry.ImageName.Buffer)) - uintptr(unsafe.Pointer(&buf[0]))) + if nameOffset >= end || uint64(entry.ImageName.Length) > end-nameOffset { + return dst, fmt.Errorf("SystemProcessInformation image name at offset %d exceeds the buffer length %d", nameOffset, end) + } + + imageName = entry.ImageName.String() + } + + dst = append(dst, processSnapshot{ + name: processName(uint32(entry.UniqueProcessID), imageName), + pid: uint32(entry.UniqueProcessID), + parentPID: uint32(entry.InheritedFromUniqueProcessID), + createTime: entry.CreateTime, + userTime: entry.UserTime, + kernelTime: entry.KernelTime, + basePriority: entry.BasePriority, + handleCount: entry.HandleCount, + threadCount: entry.NumberOfThreads, + pageFaultCount: entry.PageFaultCount, + virtualSize: uint64(entry.VirtualSize), + workingSetSize: uint64(entry.WorkingSetSize), + workingSetPeak: uint64(entry.PeakWorkingSetSize), + workingSetPrivate: uint64(entry.WorkingSetPrivateSize), + pagedPoolUsage: uint64(entry.QuotaPagedPoolUsage), + nonPagedPoolUsage: uint64(entry.QuotaNonPagedPoolUsage), + pageFileUsage: uint64(entry.PagefileUsage), + privatePageCount: uint64(entry.PrivatePageCount), + readOperationCount: uint64(entry.ReadOperationCount), + writeOperationCount: uint64(entry.WriteOperationCount), + otherOperationCount: uint64(entry.OtherOperationCount), + readTransferCount: uint64(entry.ReadTransferCount), + writeTransferCount: uint64(entry.WriteTransferCount), + otherTransferCount: uint64(entry.OtherTransferCount), + }) + + if entry.NextEntryOffset == 0 { + return dst, nil + } + + offset += uint64(entry.NextEntryOffset) + } +} + +// processName returns the instance name that the perflib Process counter set uses: +// the image name without a case-insensitive ".exe" suffix, and "Idle" for the System Idle Process. +// Other extensions, like ".scr", are kept. +func processName(pid uint32, imageName string) string { + if pid == 0 && imageName == "" { + return "Idle" + } + + if len(imageName) > len(".exe") && strings.EqualFold(imageName[len(imageName)-len(".exe"):], ".exe") { + return imageName[:len(imageName)-len(".exe")] + } + + return imageName +} diff --git a/internal/collector/process/system_process_information_test.go b/internal/collector/process/system_process_information_test.go new file mode 100644 index 00000000..48a59ef0 --- /dev/null +++ b/internal/collector/process/system_process_information_test.go @@ -0,0 +1,195 @@ +// 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 ( + "log/slog" + "os" + "testing" + "unsafe" + + "github.com/stretchr/testify/require" + "golang.org/x/sys/windows" +) + +func TestProcessName(t *testing.T) { + t.Parallel() + + // The names follow the instances of the perflib Process counter set. + for _, tc := range []struct { + pid uint32 + imageName string + want string + }{ + {0, "", "Idle"}, + {4, "System", "System"}, + {100, "Registry", "Registry"}, + {200, "svchost.exe", "svchost"}, + {300, "EPSDNMON.EXE", "EPSDNMON"}, + {400, "Microsoft.Photos.exe", "Microsoft.Photos"}, + {500, "screensaver.scr", "screensaver.scr"}, + {600, ".exe", ".exe"}, + } { + require.Equal(t, tc.want, processName(tc.pid, tc.imageName), "pid %d image %q", tc.pid, tc.imageName) + } +} + +func TestSnapshotContainsCurrentProcess(t *testing.T) { + t.Parallel() + + var s snapshotBuffer + + processes, err := s.query(nil) + require.NoError(t, err) + require.NotEmpty(t, processes) + + self := findProcess(t, processes, uint32(os.Getpid())) + require.Equal(t, uint32(os.Getppid()), self.parentPID) + require.Equal(t, currentProcessCreateTime(t), self.createTime) + require.NotZero(t, self.threadCount) + require.NotZero(t, self.handleCount) + require.NotZero(t, self.workingSetSize) + require.NotZero(t, self.privatePageCount) + + idle := findProcess(t, processes, 0) + require.Equal(t, "Idle", idle.name) +} + +func TestSnapshotGrowsBuffer(t *testing.T) { + t.Parallel() + + s := snapshotBuffer{buf: make([]uint64, 1)} + + processes, err := s.query(nil) + require.NoError(t, err) + require.NotEmpty(t, processes) + require.Greater(t, len(s.buf), 1) +} + +func TestParseSystemProcessInformationRejectsTruncatedBuffer(t *testing.T) { + t.Parallel() + + entrySize := int(unsafe.Sizeof(windows.SYSTEM_PROCESS_INFORMATION{})) + buf := make([]uint64, entrySize/8+1) + data := unsafe.Slice((*byte)(unsafe.Pointer(&buf[0])), len(buf)*8) + + // The first entry points to a second entry behind the end of the buffer. + (*windows.SYSTEM_PROCESS_INFORMATION)(unsafe.Pointer(&data[0])).NextEntryOffset = uint32(entrySize) + + processes, err := parseSystemProcessInformation(nil, data, uint32(len(data))) + require.Error(t, err) + require.Len(t, processes, 1) + + _, err = parseSystemProcessInformation(nil, data, uint32(entrySize-1)) + require.Error(t, err) +} + +func TestParseSystemProcessInformationRejectsImageNameOutsideBuffer(t *testing.T) { + t.Parallel() + + entrySize := int(unsafe.Sizeof(windows.SYSTEM_PROCESS_INFORMATION{})) + buf := make([]uint64, entrySize/8+1) + data := unsafe.Slice((*byte)(unsafe.Pointer(&buf[0])), len(buf)*8) + other := []uint16{'a', 'b'} + + entry := (*windows.SYSTEM_PROCESS_INFORMATION)(unsafe.Pointer(&data[0])) + entry.ImageName.Buffer = &other[0] + entry.ImageName.Length = 4 + + _, err := parseSystemProcessInformation(nil, data, uint32(len(data))) + require.Error(t, err) + + // A name inside the buffer, but longer than the rest of it. + entry.ImageName.Buffer = (*uint16)(unsafe.Pointer(&data[len(data)-2])) + _, err = parseSystemProcessInformation(nil, data, uint32(len(data))) + require.Error(t, err) +} + +func TestResolveProcessInfoCache(t *testing.T) { + t.Parallel() + + c := New(nil) + c.logger = slog.New(slog.DiscardHandler) + + var s snapshotBuffer + + processes, err := s.query(nil) + require.NoError(t, err) + + self := findProcess(t, processes, uint32(os.Getpid())) + exited := processSnapshot{pid: 1<<32 - 4, createTime: 1} + + infos := c.resolveProcessInfo([]*processSnapshot{&self, &exited}) + require.NotEmpty(t, infos[0].owner) + require.NotEmpty(t, infos[0].cmdLine) + require.Contains(t, c.infoCache, self.pid) + require.NotContains(t, c.infoCache, exited.pid, "failed lookups must be retried") + + // A cached entry with the same creation time is reused without opening the process. + c.infoCache[self.pid] = processInfo{createTime: self.createTime, owner: "cached"} + infos = c.resolveProcessInfo([]*processSnapshot{&self}) + require.Equal(t, "cached", infos[0].owner) + + // A reused PID has another creation time and is looked up again. + c.infoCache[self.pid] = processInfo{createTime: self.createTime - 1, owner: "cached"} + infos = c.resolveProcessInfo([]*processSnapshot{&self}) + require.NotEqual(t, "cached", infos[0].owner) + + // Processes that are gone are evicted. + c.resolveProcessInfo(nil) + require.Empty(t, c.infoCache) +} + +func TestGetProcessInformationDetectsReusedPID(t *testing.T) { + t.Parallel() + + c := New(nil) + c.logger = slog.New(slog.DiscardHandler) + + _, _, _, err := c.getProcessInformation(uint32(os.Getpid()), currentProcessCreateTime(t)+1) + require.ErrorIs(t, err, errProcessGone) + + cmdLine, owner, _, err := c.getProcessInformation(uint32(os.Getpid()), currentProcessCreateTime(t)) + require.NoError(t, err) + require.NotEmpty(t, cmdLine) + require.NotEmpty(t, owner) +} + +func findProcess(t *testing.T, processes []processSnapshot, pid uint32) processSnapshot { + t.Helper() + + for _, process := range processes { + if process.pid == pid { + return process + } + } + + require.FailNow(t, "process not found in snapshot", "pid %d", pid) + + return processSnapshot{} +} + +func currentProcessCreateTime(t *testing.T) int64 { + t.Helper() + + var creationTime, exitTime, kernelTime, userTime windows.Filetime + + require.NoError(t, windows.GetProcessTimes(windows.CurrentProcess(), &creationTime, &exitTime, &kernelTime, &userTime)) + + return int64(creationTime.HighDateTime)<<32 | int64(creationTime.LowDateTime) +} diff --git a/internal/collector/process/types.go b/internal/collector/process/types.go index 7ee77307..dea1e961 100644 --- a/internal/collector/process/types.go +++ b/internal/collector/process/types.go @@ -21,36 +21,3 @@ type WorkerProcess struct { AppPoolName string `mi:"AppPoolName"` ProcessId uint64 `mi:"ProcessId"` } - -type perfDataCounterValues struct { - Name string - - PercentProcessorTime float64 `perfdata:"% Processor Time"` - PercentPrivilegedTime float64 `perfdata:"% Privileged Time"` - PercentUserTime float64 `perfdata:"% User Time"` - CreatingProcessID float64 `perfdata:"Creating Process ID"` - ElapsedTime float64 `perfdata:"Elapsed Time"` - HandleCount float64 `perfdata:"Handle Count"` - IoDataBytesPerSec float64 `perfdata:"IO Data Bytes/sec"` - IoDataOperationsPerSec float64 `perfdata:"IO Data Operations/sec"` - IoOtherBytesPerSec float64 `perfdata:"IO Other Bytes/sec"` - IoOtherOperationsPerSec float64 `perfdata:"IO Other Operations/sec"` - IoReadBytesPerSec float64 `perfdata:"IO Read Bytes/sec"` - IoReadOperationsPerSec float64 `perfdata:"IO Read Operations/sec"` - IoWriteBytesPerSec float64 `perfdata:"IO Write Bytes/sec"` - IoWriteOperationsPerSec float64 `perfdata:"IO Write Operations/sec"` - PageFaultsPerSec float64 `perfdata:"Page Faults/sec"` - PageFileBytesPeak float64 `perfdata:"Page File Bytes Peak"` - PageFileBytes float64 `perfdata:"Page File Bytes"` - PoolNonPagedBytes float64 `perfdata:"Pool Nonpaged Bytes"` - PoolPagedBytes float64 `perfdata:"Pool Paged Bytes"` - PriorityBase float64 `perfdata:"Priority Base"` - PrivateBytes float64 `perfdata:"Private Bytes"` - ThreadCount float64 `perfdata:"Thread Count"` - VirtualBytesPeak float64 `perfdata:"Virtual Bytes Peak"` - VirtualBytes float64 `perfdata:"Virtual Bytes"` - WorkingSetPrivate float64 `perfdata:"Working Set - Private"` - WorkingSetPeak float64 `perfdata:"Working Set Peak"` - WorkingSet float64 `perfdata:"Working Set"` - ProcessID float64 `perfdata:"Process ID" perfdata_v1:"ID Process"` -} diff --git a/internal/pdh/instance_total_test.go b/internal/pdh/instance_total_test.go index 8690f111..7af918f5 100644 --- a/internal/pdh/instance_total_test.go +++ b/internal/pdh/instance_total_test.go @@ -26,7 +26,6 @@ import ( "time" "github.com/prometheus-community/windows_exporter/internal/pdh" - "github.com/prometheus-community/windows_exporter/internal/pdh/registry" "github.com/stretchr/testify/require" ) @@ -78,30 +77,22 @@ func TestTotalSuffixProcess(t *testing.T) { ID float64 `perfdata:"ID Process"` } - legacy, err := registry.NewCollector[processValues]("Process", pdh.InstancesAll) + collector, err := pdh.NewCollector[processValues](slog.New(slog.DiscardHandler), pdh.CounterTypeRaw, "Process", []string{"worker_Total"}) require.NoError(t, err) - t.Cleanup(legacy.Close) + t.Cleanup(collector.Close) - modern, err := pdh.NewCollector[processValues](slog.New(slog.DiscardHandler), pdh.CounterTypeRaw, "Process", []string{"worker_Total"}) - require.NoError(t, err) - t.Cleanup(modern.Close) + require.Eventually(t, func() bool { + var rows []processValues + if collector.Collect(&rows) != nil { + return false + } - for name, collect := range map[string]func(*[]processValues) error{"registry": legacy.Collect, "explicit PDH": modern.Collect} { - t.Run(name, func(t *testing.T) { - require.Eventually(t, func() bool { - var rows []processValues - if collect(&rows) != nil { - return false - } + for _, row := range rows { + if row.ID == float64(child.Process.Pid) && row.Name == "worker_Total" { + return true + } + } - for _, row := range rows { - if row.ID == float64(child.Process.Pid) && row.Name == "worker_Total" { - return true - } - } - - return false - }, time.Second*5, time.Millisecond*100) - }) - } + return false + }, time.Second*5, time.Millisecond*100) } diff --git a/internal/pdh/registry/LICENSE b/internal/pdh/registry/LICENSE deleted file mode 100644 index 169f2ab9..00000000 --- a/internal/pdh/registry/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2018 Leopold Schabel / The perflib_exporter authors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/internal/pdh/registry/collector.go b/internal/pdh/registry/collector.go deleted file mode 100644 index 3cd70a26..00000000 --- a/internal/pdh/registry/collector.go +++ /dev/null @@ -1,229 +0,0 @@ -// 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 registry - -import ( - "errors" - "fmt" - "reflect" - "strings" - "sync" - - "github.com/prometheus-community/windows_exporter/internal/pdh" -) - -type Collector[T any] struct { - object string - query string - - counters map[string]Counter - nameIndexValue int - - // bufferMu guards buffer, the raw data buffer of the last query. A Collect - // takes it out for the duration of the query, so concurrent calls never - // share it; they allocate their own buffer instead. - bufferMu sync.Mutex - buffer []byte -} - -type Counter struct { - Name string - Desc string - Instances map[string]uint32 - Type uint32 - Frequency float64 - - FieldIndexValue int - FieldIndexSecondValue int -} - -// NewCollector creates a collector for the counters declared by the perfdata_v1 or perfdata tags of the struct T. -func NewCollector[T any](object string, _ []string) (*Collector[T], error) { - valueType := reflect.TypeFor[T]() - if valueType.Kind() != reflect.Struct { - return nil, fmt.Errorf("expected a struct, got %s", valueType) - } - - if err := CounterNameTable.load(); err != nil { - return nil, err - } - - query := MapCounterToIndex(object) - if query == "0" { - return nil, fmt.Errorf("perflib object %q not found in the counter name table", object) - } - - collector := &Collector[T]{ - object: object, - query: query, - nameIndexValue: -1, - counters: make(map[string]Counter), - } - - if f, ok := valueType.FieldByName("Name"); ok { - if f.Type.Kind() == reflect.String { - collector.nameIndexValue = f.Index[0] - } - } - - for _, f := range reflect.VisibleFields(valueType) { - tag, ok := f.Tag.Lookup("perfdata_v1") - if !ok { - tag, ok = f.Tag.Lookup("perfdata") - if !ok { - continue - } - } - - if f.Type.Kind() != reflect.Float64 { - return nil, fmt.Errorf("field %s must be a float64", f.Name) - } - - counterName, secondValue := strings.CutSuffix(tag, ",secondvalue") - - counter, ok := collector.counters[counterName] - if !ok { - counter = Counter{ - Name: counterName, - FieldIndexSecondValue: -1, - FieldIndexValue: -1, - } - } - - if secondValue { - counter.FieldIndexSecondValue = f.Index[0] - } else { - counter.FieldIndexValue = f.Index[0] - } - - collector.counters[counterName] = counter - } - - var collectValues []T - - if err := collector.Collect(&collectValues); err != nil { - return nil, fmt.Errorf("failed to collect initial data: %w", err) - } - - return collector, nil -} - -func (c *Collector[T]) Describe() map[string]string { - return map[string]string{} -} - -// Collect replaces the content of dst with one row per collected instance. -func (c *Collector[T]) Collect(dst *[]T) error { - if dst == nil { - return errors.New("dst must not be nil") - } - - c.bufferMu.Lock() - buffer := c.buffer - c.buffer = nil - c.bufferMu.Unlock() - - perfObjects, buffer, err := queryPerformanceData(c.query, c.object, buffer) - - // Keep the larger buffer if a concurrent call put one back in the meantime. - c.bufferMu.Lock() - if cap(buffer) > cap(c.buffer) { - c.buffer = buffer - } - c.bufferMu.Unlock() - - if err != nil { - return fmt.Errorf("QueryPerformanceData: %w", err) - } - - if len(perfObjects) == 0 || perfObjects[0] == nil || len(perfObjects[0].Instances) == 0 { - return nil - } - - *dst = make([]T, 0, len(perfObjects[0].Instances)) - - for _, perfObject := range perfObjects { - if perfObject.Name != c.object { - continue - } - - for _, perfInstance := range perfObject.Instances { - instanceName := perfInstance.Name - if pdh.IsTotalInstance(c.object, instanceName) { - continue - } - - if instanceName == "" || instanceName == "*" { - instanceName = pdh.InstanceEmpty - } - - var row T - - rv := reflect.ValueOf(&row).Elem() - - if c.nameIndexValue != -1 { - rv.Field(c.nameIndexValue).SetString(instanceName) - } - - for _, perfCounter := range perfInstance.Counters { - if perfCounter.Def.IsBaseValue && !perfCounter.Def.IsNanosecondCounter { - continue - } - - counter, ok := c.counters[perfCounter.Def.Name] - if !ok { - continue - } - - switch perfCounter.Def.CounterType { - case pdh.PERF_ELAPSED_TIME: - // A zero frequency would divide by zero. The value is left unset. - if perfObject.Frequency <= 0 || counter.FieldIndexValue == -1 { - continue - } - - rv.Field(counter.FieldIndexValue). - SetFloat(float64((perfCounter.Value - pdh.WindowsEpoch) / perfObject.Frequency)) - case pdh.PERF_100NSEC_TIMER, pdh.PERF_PRECISION_100NS_TIMER: - if counter.FieldIndexValue == -1 { - continue - } - - rv.Field(counter.FieldIndexValue). - SetFloat(float64(perfCounter.Value) * pdh.TicksToSecondScaleFactor) - default: - if counter.FieldIndexSecondValue != -1 { - rv.Field(counter.FieldIndexSecondValue). - SetFloat(float64(perfCounter.SecondValue)) - } - - if counter.FieldIndexValue != -1 { - rv.Field(counter.FieldIndexValue). - SetFloat(float64(perfCounter.Value)) - } - } - } - - *dst = append(*dst, row) - } - } - - return nil -} - -func (c *Collector[T]) Close() {} diff --git a/internal/pdh/registry/nametable.go b/internal/pdh/registry/nametable.go deleted file mode 100644 index a729d5df..00000000 --- a/internal/pdh/registry/nametable.go +++ /dev/null @@ -1,125 +0,0 @@ -// 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 registry - -import ( - "bytes" - "fmt" - "strconv" - "sync" -) - -// CounterNameTable Initialize global name tables -// profiling, add option to disable name tables if necessary -// Not sure if we should resolve the names at all or just have the caller do it on demand -// (for many use cases the index is sufficient) -// -//nolint:gochecknoglobals -var CounterNameTable = QueryNameTable("Counter 009") - -func (p *perfObjectType) LookupName() string { - return CounterNameTable.LookupString(p.ObjectNameTitleIndex) -} - -type NameTable struct { - mu sync.Mutex - - name string - - table struct { - index map[uint32]string - string map[string]uint32 - } -} - -// LookupString returns the name for index. It returns an empty string if the -// index is unknown or the name table could not be loaded. -func (t *NameTable) LookupString(index uint32) string { - if t.load() != nil { - return "" - } - - return t.table.index[index] -} - -// LookupIndex returns the index for str. It returns 0 if the name is unknown -// or the name table could not be loaded. -func (t *NameTable) LookupIndex(str string) uint32 { - if t.load() != nil { - return 0 - } - - return t.table.string[str] -} - -// QueryNameTable Query a perflib name table from the v1. Specify the type and the language -// code (i.e. "Counter 009" or "Help 009") for English language. -func QueryNameTable(tableName string) *NameTable { - return &NameTable{ - name: tableName, - } -} - -// load reads the name table from the registry. A failed load is retried on the -// next call, so a transient error does not disable the table for good. The -// maps are not modified once loaded. -func (t *NameTable) load() error { - t.mu.Lock() - defer t.mu.Unlock() - - if t.table.index != nil { - return nil - } - - buffer, err := queryRawData(t.name, nil) - if err != nil { - return fmt.Errorf("failed to query perflib name table %q: %w", t.name, err) - } - - t.table.index, t.table.string = parseNameTable(buffer) - - return nil -} - -// parseNameTable parses a name table, a REG_MULTI_SZ of alternating index and -// name strings. Parsing stops at the first incomplete pair. -func parseNameTable(buffer []byte) (map[uint32]string, map[string]uint32) { - index := make(map[uint32]string) - names := make(map[string]uint32) - - r := bytes.NewReader(buffer) - - for { - indexStr, err := readUTF16String(r) - if err != nil { - break - } - - desc, err := readUTF16String(r) - if err != nil { - break - } - - indexInt, _ := strconv.ParseUint(indexStr, 10, 32) - - index[uint32(indexInt)] = desc - names[desc] = uint32(indexInt) - } - - return index, names -} diff --git a/internal/pdh/registry/perflib.go b/internal/pdh/registry/perflib.go deleted file mode 100644 index 7b92a8ef..00000000 --- a/internal/pdh/registry/perflib.go +++ /dev/null @@ -1,594 +0,0 @@ -// 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 registry - -/* -Go bindings for the HKEY_PERFORMANCE_DATA perflib / Performance Counters interface. - -# Overview - -HKEY_PERFORMANCE_DATA is a low-level alternative to the higher-level PDH library and WMI. -It operates on blocks of counters and only returns raw values without calculating rates -or formatting them, which is exactly what you want for, say, a Prometheus exporter -(not so much for a GUI like Windows Performance Monitor). - -Its overhead is much lower than the high-level libraries. - -It operates on the same set of perflib providers as PDH and WMI. See this document -for more details on the relationship between the different libraries: -https://msdn.microsoft.com/en-us/library/windows/desktop/aa371643(v=vs.85).aspx - -Example C++ source code: -https://msdn.microsoft.com/de-de/library/windows/desktop/aa372138(v=vs.85).aspx - -For now, the API is not stable and is probably going to change in future -perflib_exporter releases. If you want to use this library, send the author an email -so we can discuss your requirements and stabilize the API. - -# Names - -Counter names and help texts are resolved by looking up an index in a name table. -Since Microsoft loves internalization, both names and help texts can be requested -any locally available language. - -The library automatically loads the name tables and resolves all identifiers -in English ("Name" and "HelpText" struct members). You can manually resolve -identifiers in a different language by using the NameTable API. - -# Performance Counters intro - -Windows has a system-wide performance counter mechanism. Most performance counters -are stored as actual counters, not gauges (with some exceptions). -There's additional metadata which defines how the counter should be presented to the user -(for example, as a calculated rate). This library disregards all of the display metadata. - -At the top level, there's a number of performance counter objects. -Each object has counter definitions, which contain the metadata for a particular -counter, and either zero or multiple instances. We hide the fact that there are -objects with no instances, and simply return a single null instance. - -There's one counter per counter definition and instance (or the object itself, if -there are no instances). - -Behind the scenes, every perflib DLL provides one or more objects. -Perflib has a v1 where DLLs are dynamically registered and -unregistered. Some third party applications like VMWare provide their own counters, -but this is, sadly, a rare occurrence. - -Different Windows releases have different numbers of counters. - -Objects and counters are identified by well-known indices. - -Here's an example object with one instance: - - 4320 WSMan Quota Statistics [7 counters, 1 instance(s)] - `-- "WinRMService" - `-- Total Requests/Second [4322] = 59 - `-- User Quota Violations/Second [4324] = 0 - `-- System Quota Violations/Second [4326] = 0 - `-- Active Shells [4328] = 0 - `-- Active Operations [4330] = 0 - `-- Active Users [4332] = 0 - `-- Process ID [4334] = 928 - -All "per second" metrics are counters, the rest are gauges. - -Another example, with no instance: - - 4600 Network QoS Policy [6 counters, 1 instance(s)] - `-- (default) - `-- Packets transmitted [4602] = 1744 - `-- Packets transmitted/sec [4604] = 4852 - `-- Bytes transmitted [4606] = 4853 - `-- Bytes transmitted/sec [4608] = 180388626632 - `-- Packets dropped [4610] = 0 - `-- Packets dropped/sec [4612] = 0 - -You can access the same values using PowerShell's Get-Counter cmdlet -or the Performance Monitor. - - > Get-Counter '\WSMan Quota Statistics(WinRMService)\Process ID' - - Timestamp CounterSamples - --------- -------------- - 1/28/2018 10:18:00 PM \\DEV\wsman quota statistics(winrmservice)\process id : - 928 - - > (Get-Counter '\Process(Idle)\% Processor Time').CounterSamples[0] | Format-List * - [..detailed output...] - -Data for some of the objects is also available through WMI: - - > Get-CimInstance Win32_PerfRawData_Counters_WSManQuotaStatistics - - Name : WinRMService - [...] - ActiveOperations : 0 - ActiveShells : 0 - ActiveUsers : 0 - ProcessID : 928 - SystemQuotaViolationsPerSecond : 0 - TotalRequestsPerSecond : 59 - UserQuotaViolationsPerSecond : 0 -*/ - -import ( - "encoding/binary" - "errors" - "fmt" - "strings" - "time" - "unsafe" - - "github.com/prometheus-community/windows_exporter/internal/pdh" - "golang.org/x/sys/windows" -) - -// There's a LittleEndian field in the PERF header - we ought to check it. -// -//nolint:gochecknoglobals -var bo = binary.LittleEndian - -// PerfObject Top-level performance object (like "Process"). -type PerfObject struct { - Name string - // NameIndex Same index you pass to QueryPerformanceData - NameIndex uint - Instances []PerfInstance - CounterDefs []PerfCounterDef - - Frequency int64 -} - -// PerfInstance Each object can have multiple instances. For example, -// In case the object has no instances, we return one single PerfInstance with an empty name. -type PerfInstance struct { - // *not* resolved using a name table - Name string - // Counters holds one counter per counter definition of the object, in the - // same order. The counters of all instances share one backing array. - Counters []PerfCounter -} - -type PerfCounterDef struct { - Name string - NameIndex uint - - // For debugging - subject to removal. CounterType is a perflib - // implementation detail (see perflib.h) and should not be used outside - // of this package. We export it so we can show it on /dump. - CounterType uint32 - - // PERF_TYPE_COUNTER (otherwise, it's a gauge) - IsCounter bool - // PERF_COUNTER_BASE (base value of a multi-value fraction) - IsBaseValue bool - // PERF_TIMER_100NS - IsNanosecondCounter bool - HasSecondValue bool - - rawData perfCounterDefinition -} - -type PerfCounter struct { - Value int64 - Def *PerfCounterDef - SecondValue int64 -} - -//nolint:gochecknoglobals -var ( - bufLenGlobal = uint32(400000) - bufLenCostly = uint32(2000000) -) - -// queryRawData Queries the performance counter buffer using RegQueryValueEx, returning raw bytes. See: -// https://msdn.microsoft.com/de-de/library/windows/desktop/aa373219(v=vs.85).aspx -// -// If buffer has enough capacity for the initial size guess, it is reused instead -// of allocating a new one. The returned slice may share memory with buffer. -func queryRawData(query string, buffer []byte) ([]byte, error) { - var ( - valType uint32 - bufLen uint32 - ) - - switch query { - case "Global": - bufLen = bufLenGlobal - case "Costly": - bufLen = bufLenCostly - default: - // depends on the number of values requested - // need make an educated guess - numCounters := len(strings.Split(query, " ")) - bufLen = uint32(150000 * numCounters) - } - - // A buffer from a previous call keeps any growth from ERROR_MORE_DATA, so - // using its full capacity avoids growing it again. - if uint32(cap(buffer)) >= bufLen { - buffer = buffer[:cap(buffer)] - } else { - buffer = make([]byte, bufLen) - } - - name, err := windows.UTF16PtrFromString(query) - if err != nil { - return nil, fmt.Errorf("failed to encode query string: %w", err) - } - - for { - bufLen := uint32(len(buffer)) - - err := windows.RegQueryValueEx( - windows.HKEY_PERFORMANCE_DATA, - name, - nil, - &valType, - (*byte)(unsafe.Pointer(&buffer[0])), - &bufLen) - - switch { - case errors.Is(err, error(windows.ERROR_MORE_DATA)): - // Exponential buffer growth prevents O(N) allocation spin-loops under heavy load. - // The previous copy() was removed because the buffer contents are - // incomplete/invalid and will be overwritten on the next API call. - buffer = make([]byte, len(buffer)*2) - - continue - case errors.Is(err, error(windows.ERROR_BUSY)): - time.Sleep(50 * time.Millisecond) - - continue - case err != nil: - if errNo, ok := errors.AsType[windows.Errno](err); ok { - return nil, fmt.Errorf("ReqQueryValueEx failed: %w errno %d", err, uint(errNo)) - } - - return nil, err - } - - buffer = buffer[:bufLen] - - switch query { - case "Global": - if bufLen > bufLenGlobal { - bufLenGlobal = bufLen - } - case "Costly": - if bufLen > bufLenCostly { - bufLenCostly = bufLen - } - } - - return buffer, nil - } -} - -/* -QueryPerformanceData Query all performance counters that match a given query. - -The query can be any of the following: - -- "Global" (all performance counters except those Windows marked as costly) - -- "Costly" (only the costly ones) - -- One or more object indices, separated by spaces ("238 2 5") - -Many objects have dependencies - if you query one of them, you often get back -more than you asked for. -*/ -func QueryPerformanceData(query string, counterName string) ([]*PerfObject, error) { - objects, _, err := queryPerformanceData(query, counterName, nil) - - return objects, err -} - -// queryPerformanceData is QueryPerformanceData with a reusable raw data buffer. -// It returns the buffer for the next call. The parsed objects copy everything -// they need out of the buffer, so the buffer can be reused once this returns. -func queryPerformanceData(query string, counterName string, buffer []byte) ([]*PerfObject, []byte, error) { - // Object and counter names are resolved through the name table. - if err := CounterNameTable.load(); err != nil { - return nil, buffer, err - } - - buffer, err := queryRawData(query, buffer) - if err != nil { - return nil, buffer, err - } - - objects, err := parsePerformanceData(buffer, counterName) - if err != nil { - return nil, buffer, fmt.Errorf("failed to parse performance data for %q: %w", query, err) - } - - return objects, buffer, nil -} - -//nolint:gochecknoglobals -var ( - perfDataBlockSize = int64(binary.Size(perfDataBlock{})) - perfObjectTypeSize = int64(binary.Size(perfObjectType{})) - perfCounterDefinitionSize = int64(binary.Size(perfCounterDefinition{})) - perfInstanceDefinitionSize = int64(binary.Size(perfInstanceDefinition{})) - perfCounterBlockSize = int64(binary.Size(perfCounterBlock{})) -) - -// errMalformedPerformanceData is returned when the performance data contradicts -// itself, e.g. it declares more objects than fit into the buffer. Counts, -// offsets and lengths are checked before they size an allocation or index into -// the buffer. -var errMalformedPerformanceData = errors.New("malformed performance data") - -// parsePerformanceData parses a PERF_DATA_BLOCK as returned by queryRawData. -// If counterName is set, only the first object with that name is returned. -// -// The structures are decoded straight from buffer. The parsed objects copy -// everything they keep, so they do not reference buffer. -func parsePerformanceData(buffer []byte, counterName string) ([]*PerfObject, error) { - bufLen := int64(len(buffer)) - - // Read global header - - var header perfDataBlock - - if err := header.decode(buffer); err != nil { - return nil, fmt.Errorf("failed to read performance data block: %w", err) - } - - // Check for "PERF" signature - if header.Signature != [4]uint16{80, 69, 82, 70} { - return nil, fmt.Errorf("%w: invalid performance block signature %v", errMalformedPerformanceData, header.Signature) - } - - // Parse the performance data - - numObjects := int64(header.NumObjectTypes) - objOffset := int64(header.HeaderLength) - - if objOffset < perfDataBlockSize || numObjects*perfObjectTypeSize > bufLen-objOffset { - return nil, fmt.Errorf("%w: %d objects at offset %d do not fit into %d bytes", errMalformedPerformanceData, numObjects, objOffset, bufLen) - } - - numFilteredObjects := 0 - - objects := make([]*PerfObject, numObjects) - - // nameScratch holds the UTF16 code units of an instance name while it is decoded. - var nameScratch []uint16 - - for i := range numObjects { - var obj perfObjectType - - if err := obj.decode(bytesAt(buffer, objOffset)); err != nil { - return nil, err - } - - perfCounterName := obj.LookupName() - - if counterName != "" && perfCounterName != counterName { - objOffset += int64(obj.TotalByteLength) - - continue - } - - numCounterDefs := int64(obj.NumCounters) - numInstances := int64(obj.NumInstances) - - if numCounterDefs*perfCounterDefinitionSize > bufLen-objOffset-perfObjectTypeSize { - return nil, fmt.Errorf("%w: %d counter definitions do not fit into the buffer", errMalformedPerformanceData, numCounterDefs) - } - - // Every instance has at least an instance definition and a counter block. - if numInstances*(perfInstanceDefinitionSize+perfCounterBlockSize) > bufLen-objOffset { - return nil, fmt.Errorf("%w: %d instances do not fit into the buffer", errMalformedPerformanceData, numInstances) - } - - // Perf objects can have no instances. The perflib differentiates - // between objects with instances and without, but we just create - // an empty instance in order to simplify the interface. - if numInstances <= 0 { - numInstances = 1 - } - - // The counters of all instances share one backing array below, which - // grows with instances × counters. Counter values do not overlap, so - // each takes at least one byte of the object. Checking that keeps the - // allocation linear in the buffer size for malformed data. - if numInstances*numCounterDefs > bufLen-objOffset { - return nil, fmt.Errorf("%w: %d instances with %d counters each do not fit into the buffer", errMalformedPerformanceData, numInstances, numCounterDefs) - } - - instances := make([]PerfInstance, numInstances) - counterDefs := make([]PerfCounterDef, numCounterDefs) - counters := make([]PerfCounter, numInstances*numCounterDefs) - - objects[i] = &PerfObject{ - Name: perfCounterName, - NameIndex: uint(obj.ObjectNameTitleIndex), - Instances: instances, - CounterDefs: counterDefs, - Frequency: obj.PerfFreq, - } - - // The counter definitions follow the object header. - defOffset := objOffset + perfObjectTypeSize - - for j := range numCounterDefs { - var def perfCounterDefinition - - if err := def.decode(bytesAt(buffer, defOffset+j*perfCounterDefinitionSize)); err != nil { - return nil, err - } - - counterDefs[j] = PerfCounterDef{ - Name: def.LookupName(), - NameIndex: uint(def.CounterNameTitleIndex), - rawData: def, - - CounterType: def.CounterType, - - IsCounter: def.CounterType&0x400 == 0x400, - IsBaseValue: def.CounterType&0x00030000 == 0x00030000, - IsNanosecondCounter: def.CounterType&0x00100000 == 0x00100000, - HasSecondValue: def.CounterType == pdh.PERF_AVERAGE_BULK, - } - } - - if obj.NumInstances <= 0 { //nolint:nestif - blockOffset := objOffset + int64(obj.DefinitionLength) - - if _, err := parseCounterBlock(buffer, blockOffset, counterDefs, counters); err != nil { - return nil, err - } - - instances[0].Counters = counters - } else { - instOffset := objOffset + int64(obj.DefinitionLength) - - for j := range numInstances { - var inst perfInstanceDefinition - - if err := inst.decode(bytesAt(buffer, instOffset)); err != nil { - return nil, err - } - - namePos := instOffset + int64(inst.NameOffset) - if namePos+int64(inst.NameLength) > bufLen { - return nil, fmt.Errorf("%w: instance name at offset %d with length %d exceeds the buffer", errMalformedPerformanceData, namePos, inst.NameLength) - } - - instances[j].Name, nameScratch = decodeUTF16String(buffer[namePos:namePos+int64(inst.NameLength)], nameScratch) - - blockOffset := instOffset + int64(inst.ByteLength) - instCounters := counters[j*numCounterDefs : (j+1)*numCounterDefs : (j+1)*numCounterDefs] - - blockLength, err := parseCounterBlock(buffer, blockOffset, counterDefs, instCounters) - if err != nil { - return nil, err - } - - instances[j].Counters = instCounters - instOffset = blockOffset + blockLength - } - } - - if counterName != "" { - return objects[i : i+1], nil - } - - // Next perfObjectType - objOffset += int64(obj.TotalByteLength) - numFilteredObjects++ - } - - return objects[:numFilteredObjects], nil -} - -// bytesAt returns b from offset pos on, or nil if pos lies outside of b. -func bytesAt(b []byte, pos int64) []byte { - if pos < 0 || pos > int64(len(b)) { - return nil - } - - return b[pos:] -} - -// parseCounterBlock reads the PERF_COUNTER_BLOCK at pos into counters, which -// has one element per definition in defs. It returns the length of the block. -func parseCounterBlock(b []byte, pos int64, defs []PerfCounterDef, counters []PerfCounter) (int64, error) { - var block perfCounterBlock - - if err := block.decode(bytesAt(b, pos)); err != nil { - return 0, err - } - - // The block length includes the length field itself. A shorter block would - // make the next instance overlap this one. - if int64(block.ByteLength) < perfCounterBlockSize { - return 0, fmt.Errorf("%w: counter block at offset %d has length %d", errMalformedPerformanceData, pos, block.ByteLength) - } - - for i := range defs { - def := &defs[i] - - valueOffset := pos + int64(def.rawData.CounterOffset) - size := counterValueSize(&def.rawData) - - end := valueOffset + size - if def.HasSecondValue { - end = valueOffset + 8 + size - } - - if end > int64(len(b)) { - return 0, fmt.Errorf("%w: value of counter %s at offset %d exceeds the buffer", errMalformedPerformanceData, def.Name, valueOffset) - } - - value := convertCounterValue(&def.rawData, b, valueOffset) - secondValue := int64(0) - - if def.HasSecondValue { - secondValue = convertCounterValue(&def.rawData, b, valueOffset+8) - } - - counters[i] = PerfCounter{ - Value: value, - Def: def, - SecondValue: secondValue, - } - } - - return int64(block.ByteLength), nil -} - -// counterValueSize returns the number of bytes convertCounterValue reads. -func counterValueSize(counterDef *perfCounterDefinition) int64 { - if counterDef.CounterSize == 8 { - return 8 - } - - return 4 -} - -func convertCounterValue(counterDef *perfCounterDefinition, buffer []byte, valueOffset int64) int64 { - /* - We can safely ignore the type since we're not interested in anything except the raw value. - We also ignore all of the other attributes (timestamp, presentation, multi counter values...) - - See also: winperf.h. - - Here's the most common value for CounterType: - - 65536 32bit counter - 65792 64bit counter - 272696320 32bit rate - 272696576 64bit rate - - */ - switch counterDef.CounterSize { - case 4: - return int64(bo.Uint32(buffer[valueOffset:(valueOffset + 4)])) - case 8: - return int64(bo.Uint64(buffer[valueOffset:(valueOffset + 8)])) - default: - return int64(bo.Uint32(buffer[valueOffset:(valueOffset + 4)])) - } -} diff --git a/internal/pdh/registry/perflib_parse_test.go b/internal/pdh/registry/perflib_parse_test.go deleted file mode 100644 index 2a7a57e3..00000000 --- a/internal/pdh/registry/perflib_parse_test.go +++ /dev/null @@ -1,425 +0,0 @@ -// 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 registry - -import ( - "bytes" - "encoding/binary" - "io" - "testing" - "unsafe" - - "github.com/prometheus-community/windows_exporter/internal/pdh" - "github.com/stretchr/testify/require" - "golang.org/x/sys/windows" -) - -// syntheticPerfData describes a PERF_DATA_BLOCK with one object and one -// 64-bit counter, either without instances or with a single instance. -type syntheticPerfData struct { - header perfDataBlock - object perfObjectType - def perfCounterDefinition - instance perfInstanceDefinition - instanceName []uint16 - block perfCounterBlock - value uint64 - - withInstance bool -} - -func newSyntheticPerfData(withInstance bool) *syntheticPerfData { - d := &syntheticPerfData{ - withInstance: withInstance, - instanceName: windows.StringToUTF16("inst"), - value: 42, - } - - d.header.Signature = [4]uint16{'P', 'E', 'R', 'F'} - d.header.LittleEndian = 1 - d.header.HeaderLength = uint32(perfDataBlockSize) - d.header.NumObjectTypes = 1 - - d.object.HeaderLength = uint32(perfObjectTypeSize) - d.object.DefinitionLength = uint32(perfObjectTypeSize + perfCounterDefinitionSize) - d.object.NumCounters = 1 - d.object.NumInstances = -1 // PERF_NO_INSTANCES - d.object.PerfFreq = 10_000_000 - - d.def.ByteLength = uint32(perfCounterDefinitionSize) - d.def.CounterType = pdh.PERF_COUNTER_LARGE_RAWCOUNT - d.def.CounterSize = 8 - // The value follows the 4-byte block length and 4 bytes of padding. - d.def.CounterOffset = 8 - - d.block.ByteLength = 16 - - if withInstance { - nameLength := uint32(len(d.instanceName) * 2) - - d.object.NumInstances = 1 - d.instance.NameOffset = uint32(perfInstanceDefinitionSize) - d.instance.NameLength = nameLength - // The name is padded to 8 bytes. - d.instance.ByteLength = uint32(perfInstanceDefinitionSize) + (nameLength+7)&^7 - } - - return d -} - -func (d *syntheticPerfData) bytes(tb testing.TB) []byte { - tb.Helper() - - var body bytes.Buffer - - write := func(v any) { - require.NoError(tb, binary.Write(&body, bo, v)) - } - - write(d.def) - - if d.withInstance { - write(d.instance) - write(d.instanceName) - body.Write(make([]byte, int(d.instance.ByteLength)-int(perfInstanceDefinitionSize)-len(d.instanceName)*2)) - } - - write(d.block) - body.Write(make([]byte, 4)) - write(d.value) - - object := d.object - if object.TotalByteLength == 0 { - object.TotalByteLength = uint32(perfObjectTypeSize) + uint32(body.Len()) - } - - header := d.header - if header.TotalByteLength == 0 { - header.TotalByteLength = uint32(perfDataBlockSize) + object.TotalByteLength - } - - var buf bytes.Buffer - - require.NoError(tb, binary.Write(&buf, bo, header)) - require.NoError(tb, binary.Write(&buf, bo, object)) - buf.Write(body.Bytes()) - - return buf.Bytes() -} - -func TestParsePerformanceData(t *testing.T) { - t.Parallel() - - for _, withInstance := range []bool{false, true} { - objects, err := parsePerformanceData(newSyntheticPerfData(withInstance).bytes(t), "") - require.NoError(t, err) - require.Len(t, objects, 1) - require.Len(t, objects[0].Instances, 1) - require.Len(t, objects[0].Instances[0].Counters, 1) - require.Equal(t, int64(42), objects[0].Instances[0].Counters[0].Value) - - if withInstance { - require.Equal(t, "inst", objects[0].Instances[0].Name) - } - } -} - -// TestParsePerformanceDataMalformed checks that inconsistent performance data -// is reported as an error. The parser runs during Build of the process -// collector, where a panic would end the exporter. -func TestParsePerformanceDataMalformed(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - name string - withInstance bool - mutate func(d *syntheticPerfData) - }{ - {"invalid signature", false, func(d *syntheticPerfData) { d.header.Signature[0] = 'X' }}, - {"header length too short", false, func(d *syntheticPerfData) { d.header.HeaderLength = 0 }}, - {"too many objects", false, func(d *syntheticPerfData) { d.header.NumObjectTypes = 1 << 31 }}, - {"too many counters", false, func(d *syntheticPerfData) { d.object.NumCounters = 1 << 30 }}, - {"too many instances", true, func(d *syntheticPerfData) { d.object.NumInstances = 1 << 30 }}, - {"counter offset out of range", false, func(d *syntheticPerfData) { d.def.CounterOffset = 1 << 31 }}, - {"counter offset out of range with instance", true, func(d *syntheticPerfData) { d.def.CounterOffset = 1 << 20 }}, - {"instance name out of range", true, func(d *syntheticPerfData) { d.instance.NameLength = 1 << 31 }}, - {"empty counter block", true, func(d *syntheticPerfData) { d.block.ByteLength = 0 }}, - } { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - d := newSyntheticPerfData(tc.withInstance) - tc.mutate(d) - - buffer := d.bytes(t) - - var err error - - require.NotPanics(t, func() { _, err = parsePerformanceData(buffer, "") }) - require.ErrorIs(t, err, errMalformedPerformanceData) - }) - } -} - -func TestParsePerformanceDataTruncated(t *testing.T) { - t.Parallel() - - for _, withInstance := range []bool{false, true} { - buffer := newSyntheticPerfData(withInstance).bytes(t) - - for n := range buffer { - var err error - - require.NotPanics(t, func() { _, err = parsePerformanceData(buffer[:n], "") }, "length %d", n) - require.Error(t, err, "length %d", n) - } - } -} - -// sharedValuePerfData returns a consistent PERF_DATA_BLOCK with one object -// whose counters all read the same 4 bytes of each minimal counter block. -func sharedValuePerfData(tb testing.TB, numCounters, numInstances uint32) []byte { - tb.Helper() - - var body bytes.Buffer - - write := func(v any) { - require.NoError(tb, binary.Write(&body, bo, v)) - } - - for range numCounters { - write(perfCounterDefinition{ - ByteLength: uint32(perfCounterDefinitionSize), - CounterType: pdh.PERF_COUNTER_RAWCOUNT, - CounterSize: 4, - }) - } - - for range numInstances { - write(perfInstanceDefinition{ByteLength: uint32(perfInstanceDefinitionSize)}) - write(perfCounterBlock{ByteLength: uint32(perfCounterBlockSize)}) - } - - object := perfObjectType{ - TotalByteLength: uint32(perfObjectTypeSize) + uint32(body.Len()), - DefinitionLength: uint32(perfObjectTypeSize) + numCounters*uint32(perfCounterDefinitionSize), - HeaderLength: uint32(perfObjectTypeSize), - NumCounters: numCounters, - NumInstances: int32(numInstances), - } - - header := perfDataBlock{ - Signature: [4]uint16{'P', 'E', 'R', 'F'}, - LittleEndian: 1, - TotalByteLength: uint32(perfDataBlockSize) + object.TotalByteLength, - HeaderLength: uint32(perfDataBlockSize), - NumObjectTypes: 1, - } - - var buf bytes.Buffer - - require.NoError(tb, binary.Write(&buf, bo, header)) - require.NoError(tb, binary.Write(&buf, bo, object)) - buf.Write(body.Bytes()) - - return buf.Bytes() -} - -// TestParsePerformanceDataCounterProduct checks that the counters of all -// instances, which share one allocation, are bounded by the buffer size. Data -// whose counter values overlap could otherwise request an allocation that -// grows with the square of the buffer size. -func TestParsePerformanceDataCounterProduct(t *testing.T) { - t.Parallel() - - objects, err := parsePerformanceData(sharedValuePerfData(t, 200, 10), "") - require.NoError(t, err) - require.Len(t, objects[0].Instances, 10) - require.Len(t, objects[0].Instances[9].Counters, 200) - require.Equal(t, perfCounterBlockSize, objects[0].Instances[9].Counters[199].Value) - - _, err = parsePerformanceData(sharedValuePerfData(t, 200, 200), "") - require.ErrorIs(t, err, errMalformedPerformanceData) - require.ErrorContains(t, err, "200 instances with 200 counters each") -} - -// TestParsePerformanceDataLive parses the data of a real object. -func TestParsePerformanceDataLive(t *testing.T) { - t.Parallel() - - buffer := livePerformanceData(t, "Process") - - objects, err := parsePerformanceData(buffer, "Process") - require.NoError(t, err) - require.Len(t, objects, 1) - require.NotEmpty(t, objects[0].Instances) - require.NotEmpty(t, objects[0].CounterDefs) -} - -// TestParsePerformanceDataNoAlias checks that the parsed objects do not -// reference the buffer, which Collector.Collect reuses for the next query. -func TestParsePerformanceDataNoAlias(t *testing.T) { - t.Parallel() - - for _, buffer := range [][]byte{ - newSyntheticPerfData(true).bytes(t), - livePerformanceData(t, "Process"), - } { - objects, err := parsePerformanceData(buffer, "") - require.NoError(t, err) - - expected, err := parsePerformanceData(bytes.Clone(buffer), "") - require.NoError(t, err) - - for i := range buffer { - buffer[i] = 0xFF - } - - require.Equal(t, expected, objects) - } -} - -func TestQueryRawDataReusesBuffer(t *testing.T) { - t.Parallel() - - require.NoError(t, CounterNameTable.load()) - - query := MapCounterToIndex("Process") - - buffer, err := queryRawData(query, nil) - require.NoError(t, err) - - reused, err := queryRawData(query, buffer) - require.NoError(t, err) - require.Same(t, unsafe.SliceData(buffer), unsafe.SliceData(reused)) - - // A buffer below the initial size guess is not used. - small := make([]byte, 16) - - buffer, err = queryRawData(query, small) - require.NoError(t, err) - require.NotSame(t, unsafe.SliceData(small), unsafe.SliceData(buffer)) -} - -func livePerformanceData(tb testing.TB, object string) []byte { - tb.Helper() - - require.NoError(tb, CounterNameTable.load()) - - buffer, err := queryRawData(MapCounterToIndex(object), nil) - require.NoError(tb, err) - - return buffer -} - -func FuzzParsePerformanceData(f *testing.F) { - f.Add(newSyntheticPerfData(false).bytes(f)) - f.Add(newSyntheticPerfData(true).bytes(f)) - f.Add(livePerformanceData(f, "System")) - f.Add([]byte{}) - - f.Fuzz(func(_ *testing.T, data []byte) { - // Must not panic on any input. - _, _ = parsePerformanceData(data, "") - }) -} - -func TestParseNameTable(t *testing.T) { - t.Parallel() - - multiSZ := func(s ...string) []byte { - var buf bytes.Buffer - - for _, v := range s { - require.NoError(t, binary.Write(&buf, bo, windows.StringToUTF16(v))) - } - - return buf.Bytes() - } - - index, names := parseNameTable(multiSZ("1", "1847", "2", "System", "4", "Memory")) - require.Equal(t, map[uint32]string{1: "1847", 2: "System", 4: "Memory"}, index) - require.Equal(t, uint32(4), names["Memory"]) - - // An incomplete pair and an odd trailing byte are ignored. - buffer := append(multiSZ("2", "System", "4"), 'M') - index, _ = parseNameTable(buffer) - require.Equal(t, map[uint32]string{2: "System"}, index) - - index, _ = parseNameTable(nil) - require.Empty(t, index) -} - -func TestNewCollectorUnknownObject(t *testing.T) { - t.Parallel() - - type counterValues struct { - Name string - - ProcessorQueueLength float64 `perfdata:"Processor Queue Length"` - } - - _, err := NewCollector[counterValues]("Object That Does Not Exist", nil) - require.ErrorContains(t, err, "not found in the counter name table") -} - -// BenchmarkParsePerformanceData measures the parser alone, without the -// RegQueryValueEx call that dominates BenchmarkCollectorCollect. -func BenchmarkParsePerformanceData(b *testing.B) { - buffer := livePerformanceData(b, "Process") - - b.ReportAllocs() - - for b.Loop() { - if _, err := parsePerformanceData(buffer, "Process"); err != nil { - b.Fatal(err) - } - } -} - -// TestDecodeMatchesBinaryRead checks the hand-written decoders of the raw -// structures against binary.Read. -func TestDecodeMatchesBinaryRead(t *testing.T) { - t.Parallel() - - type decoder interface { - decode(b []byte) error - } - - for _, tc := range []struct { - decoded, expected decoder - }{ - {new(perfDataBlock), new(perfDataBlock)}, - {new(perfObjectType), new(perfObjectType)}, - {new(perfCounterDefinition), new(perfCounterDefinition)}, - {new(perfCounterBlock), new(perfCounterBlock)}, - {new(perfInstanceDefinition), new(perfInstanceDefinition)}, - } { - // Distinct bytes, so a field read from the wrong offset gets a different value. - buffer := make([]byte, binary.Size(tc.expected)) - for i := range buffer { - buffer[i] = byte(i + 1) - } - - require.NoError(t, binary.Read(bytes.NewReader(buffer), bo, tc.expected)) - require.NoError(t, tc.decoded.decode(buffer)) - require.Equal(t, tc.expected, tc.decoded) - - require.ErrorIs(t, tc.decoded.decode(buffer[:len(buffer)-1]), io.ErrUnexpectedEOF) - } -} diff --git a/internal/pdh/registry/perflib_test.go b/internal/pdh/registry/perflib_test.go deleted file mode 100644 index fb939e3c..00000000 --- a/internal/pdh/registry/perflib_test.go +++ /dev/null @@ -1,136 +0,0 @@ -// 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 registry - -import ( - "sync" - "testing" -) - -// TestNewCollectorStructTypeParam guards against a regression where -// reflect.TypeFor[T]().Elem() panicked when T is a plain struct (not a pointer). -// See https://github.com/prometheus-community/windows_exporter/issues/2365 -func TestNewCollectorStructTypeParam(t *testing.T) { - type systemCounterValues struct { - Name string - - ProcessorQueueLength float64 `perfdata:"Processor Queue Length"` - } - - _, err := NewCollector[systemCounterValues]("System", nil) - if err != nil { - t.Skipf("skipping: failed to create collector: %v", err) - } -} - -// TestNewCollectorNonStruct guards against a regression where NewCollector -// panicked instead of returning an error for a type parameter that is not a struct. -func TestNewCollectorNonStruct(t *testing.T) { - t.Parallel() - - if _, err := NewCollector[int]("System", nil); err == nil { - t.Error("expected an error, got nil") - } -} - -func TestCollectNilDestination(t *testing.T) { - t.Parallel() - - type counterValues struct { - Name string - } - - if err := (&Collector[counterValues]{}).Collect(nil); err == nil { - t.Error("expected an error, got nil") - } -} - -// TestCollectConcurrent runs Collect concurrently on one collector, as -// overlapping scrapes do, to let the race detector check the buffer reuse. -func TestCollectConcurrent(t *testing.T) { - t.Parallel() - - type systemCounterValues struct { - Name string - - ProcessorQueueLength float64 `perfdata:"Processor Queue Length"` - } - - collector, err := NewCollector[systemCounterValues]("System", nil) - if err != nil { - t.Skipf("skipping: failed to create collector: %v", err) - } - - var wg sync.WaitGroup - - for range 8 { - wg.Go(func() { - for range 10 { - var dst []systemCounterValues - - if err := collector.Collect(&dst); err != nil { - t.Error(err) - - return - } - - if len(dst) != 1 { - t.Errorf("expected 1 row, got %d", len(dst)) - - return - } - } - }) - } - - wg.Wait() -} - -func BenchmarkQueryPerformanceData(b *testing.B) { - for b.Loop() { - _, _ = QueryPerformanceData("Global", "") - } -} - -func BenchmarkCollectorCollect(b *testing.B) { - type processCounterValues struct { - Name string - - ProcessID float64 `perfdata:"ID Process"` - ProcessorTime float64 `perfdata:"% Processor Time"` - WorkingSet float64 `perfdata:"Working Set"` - HandleCount float64 `perfdata:"Handle Count"` - ThreadCount float64 `perfdata:"Thread Count"` - IOReadBytesSec float64 `perfdata:"IO Read Bytes/sec"` - } - - collector, err := NewCollector[processCounterValues]("Process", nil) - if err != nil { - b.Skipf("skipping: failed to create collector: %v", err) - } - - var dst []processCounterValues - - b.ReportAllocs() - - for b.Loop() { - if err := collector.Collect(&dst); err != nil { - b.Fatal(err) - } - } -} diff --git a/internal/pdh/registry/raw_types.go b/internal/pdh/registry/raw_types.go deleted file mode 100644 index aed100ad..00000000 --- a/internal/pdh/registry/raw_types.go +++ /dev/null @@ -1,282 +0,0 @@ -// 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 registry - -import ( - "io" - - "golang.org/x/sys/windows" -) - -/* -perfDataBlock -See: https://msdn.microsoft.com/de-de/library/windows/desktop/aa373157(v=vs.85).aspx - - typedef struct _PERF_DATA_BLOCK { - WCHAR Signature[4]; - DWORD LittleEndian; - DWORD Version; - DWORD Revision; - DWORD TotalByteLength; - DWORD HeaderLength; - DWORD NumObjectTypes; - DWORD DefaultObject; - SYSTEMTIME SystemTime; - LARGE_INTEGER PerfTime; - LARGE_INTEGER PerfFreq; - LARGE_INTEGER PerfTime100nSec; - DWORD SystemNameLength; - DWORD SystemNameOffset; - } PERF_DATA_BLOCK; -*/ -type perfDataBlock struct { - Signature [4]uint16 - LittleEndian uint32 - Version uint32 - Revision uint32 - TotalByteLength uint32 - HeaderLength uint32 - NumObjectTypes uint32 - DefaultObject int32 - SystemTime windows.Systemtime - _ uint32 // unknown field - PerfTime int64 - PerfFreq int64 - PerfTime100nSec int64 - SystemNameLength uint32 - SystemNameOffset uint32 -} - -// decode reads p from the start of b. The fields are decoded directly, since -// binary.Read allocates and walks the struct by reflection on every call. -func (p *perfDataBlock) decode(b []byte) error { - if int64(len(b)) < perfDataBlockSize { - return io.ErrUnexpectedEOF - } - - *p = perfDataBlock{ - Signature: [4]uint16{bo.Uint16(b[0:]), bo.Uint16(b[2:]), bo.Uint16(b[4:]), bo.Uint16(b[6:])}, - LittleEndian: bo.Uint32(b[8:]), - Version: bo.Uint32(b[12:]), - Revision: bo.Uint32(b[16:]), - TotalByteLength: bo.Uint32(b[20:]), - HeaderLength: bo.Uint32(b[24:]), - NumObjectTypes: bo.Uint32(b[28:]), - DefaultObject: int32(bo.Uint32(b[32:])), - SystemTime: windows.Systemtime{ - Year: bo.Uint16(b[36:]), - Month: bo.Uint16(b[38:]), - DayOfWeek: bo.Uint16(b[40:]), - Day: bo.Uint16(b[42:]), - Hour: bo.Uint16(b[44:]), - Minute: bo.Uint16(b[46:]), - Second: bo.Uint16(b[48:]), - Milliseconds: bo.Uint16(b[50:]), - }, - PerfTime: int64(bo.Uint64(b[56:])), - PerfFreq: int64(bo.Uint64(b[64:])), - PerfTime100nSec: int64(bo.Uint64(b[72:])), - SystemNameLength: bo.Uint32(b[80:]), - SystemNameOffset: bo.Uint32(b[84:]), - } - - return nil -} - -/* -perfObjectType -See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373160(v=vs.85).aspx - - typedef struct _PERF_OBJECT_TYPE { - DWORD TotalByteLength; - DWORD DefinitionLength; - DWORD HeaderLength; - DWORD ObjectNameTitleIndex; - LPWSTR ObjectNameTitle; - DWORD ObjectHelpTitleIndex; - LPWSTR ObjectHelpTitle; - DWORD DetailLevel; - DWORD NumCounters; - DWORD DefaultCounter; - DWORD NumInstances; - DWORD CodePage; - LARGE_INTEGER PerfTime; - LARGE_INTEGER PerfFreq; - } PERF_OBJECT_TYPE; -*/ -type perfObjectType struct { - TotalByteLength uint32 - DefinitionLength uint32 - HeaderLength uint32 - ObjectNameTitleIndex uint32 - ObjectNameTitle uint32 - ObjectHelpTitleIndex uint32 - ObjectHelpTitle uint32 - DetailLevel uint32 - NumCounters uint32 - DefaultCounter int32 - NumInstances int32 - CodePage uint32 - PerfTime int64 - PerfFreq int64 -} - -// decode reads p from the start of b. -func (p *perfObjectType) decode(b []byte) error { - if int64(len(b)) < perfObjectTypeSize { - return io.ErrUnexpectedEOF - } - - *p = perfObjectType{ - TotalByteLength: bo.Uint32(b[0:]), - DefinitionLength: bo.Uint32(b[4:]), - HeaderLength: bo.Uint32(b[8:]), - ObjectNameTitleIndex: bo.Uint32(b[12:]), - ObjectNameTitle: bo.Uint32(b[16:]), - ObjectHelpTitleIndex: bo.Uint32(b[20:]), - ObjectHelpTitle: bo.Uint32(b[24:]), - DetailLevel: bo.Uint32(b[28:]), - NumCounters: bo.Uint32(b[32:]), - DefaultCounter: int32(bo.Uint32(b[36:])), - NumInstances: int32(bo.Uint32(b[40:])), - CodePage: bo.Uint32(b[44:]), - PerfTime: int64(bo.Uint64(b[48:])), - PerfFreq: int64(bo.Uint64(b[56:])), - } - - return nil -} - -/* -perfCounterDefinition -See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373150(v=vs.85).aspx - - typedef struct _PERF_COUNTER_DEFINITION { - DWORD ByteLength; - DWORD CounterNameTitleIndex; - LPWSTR CounterNameTitle; - DWORD CounterHelpTitleIndex; - LPWSTR CounterHelpTitle; - LONG DefaultScale; - DWORD DetailLevel; - DWORD CounterType; - DWORD CounterSize; - DWORD CounterOffset; - } PERF_COUNTER_DEFINITION; -*/ -type perfCounterDefinition struct { - ByteLength uint32 - CounterNameTitleIndex uint32 - CounterNameTitle uint32 - CounterHelpTitleIndex uint32 - CounterHelpTitle uint32 - DefaultScale int32 - DetailLevel uint32 - CounterType uint32 - CounterSize uint32 - CounterOffset uint32 -} - -// decode reads p from the start of b. -func (p *perfCounterDefinition) decode(b []byte) error { - if int64(len(b)) < perfCounterDefinitionSize { - return io.ErrUnexpectedEOF - } - - *p = perfCounterDefinition{ - ByteLength: bo.Uint32(b[0:]), - CounterNameTitleIndex: bo.Uint32(b[4:]), - CounterNameTitle: bo.Uint32(b[8:]), - CounterHelpTitleIndex: bo.Uint32(b[12:]), - CounterHelpTitle: bo.Uint32(b[16:]), - DefaultScale: int32(bo.Uint32(b[20:])), - DetailLevel: bo.Uint32(b[24:]), - CounterType: bo.Uint32(b[28:]), - CounterSize: bo.Uint32(b[32:]), - CounterOffset: bo.Uint32(b[36:]), - } - - return nil -} - -func (p *perfCounterDefinition) LookupName() string { - return CounterNameTable.LookupString(p.CounterNameTitleIndex) -} - -/* -perfCounterBlock -See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373147(v=vs.85).aspx - - typedef struct _PERF_COUNTER_BLOCK { - DWORD ByteLength; - } PERF_COUNTER_BLOCK; -*/ -type perfCounterBlock struct { - ByteLength uint32 -} - -// decode reads p from the start of b. -func (p *perfCounterBlock) decode(b []byte) error { - if int64(len(b)) < perfCounterBlockSize { - return io.ErrUnexpectedEOF - } - - p.ByteLength = bo.Uint32(b) - - return nil -} - -/* -perfInstanceDefinition -See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373159(v=vs.85).aspx - - typedef struct _PERF_INSTANCE_DEFINITION { - DWORD ByteLength; - DWORD ParentObjectTitleIndex; - DWORD ParentObjectInstance; - DWORD UniqueID; - DWORD NameOffset; - DWORD NameLength; - } PERF_INSTANCE_DEFINITION; -*/ -type perfInstanceDefinition struct { - ByteLength uint32 - ParentObjectTitleIndex uint32 - ParentObjectInstance uint32 - UniqueID uint32 - NameOffset uint32 - NameLength uint32 -} - -// decode reads p from the start of b. -func (p *perfInstanceDefinition) decode(b []byte) error { - if int64(len(b)) < perfInstanceDefinitionSize { - return io.ErrUnexpectedEOF - } - - *p = perfInstanceDefinition{ - ByteLength: bo.Uint32(b[0:]), - ParentObjectTitleIndex: bo.Uint32(b[4:]), - ParentObjectInstance: bo.Uint32(b[8:]), - UniqueID: bo.Uint32(b[12:]), - NameOffset: bo.Uint32(b[16:]), - NameLength: bo.Uint32(b[20:]), - } - - return nil -} diff --git a/internal/pdh/registry/utf16.go b/internal/pdh/registry/utf16.go deleted file mode 100644 index 54b696da..00000000 --- a/internal/pdh/registry/utf16.go +++ /dev/null @@ -1,57 +0,0 @@ -// 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 registry - -import ( - "io" - - "golang.org/x/sys/windows" -) - -// decodeUTF16String decodes the unterminated UTF16 string b up to the first NUL. -// A trailing odd byte is ignored. scratch holds the code units; it is returned -// with any growth so the caller can reuse it for the next string. The returned -// string does not reference b. -func decodeUTF16String(b []byte, scratch []uint16) (string, []uint16) { - scratch = scratch[:0] - - for i := 0; i+1 < len(b); i += 2 { - scratch = append(scratch, bo.Uint16(b[i:])) - } - - return windows.UTF16ToString(scratch), scratch -} - -// readUTF16String Reads a null-terminated UTF16 string at the current offset. -func readUTF16String(r io.Reader) (string, error) { - b := make([]byte, 2) - out := make([]uint16, 0, 100) - - for { - // A short read at the end of the buffer is an error, not a code unit. - if _, err := io.ReadFull(r, b); err != nil { - return "", err - } - - if b[0] == 0 && b[1] == 0 { - return windows.UTF16ToString(out), nil - } - - out = append(out, bo.Uint16(b)) - } -} diff --git a/internal/pdh/registry/utils.go b/internal/pdh/registry/utils.go deleted file mode 100644 index 5add71ce..00000000 --- a/internal/pdh/registry/utils.go +++ /dev/null @@ -1,26 +0,0 @@ -// 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 registry - -import ( - "strconv" -) - -func MapCounterToIndex(name string) string { - return strconv.Itoa(int(CounterNameTable.LookupIndex(name))) -}