@@ -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
245305func shouldUseColors (w io.Writer ) bool {
0 commit comments