Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ linters:
msg: Use testify assert/require helpers in tests.
- pattern: '^exec\.Command$'
msg: Use exec.CommandContext so subprocesses honor cancellation.
- pattern: '^exec\.CommandContext$'
msg: Use consolewindow.CommandContext from go.kenn.io/kit so a non-interactive child opens no console window on Windows; use exec.CommandContext with a nolint reason only when the child needs the console.
- pattern: '^http\.(Get|Post|Head|PostForm)$'
msg: Build the request with http.NewRequestWithContext and a client with timeouts.
- pattern: '^http\.DefaultClient$'
Expand Down Expand Up @@ -172,7 +174,7 @@ linters:
- linters:
- forbidigo
path: ^(cmd/|main\.go)
text: exec\.Command
text: use of `exec\.Command`
- linters:
- modernize
text: "omitzero:"
Expand Down
5 changes: 3 additions & 2 deletions agentmcp/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"strings"

"go.kenn.io/kit/consolewindow"
)

// Listener is one entry in a CLI's `mcp status --json` response. BackendURL
Expand All @@ -27,7 +28,7 @@ type Listener struct {
// HTTP listeners. It does not start servers or fall back to a stdio command.
// The caller should supply a context deadline and query only trusted CLIs.
func Discover(ctx context.Context, executable string) ([]Listener, error) {
data, err := exec.CommandContext(ctx, executable, "mcp", "status", "--json").Output()
data, err := consolewindow.CommandContext(ctx, executable, "mcp", "status", "--json").Output()
if err != nil {
return nil, fmt.Errorf("discover MCP listeners with %s: %w", executable, errors.Join(err, ctx.Err()))
}
Expand Down
14 changes: 13 additions & 1 deletion consolewindow/consolewindow.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Package consolewindow controls console windows for child processes.
package consolewindow

import "os/exec"
import (
"context"
"os/exec"
)

// Hide prevents a non-interactive child from opening a console window on Windows.
// Call it before starting cmd when its output is captured or discarded.
Expand All @@ -11,3 +14,12 @@ import "os/exec"
func Hide(cmd *exec.Cmd) {
hide(cmd)
}

// CommandContext is exec.CommandContext for a non-interactive child, with its
// console window hidden on Windows. A child that must use the caller's
// console, such as one that prompts, uses exec.CommandContext directly.
func CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, name, arg...) //nolint:forbidigo // the hidden constructor the lint rule points to
hide(cmd)
return cmd
}
8 changes: 8 additions & 0 deletions consolewindow/consolewindow_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ func TestHide(t *testing.T) {
})
}
}

func TestCommandContextHidesConsole(t *testing.T) {
cmd := CommandContext(t.Context(), "unused", "arg")

require.NotNil(t, cmd.SysProcAttr)
assert.Equal(t, uint32(windows.CREATE_NO_WINDOW), cmd.SysProcAttr.CreationFlags)
assert.Equal(t, []string{"unused", "arg"}, cmd.Args)
}
2 changes: 1 addition & 1 deletion git/cmd/gitcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func (b *outputBuffer) Write(p []byte) (int, error) {
}

func gitCommand(ctx context.Context, hideConsoleWindow bool, args ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, "git", args...)
cmd := exec.CommandContext(ctx, "git", args...) //nolint:forbidigo // prepareGitCommand hides it unless a credential prompt needs the console
cmd.WaitDelay = gitCommandWaitDelay
prepareGitCommand(cmd, hideConsoleWindow)
return cmd
Expand Down
3 changes: 1 addition & 2 deletions git/cmd/gitcmd_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ func prepareGitCommand(cmd *exec.Cmd, hideConsoleWindow bool) {
// its own bounded context.
killCtx, cancel := context.WithTimeout(context.Background(), gitCommandWaitDelay)
defer cancel()
kill := exec.CommandContext(killCtx, filepath.Join(systemDir, "taskkill.exe"),
kill := consolewindow.CommandContext(killCtx, filepath.Join(systemDir, "taskkill.exe"),
"/PID", strconv.Itoa(cmd.Process.Pid), "/T", "/F")
consolewindow.Hide(kill)
if err = kill.Run(); err == nil {
return nil
}
Expand Down
3 changes: 2 additions & 1 deletion git/managed/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"strings"
"time"

"go.kenn.io/kit/consolewindow"
gitcmd "go.kenn.io/kit/git/cmd"
gitworktree "go.kenn.io/kit/git/worktree"
"go.kenn.io/kit/pathresolve"
Expand Down Expand Up @@ -1152,7 +1153,7 @@ func executeLifecycleHook(
if run != nil {
err = run(ctx, command)
} else {
cmd := exec.CommandContext(ctx, command.Script)
cmd := consolewindow.CommandContext(ctx, command.Script)
cmd.Dir = command.Dir
cmd.Env = command.Env
cmd.Stdout = command.Stdout
Expand Down
4 changes: 3 additions & 1 deletion lint/config/golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ linters:
msg: Use testify assert/require helpers in tests.
- pattern: '^exec\.Command$'
msg: Use exec.CommandContext so subprocesses honor cancellation.
- pattern: '^exec\.CommandContext$'
msg: Use consolewindow.CommandContext from go.kenn.io/kit so a non-interactive child opens no console window on Windows; use exec.CommandContext with a nolint reason only when the child needs the console.
- pattern: '^http\.(Get|Post|Head|PostForm)$'
msg: Build the request with http.NewRequestWithContext and a client with timeouts.
- pattern: '^http\.DefaultClient$'
Expand Down Expand Up @@ -152,7 +154,7 @@ linters:
- linters:
- forbidigo
path: ^(cmd/|main\.go)
text: exec\.Command
text: use of `exec\.Command`
paths:
- third_party$
- builtin$
Expand Down
4 changes: 3 additions & 1 deletion openssh/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"sort"
"strconv"
"strings"

"go.kenn.io/kit/consolewindow"
)

// Option is one normalized entry from ssh -G output. Names are lowercase;
Expand Down Expand Up @@ -169,7 +171,7 @@ func runOutput(
if len(argv) == 0 {
return nil, nil, -1, errors.New("empty command")
}
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
cmd := consolewindow.CommandContext(ctx, argv[0], argv[1:]...)
var stdout, stderr strings.Builder
cmd.Stdout = &stdout
cmd.Stderr = &stderr
Expand Down
2 changes: 1 addition & 1 deletion openssh/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -1021,7 +1021,7 @@ func runSSHCommand(
executable string,
arguments []string,
) (int, error) {
cmd := exec.CommandContext(ctx, executable, arguments...)
cmd := exec.CommandContext(ctx, executable, arguments...) //nolint:forbidigo // interactive: BatchMode=no connections prompt on the console
err := cmd.Run()
if err == nil {
return 0, nil
Expand Down
3 changes: 2 additions & 1 deletion tools/humacheck/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import (
"sync"
"time"

"go.kenn.io/kit/consolewindow"
gitenv "go.kenn.io/kit/git/env"
)

func gitCommand(ctx context.Context, dir string, args ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, "git", append([]string{"-C", dir}, args...)...)
cmd := consolewindow.CommandContext(ctx, "git", append([]string{"-C", dir}, args...)...)
// Bind git to dir rather than to a GIT_DIR/GIT_INDEX_FILE inherited from
// a parent hook, so the checker always judges the repository it was asked about.
cmd.Env = append(gitenv.StripInherited(os.Environ()), "GIT_TERMINAL_PROMPT=0")
Expand Down
5 changes: 3 additions & 2 deletions tools/humacheck/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import (
"go/parser"
"go/token"
"os"
"os/exec"
"path/filepath"
"strings"

"golang.org/x/tools/go/packages"

"go.kenn.io/kit/consolewindow"
)

// dependencyFiles recognizes source files that belong to the standard library
Expand All @@ -30,7 +31,7 @@ type dependencyFiles struct {
// newDependencyFiles asks the go command that go/packages will run in dir
// for GOROOT and GOMODCACHE, so a toolchain switch picks the same roots.
func newDependencyFiles(ctx context.Context, dir string, env []string) (*dependencyFiles, error) {
cmd := exec.CommandContext(ctx, "go", "env", "GOROOT", "GOMODCACHE")
cmd := consolewindow.CommandContext(ctx, "go", "env", "GOROOT", "GOMODCACHE")
cmd.Dir = dir
cmd.Env = env
var stderr bytes.Buffer
Expand Down