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: