Skip to content

Commit 52bc062

Browse files
fix(cli): fix compilation on Windows
1 parent 25d5a4f commit 52bc062

File tree

4 files changed

+146
-45
lines changed

4 files changed

+146
-45
lines changed

pkg/cmd/cmdutil.go

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -82,51 +82,14 @@ func streamOutput(label string, generateOutput func(w *os.File) error) error {
8282
return streamToStdout(generateOutput)
8383
}
8484

85-
// Windows lacks UNIX socket APIs, so we fall back to pipes there or if
86-
// socket creation fails. We prefer sockets when available because they
87-
// allow for smaller buffer sizes, preventing unnecessary data streaming
88-
// from the backend. Pipes typically have large buffers but serve as a
89-
// decent alternative when sockets aren't available.
90-
if runtime.GOOS == "windows" {
91-
return streamToPagerWithPipe(label, generateOutput)
92-
}
93-
94-
// Try to use socket pair for better buffer control
95-
pagerInput, pid, err := openSocketPairPager(label)
96-
if err != nil || pagerInput == nil {
97-
// Fall back to pipe if socket setup fails
98-
return streamToPagerWithPipe(label, generateOutput)
99-
}
100-
defer pagerInput.Close()
101-
102-
// If we would be streaming to a terminal and aren't forcing color one way
103-
// or the other, we should configure things to use color so the pager gets
104-
// colorized input.
105-
if isTerminal(os.Stdout) && os.Getenv("FORCE_COLOR") == "" {
106-
os.Setenv("FORCE_COLOR", "1")
107-
}
108-
109-
// If the pager exits before reading all input, then generateOutput() will
110-
// produce a broken pipe error, which is fine and we don't want to propagate it.
111-
if err := generateOutput(pagerInput); err != nil &&
112-
!strings.Contains(err.Error(), "broken pipe") {
113-
return err
114-
}
115-
116-
// Close the file NOW before we wait for the child process to terminate.
117-
// This way, the child will receive the end-of-file signal and know that
118-
// there is no more input. Otherwise the child process may block
119-
// indefinitely waiting for another line (this can happen when streaming
120-
// less than a screenful of data to a pager).
121-
pagerInput.Close()
122-
123-
// Wait for child process to exit
124-
var wstatus syscall.WaitStatus
125-
_, err = syscall.Wait4(pid, &wstatus, 0, nil)
126-
if wstatus.ExitStatus() != 0 {
127-
return fmt.Errorf("Pager exited with non-zero exit status: %d", wstatus.ExitStatus())
128-
}
129-
return err
85+
// When streaming output on Unix-like systems, there's a special trick involving creating two socket pairs
86+
// that we prefer because it supports small buffer sizes which results in less pagination per buffer. The
87+
// constructs needed to run it don't exist on Windows builds, so we have this function broken up into
88+
// OS-specific files with conditional build comments. Under Windows (and in case our fancy constructs fail
89+
// on Unix), we fall back to using pipes (`streamToPagerWithPipe`), which are OS agnostic.
90+
//
91+
// Defined in either cmdutil_unix.go or cmdutil_windows.go.
92+
return streamOutputOSSpecific(label, generateOutput)
13093
}
13194

13295
func streamToPagerWithPipe(label string, generateOutput func(w *os.File) error) error {

pkg/cmd/cmdutil_unix.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<<<<<<< HEAD
12
//go:build !windows
23

34
package cmd
@@ -31,3 +32,122 @@ func createSocketPair() (*os.File, *os.File, bool, error) {
3132
outputFile := os.NewFile(uintptr(parentSock), "parent_socket")
3233
return pagerInput, outputFile, true, nil
3334
}
35+
||||||| parent of fe526a1 (fix(cli): fix compilation on Windows)
36+
=======
37+
//go:build !windows
38+
39+
package cmd
40+
41+
import (
42+
"fmt"
43+
"os"
44+
"os/exec"
45+
"strings"
46+
"syscall"
47+
48+
"golang.org/x/sys/unix"
49+
)
50+
51+
func streamOutputOSSpecific(label string, generateOutput func(w *os.File) error) error {
52+
// Try to use socket pair for better buffer control
53+
pagerInput, pid, err := openSocketPairPager(label)
54+
if err != nil || pagerInput == nil {
55+
// Fall back to pipe if socket setup fails
56+
return streamToPagerWithPipe(label, generateOutput)
57+
}
58+
defer pagerInput.Close()
59+
60+
// If we would be streaming to a terminal and aren't forcing color one way
61+
// or the other, we should configure things to use color so the pager gets
62+
// colorized input.
63+
if isTerminal(os.Stdout) && os.Getenv("FORCE_COLOR") == "" {
64+
os.Setenv("FORCE_COLOR", "1")
65+
}
66+
67+
// If the pager exits before reading all input, then generateOutput() will
68+
// produce a broken pipe error, which is fine and we don't want to propagate it.
69+
if err := generateOutput(pagerInput); err != nil &&
70+
!strings.Contains(err.Error(), "broken pipe") {
71+
return err
72+
}
73+
74+
// Close the file NOW before we wait for the child process to terminate.
75+
// This way, the child will receive the end-of-file signal and know that
76+
// there is no more input. Otherwise the child process may block
77+
// indefinitely waiting for another line (this can happen when streaming
78+
// less than a screenful of data to a pager).
79+
pagerInput.Close()
80+
81+
// Wait for child process to exit
82+
var wstatus syscall.WaitStatus
83+
_, err = syscall.Wait4(pid, &wstatus, 0, nil)
84+
if wstatus.ExitStatus() != 0 {
85+
return fmt.Errorf("Pager exited with non-zero exit status: %d", wstatus.ExitStatus())
86+
}
87+
return err
88+
}
89+
90+
func openSocketPairPager(label string) (*os.File, int, error) {
91+
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
92+
if err != nil {
93+
return nil, 0, err
94+
}
95+
96+
// The child file descriptor will be sent to the child process through
97+
// ProcAttr and ForkExec(), while the parent process will always close the
98+
// child file descriptor.
99+
// The parent file descriptor will be wrapped in an os.File wrapper and
100+
// returned from this function, or closed if something goes wrong.
101+
parentFd, childFd := fds[0], fds[1]
102+
defer unix.Close(childFd)
103+
104+
// Use small buffer sizes so we don't ask the server for more paginated
105+
// values than we actually need.
106+
if err := unix.SetsockoptInt(parentFd, unix.SOL_SOCKET, unix.SO_SNDBUF, 128); err != nil {
107+
unix.Close(parentFd)
108+
return nil, 0, err
109+
}
110+
if err := unix.SetsockoptInt(childFd, unix.SOL_SOCKET, unix.SO_RCVBUF, 128); err != nil {
111+
unix.Close(parentFd)
112+
return nil, 0, err
113+
}
114+
115+
// Set CLOEXEC on the parent file descriptor so it doesn't leak to child
116+
syscall.CloseOnExec(parentFd)
117+
118+
parentConn := os.NewFile(uintptr(parentFd), "parent-socket")
119+
120+
pagerProgram := os.Getenv("PAGER")
121+
if pagerProgram == "" {
122+
pagerProgram = "less"
123+
}
124+
125+
pagerPath, err := exec.LookPath(pagerProgram)
126+
if err != nil {
127+
unix.Close(parentFd)
128+
return nil, 0, err
129+
}
130+
131+
env := os.Environ()
132+
env = append(env, "LESS=-r -P "+label)
133+
env = append(env, "MORE=-r -P "+label)
134+
135+
procAttr := &syscall.ProcAttr{
136+
Dir: "",
137+
Env: env,
138+
Files: []uintptr{
139+
uintptr(childFd), // stdin (fd 0)
140+
uintptr(syscall.Stdout), // stdout (fd 1)
141+
uintptr(syscall.Stderr), // stderr (fd 2)
142+
},
143+
}
144+
145+
pid, err := syscall.ForkExec(pagerPath, []string{pagerProgram}, procAttr)
146+
if err != nil {
147+
unix.Close(parentFd)
148+
return nil, 0, err
149+
}
150+
151+
return parentConn, pid, nil
152+
}
153+
>>>>>>> fe526a1 (fix(cli): fix compilation on Windows)

pkg/cmd/cmdutil_windows.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<<<<<<< HEAD
12
//go:build windows
23

34
package cmd
@@ -12,3 +13,17 @@ import (
1213
func createSocketPair() (*os.File, *os.File, bool, error) {
1314
return nil, nil, false, errors.New("socket pairs not supported on Windows")
1415
}
16+
||||||| parent of fe526a1 (fix(cli): fix compilation on Windows)
17+
=======
18+
//go:build windows
19+
20+
package cmd
21+
22+
import "os"
23+
24+
func streamOutputOSSpecific(label string, generateOutput func(w *os.File) error) error {
25+
// We have a trick with sockets that we use when possible on Unix-like systems. Those APIs aren't
26+
// available on Windows, so we fall back to using pipes.
27+
return streamToPagerWithPipe(label, generateOutput)
28+
}
29+
>>>>>>> fe526a1 (fix(cli): fix compilation on Windows)

scripts/test

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,6 @@ fi
5454

5555
echo "==> Running tests"
5656
go test ./... "$@"
57+
58+
echo "==> Checking tests on Windows"
59+
GOARCH=amd64 GOOS=windows go test -c ./... "$@"

0 commit comments

Comments
 (0)