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:
parent
5b9f65dac2
commit
cb91509630
@ -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
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user