From 2861fd90ab61539eb69814a5313f272f42782a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=82=86=E6=9C=88?= <2835601846@qq.com> Date: Thu, 11 Jun 2026 15:10:56 +0800 Subject: [PATCH] fix(launcher): hide console flashes in all Windows child processes (#3061) * fix(launcher): hide console flashes in all Windows child processes PR #2654 only applied HideWindow to child processes in gateway.go (powershell, tasklist, ps). Several other files still use exec.Command directly, causing visible console windows on Windows. - startup.go: reg query/add/delete for autostart registry - version.go: picoclaw version subcommand - runtime.go: rundll32 for browser launch - onboard.go: picoclaw onboard subcommand Add launcherExecCommand to the utils package (matching the api package pattern) and replace all bare exec.Command calls on Windows paths. * refactor: consolidate launcherExecCommand into utils package Export LauncherExecCommand and ApplyLauncherProcAttrs from the utils package as the single source of truth. The api package now imports and delegates to these exported functions, eliminating code duplication. Addresses review feedback from imguoguo on PR #3061. --- web/backend/api/exec_nonwindows.go | 12 +++++++++--- web/backend/api/exec_windows.go | 15 ++++----------- web/backend/api/startup.go | 14 +++++++++++--- web/backend/api/version.go | 4 +++- web/backend/utils/exec_nonwindows.go | 14 ++++++++++++++ web/backend/utils/exec_windows.go | 28 ++++++++++++++++++++++++++++ web/backend/utils/onboard.go | 3 +-- web/backend/utils/runtime.go | 2 +- 8 files changed, 71 insertions(+), 21 deletions(-) create mode 100644 web/backend/utils/exec_nonwindows.go create mode 100644 web/backend/utils/exec_windows.go diff --git a/web/backend/api/exec_nonwindows.go b/web/backend/api/exec_nonwindows.go index 0dc3c0e94..27a4fc759 100644 --- a/web/backend/api/exec_nonwindows.go +++ b/web/backend/api/exec_nonwindows.go @@ -2,10 +2,16 @@ package api -import "os/exec" +import ( + "os/exec" + + "github.com/sipeed/picoclaw/web/backend/utils" +) func launcherExecCommand(name string, args ...string) *exec.Cmd { - return exec.Command(name, args...) + return utils.LauncherExecCommand(name, args...) } -func applyLauncherProcAttrs(_ *exec.Cmd) {} +func applyLauncherProcAttrs(cmd *exec.Cmd) { + utils.ApplyLauncherProcAttrs(cmd) +} diff --git a/web/backend/api/exec_windows.go b/web/backend/api/exec_windows.go index 86d3193a0..79cbbf6a7 100644 --- a/web/backend/api/exec_windows.go +++ b/web/backend/api/exec_windows.go @@ -4,21 +4,14 @@ package api import ( "os/exec" - "syscall" + + "github.com/sipeed/picoclaw/web/backend/utils" ) func launcherExecCommand(name string, args ...string) *exec.Cmd { - cmd := exec.Command(name, args...) - applyLauncherProcAttrs(cmd) - return cmd + return utils.LauncherExecCommand(name, args...) } func applyLauncherProcAttrs(cmd *exec.Cmd) { - if cmd == nil { - return - } - if cmd.SysProcAttr == nil { - cmd.SysProcAttr = &syscall.SysProcAttr{} - } - cmd.SysProcAttr.HideWindow = true + utils.ApplyLauncherProcAttrs(cmd) } diff --git a/web/backend/api/startup.go b/web/backend/api/startup.go index 8a3b8e8ff..bc71e49da 100644 --- a/web/backend/api/startup.go +++ b/web/backend/api/startup.go @@ -277,7 +277,11 @@ func windowsCommandLine(exePath string, args []string) string { } func windowsRunKeyExists() (bool, error) { - cmd := exec.Command("reg", "query", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autoStartEntryName) + cmd := launcherExecCommand( + "reg", "query", + `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, + "/v", autoStartEntryName, + ) if err := cmd.Run(); err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { @@ -292,11 +296,15 @@ func setWindowsAutoStart(enabled bool, exePath string, args []string) error { key := `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` if enabled { commandLine := windowsCommandLine(exePath, args) - cmd := exec.Command("reg", "add", key, "/v", autoStartEntryName, "/t", "REG_SZ", "/d", commandLine, "/f") + cmd := launcherExecCommand( + "reg", "add", key, + "/v", autoStartEntryName, + "/t", "REG_SZ", "/d", commandLine, "/f", + ) return cmd.Run() } - cmd := exec.Command("reg", "delete", key, "/v", autoStartEntryName, "/f") + cmd := launcherExecCommand("reg", "delete", key, "/v", autoStartEntryName, "/f") if err := cmd.Run(); err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { diff --git a/web/backend/api/version.go b/web/backend/api/version.go index 6232b989b..1994e4201 100644 --- a/web/backend/api/version.go +++ b/web/backend/api/version.go @@ -259,7 +259,9 @@ func (c *systemVersionCache) resetForTest() { // executePicoclawVersion runs the version subcommand against the // discovered picoclaw executable. func executePicoclawVersion(ctx context.Context, execPath string) (string, error) { - out, err := exec.CommandContext(ctx, execPath, "version").CombinedOutput() + cmd := exec.CommandContext(ctx, execPath, "version") + applyLauncherProcAttrs(cmd) + out, err := cmd.CombinedOutput() if err == nil { return string(out), nil } diff --git a/web/backend/utils/exec_nonwindows.go b/web/backend/utils/exec_nonwindows.go new file mode 100644 index 000000000..b5bcb7025 --- /dev/null +++ b/web/backend/utils/exec_nonwindows.go @@ -0,0 +1,14 @@ +//go:build !windows + +package utils + +import "os/exec" + +// LauncherExecCommand creates an exec.Cmd. On non-Windows platforms, this is +// a simple wrapper around exec.Command. +func LauncherExecCommand(name string, args ...string) *exec.Cmd { + return exec.Command(name, args...) +} + +// ApplyLauncherProcAttrs is a no-op on non-Windows platforms. +func ApplyLauncherProcAttrs(_ *exec.Cmd) {} diff --git a/web/backend/utils/exec_windows.go b/web/backend/utils/exec_windows.go new file mode 100644 index 000000000..079887df5 --- /dev/null +++ b/web/backend/utils/exec_windows.go @@ -0,0 +1,28 @@ +//go:build windows + +package utils + +import ( + "os/exec" + "syscall" +) + +// LauncherExecCommand creates an exec.Cmd with the HideWindow attribute set +// to prevent console window flashes on Windows. +func LauncherExecCommand(name string, args ...string) *exec.Cmd { + cmd := exec.Command(name, args...) + ApplyLauncherProcAttrs(cmd) + return cmd +} + +// ApplyLauncherProcAttrs applies Windows-specific process attributes to hide +// the console window. It is safe to call with a nil cmd. +func ApplyLauncherProcAttrs(cmd *exec.Cmd) { + if cmd == nil { + return + } + if cmd.SysProcAttr == nil { + cmd.SysProcAttr = &syscall.SysProcAttr{} + } + cmd.SysProcAttr.HideWindow = true +} diff --git a/web/backend/utils/onboard.go b/web/backend/utils/onboard.go index 81475ac80..6f37b862a 100644 --- a/web/backend/utils/onboard.go +++ b/web/backend/utils/onboard.go @@ -3,13 +3,12 @@ package utils import ( "fmt" "os" - "os/exec" "strings" "github.com/sipeed/picoclaw/pkg/config" ) -var execCommand = exec.Command +var execCommand = LauncherExecCommand func EnsureOnboarded(configPath string) error { _, err := os.Stat(configPath) diff --git a/web/backend/utils/runtime.go b/web/backend/utils/runtime.go index 8899a664b..d4aee0b87 100644 --- a/web/backend/utils/runtime.go +++ b/web/backend/utils/runtime.go @@ -150,7 +150,7 @@ func OpenBrowser(url string) error { case "linux": return exec.Command("xdg-open", url).Start() case "windows": - return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + return LauncherExecCommand("rundll32", "url.dll,FileProtocolHandler", url).Start() case "darwin": return exec.Command("open", url).Start() default: