Skip to content

Commit 175af5c

Browse files
stainless-app[bot]yjp20
authored andcommitted
fix: paginated endpoints now behave better with pagers by default
1 parent 2b4de1f commit 175af5c

File tree

1 file changed

+120
-60
lines changed

1 file changed

+120
-60
lines changed

pkg/cmd/cmdutil.go

Lines changed: 120 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -133,28 +133,99 @@ func streamOutput(label string, generateOutput func(w *os.File) error) error {
133133
return streamToStdout(generateOutput)
134134
}
135135

136-
pagerInput, outputFile, isSocketPair, err := createPagerFiles()
137-
if err != nil {
138-
return err
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)
139150
}
140151
defer pagerInput.Close()
141-
defer outputFile.Close()
142152

143-
cmd, err := startPagerCommand(pagerInput, label, isSocketPair)
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
181+
}
182+
183+
func streamToPagerWithPipe(label string, generateOutput func(w *os.File) error) error {
184+
r, w, err := os.Pipe()
144185
if err != nil {
145186
return err
146187
}
188+
defer r.Close()
189+
defer w.Close()
190+
191+
pagerProgram := os.Getenv("PAGER")
192+
if pagerProgram == "" {
193+
pagerProgram = "less"
194+
}
147195

148-
if err := pagerInput.Close(); err != nil {
196+
if _, err := exec.LookPath(pagerProgram); err != nil {
149197
return err
150198
}
151199

152-
// If the pager exits before reading all input, then generateOutput() will
153-
// produce a broken pipe error, which is fine and we don't want to propagate it.
154-
if err := generateOutput(outputFile); err != nil && !strings.Contains(err.Error(), "broken pipe") {
200+
cmd := exec.Command(pagerProgram)
201+
cmd.Stdin = r
202+
cmd.Stdout = os.Stdout
203+
cmd.Stderr = os.Stderr
204+
cmd.Env = append(os.Environ(),
205+
"LESS=-r -P "+label,
206+
"MORE=-r -P "+label,
207+
)
208+
209+
if err := cmd.Start(); err != nil {
210+
return err
211+
}
212+
213+
if err := r.Close(); err != nil {
155214
return err
156215
}
157216

217+
// If we would be streaming to a terminal and aren't forcing color one way
218+
// or the other, we should configure things to use color so the pager gets
219+
// colorized input.
220+
if isTerminal(os.Stdout) && os.Getenv("FORCE_COLOR") == "" {
221+
os.Setenv("FORCE_COLOR", "1")
222+
}
223+
224+
if err := generateOutput(w); err != nil && !strings.Contains(err.Error(), "broken pipe") {
225+
return err
226+
}
227+
228+
w.Close()
158229
return cmd.Wait()
159230
}
160231

@@ -167,79 +238,68 @@ func streamToStdout(generateOutput func(w *os.File) error) error {
167238
return err
168239
}
169240

170-
func createPagerFiles() (*os.File, *os.File, bool, error) {
171-
// Windows lacks UNIX socket APIs, so we fall back to pipes there or if
172-
// socket creation fails. We prefer sockets when available because they
173-
// allow for smaller buffer sizes, preventing unnecessary data streaming
174-
// from the backend. Pipes typically have large buffers but serve as a
175-
// decent alternative when sockets aren't available.
176-
if runtime.GOOS != "windows" {
177-
pagerInput, outputFile, isSocketPair, err := createSocketPair()
178-
if err == nil {
179-
return pagerInput, outputFile, isSocketPair, nil
180-
}
181-
}
182-
183-
r, w, err := os.Pipe()
184-
return r, w, false, err
185-
}
186-
187-
// In order to avoid large buffers on pipes, this function create a pair of
188-
// files for reading and writing through a barely buffered socket.
189-
func createSocketPair() (*os.File, *os.File, bool, error) {
241+
func openSocketPairPager(label string) (*os.File, int, error) {
190242
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
191243
if err != nil {
192-
return nil, nil, false, err
244+
return nil, 0, err
193245
}
194246

195-
parentSock, childSock := fds[0], fds[1]
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)
196254

197255
// Use small buffer sizes so we don't ask the server for more paginated
198256
// values than we actually need.
199-
if err := unix.SetsockoptInt(parentSock, unix.SOL_SOCKET, unix.SO_SNDBUF, 128); err != nil {
200-
return nil, nil, false, err
257+
if err := unix.SetsockoptInt(parentFd, unix.SOL_SOCKET, unix.SO_SNDBUF, 128); err != nil {
258+
unix.Close(parentFd)
259+
return nil, 0, err
201260
}
202-
if err := unix.SetsockoptInt(childSock, unix.SOL_SOCKET, unix.SO_RCVBUF, 128); err != nil {
203-
return nil, nil, false, err
261+
if err := unix.SetsockoptInt(childFd, unix.SOL_SOCKET, unix.SO_RCVBUF, 128); err != nil {
262+
unix.Close(parentFd)
263+
return nil, 0, err
204264
}
205265

206-
pagerInput := os.NewFile(uintptr(childSock), "child_socket")
207-
outputFile := os.NewFile(uintptr(parentSock), "parent_socket")
208-
return pagerInput, outputFile, true, nil
209-
}
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")
210270

211-
// Start a subprocess running the user's preferred pager (or `less` if `$PAGER` is unset)
212-
func startPagerCommand(pagerInput *os.File, label string, useSocketpair bool) (*exec.Cmd, error) {
213271
pagerProgram := os.Getenv("PAGER")
214272
if pagerProgram == "" {
215273
pagerProgram = "less"
216274
}
217275

218-
if shouldUseColors(os.Stdout) {
219-
os.Setenv("FORCE_COLOR", "1")
276+
pagerPath, err := exec.LookPath(pagerProgram)
277+
if err != nil {
278+
unix.Close(parentFd)
279+
return nil, 0, err
220280
}
221281

222-
var cmd *exec.Cmd
223-
if useSocketpair {
224-
cmd = exec.Command(pagerProgram, fmt.Sprintf("/dev/fd/%d", pagerInput.Fd()))
225-
cmd.ExtraFiles = []*os.File{pagerInput}
226-
} else {
227-
cmd = exec.Command(pagerProgram)
228-
cmd.Stdin = pagerInput
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+
},
229294
}
230295

231-
cmd.Stdout = os.Stdout
232-
cmd.Stderr = os.Stderr
233-
cmd.Env = append(os.Environ(),
234-
"LESS=-r -f -P "+label,
235-
"MORE=-r -f -P "+label,
236-
)
237-
238-
if err := cmd.Start(); err != nil {
239-
return nil, err
296+
pid, err := syscall.ForkExec(pagerPath, []string{pagerProgram}, procAttr)
297+
if err != nil {
298+
unix.Close(parentFd)
299+
return nil, 0, err
240300
}
241301

242-
return cmd, nil
302+
return parentConn, pid, nil
243303
}
244304

245305
func shouldUseColors(w io.Writer) bool {

0 commit comments

Comments
 (0)