mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
324 lines
9.3 KiB
Go
324 lines
9.3 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// Copyright The Prometheus Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//go:build windows
|
|
|
|
package service
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/sys/windows"
|
|
"golang.org/x/sys/windows/registry"
|
|
)
|
|
|
|
const (
|
|
// servicesKeyPath is the registry key the service control manager stores the
|
|
// service configurations in. ChangeServiceConfig writes the values of its subkeys.
|
|
servicesKeyPath = `SYSTEM\CurrentControlSet\Services`
|
|
|
|
// REG_NOTIFY_THREAD_AGNOSTIC keeps the notification alive if the OS thread
|
|
// that armed it exits. Go may run the next arm on any thread.
|
|
servicesKeyNotifyFilter = windows.REG_NOTIFY_CHANGE_NAME |
|
|
windows.REG_NOTIFY_CHANGE_LAST_SET |
|
|
windows.REG_NOTIFY_THREAD_AGNOSTIC
|
|
|
|
// configCacheMaxAge bounds how long a cached configuration is used, in case
|
|
// a change notification is missed.
|
|
configCacheMaxAge = 10 * time.Minute
|
|
|
|
// configCacheMinKeyAge is how long a service key must be unchanged before its
|
|
// configuration is cached. Registry write times have a resolution of one
|
|
// clock tick, so a second write in the same tick would not change it.
|
|
configCacheMinKeyAge = time.Second
|
|
)
|
|
|
|
var errConfigCacheClosed = errors.New("service configuration cache is closed")
|
|
|
|
// cachedServiceConfig is the result of queryServiceConfig.
|
|
type cachedServiceConfig struct {
|
|
config serviceConfig
|
|
available bool
|
|
|
|
// lastWrite is the last write time of the service's registry key, read
|
|
// before the configuration was queried.
|
|
lastWrite time.Time
|
|
}
|
|
|
|
// serviceConfigCache caches the service configurations between scrapes.
|
|
//
|
|
// NotifyServiceStatusChange doesn't report configuration changes, so the cache
|
|
// watches the services registry key instead. The notification fires for any
|
|
// write below it, and drivers and services write to deeper subkeys (Parameters,
|
|
// Enum, State, ...) every few seconds. So a notification only triggers a check
|
|
// of the last write time of each cached service key, which changes only when
|
|
// the values of the key itself change or a direct subkey is created or deleted.
|
|
// Only services whose key changed are queried again. The state and process of a
|
|
// service are never cached.
|
|
type serviceConfigCache struct {
|
|
mu sync.Mutex
|
|
|
|
// entries is nil while the cache is disabled.
|
|
entries map[string]cachedServiceConfig
|
|
|
|
// generation is incremented on every check, so that a configuration queried
|
|
// before a check is not stored after it.
|
|
generation uint64
|
|
expiresAt time.Time
|
|
|
|
key registry.Key
|
|
changed windows.Handle
|
|
}
|
|
|
|
// init opens the services registry key below root and starts watching it for changes.
|
|
// The cache stays disabled if init fails.
|
|
func (cache *serviceConfigCache) init(root registry.Key, path string) error {
|
|
key, err := registry.OpenKey(root, path, registry.NOTIFY|registry.QUERY_VALUE|registry.ENUMERATE_SUB_KEYS)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open registry key %s: %w", path, err)
|
|
}
|
|
|
|
// Auto-reset event, so that waiting on it also resets it.
|
|
changed, err := windows.CreateEvent(nil, 0, 0, nil)
|
|
if err != nil {
|
|
_ = key.Close()
|
|
|
|
return fmt.Errorf("failed to create event: %w", err)
|
|
}
|
|
|
|
cache.mu.Lock()
|
|
defer cache.mu.Unlock()
|
|
|
|
cache.key = key
|
|
cache.changed = changed
|
|
|
|
if err := cache.armLocked(); err != nil {
|
|
return err
|
|
}
|
|
|
|
cache.entries = make(map[string]cachedServiceConfig)
|
|
cache.expiresAt = time.Now().Add(configCacheMaxAge)
|
|
|
|
return nil
|
|
}
|
|
|
|
// armLocked requests a single notification for the next change below the services key.
|
|
func (cache *serviceConfigCache) armLocked() error {
|
|
err := windows.RegNotifyChangeKeyValue(windows.Handle(cache.key), true, servicesKeyNotifyFilter, cache.changed, true)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to watch registry key: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// lastWrite returns the last write time of the registry key of a service.
|
|
func (cache *serviceConfigCache) lastWrite(name string) (time.Time, error) {
|
|
cache.mu.Lock()
|
|
defer cache.mu.Unlock()
|
|
|
|
return cache.lastWriteLocked(name)
|
|
}
|
|
|
|
// lastWriteLocked is lastWrite for callers that hold the lock, which keeps
|
|
// close from closing the services key while it is in use.
|
|
func (cache *serviceConfigCache) lastWriteLocked(name string) (time.Time, error) {
|
|
if cache.key == 0 {
|
|
return time.Time{}, errConfigCacheClosed
|
|
}
|
|
|
|
key, err := registry.OpenKey(cache.key, name, registry.QUERY_VALUE)
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
|
|
defer key.Close()
|
|
|
|
info, err := key.Stat()
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
|
|
return info.ModTime(), nil
|
|
}
|
|
|
|
// refresh drops the cached configurations of services whose registry key changed
|
|
// since they were queried, or all of them if the cache expired. It returns the
|
|
// number of dropped configurations and whether the cache expired. If the watch
|
|
// can't be armed again, the cache is disabled.
|
|
func (cache *serviceConfigCache) refresh() (int, bool, error) {
|
|
cache.mu.Lock()
|
|
defer cache.mu.Unlock()
|
|
|
|
if cache.entries == nil {
|
|
return 0, false, nil
|
|
}
|
|
|
|
if !time.Now().Before(cache.expiresAt) {
|
|
dropped := len(cache.entries)
|
|
|
|
clear(cache.entries)
|
|
cache.generation++
|
|
cache.expiresAt = time.Now().Add(configCacheMaxAge)
|
|
|
|
return dropped, true, nil
|
|
}
|
|
|
|
event, err := windows.WaitForSingleObject(cache.changed, 0)
|
|
if err != nil {
|
|
cache.entries = nil
|
|
|
|
return 0, false, fmt.Errorf("failed to check registry key for changes: %w", err)
|
|
}
|
|
|
|
if event != windows.WAIT_OBJECT_0 {
|
|
return 0, false, nil
|
|
}
|
|
|
|
// Arm before checking the keys, so that a change from now on is not missed.
|
|
if err := cache.armLocked(); err != nil {
|
|
cache.entries = nil
|
|
|
|
return 0, false, err
|
|
}
|
|
|
|
cache.generation++
|
|
|
|
var dropped int
|
|
|
|
for name, entry := range cache.entries {
|
|
// A failure, for example because the service was deleted, also drops the entry.
|
|
lastWrite, err := cache.lastWriteLocked(name)
|
|
if err == nil && lastWrite.Equal(entry.lastWrite) {
|
|
continue
|
|
}
|
|
|
|
delete(cache.entries, name)
|
|
|
|
dropped++
|
|
}
|
|
|
|
return dropped, false, nil
|
|
}
|
|
|
|
// get returns the cached configuration of a service and the current generation.
|
|
// ok is false on a miss.
|
|
func (cache *serviceConfigCache) get(name string) (cachedServiceConfig, uint64, bool) {
|
|
cache.mu.Lock()
|
|
defer cache.mu.Unlock()
|
|
|
|
entry, ok := cache.entries[name]
|
|
|
|
return entry, cache.generation, ok
|
|
}
|
|
|
|
// put stores a configuration that was queried in the given generation.
|
|
// It is dropped if the cache was checked or disabled in the meantime.
|
|
func (cache *serviceConfigCache) put(name string, generation uint64, entry cachedServiceConfig) {
|
|
cache.mu.Lock()
|
|
defer cache.mu.Unlock()
|
|
|
|
if cache.entries == nil || generation != cache.generation {
|
|
return
|
|
}
|
|
|
|
cache.entries[name] = entry
|
|
}
|
|
|
|
// enabled reports whether the cache is in use.
|
|
func (cache *serviceConfigCache) enabled() bool {
|
|
cache.mu.Lock()
|
|
defer cache.mu.Unlock()
|
|
|
|
return cache.entries != nil
|
|
}
|
|
|
|
func (cache *serviceConfigCache) close() error {
|
|
cache.mu.Lock()
|
|
defer cache.mu.Unlock()
|
|
|
|
cache.entries = nil
|
|
|
|
var errs []error
|
|
|
|
// Closing the key also signals the pending notification.
|
|
if cache.key != 0 {
|
|
if err := cache.key.Close(); err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to close registry key: %w", err))
|
|
}
|
|
|
|
cache.key = 0
|
|
}
|
|
|
|
if cache.changed != 0 {
|
|
if err := windows.CloseHandle(cache.changed); err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to close event: %w", err))
|
|
}
|
|
|
|
cache.changed = 0
|
|
}
|
|
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
// getCachedServiceConfig returns the configuration of a service from the cache,
|
|
// or queries and caches it on a miss.
|
|
func (c *Collector) getCachedServiceConfig(serviceName string) (serviceConfig, bool, error) {
|
|
entry, generation, ok := c.configCache.get(serviceName)
|
|
if ok {
|
|
return entry.config, entry.available, nil
|
|
}
|
|
|
|
if !c.configCache.enabled() {
|
|
return c.queryServiceConfig(serviceName)
|
|
}
|
|
|
|
// Read the last write time first. If the key changes while the configuration
|
|
// is queried, the next check sees a newer time and queries it again.
|
|
lastWrite, lastWriteErr := c.configCache.lastWrite(serviceName)
|
|
|
|
config, available, err := c.queryServiceConfig(serviceName)
|
|
if err != nil {
|
|
return serviceConfig{}, false, err
|
|
}
|
|
|
|
switch {
|
|
case lastWriteErr != nil:
|
|
c.logger.Debug("Failed to read the last write time of the service registry key. The service configuration is not cached",
|
|
slog.String("service", serviceName),
|
|
slog.Any("err", lastWriteErr),
|
|
)
|
|
case !available:
|
|
// An unavailable configuration, for example a display name from a missing
|
|
// MUI file, can become available without a change to the service key.
|
|
case !config.extendedAvailable:
|
|
// The delayed auto-start flag and triggers are queried again on the next scrape.
|
|
case time.Since(lastWrite) < configCacheMinKeyAge:
|
|
// Cached on a later scrape, once a second write in the same tick is ruled out.
|
|
default:
|
|
c.configCache.put(serviceName, generation, cachedServiceConfig{
|
|
config: config,
|
|
available: available,
|
|
lastWrite: lastWrite,
|
|
})
|
|
}
|
|
|
|
return config, available, nil
|
|
}
|