From 3e8a2c6fe75beda52ebacf85ab93ef7fdb26439b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sun, 18 Oct 2026 21:12:11 +0200 Subject: [PATCH] cmd: disable console QuickEdit mode while the exporter runs (#2782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Otto Kröpke --- AGENTS.md | 55 ++++++++++++++++++++++++ cmd/windows_exporter/console.go | 76 +++++++++++++++++++++++++++++++++ cmd/windows_exporter/main.go | 13 ++++++ 3 files changed, 144 insertions(+) create mode 100644 cmd/windows_exporter/console.go diff --git a/AGENTS.md b/AGENTS.md index 88b6882c..0eb8d1e8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -354,6 +354,61 @@ If the debugger cannot download a PDB, fetch it with cURL from `` is the RSDS GUID of the module as 32 uppercase hex digits without dashes, followed by its age in hex. +## Console mode and hangs + +The Windows console pauses all output of the attached processes while a text +selection is active. In a conhost window (cmd.exe, PowerShell, RDP sessions) +QuickEdit mode starts one on a click into the window, mark mode (`Ctrl+M` or +the window menu) on request; Windows Terminal selects without pausing. A +paused console blocks `WriteConsole`, so a `slog` handler writing to stdout or +stderr blocks while holding its mutex, and every scrape that logs queues +behind it: `/metrics` stops answering while the process stays alive. Ctrl+C +copies the selection instead of sending the break signal, so it looks like a +crash without a log line (#2024). `--log.level=debug` logs on every scrape and +hits this within minutes; the default level needs a warning or error first. + +`cmd/windows_exporter/console.go` turns QuickEdit off while the exporter logs +to a console and restores the previous mode when `run` returns. Rules for that +code, from the conhost source (`src/host/getset.cpp`): + +- The flags live on the console input buffer. Open it as `CONIN$` instead of + using stdin, which may be redirected while the output still is a console. +- `ENABLE_EXTENDED_FLAGS` must be set in the mode passed to `SetConsoleMode` + to change `ENABLE_QUICK_EDIT_MODE`, `ENABLE_INSERT_MODE` or + `ENABLE_AUTO_POSITION`; a mode without any of these four bits leaves them + alone and only stops `GetConsoleMode` from reporting them. +- conhost sets `CONSOLE_USE_PRIVATE_FLAGS` when it creates the console + (`src/host/output.cpp`), so `GetConsoleMode` reports those four bits with + their real state from the start. Restore the mode exactly as read; do not + guess defaults. The mode is shared with the shell that started the exporter, + so restore it on every exit path after flag parsing (`--help`, usage errors + and startup failures included). A hard kill skips the restore. +- Run it only when the log destination is stdout or stderr. A service has no + console, and a file or the event log is not affected by selections. + +When `/metrics` stops answering while the process stays alive, take +`/debug/pprof/goroutine?debug=2` (needs `--debug.enabled`), twice a few seconds +apart. One goroutine in `syscall.WriteConsole` under `log/slog` with the +collectors waiting on a mutex is the paused console above. Without goroutines +in Go code, or when pprof doesn't answer either, ask for a full process dump +and the loaded module list (`tasklist /m`); EDR hooks have caused such hangs +(#2024). A hang of the service cannot be QuickEdit. + +## Service startup + +The service control manager gives a service 30 seconds by default (45 in some +reports) to connect, and a host busy after a Windows update can spend that on +loading the executable alone (#551). `cmd/windows_exporter/0_service.go` +therefore calls `svc.Run` from the package-level `IsService` declaration, which +runs before every `init` function and `main`; the `0_` prefix keeps it the first +file of the package, and the file must not import packages with slow +initialization. `Execute` reports `StartPending` and `Running` at once and +leaves the actual startup to `run`, so a slow collector `Build` cannot time +out the service start. The MSI adds delayed automatic start, a dependency on +`wmiApSrv` and restart on failure after 60 seconds (`installer/files.wxs`). +A startup failure before the logger exists goes to the event log and to +`%TEMP%` through `logStartupError`. + ## Dashboards, rules and runbooks Read [`contrib/mixin/README.md`](contrib/mixin/README.md) and diff --git a/cmd/windows_exporter/console.go b/cmd/windows_exporter/console.go new file mode 100644 index 00000000..4852bca1 --- /dev/null +++ b/cmd/windows_exporter/console.go @@ -0,0 +1,76 @@ +// 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 main + +import ( + "golang.org/x/sys/windows" +) + +// disableConsoleQuickEdit turns off the QuickEdit mode of the console the +// process is attached to. It returns whether the mode was changed and a +// function that restores the previous input mode. Without an attached console, +// for example as a service, it changes nothing and the restore is a no-op. +// +// With QuickEdit on, a click into a conhost window starts a text selection and +// the console stops accepting output until the selection ends. A log write +// then blocks in WriteConsole while holding the slog handler lock, so every +// scrape that logs waits behind it and /metrics stops answering. Ctrl+C copies +// the selection instead of interrupting the process, which is why such a hang +// looks like a crash without a log line (#2024). +// +// The console input buffer is opened as CONIN$, because the hang comes from +// writing to the console and happens with a redirected stdin as well. +func disableConsoleQuickEdit() (bool, func()) { + noop := func() {} + + conin, err := windows.CreateFile( + windows.StringToUTF16Ptr("CONIN$"), + windows.GENERIC_READ|windows.GENERIC_WRITE, + windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, + nil, + windows.OPEN_EXISTING, + 0, + 0, + ) + if err != nil { + return false, noop + } + + var mode uint32 + if err := windows.GetConsoleMode(conin, &mode); err != nil { + _ = windows.CloseHandle(conin) + + return false, noop + } + + // ENABLE_EXTENDED_FLAGS is required to change ENABLE_QUICK_EDIT_MODE. conhost + // reports it, together with the QuickEdit and insert mode flags, from the + // creation of the console on, so the mode as read is the complete state. + newMode := (mode | windows.ENABLE_EXTENDED_FLAGS) &^ windows.ENABLE_QUICK_EDIT_MODE + + if err := windows.SetConsoleMode(conin, newMode); err != nil { + _ = windows.CloseHandle(conin) + + return false, noop + } + + return true, func() { + _ = windows.SetConsoleMode(conin, mode) + _ = windows.CloseHandle(conin) + } +} diff --git a/cmd/windows_exporter/main.go b/cmd/windows_exporter/main.go index 92c3ea5b..598ce58c 100644 --- a/cmd/windows_exporter/main.go +++ b/cmd/windows_exporter/main.go @@ -216,6 +216,19 @@ func run(ctx context.Context, args []string) int { logger.LogAttrs(ctx, slog.LevelDebug, "logging has Started") + // A console that pauses its output, for example for a QuickEdit text + // selection, blocks the logger and with it every scrape. The console mode + // is shared with the shell that started the exporter, so the previous mode + // is restored when run returns. + if dest := logFile.String(); dest == "stdout" || dest == "stderr" { + disabled, restoreConsole := disableConsoleQuickEdit() + defer restoreConsole() + + if disabled { + logger.LogAttrs(ctx, slog.LevelDebug, "disabled the QuickEdit mode of the console") + } + } + if configFile != nil && *configFile != "" { logger.LogAttrs(ctx, slog.LevelInfo, "using configuration file", slog.String("file", *configFile),