terminal: add transcript command (#2814)

Adds a transcript command that appends all command output to a file.
This command is equivalent to gdb's 'set logging'.

As part of this refactor the pkg/terminal commands to always write to a
io.Writer instead of using os.Stdout directly (through
fmt.Printf/fmt.Println).

Fixes #2237
This commit is contained in:
Alessandro Arzilli 2022-01-27 22:18:25 +01:00 committed by GitHub
parent c3eb1cf828
commit 5b925d4f5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 392 additions and 187 deletions

@ -91,6 +91,7 @@ Command | Description
[list](#list) | Show source code. [list](#list) | Show source code.
[source](#source) | Executes a file containing a list of delve commands [source](#source) | Executes a file containing a list of delve commands
[sources](#sources) | Print list of source files. [sources](#sources) | Print list of source files.
[transcript](#transcript) | Appends command output to a file.
[types](#types) | Print list of types [types](#types) | Print list of types
## args ## args
@ -636,6 +637,17 @@ See also: "help on", "help cond" and "help clear"
Aliases: t Aliases: t
## transcript
Appends command output to a file.
transcript [-t] [-x] <output file>
transcript -off
Output of Delve's command is appended to the specified output file. If '-t' is specified and the output file exists it is truncated. If '-x' is specified output to stdout is suppressed instead.
Using the -off option disables the transcript.
## types ## types
Print list of types Print list of types

@ -670,6 +670,7 @@ func traceCmd(cmd *cobra.Command, args []string) {
} }
cmds := terminal.DebugCommands(client) cmds := terminal.DebugCommands(client)
t := terminal.New(client, nil) t := terminal.New(client, nil)
t.RedirectTo(os.Stderr)
defer t.Close() defer t.Close()
if traceUseEBPF { if traceUseEBPF {
done := make(chan struct{}) done := make(chan struct{})

@ -962,7 +962,7 @@ func TestTracePid(t *testing.T) {
dlvbin, tmpdir := getDlvBin(t) dlvbin, tmpdir := getDlvBin(t)
defer os.RemoveAll(tmpdir) defer os.RemoveAll(tmpdir)
expected := []byte("goroutine(1): main.A() => ()\n") expected := []byte("goroutine(1): main.A()\n => ()\n")
// make process run // make process run
fix := protest.BuildFixture("issue2023", 0) fix := protest.BuildFixture("issue2023", 0)

@ -28,7 +28,6 @@ import (
"github.com/cosiner/argv" "github.com/cosiner/argv"
"github.com/go-delve/delve/pkg/config" "github.com/go-delve/delve/pkg/config"
"github.com/go-delve/delve/pkg/locspec" "github.com/go-delve/delve/pkg/locspec"
"github.com/go-delve/delve/pkg/terminal/colorize"
"github.com/go-delve/delve/service" "github.com/go-delve/delve/service"
"github.com/go-delve/delve/service/api" "github.com/go-delve/delve/service/api"
"github.com/go-delve/delve/service/rpc2" "github.com/go-delve/delve/service/rpc2"
@ -536,6 +535,15 @@ If display is called without arguments it will print the value of all expression
dump <output file> dump <output file>
The core dump is always written in ELF, even on systems (windows, macOS) where this is not customary. For environments other than linux/amd64 threads and registers are dumped in a format that only Delve can read back.`}, The core dump is always written in ELF, even on systems (windows, macOS) where this is not customary. For environments other than linux/amd64 threads and registers are dumped in a format that only Delve can read back.`},
{aliases: []string{"transcript"}, cmdFn: transcript, helpMsg: `Appends command output to a file.
transcript [-t] [-x] <output file>
transcript -off
Output of Delve's command is appended to the specified output file. If '-t' is specified and the output file exists it is truncated. If '-x' is specified output to stdout is suppressed instead.
Using the -off option disables the transcript.`},
} }
addrecorded := client == nil addrecorded := client == nil
@ -674,7 +682,7 @@ func (c *Commands) help(t *Term, ctx callContext, args string) error {
for _, cmd := range c.cmds { for _, cmd := range c.cmds {
for _, alias := range cmd.aliases { for _, alias := range cmd.aliases {
if alias == args { if alias == args {
fmt.Println(cmd.helpMsg) fmt.Fprintln(t.stdout, cmd.helpMsg)
return nil return nil
} }
} }
@ -682,12 +690,12 @@ func (c *Commands) help(t *Term, ctx callContext, args string) error {
return errNoCmd return errNoCmd
} }
fmt.Println("The following commands are available:") fmt.Fprintln(t.stdout, "The following commands are available:")
for _, cgd := range commandGroupDescriptions { for _, cgd := range commandGroupDescriptions {
fmt.Printf("\n%s:\n", cgd.description) fmt.Fprintf(t.stdout, "\n%s:\n", cgd.description)
w := new(tabwriter.Writer) w := new(tabwriter.Writer)
w.Init(os.Stdout, 0, 8, 0, '-', 0) w.Init(t.stdout, 0, 8, 0, '-', 0)
for _, cmd := range c.cmds { for _, cmd := range c.cmds {
if cmd.group != cgd.group { if cmd.group != cgd.group {
continue continue
@ -707,8 +715,8 @@ func (c *Commands) help(t *Term, ctx callContext, args string) error {
} }
} }
fmt.Println() fmt.Fprintln(t.stdout)
fmt.Println("Type help followed by a command for full documentation.") fmt.Fprintln(t.stdout, "Type help followed by a command for full documentation.")
return nil return nil
} }
@ -734,11 +742,11 @@ func threads(t *Term, ctx callContext, args string) error {
prefix = "* " prefix = "* "
} }
if th.Function != nil { if th.Function != nil {
fmt.Printf("%sThread %d at %#v %s:%d %s\n", fmt.Fprintf(t.stdout, "%sThread %d at %#v %s:%d %s\n",
prefix, th.ID, th.PC, t.formatPath(th.File), prefix, th.ID, th.PC, t.formatPath(th.File),
th.Line, th.Function.Name()) th.Line, th.Function.Name())
} else { } else {
fmt.Printf("%sThread %s\n", prefix, t.formatThread(th)) fmt.Fprintf(t.stdout, "%sThread %s\n", prefix, t.formatThread(th))
} }
} }
return nil return nil
@ -769,7 +777,7 @@ func thread(t *Term, ctx callContext, args string) error {
if newState.CurrentThread != nil { if newState.CurrentThread != nil {
newThread = strconv.Itoa(newState.CurrentThread.ID) newThread = strconv.Itoa(newState.CurrentThread.ID)
} }
fmt.Printf("Switched from %s to %s\n", oldThread, newThread) fmt.Fprintf(t.stdout, "Switched from %s to %s\n", oldThread, newThread)
return nil return nil
} }
@ -785,16 +793,16 @@ func printGoroutines(t *Term, indent string, gs []*api.Goroutine, fgl api.Format
if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID { if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID {
prefix = indent + "* " prefix = indent + "* "
} }
fmt.Printf("%sGoroutine %s\n", prefix, t.formatGoroutine(g, fgl)) fmt.Fprintf(t.stdout, "%sGoroutine %s\n", prefix, t.formatGoroutine(g, fgl))
if flags&api.PrintGoroutinesLabels != 0 { if flags&api.PrintGoroutinesLabels != 0 {
writeGoroutineLabels(os.Stdout, g, indent+"\t") writeGoroutineLabels(t.stdout, g, indent+"\t")
} }
if flags&api.PrintGoroutinesStack != 0 { if flags&api.PrintGoroutinesStack != 0 {
stack, err := t.client.Stacktrace(g.ID, depth, 0, nil) stack, err := t.client.Stacktrace(g.ID, depth, 0, nil)
if err != nil { if err != nil {
return err return err
} }
printStack(t, os.Stdout, stack, indent+"\t", false) printStack(t, t.stdout, stack, indent+"\t", false)
} }
} }
return nil return nil
@ -820,7 +828,7 @@ func goroutines(t *Term, ctx callContext, argstr string) error {
t.longCommandStart() t.longCommandStart()
for start >= 0 { for start >= 0 {
if t.longCommandCanceled() { if t.longCommandCanceled() {
fmt.Printf("interrupted\n") fmt.Fprintf(t.stdout, "interrupted\n")
return nil return nil
} }
gs, groups, start, tooManyGroups, err = t.client.ListGoroutinesWithFilter(start, batchSize, filters, &group) gs, groups, start, tooManyGroups, err = t.client.ListGoroutinesWithFilter(start, batchSize, filters, &group)
@ -829,18 +837,18 @@ func goroutines(t *Term, ctx callContext, argstr string) error {
} }
if len(groups) > 0 { if len(groups) > 0 {
for i := range groups { for i := range groups {
fmt.Printf("%s\n", groups[i].Name) fmt.Fprintf(t.stdout, "%s\n", groups[i].Name)
err = printGoroutines(t, "\t", gs[groups[i].Offset:][:groups[i].Count], fgl, flags, depth, state) err = printGoroutines(t, "\t", gs[groups[i].Offset:][:groups[i].Count], fgl, flags, depth, state)
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("\tTotal: %d\n", groups[i].Total) fmt.Fprintf(t.stdout, "\tTotal: %d\n", groups[i].Total)
if i != len(groups)-1 { if i != len(groups)-1 {
fmt.Printf("\n") fmt.Fprintf(t.stdout, "\n")
} }
} }
if tooManyGroups { if tooManyGroups {
fmt.Printf("Too many groups\n") fmt.Fprintf(t.stdout, "Too many groups\n")
} }
} else { } else {
sort.Sort(byGoroutineID(gs)) sort.Sort(byGoroutineID(gs))
@ -852,7 +860,7 @@ func goroutines(t *Term, ctx callContext, argstr string) error {
} }
} }
if gslen > 0 { if gslen > 0 {
fmt.Printf("[%d goroutines]\n", gslen) fmt.Fprintf(t.stdout, "[%d goroutines]\n", gslen)
} }
return nil return nil
} }
@ -893,7 +901,7 @@ func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error {
return err return err
} }
c.frame = 0 c.frame = 0
fmt.Printf("Switched from %d to %d (thread %d)\n", selectedGID(oldState), gid, newState.CurrentThread.ID) fmt.Fprintf(t.stdout, "Switched from %d to %d (thread %d)\n", selectedGID(oldState), gid, newState.CurrentThread.ID)
return nil return nil
} }
@ -950,7 +958,7 @@ func (c *Commands) frameCommand(t *Term, ctx callContext, argstr string, directi
} }
printcontext(t, state) printcontext(t, state)
th := stack[frame] th := stack[frame]
fmt.Printf("Frame %d: %s:%d (PC: %x)\n", frame, t.formatPath(th.File), th.Line, th.PC) fmt.Fprintf(t.stdout, "Frame %d: %s:%d (PC: %x)\n", frame, t.formatPath(th.File), th.Line, th.PC)
printfile(t, th.File, th.Line, true) printfile(t, th.File, th.Line, true)
return nil return nil
} }
@ -980,9 +988,9 @@ func printscope(t *Term) error {
return err return err
} }
fmt.Printf("Thread %s\n", t.formatThread(state.CurrentThread)) fmt.Fprintf(t.stdout, "Thread %s\n", t.formatThread(state.CurrentThread))
if state.SelectedGoroutine != nil { if state.SelectedGoroutine != nil {
writeGoroutineLong(t, os.Stdout, state.SelectedGoroutine, "") writeGoroutineLong(t, t.stdout, state.SelectedGoroutine, "")
} }
return nil return nil
} }
@ -1182,7 +1190,7 @@ func restartLive(t *Term, ctx callContext, args string) error {
return err return err
} }
fmt.Println("Process restarted with PID", t.client.ProcessPid()) fmt.Fprintln(t.stdout, "Process restarted with PID", t.client.ProcessPid())
return nil return nil
} }
@ -1192,7 +1200,7 @@ func restartIntl(t *Term, rerecord bool, restartPos string, resetArgs bool, newA
return err return err
} }
for i := range discarded { for i := range discarded {
fmt.Printf("Discarded %s at %s: %v\n", formatBreakpointName(discarded[i].Breakpoint, false), t.formatBreakpointLocation(discarded[i].Breakpoint), discarded[i].Reason) fmt.Fprintf(t.stdout, "Discarded %s at %s: %v\n", formatBreakpointName(discarded[i].Breakpoint, false), t.formatBreakpointLocation(discarded[i].Breakpoint), discarded[i].Reason)
} }
return nil return nil
} }
@ -1276,7 +1284,7 @@ func (c *Commands) rebuild(t *Term, ctx callContext, args string) error {
defer t.onStop() defer t.onStop()
discarded, err := t.client.Restart(true) discarded, err := t.client.Restart(true)
if len(discarded) > 0 { if len(discarded) > 0 {
fmt.Printf("not all breakpoints could be restored.") fmt.Fprintf(t.stdout, "not all breakpoints could be restored.")
} }
return err return err
} }
@ -1292,7 +1300,7 @@ func (c *Commands) cont(t *Term, ctx callContext, args string) error {
defer func() { defer func() {
for _, bp := range tmp { for _, bp := range tmp {
if _, err := t.client.ClearBreakpoint(bp.ID); err != nil { if _, err := t.client.ClearBreakpoint(bp.ID); err != nil {
fmt.Printf("failed to clear temporary breakpoint: %d", bp.ID) fmt.Fprintf(t.stdout, "failed to clear temporary breakpoint: %d", bp.ID)
} }
} }
}() }()
@ -1325,16 +1333,16 @@ func continueUntilCompleteNext(t *Term, state *api.DebuggerState, op string, sho
} }
skipBreakpoints := false skipBreakpoints := false
for { for {
fmt.Printf("\tbreakpoint hit during %s", op) fmt.Fprintf(t.stdout, "\tbreakpoint hit during %s", op)
if !skipBreakpoints { if !skipBreakpoints {
fmt.Printf("\n") fmt.Fprintf(t.stdout, "\n")
answer, err := promptAutoContinue(t, op) answer, err := promptAutoContinue(t, op)
switch answer { switch answer {
case "f": // finish next case "f": // finish next
skipBreakpoints = true skipBreakpoints = true
fallthrough fallthrough
case "c": // continue once case "c": // continue once
fmt.Printf("continuing...\n") fmt.Fprintf(t.stdout, "continuing...\n")
case "s": // stop and cancel case "s": // stop and cancel
fallthrough fallthrough
default: default:
@ -1343,7 +1351,7 @@ func continueUntilCompleteNext(t *Term, state *api.DebuggerState, op string, sho
return err return err
} }
} else { } else {
fmt.Printf(", continuing...\n") fmt.Fprintf(t.stdout, ", continuing...\n")
} }
stateChan := t.client.DirectionCongruentContinue() stateChan := t.client.DirectionCongruentContinue()
var state *api.DebuggerState var state *api.DebuggerState
@ -1542,7 +1550,7 @@ func clear(t *Term, ctx callContext, args string) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("%s cleared at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp)) fmt.Fprintf(t.stdout, "%s cleared at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp))
return nil return nil
} }
@ -1580,9 +1588,9 @@ func clearAll(t *Term, ctx callContext, args string) error {
_, err := t.client.ClearBreakpoint(bp.ID) _, err := t.client.ClearBreakpoint(bp.ID)
if err != nil { if err != nil {
fmt.Printf("Couldn't delete %s at %s: %s\n", formatBreakpointName(bp, false), t.formatBreakpointLocation(bp), err) fmt.Fprintf(t.stdout, "Couldn't delete %s at %s: %s\n", formatBreakpointName(bp, false), t.formatBreakpointLocation(bp), err)
} }
fmt.Printf("%s cleared at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp)) fmt.Fprintf(t.stdout, "%s cleared at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp))
} }
return nil return nil
} }
@ -1601,7 +1609,7 @@ func toggle(t *Term, ctx callContext, args string) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("%s toggled at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp)) fmt.Fprintf(t.stdout, "%s toggled at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp))
return nil return nil
} }
@ -1623,12 +1631,12 @@ func breakpoints(t *Term, ctx callContext, args string) error {
if bp.Disabled { if bp.Disabled {
enabled = "(disabled)" enabled = "(disabled)"
} }
fmt.Printf("%s %s at %v (%d)\n", formatBreakpointName(bp, true), enabled, t.formatBreakpointLocation(bp), bp.TotalHitCount) fmt.Fprintf(t.stdout, "%s %s at %v (%d)\n", formatBreakpointName(bp, true), enabled, t.formatBreakpointLocation(bp), bp.TotalHitCount)
attrs := formatBreakpointAttrs("\t", bp, false) attrs := formatBreakpointAttrs("\t", bp, false)
if len(attrs) > 0 { if len(attrs) > 0 {
fmt.Printf("%s\n", strings.Join(attrs, "\n")) fmt.Fprintf(t.stdout, "%s\n", strings.Join(attrs, "\n"))
} }
} }
return nil return nil
@ -1726,7 +1734,7 @@ func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) ([]
} }
created = append(created, bp) created = append(created, bp)
fmt.Printf("%s set at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp)) fmt.Fprintf(t.stdout, "%s set at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp))
} }
var shouldSetReturnBreakpoints bool var shouldSetReturnBreakpoints bool
@ -1825,7 +1833,7 @@ func watchpoint(t *Term, ctx callContext, args string) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("%s set at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp)) fmt.Fprintf(t.stdout, "%s set at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp))
return nil return nil
} }
@ -1955,7 +1963,7 @@ loop:
if err != nil { if err != nil {
return err return err
} }
fmt.Print(api.PrettyExamineMemory(uintptr(address), memArea, isLittleEndian, priFmt, size)) fmt.Fprint(t.stdout, api.PrettyExamineMemory(uintptr(address), memArea, isLittleEndian, priFmt, size))
return nil return nil
} }
@ -1984,7 +1992,7 @@ func printVar(t *Term, ctx callContext, args string) error {
return err return err
} }
fmt.Println(val.MultilineString("", fmtstr)) fmt.Fprintln(t.stdout, val.MultilineString("", fmtstr))
return nil return nil
} }
@ -1997,20 +2005,20 @@ func whatisCommand(t *Term, ctx callContext, args string) error {
return err return err
} }
if val.Flags&api.VariableCPURegister != 0 { if val.Flags&api.VariableCPURegister != 0 {
fmt.Println("CPU Register") fmt.Fprintln(t.stdout, "CPU Register")
return nil return nil
} }
if val.Type != "" { if val.Type != "" {
fmt.Println(val.Type) fmt.Fprintln(t.stdout, val.Type)
} }
if val.RealType != val.Type { if val.RealType != val.Type {
fmt.Printf("Real type: %s\n", val.RealType) fmt.Fprintf(t.stdout, "Real type: %s\n", val.RealType)
} }
if val.Kind == reflect.Interface && len(val.Children) > 0 { if val.Kind == reflect.Interface && len(val.Children) > 0 {
fmt.Printf("Concrete type: %s\n", val.Children[0].Type) fmt.Fprintf(t.stdout, "Concrete type: %s\n", val.Children[0].Type)
} }
if t.conf.ShowLocationExpr && val.LocationExpr != "" { if t.conf.ShowLocationExpr && val.LocationExpr != "" {
fmt.Printf("location: %s\n", val.LocationExpr) fmt.Fprintf(t.stdout, "location: %s\n", val.LocationExpr)
} }
return nil return nil
} }
@ -2032,7 +2040,7 @@ func setVar(t *Term, ctx callContext, args string) error {
return t.client.SetVariable(ctx.Scope, lexpr, rexpr) return t.client.SetVariable(ctx.Scope, lexpr, rexpr)
} }
func printFilteredVariables(varType string, vars []api.Variable, filter string, cfg api.LoadConfig) error { func (t *Term) printFilteredVariables(varType string, vars []api.Variable, filter string, cfg api.LoadConfig) error {
reg, err := regexp.Compile(filter) reg, err := regexp.Compile(filter)
if err != nil { if err != nil {
return err return err
@ -2046,39 +2054,39 @@ func printFilteredVariables(varType string, vars []api.Variable, filter string,
name = "(" + name + ")" name = "(" + name + ")"
} }
if cfg == ShortLoadConfig { if cfg == ShortLoadConfig {
fmt.Printf("%s = %s\n", name, v.SinglelineString()) fmt.Fprintf(t.stdout, "%s = %s\n", name, v.SinglelineString())
} else { } else {
fmt.Printf("%s = %s\n", name, v.MultilineString("", "")) fmt.Fprintf(t.stdout, "%s = %s\n", name, v.MultilineString("", ""))
} }
} }
} }
if !match { if !match {
fmt.Printf("(no %s)\n", varType) fmt.Fprintf(t.stdout, "(no %s)\n", varType)
} }
return nil return nil
} }
func printSortedStrings(v []string, err error) error { func (t *Term) printSortedStrings(v []string, err error) error {
if err != nil { if err != nil {
return err return err
} }
sort.Strings(v) sort.Strings(v)
for _, d := range v { for _, d := range v {
fmt.Println(d) fmt.Fprintln(t.stdout, d)
} }
return nil return nil
} }
func sources(t *Term, ctx callContext, args string) error { func sources(t *Term, ctx callContext, args string) error {
return printSortedStrings(t.client.ListSources(args)) return t.printSortedStrings(t.client.ListSources(args))
} }
func funcs(t *Term, ctx callContext, args string) error { func funcs(t *Term, ctx callContext, args string) error {
return printSortedStrings(t.client.ListFunctions(args)) return t.printSortedStrings(t.client.ListFunctions(args))
} }
func types(t *Term, ctx callContext, args string) error { func types(t *Term, ctx callContext, args string) error {
return printSortedStrings(t.client.ListTypes(args)) return t.printSortedStrings(t.client.ListTypes(args))
} }
func parseVarArguments(args string, t *Term) (filter string, cfg api.LoadConfig) { func parseVarArguments(args string, t *Term) (filter string, cfg api.LoadConfig) {
@ -2105,7 +2113,7 @@ func args(t *Term, ctx callContext, args string) error {
if err != nil { if err != nil {
return err return err
} }
return printFilteredVariables("args", vars, filter, cfg) return t.printFilteredVariables("args", vars, filter, cfg)
} }
func locals(t *Term, ctx callContext, args string) error { func locals(t *Term, ctx callContext, args string) error {
@ -2121,7 +2129,7 @@ func locals(t *Term, ctx callContext, args string) error {
if err != nil { if err != nil {
return err return err
} }
return printFilteredVariables("locals", locals, filter, cfg) return t.printFilteredVariables("locals", locals, filter, cfg)
} }
func vars(t *Term, ctx callContext, args string) error { func vars(t *Term, ctx callContext, args string) error {
@ -2130,7 +2138,7 @@ func vars(t *Term, ctx callContext, args string) error {
if err != nil { if err != nil {
return err return err
} }
return printFilteredVariables("vars", vars, filter, cfg) return t.printFilteredVariables("vars", vars, filter, cfg)
} }
func regs(t *Term, ctx callContext, args string) error { func regs(t *Term, ctx callContext, args string) error {
@ -2148,7 +2156,7 @@ func regs(t *Term, ctx callContext, args string) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Println(regs) fmt.Fprintln(t.stdout, regs)
return nil return nil
} }
@ -2169,19 +2177,19 @@ func stackCommand(t *Term, ctx callContext, args string) error {
if err != nil { if err != nil {
return err return err
} }
printStack(t, os.Stdout, stack, "", sa.offsets) printStack(t, t.stdout, stack, "", sa.offsets)
if sa.ancestors > 0 { if sa.ancestors > 0 {
ancestors, err := t.client.Ancestors(ctx.Scope.GoroutineID, sa.ancestors, sa.ancestorDepth) ancestors, err := t.client.Ancestors(ctx.Scope.GoroutineID, sa.ancestors, sa.ancestorDepth)
if err != nil { if err != nil {
return err return err
} }
for _, ancestor := range ancestors { for _, ancestor := range ancestors {
fmt.Printf("Created by Goroutine %d:\n", ancestor.ID) fmt.Fprintf(t.stdout, "Created by Goroutine %d:\n", ancestor.ID)
if ancestor.Unreadable != "" { if ancestor.Unreadable != "" {
fmt.Printf("\t%s\n", ancestor.Unreadable) fmt.Fprintf(t.stdout, "\t%s\n", ancestor.Unreadable)
continue continue
} }
printStack(t, os.Stdout, ancestor.Stack, "\t", false) printStack(t, t.stdout, ancestor.Stack, "\t", false)
} }
} }
return nil return nil
@ -2305,7 +2313,7 @@ func getLocation(t *Term, ctx callContext, args string, showContext bool) (file
} }
} }
if showContext { if showContext {
fmt.Printf("Goroutine %d frame %d at %s:%d (PC: %#x)\n", gid, ctx.Scope.Frame, loc.File, loc.Line, loc.PC) fmt.Fprintf(t.stdout, "Goroutine %d frame %d at %s:%d (PC: %#x)\n", gid, ctx.Scope.Frame, loc.File, loc.Line, loc.PC)
} }
return loc.File, loc.Line, true, nil return loc.File, loc.Line, true, nil
@ -2319,7 +2327,7 @@ func getLocation(t *Term, ctx callContext, args string, showContext bool) (file
} }
loc := locs[0] loc := locs[0]
if showContext { if showContext {
fmt.Printf("Showing %s:%d (PC: %#x)\n", loc.File, loc.Line, loc.PC) fmt.Fprintf(t.stdout, "Showing %s:%d (PC: %#x)\n", loc.File, loc.Line, loc.PC)
} }
return loc.File, loc.Line, false, nil return loc.File, loc.Line, false, nil
} }
@ -2417,7 +2425,7 @@ func disassCommand(t *Term, ctx callContext, args string) error {
return disasmErr return disasmErr
} }
disasmPrint(disasm, os.Stdout) disasmPrint(disasm, t.stdout)
return nil return nil
} }
@ -2429,7 +2437,7 @@ func libraries(t *Term, ctx callContext, args string) error {
} }
d := digits(len(libs)) d := digits(len(libs))
for i := range libs { for i := range libs {
fmt.Printf("%"+strconv.Itoa(d)+"d. %#x %s\n", i, libs[i].Address, libs[i].Path) fmt.Fprintf(t.stdout, "%"+strconv.Itoa(d)+"d. %#x %s\n", i, libs[i].Address, libs[i].Path)
} }
return nil return nil
} }
@ -2456,7 +2464,7 @@ func printcontext(t *Term, state *api.DebuggerState) {
} }
if state.CurrentThread == nil { if state.CurrentThread == nil {
fmt.Println("No current thread available") fmt.Fprintln(t.stdout, "No current thread available")
return return
} }
@ -2477,38 +2485,38 @@ func printcontext(t *Term, state *api.DebuggerState) {
} }
if th.File == "" { if th.File == "" {
fmt.Printf("Stopped at: 0x%x\n", state.CurrentThread.PC) fmt.Fprintf(t.stdout, "Stopped at: 0x%x\n", state.CurrentThread.PC)
_ = colorize.Print(t.stdout, "", bytes.NewReader([]byte("no source available")), 1, 10, 1, nil) t.stdout.ColorizePrint("", bytes.NewReader([]byte("no source available")), 1, 10, 1)
return return
} }
printcontextThread(t, th) printcontextThread(t, th)
if state.When != "" { if state.When != "" {
fmt.Println(state.When) fmt.Fprintln(t.stdout, state.When)
} }
for _, watchpoint := range state.WatchOutOfScope { for _, watchpoint := range state.WatchOutOfScope {
fmt.Printf("%s went out of scope and was cleared\n", formatBreakpointName(watchpoint, true)) fmt.Fprintf(t.stdout, "%s went out of scope and was cleared\n", formatBreakpointName(watchpoint, true))
} }
} }
func printcontextLocation(t *Term, loc api.Location) { func printcontextLocation(t *Term, loc api.Location) {
fmt.Printf("> %s() %s:%d (PC: %#v)\n", loc.Function.Name(), t.formatPath(loc.File), loc.Line, loc.PC) fmt.Fprintf(t.stdout, "> %s() %s:%d (PC: %#v)\n", loc.Function.Name(), t.formatPath(loc.File), loc.Line, loc.PC)
if loc.Function != nil && loc.Function.Optimized { if loc.Function != nil && loc.Function.Optimized {
fmt.Println(optimizedFunctionWarning) fmt.Fprintln(t.stdout, optimizedFunctionWarning)
} }
} }
func printReturnValues(th *api.Thread) { func printReturnValues(t *Term, th *api.Thread) {
if th.ReturnValues == nil { if th.ReturnValues == nil {
return return
} }
fmt.Println("Values returned:") fmt.Fprintln(t.stdout, "Values returned:")
for _, v := range th.ReturnValues { for _, v := range th.ReturnValues {
fmt.Printf("\t%s: %s\n", v.Name, v.MultilineString("\t", "")) fmt.Fprintf(t.stdout, "\t%s: %s\n", v.Name, v.MultilineString("\t", ""))
} }
fmt.Println() fmt.Fprintln(t.stdout)
} }
func printcontextThread(t *Term, th *api.Thread) { func printcontextThread(t *Term, th *api.Thread) {
@ -2516,7 +2524,7 @@ func printcontextThread(t *Term, th *api.Thread) {
if th.Breakpoint == nil { if th.Breakpoint == nil {
printcontextLocation(t, api.Location{PC: th.PC, File: th.File, Line: th.Line, Function: th.Function}) printcontextLocation(t, api.Location{PC: th.PC, File: th.File, Line: th.Line, Function: th.Function})
printReturnValues(th) printReturnValues(t, th)
return return
} }
@ -2553,7 +2561,7 @@ func printcontextThread(t *Term, th *api.Thread) {
} }
if hitCount, ok := th.Breakpoint.HitCount[strconv.Itoa(th.GoroutineID)]; ok { if hitCount, ok := th.Breakpoint.HitCount[strconv.Itoa(th.GoroutineID)]; ok {
fmt.Printf("> %s%s(%s) %s:%d (hits goroutine(%d):%d total:%d) (PC: %#v)\n", fmt.Fprintf(t.stdout, "> %s%s(%s) %s:%d (hits goroutine(%d):%d total:%d) (PC: %#v)\n",
bpname, bpname,
fn.Name(), fn.Name(),
args, args,
@ -2564,7 +2572,7 @@ func printcontextThread(t *Term, th *api.Thread) {
th.Breakpoint.TotalHitCount, th.Breakpoint.TotalHitCount,
th.PC) th.PC)
} else { } else {
fmt.Printf("> %s%s(%s) %s:%d (hits total:%d) (PC: %#v)\n", fmt.Fprintf(t.stdout, "> %s%s(%s) %s:%d (hits total:%d) (PC: %#v)\n",
bpname, bpname,
fn.Name(), fn.Name(),
args, args,
@ -2574,10 +2582,10 @@ func printcontextThread(t *Term, th *api.Thread) {
th.PC) th.PC)
} }
if th.Function != nil && th.Function.Optimized { if th.Function != nil && th.Function.Optimized {
fmt.Println(optimizedFunctionWarning) fmt.Fprintln(t.stdout, optimizedFunctionWarning)
} }
printReturnValues(th) printReturnValues(t, th)
printBreakpointInfo(t, th, false) printBreakpointInfo(t, th, false)
} }
@ -2598,47 +2606,47 @@ func printBreakpointInfo(t *Term, th *api.Thread, tracepointOnNewline bool) {
return return
} }
didprintnl = true didprintnl = true
fmt.Println() fmt.Fprintln(t.stdout)
} }
if bpi.Goroutine != nil { if bpi.Goroutine != nil {
tracepointnl() tracepointnl()
writeGoroutineLong(t, os.Stdout, bpi.Goroutine, "\t") writeGoroutineLong(t, t.stdout, bpi.Goroutine, "\t")
} }
for _, v := range bpi.Variables { for _, v := range bpi.Variables {
tracepointnl() tracepointnl()
fmt.Printf("\t%s: %s\n", v.Name, v.MultilineString("\t", "")) fmt.Fprintf(t.stdout, "\t%s: %s\n", v.Name, v.MultilineString("\t", ""))
} }
for _, v := range bpi.Locals { for _, v := range bpi.Locals {
tracepointnl() tracepointnl()
if *bp.LoadLocals == longLoadConfig { if *bp.LoadLocals == longLoadConfig {
fmt.Printf("\t%s: %s\n", v.Name, v.MultilineString("\t", "")) fmt.Fprintf(t.stdout, "\t%s: %s\n", v.Name, v.MultilineString("\t", ""))
} else { } else {
fmt.Printf("\t%s: %s\n", v.Name, v.SinglelineString()) fmt.Fprintf(t.stdout, "\t%s: %s\n", v.Name, v.SinglelineString())
} }
} }
if bp.LoadArgs != nil && *bp.LoadArgs == longLoadConfig { if bp.LoadArgs != nil && *bp.LoadArgs == longLoadConfig {
for _, v := range bpi.Arguments { for _, v := range bpi.Arguments {
tracepointnl() tracepointnl()
fmt.Printf("\t%s: %s\n", v.Name, v.MultilineString("\t", "")) fmt.Fprintf(t.stdout, "\t%s: %s\n", v.Name, v.MultilineString("\t", ""))
} }
} }
if bpi.Stacktrace != nil { if bpi.Stacktrace != nil {
tracepointnl() tracepointnl()
fmt.Printf("\tStack:\n") fmt.Fprintf(t.stdout, "\tStack:\n")
printStack(t, os.Stdout, bpi.Stacktrace, "\t\t", false) printStack(t, t.stdout, bpi.Stacktrace, "\t\t", false)
} }
} }
func printTracepoint(t *Term, th *api.Thread, bpname string, fn *api.Function, args string, hasReturnValue bool) { func printTracepoint(t *Term, th *api.Thread, bpname string, fn *api.Function, args string, hasReturnValue bool) {
if th.Breakpoint.Tracepoint { if th.Breakpoint.Tracepoint {
fmt.Fprintf(os.Stderr, "> goroutine(%d): %s%s(%s)", th.GoroutineID, bpname, fn.Name(), args) fmt.Fprintf(t.stdout, "> goroutine(%d): %s%s(%s)", th.GoroutineID, bpname, fn.Name(), args)
if !hasReturnValue { if !hasReturnValue {
fmt.Println() fmt.Fprintln(t.stdout)
} }
printBreakpointInfo(t, th, !hasReturnValue) printBreakpointInfo(t, th, !hasReturnValue)
} }
@ -2647,12 +2655,12 @@ func printTracepoint(t *Term, th *api.Thread, bpname string, fn *api.Function, a
for _, v := range th.ReturnValues { for _, v := range th.ReturnValues {
retVals = append(retVals, v.SinglelineString()) retVals = append(retVals, v.SinglelineString())
} }
fmt.Fprintf(os.Stderr, " => (%s)\n", strings.Join(retVals, ",")) fmt.Fprintf(t.stdout, " => (%s)\n", strings.Join(retVals, ","))
} }
if th.Breakpoint.TraceReturn || !hasReturnValue { if th.Breakpoint.TraceReturn || !hasReturnValue {
if th.BreakpointInfo != nil && th.BreakpointInfo.Stacktrace != nil { if th.BreakpointInfo != nil && th.BreakpointInfo.Stacktrace != nil {
fmt.Fprintf(os.Stderr, "\tStack:\n") fmt.Fprintf(t.stdout, "\tStack:\n")
printStack(t, os.Stderr, th.BreakpointInfo.Stacktrace, "\t\t", false) printStack(t, t.stdout, th.BreakpointInfo.Stacktrace, "\t\t", false)
} }
} }
} }
@ -2677,10 +2685,10 @@ func printfile(t *Term, filename string, line int, showArrow bool) error {
fi, _ := file.Stat() fi, _ := file.Stat()
lastModExe := t.client.LastModified() lastModExe := t.client.LastModified()
if fi.ModTime().After(lastModExe) { if fi.ModTime().After(lastModExe) {
fmt.Println("Warning: listing may not match stale executable") fmt.Fprintln(t.stdout, "Warning: listing may not match stale executable")
} }
return colorize.Print(t.stdout, file.Name(), file, line-lineCount, line+lineCount+1, arrowLine, t.colorEscapes) return t.stdout.ColorizePrint(file.Name(), file, line-lineCount, line+lineCount+1, arrowLine)
} }
// ExitRequestError is returned when the user // ExitRequestError is returned when the user
@ -2779,7 +2787,7 @@ func (c *Commands) parseBreakpointAttrs(t *Term, ctx callContext, r io.Reader) e
lineno++ lineno++
err := c.CallWithContext(scan.Text(), t, ctx) err := c.CallWithContext(scan.Text(), t, ctx)
if err != nil { if err != nil {
fmt.Printf("%d: %s\n", lineno, err.Error()) fmt.Fprintf(t.stdout, "%d: %s\n", lineno, err.Error())
} }
} }
return scan.Err() return scan.Err()
@ -2850,7 +2858,7 @@ func (c *Commands) executeFile(t *Term, name string) error {
if _, isExitRequest := err.(ExitRequestError); isExitRequest { if _, isExitRequest := err.(ExitRequestError); isExitRequest {
return err return err
} }
fmt.Printf("%s:%d: %v\n", name, lineno, err) fmt.Fprintf(t.stdout, "%s:%d: %v\n", name, lineno, err)
} }
} }
@ -2889,7 +2897,7 @@ func checkpoint(t *Term, ctx callContext, args string) error {
return err return err
} }
fmt.Printf("Checkpoint c%d created.\n", cpid) fmt.Fprintf(t.stdout, "Checkpoint c%d created.\n", cpid)
return nil return nil
} }
@ -2899,7 +2907,7 @@ func checkpoints(t *Term, ctx callContext, args string) error {
return err return err
} }
w := new(tabwriter.Writer) w := new(tabwriter.Writer)
w.Init(os.Stdout, 4, 4, 2, ' ', 0) w.Init(t.stdout, 4, 4, 2, ' ', 0)
fmt.Fprintln(w, "ID\tWhen\tNote") fmt.Fprintln(w, "ID\tWhen\tNote")
for _, cp := range cps { for _, cp := range cps {
fmt.Fprintf(w, "c%d\t%s\t%s\n", cp.ID, cp.When, cp.Where) fmt.Fprintf(w, "c%d\t%s\t%s\n", cp.ID, cp.When, cp.Where)
@ -2964,26 +2972,77 @@ func dump(t *Term, ctx callContext, args string) error {
} }
for { for {
if dumpState.ThreadsDone != dumpState.ThreadsTotal { if dumpState.ThreadsDone != dumpState.ThreadsTotal {
fmt.Printf("\rDumping threads %d / %d...", dumpState.ThreadsDone, dumpState.ThreadsTotal) fmt.Fprintf(t.stdout, "\rDumping threads %d / %d...", dumpState.ThreadsDone, dumpState.ThreadsTotal)
} else { } else {
fmt.Printf("\rDumping memory %d / %d...", dumpState.MemDone, dumpState.MemTotal) fmt.Fprintf(t.stdout, "\rDumping memory %d / %d...", dumpState.MemDone, dumpState.MemTotal)
} }
if !dumpState.Dumping { if !dumpState.Dumping {
break break
} }
dumpState = t.client.CoreDumpWait(1000) dumpState = t.client.CoreDumpWait(1000)
} }
fmt.Printf("\n") fmt.Fprintf(t.stdout, "\n")
if dumpState.Err != "" { if dumpState.Err != "" {
fmt.Printf("error dumping: %s\n", dumpState.Err) fmt.Fprintf(t.stdout, "error dumping: %s\n", dumpState.Err)
} else if !dumpState.AllDone { } else if !dumpState.AllDone {
fmt.Printf("canceled\n") fmt.Fprintf(t.stdout, "canceled\n")
} else if dumpState.MemDone != dumpState.MemTotal { } else if dumpState.MemDone != dumpState.MemTotal {
fmt.Printf("Core dump could be incomplete\n") fmt.Fprintf(t.stdout, "Core dump could be incomplete\n")
} }
return nil return nil
} }
func transcript(t *Term, ctx callContext, args string) error {
argv := strings.SplitN(args, " ", -1)
truncate := false
fileOnly := false
disable := false
path := ""
for _, arg := range argv {
switch arg {
case "-x":
fileOnly = true
case "-t":
truncate = true
case "-off":
disable = true
default:
if path != "" || strings.HasPrefix(arg, "-") {
return fmt.Errorf("unrecognized option %q", arg)
} else {
path = arg
}
}
}
if disable {
if path != "" {
return errors.New("-o option specified with an output path")
}
return t.stdout.CloseTranscript()
}
if path == "" {
return errors.New("no output path specified")
}
flags := os.O_APPEND | os.O_WRONLY | os.O_CREATE
if truncate {
flags |= os.O_TRUNC
}
fh, err := os.OpenFile(path, flags, 0660)
if err != nil {
return err
}
if err := t.stdout.CloseTranscript(); err != nil {
return err
}
t.stdout.TranscribeTo(fh, fileOnly)
return nil
}
func formatBreakpointName(bp *api.Breakpoint, upcase bool) string { func formatBreakpointName(bp *api.Breakpoint, upcase bool) string {
thing := "breakpoint" thing := "breakpoint"
if bp.Tracepoint { if bp.Tracepoint {

@ -1,6 +1,7 @@
package terminal package terminal
import ( import (
"bytes"
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -52,52 +53,28 @@ type FakeTerminal struct {
const logCommandOutput = false const logCommandOutput = false
func (ft *FakeTerminal) Exec(cmdstr string) (outstr string, err error) { func (ft *FakeTerminal) Exec(cmdstr string) (outstr string, err error) {
outfh, err := ioutil.TempFile("", "cmdtestout") var buf bytes.Buffer
if err != nil { ft.Term.stdout.w = &buf
ft.t.Fatalf("could not create temporary file: %v", err) ft.Term.starlarkEnv.Redirect(ft.Term.stdout)
} err = ft.cmds.Call(cmdstr, ft.Term)
outstr = buf.String()
stdout, stderr, termstdout := os.Stdout, os.Stderr, ft.Term.stdout
os.Stdout, os.Stderr, ft.Term.stdout = outfh, outfh, outfh
defer func() {
os.Stdout, os.Stderr, ft.Term.stdout = stdout, stderr, termstdout
outfh.Close()
outbs, err1 := ioutil.ReadFile(outfh.Name())
if err1 != nil {
ft.t.Fatalf("could not read temporary output file: %v", err)
}
outstr = string(outbs)
if logCommandOutput { if logCommandOutput {
ft.t.Logf("command %q -> %q", cmdstr, outstr) ft.t.Logf("command %q -> %q", cmdstr, outstr)
} }
os.Remove(outfh.Name()) ft.Term.stdout.Flush()
}()
err = ft.cmds.Call(cmdstr, ft.Term)
return return
} }
func (ft *FakeTerminal) ExecStarlark(starlarkProgram string) (outstr string, err error) { func (ft *FakeTerminal) ExecStarlark(starlarkProgram string) (outstr string, err error) {
outfh, err := ioutil.TempFile("", "cmdtestout") var buf bytes.Buffer
if err != nil { ft.Term.stdout.w = &buf
ft.t.Fatalf("could not create temporary file: %v", err) ft.Term.starlarkEnv.Redirect(ft.Term.stdout)
} _, err = ft.Term.starlarkEnv.Execute("<stdin>", starlarkProgram, "main", nil)
outstr = buf.String()
stdout, stderr, termstdout := os.Stdout, os.Stderr, ft.Term.stdout
os.Stdout, os.Stderr, ft.Term.stdout = outfh, outfh, outfh
defer func() {
os.Stdout, os.Stderr, ft.Term.stdout = stdout, stderr, termstdout
outfh.Close()
outbs, err1 := ioutil.ReadFile(outfh.Name())
if err1 != nil {
ft.t.Fatalf("could not read temporary output file: %v", err)
}
outstr = string(outbs)
if logCommandOutput { if logCommandOutput {
ft.t.Logf("command %q -> %q", starlarkProgram, outstr) ft.t.Logf("command %q -> %q", starlarkProgram, outstr)
} }
os.Remove(outfh.Name()) ft.Term.stdout.Flush()
}()
_, err = ft.Term.starlarkEnv.Execute("<stdin>", starlarkProgram, "main", nil)
return return
} }
@ -1269,3 +1246,48 @@ func TestBreakpointEditing(t *testing.T) {
} }
} }
} }
func TestTranscript(t *testing.T) {
withTestTerminal("math", t, func(term *FakeTerminal) {
term.MustExec("break main.main")
out := term.MustExec("continue")
if !strings.HasPrefix(out, "> main.main()") {
t.Fatalf("Wrong output for next: <%s>", out)
}
fh, err := ioutil.TempFile("", "test-transcript-*")
if err != nil {
t.Fatalf("TempFile: %v", err)
}
name := fh.Name()
fh.Close()
t.Logf("output to %q", name)
slurp := func() string {
b, err := ioutil.ReadFile(name)
if err != nil {
t.Fatalf("could not read transcript file: %v", err)
}
return string(b)
}
term.MustExec(fmt.Sprintf("transcript %s", name))
out = term.MustExec("list")
//term.MustExec("transcript -off")
if out != slurp() {
t.Logf("output of list %s", out)
t.Logf("contents of transcript: %s", slurp())
t.Errorf("transcript and command out differ")
}
term.MustExec(fmt.Sprintf("transcript -t -x %s", name))
out = term.MustExec(`print "hello"`)
if out != "" {
t.Errorf("output of print is %q but should have been suppressed by transcript", out)
}
if slurp() != "\"hello\"\n" {
t.Errorf("wrong contents of transcript: %q", slurp())
}
os.Remove(name)
})
}

@ -58,14 +58,14 @@ func (env *Env) REPL() error {
if err := isCancelled(thread); err != nil { if err := isCancelled(thread); err != nil {
return err return err
} }
if err := rep(rl, thread, globals); err != nil { if err := rep(rl, thread, globals, env.out); err != nil {
if err == io.EOF { if err == io.EOF {
break break
} }
return err return err
} }
} }
fmt.Println() fmt.Fprintln(env.out)
return env.exportGlobals(globals) return env.exportGlobals(globals)
} }
@ -80,12 +80,14 @@ const (
// //
// It returns an error (possibly readline.ErrInterrupt) // It returns an error (possibly readline.ErrInterrupt)
// only if readline failed. Starlark errors are printed. // only if readline failed. Starlark errors are printed.
func rep(rl *liner.State, thread *starlark.Thread, globals starlark.StringDict) error { func rep(rl *liner.State, thread *starlark.Thread, globals starlark.StringDict, out EchoWriter) error {
defer out.Flush()
eof := false eof := false
prompt := normalPrompt prompt := normalPrompt
readline := func() ([]byte, error) { readline := func() ([]byte, error) {
line, err := rl.Prompt(prompt) line, err := rl.Prompt(prompt)
out.Echo(prompt + line)
if line == exitCommand { if line == exitCommand {
eof = true eof = true
return nil, io.EOF return nil, io.EOF
@ -122,7 +124,7 @@ func rep(rl *liner.State, thread *starlark.Thread, globals starlark.StringDict)
// print // print
if v != starlark.None { if v != starlark.None {
fmt.Println(v) fmt.Fprintln(out, v)
} }
} else { } else {
// compile // compile

@ -3,6 +3,7 @@ package starbind
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"runtime" "runtime"
"strings" "strings"
@ -56,13 +57,15 @@ type Env struct {
cancelfn context.CancelFunc cancelfn context.CancelFunc
ctx Context ctx Context
out EchoWriter
} }
// New creates a new starlark binding environment. // New creates a new starlark binding environment.
func New(ctx Context) *Env { func New(ctx Context, out EchoWriter) *Env {
env := &Env{} env := &Env{}
env.ctx = ctx env.ctx = ctx
env.out = out
env.env = env.starlarkPredeclare() env.env = env.starlarkPredeclare()
env.env[dlvCommandBuiltinName] = starlark.NewBuiltin(dlvCommandBuiltinName, func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { env.env[dlvCommandBuiltinName] = starlark.NewBuiltin(dlvCommandBuiltinName, func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
@ -117,6 +120,18 @@ func New(ctx Context) *Env {
return env return env
} }
// Redirect redirects starlark output to out.
func (env *Env) Redirect(out EchoWriter) {
env.out = out
if env.thread != nil {
env.thread.Print = env.printFunc()
}
}
func (env *Env) printFunc() func(_ *starlark.Thread, msg string) {
return func(_ *starlark.Thread, msg string) { fmt.Fprintln(env.out, msg) }
}
// Execute executes a script. Path is the name of the file to execute and // Execute executes a script. Path is the name of the file to execute and
// source is the source code to execute. // source is the source code to execute.
// Source can be either a []byte, a string or a io.Reader. If source is nil // Source can be either a []byte, a string or a io.Reader. If source is nil
@ -128,7 +143,7 @@ func (env *Env) Execute(path string, source interface{}, mainFnName string, args
if err == nil { if err == nil {
return return
} }
fmt.Printf("panic executing starlark script: %v\n", err) fmt.Fprintf(env.out, "panic executing starlark script: %v\n", err)
for i := 0; ; i++ { for i := 0; ; i++ {
pc, file, line, ok := runtime.Caller(i) pc, file, line, ok := runtime.Caller(i)
if !ok { if !ok {
@ -139,7 +154,7 @@ func (env *Env) Execute(path string, source interface{}, mainFnName string, args
if fn != nil { if fn != nil {
fname = fn.Name() fname = fn.Name()
} }
fmt.Printf("%s\n\tin %s:%d\n", fname, file, line) fmt.Fprintf(env.out, "%s\n\tin %s:%d\n", fname, file, line)
} }
}() }()
@ -193,7 +208,7 @@ func (env *Env) Cancel() {
func (env *Env) newThread() *starlark.Thread { func (env *Env) newThread() *starlark.Thread {
thread := &starlark.Thread{ thread := &starlark.Thread{
Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) }, Print: env.printFunc(),
} }
env.contextMu.Lock() env.contextMu.Lock()
var ctx context.Context var ctx context.Context
@ -287,3 +302,9 @@ func decorateError(thread *starlark.Thread, err error) error {
} }
return fmt.Errorf("%s:%d: %v", pos.Filename(), pos.Line, err) return fmt.Errorf("%s:%d: %v", pos.Filename(), pos.Line, err)
} }
type EchoWriter interface {
io.Writer
Echo(string)
Flush()
}

@ -3,6 +3,7 @@ package terminal
//lint:file-ignore ST1005 errors here can be capitalized //lint:file-ignore ST1005 errors here can be capitalized
import ( import (
"bufio"
"fmt" "fmt"
"io" "io"
"net/rpc" "net/rpc"
@ -55,10 +56,9 @@ type Term struct {
prompt string prompt string
line *liner.State line *liner.State
cmds *Commands cmds *Commands
stdout io.Writer stdout *transcriptWriter
InitFile string InitFile string
displays []displayEntry displays []displayEntry
colorEscapes map[colorize.Style]string
historyFile *os.File historyFile *os.File
@ -99,34 +99,34 @@ func New(client service.Client, conf *config.Config) *Term {
prompt: "(dlv) ", prompt: "(dlv) ",
line: liner.NewLiner(), line: liner.NewLiner(),
cmds: cmds, cmds: cmds,
stdout: os.Stdout, stdout: &transcriptWriter{w: os.Stdout},
} }
if strings.ToLower(os.Getenv("TERM")) != "dumb" { if strings.ToLower(os.Getenv("TERM")) != "dumb" {
t.stdout = getColorableWriter() t.stdout.w = getColorableWriter()
t.colorEscapes = make(map[colorize.Style]string) t.stdout.colorEscapes = make(map[colorize.Style]string)
t.colorEscapes[colorize.NormalStyle] = terminalResetEscapeCode t.stdout.colorEscapes[colorize.NormalStyle] = terminalResetEscapeCode
wd := func(s string, defaultCode int) string { wd := func(s string, defaultCode int) string {
if s == "" { if s == "" {
return fmt.Sprintf(terminalHighlightEscapeCode, defaultCode) return fmt.Sprintf(terminalHighlightEscapeCode, defaultCode)
} }
return s return s
} }
t.colorEscapes[colorize.KeywordStyle] = conf.SourceListKeywordColor t.stdout.colorEscapes[colorize.KeywordStyle] = conf.SourceListKeywordColor
t.colorEscapes[colorize.StringStyle] = wd(conf.SourceListStringColor, ansiGreen) t.stdout.colorEscapes[colorize.StringStyle] = wd(conf.SourceListStringColor, ansiGreen)
t.colorEscapes[colorize.NumberStyle] = conf.SourceListNumberColor t.stdout.colorEscapes[colorize.NumberStyle] = conf.SourceListNumberColor
t.colorEscapes[colorize.CommentStyle] = wd(conf.SourceListCommentColor, ansiBrMagenta) t.stdout.colorEscapes[colorize.CommentStyle] = wd(conf.SourceListCommentColor, ansiBrMagenta)
t.colorEscapes[colorize.ArrowStyle] = wd(conf.SourceListArrowColor, ansiYellow) t.stdout.colorEscapes[colorize.ArrowStyle] = wd(conf.SourceListArrowColor, ansiYellow)
switch x := conf.SourceListLineColor.(type) { switch x := conf.SourceListLineColor.(type) {
case string: case string:
t.colorEscapes[colorize.LineNoStyle] = x t.stdout.colorEscapes[colorize.LineNoStyle] = x
case int: case int:
if (x > ansiWhite && x < ansiBrBlack) || x < ansiBlack || x > ansiBrWhite { if (x > ansiWhite && x < ansiBrBlack) || x < ansiBlack || x > ansiBrWhite {
x = ansiBlue x = ansiBlue
} }
t.colorEscapes[colorize.LineNoStyle] = fmt.Sprintf(terminalHighlightEscapeCode, x) t.stdout.colorEscapes[colorize.LineNoStyle] = fmt.Sprintf(terminalHighlightEscapeCode, x)
case nil: case nil:
t.colorEscapes[colorize.LineNoStyle] = fmt.Sprintf(terminalHighlightEscapeCode, ansiBlue) t.stdout.colorEscapes[colorize.LineNoStyle] = fmt.Sprintf(terminalHighlightEscapeCode, ansiBlue)
} }
} }
@ -135,13 +135,16 @@ func New(client service.Client, conf *config.Config) *Term {
client.SetReturnValuesLoadConfig(&lcfg) client.SetReturnValuesLoadConfig(&lcfg)
} }
t.starlarkEnv = starbind.New(starlarkContext{t}) t.starlarkEnv = starbind.New(starlarkContext{t}, t.stdout)
return t return t
} }
// Close returns the terminal to its previous mode. // Close returns the terminal to its previous mode.
func (t *Term) Close() { func (t *Term) Close() {
t.line.Close() t.line.Close()
if err := t.stdout.CloseTranscript(); err != nil {
fmt.Fprintf(os.Stderr, "error closing transcript file: %v\n", err)
}
} }
func (t *Term) sigintGuard(ch <-chan os.Signal, multiClient bool) { func (t *Term) sigintGuard(ch <-chan os.Signal, multiClient bool) {
@ -150,7 +153,7 @@ func (t *Term) sigintGuard(ch <-chan os.Signal, multiClient bool) {
t.starlarkEnv.Cancel() t.starlarkEnv.Cancel()
state, err := t.client.GetStateNonBlocking() state, err := t.client.GetStateNonBlocking()
if err == nil && state.Recording { if err == nil && state.Recording {
fmt.Printf("received SIGINT, stopping recording (will not forward signal)\n") fmt.Fprintf(t.stdout, "received SIGINT, stopping recording (will not forward signal)\n")
err := t.client.StopRecording() err := t.client.StopRecording()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "%v\n", err)
@ -158,7 +161,7 @@ func (t *Term) sigintGuard(ch <-chan os.Signal, multiClient bool) {
continue continue
} }
if err == nil && state.CoreDumping { if err == nil && state.CoreDumping {
fmt.Printf("received SIGINT, stopping dump\n") fmt.Fprintf(t.stdout, "received SIGINT, stopping dump\n")
err := t.client.CoreDumpCancel() err := t.client.CoreDumpCancel()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "%v\n", err)
@ -189,14 +192,14 @@ func (t *Term) sigintGuard(ch <-chan os.Signal, multiClient bool) {
t.Close() t.Close()
} }
default: default:
fmt.Println("only p or q allowed") fmt.Fprintln(t.stdout, "only p or q allowed")
} }
} else { } else {
fmt.Printf("received SIGINT, stopping process (will not forward signal)\n") fmt.Fprintf(t.stdout, "received SIGINT, stopping process (will not forward signal)\n")
_, err := t.client.Halt() _, err := t.client.Halt()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%v", err) fmt.Fprintf(t.stdout, "%v", err)
} }
} }
} }
@ -278,11 +281,12 @@ func (t *Term) Run() (int, error) {
cmdstr, err := t.promptForInput() cmdstr, err := t.promptForInput()
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
fmt.Println("exit") fmt.Fprintln(t.stdout, "exit")
return t.handleExit() return t.handleExit()
} }
return 1, fmt.Errorf("Prompt for input failed.\n") return 1, fmt.Errorf("Prompt for input failed.\n")
} }
t.stdout.Echo(t.prompt + cmdstr + "\n")
if strings.TrimSpace(cmdstr) == "" { if strings.TrimSpace(cmdstr) == "" {
cmdstr = lastCmd cmdstr = lastCmd
@ -309,6 +313,8 @@ func (t *Term) Run() (int, error) {
fmt.Fprintf(os.Stderr, "Command failed: %s\n", err) fmt.Fprintf(os.Stderr, "Command failed: %s\n", err)
} }
} }
t.stdout.Flush()
} }
} }
@ -497,10 +503,10 @@ func (t *Term) printDisplay(i int) {
if isErrProcessExited(err) { if isErrProcessExited(err) {
return return
} }
fmt.Printf("%d: %s = error %v\n", i, expr, err) fmt.Fprintf(t.stdout, "%d: %s = error %v\n", i, expr, err)
return return
} }
fmt.Printf("%d: %s = %s\n", i, val.Name, val.SinglelineStringFormatted(fmtstr)) fmt.Fprintf(t.stdout, "%d: %s = %s\n", i, val.Name, val.SinglelineStringFormatted(fmtstr))
} }
func (t *Term) printDisplays() { func (t *Term) printDisplays() {
@ -533,8 +539,90 @@ func (t *Term) longCommandCanceled() bool {
return t.longCommandCancelFlag return t.longCommandCancelFlag
} }
// RedirectTo redirects the output of this terminal to the specified writer.
func (t *Term) RedirectTo(w io.Writer) {
t.stdout.w = w
}
// isErrProcessExited returns true if `err` is an RPC error equivalent of proc.ErrProcessExited // isErrProcessExited returns true if `err` is an RPC error equivalent of proc.ErrProcessExited
func isErrProcessExited(err error) bool { func isErrProcessExited(err error) bool {
rpcError, ok := err.(rpc.ServerError) rpcError, ok := err.(rpc.ServerError)
return ok && strings.Contains(rpcError.Error(), "has exited with status") 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
}