Skip to content

Commit 271fa15

Browse files
stainless-app[bot]yjp20
authored andcommitted
fix(cli): fix compilation on Windows
1 parent fed3893 commit 271fa15

File tree

4 files changed

+138
-111
lines changed

4 files changed

+138
-111
lines changed

pkg/cmd/cmdutil.go

Lines changed: 8 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"os/exec"
1111
"os/signal"
1212
"path"
13-
"runtime"
1413
"strings"
1514
"syscall"
1615

@@ -22,7 +21,6 @@ import (
2221
"github.com/tidwall/gjson"
2322
"github.com/tidwall/pretty"
2423
"github.com/urfave/cli/v3"
25-
"golang.org/x/sys/unix"
2624
"golang.org/x/term"
2725
)
2826

@@ -133,51 +131,14 @@ func streamOutput(label string, generateOutput func(w *os.File) error) error {
133131
return streamToStdout(generateOutput)
134132
}
135133

136-
// Windows lacks UNIX socket APIs, so we fall back to pipes there or if
137-
// socket creation fails. We prefer sockets when available because they
138-
// allow for smaller buffer sizes, preventing unnecessary data streaming
139-
// from the backend. Pipes typically have large buffers but serve as a
140-
// decent alternative when sockets aren't available.
141-
if runtime.GOOS == "windows" {
142-
return streamToPagerWithPipe(label, generateOutput)
143-
}
144-
145-
// Try to use socket pair for better buffer control
146-
pagerInput, pid, err := openSocketPairPager(label)
147-
if err != nil || pagerInput == nil {
148-
// Fall back to pipe if socket setup fails
149-
return streamToPagerWithPipe(label, generateOutput)
150-
}
151-
defer pagerInput.Close()
152-
153-
// If we would be streaming to a terminal and aren't forcing color one way
154-
// or the other, we should configure things to use color so the pager gets
155-
// colorized input.
156-
if isTerminal(os.Stdout) && os.Getenv("FORCE_COLOR") == "" {
157-
os.Setenv("FORCE_COLOR", "1")
158-
}
159-
160-
// If the pager exits before reading all input, then generateOutput() will
161-
// produce a broken pipe error, which is fine and we don't want to propagate it.
162-
if err := generateOutput(pagerInput); err != nil &&
163-
!strings.Contains(err.Error(), "broken pipe") {
164-
return err
165-
}
166-
167-
// Close the file NOW before we wait for the child process to terminate.
168-
// This way, the child will receive the end-of-file signal and know that
169-
// there is no more input. Otherwise the child process may block
170-
// indefinitely waiting for another line (this can happen when streaming
171-
// less than a screenful of data to a pager).
172-
pagerInput.Close()
173-
174-
// Wait for child process to exit
175-
var wstatus syscall.WaitStatus
176-
_, err = syscall.Wait4(pid, &wstatus, 0, nil)
177-
if wstatus.ExitStatus() != 0 {
178-
return fmt.Errorf("Pager exited with non-zero exit status: %d", wstatus.ExitStatus())
179-
}
180-
return err
134+
// When streaming output on Unix-like systems, there's a special trick involving creating two socket pairs
135+
// that we prefer because it supports small buffer sizes which results in less pagination per buffer. The
136+
// constructs needed to run it don't exist on Windows builds, so we have this function broken up into
137+
// OS-specific files with conditional build comments. Under Windows (and in case our fancy constructs fail
138+
// on Unix), we fall back to using pipes (`streamToPagerWithPipe`), which are OS agnostic.
139+
//
140+
// Defined in either cmdutil_unix.go or cmdutil_windows.go.
141+
return streamOutputOSSpecific(label, generateOutput)
181142
}
182143

183144
func streamToPagerWithPipe(label string, generateOutput func(w *os.File) error) error {
@@ -238,70 +199,6 @@ func streamToStdout(generateOutput func(w *os.File) error) error {
238199
return err
239200
}
240201

241-
func openSocketPairPager(label string) (*os.File, int, error) {
242-
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
243-
if err != nil {
244-
return nil, 0, err
245-
}
246-
247-
// The child file descriptor will be sent to the child process through
248-
// ProcAttr and ForkExec(), while the parent process will always close the
249-
// child file descriptor.
250-
// The parent file descriptor will be wrapped in an os.File wrapper and
251-
// returned from this function, or closed if something goes wrong.
252-
parentFd, childFd := fds[0], fds[1]
253-
defer unix.Close(childFd)
254-
255-
// Use small buffer sizes so we don't ask the server for more paginated
256-
// values than we actually need.
257-
if err := unix.SetsockoptInt(parentFd, unix.SOL_SOCKET, unix.SO_SNDBUF, 128); err != nil {
258-
unix.Close(parentFd)
259-
return nil, 0, err
260-
}
261-
if err := unix.SetsockoptInt(childFd, unix.SOL_SOCKET, unix.SO_RCVBUF, 128); err != nil {
262-
unix.Close(parentFd)
263-
return nil, 0, err
264-
}
265-
266-
// Set CLOEXEC on the parent file descriptor so it doesn't leak to child
267-
syscall.CloseOnExec(parentFd)
268-
269-
parentConn := os.NewFile(uintptr(parentFd), "parent-socket")
270-
271-
pagerProgram := os.Getenv("PAGER")
272-
if pagerProgram == "" {
273-
pagerProgram = "less"
274-
}
275-
276-
pagerPath, err := exec.LookPath(pagerProgram)
277-
if err != nil {
278-
unix.Close(parentFd)
279-
return nil, 0, err
280-
}
281-
282-
env := os.Environ()
283-
env = append(env, "LESS=-r -P "+label)
284-
env = append(env, "MORE=-r -P "+label)
285-
286-
procAttr := &syscall.ProcAttr{
287-
Dir: "",
288-
Env: env,
289-
Files: []uintptr{
290-
uintptr(childFd), // stdin (fd 0)
291-
uintptr(syscall.Stdout), // stdout (fd 1)
292-
uintptr(syscall.Stderr), // stderr (fd 2)
293-
},
294-
}
295-
296-
pid, err := syscall.ForkExec(pagerPath, []string{pagerProgram}, procAttr)
297-
if err != nil {
298-
unix.Close(parentFd)
299-
return nil, 0, err
300-
}
301-
302-
return parentConn, pid, nil
303-
}
304-
305202
func shouldUseColors(w io.Writer) bool {
306203
// Check if NO_COLOR environment variable is set
307204
if _, noColor := os.LookupEnv("NO_COLOR"); noColor {

pkg/cmd/cmdutil_unix.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//go:build !windows
2+
3+
package cmd
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"strings"
10+
"syscall"
11+
12+
"golang.org/x/sys/unix"
13+
)
14+
15+
func streamOutputOSSpecific(label string, generateOutput func(w *os.File) error) error {
16+
// Try to use socket pair for better buffer control
17+
pagerInput, pid, err := openSocketPairPager(label)
18+
if err != nil || pagerInput == nil {
19+
// Fall back to pipe if socket setup fails
20+
return streamToPagerWithPipe(label, generateOutput)
21+
}
22+
defer pagerInput.Close()
23+
24+
// If we would be streaming to a terminal and aren't forcing color one way
25+
// or the other, we should configure things to use color so the pager gets
26+
// colorized input.
27+
if isTerminal(os.Stdout) && os.Getenv("FORCE_COLOR") == "" {
28+
os.Setenv("FORCE_COLOR", "1")
29+
}
30+
31+
// If the pager exits before reading all input, then generateOutput() will
32+
// produce a broken pipe error, which is fine and we don't want to propagate it.
33+
if err := generateOutput(pagerInput); err != nil &&
34+
!strings.Contains(err.Error(), "broken pipe") {
35+
return err
36+
}
37+
38+
// Close the file NOW before we wait for the child process to terminate.
39+
// This way, the child will receive the end-of-file signal and know that
40+
// there is no more input. Otherwise the child process may block
41+
// indefinitely waiting for another line (this can happen when streaming
42+
// less than a screenful of data to a pager).
43+
pagerInput.Close()
44+
45+
// Wait for child process to exit
46+
var wstatus syscall.WaitStatus
47+
_, err = syscall.Wait4(pid, &wstatus, 0, nil)
48+
if wstatus.ExitStatus() != 0 {
49+
return fmt.Errorf("Pager exited with non-zero exit status: %d", wstatus.ExitStatus())
50+
}
51+
return err
52+
}
53+
54+
func openSocketPairPager(label string) (*os.File, int, error) {
55+
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
56+
if err != nil {
57+
return nil, 0, err
58+
}
59+
60+
// The child file descriptor will be sent to the child process through
61+
// ProcAttr and ForkExec(), while the parent process will always close the
62+
// child file descriptor.
63+
// The parent file descriptor will be wrapped in an os.File wrapper and
64+
// returned from this function, or closed if something goes wrong.
65+
parentFd, childFd := fds[0], fds[1]
66+
defer unix.Close(childFd)
67+
68+
// Use small buffer sizes so we don't ask the server for more paginated
69+
// values than we actually need.
70+
if err := unix.SetsockoptInt(parentFd, unix.SOL_SOCKET, unix.SO_SNDBUF, 128); err != nil {
71+
unix.Close(parentFd)
72+
return nil, 0, err
73+
}
74+
if err := unix.SetsockoptInt(childFd, unix.SOL_SOCKET, unix.SO_RCVBUF, 128); err != nil {
75+
unix.Close(parentFd)
76+
return nil, 0, err
77+
}
78+
79+
// Set CLOEXEC on the parent file descriptor so it doesn't leak to child
80+
syscall.CloseOnExec(parentFd)
81+
82+
parentConn := os.NewFile(uintptr(parentFd), "parent-socket")
83+
84+
pagerProgram := os.Getenv("PAGER")
85+
if pagerProgram == "" {
86+
pagerProgram = "less"
87+
}
88+
89+
pagerPath, err := exec.LookPath(pagerProgram)
90+
if err != nil {
91+
unix.Close(parentFd)
92+
return nil, 0, err
93+
}
94+
95+
env := os.Environ()
96+
env = append(env, "LESS=-r -P "+label)
97+
env = append(env, "MORE=-r -P "+label)
98+
99+
procAttr := &syscall.ProcAttr{
100+
Dir: "",
101+
Env: env,
102+
Files: []uintptr{
103+
uintptr(childFd), // stdin (fd 0)
104+
uintptr(syscall.Stdout), // stdout (fd 1)
105+
uintptr(syscall.Stderr), // stderr (fd 2)
106+
},
107+
}
108+
109+
pid, err := syscall.ForkExec(pagerPath, []string{pagerProgram}, procAttr)
110+
if err != nil {
111+
unix.Close(parentFd)
112+
return nil, 0, err
113+
}
114+
115+
return parentConn, pid, nil
116+
}

pkg/cmd/cmdutil_windows.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build windows
2+
3+
package cmd
4+
5+
import "os"
6+
7+
func streamOutputOSSpecific(label string, generateOutput func(w *os.File) error) error {
8+
// We have a trick with sockets that we use when possible on Unix-like systems. Those APIs aren't
9+
// available on Windows, so we fall back to using pipes.
10+
return streamToPagerWithPipe(label, generateOutput)
11+
}

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)