Repository: go-delve/delve
Pull request #2750 (2021-10-15 19:55:51)

service/dap: add config expressions to evaluate requests
We want to provide more flexibility for users to make changes to their configurations while the debug session is running. This could be accomplished by creating a custom request, but that were require a new UI as well, and every client of dlv dap to provide its own UI for this. By using the evaluate context, users can use the already existing debug console to change their configurations.

This change includes a refactor of the terminal code in order to share the code with the dap package.

This change provides a very similar to UI as the terminal package, but there are different configuration options that are DAP specific. We plan to use this same mechanism to expose a few other commands including "sources" to help users debug an ineffective substitutePath configuration.


Patch:
diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go
index 33ce65cc..15df5f63 100644
--- a/cmd/dlv/cmds/commands.go
+++ b/cmd/dlv/cmds/commands.go
@@ -673,6 +673,7 @@ func traceCmd(cmd *cobra.Command, args []string) {
 			done := make(chan struct{})
 			defer close(done)
 			go func() {
+				gFnEntrySeen := map[int]struct{}{}
 				for {
 					select {
 					case <-done:
@@ -694,7 +695,16 @@ func traceCmd(cmd *cobra.Command, args []string) {
 									params.WriteString(p.Value)
 								}
 							}
-							fmt.Fprintf(os.Stderr, "> (%d) %s(%s)\n", t.GoroutineID, t.FunctionName, params.String())
+							_, seen := gFnEntrySeen[t.GoroutineID]
+							if seen {
+								for _, p := range t.ReturnParams {
+									fmt.Fprintf(os.Stderr, "=> %#v\n", p.Value)
+								}
+								delete(gFnEntrySeen, t.GoroutineID)
+							} else {
+								gFnEntrySeen[t.GoroutineID] = struct{}{}
+								fmt.Fprintf(os.Stderr, "> (%d) %s(%s)\n", t.GoroutineID, t.FunctionName, params.String())
+							}
 						}
 					}
 				}
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/breakpoints.go b/pkg/proc/breakpoints.go
index b8434c4f..16019f2d 100644
--- a/pkg/proc/breakpoints.go
+++ b/pkg/proc/breakpoints.go
@@ -494,7 +494,6 @@ func (t *Target) SetEBPFTracepoint(fnName string) error {
 	}
 	// Start putting together the argument map. This will tell the eBPF program
 	// all of the arguments we want to trace and how to find them.
-	var args []ebpf.UProbeArgMap
 	fn, ok := t.BinInfo().LookupFunc[fnName]
 	if !ok {
 		return fmt.Errorf("could not find function %s", fnName)
@@ -533,16 +532,14 @@ func (t *Target) SetEBPFTracepoint(fnName string) error {
 	}
 	_, l, _ := t.BinInfo().PCToLine(fn.Entry)
 
+	var args []ebpf.UProbeArgMap
 	varEntries := reader.Variables(dwarfTree, fn.Entry, l, variablesFlags)
 	for _, entry := range varEntries {
-		isret, _ := entry.Val(dwarf.AttrVarParam).(bool)
-		if isret {
-			continue
-		}
 		_, dt, err := readVarEntry(entry.Tree, fn.cu.image)
 		if err != nil {
 			return err
 		}
+
 		offset, pieces, _, err := t.BinInfo().Location(entry, dwarf.AttrLocation, fn.Entry, op.DwarfRegisters{}, nil)
 		if err != nil {
 			return err
@@ -553,8 +550,16 @@ func (t *Target) SetEBPFTracepoint(fnName string) error {
 				paramPieces = append(paramPieces, int(piece.Val))
 			}
 		}
+		isret, _ := entry.Val(dwarf.AttrVarParam).(bool)
 		offset += int64(t.BinInfo().Arch.PtrSize())
-		args = append(args, ebpf.UProbeArgMap{Offset: offset, Size: dt.Size(), Kind: dt.Common().ReflectKind, Pieces: paramPieces, InReg: len(pieces) > 0})
+		args = append(args, ebpf.UProbeArgMap{
+			Offset: offset,
+			Size:   dt.Size(),
+			Kind:   dt.Common().ReflectKind,
+			Pieces: paramPieces,
+			InReg:  len(pieces) > 0,
+			Ret:    isret,
+		})
 	}
 
 	// Finally, set the uprobe on the function.
diff --git a/pkg/proc/core/core.go b/pkg/proc/core/core.go
index e28e04e4..392d9f93 100644
--- a/pkg/proc/core/core.go
+++ b/pkg/proc/core/core.go
@@ -281,6 +281,14 @@ func (dbp *process) SetUProbe(fnName string, goidOffset int64, args []ebpf.UProb
 // StartCallInjection notifies the backend that we are about to inject a function call.
 func (p *process) StartCallInjection() (func(), error) { return func() {}, nil }
 
+func (dbp *process) EnableURetProbes() error {
+	panic("not implemented")
+}
+
+func (dbp *process) DisableURetProbes() error {
+	panic("not implemented")
+}
+
 // ReadMemory will return memory from the core file at the specified location and put the
 // read memory into `data`, returning the length read, and returning an error if
 // the length read is shorter than the length of the `data` buffer.
diff --git a/pkg/proc/internal/ebpf/context.go b/pkg/proc/internal/ebpf/context.go
index c457dbc2..695b8105 100644
--- a/pkg/proc/internal/ebpf/context.go
+++ b/pkg/proc/internal/ebpf/context.go
@@ -13,6 +13,7 @@ type UProbeArgMap struct {
 	Kind   reflect.Kind // Kind of variable.
 	Pieces []int        // Pieces of the variables as stored in registers.
 	InReg  bool         // True if this param is contained in a register.
+	Ret    bool         // True if this param is a return value.
 }
 
 type RawUProbeParam struct {
@@ -26,7 +27,8 @@ type RawUProbeParam struct {
 }
 
 type RawUProbeParams struct {
-	FnAddr      int
-	GoroutineID int
-	InputParams []*RawUProbeParam
+	FnAddr       int
+	GoroutineID  int
+	InputParams  []*RawUProbeParam
+	ReturnParams []*RawUProbeParam
 }
diff --git a/pkg/proc/internal/ebpf/helpers.go b/pkg/proc/internal/ebpf/helpers.go
index 2936f2ca..0a0c66d0 100644
--- a/pkg/proc/internal/ebpf/helpers.go
+++ b/pkg/proc/internal/ebpf/helpers.go
@@ -6,6 +6,7 @@ package ebpf
 // #include "./trace_probe/function_vals.bpf.h"
 import "C"
 import (
+	"debug/elf"
 	_ "embed"
 	"encoding/binary"
 	"errors"
@@ -51,11 +52,11 @@ func (ctx *EBPFContext) AttachUprobe(pid int, name string, offset uint32) error
 	return err
 }
 
-func (ctx *EBPFContext) UpdateArgMap(key uint64, goidOffset int64, args []UProbeArgMap, gAddrOffset uint64) error {
+func (ctx *EBPFContext) UpdateArgMap(key uint64, goidOffset int64, args []UProbeArgMap, gAddrOffset uint64, isret bool) error {
 	if ctx.bpfArgMap == nil {
 		return errors.New("eBPF map not loaded")
 	}
-	params := createFunctionParameterList(key, goidOffset, args)
+	params := createFunctionParameterList(key, goidOffset, args, isret)
 	params.g_addr_offset = C.longlong(gAddrOffset)
 	return ctx.bpfArgMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&params))
 }
@@ -82,7 +83,7 @@ func LoadEBPFTracingProgram() (*EBPFContext, error) {
 	var ctx EBPFContext
 	var err error
 
-	ctx.bpfModule, err = bpf.NewModuleFromBuffer(TraceProbeBytes, "trace.o")
+	ctx.bpfModule, err = bpf.NewModuleFromBuffer(TraceProbeBytes, "trace_probe/trace.o")
 	if err != nil {
 		return nil, err
 	}
@@ -114,7 +115,7 @@ func LoadEBPFTracingProgram() (*EBPFContext, error) {
 				return
 			}
 
-			parsed := ParseFunctionParameterList(b)
+			parsed := parseFunctionParameterList(b)
 
 			ctx.m.Lock()
 			ctx.parsedBpfEvents = append(ctx.parsedBpfEvents, parsed)
@@ -125,7 +126,7 @@ func LoadEBPFTracingProgram() (*EBPFContext, error) {
 	return &ctx, nil
 }
 
-func ParseFunctionParameterList(rawParamBytes []byte) RawUProbeParams {
+func parseFunctionParameterList(rawParamBytes []byte) RawUProbeParams {
 	params := (*C.function_parameter_list_t)(unsafe.Pointer(&rawParamBytes[0]))
 
 	defer runtime.KeepAlive(params) // Ensure the param is not garbage collected.
@@ -134,10 +135,10 @@ func ParseFunctionParameterList(rawParamBytes []byte) RawUProbeParams {
 	rawParams.FnAddr = int(params.fn_addr)
 	rawParams.GoroutineID = int(params.goroutine_id)
 
-	for i := 0; i < int(params.n_parameters); i++ {
+	parseParam := func(param C.function_parameter_t) *RawUProbeParam {
 		iparam := &RawUProbeParam{}
 		data := make([]byte, 0x60)
-		ret := params.params[i]
+		ret := param
 		iparam.Kind = reflect.Kind(ret.kind)
 
 		val := C.GoBytes(unsafe.Pointer(&ret.val), C.int(ret.size))
@@ -161,22 +162,30 @@ func ParseFunctionParameterList(rawParamBytes []byte) RawUProbeParams {
 			iparam.Base = FakeAddressBase + 0x30
 			iparam.Len = int64(strLen)
 		}
+		return iparam
+	}
 
-		rawParams.InputParams = append(rawParams.InputParams, iparam)
+	for i := 0; i < int(params.n_parameters); i++ {
+		rawParams.InputParams = append(rawParams.InputParams, parseParam(params.params[i]))
+	}
+	for i := 0; i < int(params.n_ret_parameters); i++ {
+		rawParams.ReturnParams = append(rawParams.ReturnParams, parseParam(params.ret_params[i]))
 	}
 
 	return rawParams
 }
 
-func createFunctionParameterList(entry uint64, goidOffset int64, args []UProbeArgMap) C.function_parameter_list_t {
+func createFunctionParameterList(entry uint64, goidOffset int64, args []UProbeArgMap, isret bool) C.function_parameter_list_t {
 	var params C.function_parameter_list_t
 	params.goid_offset = C.uint(goidOffset)
-	params.n_parameters = C.uint(len(args))
 	params.fn_addr = C.uint(entry)
-	for i, arg := range args {
+	params.is_ret = C.bool(isret)
+	params.n_parameters = C.uint(0)
+	params.n_ret_parameters = C.uint(0)
+	for _, arg := range args {
 		var param C.function_parameter_t
 		param.size = C.uint(arg.Size)
-		param.offset = C.uint(arg.Offset)
+		param.offset = C.int(arg.Offset)
 		param.kind = C.uint(arg.Kind)
 		if arg.InReg {
 			param.in_reg = true
@@ -188,7 +197,40 @@ func createFunctionParameterList(entry uint64, goidOffset int64, args []UProbeAr
 				param.reg_nums[i] = C.int(arg.Pieces[i])
 			}
 		}
-		params.params[i] = param
+		if !arg.Ret {
+			params.params[params.n_parameters] = param
+			params.n_parameters++
+		} else {
+			params.ret_params[params.n_ret_parameters] = param
+			params.n_ret_parameters++
+		}
 	}
 	return params
 }
+
+func AddressToOffset(f *elf.File, addr uint64) (uint32, error) {
+	sectionsToSearchForSymbol := []*elf.Section{}
+
+	for i := range f.Sections {
+		if f.Sections[i].Flags == elf.SHF_ALLOC+elf.SHF_EXECINSTR {
+			sectionsToSearchForSymbol = append(sectionsToSearchForSymbol, f.Sections[i])
+		}
+	}
+
+	var executableSection *elf.Section
+
+	// Find what section the symbol is in by checking the executable section's
+	// addr space.
+	for m := range sectionsToSearchForSymbol {
+		if addr > sectionsToSearchForSymbol[m].Addr &&
+			addr < sectionsToSearchForSymbol[m].Addr+sectionsToSearchForSymbol[m].Size {
+			executableSection = sectionsToSearchForSymbol[m]
+		}
+	}
+
+	if executableSection == nil {
+		return 0, errors.New("could not find symbol in executable sections of binary")
+	}
+
+	return uint32(addr - executableSection.Addr + executableSection.Offset), nil
+}
diff --git a/pkg/proc/internal/ebpf/helpers_disabled.go b/pkg/proc/internal/ebpf/helpers_disabled.go
index 8ea90950..1dc274ea 100644
--- a/pkg/proc/internal/ebpf/helpers_disabled.go
+++ b/pkg/proc/internal/ebpf/helpers_disabled.go
@@ -4,6 +4,7 @@
 package ebpf
 
 import (
+	"debug/elf"
 	"errors"
 )
 
@@ -18,7 +19,11 @@ func (ctx *EBPFContext) AttachUprobe(pid int, name string, offset uint32) error
 	return errors.New("eBPF is disabled")
 }
 
-func (ctx *EBPFContext) UpdateArgMap(key uint64, goidOffset int64, args []UProbeArgMap, gAddrOffset uint64) error {
+func (ctx *EBPFContext) AttachURetprobe(pid int, name string, offset uint32) error {
+	return errors.New("eBPF is disabled")
+}
+
+func (ctx *EBPFContext) UpdateArgMap(key uint64, goidOffset int64, args []UProbeArgMap, gAddrOffset uint64, isret bool) error {
 	return errors.New("eBPF is disabled")
 }
 
@@ -34,6 +39,6 @@ func LoadEBPFTracingProgram() (*EBPFContext, error) {
 	return nil, errors.New("eBPF disabled")
 }
 
-func ParseFunctionParameterList(rawParamBytes []byte) RawUProbeParams {
-	return RawUProbeParams{}
+func AddressToOffset(f *elf.File, addr uint64) (uint32, error) {
+	return 0, errors.New("eBPF disabled")
 }
diff --git a/pkg/proc/internal/ebpf/trace_probe/function_vals.bpf.h b/pkg/proc/internal/ebpf/trace_probe/function_vals.bpf.h
index fec16670..0df7b9b7 100644
--- a/pkg/proc/internal/ebpf/trace_probe/function_vals.bpf.h
+++ b/pkg/proc/internal/ebpf/trace_probe/function_vals.bpf.h
@@ -8,7 +8,7 @@ typedef struct function_parameter {
       unsigned int size; 
 
       // Offset from stack pointer. This should only be set from the Go side.
-      unsigned int offset; 
+      int offset;
 
       // If true, the parameter is passed in a register.
       bool in_reg;      
@@ -20,7 +20,7 @@ typedef struct function_parameter {
       int reg_nums[6]; 
 
       // The following are filled in by the eBPF program.
-      unsigned int daddr;   // Data address.
+      size_t daddr;   // Data address.
       char val[0x30];       // Value of the parameter.
       char deref_val[0x30]; // Dereference value of the parameter.
 } function_parameter_t;
@@ -33,6 +33,11 @@ typedef struct function_parameter_list {
       int goroutine_id;
 
       unsigned int fn_addr;
+      bool is_ret;
+
       unsigned int n_parameters;          // number of parameters.
       function_parameter_t params[6];     // list of parameters.
+
+      unsigned int n_ret_parameters;      // number of return parameters.
+      function_parameter_t ret_params[6]; // list of return parameters.
 } function_parameter_list_t;
diff --git a/pkg/proc/internal/ebpf/trace_probe/trace.bpf.c b/pkg/proc/internal/ebpf/trace_probe/trace.bpf.c
index 505981c2..39fdff22 100644
--- a/pkg/proc/internal/ebpf/trace_probe/trace.bpf.c
+++ b/pkg/proc/internal/ebpf/trace_probe/trace.bpf.c
@@ -124,9 +124,9 @@ int parse_param(struct pt_regs *ctx, function_parameter_t *param) {
     // a slice we will need some further processing below.
     int ret = 0;
     if (param->in_reg) {
-        parse_param_registers(ctx, param);
+        ret = parse_param_registers(ctx, param);
     } else {
-        parse_param_stack(ctx, param);
+        ret = parse_param_stack(ctx, param);
     }
     if (ret != 0) {
         return ret;
@@ -176,11 +176,30 @@ int get_goroutine_id(function_parameter_list_t *parsed_args) {
     return 1;
 }
 
+__always_inline
+void parse_params(struct pt_regs *ctx, unsigned int n_params, function_parameter_t params[6]) {
+    // Since we cannot loop in eBPF programs let's take adavantage of the
+    // fact that in C switch cases will pass through automatically.
+    switch (n_params) {
+        case 6:
+            parse_param(ctx, &params[5]);
+        case 5:
+            parse_param(ctx, &params[4]);
+        case 4:
+            parse_param(ctx, &params[3]);
+        case 3:
+            parse_param(ctx, &params[2]);
+        case 2:
+            parse_param(ctx, &params[1]);
+        case 1:
+            parse_param(ctx, &params[0]);
+    }
+}
+
 SEC("uprobe/dlv_trace")
 int uprobe__dlv_trace(struct pt_regs *ctx) {
     function_parameter_list_t *args;
     function_parameter_list_t *parsed_args;
-    function_parameter_t param;
     uint64_t key = ctx->ip;
 
     args = bpf_map_lookup_elem(&arg_map, &key);
@@ -192,28 +211,32 @@ int uprobe__dlv_trace(struct pt_regs *ctx) {
     if (!parsed_args) {
         return 1;
     }
-    memcpy(parsed_args, args, sizeof(function_parameter_list_t));
+
+    // Initialize the parsed_args struct.
+    parsed_args->goid_offset = args->goid_offset;
+    parsed_args->g_addr_offset = args->g_addr_offset;
+    parsed_args->goroutine_id = args->goroutine_id;
+    parsed_args->fn_addr = args->fn_addr;
+    parsed_args->n_parameters = args->n_parameters;
+    parsed_args->n_ret_parameters = args->n_ret_parameters;
+    memcpy(parsed_args->params, args->params, sizeof(args->params));
+    memcpy(parsed_args->ret_params, args->ret_params, sizeof(args->ret_params));
 
     if (!get_goroutine_id(parsed_args)) {
         bpf_ringbuf_discard(parsed_args, 0);
         return 1;
     }
 
-    // Since we cannot loop in eBPF programs let's take adavantage of the
-    // fact that in C switch cases will pass through automatically.
-    switch (args->n_parameters) {
-        case 6:
-            parse_param(ctx, &parsed_args->params[5]);
-        case 5:
-            parse_param(ctx, &parsed_args->params[4]);
-        case 4:
-            parse_param(ctx, &parsed_args->params[3]);
-        case 3:
-            parse_param(ctx, &parsed_args->params[2]);
-        case 2:
-            parse_param(ctx, &parsed_args->params[1]);
-        case 1:
-            parse_param(ctx, &parsed_args->params[0]);
+    if (!args->is_ret) {
+        // In uprobe at function entry.
+
+        // Parse input parameters.
+        parse_params(ctx, args->n_parameters, parsed_args->params);
+    } else {
+        // We are now stopped at the RET instruction for this function.
+
+        // Parse output parameters.
+        parse_params(ctx, args->n_ret_parameters, parsed_args->ret_params);
     }
 
     bpf_ringbuf_submit(parsed_args, 0);
diff --git a/pkg/proc/internal/ebpf/trace_probe/trace.bpf.h b/pkg/proc/internal/ebpf/trace_probe/trace.bpf.h
index b46d2025..971db59f 100644
--- a/pkg/proc/internal/ebpf/trace_probe/trace.bpf.h
+++ b/pkg/proc/internal/ebpf/trace_probe/trace.bpf.h
@@ -16,6 +16,7 @@ struct {
    __uint(max_entries, BPF_MAX_VAR_SIZ);
 } heap SEC(".maps");
 
+// Map which uses instruction address as key and function parameter info as the value.
 struct {
     __uint(max_entries, 42);
     __uint(type, BPF_MAP_TYPE_HASH);
diff --git a/pkg/proc/native/proc_linux.go b/pkg/proc/native/proc_linux.go
index 98734246..6b5613a2 100644
--- a/pkg/proc/native/proc_linux.go
+++ b/pkg/proc/native/proc_linux.go
@@ -3,6 +3,7 @@ package native
 import (
 	"bufio"
 	"bytes"
+	"debug/elf"
 	"errors"
 	"fmt"
 	"io/ioutil"
@@ -708,13 +709,18 @@ func (dbp *nativeProcess) EntryPoint() (uint64, error) {
 func (dbp *nativeProcess) SetUProbe(fnName string, goidOffset int64, args []ebpf.UProbeArgMap) error {
 	// Lazily load and initialize the BPF program upon request to set a uprobe.
 	if dbp.os.ebpf == nil {
-		dbp.os.ebpf, _ = ebpf.LoadEBPFTracingProgram()
+		var err error
+		dbp.os.ebpf, err = ebpf.LoadEBPFTracingProgram()
+		if err != nil {
+			return err
+		}
 	}
 
-	// We only allow up to 6 args for a BPF probe.
+	// We only allow up to 12 args for a BPF probe.
+	// 6 inputs + 6 outputs.
 	// Return early if we have more.
-	if len(args) > 6 {
-		return errors.New("too many arguments in traced function, max is 6")
+	if len(args) > 12 {
+		return errors.New("too many arguments in traced function, max is 12 input+return")
 	}
 
 	fn, ok := dbp.bi.LookupFunc[fnName]
@@ -723,7 +729,7 @@ func (dbp *nativeProcess) SetUProbe(fnName string, goidOffset int64, args []ebpf
 	}
 
 	key := fn.Entry
-	err := dbp.os.ebpf.UpdateArgMap(key, goidOffset, args, dbp.BinInfo().GStructOffset())
+	err := dbp.os.ebpf.UpdateArgMap(key, goidOffset, args, dbp.BinInfo().GStructOffset(), false)
 	if err != nil {
 		return err
 	}
@@ -733,6 +739,49 @@ func (dbp *nativeProcess) SetUProbe(fnName string, goidOffset int64, args []ebpf
 	if err != nil {
 		return err
 	}
+
+	// First attach a uprobe at all return addresses. We do this instead of using a uretprobe
+	// for two reasons:
+	// 1. uretprobes do not play well with Go
+	// 2. uretprobes seem to not restore the function return addr on the stack when removed, destroying any
+	//    kind of workaround we could come up with.
+	// TODO(derekparker): this whole thing could likely be optimized a bit.
+	img := dbp.BinInfo().PCToImage(fn.Entry)
+	f, err := elf.Open(img.Path)
+	if err != nil {
+		return fmt.Errorf("could not open elf file to resolve symbol offset: %w", err)
+	}
+
+	var regs proc.Registers
+	mem := dbp.Memory()
+	regs, _ = dbp.memthread.Registers()
+	instructions, err := proc.Disassemble(mem, regs, &proc.BreakpointMap{}, dbp.BinInfo(), fn.Entry, fn.End)
+	if err != nil {
+		return err
+	}
+
+	var addrs []uint64
+	for _, instruction := range instructions {
+		if instruction.IsRet() {
+			addrs = append(addrs, instruction.Loc.PC)
+		}
+	}
+	addrs = append(addrs, proc.FindDeferReturnCalls(instructions)...)
+	for _, addr := range addrs {
+		err := dbp.os.ebpf.UpdateArgMap(addr, goidOffset, args, dbp.BinInfo().GStructOffset(), true)
+		if err != nil {
+			return err
+		}
+		off, err := ebpf.AddressToOffset(f, addr)
+		if err != nil {
+			return err
+		}
+		err = dbp.os.ebpf.AttachUprobe(dbp.Pid(), debugname, off)
+		if err != nil {
+			return err
+		}
+	}
+
 	return dbp.os.ebpf.AttachUprobe(dbp.Pid(), debugname, offset)
 }
 
diff --git a/pkg/proc/target.go b/pkg/proc/target.go
index 33485d87..dc503e19 100644
--- a/pkg/proc/target.go
+++ b/pkg/proc/target.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/go-delve/delve/pkg/dwarf/op"
 	"github.com/go-delve/delve/pkg/goversion"
+	"github.com/go-delve/delve/pkg/proc/internal/ebpf"
 )
 
 var (
@@ -401,35 +402,45 @@ func (t *Target) CurrentThread() Thread {
 }
 
 type UProbeTraceResult struct {
-	FnAddr      int
-	GoroutineID int
-	InputParams []*Variable
+	FnAddr       int
+	GoroutineID  int
+	InputParams  []*Variable
+	ReturnParams []*Variable
 }
 
 func (t *Target) GetBufferedTracepoints() []*UProbeTraceResult {
 	var results []*UProbeTraceResult
 	tracepoints := t.proc.GetBufferedTracepoints()
+	convertInputParamToVariable := func(ip *ebpf.RawUProbeParam) *Variable {
+		v := &Variable{}
+		v.RealType = ip.RealType
+		v.Len = ip.Len
+		v.Base = ip.Base
+		v.Addr = ip.Addr
+		v.Kind = ip.Kind
+
+		cachedMem := CreateLoadedCachedMemory(ip.Data)
+		compMem, _ := CreateCompositeMemory(cachedMem, t.BinInfo().Arch, op.DwarfRegisters{}, ip.Pieces)
+		v.mem = compMem
+
+		// Load the value here so that we don't have to export
+		// loadValue outside of proc.
+		v.loadValue(loadFullValue)
+
+		return v
+	}
 	for _, tp := range tracepoints {
 		r := &UProbeTraceResult{}
 		r.FnAddr = tp.FnAddr
 		r.GoroutineID = tp.GoroutineID
 		for _, ip := range tp.InputParams {
-			v := &Variable{}
-			v.RealType = ip.RealType
-			v.Len = ip.Len
-			v.Base = ip.Base
-			v.Addr = ip.Addr
-			v.Kind = ip.Kind
-
-			cachedMem := CreateLoadedCachedMemory(ip.Data)
-			compMem, _ := CreateCompositeMemory(cachedMem, t.BinInfo().Arch, op.DwarfRegisters{}, ip.Pieces)
-			v.mem = compMem
-
-			// Load the value here so that we don't have to export
-			// loadValue outside of proc.
-			v.loadValue(loadFullValue)
+			v := convertInputParamToVariable(ip)
 			r.InputParams = append(r.InputParams, v)
 		}
+		for _, ip := range tp.ReturnParams {
+			v := convertInputParamToVariable(ip)
+			r.ReturnParams = append(r.ReturnParams, v)
+		}
 		results = append(results, r)
 	}
 	return results
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 b185f7fd..2b8d8ced 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -166,9 +166,9 @@ type Config struct {
 
 	// log is used for structured logging.
 	log *logrus.Entry
-	// stopTriggered is closed when the server is Stop()-ed.
+	// StopTriggered is closed when the server is Stop()-ed.
 	// Can be used to safeguard against duplicate shutdown sequences.
-	stopTriggered chan struct{}
+	StopTriggered chan struct{}
 }
 
 type process struct {
@@ -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{},
 }
@@ -277,7 +278,7 @@ func NewServer(config *service.Config) *Server {
 		config: &Config{
 			Config:        config,
 			log:           logger,
-			stopTriggered: make(chan struct{}),
+			StopTriggered: make(chan struct{}),
 		},
 		listener: config.Listener,
 	}
@@ -286,11 +287,14 @@ func NewServer(config *service.Config) *Server {
 // NewSession creates a new client session that can handle DAP traffic.
 // It takes an open connection and provides a Close() method to shut it
 // down when the DAP session disconnects or a connection error occurs.
-func NewSession(conn io.ReadWriteCloser, config *Config) *Session {
+func NewSession(conn io.ReadWriteCloser, config *Config, debugger *debugger.Debugger) *Session {
 	if config.log == nil {
 		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,
@@ -298,6 +302,7 @@ func NewSession(conn io.ReadWriteCloser, config *Config) *Session {
 		variableHandles:   newVariablesHandlesMap(),
 		args:              defaultArgs,
 		exceptionErr:      nil,
+		debugger:          debugger,
 	}
 }
 
@@ -306,11 +311,11 @@ func NewSession(conn io.ReadWriteCloser, config *Config) *Session {
 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))
@@ -328,11 +333,11 @@ func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) error {
 // connection. It shuts down the underlying debugger and kills the target
 // process if it was launched by it or stops the noDebug process.
 // This method mustn't be called more than once.
-// stopTriggered notifies other goroutines that stop is in progreess.
+// StopTriggered notifies other goroutines that stop is in progreess.
 func (s *Server) Stop() {
 	s.config.log.Debug("DAP server stopping...")
 	defer s.config.log.Debug("DAP server stopped")
-	close(s.config.stopTriggered)
+	close(s.config.StopTriggered)
 
 	if s.listener != nil {
 		// If run goroutine is blocked on accept, this will unblock it.
@@ -349,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()
@@ -356,19 +362,25 @@ 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.
-	// Unless Stop() was called after read loop in serveDAPCodec()
+	// Unless Stop() was called after read loop in ServeDAPCodec()
 	// 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()
 }
 
@@ -381,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
@@ -418,7 +424,7 @@ func (s *Server) Run() {
 		conn, err := s.listener.Accept() // listener is closed in Stop()
 		if err != nil {
 			select {
-			case <-s.config.stopTriggered:
+			case <-s.config.StopTriggered:
 			default:
 				s.config.log.Errorf("Error accepting client connection: %s\n", err)
 				s.config.triggerServerStop()
@@ -438,9 +444,9 @@ func (s *Server) Run() {
 
 func (s *Server) runSession(conn io.ReadWriteCloser) {
 	s.sessionMu.Lock()
-	s.session = NewSession(conn, s.config) // closed in Stop()
+	s.session = NewSession(conn, s.config, nil) // closed in Stop()
 	s.sessionMu.Unlock()
-	s.session.serveDAPCodec()
+	s.session.ServeDAPCodec()
 }
 
 // RunWithClient is similar to Run but works only with an already established
@@ -456,11 +462,13 @@ func (s *Server) RunWithClient(conn net.Conn) {
 	go s.runSession(conn)
 }
 
-// serveDAPCodec reads and decodes requests from the client
+// ServeDAPCodec reads and decodes requests from the client
 // 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
+func (s *Session) ServeDAPCodec() {
+	// 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)
@@ -475,8 +483,11 @@ func (s *Session) serveDAPCodec() {
 		if err != nil {
 			s.config.log.Debug("DAP error: ", err)
 			select {
-			case <-s.config.stopTriggered:
+			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.
@@ -485,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
+		}
 	}
 }
 
@@ -1136,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
@@ -1161,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 {
@@ -1178,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
 	}
@@ -1238,10 +1256,12 @@ func (s *Session) stopDebugSession(killProcess bool) error {
 // halt sends a halt request if the debuggee is running.
 // changeStateMu should be held when calling (*Server).halt.
 func (s *Session) halt() (*api.DebuggerState, error) {
+	s.config.log.Debug("halting")
 	// Only send a halt request if the debuggee is running.
 	if s.debugger.IsRunning() {
 		return s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
 	}
+	s.config.log.Debug("process not running")
 	return s.debugger.State(false)
 }
 
@@ -1536,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)
 	}
@@ -1582,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,
@@ -1897,7 +1919,7 @@ func (s *Session) onStackTraceRequest(request *dap.StackTraceRequest) {
 	if start < 0 {
 		start = 0
 	}
-	levels := s.args.stackTraceDepth
+	levels := s.args.StackTraceDepth
 	if request.Arguments.Levels > 0 {
 		levels = request.Arguments.Levels
 	}
@@ -1941,7 +1963,7 @@ func (s *Session) onStackTraceRequest(request *dap.StackTraceRequest) {
 		// We don't know the exact number of available stack frames, so
 		// add an arbitrary number so the client knows to request additional
 		// frames.
-		totalFrames += s.args.stackTraceDepth
+		totalFrames += s.args.StackTraceDepth
 	}
 	response := &dap.StackTraceResponse{
 		Response: *newResponse(request.Request),
@@ -1992,7 +2014,7 @@ func (s *Session) onScopesRequest(request *dap.ScopesRequest) {
 	scopeLocals := dap.Scope{Name: locScope.Name, VariablesReference: s.variableHandles.create(locScope)}
 	scopes := []dap.Scope{scopeLocals}
 
-	if s.args.showGlobalVariables {
+	if s.args.ShowGlobalVariables {
 		// Limit what global variables we will return to the current package only.
 		// TODO(polina): This is how vscode-go currently does it to make
 		// the amount of the returned data manageable. In fact, this is
@@ -2027,7 +2049,7 @@ func (s *Session) onScopesRequest(request *dap.ScopesRequest) {
 		scopes = append(scopes, scopeGlobals)
 	}
 
-	if s.args.showRegisters {
+	if s.args.ShowRegisters {
 		// Retrieve registers
 		regs, err := s.debugger.ScopeRegisters(goid, frame, 0, false)
 		if err != nil {
@@ -2546,6 +2568,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr
 // Support the following expressions:
 // -- {expression} - evaluates the expression and returns the result as a variable
 // -- call {function} - injects a function call and returns the result as a variable
+// -- config {expression} - updates configuration paramaters
 // TODO(polina): users have complained about having to click to expand multi-level
 // variables, so consider also adding the following:
 // -- print {expression} - return the result as a string like from dlv cli
@@ -2565,9 +2588,20 @@ func (s *Session) onEvaluateRequest(request *dap.EvaluateRequest) {
 	}
 
 	response := &dap.EvaluateResponse{Response: *newResponse(request.Request)}
-	isCall, err := regexp.MatchString(`^\s*call\s+\S+`, request.Arguments.Expression)
-	if err == nil && isCall { // call {expression}
-		expr := strings.Replace(request.Arguments.Expression, "call ", "", 1)
+	expr := request.Arguments.Expression
+
+	if isConfig, err := regexp.MatchString(`^\s*dlv\s+\S+`, expr); err == nil && isConfig { // dlv {command}
+		expr := strings.Replace(expr, "dlv ", "", 1)
+		result, err := s.delveCmd(goid, frame, expr)
+		if err != nil {
+			s.sendErrorResponseWithOpts(request.Request, UnableToRunDlvCommand, "Unable to run dlv command", err.Error(), showErrorToUser)
+			return
+		}
+		response.Body = dap.EvaluateResponseBody{
+			Result: result,
+		}
+	} else if isCall, err := regexp.MatchString(`^\s*call\s+\S+`, expr); err == nil && isCall { // call {expression}
+		expr := strings.Replace(expr, "call ", "", 1)
 		_, retVars, err := s.doCall(goid, frame, expr)
 		if err != nil {
 			s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
@@ -2589,7 +2623,7 @@ func (s *Session) onEvaluateRequest(request *dap.EvaluateRequest) {
 			}
 		}
 	} else { // {expression}
-		exprVar, err := s.debugger.EvalVariableInScope(goid, frame, 0, request.Arguments.Expression, DefaultLoadConfig)
+		exprVar, err := s.debugger.EvalVariableInScope(goid, frame, 0, expr, DefaultLoadConfig)
 		if err != nil {
 			s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
 			return
@@ -3182,7 +3216,7 @@ func (s *Session) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
 }
 
 func (s *Session) stacktrace(goroutineID int, g *proc.G) (string, error) {
-	frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0)
+	frames, err := s.debugger.Stacktrace(goroutineID, s.args.StackTraceDepth, 0)
 	if err != nil {
 		return "", err
 	}
diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go
index 2562d672..eee82b8e 100644
--- a/service/debugger/debugger.go
+++ b/service/debugger/debugger.go
@@ -2157,6 +2157,9 @@ func (d *Debugger) GetBufferedTracepoints() []api.TracepointResult {
 		for _, p := range trace.InputParams {
 			results[i].InputParams = append(results[i].InputParams, *api.ConvertVar(p))
 		}
+		for _, p := range trace.ReturnParams {
+			results[i].ReturnParams = append(results[i].ReturnParams, *api.ConvertVar(p))
+		}
 	}
 	return results
 }
diff --git a/service/rpccommon/server.go b/service/rpccommon/server.go
index db88b9d5..328885fc 100644
--- a/service/rpccommon/server.go
+++ b/service/rpccommon/server.go
@@ -1,6 +1,7 @@
 package rpccommon
 
 import (
+	"bufio"
 	"bytes"
 	"encoding/json"
 	"fmt"
@@ -19,6 +20,7 @@ import (
 	"github.com/go-delve/delve/pkg/version"
 	"github.com/go-delve/delve/service"
 	"github.com/go-delve/delve/service/api"
+	"github.com/go-delve/delve/service/dap"
 	"github.com/go-delve/delve/service/debugger"
 	"github.com/go-delve/delve/service/internal/sameuser"
 	"github.com/go-delve/delve/service/rpc1"
@@ -156,7 +158,7 @@ func (s *ServerImpl) Run() error {
 				}
 			}
 
-			go s.serveJSONCodec(c)
+			go s.serveConnectionDemux(c)
 			if !s.config.AcceptMulti {
 				break
 			}
@@ -165,6 +167,28 @@ func (s *ServerImpl) Run() error {
 	return nil
 }
 
+type bufReadWriteCloser struct {
+	*bufio.Reader
+	io.WriteCloser
+}
+
+func (s *ServerImpl) serveConnectionDemux(c io.ReadWriteCloser) {
+	conn := &bufReadWriteCloser{bufio.NewReader(c), c}
+	b, err := conn.Peek(1)
+	if err != nil {
+		s.log.Warnf("error determining new connection protocol: %v", err)
+		return
+	}
+	if b[0] == 'C' { // C is for DAP's Content-Length
+		s.log.Debugf("serving DAP on new connection")
+		ds := dap.NewSession(conn, &dap.Config{Config: s.config, StopTriggered: s.stopChan}, s.debugger)
+		go ds.ServeDAPCodec()
+	} else {
+		s.log.Debugf("serving JSON-RPC on new connection")
+		go s.serveJSONCodec(conn)
+	}
+}
+
 // Precompute the reflect type for error.  Can't use error directly
 // because Typeof takes an empty interface value.  This is annoying.
 var typeOfError = reflect.TypeOf((*error)(nil)).Elem()


Test patch:
diff --git a/_scripts/latestver.py b/_scripts/latestver.py
index cad2f80a..886d476e 100644
--- a/_scripts/latestver.py
+++ b/_scripts/latestver.py
@@ -2,11 +2,25 @@
 import json
 import urllib
 import sys
-from distutils.version import LooseVersion
+import re
+
+def splitver(x):
+	v = re.split(r'([^\d]+)', x)
+	v[0] = int(v[0])
+	if len(v) > 2:
+		v[2] = int(v[2])
+	if len(v) > 4:
+		v[4] = int(v[4])
+	# make rc/beta versions sort before normal versions
+	if len(v) > 3 and v[3] == '.':
+		v[3] = '~' 
+	elif len(v) == 3:
+		v.append('~')
+	return v
 
 ver = sys.argv[1]
 d = json.loads(urllib.urlopen('https://golang.org/dl/?mode=json&include=all').read())
-ds = sorted(d, reverse=True, key=lambda it: LooseVersion(it['version'][2:]))
+ds = sorted(d, reverse=True, key=lambda it: splitver(it['version'][2:]))
 for x in ds:
 	if x['version'][:len(ver)] == ver:
 		print x['version']
diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go
index 7743f522..565d868c 100644
--- a/cmd/dlv/dlv_test.go
+++ b/cmd/dlv/dlv_test.go
@@ -24,8 +24,10 @@ import (
 	"github.com/go-delve/delve/pkg/goversion"
 	protest "github.com/go-delve/delve/pkg/proc/test"
 	"github.com/go-delve/delve/pkg/terminal"
+	"github.com/go-delve/delve/service/dap"
 	"github.com/go-delve/delve/service/dap/daptest"
 	"github.com/go-delve/delve/service/rpc2"
+	godap "github.com/google/go-dap"
 	"golang.org/x/tools/go/packages"
 )
 
@@ -52,6 +54,7 @@ func TestMain(m *testing.M) {
 }
 
 func assertNoError(err error, t testing.TB, s string) {
+	t.Helper()
 	if err != nil {
 		_, file, line, _ := runtime.Caller(1)
 		fname := filepath.Base(file)
@@ -647,8 +650,8 @@ func TestTypecheckRPC(t *testing.T) {
 	}
 }
 
-// TestDap verifies that a dap server can be started and shut down.
-func TestDap(t *testing.T) {
+// TestDAPCmd verifies that a dap server can be started and shut down.
+func TestDAPCmd(t *testing.T) {
 	const listenAddr = "127.0.0.1:40575"
 
 	dlvbin, tmpdir := getDlvBin(t)
@@ -691,8 +694,179 @@ func TestDap(t *testing.T) {
 	cmd.Wait()
 }
 
-// TestDapWithClient tests dlv dap --client-addr can be started and shut down.
-func TestDapWithClient(t *testing.T) {
+func newDAPRemoteClient(t *testing.T, addr string) *daptest.Client {
+	c := daptest.NewClient(addr)
+	c.AttachRequest(map[string]interface{}{"mode": "remote", "stopOnEntry": true})
+	c.ExpectInitializedEvent(t)
+	c.ExpectAttachResponse(t)
+	c.ConfigurationDoneRequest()
+	c.ExpectStoppedEvent(t)
+	c.ExpectConfigurationDoneResponse(t)
+	return c
+}
+
+func TestRemoteDAPClient(t *testing.T) {
+	const listenAddr = "127.0.0.1:40576"
+
+	dlvbin, tmpdir := getDlvBin(t)
+	defer os.RemoveAll(tmpdir)
+
+	buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest")
+	cmd := exec.Command(dlvbin, "debug", "--headless", "--log-output=dap", "--log", "--listen", listenAddr)
+	cmd.Dir = buildtestdir
+	stdout, err := cmd.StdoutPipe()
+	assertNoError(err, t, "stdout pipe")
+	defer stdout.Close()
+	stderr, err := cmd.StderrPipe()
+	assertNoError(err, t, "stderr pipe")
+	defer stderr.Close()
+	assertNoError(cmd.Start(), t, "start headless instance")
+
+	scanOut := bufio.NewScanner(stdout)
+	scanErr := bufio.NewScanner(stderr)
+	// Wait for the debug server to start
+	scanOut.Scan()
+	t.Log(scanOut.Text())
+	go func() { // Capture logging
+		for scanErr.Scan() {
+			t.Log(scanErr.Text())
+		}
+	}()
+
+	client := newDAPRemoteClient(t, listenAddr)
+	client.ContinueRequest(1)
+	client.ExpectContinueResponse(t)
+	client.ExpectTerminatedEvent(t)
+
+	client.DisconnectRequest()
+	client.ExpectOutputEventProcessExited(t, 0)
+	client.ExpectOutputEventDetaching(t)
+	client.ExpectDisconnectResponse(t)
+	client.ExpectTerminatedEvent(t)
+	if _, err := client.ReadMessage(); err == nil {
+		t.Error("expected read error upon shutdown")
+	}
+	client.Close()
+	cmd.Wait()
+}
+
+func closeDAPRemoteMultiClient(t *testing.T, c *daptest.Client) {
+	c.DisconnectRequest()
+	c.ExpectOutputEventClosingClient(t)
+	c.ExpectDisconnectResponse(t)
+	c.ExpectTerminatedEvent(t)
+	c.Close()
+	time.Sleep(10 * time.Millisecond)
+}
+
+func TestRemoteDAPClientMulti(t *testing.T) {
+	const listenAddr = "127.0.0.1:40577"
+
+	dlvbin, tmpdir := getDlvBin(t)
+	defer os.RemoveAll(tmpdir)
+
+	buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest")
+	cmd := exec.Command(dlvbin, "debug", "--headless", "--accept-multiclient", "--log-output=debugger", "--log", "--listen", listenAddr)
+	cmd.Dir = buildtestdir
+	stdout, err := cmd.StdoutPipe()
+	assertNoError(err, t, "stdout pipe")
+	defer stdout.Close()
+	stderr, err := cmd.StderrPipe()
+	assertNoError(err, t, "stderr pipe")
+	defer stderr.Close()
+	assertNoError(cmd.Start(), t, "start headless instance")
+
+	scanOut := bufio.NewScanner(stdout)
+	scanErr := bufio.NewScanner(stderr)
+	// Wait for the debug server to start
+	scanOut.Scan()
+	t.Log(scanOut.Text())
+	go func() { // Capture logging
+		for scanErr.Scan() {
+			t.Log(scanErr.Text())
+		}
+	}()
+
+	// Client 1 connects and continues to main.main
+	dapclient := newDAPRemoteClient(t, listenAddr)
+	dapclient.SetFunctionBreakpointsRequest([]godap.FunctionBreakpoint{{Name: "main.main"}})
+	dapclient.ExpectSetFunctionBreakpointsResponse(t)
+	dapclient.ContinueRequest(1)
+	dapclient.ExpectContinueResponse(t)
+	dapclient.ExpectStoppedEvent(t)
+	dapclient.CheckStopLocation(t, 1, "main.main", 5)
+	closeDAPRemoteMultiClient(t, dapclient)
+
+	// Client 2 reconnects at main.main and continues to process exit
+	dapclient2 := newDAPRemoteClient(t, listenAddr)
+	dapclient2.CheckStopLocation(t, 1, "main.main", 5)
+	dapclient2.ContinueRequest(1)
+	dapclient2.ExpectContinueResponse(t)
+	dapclient2.ExpectTerminatedEvent(t)
+	closeDAPRemoteMultiClient(t, dapclient2)
+
+	// Attach to exited processs is an error
+	dapclient3 := daptest.NewClient(listenAddr)
+	dapclient3.AttachRequest(map[string]interface{}{"mode": "remote", "stopOnEntry": true})
+	dapclient3.ExpectErrorResponseWith(t, dap.FailedToAttach, `Process \d+ has exited with status 0`, true)
+	closeDAPRemoteMultiClient(t, dapclient3)
+
+	// But rpc clients can still connect and restart
+	rpcclient := rpc2.NewClient(listenAddr)
+	if _, err := rpcclient.Restart(false); err != nil {
+		t.Errorf("error restarting with rpc client: %v", err)
+	}
+	if err := rpcclient.Detach(true); err != nil {
+		t.Fatalf("error detaching from headless instance: %v", err)
+	}
+	cmd.Wait()
+}
+
+func TestRemoteDAPClientAfterContinue(t *testing.T) {
+	const listenAddr = "127.0.0.1:40578"
+
+	dlvbin, tmpdir := getDlvBin(t)
+	defer os.RemoveAll(tmpdir)
+
+	fixture := protest.BuildFixture("loopprog", 0)
+	cmd := exec.Command(dlvbin, "exec", fixture.Path, "--headless", "--continue", "--accept-multiclient", "--log-output=debugger,dap", "--log", "--listen", listenAddr)
+	stdout, err := cmd.StdoutPipe()
+	assertNoError(err, t, "stdout pipe")
+	defer stdout.Close()
+	stderr, err := cmd.StderrPipe()
+	assertNoError(err, t, "stderr pipe")
+	defer stderr.Close()
+	assertNoError(cmd.Start(), t, "start headless instance")
+
+	scanOut := bufio.NewScanner(stdout)
+	scanErr := bufio.NewScanner(stderr)
+	// Wait for the debug server to start
+	scanOut.Scan() // "API server listening...""
+	t.Log(scanOut.Text())
+	// Wait for the program to start
+	scanOut.Scan() // "past main"
+	t.Log(scanOut.Text())
+
+	go func() { // Capture logging
+		for scanErr.Scan() {
+			t.Log(scanErr.Text())
+		}
+	}()
+
+	c := newDAPRemoteClient(t, listenAddr)
+	c.DisconnectRequestWithKillOption(true)
+	c.ExpectOutputEventDetachingKill(t)
+	c.ExpectDisconnectResponse(t)
+	c.ExpectTerminatedEvent(t)
+	if _, err := c.ReadMessage(); err == nil {
+		t.Error("expected read error upon shutdown")
+	}
+	c.Close()
+	cmd.Wait()
+}
+
+// TestDAPCmdWithClient tests dlv dap --client-addr can be started and shut down.
+func TestDAPCmdWithClient(t *testing.T) {
 	listener, err := net.Listen("tcp", ":0")
 	if err != nil {
 		t.Fatalf("cannot setup listener required for testing: %v", err)
@@ -871,7 +1045,7 @@ func TestTraceEBPF(t *testing.T) {
 	dlvbin, tmpdir := getDlvBinEBPF(t)
 	defer os.RemoveAll(tmpdir)
 
-	expected := []byte("> (1) main.foo(99, 9801)\n")
+	expected := []byte("> (1) main.foo(99, 9801)\n=> \"9900\"")
 
 	fixtures := protest.FindFixturesDir()
 	cmd := exec.Command(dlvbin, "trace", "--ebpf", "--output", filepath.Join(tmpdir, "__debug"), filepath.Join(fixtures, "issue573.go"), "foo")
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/client.go b/service/dap/daptest/client.go
index bf2143f6..0dfb1e23 100644
--- a/service/dap/daptest/client.go
+++ b/service/dap/daptest/client.go
@@ -88,11 +88,11 @@ func (c *Client) ExpectVisibleErrorResponse(t *testing.T) *dap.ErrorResponse {
 	return er
 }
 
-func (c *Client) expectErrorResponse(t *testing.T, id int, message string) *dap.ErrorResponse {
+func (c *Client) ExpectErrorResponseWith(t *testing.T, id int, message string, showUser bool) *dap.ErrorResponse {
 	t.Helper()
 	er := c.ExpectErrorResponse(t)
-	if er.Body.Error.Id != id || er.Message != message {
-		t.Errorf("\ngot %#v\nwant Id=%d Message=%q", er, id, message)
+	if matched, _ := regexp.MatchString(message, er.Body.Error.Format); !matched || er.Body.Error.Id != id || er.Body.Error.ShowUser != showUser {
+		t.Errorf("got %#v, want Id=%d Format=%q ShowUser=%v", er, id, message, showUser)
 	}
 	return er
 }
@@ -129,12 +129,12 @@ func pretty(v interface{}) string {
 
 func (c *Client) ExpectNotYetImplementedErrorResponse(t *testing.T) *dap.ErrorResponse {
 	t.Helper()
-	return c.expectErrorResponse(t, 7777, "Not yet implemented")
+	return c.ExpectErrorResponseWith(t, 7777, "Not yet implemented", false)
 }
 
 func (c *Client) ExpectUnsupportedCommandErrorResponse(t *testing.T) *dap.ErrorResponse {
 	t.Helper()
-	return c.expectErrorResponse(t, 9999, "Unsupported command")
+	return c.ExpectErrorResponseWith(t, 9999, "Unsupported command", false)
 }
 
 func (c *Client) ExpectOutputEventRegex(t *testing.T, want string) *dap.OutputEvent {
@@ -173,6 +173,27 @@ func (c *Client) ExpectOutputEventTerminating(t *testing.T) *dap.OutputEvent {
 	return c.ExpectOutputEventRegex(t, `Terminating process [0-9]+\n`)
 }
 
+func (c *Client) ExpectOutputEventClosingClient(t *testing.T) *dap.OutputEvent {
+	t.Helper()
+	return c.ExpectOutputEventRegex(t, `Closing client session, but leaving multi-client DAP server running at .+:[0-9]+\n`)
+}
+
+func (c *Client) CheckStopLocation(t *testing.T, thread int, name string, line int) {
+	t.Helper()
+	c.StackTraceRequest(thread, 0, 20)
+	st := c.ExpectStackTraceResponse(t)
+	if len(st.Body.StackFrames) < 1 {
+		t.Errorf("\ngot  %#v\nwant len(stackframes) => 1", st)
+	} else {
+		if line != -1 && st.Body.StackFrames[0].Line != line {
+			t.Errorf("\ngot  %#v\nwant Line=%d", st, line)
+		}
+		if st.Body.StackFrames[0].Name != name {
+			t.Errorf("\ngot  %#v\nwant Name=%q", st, name)
+		}
+	}
+}
+
 // InitializeRequest sends an 'initialize' request.
 func (c *Client) InitializeRequest() {
 	request := &dap.InitializeRequest{Request: *c.newRequest("initialize")}
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 810d2a8d..b8b20fd5 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,
@@ -3654,6 +3808,105 @@ func TestEvaluateRequest(t *testing.T) {
 	})
 }
 
+func formatConfig(depth int, showGlobals, showRegisters, hideSystemGoroutines bool, substitutePath [][2]string) string {
+	formatStr := `stackTraceDepth	%d
+showGlobalVariables	%v
+showRegisters	%v
+hideSystemGoroutines	%v
+substitutePath	%v
+`
+	return fmt.Sprintf(formatStr, depth, showGlobals, showRegisters, hideSystemGoroutines, substitutePath)
+}
+
+func TestEvaluateCommandRequest(t *testing.T) {
+	runTest(t, "testvariables", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			fixture.Source, []int{}, // Breakpoint set in the program
+			[]onBreakpoint{{ // Stop at first breakpoint
+				execute: func() {
+					checkStop(t, client, 1, "main.foobar", 66)
+
+					// Request help.
+					const dlvHelp = `The following commands are available:
+    help (alias: h) 	 Prints the help message.
+    config 	 Changes configuration parameters.
+
+Type help followed by a command for full documentation.
+`
+					client.EvaluateRequest("dlv help", 1000, "repl")
+					got := client.ExpectEvaluateResponse(t)
+					checkEval(t, got, dlvHelp, noChildren)
+
+					client.EvaluateRequest("dlv help config", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, msgConfig, noChildren)
+
+					// Test config.
+					client.EvaluateRequest("dlv config -list", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, formatConfig(50, false, false, false, [][2]string{}), noChildren)
+
+					// Read and modify showGlobalVariables.
+					client.EvaluateRequest("dlv config showGlobalVariables", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, "showGlobalVariables\tfalse\n", noChildren)
+
+					client.ScopesRequest(1000)
+					scopes := client.ExpectScopesResponse(t)
+					if len(scopes.Body.Scopes) > 1 {
+						t.Errorf("\ngot  %#v\nwant len(scopes)=1 (Locals)", scopes)
+					}
+					checkScope(t, scopes, 0, "Locals", -1)
+
+					client.EvaluateRequest("dlv config showGlobalVariables true", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, "showGlobalVariables\ttrue\n\nUpdated", noChildren)
+
+					client.EvaluateRequest("dlv config -list", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, formatConfig(50, true, false, false, [][2]string{}), noChildren)
+
+					client.ScopesRequest(1000)
+					scopes = client.ExpectScopesResponse(t)
+					if len(scopes.Body.Scopes) < 2 {
+						t.Errorf("\ngot  %#v\nwant len(scopes)=2 (Locals & Globals)", scopes)
+					}
+					checkScope(t, scopes, 0, "Locals", -1)
+					checkScope(t, scopes, 1, "Globals (package main)", -1)
+
+					// Read and modify substitutePath.
+					client.EvaluateRequest("dlv config substitutePath", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, "substitutePath\t[]\n", noChildren)
+
+					client.EvaluateRequest(fmt.Sprintf("dlv config substitutePath %q %q", "my/client/path", "your/server/path"), 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, "substitutePath\t[[my/client/path your/server/path]]\n\nUpdated", noChildren)
+
+					client.EvaluateRequest(fmt.Sprintf("dlv config substitutePath %q %q", "my/client/path", "new/your/server/path"), 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, "substitutePath\t[[my/client/path new/your/server/path]]\n\nUpdated", noChildren)
+
+					client.EvaluateRequest(fmt.Sprintf("dlv config substitutePath %q", "my/client/path"), 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, "substitutePath\t[]\n\nUpdated", noChildren)
+
+					// Test bad inputs.
+					client.EvaluateRequest("dlv help bad", 1000, "repl")
+					client.ExpectErrorResponse(t)
+
+					client.EvaluateRequest("dlv bad", 1000, "repl")
+					client.ExpectErrorResponse(t)
+				},
+				disconnect: true,
+			}})
+	})
+}
+
 // From testvariables2 fixture
 const (
 	// As defined in the code
@@ -4598,23 +4851,6 @@ func TestFatalThrowBreakpoint(t *testing.T) {
 	})
 }
 
-func verifyStopLocation(t *testing.T, client *daptest.Client, thread int, name string, line int) {
-	t.Helper()
-
-	client.StackTraceRequest(thread, 0, 20)
-	st := client.ExpectStackTraceResponse(t)
-	if len(st.Body.StackFrames) < 1 {
-		t.Errorf("\ngot  %#v\nwant len(stackframes) => 1", st)
-	} else {
-		if line != -1 && st.Body.StackFrames[0].Line != line {
-			t.Errorf("\ngot  %#v\nwant Line=%d", st, line)
-		}
-		if st.Body.StackFrames[0].Name != name {
-			t.Errorf("\ngot  %#v\nwant Name=%q", st, name)
-		}
-	}
-}
-
 // checkStop covers the standard sequence of requests issued by
 // a client at a breakpoint or another non-terminal stop event.
 // The details have been tested by other tests,
@@ -4625,7 +4861,7 @@ func checkStop(t *testing.T, client *daptest.Client, thread int, fname string, l
 	client.ThreadsRequest()
 	client.ExpectThreadsResponse(t)
 
-	verifyStopLocation(t, client, thread, fname, line)
+	client.CheckStopLocation(t, thread, fname, line)
 
 	client.ScopesRequest(1000)
 	client.ExpectScopesResponse(t)
@@ -5150,7 +5386,7 @@ func TestPauseAndContinue(t *testing.T) {
 			fixture.Source, []int{6},
 			[]onBreakpoint{{
 				execute: func() {
-					verifyStopLocation(t, client, 1, "main.loop", 6)
+					client.CheckStopLocation(t, 1, "main.loop", 6)
 
 					// Continue resumes all goroutines, so thread id is ignored
 					client.ContinueRequest(12345)
@@ -5967,7 +6203,6 @@ func runTestWithDebugger(t *testing.T, dbg *debugger.Debugger, test func(c *dapt
 	server, _ := startDAPServer(t, serverStopped)
 	client := daptest.NewClient(server.listener.Addr().String())
 	time.Sleep(100 * time.Millisecond) // Give time for connection to be set as dap.Session
-	// TODO(polina): update once the server interface is refactored to take debugger as arg
 	server.sessionMu.Lock()
 	if server.session == nil {
 		t.Fatal("DAP session is not ready")
@@ -6061,22 +6296,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 {
@@ -6085,7 +6321,6 @@ func TestAttachRemoteMultiClient(t *testing.T) {
 			// DAP server doesn't support accept-multiclient, but we can use this
 			// hack to test the inner connection logic that can be used by a server that does.
 			server.session.config.AcceptMulti = true
-			// TODO(polina): update once the server interface is refactored to take debugger as arg
 			_, server.session.debugger = launchDebuggerWithTargetHalted(t, "increment")
 			server.sessionMu.Unlock()
 
@@ -6106,12 +6341,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