Skip to content

Commit 35d81f5

Browse files
stainless-app[bot]batuhan
authored andcommitted
fix: cli no longer hangs when stdin is attached to a pipe with empty input
1 parent 73da214 commit 35d81f5

File tree

3 files changed

+64
-3
lines changed

3 files changed

+64
-3
lines changed

pkg/cmd/cmdutil.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,35 @@ var debugMiddlewareOption = option.WithMiddleware(
7070
},
7171
)
7272

73+
// isInputPiped tries to check for input being piped into the CLI which tells us that we should try to read
74+
// from stdin. This can be a bit tricky in some cases like when an stdin is connected to a pipe but nothing is
75+
// being piped in (this may happen in some environments like Cursor's integration terminal or CI), which is
76+
// why this function is a little more elaborate than it'd be otherwise.
7377
func isInputPiped() bool {
74-
stat, _ := os.Stdin.Stat()
75-
return (stat.Mode() & os.ModeCharDevice) == 0
78+
stat, err := os.Stdin.Stat()
79+
if err != nil {
80+
return false
81+
}
82+
83+
mode := stat.Mode()
84+
85+
// Regular file (redirect like < file.txt) — only if non-empty.
86+
//
87+
// Notably, on Unix the case like `< /dev/null` is handled below because `/dev/null` is not a regular
88+
// file. On Windows, NUL appears as a regular file with size 0, so it's also handled correctly.
89+
if mode.IsRegular() && stat.Size() > 0 {
90+
return true
91+
}
92+
93+
// For pipes/sockets (e.g. `echo foo | stainlesscli`), use an OS-specific check to determine whether
94+
// data is actually available. Some environments like Cursor's integrated terminal connect stdin as a
95+
// pipe even when nothing is being piped.
96+
if mode&(os.ModeNamedPipe|os.ModeSocket) != 0 {
97+
// Defined in either cmdutil_unix.go or cmdutil_windows.go.
98+
return isPipedDataAvailableOSSpecific()
99+
}
100+
101+
return false
76102
}
77103

78104
func isTerminal(w io.Writer) bool {

pkg/cmd/cmdutil_unix.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ import (
1212
"golang.org/x/sys/unix"
1313
)
1414

15+
func isPipedDataAvailableOSSpecific() bool {
16+
// Try to determine if there's non-empty data being piped into the command by polling for data for a short
17+
// amount of time. This is necessary because some environments (e.g. Cursor's integrated terminal) connect
18+
// stdin as a pipe even when nothing is being piped, which would cause the command to block indefinitely
19+
// waiting for input that will never come. The 10 ms timeout is arbitrary -- designed to be long enough to
20+
// allow data to be detected, but short enough that it shouldn't cause a noticeable delay in command runs.
21+
fds := []unix.PollFd{{Fd: int32(os.Stdin.Fd()), Events: unix.POLLIN}}
22+
n, _ := unix.Poll(fds, 10 /* ms */)
23+
return n > 0
24+
}
25+
1526
func streamOutputOSSpecific(label string, generateOutput func(w *os.File) error) error {
1627
// Try to use socket pair for better buffer control
1728
pagerInput, pid, err := openSocketPairPager(label)

pkg/cmd/cmdutil_windows.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,31 @@
22

33
package cmd
44

5-
import "os"
5+
import (
6+
"os"
7+
"syscall"
8+
"unsafe"
9+
)
10+
11+
var (
12+
kernel32 = syscall.NewLazyDLL("kernel32.dll")
13+
procPeekNamedPipe = kernel32.NewProc("PeekNamedPipe")
14+
)
15+
16+
func isPipedDataAvailableOSSpecific() bool {
17+
// On Windows, unix.Poll is not available. Use PeekNamedPipe to check if data is available
18+
// on the pipe without consuming it.
19+
var available uint32
20+
r, _, _ := procPeekNamedPipe.Call(
21+
os.Stdin.Fd(),
22+
0,
23+
0,
24+
0,
25+
uintptr(unsafe.Pointer(&available)),
26+
0,
27+
)
28+
return r != 0 && available > 0
29+
}
630

731
func streamOutputOSSpecific(label string, generateOutput func(w *os.File) error) error {
832
// We have a trick with sockets that we use when possible on Unix-like systems. Those APIs aren't

0 commit comments

Comments
 (0)