Repository: go-delve/delve
Pull request #2769 (2021-10-29 15:13:58)

proc/native: disable watchpoints on Windows
There are persistent issues with watchpoints on Windows, it is not
clear whether it's a problem with the VM running the tests or if there
is a real bug in our implementation of hardware breakpoints on Windows.

Until the cause can be determined watchpoints on Windows will be
disabled.

Updates #2768



Patch:
diff --git a/_fixtures/goroutinebreak.go b/_fixtures/goroutinebreak.go
new file mode 100644
index 00000000..438d1ed8
--- /dev/null
+++ b/_fixtures/goroutinebreak.go
@@ -0,0 +1,26 @@
+package main
+
+import "runtime"
+
+const N = 10
+
+func agoroutine(started chan<- struct{}, done chan<- struct{}, i int) {
+	started <- struct{}{}
+	done <- struct{}{}
+}
+
+func main() {
+	done := make(chan struct{})
+	started := make(chan struct{})
+	for i := 0; i < N; i++ {
+		runtime.Breakpoint()
+		go agoroutine(started, done, i)
+	}
+	for i := 0; i < N; i++ {
+		<-started
+	}
+	runtime.Gosched()
+	for i := 0; i < N; i++ {
+		<-done
+	}
+}
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<not defined>\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/proc/native/threads_windows.go b/pkg/proc/native/threads_windows.go
index cb9a7524..64231349 100644
--- a/pkg/proc/native/threads_windows.go
+++ b/pkg/proc/native/threads_windows.go
@@ -11,6 +11,8 @@ import (
 	"github.com/go-delve/delve/pkg/proc/winutil"
 )
 
+const enableHardwareBreakpoints = false // see https://github.com/go-delve/delve/issues/2768
+
 // waitStatus is a synonym for the platform-specific WaitStatus
 type waitStatus sys.WaitStatus
 
@@ -159,6 +161,10 @@ func (t *nativeThread) restoreRegisters(savedRegs proc.Registers) error {
 }
 
 func (t *nativeThread) withDebugRegisters(f func(*amd64util.DebugRegisters) error) error {
+	if !enableHardwareBreakpoints {
+		return errors.New("hardware breakpoints not supported")
+	}
+
 	context := winutil.NewCONTEXT()
 	context.ContextFlags = _CONTEXT_DEBUG_REGISTERS
 
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<not defined>\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 <parameter> <value>
+	
+	Changes the value of a configuration parameter.
+	
+	config substitutePath <from> <to>
+	config substitutePath <from>
+	
+	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/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..cee513ae 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))
@@ -1354,9 +1355,10 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func(
 		} else {
 			got.Cond = want.condition
 			got.HitCond = want.hitCondition
-			got.Tracepoint = want.logMessage != ""
-			got.UserData = want.logMessage
-			err = s.debugger.AmendBreakpoint(got)
+			err = setLogMessage(got, want.logMessage)
+			if err == nil {
+				err = s.debugger.AmendBreakpoint(got)
+			}
 		}
 		createdBps[want.name] = struct{}{}
 		s.updateBreakpointsResponse(breakpoints, i, err, got)
@@ -1379,19 +1381,20 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func(
 			if _, ok := createdBps[want.name]; ok {
 				err = fmt.Errorf("breakpoint already exists")
 			} else {
-				// Create new breakpoints.
-				got, err = s.debugger.CreateBreakpoint(
-					&api.Breakpoint{
-						Name:       want.name,
-						File:       wantLoc.file,
-						Line:       wantLoc.line,
-						Addr:       wantLoc.addr,
-						Addrs:      wantLoc.addrs,
-						Cond:       want.condition,
-						HitCond:    want.hitCondition,
-						Tracepoint: want.logMessage != "",
-						UserData:   want.logMessage,
-					})
+				bp := &api.Breakpoint{
+					Name:    want.name,
+					File:    wantLoc.file,
+					Line:    wantLoc.line,
+					Addr:    wantLoc.addr,
+					Addrs:   wantLoc.addrs,
+					Cond:    want.condition,
+					HitCond: want.hitCondition,
+				}
+				err = setLogMessage(bp, want.logMessage)
+				if err == nil {
+					// Create new breakpoints.
+					got, err = s.debugger.CreateBreakpoint(bp)
+				}
 			}
 		}
 		createdBps[want.name] = struct{}{}
@@ -1400,6 +1403,18 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func(
 	return breakpoints
 }
 
+func setLogMessage(bp *api.Breakpoint, msg string) error {
+	tracepoint, userdata, err := parseLogPoint(msg)
+	if err != nil {
+		return err
+	}
+	bp.Tracepoint = tracepoint
+	if userdata != nil {
+		bp.UserData = *userdata
+	}
+	return nil
+}
+
 func (s *Session) updateBreakpointsResponse(breakpoints []dap.Breakpoint, i int, err error, got *api.Breakpoint) {
 	breakpoints[i].Verified = (err == nil)
 	if err != nil {
@@ -1555,7 +1570,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 +1618,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,
@@ -1816,25 +1833,24 @@ func stoppedGoroutineID(state *api.DebuggerState) (id int) {
 // stoppedOnBreakpointGoroutineID gets the goroutine id of the first goroutine
 // that is stopped on a real breakpoint, starting with the selected goroutine.
 func (s *Session) stoppedOnBreakpointGoroutineID(state *api.DebuggerState) (int, *api.Breakpoint) {
-	// Check if the selected goroutine is stopped on a real breakpoint
-	// since we would prefer to use that one.
-	goid := stoppedGoroutineID(state)
-	if g, _ := s.debugger.FindGoroutine(goid); g != nil && g.Thread != nil {
-		if bp := g.Thread.Breakpoint(); bp != nil && bp.Breakpoint != nil && !bp.Breakpoint.Tracepoint {
-			return goid, api.ConvertBreakpoint(bp.Breakpoint)
-		}
+	// Use the first goroutine that is stopped on a breakpoint.
+	gs := s.stoppedGs(state)
+	if len(gs) == 0 {
+		return 0, nil
 	}
-
-	// Some of the breakpoints may be log points, choose the goroutine
-	// that is not stopped on a tracepoint.
-	for _, th := range state.Threads {
-		if bp := th.Breakpoint; bp != nil {
-			if !bp.Tracepoint {
-				return th.GoroutineID, bp
-			}
-		}
+	goid := gs[0]
+	if goid == 0 {
+		return goid, state.CurrentThread.Breakpoint
+	}
+	g, _ := s.debugger.FindGoroutine(goid)
+	if g == nil || g.Thread == nil {
+		return goid, nil
+	}
+	bp := g.Thread.Breakpoint()
+	if bp == nil || bp.Breakpoint == nil {
+		return goid, nil
 	}
-	return 0, nil
+	return goid, api.ConvertBreakpoint(bp.Breakpoint)
 }
 
 // stepUntilStopAndNotify is a wrapper around runUntilStopAndNotify that
@@ -1916,7 +1932,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 +1976,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 +2027,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 +2062,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 +2581,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 +2601,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 +2636,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 +3229,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
 	}
@@ -3524,12 +3552,13 @@ func (s *Session) resumeOnceAndCheckStop(command string, allowNextStateChange ch
 		return state, err
 	}
 
-	foundRealBreakpoint := s.handleLogPoints(state)
+	s.handleLogPoints(state)
+	gsOnBp := s.stoppedGs(state)
 
 	switch s.debugger.StopReason() {
 	case proc.StopBreakpoint, proc.StopManual:
 		// Make sure a real manual stop was requested or a real breakpoint was hit.
-		if foundRealBreakpoint || s.checkHaltRequested() {
+		if len(gsOnBp) > 0 || s.checkHaltRequested() {
 			s.setRunningCmd(false)
 		}
 	default:
@@ -3544,25 +3573,50 @@ func (s *Session) resumeOnceAndCheckStop(command string, allowNextStateChange ch
 	return state, err
 }
 
-func (s *Session) handleLogPoints(state *api.DebuggerState) bool {
-	foundRealBreakpoint := false
+func (s *Session) handleLogPoints(state *api.DebuggerState) {
+	for _, th := range state.Threads {
+		if bp := th.Breakpoint; bp != nil {
+			s.logBreakpointMessage(bp, th.GoroutineID)
+		}
+	}
+}
+
+func (s *Session) stoppedGs(state *api.DebuggerState) (gs []int) {
+	// Check the current thread first. There may be no selected goroutine.
+	if state.CurrentThread.Breakpoint != nil && !state.CurrentThread.Breakpoint.Tracepoint {
+		gs = append(gs, state.CurrentThread.GoroutineID)
+	}
+	if s.debugger.StopReason() == proc.StopHardcodedBreakpoint {
+		gs = append(gs, stoppedGoroutineID(state))
+	}
 	for _, th := range state.Threads {
+		// Some threads may be stopped on a hardcoded breakpoint.
+		// TODO(suzmue): This is a workaround for detecting hard coded breakpoints,
+		// though this check is likely not sufficient. It would be better to resolve
+		// this in the debugger layer instead.
+		if th.Function.Name() == "runtime.breakpoint" {
+			gs = append(gs, th.GoroutineID)
+			continue
+		}
+		// We already added the current thread if it had a breakpoint.
+		if th.ID == state.CurrentThread.ID {
+			continue
+		}
 		if bp := th.Breakpoint; bp != nil {
-			logged := s.logBreakpointMessage(bp, th.GoroutineID)
-			if !logged {
-				foundRealBreakpoint = true
+			if !th.Breakpoint.Tracepoint {
+				gs = append(gs, th.GoroutineID)
 			}
 		}
 	}
-	return foundRealBreakpoint
+	return gs
 }
 
 func (s *Session) logBreakpointMessage(bp *api.Breakpoint, goid int) bool {
 	if !bp.Tracepoint {
 		return false
 	}
-	// TODO(suzmue): allow evaluate expressions within log points.
-	if msg, ok := bp.UserData.(string); ok {
+	if lMsg, ok := bp.UserData.(logMessage); ok {
+		msg := lMsg.evaluate(s, goid)
 		s.send(&dap.OutputEvent{
 			Event: *newEvent("output"),
 			Body: dap.OutputEventBody{
@@ -3578,6 +3632,19 @@ func (s *Session) logBreakpointMessage(bp *api.Breakpoint, goid int) bool {
 	return true
 }
 
+func (msg *logMessage) evaluate(s *Session, goid int) string {
+	evaluated := make([]interface{}, len(msg.args))
+	for i := range msg.args {
+		exprVar, err := s.debugger.EvalVariableInScope(goid, 0, 0, msg.args[i], DefaultLoadConfig)
+		if err != nil {
+			evaluated[i] = fmt.Sprintf("{eval err: %e}", err)
+			continue
+		}
+		evaluated[i] = s.convertVariableToString(exprVar)
+	}
+	return fmt.Sprintf(msg.format, evaluated...)
+}
+
 func (s *Session) toClientPath(path string) string {
 	if len(s.args.substitutePathServerToClient) == 0 {
 		return path
@@ -3599,3 +3666,64 @@ func (s *Session) toServerPath(path string) string {
 	}
 	return serverPath
 }
+
+type logMessage struct {
+	format string
+	args   []string
+}
+
+// parseLogPoint parses a log message according to the DAP spec:
+//   "Expressions within {} are interpolated."
+func parseLogPoint(msg string) (bool, *logMessage, error) {
+	// Note: All braces *must* come in pairs, even those within an
+	// expression to be interpolated.
+	// TODO(suzmue): support individual braces in string values in
+	// eval expressions.
+	var args []string
+
+	var isArg bool
+	var formatSlice, argSlice []rune
+	braceCount := 0
+	for _, r := range msg {
+		if isArg {
+			switch r {
+			case '}':
+				if braceCount--; braceCount == 0 {
+					argStr := strings.TrimSpace(string(argSlice))
+					if len(argStr) == 0 {
+						return false, nil, fmt.Errorf("empty evaluation string")
+					}
+					args = append(args, argStr)
+					formatSlice = append(formatSlice, '%', 's')
+					isArg = false
+					continue
+				}
+			case '{':
+				braceCount += 1
+			}
+			argSlice = append(argSlice, r)
+			continue
+		}
+
+		switch r {
+		case '}':
+			return false, nil, fmt.Errorf("invalid log point format, unexpected '}'")
+		case '{':
+			if braceCount++; braceCount == 1 {
+				isArg, argSlice = true, []rune{}
+				continue
+			}
+		}
+		formatSlice = append(formatSlice, r)
+	}
+	if isArg {
+		return false, nil, fmt.Errorf("invalid log point format")
+	}
+	if len(formatSlice) == 0 {
+		return false, nil, nil
+	}
+	return true, &logMessage{
+		format: string(formatSlice),
+		args:   args,
+	}, nil
+}


Test patch:
diff --git a/Documentation/backend_test_health.md b/Documentation/backend_test_health.md
index 27cf1bf5..48c1725b 100644
--- a/Documentation/backend_test_health.md
+++ b/Documentation/backend_test_health.md
@@ -20,6 +20,7 @@ Tests skipped by each supported backend:
 	* 1 broken - cgo stacktraces
 * pie skipped = 2
 	* 2 upstream issue - https://github.com/golang/go/issues/29322
-* windows skipped = 2
+* windows skipped = 5
 	* 1 broken
+	* 3 see https://github.com/go-delve/delve/issues/2768
 	* 1 upstream issue
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/proc/proc_test.go b/pkg/proc/proc_test.go
index 7b08d515..b9ee9d66 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -5378,6 +5378,7 @@ func TestVariablesWithExternalLinking(t *testing.T) {
 func TestWatchpointsBasic(t *testing.T) {
 	skipOn(t, "not implemented", "freebsd")
 	skipOn(t, "not implemented", "386")
+	skipOn(t, "see https://github.com/go-delve/delve/issues/2768", "windows")
 	protest.AllowRecording(t)
 
 	position1 := 19
@@ -5436,6 +5437,7 @@ func TestWatchpointsBasic(t *testing.T) {
 func TestWatchpointCounts(t *testing.T) {
 	skipOn(t, "not implemented", "freebsd")
 	skipOn(t, "not implemented", "386")
+	skipOn(t, "see https://github.com/go-delve/delve/issues/2768", "windows")
 	protest.AllowRecording(t)
 
 	withTestProcess("databpcountstest", t, func(p *proc.Target, fixture protest.Fixture) {
@@ -5550,6 +5552,7 @@ func TestDwrapStartLocation(t *testing.T) {
 func TestWatchpointStack(t *testing.T) {
 	skipOn(t, "not implemented", "freebsd")
 	skipOn(t, "not implemented", "386")
+	skipOn(t, "see https://github.com/go-delve/delve/issues/2768", "windows")
 	protest.AllowRecording(t)
 
 	position1 := 17
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/server_test.go b/service/dap/server_test.go
index 1eb4da36..cbd75b3a 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,
@@ -3123,7 +3126,7 @@ func TestLogPoints(t *testing.T) {
 				execute: func() {
 					checkStop(t, client, 1, "main.main", 23)
 					bps := []int{6, 25, 27, 16}
-					logMessages := map[int]string{6: "in callme!", 16: "in callme2!"}
+					logMessages := map[int]string{6: "{i*2}: in callme!", 16: "in callme2!"}
 					client.SetBreakpointsRequestWithArgs(fixture.Source, bps, nil, nil, logMessages)
 					client.ExpectSetBreakpointsResponse(t)
 
@@ -3139,7 +3142,7 @@ func TestLogPoints(t *testing.T) {
 
 						client.ContinueRequest(1)
 						client.ExpectContinueResponse(t)
-						checkLogMessage(t, client.ExpectOutputEvent(t), 1, "in callme!", fixture.Source, 6)
+						checkLogMessage(t, client.ExpectOutputEvent(t), 1, fmt.Sprintf("%d: in callme!", i*2), fixture.Source, 6)
 					}
 					se := client.ExpectStoppedEvent(t)
 					if se.Body.Reason != "breakpoint" || se.Body.ThreadId != 1 {
@@ -3255,62 +3258,76 @@ func TestHaltPreventsAutoResume(t *testing.T) {
 	})
 }
 
-// TestConcurrentBreakpointsLogPoints executes to a breakpoint and then tests
-// that a breakpoint set in the main goroutine is hit the correct number of times
-// and log points set in the children goroutines produce the correct number of
-// output events.
+// TestConcurrentBreakpointsLogPoints tests that a breakpoint set in the main
+// goroutine is hit the correct number of times and log points set in the
+// children goroutines produce the correct number of output events.
 func TestConcurrentBreakpointsLogPoints(t *testing.T) {
 	if runtime.GOOS == "freebsd" {
 		t.SkipNow()
 	}
-	runTest(t, "goroutinestackprog", func(client *daptest.Client, fixture protest.Fixture) {
-		runDebugSessionWithBPs(t, client, "launch",
-			// Launch
-			func() {
-				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
-			},
-			// Set breakpoints
-			fixture.Source, []int{20},
-			[]onBreakpoint{{
-				// Stop at line 20
-				execute: func() {
-					checkStop(t, client, 1, "main.main", 20)
-					bps := []int{8, 23}
-					logMessages := map[int]string{8: "hello"}
-					client.SetBreakpointsRequestWithArgs(fixture.Source, bps, nil, nil, logMessages)
-					client.ExpectSetBreakpointsResponse(t)
-
-					client.ContinueRequest(1)
-					client.ExpectContinueResponse(t)
+	tests := []struct {
+		name        string
+		fixture     string
+		start       int
+		breakpoints []int
+	}{
+		{
+			name:        "source breakpoints",
+			fixture:     "goroutinestackprog",
+			breakpoints: []int{23},
+		},
+		{
+			name:    "hardcoded breakpoint",
+			fixture: "goroutinebreak",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			runTest(t, tt.fixture, func(client *daptest.Client, fixture protest.Fixture) {
+				client.InitializeRequest()
+				client.ExpectInitializeResponseAndCapabilities(t)
 
-					// There may be up to 1 breakpoint and any number of log points that are
-					// hit concurrently. We should get a stopped event everytime the breakpoint
-					// is hit and an output event for each log point hit.
-					var oeCount, seCount int
-					for oeCount < 10 || seCount < 10 {
-						switch m := client.ExpectMessage(t).(type) {
-						case *dap.StoppedEvent:
-							if m.Body.Reason != "breakpoint" || !m.Body.AllThreadsStopped || m.Body.ThreadId != 1 {
-								t.Errorf("\ngot  %#v\nwant Reason='breakpoint' AllThreadsStopped=true ThreadId=1", m)
-							}
-							checkStop(t, client, 1, "main.main", 23)
-							seCount++
-							client.ContinueRequest(1)
-						case *dap.OutputEvent:
-							checkLogMessage(t, m, -1, "hello", fixture.Source, 8)
-							oeCount++
-						case *dap.ContinueResponse:
-						case *dap.TerminatedEvent:
-							t.Fatalf("\nexpected 10 output events and 10 stopped events, got %d output events and %d stopped events", oeCount, seCount)
-						default:
-							t.Fatalf("Unexpected message type: expect StoppedEvent, OutputEvent, or ContinueResponse, got %#v", m)
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+				client.ExpectInitializedEvent(t)
+				client.ExpectLaunchResponse(t)
+
+				bps := append([]int{8}, tt.breakpoints...)
+				logMessages := map[int]string{8: "hello"}
+				client.SetBreakpointsRequestWithArgs(fixture.Source, bps, nil, nil, logMessages)
+				client.ExpectSetBreakpointsResponse(t)
+
+				client.ConfigurationDoneRequest()
+				client.ExpectConfigurationDoneResponse(t)
+
+				// There may be up to 1 breakpoint and any number of log points that are
+				// hit concurrently. We should get a stopped event everytime the breakpoint
+				// is hit and an output event for each log point hit.
+				var oeCount, seCount int
+				for oeCount < 10 || seCount < 10 {
+					switch m := client.ExpectMessage(t).(type) {
+					case *dap.StoppedEvent:
+						if m.Body.Reason != "breakpoint" || !m.Body.AllThreadsStopped || m.Body.ThreadId != 1 {
+							t.Errorf("\ngot  %#v\nwant Reason='breakpoint' AllThreadsStopped=true ThreadId=1", m)
 						}
+						seCount++
+						client.ContinueRequest(1)
+					case *dap.OutputEvent:
+						checkLogMessage(t, m, -1, "hello", fixture.Source, 8)
+						oeCount++
+					case *dap.ContinueResponse:
+					case *dap.TerminatedEvent:
+						t.Fatalf("\nexpected 10 output events and 10 stopped events, got %d output events and %d stopped events", oeCount, seCount)
+					default:
+						t.Fatalf("Unexpected message type: expect StoppedEvent, OutputEvent, or ContinueResponse, got %#v", m)
 					}
-					client.ExpectTerminatedEvent(t)
-				},
-				disconnect: false,
-			}})
-	})
+				}
+				// TODO(suzmue): The dap server may identify some false
+				// positives for hard coded breakpoints, so there may still
+				// be more stopped events.
+				client.DisconnectRequestWithKillOption(true)
+			})
+		})
+	}
 }
 
 func TestSetBreakpointWhileRunning(t *testing.T) {
@@ -3805,6 +3822,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
@@ -4170,6 +4286,30 @@ func TestNextAndStep(t *testing.T) {
 	})
 }
 
+func TestHardCodedBreakpoints(t *testing.T) {
+	runTest(t, "consts", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			fixture.Source, []int{28},
+			[]onBreakpoint{{ // Stop at line 28
+				execute: func() {
+					checkStop(t, client, 1, "main.main", 28)
+
+					client.ContinueRequest(1)
+					client.ExpectContinueResponse(t)
+					se := client.ExpectStoppedEvent(t)
+					if se.Body.ThreadId != 1 || se.Body.Reason != "breakpoint" {
+						t.Errorf("\ngot  %#v\nwant ThreadId=1 Reason=\"breakpoint\"", se)
+					}
+				},
+				disconnect: false,
+			}})
+	})
+}
+
 // TestStepInstruction executes to a breakpoint and tests stepping
 // a single instruction
 func TestStepInstruction(t *testing.T) {
@@ -6388,6 +6528,83 @@ func TestBadlyFormattedMessageToServer(t *testing.T) {
 	})
 }
 
+func TestParseLogPoint(t *testing.T) {
+	tests := []struct {
+		name           string
+		msg            string
+		wantTracepoint bool
+		wantFormat     string
+		wantArgs       []string
+		wantErr        bool
+	}{
+		// Test simple log messages.
+		{name: "simple string", msg: "hello, world!", wantTracepoint: true, wantFormat: "hello, world!"},
+		{name: "empty string", msg: "", wantTracepoint: false, wantErr: false},
+		// Test parse eval expressions.
+		{
+			name:           "simple eval",
+			msg:            "{x}",
+			wantTracepoint: true,
+			wantFormat:     "%s",
+			wantArgs:       []string{"x"},
+		},
+		{
+			name:           "type cast",
+			msg:            "hello {string(x)}",
+			wantTracepoint: true,
+			wantFormat:     "hello %s",
+			wantArgs:       []string{"string(x)"},
+		},
+		{
+			name:           "multiple eval",
+			msg:            "{x} {y} {z}",
+			wantTracepoint: true,
+			wantFormat:     "%s %s %s",
+			wantArgs:       []string{"x", "y", "z"},
+		},
+		{
+			name:           "eval expressions contain braces",
+			msg:            "{interface{}(x)} {myType{y}} {[]myType{{z}}}",
+			wantTracepoint: true,
+			wantFormat:     "%s %s %s",
+			wantArgs:       []string{"interface{}(x)", "myType{y}", "[]myType{{z}}"},
+		},
+		// Test parse errors.
+		{name: "empty evaluation", msg: "{}", wantErr: true},
+		{name: "empty space evaluation", msg: "{   \n}", wantErr: true},
+		{name: "open brace missing closed", msg: "{", wantErr: true},
+		{name: "closed brace missing open", msg: "}", wantErr: true},
+		{name: "open brace in expression", msg: `{m["{"]}`, wantErr: true},
+		{name: "closed brace in expression", msg: `{m["}"]}`, wantErr: true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotTracepoint, gotLogMessage, err := parseLogPoint(tt.msg)
+			if gotTracepoint != tt.wantTracepoint {
+				t.Errorf("parseLogPoint() tracepoint = %v, wantTracepoint %v", gotTracepoint, tt.wantTracepoint)
+				return
+			}
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseLogPoint() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !tt.wantTracepoint {
+				return
+			}
+			if gotLogMessage == nil {
+				t.Errorf("parseLogPoint() gotLogMessage = nil, want log message")
+				return
+			}
+			if gotLogMessage.format != tt.wantFormat {
+				t.Errorf("parseLogPoint() gotFormat = %v, want %v", gotLogMessage.format, tt.wantFormat)
+			}
+			if !reflect.DeepEqual(gotLogMessage.args, tt.wantArgs) {
+				t.Errorf("parseLogPoint() gotArgs = %v, want %v", gotLogMessage.args, tt.wantArgs)
+			}
+		})
+	}
+}
+
 func TestDisassemble(t *testing.T) {
 	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
 		runDebugSessionWithBPs(t, client, "launch",