blob: e96803fe411aea1f5e21915f825bb196121830f2 [file] [log] [blame]
// Copyright 2019 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package pager implements paging using commands "less" or "more",
// depending on availability.
package pager
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"go.chromium.org/luci/common/system/exitcode"
"go.chromium.org/luci/common/system/terminal"
)
func done(err error) int {
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
return 0
}
// Main implements paging using commands "less" or "more" if they are available.
// If os.Stdout is not terminal or less/more are not available in $PATH, Main
// calls fn with out set to os.Stdout and returns its exit code. Otherwise
// creates a pager subprocess, directs its stdout to os.Stdout and calls fn with
// out set to pager stdin. fn's context is canceled if the user quits pager.
//
// If fn returns non-zero exit code before pager exits, Main returns that exit
// code. Otherwise Main returns pager's exit code.
// It is a race between the user hitting q and fn failing.
//
// Example:
//
// func main() int {
// return Main(context.Background(), func(ctx context.Context, out io.WriteCloser) int {
// for i := 0; i < 100000 && ctx.Err() == nil; i++ {
// fmt.Fprintln(out, i)
// }
// return 0
// })
// }
func Main(ctx context.Context, fn func(ctx context.Context, out io.WriteCloser) int) int {
if !terminal.IsTerminal(int(os.Stdout.Fd())) {
return fn(ctx, os.Stdout)
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
sigC := make(chan os.Signal, 1)
var cmd *exec.Cmd
if lessPath, _ := exec.LookPath("less"); lessPath != "" {
cmd = exec.Command(lessPath, "-FXr")
// Swallow interrupts. Less is supposed to be quit by pressing q.
// In particular, it does not respond to Ctrl+C.
signal.Notify(sigC, os.Interrupt, os.Kill)
defer signal.Stop(sigC)
} else if morePath, _ := exec.LookPath("more"); morePath != "" {
moreCtx, cancelMore := context.WithCancel(ctx)
cmd = exec.CommandContext(moreCtx, morePath)
// Forward Ctrl+C to more.
signal.Notify(sigC, os.Interrupt, os.Kill)
go func() {
for range sigC {
cancelMore()
}
}()
} else {
// A pager program is not available.
return fn(ctx, os.Stdout)
}
defer signal.Stop(sigC)
cmd.Stdout = os.Stdout
stdin, err := cmd.StdinPipe()
if err != nil {
return done(err)
}
if err := cmd.Start(); err != nil {
return done(err)
}
// Listen to both fn and pager.
exitCodeC := make(chan int, 2)
go func() {
if exitCode := fn(ctx, stdin); exitCode != 0 {
exitCodeC <- exitCode
}
// Let the pager know that this is the end.
stdin.Close()
}()
go func() {
if exitCode, ok := exitcode.Get(cmd.Wait()); ok {
exitCodeC <- exitCode
} else {
exitCodeC <- done(err)
}
cancel()
}()
return <-exitCodeC
}