From cb6773ea49dde61f7de6b4cf20a1f0dc601cd675 Mon Sep 17 00:00:00 2001 From: DaisyHunter <20070195+DaisyHunter@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:31:56 +0800 Subject: [PATCH] fix(fs): suppress visible console windows for git.exe children on Windows UntrustedRepositoryGitCommand is the single channel for every git fact aoci reads about a repository. When aoci itself runs console-less (MCP stdio spawned by a host), Windows allocates a new visible console for each git.exe child, so every refresh flashes a terminal window and steals focus (issue #79). hideChildConsole pins CREATE_NO_WINDOW + HideWindow on Windows and is a no-op on other platforms. Stdio handle inheritance is untouched, so host pipes and interactive terminal output behave exactly as before. Evidence: console-less 'aoci check' sampled 1310 git.exe children with zero windowed handles; on the patched binary gofmt/vet/staticcheck are clean, the fs package tests (including a new invariant test) pass, and linux/darwin/windows builds verify the platform stub. Blackbox suites on Windows: conformance 44/46 (the 2 misses are the suite's own aoci-code-relative expectations run against a third-party repo) and scenarios 57 PASS / 2 CHARACTERIZED / 0 FAIL. Fixes #79 --- internal/fs/git_command.go | 1 + internal/fs/git_command_other.go | 8 +++++++ internal/fs/git_command_windows.go | 28 +++++++++++++++++++++++++ internal/fs/git_command_windows_test.go | 25 ++++++++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 internal/fs/git_command_other.go create mode 100644 internal/fs/git_command_windows.go create mode 100644 internal/fs/git_command_windows_test.go diff --git a/internal/fs/git_command.go b/internal/fs/git_command.go index 6a12411e..d45c595e 100644 --- a/internal/fs/git_command.go +++ b/internal/fs/git_command.go @@ -28,5 +28,6 @@ func UntrustedRepositoryGitCommand(root string, args ...string) *exec.Cmd { } command := exec.Command("git", append(hardened, args...)...) command.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0") + hideChildConsole(command) return command } diff --git a/internal/fs/git_command_other.go b/internal/fs/git_command_other.go new file mode 100644 index 00000000..20445697 --- /dev/null +++ b/internal/fs/git_command_other.go @@ -0,0 +1,8 @@ +//go:build !windows + +package fs + +import "os/exec" + +// hideChildConsole 在非 Windows 平台为空操作;可见控制台窗口问题仅 Windows 存在。 +func hideChildConsole(*exec.Cmd) {} diff --git a/internal/fs/git_command_windows.go b/internal/fs/git_command_windows.go new file mode 100644 index 00000000..eca20401 --- /dev/null +++ b/internal/fs/git_command_windows.go @@ -0,0 +1,28 @@ +//go:build windows + +// Windows 控制台抑制 —— 与 git_command.go 同包的平台侧实现 +// +// 威胁: aoci 常以无控制台形态运行(如 MCP stdio 由宿主拉起)。此时 exec 拉起 +// 控制台子进程(git.exe)时,Windows 会为子进程新分配一个可见控制台窗口—— +// 刷新期间反复弹窗并抢占用户焦点。 +// 边界: CREATE_NO_WINDOW 只抑制控制台分配,不改变 stdio 句柄继承;宿主提供的 +// 管道与真实终端下的交互输出不受影响。HideWindow(STARTF_USESHOWWINDOW+SW_HIDE) +// 作为兜底。调用方环境变量属操作者自身信任域,不在此处覆盖。 +package fs + +import ( + "os/exec" + "syscall" +) + +// createNoWindow 即 Win32 CREATE_NO_WINDOW(0x08000000)。 +const createNoWindow = 0x08000000 + +// hideChildConsole 阻止 Windows 为 git.exe 子进程新建可见控制台窗口。 +func hideChildConsole(cmd *exec.Cmd) { + if cmd.SysProcAttr == nil { + cmd.SysProcAttr = &syscall.SysProcAttr{} + } + cmd.SysProcAttr.HideWindow = true + cmd.SysProcAttr.CreationFlags |= createNoWindow +} diff --git a/internal/fs/git_command_windows_test.go b/internal/fs/git_command_windows_test.go new file mode 100644 index 00000000..d8014d3c --- /dev/null +++ b/internal/fs/git_command_windows_test.go @@ -0,0 +1,25 @@ +//go:build windows + +package fs + +import ( + "testing" +) + +// 无控制台父进程(如 MCP stdio 宿主)拉起 git.exe 时,Windows 会为子进程新分配 +// 可见控制台窗口。构造出的命令必须携带 CREATE_NO_WINDOW+HideWindow;本断言把 +// 窗口抑制固定为构造器不变量,防止后续重构静默丢失。 +func TestUntrustedRepositoryGitCommandSuppressesChildConsoleWindow(t *testing.T) { + command := UntrustedRepositoryGitCommand(t.TempDir(), "rev-parse", "--show-toplevel") + attr := command.SysProcAttr + if attr == nil { + t.Fatalf("git 调用必须设置 SysProcAttr 以抑制子进程控制台窗口") + } + if !attr.HideWindow { + t.Fatalf("git 调用必须设置 HideWindow") + } + if attr.CreationFlags&createNoWindow == 0 { + t.Fatalf("git 调用必须携带 CREATE_NO_WINDOW(0x%x),实际 CreationFlags=0x%x", + createNoWindow, attr.CreationFlags) + } +}