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.
[source](#source) | Executes a file containing a list of delve commands
[sources](#sources) | Print list of source files.
[transcript](#transcript) | Appends command output to a file.
[types](#types) | Print list of types
## args
@ -636,6 +637,17 @@ See also: "help on", "help cond" and "help clear"
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
Print list of types

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

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

@ -28,7 +28,6 @@ import (
"github.com/cosiner/argv"
"github.com/go-delve/delve/pkg/config"
"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/api"
"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>
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
@ -674,7 +682,7 @@ func (c *Commands) help(t *Term, ctx callContext, args string) error {
for _, cmd := range c.cmds {
for _, alias := range cmd.aliases {
if alias == args {
fmt.Println(cmd.helpMsg)
fmt.Fprintln(t.stdout, cmd.helpMsg)
return nil
}
}
@ -682,12 +690,12 @@ func (c *Commands) help(t *Term, ctx callContext, args string) error {
return errNoCmd
}
fmt.Println("The following commands are available:")
fmt.Fprintln(t.stdout, "The following commands are available:")
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.Init(os.Stdout, 0, 8, 0, '-', 0)
w.Init(t.stdout, 0, 8, 0, '-', 0)
for _, cmd := range c.cmds {
if cmd.group != cgd.group {
continue
@ -707,8 +715,8 @@ func (c *Commands) help(t *Term, ctx callContext, args string) error {
}
}
fmt.Println()
fmt.Println("Type help followed by a command for full documentation.")
fmt.Fprintln(t.stdout)
fmt.Fprintln(t.stdout, "Type help followed by a command for full documentation.")
return nil
}
@ -734,11 +742,11 @@ func threads(t *Term, ctx callContext, args string) error {
prefix = "* "
}
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),
th.Line, th.Function.Name())
} else {
fmt.Printf("%sThread %s\n", prefix, t.formatThread(th))
fmt.Fprintf(t.stdout, "%sThread %s\n", prefix, t.formatThread(th))
}
}
return nil
@ -769,7 +777,7 @@ func thread(t *Term, ctx callContext, args string) error {
if newState.CurrentThread != nil {
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
}
@ -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 {
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 {
writeGoroutineLabels(os.Stdout, g, indent+"\t")
writeGoroutineLabels(t.stdout, g, indent+"\t")
}
if flags&api.PrintGoroutinesStack != 0 {
stack, err := t.client.Stacktrace(g.ID, depth, 0, nil)
if err != nil {
return err
}
printStack(t, os.Stdout, stack, indent+"\t", false)
printStack(t, t.stdout, stack, indent+"\t", false)
}
}
return nil
@ -820,7 +828,7 @@ func goroutines(t *Term, ctx callContext, argstr string) error {
t.longCommandStart()
for start >= 0 {
if t.longCommandCanceled() {
fmt.Printf("interrupted\n")
fmt.Fprintf(t.stdout, "interrupted\n")
return nil
}
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 {
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)
if err != nil {
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 {
fmt.Printf("\n")
fmt.Fprintf(t.stdout, "\n")
}
}
if tooManyGroups {
fmt.Printf("Too many groups\n")
fmt.Fprintf(t.stdout, "Too many groups\n")
}
} else {
sort.Sort(byGoroutineID(gs))
@ -852,7 +860,7 @@ func goroutines(t *Term, ctx callContext, argstr string) error {
}
}
if gslen > 0 {
fmt.Printf("[%d goroutines]\n", gslen)
fmt.Fprintf(t.stdout, "[%d goroutines]\n", gslen)
}
return nil
}
@ -893,7 +901,7 @@ func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error {
return err
}
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
}
@ -950,7 +958,7 @@ func (c *Commands) frameCommand(t *Term, ctx callContext, argstr string, directi
}
printcontext(t, state)
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)
return nil
}
@ -980,9 +988,9 @@ func printscope(t *Term) error {
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 {
writeGoroutineLong(t, os.Stdout, state.SelectedGoroutine, "")
writeGoroutineLong(t, t.stdout, state.SelectedGoroutine, "")
}
return nil
}
@ -1182,7 +1190,7 @@ func restartLive(t *Term, ctx callContext, args string) error {
return err
}
fmt.Println("Process restarted with PID", t.client.ProcessPid())
fmt.Fprintln(t.stdout, "Process restarted with PID", t.client.ProcessPid())
return nil
}
@ -1192,7 +1200,7 @@ func restartIntl(t *Term, rerecord bool, restartPos string, resetArgs bool, newA
return err
}
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
}
@ -1276,7 +1284,7 @@ func (c *Commands) rebuild(t *Term, ctx callContext, args string) error {
defer t.onStop()
discarded, err := t.client.Restart(true)
if len(discarded) > 0 {
fmt.Printf("not all breakpoints could be restored.")
fmt.Fprintf(t.stdout, "not all breakpoints could be restored.")
}
return err
}
@ -1292,7 +1300,7 @@ func (c *Commands) cont(t *Term, ctx callContext, args string) error {
defer func() {
for _, bp := range tmp {
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
for {
fmt.Printf("\tbreakpoint hit during %s", op)
fmt.Fprintf(t.stdout, "\tbreakpoint hit during %s", op)
if !skipBreakpoints {
fmt.Printf("\n")
fmt.Fprintf(t.stdout, "\n")
answer, err := promptAutoContinue(t, op)
switch answer {
case "f": // finish next
skipBreakpoints = true
fallthrough
case "c": // continue once
fmt.Printf("continuing...\n")
fmt.Fprintf(t.stdout, "continuing...\n")
case "s": // stop and cancel
fallthrough
default:
@ -1343,7 +1351,7 @@ func continueUntilCompleteNext(t *Term, state *api.DebuggerState, op string, sho
return err
}
} else {
fmt.Printf(", continuing...\n")
fmt.Fprintf(t.stdout, ", continuing...\n")
}
stateChan := t.client.DirectionCongruentContinue()
var state *api.DebuggerState
@ -1542,7 +1550,7 @@ func clear(t *Term, ctx callContext, args string) error {
if err != nil {
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
}
@ -1580,9 +1588,9 @@ func clearAll(t *Term, ctx callContext, args string) error {
_, err := t.client.ClearBreakpoint(bp.ID)
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
}
@ -1601,7 +1609,7 @@ func toggle(t *Term, ctx callContext, args string) error {
if err != nil {
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
}
@ -1623,12 +1631,12 @@ func breakpoints(t *Term, ctx callContext, args string) error {
if bp.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)
if len(attrs) > 0 {
fmt.Printf("%s\n", strings.Join(attrs, "\n"))
fmt.Fprintf(t.stdout, "%s\n", strings.Join(attrs, "\n"))
}
}
return nil
@ -1726,7 +1734,7 @@ func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) ([]
}
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
@ -1825,7 +1833,7 @@ func watchpoint(t *Term, ctx callContext, args string) error {
if err != nil {
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
}
@ -1955,7 +1963,7 @@ loop:
if err != nil {
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
}
@ -1984,7 +1992,7 @@ func printVar(t *Term, ctx callContext, args string) error {
return err
}
fmt.Println(val.MultilineString("", fmtstr))
fmt.Fprintln(t.stdout, val.MultilineString("", fmtstr))
return nil
}
@ -1997,20 +2005,20 @@ func whatisCommand(t *Term, ctx callContext, args string) error {
return err
}
if val.Flags&api.VariableCPURegister != 0 {
fmt.Println("CPU Register")
fmt.Fprintln(t.stdout, "CPU Register")
return nil
}
if val.Type != "" {
fmt.Println(val.Type)
fmt.Fprintln(t.stdout, 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 {
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 != "" {
fmt.Printf("location: %s\n", val.LocationExpr)
fmt.Fprintf(t.stdout, "location: %s\n", val.LocationExpr)
}
return nil
}
@ -2032,7 +2040,7 @@ func setVar(t *Term, ctx callContext, args string) error {
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)
if err != nil {
return err
@ -2046,39 +2054,39 @@ func printFilteredVariables(varType string, vars []api.Variable, filter string,
name = "(" + name + ")"
}
if cfg == ShortLoadConfig {
fmt.Printf("%s = %s\n", name, v.SinglelineString())
fmt.Fprintf(t.stdout, "%s = %s\n", name, v.SinglelineString())
} else {
fmt.Printf("%s = %s\n", name, v.MultilineString("", ""))
fmt.Fprintf(t.stdout, "%s = %s\n", name, v.MultilineString("", ""))
}
}
}
if !match {
fmt.Printf("(no %s)\n", varType)
fmt.Fprintf(t.stdout, "(no %s)\n", varType)
}
return nil
}
func printSortedStrings(v []string, err error) error {
func (t *Term) printSortedStrings(v []string, err error) error {
if err != nil {
return err
}
sort.Strings(v)
for _, d := range v {
fmt.Println(d)
fmt.Fprintln(t.stdout, d)
}
return nil
}
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 {
return printSortedStrings(t.client.ListFunctions(args))
return t.printSortedStrings(t.client.ListFunctions(args))
}
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) {
@ -2105,7 +2113,7 @@ func args(t *Term, ctx callContext, args string) error {
if err != nil {
return err
}
return printFilteredVariables("args", vars, filter, cfg)
return t.printFilteredVariables("args", vars, filter, cfg)
}
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 {
return err
}
return printFilteredVariables("locals", locals, filter, cfg)
return t.printFilteredVariables("locals", locals, filter, cfg)
}
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 {
return err
}
return printFilteredVariables("vars", vars, filter, cfg)
return t.printFilteredVariables("vars", vars, filter, cfg)
}
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 {
return err
}
fmt.Println(regs)
fmt.Fprintln(t.stdout, regs)
return nil
}
@ -2169,19 +2177,19 @@ func stackCommand(t *Term, ctx callContext, args string) error {
if err != nil {
return err
}
printStack(t, os.Stdout, stack, "", sa.offsets)
printStack(t, t.stdout, stack, "", sa.offsets)
if sa.ancestors > 0 {
ancestors, err := t.client.Ancestors(ctx.Scope.GoroutineID, sa.ancestors, sa.ancestorDepth)
if err != nil {
return err
}
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 != "" {
fmt.Printf("\t%s\n", ancestor.Unreadable)
fmt.Fprintf(t.stdout, "\t%s\n", ancestor.Unreadable)
continue
}
printStack(t, os.Stdout, ancestor.Stack, "\t", false)
printStack(t, t.stdout, ancestor.Stack, "\t", false)
}
}
return nil
@ -2305,7 +2313,7 @@ func getLocation(t *Term, ctx callContext, args string, showContext bool) (file
}
}
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
@ -2319,7 +2327,7 @@ func getLocation(t *Term, ctx callContext, args string, showContext bool) (file
}
loc := locs[0]
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
}
@ -2417,7 +2425,7 @@ func disassCommand(t *Term, ctx callContext, args string) error {
return disasmErr
}
disasmPrint(disasm, os.Stdout)
disasmPrint(disasm, t.stdout)
return nil
}
@ -2429,7 +2437,7 @@ func libraries(t *Term, ctx callContext, args string) error {
}
d := digits(len(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
}
@ -2456,7 +2464,7 @@ func printcontext(t *Term, state *api.DebuggerState) {
}
if state.CurrentThread == nil {
fmt.Println("No current thread available")
fmt.Fprintln(t.stdout, "No current thread available")
return
}
@ -2477,38 +2485,38 @@ func printcontext(t *Term, state *api.DebuggerState) {
}
if th.File == "" {
fmt.Printf("Stopped at: 0x%x\n", state.CurrentThread.PC)
_ = colorize.Print(t.stdout, "", bytes.NewReader([]byte("no source available")), 1, 10, 1, nil)
fmt.Fprintf(t.stdout, "Stopped at: 0x%x\n", state.CurrentThread.PC)
t.stdout.ColorizePrint("", bytes.NewReader([]byte("no source available")), 1, 10, 1)
return
}
printcontextThread(t, th)
if state.When != "" {
fmt.Println(state.When)
fmt.Fprintln(t.stdout, state.When)
}
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) {
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 {
fmt.Println(optimizedFunctionWarning)
fmt.Fprintln(t.stdout, optimizedFunctionWarning)
}
}
func printReturnValues(th *api.Thread) {
func printReturnValues(t *Term, th *api.Thread) {
if th.ReturnValues == nil {
return
}
fmt.Println("Values returned:")
fmt.Fprintln(t.stdout, "Values returned:")
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) {
@ -2516,7 +2524,7 @@ func printcontextThread(t *Term, th *api.Thread) {
if th.Breakpoint == nil {
printcontextLocation(t, api.Location{PC: th.PC, File: th.File, Line: th.Line, Function: th.Function})
printReturnValues(th)
printReturnValues(t, th)
return
}
@ -2553,7 +2561,7 @@ func printcontextThread(t *Term, th *api.Thread) {
}
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,
fn.Name(),
args,
@ -2564,7 +2572,7 @@ func printcontextThread(t *Term, th *api.Thread) {
th.Breakpoint.TotalHitCount,
th.PC)
} 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,
fn.Name(),
args,
@ -2574,10 +2582,10 @@ func printcontextThread(t *Term, th *api.Thread) {
th.PC)
}
if th.Function != nil && th.Function.Optimized {
fmt.Println(optimizedFunctionWarning)
fmt.Fprintln(t.stdout, optimizedFunctionWarning)
}
printReturnValues(th)
printReturnValues(t, th)
printBreakpointInfo(t, th, false)
}
@ -2598,47 +2606,47 @@ func printBreakpointInfo(t *Term, th *api.Thread, tracepointOnNewline bool) {
return
}
didprintnl = true
fmt.Println()
fmt.Fprintln(t.stdout)
}
if bpi.Goroutine != nil {
tracepointnl()
writeGoroutineLong(t, os.Stdout, bpi.Goroutine, "\t")
writeGoroutineLong(t, t.stdout, bpi.Goroutine, "\t")
}
for _, v := range bpi.Variables {
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 {
tracepointnl()
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 {
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 {
for _, v := range bpi.Arguments {
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 {
tracepointnl()
fmt.Printf("\tStack:\n")
printStack(t, os.Stdout, bpi.Stacktrace, "\t\t", false)
fmt.Fprintf(t.stdout, "\tStack:\n")
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) {
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 {
fmt.Println()
fmt.Fprintln(t.stdout)
}
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 {
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.BreakpointInfo != nil && th.BreakpointInfo.Stacktrace != nil {
fmt.Fprintf(os.Stderr, "\tStack:\n")
printStack(t, os.Stderr, th.BreakpointInfo.Stacktrace, "\t\t", false)
fmt.Fprintf(t.stdout, "\tStack:\n")
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()
lastModExe := t.client.LastModified()
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
@ -2779,7 +2787,7 @@ func (c *Commands) parseBreakpointAttrs(t *Term, ctx callContext, r io.Reader) e
lineno++
err := c.CallWithContext(scan.Text(), t, ctx)
if err != nil {
fmt.Printf("%d: %s\n", lineno, err.Error())
fmt.Fprintf(t.stdout, "%d: %s\n", lineno, err.Error())
}
}
return scan.Err()
@ -2850,7 +2858,7 @@ func (c *Commands) executeFile(t *Term, name string) error {
if _, isExitRequest := err.(ExitRequestError); isExitRequest {
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
}
fmt.Printf("Checkpoint c%d created.\n", cpid)
fmt.Fprintf(t.stdout, "Checkpoint c%d created.\n", cpid)
return nil
}
@ -2899,7 +2907,7 @@ func checkpoints(t *Term, ctx callContext, args string) error {
return err
}
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")
for _, cp := range cps {
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 {
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 {
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 {
break
}
dumpState = t.client.CoreDumpWait(1000)
}
fmt.Printf("\n")
fmt.Fprintf(t.stdout, "\n")
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 {
fmt.Printf("canceled\n")
fmt.Fprintf(t.stdout, "canceled\n")
} 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
}
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 {
thing := "breakpoint"
if bp.Tracepoint {

@ -1,6 +1,7 @@
package terminal
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
@ -52,52 +53,28 @@ type FakeTerminal struct {
const logCommandOutput = false
func (ft *FakeTerminal) Exec(cmdstr string) (outstr string, err error) {
outfh, err := ioutil.TempFile("", "cmdtestout")
if err != nil {
ft.t.Fatalf("could not create temporary file: %v", err)
}
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 {
ft.t.Logf("command %q -> %q", cmdstr, outstr)
}
os.Remove(outfh.Name())
}()
var buf bytes.Buffer
ft.Term.stdout.w = &buf
ft.Term.starlarkEnv.Redirect(ft.Term.stdout)
err = ft.cmds.Call(cmdstr, ft.Term)
outstr = buf.String()
if logCommandOutput {
ft.t.Logf("command %q -> %q", cmdstr, outstr)
}
ft.Term.stdout.Flush()
return
}
func (ft *FakeTerminal) ExecStarlark(starlarkProgram string) (outstr string, err error) {
outfh, err := ioutil.TempFile("", "cmdtestout")
if err != nil {
ft.t.Fatalf("could not create temporary file: %v", err)
}
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 {
ft.t.Logf("command %q -> %q", starlarkProgram, outstr)
}
os.Remove(outfh.Name())
}()
var buf bytes.Buffer
ft.Term.stdout.w = &buf
ft.Term.starlarkEnv.Redirect(ft.Term.stdout)
_, err = ft.Term.starlarkEnv.Execute("<stdin>", starlarkProgram, "main", nil)
outstr = buf.String()
if logCommandOutput {
ft.t.Logf("command %q -> %q", starlarkProgram, outstr)
}
ft.Term.stdout.Flush()
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 {
return err
}
if err := rep(rl, thread, globals); err != nil {
if err := rep(rl, thread, globals, env.out); err != nil {
if err == io.EOF {
break
}
return err
}
}
fmt.Println()
fmt.Fprintln(env.out)
return env.exportGlobals(globals)
}
@ -80,12 +80,14 @@ const (
//
// It returns an error (possibly readline.ErrInterrupt)
// 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
prompt := normalPrompt
readline := func() ([]byte, error) {
line, err := rl.Prompt(prompt)
out.Echo(prompt + line)
if line == exitCommand {
eof = true
return nil, io.EOF
@ -122,7 +124,7 @@ func rep(rl *liner.State, thread *starlark.Thread, globals starlark.StringDict)
// print
if v != starlark.None {
fmt.Println(v)
fmt.Fprintln(out, v)
}
} else {
// compile

@ -3,6 +3,7 @@ package starbind
import (
"context"
"fmt"
"io"
"io/ioutil"
"runtime"
"strings"
@ -56,13 +57,15 @@ type Env struct {
cancelfn context.CancelFunc
ctx Context
out EchoWriter
}
// New creates a new starlark binding environment.
func New(ctx Context) *Env {
func New(ctx Context, out EchoWriter) *Env {
env := &Env{}
env.ctx = ctx
env.out = out
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) {
@ -117,6 +120,18 @@ func New(ctx Context) *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
// source is the source code to execute.
// 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 {
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++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
@ -139,7 +154,7 @@ func (env *Env) Execute(path string, source interface{}, mainFnName string, args
if fn != nil {
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 {
thread := &starlark.Thread{
Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
Print: env.printFunc(),
}
env.contextMu.Lock()
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)
}
type EchoWriter interface {
io.Writer
Echo(string)
Flush()
}

@ -3,6 +3,7 @@ package terminal
//lint:file-ignore ST1005 errors here can be capitalized
import (
"bufio"
"fmt"
"io"
"net/rpc"
@ -50,15 +51,14 @@ const (
// Term represents the terminal running dlv.
type Term struct {
client service.Client
conf *config.Config
prompt string
line *liner.State
cmds *Commands
stdout io.Writer
InitFile string
displays []displayEntry
colorEscapes map[colorize.Style]string
client service.Client
conf *config.Config
prompt string
line *liner.State
cmds *Commands
stdout *transcriptWriter
InitFile string
displays []displayEntry
historyFile *os.File
@ -99,34 +99,34 @@ func New(client service.Client, conf *config.Config) *Term {
prompt: "(dlv) ",
line: liner.NewLiner(),
cmds: cmds,
stdout: os.Stdout,
stdout: &transcriptWriter{w: os.Stdout},
}
if strings.ToLower(os.Getenv("TERM")) != "dumb" {
t.stdout = getColorableWriter()
t.colorEscapes = make(map[colorize.Style]string)
t.colorEscapes[colorize.NormalStyle] = terminalResetEscapeCode
t.stdout.w = getColorableWriter()
t.stdout.colorEscapes = make(map[colorize.Style]string)
t.stdout.colorEscapes[colorize.NormalStyle] = terminalResetEscapeCode
wd := func(s string, defaultCode int) string {
if s == "" {
return fmt.Sprintf(terminalHighlightEscapeCode, defaultCode)
}
return s
}
t.colorEscapes[colorize.KeywordStyle] = conf.SourceListKeywordColor
t.colorEscapes[colorize.StringStyle] = wd(conf.SourceListStringColor, ansiGreen)
t.colorEscapes[colorize.NumberStyle] = conf.SourceListNumberColor
t.colorEscapes[colorize.CommentStyle] = wd(conf.SourceListCommentColor, ansiBrMagenta)
t.colorEscapes[colorize.ArrowStyle] = wd(conf.SourceListArrowColor, ansiYellow)
t.stdout.colorEscapes[colorize.KeywordStyle] = conf.SourceListKeywordColor
t.stdout.colorEscapes[colorize.StringStyle] = wd(conf.SourceListStringColor, ansiGreen)
t.stdout.colorEscapes[colorize.NumberStyle] = conf.SourceListNumberColor
t.stdout.colorEscapes[colorize.CommentStyle] = wd(conf.SourceListCommentColor, ansiBrMagenta)
t.stdout.colorEscapes[colorize.ArrowStyle] = wd(conf.SourceListArrowColor, ansiYellow)
switch x := conf.SourceListLineColor.(type) {
case string:
t.colorEscapes[colorize.LineNoStyle] = x
t.stdout.colorEscapes[colorize.LineNoStyle] = x
case int:
if (x > ansiWhite && x < ansiBrBlack) || x < ansiBlack || x > ansiBrWhite {
x = ansiBlue
}
t.colorEscapes[colorize.LineNoStyle] = fmt.Sprintf(terminalHighlightEscapeCode, x)
t.stdout.colorEscapes[colorize.LineNoStyle] = fmt.Sprintf(terminalHighlightEscapeCode, x)
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)
}
t.starlarkEnv = starbind.New(starlarkContext{t})
t.starlarkEnv = starbind.New(starlarkContext{t}, t.stdout)
return t
}
// Close returns the terminal to its previous mode.
func (t *Term) 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) {
@ -150,7 +153,7 @@ func (t *Term) sigintGuard(ch <-chan os.Signal, multiClient bool) {
t.starlarkEnv.Cancel()
state, err := t.client.GetStateNonBlocking()
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()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
@ -158,7 +161,7 @@ func (t *Term) sigintGuard(ch <-chan os.Signal, multiClient bool) {
continue
}
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()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
@ -189,14 +192,14 @@ func (t *Term) sigintGuard(ch <-chan os.Signal, multiClient bool) {
t.Close()
}
default:
fmt.Println("only p or q allowed")
fmt.Fprintln(t.stdout, "only p or q allowed")
}
} 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()
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()
if err != nil {
if err == io.EOF {
fmt.Println("exit")
fmt.Fprintln(t.stdout, "exit")
return t.handleExit()
}
return 1, fmt.Errorf("Prompt for input failed.\n")
}
t.stdout.Echo(t.prompt + cmdstr + "\n")
if strings.TrimSpace(cmdstr) == "" {
cmdstr = lastCmd
@ -309,6 +313,8 @@ func (t *Term) Run() (int, error) {
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) {
return
}
fmt.Printf("%d: %s = error %v\n", i, expr, err)
fmt.Fprintf(t.stdout, "%d: %s = error %v\n", i, expr, err)
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() {
@ -533,8 +539,90 @@ func (t *Term) longCommandCanceled() bool {
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
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
}