From 66dfdbf44c9c4a411f0988b53b2c037630d07208 Mon Sep 17 00:00:00 2001 From: X9 Dev Date: Thu, 16 Apr 2026 14:39:36 +0200 Subject: [PATCH] runner: hide PowerShell window + filter PS error noise from log - HideWindow: true in SysProcAttr (Windows) prevents powershell.exe from opening a console window over the GUI - skipPSNoiseLine filters multi-line PS error blocks (At line:, CategoryInfo, FullyQualifiedErrorId, VERBOSE: etc.) - errors still appear via Write-Log Co-Authored-By: Claude Sonnet 4.6 --- internal/runner/hidecmd_other.go | 7 +++++++ internal/runner/hidecmd_windows.go | 14 ++++++++++++++ internal/runner/runner.go | 31 ++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 internal/runner/hidecmd_other.go create mode 100644 internal/runner/hidecmd_windows.go diff --git a/internal/runner/hidecmd_other.go b/internal/runner/hidecmd_other.go new file mode 100644 index 0000000..a41ac38 --- /dev/null +++ b/internal/runner/hidecmd_other.go @@ -0,0 +1,7 @@ +//go:build !windows + +package runner + +import "os/exec" + +func hideWindow(cmd *exec.Cmd) {} diff --git a/internal/runner/hidecmd_windows.go b/internal/runner/hidecmd_windows.go new file mode 100644 index 0000000..30e7fd8 --- /dev/null +++ b/internal/runner/hidecmd_windows.go @@ -0,0 +1,14 @@ +//go:build windows + +package runner + +import ( + "os/exec" + "syscall" +) + +// hideWindow prevents the child process from creating a visible console window. +// Without this, powershell.exe opens a full-size window on top of the GUI. +func hideWindow(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} +} diff --git a/internal/runner/runner.go b/internal/runner/runner.go index a85b55b..4bec1d8 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -223,6 +223,7 @@ func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error } cmd := exec.CommandContext(ctx, "powershell.exe", args...) + hideWindow(cmd) // prevent PS console window from appearing over the GUI stdout, err := cmd.StdoutPipe() if err != nil { @@ -237,6 +238,9 @@ func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error scanner := bufio.NewScanner(stdout) for scanner.Scan() { line := scanner.Text() + if skipPSNoiseLine(line) { + continue + } r.onLog(LogLine{ StepID: step.ID, Text: line, @@ -247,6 +251,33 @@ func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error return cmd.Wait() } +// skipPSNoiseLine returns true for PowerShell stderr noise that clutters the log: +// multi-line error blocks (At line:N, CategoryInfo, FullyQualifiedErrorId, etc.), +// blank lines, and VERBOSE: prefix lines already handled by Write-Log. +func skipPSNoiseLine(line string) bool { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + return true + } + for _, prefix := range []string{ + "At line:", + "+ CategoryInfo", + "+ FullyQualifiedErrorId", + "+ PositionMessage", + "VERBOSE:", + "DEBUG:", + } { + if strings.HasPrefix(trimmed, prefix) { + return true + } + } + // PS error continuation lines start with spaces + "+" or "~" + if len(trimmed) > 0 && (trimmed[0] == '+' || strings.HasPrefix(trimmed, "~")) { + return true + } + return false +} + // parseLevel extracts the log level from lines formatted as "[HH:mm:ss] [LEVEL] message". func parseLevel(line string) string { if strings.Contains(line, "] [OK]") {