diff --git a/pkg/config/split.go b/pkg/config/split.go index 8279f22b..22fe18cb 100644 --- a/pkg/config/split.go +++ b/pkg/config/split.go @@ -2,6 +2,11 @@ package config import ( "bytes" + "fmt" + "io" + "reflect" + "strconv" + "strings" "unicode" ) @@ -61,3 +66,138 @@ func SplitQuotedFields(in string, quote rune) []string { return r } + +func ConfigureSetSimple(rest string, cfgname string, field reflect.Value) error { + simpleArg := func(typ reflect.Type) (reflect.Value, error) { + switch typ.Kind() { + case reflect.Int: + n, err := strconv.Atoi(rest) + if err != nil { + return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number", cfgname) + } + if n < 0 { + return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number greater than zero", cfgname) + } + return reflect.ValueOf(&n), nil + case reflect.Bool: + v := rest == "true" + return reflect.ValueOf(&v), nil + case reflect.String: + return reflect.ValueOf(&rest), nil + default: + return reflect.ValueOf(nil), fmt.Errorf("unsupported type for configuration key %q", cfgname) + } + } + + if field.Kind() == reflect.Ptr { + val, err := simpleArg(field.Type().Elem()) + if err != nil { + return err + } + field.Set(val) + } else { + val, err := simpleArg(field.Type()) + if err != nil { + return err + } + field.Set(val.Elem()) + } + return nil +} + +func ConfigureList(w io.Writer, config interface{}, tag string) { + it := IterateConfiguration(config, tag) + for it.Next() { + fieldName, field := it.Field() + if fieldName == "" { + continue + } + + writeField(w, field, fieldName) + } +} + +func writeField(w io.Writer, field reflect.Value, fieldName string) { + switch field.Kind() { + case reflect.Interface: + switch field := field.Interface().(type) { + case string: + fmt.Fprintf(w, "%s\t%q\n", fieldName, field) + default: + fmt.Fprintf(w, "%s\t%v\n", fieldName, field) + } + case reflect.Ptr: + if !field.IsNil() { + fmt.Fprintf(w, "%s\t%v\n", fieldName, field.Elem()) + } else { + fmt.Fprintf(w, "%s\t\n", fieldName) + } + case reflect.String: + fmt.Fprintf(w, "%s\t%q\n", fieldName, field) + default: + fmt.Fprintf(w, "%s\t%v\n", fieldName, field) + } +} + +type configureIterator struct { + cfgValue reflect.Value + cfgType reflect.Type + i int + tag string +} + +func IterateConfiguration(conf interface{}, tag string) *configureIterator { + cfgValue := reflect.ValueOf(conf).Elem() + cfgType := cfgValue.Type() + + return &configureIterator{cfgValue, cfgType, -1, tag} +} + +func (it *configureIterator) Next() bool { + it.i++ + return it.i < it.cfgValue.NumField() +} + +func (it *configureIterator) Field() (name string, field reflect.Value) { + name = it.cfgType.Field(it.i).Tag.Get(it.tag) + if comma := strings.Index(name, ","); comma >= 0 { + name = name[:comma] + } + field = it.cfgValue.Field(it.i) + return +} + +func ConfigureListByName(conf interface{}, name, tag string) string { + if name == "" { + return "" + } + it := IterateConfiguration(conf, tag) + for it.Next() { + fieldName, field := it.Field() + if fieldName == name { + var buf bytes.Buffer + writeField(&buf, field, fieldName) + return buf.String() + } + } + return "" +} + +func ConfigureFindFieldByName(conf interface{}, name, tag string) reflect.Value { + it := IterateConfiguration(conf, tag) + for it.Next() { + fieldName, field := it.Field() + if fieldName == name { + return field + } + } + return reflect.ValueOf(nil) +} + +func Split2PartsBySpace(s string) []string { + v := strings.SplitN(s, " ", 2) + for i := range v { + v[i] = strings.TrimSpace(v[i]) + } + return v +} diff --git a/pkg/config/split_test.go b/pkg/config/split_test.go index c49283b0..d2c258d0 100644 --- a/pkg/config/split_test.go +++ b/pkg/config/split_test.go @@ -35,3 +35,67 @@ func TestSplitDoubleQuotedFields(t *testing.T) { } } } + +func TestConfigureListByName(t *testing.T) { + type testConfig struct { + boolArg bool `cfgName:"bool-arg"` + listArg []string `cfgName:"list-arg"` + } + + type args struct { + sargs *testConfig + cfgname string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "basic bool", + args: args{ + sargs: &testConfig{ + boolArg: true, + listArg: []string{}, + }, + cfgname: "bool-arg", + }, + want: "bool-arg true\n", + }, + { + name: "list arg", + args: args{ + sargs: &testConfig{ + boolArg: true, + listArg: []string{"item 1", "item 2"}, + }, + + cfgname: "list-arg", + }, + want: "list-arg [item 1 item 2]\n", + }, + { + name: "empty", + args: args{ + sargs: &testConfig{}, + cfgname: "", + }, + want: "", + }, + { + name: "invalid", + args: args{ + sargs: &testConfig{}, + cfgname: "nonexistent", + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ConfigureListByName(tt.args.sargs, tt.args.cfgname, "cfgName"); got != tt.want { + t.Errorf("ConfigureListByName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 028bd13f..5dfe5224 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -26,6 +26,7 @@ import ( "time" "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" @@ -985,16 +986,8 @@ func selectedGID(state *api.DebuggerState) int { return state.SelectedGoroutine.ID } -func split2PartsBySpace(s string) []string { - v := strings.SplitN(s, " ", 2) - for i := range v { - v[i] = strings.TrimSpace(v[i]) - } - return v -} - func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error { - args := split2PartsBySpace(argstr) + args := config.Split2PartsBySpace(argstr) if ctx.Prefix == onPrefix { if len(args) != 1 || args[0] != "" { @@ -1043,7 +1036,7 @@ func (c *Commands) frameCommand(t *Term, ctx callContext, argstr string, directi return errors.New("not enough arguments") } } else { - args := split2PartsBySpace(argstr) + args := config.Split2PartsBySpace(argstr) var err error if frame, err = strconv.Atoi(args[0]); err != nil { return err @@ -1258,7 +1251,7 @@ func restart(t *Term, ctx callContext, args string) error { } func restartRecorded(t *Term, ctx callContext, args string) error { - v := split2PartsBySpace(args) + v := config.Split2PartsBySpace(args) rerecord := false resetArgs := false @@ -1810,7 +1803,7 @@ func formatBreakpointAttrs(prefix string, bp *api.Breakpoint, includeTrace bool) } func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) ([]*api.Breakpoint, error) { - args := split2PartsBySpace(argstr) + args := config.Split2PartsBySpace(argstr) requestedBp := &api.Breakpoint{} spec := "" @@ -2212,7 +2205,7 @@ func types(t *Term, ctx callContext, args string) error { } func parseVarArguments(args string, t *Term) (filter string, cfg api.LoadConfig) { - if v := split2PartsBySpace(args); len(v) >= 1 && v[0] == "-v" { + if v := config.Split2PartsBySpace(args); len(v) >= 1 && v[0] == "-v" { if len(v) == 2 { return v[1], t.loadConfig() } else { @@ -2486,7 +2479,7 @@ func disassCommand(t *Term, ctx callContext, args string) error { var cmd, rest string if args != "" { - argv := split2PartsBySpace(args) + argv := config.Split2PartsBySpace(args) if len(argv) != 2 { return errDisasmUsage } @@ -2517,7 +2510,7 @@ func disassCommand(t *Term, ctx callContext, args string) error { } disasm, disasmErr = t.client.DisassemblePC(ctx.Scope, locs[0].PC, flavor) case "-a": - v := split2PartsBySpace(rest) + v := config.Split2PartsBySpace(rest) if len(v) != 2 { return errDisasmUsage } @@ -2839,7 +2832,7 @@ func getBreakpointByIDOrName(t *Term, arg string) (*api.Breakpoint, error) { } func (c *Commands) onCmd(t *Term, ctx callContext, argstr string) error { - args := split2PartsBySpace(argstr) + args := config.Split2PartsBySpace(argstr) if len(args) < 2 { return errors.New("not enough arguments") @@ -2916,7 +2909,7 @@ func (c *Commands) parseBreakpointAttrs(t *Term, ctx callContext, r io.Reader) e } func conditionCmd(t *Term, ctx callContext, argstr string) error { - args := split2PartsBySpace(argstr) + args := config.Split2PartsBySpace(argstr) if len(args) < 2 { return fmt.Errorf("not enough arguments") @@ -2930,7 +2923,7 @@ func conditionCmd(t *Term, ctx callContext, argstr string) error { return nil } - args = split2PartsBySpace(args[1]) + args = config.Split2PartsBySpace(args[1]) if len(args) < 2 { return fmt.Errorf("not enough arguments") } diff --git a/pkg/terminal/config.go b/pkg/terminal/config.go index 2d1ef51e..a005c51a 100644 --- a/pkg/terminal/config.go +++ b/pkg/terminal/config.go @@ -4,8 +4,6 @@ import ( "fmt" "os" "reflect" - "strconv" - "strings" "text/tabwriter" "github.com/go-delve/delve/pkg/config" @@ -32,80 +30,15 @@ func configureCmd(t *Term, ctx callContext, args string) error { } } -type configureIterator struct { - cfgValue reflect.Value - cfgType reflect.Type - i int -} - -func iterateConfiguration(conf *config.Config) *configureIterator { - cfgValue := reflect.ValueOf(conf).Elem() - cfgType := cfgValue.Type() - - return &configureIterator{cfgValue, cfgType, -1} -} - -func (it *configureIterator) Next() bool { - it.i++ - return it.i < it.cfgValue.NumField() -} - -func (it *configureIterator) Field() (name string, field reflect.Value) { - name = it.cfgType.Field(it.i).Tag.Get("yaml") - if comma := strings.Index(name, ","); comma >= 0 { - name = name[:comma] - } - field = it.cfgValue.Field(it.i) - return -} - -func configureFindFieldByName(conf *config.Config, name string) reflect.Value { - it := iterateConfiguration(conf) - for it.Next() { - fieldName, field := it.Field() - if fieldName == name { - return field - } - } - return reflect.ValueOf(nil) -} - func configureList(t *Term) error { w := new(tabwriter.Writer) w.Init(os.Stdout, 0, 8, 1, ' ', 0) - - it := iterateConfiguration(t.conf) - for it.Next() { - fieldName, field := it.Field() - if fieldName == "" { - continue - } - - switch field.Kind() { - case reflect.Interface: - switch field := field.Interface().(type) { - case string: - fmt.Fprintf(w, "%s\t%q\n", fieldName, field) - default: - fmt.Fprintf(w, "%s\t%v\n", fieldName, field) - } - case reflect.Ptr: - if !field.IsNil() { - fmt.Fprintf(w, "%s\t%v\n", fieldName, field.Elem()) - } else { - fmt.Fprintf(w, "%s\t\n", fieldName) - } - case reflect.String: - fmt.Fprintf(w, "%s\t%q\n", fieldName, field) - default: - fmt.Fprintf(w, "%s\t%v\n", fieldName, field) - } - } + config.ConfigureList(w, t.conf, "yaml") return w.Flush() } func configureSet(t *Term, args string) error { - v := split2PartsBySpace(args) + v := config.Split2PartsBySpace(args) cfgname := v[0] var rest string @@ -117,7 +50,7 @@ func configureSet(t *Term, args string) error { return configureSetAlias(t, rest) } - field := configureFindFieldByName(t.conf, cfgname) + field := config.ConfigureFindFieldByName(t.conf, cfgname, "yaml") if !field.CanAddr() { return fmt.Errorf("%q is not a configuration parameter", cfgname) } @@ -126,41 +59,7 @@ func configureSet(t *Term, args string) error { return configureSetSubstitutePath(t, rest) } - simpleArg := func(typ reflect.Type) (reflect.Value, error) { - switch typ.Kind() { - case reflect.Int: - n, err := strconv.Atoi(rest) - if err != nil { - return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number", cfgname) - } - if n < 0 { - return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number greater than zero", cfgname) - } - return reflect.ValueOf(&n), nil - case reflect.Bool: - v := rest == "true" - return reflect.ValueOf(&v), nil - case reflect.String: - return reflect.ValueOf(&rest), nil - default: - return reflect.ValueOf(nil), fmt.Errorf("unsupported type for configuration key %q", cfgname) - } - } - - if field.Kind() == reflect.Ptr { - val, err := simpleArg(field.Type().Elem()) - if err != nil { - return err - } - field.Set(val) - } else { - val, err := simpleArg(field.Type()) - if err != nil { - return err - } - field.Set(val.Elem()) - } - return nil + return config.ConfigureSetSimple(rest, cfgname, field) } func configureSetSubstitutePath(t *Term, rest string) error { diff --git a/service/dap/command.go b/service/dap/command.go new file mode 100644 index 00000000..0b08c569 --- /dev/null +++ b/service/dap/command.go @@ -0,0 +1,115 @@ +package dap + +import ( + "bytes" + "errors" + "fmt" + "strings" + + "github.com/go-delve/delve/pkg/config" +) + +func (s *Session) delveCmd(goid, frame int, cmdstr string) (string, error) { + vals := strings.SplitN(strings.TrimSpace(cmdstr), " ", 2) + cmdname := vals[0] + var args string + if len(vals) > 1 { + args = strings.TrimSpace(vals[1]) + } + for _, cmd := range debugCommands(s) { + for _, alias := range cmd.aliases { + if alias == cmdname { + return cmd.cmdFn(goid, frame, args) + } + } + } + return "", errNoCmd +} + +type cmdfunc func(goid, frame int, args string) (string, error) + +type command struct { + aliases []string + helpMsg string + cmdFn cmdfunc +} + +const ( + msgHelp = `Prints the help message. + +help [command] + +Type "help" followed by the name of a command for more information about it.` + + msgConfig = `Changes configuration parameters. + + config -list + + Show all configuration parameters. + + config + + Changes the value of a configuration parameter. + + config substitutePath + config substitutePath + + Adds or removes a path substitution rule.` +) + +// debugCommands returns a list of commands with default commands defined. +func debugCommands(s *Session) []command { + return []command{ + {aliases: []string{"help", "h"}, cmdFn: s.helpMessage, helpMsg: msgHelp}, + {aliases: []string{"config"}, cmdFn: s.evaluateConfig, helpMsg: msgConfig}, + } +} + +var errNoCmd = errors.New("command not available") + +func (s *Session) helpMessage(_, _ int, args string) (string, error) { + var buf bytes.Buffer + if args != "" { + for _, cmd := range debugCommands(s) { + for _, alias := range cmd.aliases { + if alias == args { + return cmd.helpMsg, nil + } + } + } + return "", errNoCmd + } + + fmt.Fprintln(&buf, "The following commands are available:") + + for _, cmd := range debugCommands(s) { + h := cmd.helpMsg + if idx := strings.Index(h, "\n"); idx >= 0 { + h = h[:idx] + } + if len(cmd.aliases) > 1 { + fmt.Fprintf(&buf, " %s (alias: %s) \t %s\n", cmd.aliases[0], strings.Join(cmd.aliases[1:], " | "), h) + } else { + fmt.Fprintf(&buf, " %s \t %s\n", cmd.aliases[0], h) + } + } + + fmt.Fprintln(&buf) + fmt.Fprintln(&buf, "Type help followed by a command for full documentation.") + return buf.String(), nil +} + +func (s *Session) evaluateConfig(_, _ int, expr string) (string, error) { + argv := config.Split2PartsBySpace(expr) + name := argv[0] + switch name { + case "-list": + return listConfig(&s.args), nil + default: + res, err := configureSet(&s.args, expr) + if err != nil { + return "", err + } + return res, nil + } +} diff --git a/service/dap/config.go b/service/dap/config.go new file mode 100644 index 00000000..ec824211 --- /dev/null +++ b/service/dap/config.go @@ -0,0 +1,80 @@ +package dap + +import ( + "bytes" + "fmt" + + "github.com/go-delve/delve/pkg/config" +) + +func listConfig(args *launchAttachArgs) string { + var buf bytes.Buffer + config.ConfigureList(&buf, args, "cfgName") + return buf.String() +} + +func configureSet(sargs *launchAttachArgs, args string) (string, error) { + v := config.Split2PartsBySpace(args) + + cfgname := v[0] + var rest string + if len(v) == 2 { + rest = v[1] + } + + field := config.ConfigureFindFieldByName(sargs, cfgname, "cfgName") + if !field.CanAddr() { + return "", fmt.Errorf("%q is not a configuration parameter", cfgname) + } + + // If there were no arguments provided, just list the value. + if len(v) == 1 { + return config.ConfigureListByName(sargs, cfgname, "cfgName"), nil + } + + if cfgname == "substitutePath" { + err := configureSetSubstitutePath(sargs, rest) + if err != nil { + return "", err + } + // Print the updated client to server and server to client maps. + return fmt.Sprintf("%s\nUpdated", config.ConfigureListByName(sargs, cfgname, "cfgName")), nil + } + + err := config.ConfigureSetSimple(rest, cfgname, field) + if err != nil { + return "", err + } + return fmt.Sprintf("%s\nUpdated", config.ConfigureListByName(sargs, cfgname, "cfgName")), nil +} + +func configureSetSubstitutePath(args *launchAttachArgs, rest string) error { + argv := config.SplitQuotedFields(rest, '"') + switch len(argv) { + case 1: // delete substitute-path rule + for i := range args.substitutePathClientToServer { + if args.substitutePathClientToServer[i][0] == argv[0] { + copy(args.substitutePathClientToServer[i:], args.substitutePathClientToServer[i+1:]) + args.substitutePathClientToServer = args.substitutePathClientToServer[:len(args.substitutePathClientToServer)-1] + copy(args.substitutePathServerToClient[i:], args.substitutePathServerToClient[i+1:]) + args.substitutePathServerToClient = args.substitutePathServerToClient[:len(args.substitutePathServerToClient)-1] + return nil + } + } + return fmt.Errorf("could not find rule for %q", argv[0]) + case 2: // add substitute-path rule + for i := range args.substitutePathClientToServer { + if args.substitutePathClientToServer[i][0] == argv[0] { + args.substitutePathClientToServer[i][1] = argv[1] + args.substitutePathServerToClient[i][0] = argv[1] + return nil + } + } + args.substitutePathClientToServer = append(args.substitutePathClientToServer, [2]string{argv[0], argv[1]}) + args.substitutePathServerToClient = append(args.substitutePathServerToClient, [2]string{argv[1], argv[0]}) + + default: + return fmt.Errorf("too many arguments to \"config substitute-path\"") + } + return nil +} diff --git a/service/dap/config_test.go b/service/dap/config_test.go new file mode 100644 index 00000000..2b5eb0fa --- /dev/null +++ b/service/dap/config_test.go @@ -0,0 +1,197 @@ +package dap + +import ( + "testing" +) + +func TestListConfig(t *testing.T) { + type args struct { + args *launchAttachArgs + } + tests := []struct { + name string + args args + want string + }{ + { + name: "empty", + args: args{ + args: &launchAttachArgs{}, + }, + want: formatConfig(0, false, false, false, [][2]string{}), + }, + { + name: "default values", + args: args{ + args: &defaultArgs, + }, + want: formatConfig(50, false, false, false, [][2]string{}), + }, + { + name: "custom values", + args: args{ + args: &launchAttachArgs{ + StackTraceDepth: 35, + ShowGlobalVariables: true, + substitutePathClientToServer: [][2]string{{"hello", "world"}}, + substitutePathServerToClient: [][2]string{{"world", "hello"}}, + }, + }, + want: formatConfig(35, true, false, false, [][2]string{{"hello", "world"}}), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := listConfig(tt.args.args); got != tt.want { + t.Errorf("listConfig() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestConfigureSetSubstitutePath(t *testing.T) { + type args struct { + args *launchAttachArgs + rest string + } + tests := []struct { + name string + args args + wantRules [][2]string + wantErr bool + }{ + // Test add rule. + { + name: "add rule", + args: args{ + args: &launchAttachArgs{ + substitutePathClientToServer: [][2]string{}, + substitutePathServerToClient: [][2]string{}, + }, + rest: "/path/to/client/dir /path/to/server/dir", + }, + wantRules: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}}, + wantErr: false, + }, + { + name: "add rule (multiple)", + args: args{ + args: &launchAttachArgs{ + substitutePathClientToServer: [][2]string{ + {"/path/to/client/dir/a", "/path/to/server/dir/a"}, + {"/path/to/client/dir/b", "/path/to/server/dir/b"}, + }, + substitutePathServerToClient: [][2]string{ + {"/path/to/server/dir/a", "/path/to/client/dir/a"}, + {"/path/to/server/dir/b", "/path/to/client/dir/b"}, + }, + }, + rest: "/path/to/client/dir/c /path/to/server/dir/b", + }, + wantRules: [][2]string{ + {"/path/to/client/dir/a", "/path/to/server/dir/a"}, + {"/path/to/client/dir/b", "/path/to/server/dir/b"}, + {"/path/to/client/dir/c", "/path/to/server/dir/b"}, + }, + wantErr: false, + }, + // Test modify rule. + { + name: "modify rule", + args: args{ + args: &launchAttachArgs{ + substitutePathClientToServer: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}}, + substitutePathServerToClient: [][2]string{{"/path/to/server/dir", "/path/to/client/dir"}}, + }, + rest: "/path/to/client/dir /new/path/to/server/dir", + }, + wantRules: [][2]string{{"/path/to/client/dir", "/new/path/to/server/dir"}}, + wantErr: false, + }, + { + name: "modify rule (multiple)", + args: args{ + args: &launchAttachArgs{ + substitutePathClientToServer: [][2]string{ + {"/path/to/client/dir/a", "/path/to/server/dir/a"}, + {"/path/to/client/dir/b", "/path/to/server/dir/b"}, + {"/path/to/client/dir/c", "/path/to/server/dir/b"}, + }, + substitutePathServerToClient: [][2]string{ + {"/path/to/server/dir/a", "/path/to/client/dir/a"}, + {"/path/to/server/dir/b", "/path/to/client/dir/b"}, + {"/path/to/server/dir/b", "/path/to/client/dir/c"}, + }, + }, + rest: "/path/to/client/dir/b /new/path", + }, + wantRules: [][2]string{ + {"/path/to/client/dir/a", "/path/to/server/dir/a"}, + {"/path/to/client/dir/b", "/new/path"}, + {"/path/to/client/dir/c", "/path/to/server/dir/b"}, + }, + wantErr: false, + }, + // Test delete rule. + { + name: "delete rule", + args: args{ + args: &launchAttachArgs{ + substitutePathClientToServer: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}}, + substitutePathServerToClient: [][2]string{{"/path/to/server/dir", "/path/to/client/dir"}}, + }, + rest: "/path/to/client/dir", + }, + wantRules: [][2]string{}, + wantErr: false, + }, + // Test invalid input. + { + name: "error on empty args", + args: args{ + args: &launchAttachArgs{ + substitutePathClientToServer: [][2]string{}, + substitutePathServerToClient: [][2]string{}, + }, + rest: " \n\r ", + }, + wantErr: true, + }, + { + name: "error on delete nonexistent rule", + args: args{ + args: &launchAttachArgs{ + substitutePathClientToServer: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}}, + substitutePathServerToClient: [][2]string{{"/path/to/server/dir", "/path/to/client/dir"}}, + }, + rest: "/path/to/server/dir", + }, + wantRules: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := configureSetSubstitutePath(tt.args.args, tt.args.rest) + if (err != nil) != tt.wantErr { + t.Errorf("configureSetSubstitutePath() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(tt.args.args.substitutePathClientToServer) != len(tt.wantRules) { + t.Errorf("configureSetSubstitutePath() got substitutePathClientToServer=%v, want %d rules", tt.args.args.substitutePathClientToServer, len(tt.wantRules)) + return + } + gotClient2Server := tt.args.args.substitutePathClientToServer + gotServer2Client := tt.args.args.substitutePathServerToClient + for i, rule := range tt.wantRules { + if gotClient2Server[i][0] != rule[0] || gotClient2Server[i][1] != rule[1] { + t.Errorf("configureSetSubstitutePath() got substitutePathClientToServer[%d]=%#v,\n want %#v rules", i, gotClient2Server[i], rule) + } + if gotServer2Client[i][1] != rule[0] || gotServer2Client[i][0] != rule[1] { + reverseRule := [2]string{rule[1], rule[0]} + t.Errorf("configureSetSubstitutePath() got substitutePathServerToClient[%d]=%#v,\n want %#v rules", i, gotClient2Server[i], reverseRule) + } + } + }) + } +} diff --git a/service/dap/daptest/gen/main.go b/service/dap/daptest/gen/main.go index 13010d4f..a5e3870e 100644 --- a/service/dap/daptest/gen/main.go +++ b/service/dap/daptest/gen/main.go @@ -44,7 +44,15 @@ func (c *Client) Check{{.}}(t *testing.T, m dap.Message) *dap.{{.}} { if !ok { t.Fatalf("got %#v, want *dap.ContinuedEvent", m) } - m = c.ExpectMessage(t){{end}} + m = c.ExpectMessage(t){{else}}{{if (eq . "ConfigurationDoneResponse") }} + oe, ok := m.(*dap.OutputEvent) + if !ok { + t.Fatalf("got %#v, want *dap.OutputEvent", m) + } + if oe.Body.Output != "Type 'dlv help' for list of commands.\n" { + t.Fatalf("got %#v, want Output=%q", m, "Type 'dlv help' for list of commands.\n") + } + m = c.ExpectMessage(t){{end}}{{end}} r, ok := m.(*dap.{{.}}) if !ok { t.Fatalf("got %#v, want *dap.{{.}}", m) diff --git a/service/dap/daptest/resp.go b/service/dap/daptest/resp.go index 6501a51b..0a253299 100644 --- a/service/dap/daptest/resp.go +++ b/service/dap/daptest/resp.go @@ -128,6 +128,14 @@ func (c *Client) ExpectConfigurationDoneResponse(t *testing.T) *dap.Configuratio // CheckConfigurationDoneResponse fails the test if m is not *ConfigurationDoneResponse. func (c *Client) CheckConfigurationDoneResponse(t *testing.T, m dap.Message) *dap.ConfigurationDoneResponse { t.Helper() + oe, ok := m.(*dap.OutputEvent) + if !ok { + t.Fatalf("got %#v, want *dap.OutputEvent", m) + } + if oe.Body.Output != "Type 'dlv help' for list of commands.\n" { + t.Fatalf("got %#v, want Output=%q", m, "Type 'dlv help' for list of commands.\n") + } + m = c.ExpectMessage(t) r, ok := m.(*dap.ConfigurationDoneResponse) if !ok { t.Fatalf("got %#v, want *dap.ConfigurationDoneResponse", m) @@ -441,6 +449,24 @@ func (c *Client) CheckLoadedSourcesResponse(t *testing.T, m dap.Message) *dap.Lo return r } +// ExpectMemoryEvent reads a protocol message from the connection +// and fails the test if the read message is not *MemoryEvent. +func (c *Client) ExpectMemoryEvent(t *testing.T) *dap.MemoryEvent { + t.Helper() + m := c.ExpectMessage(t) + return c.CheckMemoryEvent(t, m) +} + +// CheckMemoryEvent fails the test if m is not *MemoryEvent. +func (c *Client) CheckMemoryEvent(t *testing.T, m dap.Message) *dap.MemoryEvent { + t.Helper() + r, ok := m.(*dap.MemoryEvent) + if !ok { + t.Fatalf("got %#v, want *dap.MemoryEvent", m) + } + return r +} + // ExpectModuleEvent reads a protocol message from the connection // and fails the test if the read message is not *ModuleEvent. func (c *Client) ExpectModuleEvent(t *testing.T) *dap.ModuleEvent { @@ -1085,3 +1111,21 @@ func (c *Client) CheckVariablesResponse(t *testing.T, m dap.Message) *dap.Variab } return r } + +// ExpectWriteMemoryResponse reads a protocol message from the connection +// and fails the test if the read message is not *WriteMemoryResponse. +func (c *Client) ExpectWriteMemoryResponse(t *testing.T) *dap.WriteMemoryResponse { + t.Helper() + m := c.ExpectMessage(t) + return c.CheckWriteMemoryResponse(t, m) +} + +// CheckWriteMemoryResponse fails the test if m is not *WriteMemoryResponse. +func (c *Client) CheckWriteMemoryResponse(t *testing.T, m dap.Message) *dap.WriteMemoryResponse { + t.Helper() + r, ok := m.(*dap.WriteMemoryResponse) + if !ok { + t.Fatalf("got %#v, want *dap.WriteMemoryResponse", m) + } + return r +} diff --git a/service/dap/error_ids.go b/service/dap/error_ids.go index 214be792..09567776 100644 --- a/service/dap/error_ids.go +++ b/service/dap/error_ids.go @@ -26,6 +26,7 @@ const ( UnableToSetVariable = 2012 UnableToDisassemble = 2013 UnableToListRegisters = 2014 + UnableToRunDlvCommand = 2015 // Add more codes as we support more requests NoDebugIsRunning = 3000 DebuggeeIsRunning = 4000 diff --git a/service/dap/server.go b/service/dap/server.go index 46ddfc16..2b8d8ced 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -178,21 +178,22 @@ type process struct { // launchAttachArgs captures arguments from launch/attach request that // impact handling of subsequent requests. +// The fields with cfgName tag can be updated through an evaluation request. type launchAttachArgs struct { // stopOnEntry is set to automatically stop the debugee after start. stopOnEntry bool - // stackTraceDepth is the maximum length of the returned list of stack frames. - stackTraceDepth int - // showGlobalVariables indicates if global package variables should be loaded. - showGlobalVariables bool - // hideSystemGoroutines indicates if system goroutines should be removed from threads + // StackTraceDepth is the maximum length of the returned list of stack frames. + StackTraceDepth int `cfgName:"stackTraceDepth"` + // ShowGlobalVariables indicates if global package variables should be loaded. + ShowGlobalVariables bool `cfgName:"showGlobalVariables"` + // ShowRegisters indicates if register values should be loaded. + ShowRegisters bool `cfgName:"showRegisters"` + // HideSystemGoroutines indicates if system goroutines should be removed from threads // responses. - hideSystemGoroutines bool - // showRegisters indicates if register values should be loaded. - showRegisters bool + HideSystemGoroutines bool `cfgName:"hideSystemGoroutines"` // substitutePathClientToServer indicates rules for converting file paths between client and debugger. // These must be directory paths. - substitutePathClientToServer [][2]string + substitutePathClientToServer [][2]string `cfgName:"substitutePath"` // substitutePathServerToClient indicates rules for converting file paths between debugger and client. // These must be directory paths. substitutePathServerToClient [][2]string @@ -203,10 +204,10 @@ type launchAttachArgs struct { // in favor of default*Config variables defined in types.go. var defaultArgs = launchAttachArgs{ stopOnEntry: false, - stackTraceDepth: 50, - showGlobalVariables: false, - hideSystemGoroutines: false, - showRegisters: false, + StackTraceDepth: 50, + ShowGlobalVariables: false, + HideSystemGoroutines: false, + ShowRegisters: false, substitutePathClientToServer: [][2]string{}, substitutePathServerToClient: [][2]string{}, } @@ -310,11 +311,11 @@ func NewSession(conn io.ReadWriteCloser, config *Config, debugger *debugger.Debu func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) error { s.args.stopOnEntry = args.StopOnEntry if depth := args.StackTraceDepth; depth > 0 { - s.args.stackTraceDepth = depth + s.args.StackTraceDepth = depth } - s.args.showGlobalVariables = args.ShowGlobalVariables - s.args.hideSystemGoroutines = args.HideSystemGoroutines - s.args.showRegisters = args.ShowRegisters + s.args.ShowGlobalVariables = args.ShowGlobalVariables + s.args.ShowRegisters = args.ShowRegisters + s.args.HideSystemGoroutines = args.HideSystemGoroutines if paths := args.SubstitutePath; len(paths) > 0 { clientToServer := make([][2]string, 0, len(paths)) serverToClient := make([][2]string, 0, len(paths)) @@ -1555,7 +1556,9 @@ func (s *Session) onConfigurationDoneRequest(request *dap.ConfigurationDoneReque } s.debugger.Target().KeepSteppingBreakpoints = proc.HaltKeepsSteppingBreakpoints | proc.TracepointKeepsSteppingBreakpoints + s.logToConsole("Type 'dlv help' for list of commands.") s.send(&dap.ConfigurationDoneResponse{Response: *newResponse(request.Request)}) + if !s.args.stopOnEntry { s.runUntilStopAndNotify(api.Continue, allowNextStateChange) } @@ -1601,7 +1604,7 @@ func (s *Session) onThreadsRequest(request *dap.ThreadsRequest) { var next int if s.debugger != nil { gs, next, err = s.debugger.Goroutines(0, maxGoroutines) - if err == nil && s.args.hideSystemGoroutines { + if err == nil && s.args.HideSystemGoroutines { gs = s.debugger.FilterGoroutines(gs, []api.ListGoroutinesFilter{{ Kind: api.GoroutineUser, Negated: false, @@ -1916,7 +1919,7 @@ func (s *Session) onStackTraceRequest(request *dap.StackTraceRequest) { if start < 0 { start = 0 } - levels := s.args.stackTraceDepth + levels := s.args.StackTraceDepth if request.Arguments.Levels > 0 { levels = request.Arguments.Levels } @@ -1960,7 +1963,7 @@ func (s *Session) onStackTraceRequest(request *dap.StackTraceRequest) { // We don't know the exact number of available stack frames, so // add an arbitrary number so the client knows to request additional // frames. - totalFrames += s.args.stackTraceDepth + totalFrames += s.args.StackTraceDepth } response := &dap.StackTraceResponse{ Response: *newResponse(request.Request), @@ -2011,7 +2014,7 @@ func (s *Session) onScopesRequest(request *dap.ScopesRequest) { scopeLocals := dap.Scope{Name: locScope.Name, VariablesReference: s.variableHandles.create(locScope)} scopes := []dap.Scope{scopeLocals} - if s.args.showGlobalVariables { + if s.args.ShowGlobalVariables { // Limit what global variables we will return to the current package only. // TODO(polina): This is how vscode-go currently does it to make // the amount of the returned data manageable. In fact, this is @@ -2046,7 +2049,7 @@ func (s *Session) onScopesRequest(request *dap.ScopesRequest) { scopes = append(scopes, scopeGlobals) } - if s.args.showRegisters { + if s.args.ShowRegisters { // Retrieve registers regs, err := s.debugger.ScopeRegisters(goid, frame, 0, false) if err != nil { @@ -2565,6 +2568,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr // Support the following expressions: // -- {expression} - evaluates the expression and returns the result as a variable // -- call {function} - injects a function call and returns the result as a variable +// -- config {expression} - updates configuration paramaters // TODO(polina): users have complained about having to click to expand multi-level // variables, so consider also adding the following: // -- print {expression} - return the result as a string like from dlv cli @@ -2584,9 +2588,20 @@ func (s *Session) onEvaluateRequest(request *dap.EvaluateRequest) { } response := &dap.EvaluateResponse{Response: *newResponse(request.Request)} - isCall, err := regexp.MatchString(`^\s*call\s+\S+`, request.Arguments.Expression) - if err == nil && isCall { // call {expression} - expr := strings.Replace(request.Arguments.Expression, "call ", "", 1) + expr := request.Arguments.Expression + + if isConfig, err := regexp.MatchString(`^\s*dlv\s+\S+`, expr); err == nil && isConfig { // dlv {command} + expr := strings.Replace(expr, "dlv ", "", 1) + result, err := s.delveCmd(goid, frame, expr) + if err != nil { + s.sendErrorResponseWithOpts(request.Request, UnableToRunDlvCommand, "Unable to run dlv command", err.Error(), showErrorToUser) + return + } + response.Body = dap.EvaluateResponseBody{ + Result: result, + } + } else if isCall, err := regexp.MatchString(`^\s*call\s+\S+`, expr); err == nil && isCall { // call {expression} + expr := strings.Replace(expr, "call ", "", 1) _, retVars, err := s.doCall(goid, frame, expr) if err != nil { s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser) @@ -2608,7 +2623,7 @@ func (s *Session) onEvaluateRequest(request *dap.EvaluateRequest) { } } } else { // {expression} - exprVar, err := s.debugger.EvalVariableInScope(goid, frame, 0, request.Arguments.Expression, DefaultLoadConfig) + exprVar, err := s.debugger.EvalVariableInScope(goid, frame, 0, expr, DefaultLoadConfig) if err != nil { s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser) return @@ -3201,7 +3216,7 @@ func (s *Session) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) { } func (s *Session) stacktrace(goroutineID int, g *proc.G) (string, error) { - frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0) + frames, err := s.debugger.Stacktrace(goroutineID, s.args.StackTraceDepth, 0) if err != nil { return "", err } diff --git a/service/dap/server_test.go b/service/dap/server_test.go index 1eb4da36..b8b20fd5 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -844,14 +844,14 @@ func checkStackFramesNamed(testName string, t *testing.T, got *dap.StackTraceRes // checkScope is a helper for verifying the values within a ScopesResponse. // i - index of the scope within ScopesRespose.Body.Scopes array // name - name of the scope -// varRef - reference to retrieve variables of this scope +// varRef - reference to retrieve variables of this scope. If varRef is negative, the reference is not checked. func checkScope(t *testing.T, got *dap.ScopesResponse, i int, name string, varRef int) { t.Helper() if len(got.Body.Scopes) <= i { t.Errorf("\ngot %d\nwant len(Scopes)>%d", len(got.Body.Scopes), i) } goti := got.Body.Scopes[i] - if goti.Name != name || goti.VariablesReference != varRef || goti.Expensive { + if goti.Name != name || (varRef >= 0 && goti.VariablesReference != varRef) || goti.Expensive { t.Errorf("\ngot %#v\nwant Name=%q VariablesReference=%d Expensive=false", goti, name, varRef) } } @@ -1063,6 +1063,7 @@ func TestStackTraceRequest(t *testing.T) { client.StackTraceRequest(1, 0, 0) stResp = client.ExpectStackTraceResponse(t) checkStackFramesExact(t, stResp, "main.main", 18, startHandle, 3, 3) + }, disconnect: false, }}) @@ -2842,7 +2843,9 @@ func TestHitBreakpointIds(t *testing.T) { client.ContinueRequest(1) client.ExpectContinueResponse(t) se = client.ExpectStoppedEvent(t) + checkHitBreakpointIds(t, se, "function breakpoint", functionBps[1].Id) + checkStop(t, client, 1, "main.anotherFunction", 27) }, disconnect: true, @@ -3805,6 +3808,105 @@ func TestEvaluateRequest(t *testing.T) { }) } +func formatConfig(depth int, showGlobals, showRegisters, hideSystemGoroutines bool, substitutePath [][2]string) string { + formatStr := `stackTraceDepth %d +showGlobalVariables %v +showRegisters %v +hideSystemGoroutines %v +substitutePath %v +` + return fmt.Sprintf(formatStr, depth, showGlobals, showRegisters, hideSystemGoroutines, substitutePath) +} + +func TestEvaluateCommandRequest(t *testing.T) { + runTest(t, "testvariables", func(client *daptest.Client, fixture protest.Fixture) { + runDebugSessionWithBPs(t, client, "launch", + // Launch + func() { + client.LaunchRequest("exec", fixture.Path, !stopOnEntry) + }, + fixture.Source, []int{}, // Breakpoint set in the program + []onBreakpoint{{ // Stop at first breakpoint + execute: func() { + checkStop(t, client, 1, "main.foobar", 66) + + // Request help. + const dlvHelp = `The following commands are available: + help (alias: h) Prints the help message. + config Changes configuration parameters. + +Type help followed by a command for full documentation. +` + client.EvaluateRequest("dlv help", 1000, "repl") + got := client.ExpectEvaluateResponse(t) + checkEval(t, got, dlvHelp, noChildren) + + client.EvaluateRequest("dlv help config", 1000, "repl") + got = client.ExpectEvaluateResponse(t) + checkEval(t, got, msgConfig, noChildren) + + // Test config. + client.EvaluateRequest("dlv config -list", 1000, "repl") + got = client.ExpectEvaluateResponse(t) + checkEval(t, got, formatConfig(50, false, false, false, [][2]string{}), noChildren) + + // Read and modify showGlobalVariables. + client.EvaluateRequest("dlv config showGlobalVariables", 1000, "repl") + got = client.ExpectEvaluateResponse(t) + checkEval(t, got, "showGlobalVariables\tfalse\n", noChildren) + + client.ScopesRequest(1000) + scopes := client.ExpectScopesResponse(t) + if len(scopes.Body.Scopes) > 1 { + t.Errorf("\ngot %#v\nwant len(scopes)=1 (Locals)", scopes) + } + checkScope(t, scopes, 0, "Locals", -1) + + client.EvaluateRequest("dlv config showGlobalVariables true", 1000, "repl") + got = client.ExpectEvaluateResponse(t) + checkEval(t, got, "showGlobalVariables\ttrue\n\nUpdated", noChildren) + + client.EvaluateRequest("dlv config -list", 1000, "repl") + got = client.ExpectEvaluateResponse(t) + checkEval(t, got, formatConfig(50, true, false, false, [][2]string{}), noChildren) + + client.ScopesRequest(1000) + scopes = client.ExpectScopesResponse(t) + if len(scopes.Body.Scopes) < 2 { + t.Errorf("\ngot %#v\nwant len(scopes)=2 (Locals & Globals)", scopes) + } + checkScope(t, scopes, 0, "Locals", -1) + checkScope(t, scopes, 1, "Globals (package main)", -1) + + // Read and modify substitutePath. + client.EvaluateRequest("dlv config substitutePath", 1000, "repl") + got = client.ExpectEvaluateResponse(t) + checkEval(t, got, "substitutePath\t[]\n", noChildren) + + client.EvaluateRequest(fmt.Sprintf("dlv config substitutePath %q %q", "my/client/path", "your/server/path"), 1000, "repl") + got = client.ExpectEvaluateResponse(t) + checkEval(t, got, "substitutePath\t[[my/client/path your/server/path]]\n\nUpdated", noChildren) + + client.EvaluateRequest(fmt.Sprintf("dlv config substitutePath %q %q", "my/client/path", "new/your/server/path"), 1000, "repl") + got = client.ExpectEvaluateResponse(t) + checkEval(t, got, "substitutePath\t[[my/client/path new/your/server/path]]\n\nUpdated", noChildren) + + client.EvaluateRequest(fmt.Sprintf("dlv config substitutePath %q", "my/client/path"), 1000, "repl") + got = client.ExpectEvaluateResponse(t) + checkEval(t, got, "substitutePath\t[]\n\nUpdated", noChildren) + + // Test bad inputs. + client.EvaluateRequest("dlv help bad", 1000, "repl") + client.ExpectErrorResponse(t) + + client.EvaluateRequest("dlv bad", 1000, "repl") + client.ExpectErrorResponse(t) + }, + disconnect: true, + }}) + }) +} + // From testvariables2 fixture const ( // As defined in the code