fix: hide backend subprocess windows on Windows (#2179)

This commit is contained in:
Priyanshu Choudhary 2026-06-25 16:21:18 +05:30 committed by GitHub
parent a96143b502
commit 8bbc4c94fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 90 additions and 18 deletions

View File

@ -4,10 +4,11 @@ import (
"context" "context"
"errors" "errors"
"os" "os"
"os/exec"
"strings" "strings"
"sync" "sync"
"time" "time"
aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process"
) )
// TokenSource yields a GitHub bearer token on demand. Production wires this // 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) { 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 { if err != nil {
return "", err return "", err
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/domain"
"github.com/aoagents/agent-orchestrator/backend/internal/ports" "github.com/aoagents/agent-orchestrator/backend/internal/ports"
aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process"
) )
const ( const (
@ -752,7 +753,7 @@ func pathExistsNonEmpty(path string) (bool, error) {
} }
func runCommand(ctx context.Context, binary string, args ...string) ([]byte, 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() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return out, commandError{args: append([]string{binary}, args...), output: string(out), err: err} return out, commandError{args: append([]string{binary}, args...), output: string(out), err: err}

View File

@ -15,6 +15,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/aoagents/agent-orchestrator/backend/internal/daemon" "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" "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) { 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) { 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 cmd.Dir = dir
return cmd.CombinedOutput() return cmd.CombinedOutput()
} }

View File

@ -4,13 +4,13 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"os/exec"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"time" "time"
"github.com/aoagents/agent-orchestrator/backend/internal/domain" "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 // 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 == "" { if path == "" {
return "" 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() out, err := cmd.Output()
if err != nil { if err != nil {
return "" return ""

View File

@ -12,7 +12,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"os/exec"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -20,6 +19,7 @@ import (
"github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/domain"
"github.com/aoagents/agent-orchestrator/backend/internal/observe" "github.com/aoagents/agent-orchestrator/backend/internal/observe"
"github.com/aoagents/agent-orchestrator/backend/internal/ports" "github.com/aoagents/agent-orchestrator/backend/internal/ports"
aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process"
) )
const ( const (
@ -1212,7 +1212,7 @@ func normalizePRState(draft, merged, closed bool) string {
// The observer uses this to backfill projects that were registered before // The observer uses this to backfill projects that were registered before
// project.Add resolved origin URLs at add time. // project.Add resolved origin URLs at add time.
func resolveGitOriginURL(path string) string { 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 { if err != nil {
return "" return ""
} }

View File

@ -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
}

View File

@ -0,0 +1,7 @@
//go:build !windows
package process
import "os/exec"
func configureHidden(_ *exec.Cmd) {}

View File

@ -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,
}
}

View File

@ -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")
}
}

View File

@ -3,7 +3,6 @@ package project
import ( import (
"context" "context"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv" "strconv"
@ -14,6 +13,7 @@ import (
"github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/domain"
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/apierr" "github.com/aoagents/agent-orchestrator/backend/internal/httpd/apierr"
"github.com/aoagents/agent-orchestrator/backend/internal/ports" "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. // 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 // other git error returns an empty string — `project add` must not fail just
// because no origin is configured (the SCM observer skips such projects). // because no origin is configured (the SCM observer skips such projects).
func resolveGitOriginURL(path string) string { 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 { if err != nil {
return "" return ""
} }
@ -313,14 +313,14 @@ func resolveGitOriginURL(path string) string {
// returns an empty string — `project add` must not fail just because the branch // returns an empty string — `project add` must not fail just because the branch
// can't be resolved (the caller falls back to DefaultBranchName). // can't be resolved (the caller falls back to DefaultBranchName).
func resolveDefaultBranch(path string) string { 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", "git", "-C", path, "symbolic-ref", "--short", "refs/remotes/origin/HEAD",
).Output(); err == nil { ).Output(); err == nil {
if ref := strings.TrimSpace(string(out)); ref != "" { if ref := strings.TrimSpace(string(out)); ref != "" {
return strings.TrimPrefix(ref, "origin/") 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 { if err != nil {
return "" return ""
} }
@ -412,7 +412,7 @@ func normalizePath(raw string) (string, error) {
} }
func isGitRepo(path string) bool { 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() out, err := cmd.Output()
if err != nil { if err != nil {
return false return false

View File

@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@ -14,6 +13,7 @@ import (
"github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/domain"
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/apierr" "github.com/aoagents/agent-orchestrator/backend/internal/httpd/apierr"
aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process"
) )
var workspaceRootIgnoreDenylist = []string{ var workspaceRootIgnoreDenylist = []string{
@ -358,7 +358,7 @@ func workspaceReposFromRecords(records []domain.WorkspaceRepoRecord) []Workspace
} }
func gitOutput(ctx context.Context, dir string, args ...string) (string, error) { 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() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return "", fmt.Errorf("git -C %s %s: %w: %s", dir, strings.Join(args, " "), err, strings.TrimSpace(string(out))) return "", fmt.Errorf("git -C %s %s: %w: %s", dir, strings.Join(args, " "), err, strings.TrimSpace(string(out)))

View File

@ -16,6 +16,7 @@ import (
"github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/domain"
"github.com/aoagents/agent-orchestrator/backend/internal/ports" "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 // Sentinel errors returned by the Session Manager; callers match them with
@ -1176,9 +1177,9 @@ func runPostCreate(ctx context.Context, workspacePath string, commands []string)
} }
var cmd *exec.Cmd var cmd *exec.Cmd
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
cmd = exec.CommandContext(ctx, "cmd", "/c", command) cmd = aoprocess.CommandContext(ctx, "cmd", "/c", command)
} else { } else {
cmd = exec.CommandContext(ctx, "sh", "-c", command) cmd = aoprocess.CommandContext(ctx, "sh", "-c", command)
} }
cmd.Dir = workspacePath cmd.Dir = workspacePath
if out, err := cmd.CombinedOutput(); err != nil { if out, err := cmd.CombinedOutput(); err != nil {