Skip to content

Commit a430ef3

Browse files
committed
fix: hide backend subprocess windows on Windows
1 parent 9ae0573 commit a430ef3

12 files changed

Lines changed: 93 additions & 21 deletions

File tree

backend/internal/adapters/scm/github/auth.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import (
44
"context"
55
"errors"
66
"os"
7-
"os/exec"
87
"strings"
98
"sync"
109
"time"
10+
11+
aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process"
1112
)
1213

1314
// TokenSource yields a GitHub bearer token on demand. Production wires this
@@ -169,7 +170,7 @@ func (s *GHTokenSource) ttl() time.Duration {
169170
}
170171

171172
func ghAuthToken(ctx context.Context) (string, error) {
172-
out, err := exec.CommandContext(ctx, "gh", "auth", "token").Output()
173+
out, err := aoprocess.CommandContext(ctx, "gh", "auth", "token").Output()
173174
if err != nil {
174175
return "", err
175176
}

backend/internal/adapters/workspace/gitworktree/workspace.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
1414
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
15+
aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process"
1516
)
1617

1718
const (
@@ -281,14 +282,14 @@ func (w *Workspace) StashUncommitted(ctx context.Context, info ports.WorkspaceIn
281282

282283
// Stage all tracked and non-ignored untracked files into the temp index.
283284
// GIT_INDEX_FILE overrides the index so the real index is never touched.
284-
addCmd := exec.CommandContext(ctx, w.binary, addAllTempIndexArgs(info.Path)...)
285+
addCmd := aoprocess.CommandContext(ctx, w.binary, addAllTempIndexArgs(info.Path)...)
285286
addCmd.Env = append(os.Environ(), "GIT_INDEX_FILE="+tmpIdxPath)
286287
if out, err := addCmd.CombinedOutput(); err != nil {
287288
return "", commandError{args: append([]string{w.binary}, addAllTempIndexArgs(info.Path)...), output: string(out), err: err}
288289
}
289290

290291
// Write the staged tree to get a tree SHA.
291-
writeTreeCmd := exec.CommandContext(ctx, w.binary, writeTreeArgs(info.Path)...)
292+
writeTreeCmd := aoprocess.CommandContext(ctx, w.binary, writeTreeArgs(info.Path)...)
292293
writeTreeCmd.Env = append(os.Environ(), "GIT_INDEX_FILE="+tmpIdxPath)
293294
treeOut, err := writeTreeCmd.CombinedOutput()
294295
if err != nil {
@@ -402,7 +403,7 @@ func (w *Workspace) ApplyPreserved(ctx context.Context, info ports.WorkspaceInfo
402403
// returned commandError. Exit code detection happens in the caller.
403404
func (w *Workspace) runCherryPickNoCommit(ctx context.Context, worktree, commitSHA string) error {
404405
args := cherryPickNoCommitArgs(worktree, commitSHA)
405-
cmd := exec.CommandContext(ctx, w.binary, args...)
406+
cmd := aoprocess.CommandContext(ctx, w.binary, args...)
406407
out, err := cmd.CombinedOutput()
407408
if err != nil {
408409
return commandError{args: append([]string{w.binary}, args...), output: string(out), err: err}
@@ -752,7 +753,7 @@ func pathExistsNonEmpty(path string) (bool, error) {
752753
}
753754

754755
func runCommand(ctx context.Context, binary string, args ...string) ([]byte, error) {
755-
cmd := exec.CommandContext(ctx, binary, args...)
756+
cmd := aoprocess.CommandContext(ctx, binary, args...)
756757
out, err := cmd.CombinedOutput()
757758
if err != nil {
758759
return out, commandError{args: append([]string{binary}, args...), output: string(out), err: err}

backend/internal/cli/root.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/spf13/cobra"
1616

1717
"github.com/aoagents/agent-orchestrator/backend/internal/daemon"
18+
aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process"
1819
"github.com/aoagents/agent-orchestrator/backend/internal/processalive"
1920
)
2021

@@ -96,11 +97,11 @@ func DefaultDeps() Deps {
9697
}
9798

9899
func commandOutput(ctx context.Context, name string, args ...string) ([]byte, error) {
99-
return exec.CommandContext(ctx, name, args...).CombinedOutput()
100+
return aoprocess.CommandContext(ctx, name, args...).CombinedOutput()
100101
}
101102

102103
func commandOutputInDir(ctx context.Context, dir, name string, args ...string) ([]byte, error) {
103-
cmd := exec.CommandContext(ctx, name, args...)
104+
cmd := aoprocess.CommandContext(ctx, name, args...)
104105
cmd.Dir = dir
105106
return cmd.CombinedOutput()
106107
}

backend/internal/legacyimport/importer.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import (
44
"context"
55
"fmt"
66
"os"
7-
"os/exec"
87
"regexp"
98
"sort"
109
"strings"
1110
"time"
1211

1312
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
13+
aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process"
1414
)
1515

1616
// Store is the narrow slice of the rewrite's native storage layer the importer
@@ -235,7 +235,7 @@ func defaultRepoOriginURL(path string) string {
235235
if path == "" {
236236
return ""
237237
}
238-
cmd := exec.Command("git", "-C", path, "remote", "get-url", "origin")
238+
cmd := aoprocess.Command("git", "-C", path, "remote", "get-url", "origin")
239239
out, err := cmd.Output()
240240
if err != nil {
241241
return ""

backend/internal/observe/scm/observer.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import (
1212
"errors"
1313
"fmt"
1414
"log/slog"
15-
"os/exec"
1615
"strings"
1716
"sync"
1817
"time"
1918

2019
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
2120
"github.com/aoagents/agent-orchestrator/backend/internal/observe"
2221
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
22+
aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process"
2323
)
2424

2525
const (
@@ -1212,7 +1212,7 @@ func normalizePRState(draft, merged, closed bool) string {
12121212
// The observer uses this to backfill projects that were registered before
12131213
// project.Add resolved origin URLs at add time.
12141214
func resolveGitOriginURL(path string) string {
1215-
out, err := exec.Command("git", "-C", path, "remote", "get-url", "origin").Output()
1215+
out, err := aoprocess.Command("git", "-C", path, "remote", "get-url", "origin").Output()
12161216
if err != nil {
12171217
return ""
12181218
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package process
2+
3+
import (
4+
"context"
5+
"os/exec"
6+
)
7+
8+
// Command creates a non-interactive child process. On Windows it suppresses
9+
// transient console windows for CLI tools launched by the desktop daemon.
10+
func Command(name string, args ...string) *exec.Cmd {
11+
cmd := exec.Command(name, args...)
12+
configureHidden(cmd)
13+
return cmd
14+
}
15+
16+
// CommandContext is Command with cancellation support.
17+
func CommandContext(ctx context.Context, name string, args ...string) *exec.Cmd {
18+
cmd := exec.CommandContext(ctx, name, args...)
19+
configureHidden(cmd)
20+
return cmd
21+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//go:build !windows
2+
3+
package process
4+
5+
import "os/exec"
6+
7+
func configureHidden(_ *exec.Cmd) {}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//go:build windows
2+
3+
package process
4+
5+
import (
6+
"os/exec"
7+
"syscall"
8+
9+
"golang.org/x/sys/windows"
10+
)
11+
12+
func configureHidden(cmd *exec.Cmd) {
13+
cmd.SysProcAttr = &syscall.SysProcAttr{
14+
CreationFlags: windows.CREATE_NO_WINDOW,
15+
HideWindow: true,
16+
}
17+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//go:build windows
2+
3+
package process
4+
5+
import (
6+
"context"
7+
"testing"
8+
9+
"golang.org/x/sys/windows"
10+
)
11+
12+
func TestCommandContextHidesConsoleWindow(t *testing.T) {
13+
cmd := CommandContext(context.Background(), "git", "--version")
14+
if cmd.SysProcAttr == nil {
15+
t.Fatal("SysProcAttr = nil, want hidden Windows process attributes")
16+
}
17+
if got := cmd.SysProcAttr.CreationFlags; got&windows.CREATE_NO_WINDOW == 0 {
18+
t.Fatalf("CreationFlags = %#x, want CREATE_NO_WINDOW", got)
19+
}
20+
if !cmd.SysProcAttr.HideWindow {
21+
t.Fatal("HideWindow = false, want true")
22+
}
23+
}

backend/internal/service/project/service.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package project
33
import (
44
"context"
55
"os"
6-
"os/exec"
76
"path/filepath"
87
"regexp"
98
"strconv"
@@ -14,6 +13,7 @@ import (
1413
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
1514
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/apierr"
1615
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
16+
aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process"
1717
)
1818

1919
// 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
294294
// other git error returns an empty string — `project add` must not fail just
295295
// because no origin is configured (the SCM observer skips such projects).
296296
func resolveGitOriginURL(path string) string {
297-
out, err := exec.Command("git", "-C", path, "remote", "get-url", "origin").Output()
297+
out, err := aoprocess.Command("git", "-C", path, "remote", "get-url", "origin").Output()
298298
if err != nil {
299299
return ""
300300
}
@@ -313,14 +313,14 @@ func resolveGitOriginURL(path string) string {
313313
// returns an empty string — `project add` must not fail just because the branch
314314
// can't be resolved (the caller falls back to DefaultBranchName).
315315
func resolveDefaultBranch(path string) string {
316-
if out, err := exec.Command(
316+
if out, err := aoprocess.Command(
317317
"git", "-C", path, "symbolic-ref", "--short", "refs/remotes/origin/HEAD",
318318
).Output(); err == nil {
319319
if ref := strings.TrimSpace(string(out)); ref != "" {
320320
return strings.TrimPrefix(ref, "origin/")
321321
}
322322
}
323-
out, err := exec.Command("git", "-C", path, "symbolic-ref", "--short", "HEAD").Output()
323+
out, err := aoprocess.Command("git", "-C", path, "symbolic-ref", "--short", "HEAD").Output()
324324
if err != nil {
325325
return ""
326326
}
@@ -412,7 +412,7 @@ func normalizePath(raw string) (string, error) {
412412
}
413413

414414
func isGitRepo(path string) bool {
415-
cmd := exec.Command("git", "-C", path, "rev-parse", "--show-toplevel")
415+
cmd := aoprocess.Command("git", "-C", path, "rev-parse", "--show-toplevel")
416416
out, err := cmd.Output()
417417
if err != nil {
418418
return false

0 commit comments

Comments
 (0)