terminal: send large output to pager (#3060)

For commands that could produce large amounts of output switch to a
pager command ($DELVE_PAGER, $PAGER or more) after a certain amount of
output is produced.

Fixes #919
This commit is contained in:
Alessandro Arzilli 2022-08-17 09:02:53 +02:00 committed by GitHub
parent 5b9f65dac2
commit cb91509630
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 258 additions and 87 deletions

@ -33,3 +33,12 @@ The [available commands](dlv.md) can be grouped into the following categories:
* [dlv version](dlv_version.md)
The above list may be incomplete. Refer to the auto-generated [complete usage document](dlv.md) to further explore all available commands.
## Environment variables
Delve also reads the following environment variables:
* `$DELVE_EDITOR` is used by the `edit` command (if it isn't set the `$EDITOR` variable is used instead)
* `$DELVE_PAGER` is used by commands that emit large output (if it isn't set the `$PAGER` variable is used instead, if neither is set `more` is used)
* `$TERM` is used to decide whether or not ANSI escape codes should be used for colorized output
* `$DELVE_DEBUGSERVER_PATH` is used to locate the debugserver executable on macOS

@ -751,7 +751,12 @@ func threads(t *Term, ctx callContext, args string) error {
return err
}
sort.Sort(byThreadID(threads))
done := false
t.stdout.pw.PageMaybe(func() { done = false })
for _, th := range threads {
if done {
break
}
prefix := " "
if state.CurrentThread != nil && state.CurrentThread.ID == th.ID {
prefix = "* "
@ -802,8 +807,11 @@ func (a byGoroutineID) Len() int { return len(a) }
func (a byGoroutineID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byGoroutineID) Less(i, j int) bool { return a[i].ID < a[j].ID }
func (c *Commands) printGoroutines(t *Term, ctx callContext, indent string, gs []*api.Goroutine, fgl api.FormatGoroutineLoc, flags api.PrintGoroutinesFlags, depth int, cmd string, state *api.DebuggerState) error {
func (c *Commands) printGoroutines(t *Term, ctx callContext, indent string, gs []*api.Goroutine, fgl api.FormatGoroutineLoc, flags api.PrintGoroutinesFlags, depth int, cmd string, pdone *bool, state *api.DebuggerState) error {
for _, g := range gs {
if t.longCommandCanceled() || (pdone != nil && *pdone) {
break
}
prefix := indent + " "
if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID {
prefix = indent + "* "
@ -846,9 +854,11 @@ func (c *Commands) goroutines(t *Term, ctx callContext, argstr string) error {
groups []api.GoroutineGroup
tooManyGroups bool
)
done := false
t.stdout.pw.PageMaybe(func() { done = true })
t.longCommandStart()
for start >= 0 {
if t.longCommandCanceled() {
if t.longCommandCanceled() || done {
fmt.Fprintf(t.stdout, "interrupted\n")
return nil
}
@ -859,7 +869,7 @@ func (c *Commands) goroutines(t *Term, ctx callContext, argstr string) error {
if len(groups) > 0 {
for i := range groups {
fmt.Fprintf(t.stdout, "%s\n", groups[i].Name)
err = c.printGoroutines(t, ctx, "\t", gs[groups[i].Offset:][:groups[i].Count], fgl, flags, depth, cmd, state)
err = c.printGoroutines(t, ctx, "\t", gs[groups[i].Offset:][:groups[i].Count], fgl, flags, depth, cmd, &done, state)
if err != nil {
return err
}
@ -873,7 +883,7 @@ func (c *Commands) goroutines(t *Term, ctx callContext, argstr string) error {
}
} else {
sort.Sort(byGoroutineID(gs))
err = c.printGoroutines(t, ctx, "", gs, fgl, flags, depth, cmd, state)
err = c.printGoroutines(t, ctx, "", gs, fgl, flags, depth, cmd, &done, state)
if err != nil {
return err
}
@ -1988,6 +1998,7 @@ loop:
if err != nil {
return err
}
t.stdout.pw.PageMaybe(nil)
fmt.Fprint(t.stdout, api.PrettyExamineMemory(uintptr(address), memArea, isLittleEndian, priFmt, size))
return nil
}
@ -2096,7 +2107,12 @@ func (t *Term) printSortedStrings(v []string, err error) error {
return err
}
sort.Strings(v)
done := false
t.stdout.pw.PageMaybe(func() { done = false })
for _, d := range v {
if done {
break
}
fmt.Fprintln(t.stdout, d)
}
return nil
@ -2202,6 +2218,7 @@ func stackCommand(t *Term, ctx callContext, args string) error {
if err != nil {
return err
}
t.stdout.pw.PageMaybe(nil)
printStack(t, t.stdout, stack, "", sa.offsets)
if sa.ancestors > 0 {
ancestors, err := t.client.Ancestors(ctx.Scope.GoroutineID, sa.ancestors, sa.ancestorDepth)
@ -2397,6 +2414,8 @@ func disassCommand(t *Term, ctx callContext, args string) error {
rest = argv[1]
}
t.stdout.pw.PageMaybe(nil)
flavor := t.conf.GetDisassembleFlavour()
var disasm api.AsmInstructions

@ -54,7 +54,7 @@ const logCommandOutput = false
func (ft *FakeTerminal) Exec(cmdstr string) (outstr string, err error) {
var buf bytes.Buffer
ft.Term.stdout.w = &buf
ft.Term.stdout.pw.w = &buf
ft.Term.starlarkEnv.Redirect(ft.Term.stdout)
err = ft.cmds.Call(cmdstr, ft.Term)
outstr = buf.String()
@ -67,7 +67,7 @@ func (ft *FakeTerminal) Exec(cmdstr string) (outstr string, err error) {
func (ft *FakeTerminal) ExecStarlark(starlarkProgram string) (outstr string, err error) {
var buf bytes.Buffer
ft.Term.stdout.w = &buf
ft.Term.stdout.pw.w = &buf
ft.Term.starlarkEnv.Redirect(ft.Term.stdout)
_, err = ft.Term.starlarkEnv.Execute("<stdin>", starlarkProgram, "main", nil)
outstr = buf.String()

220
pkg/terminal/out.go Normal file

@ -0,0 +1,220 @@
package terminal
import (
"bufio"
"io"
"os"
"os/exec"
"strings"
"github.com/go-delve/delve/pkg/terminal/colorize"
"github.com/mattn/go-isatty"
)
// transcriptWriter writes to a pagingWriter and also, optionally, to a
// buffered file.
type transcriptWriter struct {
fileOnly bool
pw *pagingWriter
file *bufio.Writer
fh io.Closer
colorEscapes map[colorize.Style]string
}
func (w *transcriptWriter) Write(p []byte) (nn int, err error) {
if !w.fileOnly {
nn, err = w.pw.Write(p)
}
if err == nil {
if w.file != nil {
return w.file.Write(p)
}
}
return
}
// ColorizePrint prints to out a syntax highlighted version of the text read from
// reader, between lines startLine and endLine.
func (w *transcriptWriter) ColorizePrint(path string, reader io.ReadSeeker, startLine, endLine, arrowLine int) error {
var err error
if !w.fileOnly {
err = colorize.Print(w.pw.w, path, reader, startLine, endLine, arrowLine, w.colorEscapes)
}
if err == nil {
if w.file != nil {
reader.Seek(0, io.SeekStart)
return colorize.Print(w.file, path, reader, startLine, endLine, arrowLine, nil)
}
}
return err
}
// Echo outputs str only to the optional transcript file.
func (w *transcriptWriter) Echo(str string) {
if w.file != nil {
w.file.WriteString(str)
}
}
// Flush flushes the optional transcript file.
func (w *transcriptWriter) Flush() {
if w.file != nil {
w.file.Flush()
}
}
// CloseTranscript closes the optional transcript file.
func (w *transcriptWriter) CloseTranscript() error {
if w.file == nil {
return nil
}
w.file.Flush()
w.fileOnly = false
err := w.fh.Close()
w.file = nil
w.fh = nil
return err
}
// TranscribeTo starts transcribing the output to the specified file. If
// fileOnly is true the output will only go to the file, output to the
// io.Writer will be suppressed.
func (w *transcriptWriter) TranscribeTo(fh io.WriteCloser, fileOnly bool) {
if w.file == nil {
w.CloseTranscript()
}
w.fh = fh
w.file = bufio.NewWriter(fh)
w.fileOnly = fileOnly
}
// pagingWriter writes to w. If PageMaybe is called, after a large amount of
// text has been written to w it will pipe the output to a pager instead.
type pagingWriter struct {
mode pagingWriterMode
w io.Writer
buf []byte
cmd *exec.Cmd
cmdStdin io.WriteCloser
pager string
lastnl bool
cancel func()
}
type pagingWriterMode uint8
const (
pagingWriterNormal pagingWriterMode = iota
pagingWriterMaybe
pagingWriterPaging
pagingWriterMaxLines = 30
pagingWriterColsPerLine = 100
)
func (w *pagingWriter) Write(p []byte) (nn int, err error) {
switch w.mode {
default:
fallthrough
case pagingWriterNormal:
return w.w.Write(p)
case pagingWriterMaybe:
w.buf = append(w.buf, p...)
if w.largeOutput() {
w.cmd = exec.Command(w.pager)
w.cmd.Stdout = os.Stdout
w.cmd.Stderr = os.Stderr
var err1, err2 error
w.cmdStdin, err1 = w.cmd.StdinPipe()
err2 = w.cmd.Start()
if err1 != nil || err2 != nil {
w.cmd = nil
w.mode = pagingWriterNormal
return w.w.Write(p)
}
if !w.lastnl {
w.w.Write([]byte("\n"))
}
w.w.Write([]byte("Sending output to pager...\n"))
w.cmdStdin.Write(w.buf)
w.buf = nil
w.mode = pagingWriterPaging
return w.cmdStdin.Write(p)
} else {
if len(p) > 0 {
w.lastnl = p[len(p)-1] == '\n'
}
return w.w.Write(p)
}
case pagingWriterPaging:
n, err := w.cmdStdin.Write(p)
if err != nil && w.cancel != nil {
w.cancel()
w.cancel = nil
}
return n, err
}
}
// Reset returns the pagingWriter to its normal mode.
func (w *pagingWriter) Reset() {
if w.mode == pagingWriterNormal {
return
}
w.mode = pagingWriterNormal
w.buf = nil
if w.cmd != nil {
w.cmdStdin.Close()
w.cmd.Wait()
w.cmd = nil
w.cmdStdin = nil
}
}
// PageMaybe configures pagingWriter to cache the output, after a large
// amount of text has been written to w it will automatically switch to
// piping output to a pager.
// The cancel function is called the first time a write to the pager errors.
func (w *pagingWriter) PageMaybe(cancel func()) {
if w.mode != pagingWriterNormal {
return
}
dlvpager := os.Getenv("DELVE_PAGER")
if dlvpager == "" {
if stdout, _ := w.w.(*os.File); stdout != nil {
if !isatty.IsTerminal(stdout.Fd()) {
return
}
}
if strings.ToLower(os.Getenv("TERM")) == "dumb" {
return
}
}
w.mode = pagingWriterMaybe
w.pager = dlvpager
if w.pager == "" {
w.pager = os.Getenv("PAGER")
if w.pager == "" {
w.pager = "more"
}
}
w.lastnl = true
w.cancel = cancel
}
func (w *pagingWriter) largeOutput() bool {
if len(w.buf) > pagingWriterMaxLines*pagingWriterColsPerLine {
return true
}
nl := 0
for i := range w.buf {
if w.buf[i] == '\n' {
nl++
if nl > pagingWriterMaxLines {
return true
}
}
}
return false
}

@ -3,7 +3,6 @@ package terminal
//lint:file-ignore ST1005 errors here can be capitalized
import (
"bufio"
"fmt"
"io"
"net/rpc"
@ -101,12 +100,12 @@ func New(client service.Client, conf *config.Config) *Term {
prompt: "(dlv) ",
line: liner.NewLiner(),
cmds: cmds,
stdout: &transcriptWriter{w: os.Stdout},
stdout: &transcriptWriter{pw: &pagingWriter{w: os.Stdout}},
}
t.line.SetCtrlZStop(true)
if strings.ToLower(os.Getenv("TERM")) != "dumb" {
t.stdout.w = getColorableWriter()
t.stdout.pw = &pagingWriter{w: getColorableWriter()}
t.stdout.colorEscapes = make(map[colorize.Style]string)
t.stdout.colorEscapes[colorize.NormalStyle] = terminalResetEscapeCode
wd := func(s string, defaultCode int) string {
@ -354,6 +353,7 @@ func (t *Term) Run() (int, error) {
}
t.stdout.Flush()
t.stdout.pw.Reset()
}
}
@ -580,7 +580,7 @@ func (t *Term) longCommandCanceled() bool {
// RedirectTo redirects the output of this terminal to the specified writer.
func (t *Term) RedirectTo(w io.Writer) {
t.stdout.w = w
t.stdout.pw.w = w
}
// isErrProcessExited returns true if `err` is an RPC error equivalent of proc.ErrProcessExited
@ -588,80 +588,3 @@ func isErrProcessExited(err error) bool {
rpcError, ok := err.(rpc.ServerError)
return ok && strings.Contains(rpcError.Error(), "has exited with status")
}
// transcriptWriter writes to a io.Writer and also, optionally, to a
// buffered file.
type transcriptWriter struct {
fileOnly bool
w io.Writer
file *bufio.Writer
fh io.Closer
colorEscapes map[colorize.Style]string
}
func (w *transcriptWriter) Write(p []byte) (nn int, err error) {
if !w.fileOnly {
nn, err = w.w.Write(p)
}
if err == nil {
if w.file != nil {
return w.file.Write(p)
}
}
return
}
// ColorizePrint prints to out a syntax highlighted version of the text read from
// reader, between lines startLine and endLine.
func (w *transcriptWriter) ColorizePrint(path string, reader io.ReadSeeker, startLine, endLine, arrowLine int) error {
var err error
if !w.fileOnly {
err = colorize.Print(w.w, path, reader, startLine, endLine, arrowLine, w.colorEscapes)
}
if err == nil {
if w.file != nil {
reader.Seek(0, io.SeekStart)
return colorize.Print(w.file, path, reader, startLine, endLine, arrowLine, nil)
}
}
return err
}
// Echo outputs str only to the optional transcript file.
func (w *transcriptWriter) Echo(str string) {
if w.file != nil {
w.file.WriteString(str)
}
}
// Flush flushes the optional transcript file.
func (w *transcriptWriter) Flush() {
if w.file != nil {
w.file.Flush()
}
}
// CloseTranscript closes the optional transcript file.
func (w *transcriptWriter) CloseTranscript() error {
if w.file == nil {
return nil
}
w.file.Flush()
w.fileOnly = false
err := w.fh.Close()
w.file = nil
w.fh = nil
return err
}
// TranscribeTo starts transcribing the output to the specified file. If
// fileOnly is true the output will only go to the file, output to the
// io.Writer will be suppressed.
func (w *transcriptWriter) TranscribeTo(fh io.WriteCloser, fileOnly bool) {
if w.file == nil {
w.CloseTranscript()
}
w.fh = fh
w.file = bufio.NewWriter(fh)
w.fileOnly = fileOnly
}