Repository: go-delve/delve
Pull request #2748 (2021-10-15 14:50:51)

service/dap: fix goroutine id selection for hardcoded breakpoints
Determining the stopped goroutine id on a breakpoint required
checking for breakpoints since some may be tracepoints. However,
there may be goroutines stopped on hardcoded breakpoints with
no breakpoint. We fix this by checking for runtime.breakpoint or
StopReason=proc.StopHardcodedBreakpoint.


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/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 b7c1c7c1..3bf160fa 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{},
 }
@@ -291,6 +292,9 @@ func NewSession(conn io.ReadWriteCloser, config *Config, debugger *debugger.Debu
 		config.log = logflags.DAPLogger()
 	}
 	config.log.Debug("DAP connection started")
+	if config.StopTriggered == nil {
+		config.log.Fatal("Session must be configured with StopTriggered")
+	}
 	return &Session{
 		config:            config,
 		conn:              conn,
@@ -307,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))
@@ -350,6 +354,7 @@ func (s *Server) Stop() {
 }
 
 // Close closes the underlying debugger/process and connection.
+// May be called more than once.
 func (s *Session) Close() {
 	s.mu.Lock()
 	defer s.mu.Unlock()
@@ -357,12 +362,13 @@ func (s *Session) Close() {
 	if s.debugger != nil {
 		killProcess := s.config.Debugger.AttachPid == 0
 		s.stopDebugSession(killProcess)
-	} else {
+	} else if s.noDebugProcess != nil {
 		s.stopNoDebugProcess()
 	}
 	// The binary is no longer in use by the debugger. It is safe to remove it.
 	if s.binaryToRemove != "" {
 		gobuild.Remove(s.binaryToRemove)
+		s.binaryToRemove = "" // avoid error printed on duplicate removal
 	}
 	// Close client connection last, so other shutdown stages
 	// can send client notifications.
@@ -370,6 +376,11 @@ func (s *Session) Close() {
 	// returned, this will result in a closed connection error
 	// on next read, breaking out the read loop andd
 	// allowing the run goroutinee to exit.
+	// This connection is closed here and in serveDAPCodec().
+	// If this was a forced shutdown, external stop logic can close this first.
+	// If this was a client loop exit (on error or disconnect), serveDAPCodec()
+	// will be first.
+	// Duplicate close calls return an error, but are not fatal.
 	_ = s.conn.Close()
 }
 
@@ -382,14 +393,8 @@ func (s *Session) Close() {
 // and is currently only called from the run goroutine.
 func (c *Config) triggerServerStop() {
 	// Avoid accidentally closing the channel twice and causing a panic, when
-	// this function is called more than once. For example, we could have the
-	// following sequence of events:
-	// -- run goroutine: calls onDisconnectRequest()
-	// -- run goroutine: calls triggerServerStop()
-	// -- main goroutine: calls Stop()
-	// -- main goroutine: Stop() closes client connection (or client closed it)
-	// -- run goroutine: ServeDAPCodec() gets "closed network connection"
-	// -- run goroutine: ServeDAPCodec() returns and calls triggerServerStop()
+	// this function is called more than once because stop was triggered
+	// by multiple conditions simultenously.
 	if c.DisconnectChan != nil {
 		close(c.DisconnectChan)
 		c.DisconnectChan = nil
@@ -461,7 +466,9 @@ func (s *Server) RunWithClient(conn net.Conn) {
 // until it encounters an error or EOF, when it sends
 // a disconnect signal and returns.
 func (s *Session) ServeDAPCodec() {
-	// TODO(polina): defer-close conn/session like in serveJSONCodec
+	// Close conn, but not the debugger in case we are in AcceptMuli mode.
+	// If not, debugger will be shut down in Stop().
+	defer s.conn.Close()
 	reader := bufio.NewReader(s.conn)
 	for {
 		request, err := dap.ReadProtocolMessage(reader)
@@ -478,6 +485,9 @@ func (s *Session) ServeDAPCodec() {
 			select {
 			case <-s.config.StopTriggered:
 			default:
+				if !s.config.AcceptMulti {
+					defer s.config.triggerServerStop()
+				}
 				if err != io.EOF { // EOF means client closed connection
 					if decodeErr, ok := err.(*dap.DecodeProtocolMessageFieldError); ok {
 						// Send an error response to the users if we were unable to process the message.
@@ -486,15 +496,15 @@ func (s *Session) ServeDAPCodec() {
 					}
 					s.config.log.Error("DAP error: ", err)
 				}
-				if s.config.AcceptMulti {
-					s.conn.Close()
-				} else {
-					s.config.triggerServerStop()
-				}
 			}
 			return
 		}
 		s.handleRequest(request)
+
+		if _, ok := request.(*dap.DisconnectRequest); ok {
+			// disconnect already shut things down and triggered stopping
+			return
+		}
 	}
 }
 
@@ -1137,11 +1147,13 @@ func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
 
 	if s.debugger != nil && s.config.AcceptMulti && !request.Arguments.TerminateDebuggee {
 		// This is a multi-use server/debugger, so a disconnect request that doesn't
-		// terminate the debuggee should clean up only the client connection, but not the server.
+		// terminate the debuggee should clean up only the client connection and pointer to debugger,
+		// but not the entire server.
 		s.logToConsole("Closing client session, but leaving multi-client DAP server running at " + s.config.Listener.Addr().String())
 		s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
 		s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
 		s.conn.Close()
+		s.debugger = nil
 		// The target is left in whatever state it is already in - halted or running.
 		// The users therefore have the flexibility to choose the appropriate state
 		// for their case before disconnecting. This is also desirable in case of
@@ -1162,7 +1174,7 @@ func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
 		// overridden by an explicit request to terminate.
 		killProcess := s.config.Debugger.AttachPid == 0 || request.Arguments.TerminateDebuggee
 		err = s.stopDebugSession(killProcess)
-	} else {
+	} else if s.noDebugProcess != nil {
 		s.stopNoDebugProcess()
 	}
 	if err != nil {
@@ -1179,7 +1191,12 @@ func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
 // Returns any detach error other than proc.ErrProcessExited.
 func (s *Session) stopDebugSession(killProcess bool) error {
 	s.changeStateMu.Lock()
-	defer s.changeStateMu.Unlock()
+	defer func() {
+		// Avoid running stop sequence twice.
+		// It's not fatal, but will result in duplicate logging.
+		s.debugger = nil
+		s.changeStateMu.Unlock()
+	}()
 	if s.debugger == nil {
 		return nil
 	}
@@ -1539,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)
 	}
@@ -1585,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,
@@ -1800,25 +1819,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
 	}
-	return 0, nil
+	bp := g.Thread.Breakpoint()
+	if bp == nil || bp.Breakpoint == nil {
+		return goid, nil
+	}
+	return goid, api.ConvertBreakpoint(bp.Breakpoint)
 }
 
 // stepUntilStopAndNotify is a wrapper around runUntilStopAndNotify that
@@ -1900,7 +1918,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
 	}
@@ -1944,7 +1962,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),
@@ -1995,7 +2013,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
@@ -2030,7 +2048,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 {
@@ -2549,6 +2567,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
@@ -2568,9 +2587,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)
@@ -2592,7 +2622,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
@@ -3185,7 +3215,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
 	}
@@ -3508,12 +3538,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:
@@ -3528,17 +3559,42 @@ 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 {


Test patch:
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/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 7a36679c..d1d8e508 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -111,39 +111,189 @@ func startDAPServer(t *testing.T, serverStopped chan struct{}) (server *Server,
 	return server, forceStop
 }
 
-func TestForceStopNoClient(t *testing.T) {
-	serverStopped := make(chan struct{})
-	_, forceStop := startDAPServer(t, serverStopped)
-	close(forceStop)
-	<-serverStopped
+func verifyServerStopped(t *testing.T, server *Server) {
+	t.Helper()
+	if server.listener != nil {
+		if server.listener.Close() == nil {
+			t.Error("server should have closed listener after shutdown")
+		}
+	}
+	verifySessionStopped(t, server.session)
 }
 
-func TestForceStopNoTarget(t *testing.T) {
-	serverStopped := make(chan struct{})
-	server, forceStop := startDAPServer(t, serverStopped)
-	client := daptest.NewClient(server.config.Listener.Addr().String())
-	defer client.Close()
+func verifySessionStopped(t *testing.T, session *Session) {
+	t.Helper()
+	if session == nil {
+		return
+	}
+	if session.conn == nil {
+		t.Error("session must always have a set connection")
+	}
+	verifyConnStopped(t, session.conn)
+	if session.debugger != nil {
+		t.Error("session should have no pointer to debugger after shutdown")
+	}
+	if session.binaryToRemove != "" {
+		t.Error("session should have no binary to remove after shutdown")
+	}
+}
 
-	client.InitializeRequest()
-	client.ExpectInitializeResponseAndCapabilities(t)
-	close(forceStop)
-	<-serverStopped
+func verifyConnStopped(t *testing.T, conn io.ReadWriteCloser) {
+	t.Helper()
+	if conn.Close() == nil {
+		t.Error("client connection should be closed after shutdown")
+	}
 }
 
-func TestForceStopWithTarget(t *testing.T) {
-	serverStopped := make(chan struct{})
-	server, forceStop := startDAPServer(t, serverStopped)
-	client := daptest.NewClient(server.config.Listener.Addr().String())
-	defer client.Close()
+func TestStopNoClient(t *testing.T) {
+	for name, triggerStop := range map[string]func(s *Server, forceStop chan struct{}){
+		"force":        func(s *Server, forceStop chan struct{}) { close(forceStop) },
+		"accept error": func(s *Server, forceStop chan struct{}) { s.config.Listener.Close() },
+	} {
+		t.Run(name, func(t *testing.T) {
+			serverStopped := make(chan struct{})
+			server, forceStop := startDAPServer(t, serverStopped)
+			triggerStop(server, forceStop)
+			<-serverStopped
+			verifyServerStopped(t, server)
+		})
+	}
+}
 
-	client.InitializeRequest()
-	client.ExpectInitializeResponseAndCapabilities(t)
-	fixture := protest.BuildFixture("increment", protest.AllNonOptimized)
-	client.LaunchRequest("exec", fixture.Path, stopOnEntry)
-	client.ExpectInitializedEvent(t)
-	client.ExpectLaunchResponse(t)
-	close(forceStop)
-	<-serverStopped
+func TestStopNoTarget(t *testing.T) {
+	for name, triggerStop := range map[string]func(c *daptest.Client, forceStop chan struct{}){
+		"force":      func(c *daptest.Client, forceStop chan struct{}) { close(forceStop) },
+		"read error": func(c *daptest.Client, forceStop chan struct{}) { c.Close() },
+		"disconnect": func(c *daptest.Client, forceStop chan struct{}) { c.DisconnectRequest() },
+	} {
+		t.Run(name, func(t *testing.T) {
+			serverStopped := make(chan struct{})
+			server, forceStop := startDAPServer(t, serverStopped)
+			client := daptest.NewClient(server.config.Listener.Addr().String())
+			defer client.Close()
+
+			client.InitializeRequest()
+			client.ExpectInitializeResponseAndCapabilities(t)
+			triggerStop(client, forceStop)
+			<-serverStopped
+			verifyServerStopped(t, server)
+		})
+	}
+}
+
+func TestStopWithTarget(t *testing.T) {
+	for name, triggerStop := range map[string]func(c *daptest.Client, forceStop chan struct{}){
+		"force":                  func(c *daptest.Client, forceStop chan struct{}) { close(forceStop) },
+		"read error":             func(c *daptest.Client, forceStop chan struct{}) { c.Close() },
+		"disconnect before exit": func(c *daptest.Client, forceStop chan struct{}) { c.DisconnectRequest() },
+		"disconnect after  exit": func(c *daptest.Client, forceStop chan struct{}) {
+			c.ContinueRequest(1)
+			c.ExpectContinueResponse(t)
+			c.ExpectTerminatedEvent(t)
+			c.DisconnectRequest()
+		},
+	} {
+		t.Run(name, func(t *testing.T) {
+			serverStopped := make(chan struct{})
+			server, forceStop := startDAPServer(t, serverStopped)
+			client := daptest.NewClient(server.config.Listener.Addr().String())
+			defer client.Close()
+
+			client.InitializeRequest()
+			client.ExpectInitializeResponseAndCapabilities(t)
+			fixture := protest.BuildFixture("increment", protest.AllNonOptimized)
+			client.LaunchRequest("debug", fixture.Source, stopOnEntry)
+			client.ExpectInitializedEvent(t)
+			client.ExpectLaunchResponse(t)
+			triggerStop(client, forceStop)
+			<-serverStopped
+			verifyServerStopped(t, server)
+		})
+	}
+}
+
+func TestSessionStop(t *testing.T) {
+	verifySessionState := func(t *testing.T, s *Session, binaryToRemoveSet bool, debuggerSet bool, disconnectChanSet bool) {
+		t.Helper()
+		if binaryToRemoveSet && s.binaryToRemove == "" || !binaryToRemoveSet && s.binaryToRemove != "" {
+			t.Errorf("binaryToRemove: got %s, want set=%v", s.binaryToRemove, binaryToRemoveSet)
+		}
+		if debuggerSet && s.debugger == nil || !debuggerSet && s.debugger != nil {
+			t.Errorf("debugger: got %v, want set=%v", s.debugger, debuggerSet)
+		}
+		if disconnectChanSet && s.config.DisconnectChan == nil || !disconnectChanSet && s.config.DisconnectChan != nil {
+			t.Errorf("disconnectChan: got %v, want set=%v", s.config.DisconnectChan, disconnectChanSet)
+		}
+	}
+	for name, stopSession := range map[string]func(s *Session, c *daptest.Client, serveDone chan struct{}){
+		"force": func(s *Session, c *daptest.Client, serveDone chan struct{}) {
+			s.Close()
+			<-serveDone
+			verifySessionState(t, s, false /*binaryToRemoveSet*/, false /*debuggerSet*/, false /*disconnectChanSet*/)
+		},
+		"read error": func(s *Session, c *daptest.Client, serveDone chan struct{}) {
+			c.Close()
+			<-serveDone
+			verifyConnStopped(t, s.conn)
+			verifySessionState(t, s, true /*binaryToRemoveSet*/, true /*debuggerSet*/, false /*disconnectChanSet*/)
+			s.Close()
+		},
+		"disconnect before exit": func(s *Session, c *daptest.Client, serveDone chan struct{}) {
+			c.DisconnectRequest()
+			<-serveDone
+			verifyConnStopped(t, s.conn)
+			verifySessionState(t, s, true /*binaryToRemoveSet*/, false /*debuggerSet*/, false /*disconnectChanSet*/)
+			s.Close()
+		},
+		"disconnect after exit": func(s *Session, c *daptest.Client, serveDone chan struct{}) {
+			c.ContinueRequest(1)
+			c.ExpectContinueResponse(t)
+			c.ExpectTerminatedEvent(t)
+			c.DisconnectRequest()
+			<-serveDone
+			verifyConnStopped(t, s.conn)
+			verifySessionState(t, s, true /*binaryToRemoveSet*/, false /*debuggerSet*/, false /*disconnectChanSet*/)
+			s.Close()
+		},
+	} {
+		t.Run(name, func(t *testing.T) {
+			listener, err := net.Listen("tcp", ":0")
+			if err != nil {
+				t.Fatalf("cannot setup listener required for testing: %v", err)
+			}
+			defer listener.Close()
+			acceptDone := make(chan struct{})
+			var conn net.Conn
+			go func() {
+				conn, err = listener.Accept()
+				close(acceptDone)
+			}()
+			time.Sleep(10 * time.Millisecond) // give time to start listening
+			client := daptest.NewClient(listener.Addr().String())
+			defer client.Close()
+			<-acceptDone
+			if err != nil {
+				t.Fatalf("cannot accept client requireed for testing: %v", err)
+			}
+			session := NewSession(conn, &Config{
+				Config:        &service.Config{DisconnectChan: make(chan struct{})},
+				StopTriggered: make(chan struct{})}, nil)
+			serveDAPCodecDone := make(chan struct{})
+			go func() {
+				session.ServeDAPCodec()
+				close(serveDAPCodecDone)
+			}()
+			time.Sleep(10 * time.Millisecond) // give time to start reading
+			client.InitializeRequest()
+			client.ExpectInitializeResponseAndCapabilities(t)
+			fixture := protest.BuildFixture("increment", protest.AllNonOptimized)
+			client.LaunchRequest("debug", fixture.Source, stopOnEntry)
+			client.ExpectInitializedEvent(t)
+			client.ExpectLaunchResponse(t)
+			stopSession(session, client, serveDAPCodecDone)
+			verifySessionStopped(t, session)
+		})
+	}
 }
 
 func TestForceStopWhileStopping(t *testing.T) {
@@ -160,6 +310,7 @@ func TestForceStopWhileStopping(t *testing.T) {
 	time.Sleep(time.Microsecond)
 	close(forceStop) // depending on timing may trigger Stop()
 	<-serverStopped
+	verifyServerStopped(t, server)
 }
 
 // TestLaunchStopOnEntry emulates the message exchange that can be observed with
@@ -693,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)
 	}
 }
@@ -912,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,
 			}})
@@ -2691,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,
@@ -3104,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) {
@@ -3654,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
@@ -4019,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) {
@@ -6043,22 +6334,23 @@ func TestAttachRemoteToRunningTargetContinueOnEntry(t *testing.T) {
 // TestMultiClient tests that that remote attach doesn't take down
 // the server in multi-client mode unless terminateDebugee is explicitely set.
 func TestAttachRemoteMultiClient(t *testing.T) {
-	closingClientSession := "Closing client session, but leaving multi-client DAP server running at"
+	closingClientSessionOnly := "Closing client session, but leaving multi-client DAP server running at"
 	detachingAndTerminating := "Detaching and terminating target process"
 	tests := []struct {
 		name              string
 		disconnectRequest func(client *daptest.Client)
 		expect            string
 	}{
-		{"default", func(c *daptest.Client) { c.DisconnectRequest() }, closingClientSession},
+		{"default", func(c *daptest.Client) { c.DisconnectRequest() }, closingClientSessionOnly},
 		{"terminate=true", func(c *daptest.Client) { c.DisconnectRequestWithKillOption(true) }, detachingAndTerminating},
-		{"terminate=false", func(c *daptest.Client) { c.DisconnectRequestWithKillOption(false) }, closingClientSession},
+		{"terminate=false", func(c *daptest.Client) { c.DisconnectRequestWithKillOption(false) }, closingClientSessionOnly},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
 			serverStopped := make(chan struct{})
 			server, forceStop := startDAPServer(t, serverStopped)
 			client := daptest.NewClient(server.listener.Addr().String())
+			defer client.Close()
 			time.Sleep(100 * time.Millisecond) // Give time for connection to be set as dap.Session
 			server.sessionMu.Lock()
 			if server.session == nil {
@@ -6087,12 +6379,13 @@ func TestAttachRemoteMultiClient(t *testing.T) {
 			}
 			client.ExpectDisconnectResponse(t)
 			client.ExpectTerminatedEvent(t)
-			client.Close()
+			time.Sleep(10 * time.Millisecond) // give time for things to shut down
 
-			if tc.expect == closingClientSession {
-				// At this point a multi-client server is still running, but since
-				// it is a dap server, it cannot accept another client, so the only
-				// way to take down the server is too force kill it.
+			if tc.expect == closingClientSessionOnly {
+				// At this point a multi-client server is still running.
+				verifySessionStopped(t, server.session)
+				// Since it is a dap server, it cannot accept another client, so the only
+				// way to take down the server is to force-kill it.
 				close(forceStop)
 			}
 			<-serverStopped