From 5558d6093dc48b07d4b09f2b6c8ffab92ad77c5d Mon Sep 17 00:00:00 2001 From: Priyanshu Choudhary <57816400+Priyanchew@users.noreply.github.com> Date: Thu, 25 Jun 2026 03:48:01 +0530 Subject: [PATCH] fix: hide backend subprocess windows on Windows --- backend/internal/adapters/scm/github/auth.go | 5 ++-- .../workspace/gitworktree/workspace.go | 3 ++- backend/internal/cli/root.go | 5 ++-- backend/internal/legacyimport/importer.go | 4 ++-- backend/internal/observe/scm/observer.go | 4 ++-- backend/internal/process/command.go | 21 +++++++++++++++++ backend/internal/process/command_other.go | 7 ++++++ backend/internal/process/command_windows.go | 17 ++++++++++++++ .../internal/process/command_windows_test.go | 23 +++++++++++++++++++ backend/internal/service/project/service.go | 10 ++++---- .../service/project/workspace_registration.go | 4 ++-- backend/internal/session_manager/manager.go | 5 ++-- 12 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 backend/internal/process/command.go create mode 100644 backend/internal/process/command_other.go create mode 100644 backend/internal/process/command_windows.go create mode 100644 backend/internal/process/command_windows_test.go diff --git a/backend/internal/adapters/scm/github/auth.go b/backend/internal/adapters/scm/github/auth.go index 1299387376..c297e91567 100644 --- a/backend/internal/adapters/scm/github/auth.go +++ b/backend/internal/adapters/scm/github/auth.go @@ -4,10 +4,11 @@ import ( "context" "errors" "os" - "os/exec" "strings" "sync" "time" + + aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process" ) // TokenSource yields a GitHub bearer token on demand. Production wires this @@ -169,7 +170,7 @@ func (s *GHTokenSource) ttl() time.Duration { } func ghAuthToken(ctx context.Context) (string, error) { - out, err := exec.CommandContext(ctx, "gh", "auth", "token").Output() + out, err := aoprocess.CommandContext(ctx, "gh", "auth", "token").Output() if err != nil { return "", err } diff --git a/backend/internal/adapters/workspace/gitworktree/workspace.go b/backend/internal/adapters/workspace/gitworktree/workspace.go index d42b336395..d3725522b7 100644 --- a/backend/internal/adapters/workspace/gitworktree/workspace.go +++ b/backend/internal/adapters/workspace/gitworktree/workspace.go @@ -11,6 +11,7 @@ import ( "github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/ports" + aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process" ) const ( @@ -527,7 +528,7 @@ func pathExistsNonEmpty(path string) (bool, error) { } func runCommand(ctx context.Context, binary string, args ...string) ([]byte, error) { - cmd := exec.CommandContext(ctx, binary, args...) + cmd := aoprocess.CommandContext(ctx, binary, args...) out, err := cmd.CombinedOutput() if err != nil { return out, commandError{args: append([]string{binary}, args...), output: string(out), err: err} diff --git a/backend/internal/cli/root.go b/backend/internal/cli/root.go index 3bb1fcedd2..73ae4908d1 100644 --- a/backend/internal/cli/root.go +++ b/backend/internal/cli/root.go @@ -15,6 +15,7 @@ import ( "github.com/spf13/cobra" "github.com/aoagents/agent-orchestrator/backend/internal/daemon" + aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process" "github.com/aoagents/agent-orchestrator/backend/internal/processalive" ) @@ -96,11 +97,11 @@ func DefaultDeps() Deps { } func commandOutput(ctx context.Context, name string, args ...string) ([]byte, error) { - return exec.CommandContext(ctx, name, args...).CombinedOutput() + return aoprocess.CommandContext(ctx, name, args...).CombinedOutput() } func commandOutputInDir(ctx context.Context, dir, name string, args ...string) ([]byte, error) { - cmd := exec.CommandContext(ctx, name, args...) + cmd := aoprocess.CommandContext(ctx, name, args...) cmd.Dir = dir return cmd.CombinedOutput() } diff --git a/backend/internal/legacyimport/importer.go b/backend/internal/legacyimport/importer.go index cc3712f361..f108412598 100644 --- a/backend/internal/legacyimport/importer.go +++ b/backend/internal/legacyimport/importer.go @@ -4,13 +4,13 @@ import ( "context" "fmt" "os" - "os/exec" "regexp" "sort" "strings" "time" "github.com/aoagents/agent-orchestrator/backend/internal/domain" + aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process" ) // Store is the narrow slice of the rewrite's native storage layer the importer @@ -235,7 +235,7 @@ func defaultRepoOriginURL(path string) string { if path == "" { return "" } - cmd := exec.Command("git", "-C", path, "remote", "get-url", "origin") + cmd := aoprocess.Command("git", "-C", path, "remote", "get-url", "origin") out, err := cmd.Output() if err != nil { return "" diff --git a/backend/internal/observe/scm/observer.go b/backend/internal/observe/scm/observer.go index 10b6173372..2d7ec08458 100644 --- a/backend/internal/observe/scm/observer.go +++ b/backend/internal/observe/scm/observer.go @@ -12,7 +12,6 @@ import ( "errors" "fmt" "log/slog" - "os/exec" "strings" "sync" "time" @@ -20,6 +19,7 @@ import ( "github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/observe" "github.com/aoagents/agent-orchestrator/backend/internal/ports" + aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process" ) const ( @@ -1212,7 +1212,7 @@ func normalizePRState(draft, merged, closed bool) string { // The observer uses this to backfill projects that were registered before // project.Add resolved origin URLs at add time. func resolveGitOriginURL(path string) string { - out, err := exec.Command("git", "-C", path, "remote", "get-url", "origin").Output() + out, err := aoprocess.Command("git", "-C", path, "remote", "get-url", "origin").Output() if err != nil { return "" } diff --git a/backend/internal/process/command.go b/backend/internal/process/command.go new file mode 100644 index 0000000000..17659b5290 --- /dev/null +++ b/backend/internal/process/command.go @@ -0,0 +1,21 @@ +package process + +import ( + "context" + "os/exec" +) + +// Command creates a non-interactive child process. On Windows it suppresses +// transient console windows for CLI tools launched by the desktop daemon. +func Command(name string, args ...string) *exec.Cmd { + cmd := exec.Command(name, args...) + configureHidden(cmd) + return cmd +} + +// CommandContext is Command with cancellation support. +func CommandContext(ctx context.Context, name string, args ...string) *exec.Cmd { + cmd := exec.CommandContext(ctx, name, args...) + configureHidden(cmd) + return cmd +} diff --git a/backend/internal/process/command_other.go b/backend/internal/process/command_other.go new file mode 100644 index 0000000000..f924476c9c --- /dev/null +++ b/backend/internal/process/command_other.go @@ -0,0 +1,7 @@ +//go:build !windows + +package process + +import "os/exec" + +func configureHidden(_ *exec.Cmd) {} diff --git a/backend/internal/process/command_windows.go b/backend/internal/process/command_windows.go new file mode 100644 index 0000000000..a695fba7d8 --- /dev/null +++ b/backend/internal/process/command_windows.go @@ -0,0 +1,17 @@ +//go:build windows + +package process + +import ( + "os/exec" + "syscall" + + "golang.org/x/sys/windows" +) + +func configureHidden(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: windows.CREATE_NO_WINDOW, + HideWindow: true, + } +} diff --git a/backend/internal/process/command_windows_test.go b/backend/internal/process/command_windows_test.go new file mode 100644 index 0000000000..ab137be728 --- /dev/null +++ b/backend/internal/process/command_windows_test.go @@ -0,0 +1,23 @@ +//go:build windows + +package process + +import ( + "context" + "testing" + + "golang.org/x/sys/windows" +) + +func TestCommandContextHidesConsoleWindow(t *testing.T) { + cmd := CommandContext(context.Background(), "git", "--version") + if cmd.SysProcAttr == nil { + t.Fatal("SysProcAttr = nil, want hidden Windows process attributes") + } + if got := cmd.SysProcAttr.CreationFlags; got&windows.CREATE_NO_WINDOW == 0 { + t.Fatalf("CreationFlags = %#x, want CREATE_NO_WINDOW", got) + } + if !cmd.SysProcAttr.HideWindow { + t.Fatal("HideWindow = false, want true") + } +} diff --git a/backend/internal/service/project/service.go b/backend/internal/service/project/service.go index e21281a69e..af8593737f 100644 --- a/backend/internal/service/project/service.go +++ b/backend/internal/service/project/service.go @@ -3,7 +3,6 @@ package project import ( "context" "os" - "os/exec" "path/filepath" "regexp" "strconv" @@ -14,6 +13,7 @@ import ( "github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/httpd/apierr" "github.com/aoagents/agent-orchestrator/backend/internal/ports" + aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process" ) // Manager is the controller-facing contract for the /api/v1/projects surface. @@ -294,7 +294,7 @@ func (m *Service) SetConfig(ctx context.Context, id domain.ProjectID, in SetConf // other git error returns an empty string — `project add` must not fail just // because no origin is configured (the SCM observer skips such projects). func resolveGitOriginURL(path string) string { - out, err := exec.Command("git", "-C", path, "remote", "get-url", "origin").Output() + out, err := aoprocess.Command("git", "-C", path, "remote", "get-url", "origin").Output() if err != nil { return "" } @@ -313,14 +313,14 @@ func resolveGitOriginURL(path string) string { // returns an empty string — `project add` must not fail just because the branch // can't be resolved (the caller falls back to DefaultBranchName). func resolveDefaultBranch(path string) string { - if out, err := exec.Command( + if out, err := aoprocess.Command( "git", "-C", path, "symbolic-ref", "--short", "refs/remotes/origin/HEAD", ).Output(); err == nil { if ref := strings.TrimSpace(string(out)); ref != "" { return strings.TrimPrefix(ref, "origin/") } } - out, err := exec.Command("git", "-C", path, "symbolic-ref", "--short", "HEAD").Output() + out, err := aoprocess.Command("git", "-C", path, "symbolic-ref", "--short", "HEAD").Output() if err != nil { return "" } @@ -412,7 +412,7 @@ func normalizePath(raw string) (string, error) { } func isGitRepo(path string) bool { - cmd := exec.Command("git", "-C", path, "rev-parse", "--show-toplevel") + cmd := aoprocess.Command("git", "-C", path, "rev-parse", "--show-toplevel") out, err := cmd.Output() if err != nil { return false diff --git a/backend/internal/service/project/workspace_registration.go b/backend/internal/service/project/workspace_registration.go index dc44aab31b..fe072984cc 100644 --- a/backend/internal/service/project/workspace_registration.go +++ b/backend/internal/service/project/workspace_registration.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "os" - "os/exec" "path/filepath" "sort" "strings" @@ -14,6 +13,7 @@ import ( "github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/httpd/apierr" + aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process" ) var workspaceRootIgnoreDenylist = []string{ @@ -358,7 +358,7 @@ func workspaceReposFromRecords(records []domain.WorkspaceRepoRecord) []Workspace } func gitOutput(ctx context.Context, dir string, args ...string) (string, error) { - cmd := exec.CommandContext(ctx, "git", append([]string{"-C", dir}, args...)...) + cmd := aoprocess.CommandContext(ctx, "git", append([]string{"-C", dir}, args...)...) out, err := cmd.CombinedOutput() if err != nil { return "", fmt.Errorf("git -C %s %s: %w: %s", dir, strings.Join(args, " "), err, strings.TrimSpace(string(out))) diff --git a/backend/internal/session_manager/manager.go b/backend/internal/session_manager/manager.go index 104982bd9c..49c3a697e2 100644 --- a/backend/internal/session_manager/manager.go +++ b/backend/internal/session_manager/manager.go @@ -16,6 +16,7 @@ import ( "github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/ports" + aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process" ) // Sentinel errors returned by the Session Manager; callers match them with @@ -875,9 +876,9 @@ func runPostCreate(ctx context.Context, workspacePath string, commands []string) } var cmd *exec.Cmd if runtime.GOOS == "windows" { - cmd = exec.CommandContext(ctx, "cmd", "/c", command) + cmd = aoprocess.CommandContext(ctx, "cmd", "/c", command) } else { - cmd = exec.CommandContext(ctx, "sh", "-c", command) + cmd = aoprocess.CommandContext(ctx, "sh", "-c", command) } cmd.Dir = workspacePath if out, err := cmd.CombinedOutput(); err != nil {