Repository: go-delve/delve
Pull request #2798 (2021-11-17 21:44:55)

_scripts: add option to disable invoking git during build
This is useful to be able to build using Delve's build scripts when
working from within a directory that has been extracted from a source tarball,
or wherever we wouldn't have git history.

This can be invoked as:

```
  go run _scripts/make.go build --no-git
```

herbygillot: At first glance, build failures look unrelated to this change...
aarzilli: Approved but this is just going the long way around to run `go install github.com/go-delve/delve/cmd/dlv`
herbygillot: > Approved but this is just going the long way around to run `go install github.com/go-delve/delve/cmd/dlv`

So I'm a "package maintainer", if you will, maintaining the port (the "package") for `delve` in the [MacPorts](https://www.macports.org) project.  You can see more details here: https://ports.macports.org/port/delve/details/

There is value in this build script (`_scripts/make.go`) as it makes certain determinations for how to build and test `delve` by making decisions from a number of environment-specific factors, such as the macOS version, the presence of certain system headers, and the version of Go being used. We can't just unilaterally do a `go install` when a user is installs `delve` through MacPorts because all these details matter (especially the macOS version the user might be on).

We need this change because the MacPorts project in general prefers to use checksummed source tarballs because tarballs can be verified, cached and mirrored.  Since we're building from an extracted tarball, we don't have git history, and so before this change, this script would panic when `git` crashes due to the absence of the `.git` directory.
herbygillot: > > We can't just unilaterally do a `go install` when a user is installing `delve` through MacPorts because all these details matter (especially the macOS version the user is currently on).
> 
> You can unilaterally do `go install` (see [install instructions](https://github.com/go-delve/delve/tree/master/Documentation/installation)) and all those details don't matter at this point in time.

Alright, well that's good to know in that case. Thanks @aarzilli.

Patch:
diff --git a/Documentation/installation/README.md b/Documentation/installation/README.md
index cbbf1f2c..d0204fa6 100644
--- a/Documentation/installation/README.md
+++ b/Documentation/installation/README.md
@@ -9,11 +9,20 @@ $ cd delve
 $ go install github.com/go-delve/delve/cmd/dlv
 ```
 
-On Go version 1.16 or later, this command will also work:
+Alternatively, on Go version 1.16 or later:
 
 ```
+# Install the latest release:
 $ go install github.com/go-delve/delve/cmd/dlv@latest
+
+# Install at tree head:
+$ go install github.com/go-delve/delve/cmd/dlv@master
+
+# Install at a specific version or pseudo-version:
+$ go install github.com/go-delve/delve/cmd/dlv@v1.7.3
+$ go install github.com/go-delve/delve/cmd/dlv@v1.7.4-0.20211208103735-2f13672765fe
 ```
+See [Versions](https://go.dev/ref/mod#versions) and [Pseudo-versions](https://go.dev/ref/mod#pseudo-versions) for how to format the version suffixes.
 
 See `go help install` for details on where the `dlv` executable is saved.
 
diff --git a/_fixtures/asmnilptr/main.go b/_fixtures/asmnilptr/main.go
new file mode 100644
index 00000000..97d6d1f7
--- /dev/null
+++ b/_fixtures/asmnilptr/main.go
@@ -0,0 +1,9 @@
+package main
+
+import "fmt"
+
+func asmFunc(*int) int
+
+func main() {
+	fmt.Printf("%d\n", asmFunc(nil))
+}
diff --git a/_fixtures/asmnilptr/main_386.s b/_fixtures/asmnilptr/main_386.s
new file mode 100644
index 00000000..1b199d58
--- /dev/null
+++ b/_fixtures/asmnilptr/main_386.s
@@ -0,0 +1,7 @@
+#include "textflag.h"
+
+TEXT ·asmFunc(SB),0,$0-16
+	MOVL arg+0(FP), AX
+	MOVL (AX), AX
+	MOVL AX, ret+4(FP)
+	RET
diff --git a/_fixtures/asmnilptr/main_amd64.s b/_fixtures/asmnilptr/main_amd64.s
new file mode 100644
index 00000000..bc365a57
--- /dev/null
+++ b/_fixtures/asmnilptr/main_amd64.s
@@ -0,0 +1,7 @@
+#include "textflag.h"
+
+TEXT ·asmFunc(SB),0,$0-16
+	MOVQ arg+0(FP), AX
+	MOVQ (AX), AX
+	MOVQ AX, ret+8(FP)
+	RET
diff --git a/_fixtures/asmnilptr/main_arm64.s b/_fixtures/asmnilptr/main_arm64.s
new file mode 100644
index 00000000..fb57de19
--- /dev/null
+++ b/_fixtures/asmnilptr/main_arm64.s
@@ -0,0 +1,7 @@
+#include "textflag.h"
+
+TEXT ·asmFunc(SB),0,$0-16
+	MOVD arg+0(FP), R5
+	MOVD (R5), R5
+	MOVD R5, ret+8(FP)
+	RET
diff --git a/_scripts/make.go b/_scripts/make.go
index 5ad9bbeb..9bc13311 100644
--- a/_scripts/make.go
+++ b/_scripts/make.go
@@ -24,6 +24,7 @@ var TestSet, TestRegex, TestBackend, TestBuildMode string
 var Tags *[]string
 var Architecture string
 var OS string
+var DisableGit bool
 
 func NewMakeCommands() *cobra.Command {
 	RootCommand := &cobra.Command{
@@ -68,6 +69,7 @@ func NewMakeCommands() *cobra.Command {
 		},
 	}
 	Tags = buildCmd.PersistentFlags().StringArray("tags", []string{}, "Build tags")
+	buildCmd.PersistentFlags().BoolVarP(&DisableGit, "no-git", "G", false, "Do not use git")
 	buildCmd.PersistentFlags().StringVar(&Architecture, "GOARCH", "", "Architecture to build for")
 	buildCmd.PersistentFlags().StringVar(&OS, "GOOS", "", "OS to build for")
 	RootCommand.AddCommand(buildCmd)
@@ -291,11 +293,11 @@ func prepareMacnative() string {
 }
 
 func buildFlags() []string {
-	buildSHA, err := exec.Command("git", "rev-parse", "HEAD").CombinedOutput()
+	buildSHA, err := getBuildSHA()
 	if err != nil {
-		log.Fatal(err)
+		log.Fatal(fmt.Errorf("error getting build SHA via git: %w", err))
 	}
-	ldFlags := "-X main.Build=" + strings.TrimSpace(string(buildSHA))
+	ldFlags := "-X main.Build=" + buildSHA
 	if runtime.GOOS == "darwin" {
 		ldFlags = "-s " + ldFlags
 	}
@@ -496,6 +498,22 @@ func allPackages() []string {
 	return r
 }
 
+// getBuildSHA will invoke git to return the current SHA of the commit at HEAD.
+// If invoking git has been disabled, it will return an empty string instead.
+func getBuildSHA() (string, error) {
+	if DisableGit {
+		return "", nil
+	}
+
+	buildSHA, err := exec.Command("git", "rev-parse", "HEAD").CombinedOutput()
+	if err != nil {
+		return "", err
+	}
+
+	shaStr := strings.TrimSpace(string(buildSHA))
+	return shaStr, nil
+}
+
 func main() {
 	allPackages() // checks that vendor directory is synced as a side effect
 	NewMakeCommands().Execute()
diff --git a/pkg/dwarf/godwarf/type.go b/pkg/dwarf/godwarf/type.go
index 49ff1c0a..5e257d28 100644
--- a/pkg/dwarf/godwarf/type.go
+++ b/pkg/dwarf/godwarf/type.go
@@ -555,7 +555,7 @@ type delayedSize struct {
 }
 
 // readType reads a type from r at off of name using and updating a
-// type cache, callers sohuld pass nil to delayedSize, it is used for recursion.
+// type cache, callers should pass nil to delayedSize, it is used for recursion.
 func readType(d *dwarf.Data, name string, r *dwarf.Reader, off dwarf.Offset, typeCache map[dwarf.Offset]Type, delayedSizes *[]delayedSize) (Type, error) {
 	if t, ok := typeCache[off]; ok {
 		return t, nil
diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go
index f60ae515..7d2e6839 100644
--- a/pkg/proc/bininfo.go
+++ b/pkg/proc/bininfo.go
@@ -911,10 +911,13 @@ func (image *Image) Close() error {
 	return err2
 }
 
-func (image *Image) setLoadError(fmtstr string, args ...interface{}) {
+func (image *Image) setLoadError(logger *logrus.Entry, fmtstr string, args ...interface{}) {
 	image.loadErrMu.Lock()
 	image.loadErr = fmt.Errorf(fmtstr, args...)
 	image.loadErrMu.Unlock()
+	if logger != nil {
+		logger.Errorf("error loading binary %q: %v", image.Path, image.loadErr)
+	}
 }
 
 // LoadError returns any error incurred while loading this image.
@@ -1161,14 +1164,14 @@ func (bi *BinaryInfo) funcToImage(fn *Function) *Image {
 // debug_frame is present it must be parsable correctly.
 func (bi *BinaryInfo) parseDebugFrameGeneral(image *Image, debugFrameBytes []byte, debugFrameName string, debugFrameErr error, ehFrameBytes []byte, ehFrameAddr uint64, ehFrameName string, byteOrder binary.ByteOrder) {
 	if debugFrameBytes == nil && ehFrameBytes == nil {
-		image.setLoadError("could not get %s section: %v", debugFrameName, debugFrameErr)
+		image.setLoadError(bi.logger, "could not get %s section: %v", debugFrameName, debugFrameErr)
 		return
 	}
 
 	if debugFrameBytes != nil {
 		fe, err := frame.Parse(debugFrameBytes, byteOrder, image.StaticBase, bi.Arch.PtrSize(), 0)
 		if err != nil {
-			image.setLoadError("could not parse %s section: %v", debugFrameName, err)
+			image.setLoadError(bi.logger, "could not parse %s section: %v", debugFrameName, err)
 			return
 		}
 		bi.frameEntries = bi.frameEntries.Append(fe)
@@ -1178,7 +1181,7 @@ func (bi *BinaryInfo) parseDebugFrameGeneral(image *Image, debugFrameBytes []byt
 		fe, err := frame.Parse(ehFrameBytes, byteOrder, image.StaticBase, bi.Arch.PtrSize(), ehFrameAddr)
 		if err != nil {
 			if debugFrameBytes == nil {
-				image.setLoadError("could not parse %s section: %v", ehFrameName, err)
+				image.setLoadError(bi.logger, "could not parse %s section: %v", ehFrameName, err)
 				return
 			}
 			bi.logger.Warnf("could not parse %s section: %v", ehFrameName, err)
@@ -1431,7 +1434,7 @@ func (bi *BinaryInfo) setGStructOffsetElf(image *Image, exe *elf.File, wg *sync.
 
 	switch exe.Machine {
 	case elf.EM_X86_64, elf.EM_386:
-		tlsg := getSymbol(image, exe, "runtime.tlsg")
+		tlsg := getSymbol(image, bi.logger, exe, "runtime.tlsg")
 		if tlsg == nil || tls == nil {
 			bi.gStructOffset = ^uint64(bi.Arch.PtrSize()) + 1 //-ptrSize
 			return
@@ -1448,7 +1451,7 @@ func (bi *BinaryInfo) setGStructOffsetElf(image *Image, exe *elf.File, wg *sync.
 		bi.gStructOffset = ^(memsz) + 1 + tlsg.Value // -tls.Memsz + tlsg.Value
 
 	case elf.EM_AARCH64:
-		tlsg := getSymbol(image, exe, "runtime.tls_g")
+		tlsg := getSymbol(image, bi.logger, exe, "runtime.tls_g")
 		if tlsg == nil || tls == nil {
 			bi.gStructOffset = 2 * uint64(bi.Arch.PtrSize())
 			return
@@ -1462,10 +1465,10 @@ func (bi *BinaryInfo) setGStructOffsetElf(image *Image, exe *elf.File, wg *sync.
 	}
 }
 
-func getSymbol(image *Image, exe *elf.File, name string) *elf.Symbol {
+func getSymbol(image *Image, logger *logrus.Entry, exe *elf.File, name string) *elf.Symbol {
 	symbols, err := exe.Symbols()
 	if err != nil {
-		image.setLoadError("could not parse ELF symbols: %v", err)
+		image.setLoadError(logger, "could not parse ELF symbols: %v", err)
 		return nil
 	}
 
@@ -1524,6 +1527,8 @@ func loadBinaryInfoPE(bi *BinaryInfo, image *Image, path string, entryPoint uint
 	image.loclist5 = loclist.NewDwarf5Reader(debugLoclistBytes)
 	debugAddrBytes, _ := godwarf.GetDebugSectionPE(peFile, "addr")
 	image.debugAddr = godwarf.ParseAddr(debugAddrBytes)
+	debugLineStrBytes, _ := godwarf.GetDebugSectionPE(peFile, "line_str")
+	image.debugLineStr = debugLineStrBytes
 
 	wg.Add(2)
 	go bi.parseDebugFramePE(image, peFile, debugInfoBytes, wg)
@@ -1600,6 +1605,8 @@ func loadBinaryInfoMacho(bi *BinaryInfo, image *Image, path string, entryPoint u
 	image.loclist5 = loclist.NewDwarf5Reader(debugLoclistBytes)
 	debugAddrBytes, _ := godwarf.GetDebugSectionMacho(exe, "addr")
 	image.debugAddr = godwarf.ParseAddr(debugAddrBytes)
+	debugLineStrBytes, _ := godwarf.GetDebugSectionMacho(exe, "line_str")
+	image.debugLineStr = debugLineStrBytes
 
 	wg.Add(2)
 	go bi.parseDebugFrameMacho(image, exe, debugInfoBytes, wg)
@@ -1846,9 +1853,13 @@ func (bi *BinaryInfo) loadDebugInfoMaps(image *Image, debugInfoBytes, debugLineB
 
 	reader := image.DwarfReader()
 
-	for entry, err := reader.Next(); entry != nil; entry, err = reader.Next() {
+	for {
+		entry, err := reader.Next()
 		if err != nil {
-			image.setLoadError("error reading debug_info: %v", err)
+			image.setLoadError(bi.logger, "error reading debug_info: %v", err)
+			break
+		}
+		if entry == nil {
 			break
 		}
 		switch entry.Tag {
@@ -1986,11 +1997,15 @@ func (bi *BinaryInfo) loadDebugInfoMapsCompileUnit(ctxt *loadDebugInfoMapsContex
 
 	depth := 0
 
-	for entry, err := reader.Next(); entry != nil; entry, err = reader.Next() {
+	for {
+		entry, err := reader.Next()
 		if err != nil {
-			image.setLoadError("error reading debug_info: %v", err)
+			image.setLoadError(bi.logger, "error reading debug_info: %v", err)
 			return
 		}
+		if entry == nil {
+			break
+		}
 		switch entry.Tag {
 		case 0:
 			if depth == 0 {
@@ -2197,7 +2212,7 @@ func (bi *BinaryInfo) loadDebugInfoMapsInlinedCalls(ctxt *loadDebugInfoMapsConte
 	for {
 		entry, err := reader.Next()
 		if err != nil {
-			cu.image.setLoadError("error reading debug_info: %v", err)
+			cu.image.setLoadError(bi.logger, "error reading debug_info: %v", err)
 			return
 		}
 		switch entry.Tag {
diff --git a/pkg/proc/core/core.go b/pkg/proc/core/core.go
index 392d9f93..10ff0974 100644
--- a/pkg/proc/core/core.go
+++ b/pkg/proc/core/core.go
@@ -164,8 +164,6 @@ type process struct {
 	breakpoints proc.BreakpointMap
 }
 
-var _ proc.ProcessInternal = &process{}
-
 // thread represents a thread in the core file being debugged.
 type thread struct {
 	th     osThread
@@ -222,7 +220,7 @@ func OpenCore(corePath, exePath string, debugInfoDirs []string) (*proc.Target, e
 		return nil, ErrNoThreads
 	}
 
-	return proc.NewTarget(p, currentThread, proc.NewTargetConfig{
+	return proc.NewTarget(p, p.pid, currentThread, proc.NewTargetConfig{
 		Path:                exePath,
 		DebugInfoDirs:       debugInfoDirs,
 		DisableAsyncPreempt: false,
@@ -459,11 +457,6 @@ func (p *process) Valid() (bool, error) {
 	return true, nil
 }
 
-// Pid returns the process ID of this process.
-func (p *process) Pid() int {
-	return p.pid
-}
-
 // ResumeNotify is a no-op on core files as we cannot
 // control execution.
 func (p *process) ResumeNotify(chan<- struct{}) {
diff --git a/pkg/proc/dump.go b/pkg/proc/dump.go
index 0cd5dd39..3989dc00 100644
--- a/pkg/proc/dump.go
+++ b/pkg/proc/dump.go
@@ -155,7 +155,7 @@ func (t *Target) Dump(out elfwriter.WriteCloserSeeker, flags DumpFlags, state *D
 	notes = append(notes, elfwriter.Note{
 		Type: elfwriter.DelveHeaderNoteType,
 		Name: "Delve Header",
-		Data: []byte(fmt.Sprintf("%s/%s\n%s\n%s%d\n%s%#x\n", bi.GOOS, bi.Arch.Name, version.DelveVersion.String(), elfwriter.DelveHeaderTargetPidPrefix, t.Pid(), elfwriter.DelveHeaderEntryPointPrefix, entryPoint)),
+		Data: []byte(fmt.Sprintf("%s/%s\n%s\n%s%d\n%s%#x\n", bi.GOOS, bi.Arch.Name, version.DelveVersion.String(), elfwriter.DelveHeaderTargetPidPrefix, t.pid, elfwriter.DelveHeaderEntryPointPrefix, entryPoint)),
 	})
 
 	threads := t.ThreadList()
diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go
index c5dad939..29218c8e 100644
--- a/pkg/proc/eval.go
+++ b/pkg/proc/eval.go
@@ -423,11 +423,6 @@ func (scope *EvalScope) setValue(dstv, srcv *Variable, srcExpr string) error {
 	return fmt.Errorf("can not set variables of type %s (not implemented)", dstv.Kind.String())
 }
 
-// EvalVariable returns the value of the given expression (backwards compatibility).
-func (scope *EvalScope) EvalVariable(name string, cfg LoadConfig) (*Variable, error) {
-	return scope.EvalExpression(name, cfg)
-}
-
 // SetVariable sets the value of the named variable
 func (scope *EvalScope) SetVariable(name, value string) error {
 	t, err := parser.ParseExpr(name)
diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go
index b4b09a74..da633208 100644
--- a/pkg/proc/gdbserial/gdbserver.go
+++ b/pkg/proc/gdbserial/gdbserver.go
@@ -170,7 +170,7 @@ type gdbProcess struct {
 	onDetach func() // called after a successful detach
 }
 
-var _ proc.ProcessInternal = &gdbProcess{}
+var _ proc.RecordingManipulationInternal = &gdbProcess{}
 
 // gdbThread represents an operating system thread.
 type gdbThread struct {
@@ -232,6 +232,7 @@ func newProcess(process *os.Process) *gdbProcess {
 			direction:           proc.Forward,
 			log:                 logger,
 			goarch:              runtime.GOARCH,
+			goos:                runtime.GOOS,
 		},
 		threads:        make(map[int]*gdbThread),
 		bi:             proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH),
@@ -704,7 +705,7 @@ func (p *gdbProcess) initialize(path string, debugInfoDirs []string, stopReason
 			return nil, err
 		}
 	}
-	tgt, err := proc.NewTarget(p, p.currentThread, proc.NewTargetConfig{
+	tgt, err := proc.NewTarget(p, p.conn.pid, p.currentThread, proc.NewTargetConfig{
 		Path:                path,
 		DebugInfoDirs:       debugInfoDirs,
 		DisableAsyncPreempt: runtime.GOOS == "darwin",
@@ -790,6 +791,9 @@ const (
 	childSignal      = 0x11
 	stopSignal       = 0x13
 
+	_SIGILL = 0x4 // ok
+	_SIGFPE = 0x8 // ok
+
 	debugServerTargetExcBadAccess      = 0x91
 	debugServerTargetExcBadInstruction = 0x92
 	debugServerTargetExcArithmetic     = 0x93
@@ -880,21 +884,7 @@ continueLoop:
 		return nil, stopReason, fmt.Errorf("could not find thread %s", threadID)
 	}
 
-	var err error
-	switch trapthread.sig {
-	case 0x91:
-		err = errors.New("bad access")
-	case 0x92:
-		err = errors.New("bad instruction")
-	case 0x93:
-		err = errors.New("arithmetic exception")
-	case 0x94:
-		err = errors.New("emulation exception")
-	case 0x95:
-		err = errors.New("software exception")
-	case 0x96:
-		err = errors.New("breakpoint exception")
-	}
+	err := machTargetExcToError(trapthread.sig)
 	if err != nil {
 		// the signals that are reported here can not be propagated back to the target process.
 		trapthread.sig = 0
@@ -1522,7 +1512,7 @@ func (t *gdbThread) StepInstruction() error {
 	// Reset thread registers so the next call to
 	// Thread.Registers will not be cached.
 	t.regs.regs = nil
-	return t.p.conn.step(t.strID, &threadUpdater{p: t.p}, false)
+	return t.p.conn.step(t, &threadUpdater{p: t.p}, false)
 }
 
 // Blocked returns true if the thread is blocked in runtime or kernel code.
@@ -1784,7 +1774,7 @@ func (t *gdbThread) reloadGAtPC() error {
 		}
 	}()
 
-	err = t.p.conn.step(t.strID, nil, true)
+	err = t.p.conn.step(t, nil, true)
 	if err != nil {
 		if err == errThreadBlocked {
 			t.regs.tls = 0
@@ -1837,7 +1827,7 @@ func (t *gdbThread) reloadGAlloc() error {
 		}
 	}()
 
-	err = t.p.conn.step(t.strID, nil, true)
+	err = t.p.conn.step(t, nil, true)
 	if err != nil {
 		if err == errThreadBlocked {
 			t.regs.tls = 0
@@ -2056,3 +2046,21 @@ func registerName(arch *proc.Arch, regNum uint64) string {
 	regName, _, _ := arch.DwarfRegisterToString(int(regNum), nil)
 	return strings.ToLower(regName)
 }
+
+func machTargetExcToError(sig uint8) error {
+	switch sig {
+	case 0x91:
+		return errors.New("bad access")
+	case 0x92:
+		return errors.New("bad instruction")
+	case 0x93:
+		return errors.New("arithmetic exception")
+	case 0x94:
+		return errors.New("emulation exception")
+	case 0x95:
+		return errors.New("software exception")
+	case 0x96:
+		return errors.New("breakpoint exception")
+	}
+	return nil
+}
diff --git a/pkg/proc/gdbserial/gdbserver_conn.go b/pkg/proc/gdbserial/gdbserver_conn.go
index 4a2c7c95..c571940d 100644
--- a/pkg/proc/gdbserial/gdbserver_conn.go
+++ b/pkg/proc/gdbserial/gdbserver_conn.go
@@ -48,6 +48,7 @@ type gdbConn struct {
 	isDebugserver         bool // true if the stub is debugserver
 	xcmdok                bool // x command can be used to transfer memory
 	goarch                string
+	goos                  string
 
 	useXcmd bool // forces writeMemory to use the 'X' command
 
@@ -603,7 +604,8 @@ func (conn *gdbConn) resume(threads map[int]*gdbThread, tu *threadUpdater) (stop
 }
 
 // step executes a 'vCont' command on the specified thread with 's' action.
-func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal bool) error {
+func (conn *gdbConn) step(th *gdbThread, tu *threadUpdater, ignoreFaultSignal bool) error {
+	threadID := th.strID
 	if conn.direction != proc.Forward {
 		if err := conn.selectThread('c', threadID, "step"); err != nil {
 			return err
@@ -616,6 +618,17 @@ func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal
 		_, err := conn.waitForvContStop("singlestep", threadID, tu)
 		return err
 	}
+
+	var _SIGBUS uint8
+	switch conn.goos {
+	case "linux":
+		_SIGBUS = 0x7
+	case "darwin":
+		_SIGBUS = 0xa
+	default:
+		panic(fmt.Errorf("unknown GOOS %s", conn.goos))
+	}
+
 	var sig uint8 = 0
 	for {
 		conn.outbuf.Reset()
@@ -640,6 +653,8 @@ func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal
 			if ignoreFaultSignal { // we attempting to read the TLS, a fault here should be ignored
 				return nil
 			}
+		case _SIGILL, _SIGBUS, _SIGFPE:
+			// propagate these signals to inferior immediately
 		case interruptSignal, breakpointSignal, stopSignal:
 			return nil
 		case childSignal: // stop on debugserver but SIGCHLD on lldb-server/linux
@@ -647,9 +662,15 @@ func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal
 				return nil
 			}
 		case debugServerTargetExcBadAccess, debugServerTargetExcBadInstruction, debugServerTargetExcArithmetic, debugServerTargetExcEmulation, debugServerTargetExcSoftware, debugServerTargetExcBreakpoint:
-			return nil
+			if ignoreFaultSignal {
+				return nil
+			}
+			return machTargetExcToError(sig)
+		default:
+			// delay propagation of any other signal to until after the stepping is done
+			th.sig = sig
+			sig = 0
 		}
-		// any other signal is propagated to the inferior
 	}
 }
 
diff --git a/pkg/proc/interface.go b/pkg/proc/interface.go
index e5797e26..e403e1c6 100644
--- a/pkg/proc/interface.go
+++ b/pkg/proc/interface.go
@@ -13,9 +13,20 @@ import (
 // There is one exception to this rule: it is safe to call RequestManualStop
 // concurrently with ContinueOnce.
 type Process interface {
-	Info
-	ProcessManipulation
-	RecordingManipulation
+	// ResumeNotify specifies a channel that will be closed the next time
+	// ContinueOnce finishes resuming the target.
+	ResumeNotify(chan<- struct{})
+	BinInfo() *BinaryInfo
+	EntryPoint() (uint64, error)
+
+	// RequestManualStop attempts to stop all the process' threads.
+	RequestManualStop() error
+	// CheckAndClearManualStopRequest returns true the first time it's called
+	// after a call to RequestManualStop.
+	CheckAndClearManualStopRequest() bool
+
+	FindThread(threadID int) (Thread, bool)
+	ThreadList() []Thread
 
 	Breakpoints() *BreakpointMap
 
@@ -23,22 +34,16 @@ type Process interface {
 	Memory() MemoryReadWriter
 }
 
-// ProcessInternal holds a set of methods that are not meant to be called by
-// anyone except for an instance of `proc.Target`. These methods are not
-// safe to use by themselves and should never be called directly outside of
-// the `proc` package.
-// This is temporary and in support of an ongoing refactor.
+// ProcessInternal holds a set of methods that need to be implemented by a
+// Delve backend. Methods in the Process interface are safe to be called by
+// clients of the 'proc' library, while all other methods are only called
+// directly within 'proc'.
 type ProcessInternal interface {
+	Process
 	// Valid returns true if this Process can be used. When it returns false it
 	// also returns an error describing why the Process is invalid (either
 	// ErrProcessExited or ErrProcessDetached).
 	Valid() (bool, error)
-	// Restart restarts the recording from the specified position, or from the
-	// last checkpoint if pos == "".
-	// If pos starts with 'c' it's a checkpoint ID, otherwise it's an event
-	// number.
-	// Returns the new current thread after the restart has completed.
-	Restart(pos string) (Thread, error)
 	Detach(bool) error
 	ContinueOnce() (trapthread Thread, stopReason StopReason, err error)
 
@@ -47,6 +52,7 @@ type ProcessInternal interface {
 
 	SupportsBPF() bool
 	SetUProbe(string, int64, []ebpf.UProbeArgMap) error
+	GetBufferedTracepoints() []ebpf.RawUProbeParams
 
 	// DumpProcessNotes returns ELF core notes describing the process and its threads.
 	// Implementing this method is optional.
@@ -54,8 +60,6 @@ type ProcessInternal interface {
 	// MemoryMap returns the memory map of the target process. This method must be implemented if CanDump is true.
 	MemoryMap() ([]MemoryMapEntry, error)
 
-	GetBufferedTracepoints() []ebpf.RawUProbeParams
-
 	// StartCallInjection notifies the backend that we are about to inject a function call.
 	StartCallInjection() (func(), error)
 }
@@ -79,6 +83,19 @@ type RecordingManipulation interface {
 	ClearCheckpoint(id int) error
 }
 
+// RecordingManipulationInternal is an interface that a Delve backend can
+// implement if it is a recording.
+type RecordingManipulationInternal interface {
+	RecordingManipulation
+
+	// Restart restarts the recording from the specified position, or from the
+	// last checkpoint if pos == "".
+	// If pos starts with 'c' it's a checkpoint ID, otherwise it's an event
+	// number.
+	// Returns the new current thread after the restart has completed.
+	Restart(pos string) (Thread, error)
+}
+
 // Direction is the direction of execution for the target process.
 type Direction int8
 
@@ -95,31 +112,3 @@ type Checkpoint struct {
 	When  string
 	Where string
 }
-
-// Info is an interface that provides general information on the target.
-type Info interface {
-	Pid() int
-	// ResumeNotify specifies a channel that will be closed the next time
-	// ContinueOnce finishes resuming the target.
-	ResumeNotify(chan<- struct{})
-	BinInfo() *BinaryInfo
-	EntryPoint() (uint64, error)
-
-	ThreadInfo
-}
-
-// ThreadInfo is an interface for getting information on active threads
-// in the process.
-type ThreadInfo interface {
-	FindThread(threadID int) (Thread, bool)
-	ThreadList() []Thread
-}
-
-// ProcessManipulation is an interface for changing the execution state of a process.
-type ProcessManipulation interface {
-	// RequestManualStop attempts to stop all the process' threads.
-	RequestManualStop() error
-	// CheckAndClearManualStopRequest returns true the first time it's called
-	// after a call to RequestManualStop.
-	CheckAndClearManualStopRequest() bool
-}
diff --git a/pkg/proc/internal/ebpf/bpf/dummy.go b/pkg/proc/internal/ebpf/bpf/dummy.go
new file mode 100644
index 00000000..4983aacb
--- /dev/null
+++ b/pkg/proc/internal/ebpf/bpf/dummy.go
@@ -0,0 +1,4 @@
+//go:build dummy
+// +build dummy
+
+package ebpf
diff --git a/pkg/proc/internal/ebpf/bpf/include/dummy.go b/pkg/proc/internal/ebpf/bpf/include/dummy.go
new file mode 100644
index 00000000..4983aacb
--- /dev/null
+++ b/pkg/proc/internal/ebpf/bpf/include/dummy.go
@@ -0,0 +1,4 @@
+//go:build dummy
+// +build dummy
+
+package ebpf
diff --git a/pkg/proc/internal/ebpf/dummy.go b/pkg/proc/internal/ebpf/dummy.go
new file mode 100644
index 00000000..9ef6e6d3
--- /dev/null
+++ b/pkg/proc/internal/ebpf/dummy.go
@@ -0,0 +1,14 @@
+//go:build dummy
+// +build dummy
+
+// This file is part of a workaround for `go mod vendor` which won't
+// vendor C files if there are no Go files in the same directory.
+//
+// See https://github.com/golang/go/issues/26366
+
+package ebpf
+
+import (
+	_ "github.com/go-delve/delve/pkg/proc/internal/ebpf/bpf"
+	_ "github.com/go-delve/delve/pkg/proc/internal/ebpf/bpf/include"
+)
diff --git a/pkg/proc/native/dump_linux.go b/pkg/proc/native/dump_linux.go
index dd38d68a..fc393c45 100644
--- a/pkg/proc/native/dump_linux.go
+++ b/pkg/proc/native/dump_linux.go
@@ -12,10 +12,10 @@ import (
 func (p *nativeProcess) MemoryMap() ([]proc.MemoryMapEntry, error) {
 	const VmFlagsPrefix = "VmFlags:"
 
-	smapsbuf, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/smaps", p.Pid()))
+	smapsbuf, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/smaps", p.pid))
 	if err != nil {
 		// Older versions of Linux don't have smaps but have maps which is in a similar format.
-		smapsbuf, err = ioutil.ReadFile(fmt.Sprintf("/proc/%d/maps", p.Pid()))
+		smapsbuf, err = ioutil.ReadFile(fmt.Sprintf("/proc/%d/maps", p.pid))
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/proc/native/nonative_darwin.go b/pkg/proc/native/nonative_darwin.go
index 73308b0d..0ef1e43a 100644
--- a/pkg/proc/native/nonative_darwin.go
+++ b/pkg/proc/native/nonative_darwin.go
@@ -37,10 +37,6 @@ type osProcessDetails struct{}
 
 func (os *osProcessDetails) Close() {}
 
-func findExecutable(path string, pid int) string {
-	panic(ErrNativeBackendDisabled)
-}
-
 func killProcess(pid int) error {
 	panic(ErrNativeBackendDisabled)
 }
diff --git a/pkg/proc/native/proc.go b/pkg/proc/native/proc.go
index bae9c86a..f5b25cb4 100644
--- a/pkg/proc/native/proc.go
+++ b/pkg/proc/native/proc.go
@@ -46,8 +46,6 @@ type nativeProcess struct {
 	exited, detached bool
 }
 
-var _ proc.ProcessInternal = &nativeProcess{}
-
 // newProcess returns an initialized Process struct. Before returning,
 // it will also launch a goroutine in order to handle ptrace(2)
 // functions. For more information, see the documentation on
@@ -72,40 +70,6 @@ func (dbp *nativeProcess) BinInfo() *proc.BinaryInfo {
 	return dbp.bi
 }
 
-// Recorded always returns false for the native proc backend.
-func (dbp *nativeProcess) Recorded() (bool, string) { return false, "" }
-
-// Restart will always return an error in the native proc backend, only for
-// recorded traces.
-func (dbp *nativeProcess) Restart(string) (proc.Thread, error) { return nil, proc.ErrNotRecorded }
-
-// ChangeDirection will always return an error in the native proc backend, only for
-// recorded traces.
-func (dbp *nativeProcess) ChangeDirection(dir proc.Direction) error {
-	if dir != proc.Forward {
-		return proc.ErrNotRecorded
-	}
-	return nil
-}
-
-// GetDirection will always return Forward.
-func (p *nativeProcess) GetDirection() proc.Direction { return proc.Forward }
-
-// When will always return an empty string and nil, not supported on native proc backend.
-func (dbp *nativeProcess) When() (string, error) { return "", nil }
-
-// Checkpoint will always return an error on the native proc backend,
-// only supported for recorded traces.
-func (dbp *nativeProcess) Checkpoint(string) (int, error) { return -1, proc.ErrNotRecorded }
-
-// Checkpoints will always return an error on the native proc backend,
-// only supported for recorded traces.
-func (dbp *nativeProcess) Checkpoints() ([]proc.Checkpoint, error) { return nil, proc.ErrNotRecorded }
-
-// ClearCheckpoint will always return an error on the native proc backend,
-// only supported in recorded traces.
-func (dbp *nativeProcess) ClearCheckpoint(int) error { return proc.ErrNotRecorded }
-
 // StartCallInjection notifies the backend that we are about to inject a function call.
 func (dbp *nativeProcess) StartCallInjection() (func(), error) { return func() {}, nil }
 
@@ -143,7 +107,7 @@ func (dbp *nativeProcess) Valid() (bool, error) {
 		return false, proc.ErrProcessDetached
 	}
 	if dbp.exited {
-		return false, proc.ErrProcessExited{Pid: dbp.Pid()}
+		return false, proc.ErrProcessExited{Pid: dbp.pid}
 	}
 	return true, nil
 }
@@ -154,11 +118,6 @@ func (dbp *nativeProcess) ResumeNotify(ch chan<- struct{}) {
 	dbp.resumeChan = ch
 }
 
-// Pid returns the process ID.
-func (dbp *nativeProcess) Pid() int {
-	return dbp.pid
-}
-
 // ThreadList returns a list of threads in the process.
 func (dbp *nativeProcess) ThreadList() []proc.Thread {
 	r := make([]proc.Thread, 0, len(dbp.threads))
@@ -188,7 +147,7 @@ func (dbp *nativeProcess) Breakpoints() *proc.BreakpointMap {
 // sends SIGSTOP to all threads.
 func (dbp *nativeProcess) RequestManualStop() error {
 	if dbp.exited {
-		return proc.ErrProcessExited{Pid: dbp.Pid()}
+		return proc.ErrProcessExited{Pid: dbp.pid}
 	}
 	dbp.stopMu.Lock()
 	defer dbp.stopMu.Unlock()
@@ -245,7 +204,7 @@ func (dbp *nativeProcess) EraseBreakpoint(bp *proc.Breakpoint) error {
 // This could be the result of a breakpoint or signal.
 func (dbp *nativeProcess) ContinueOnce() (proc.Thread, proc.StopReason, error) {
 	if dbp.exited {
-		return nil, proc.StopExited, proc.ErrProcessExited{Pid: dbp.Pid()}
+		return nil, proc.StopExited, proc.ErrProcessExited{Pid: dbp.pid}
 	}
 
 	for {
@@ -306,7 +265,7 @@ func (dbp *nativeProcess) initialize(path string, debugInfoDirs []string) (*proc
 	if !dbp.childProcess {
 		stopReason = proc.StopAttached
 	}
-	tgt, err := proc.NewTarget(dbp, dbp.memthread, proc.NewTargetConfig{
+	tgt, err := proc.NewTarget(dbp, dbp.pid, dbp.memthread, proc.NewTargetConfig{
 		Path:                path,
 		DebugInfoDirs:       debugInfoDirs,
 		DisableAsyncPreempt: runtime.GOOS == "windows" || runtime.GOOS == "freebsd",
diff --git a/pkg/proc/native/proc_darwin.go b/pkg/proc/native/proc_darwin.go
index 4be4462e..74e176be 100644
--- a/pkg/proc/native/proc_darwin.go
+++ b/pkg/proc/native/proc_darwin.go
@@ -435,7 +435,7 @@ func (dbp *nativeProcess) resume() error {
 // stop stops all running threads and sets breakpoints
 func (dbp *nativeProcess) stop(trapthread *nativeThread) (*nativeThread, error) {
 	if dbp.exited {
-		return nil, proc.ErrProcessExited{Pid: dbp.Pid()}
+		return nil, proc.ErrProcessExited{Pid: dbp.pid}
 	}
 	for _, th := range dbp.threads {
 		if !th.Stopped() {
diff --git a/pkg/proc/native/proc_freebsd.go b/pkg/proc/native/proc_freebsd.go
index d7fb5d5b..c2adc73e 100644
--- a/pkg/proc/native/proc_freebsd.go
+++ b/pkg/proc/native/proc_freebsd.go
@@ -352,7 +352,7 @@ func (dbp *nativeProcess) resume() error {
 // stop stops all running threads and sets breakpoints
 func (dbp *nativeProcess) stop(trapthread *nativeThread) (*nativeThread, error) {
 	if dbp.exited {
-		return nil, proc.ErrProcessExited{Pid: dbp.Pid()}
+		return nil, proc.ErrProcessExited{Pid: dbp.pid}
 	}
 	// set breakpoints on all threads
 	for _, th := range dbp.threads {
diff --git a/pkg/proc/native/proc_linux.go b/pkg/proc/native/proc_linux.go
index 848d4fbf..371735fe 100644
--- a/pkg/proc/native/proc_linux.go
+++ b/pkg/proc/native/proc_linux.go
@@ -540,7 +540,7 @@ func (dbp *nativeProcess) resume() error {
 // stop stops all running threads and sets breakpoints
 func (dbp *nativeProcess) stop(trapthread *nativeThread) (*nativeThread, error) {
 	if dbp.exited {
-		return nil, proc.ErrProcessExited{Pid: dbp.Pid()}
+		return nil, proc.ErrProcessExited{Pid: dbp.pid}
 	}
 
 	for _, th := range dbp.threads {
@@ -772,7 +772,7 @@ func (dbp *nativeProcess) SetUProbe(fnName string, goidOffset int64, args []ebpf
 		if err != nil {
 			return err
 		}
-		err = dbp.os.ebpf.AttachUprobe(dbp.Pid(), debugname, off)
+		err = dbp.os.ebpf.AttachUprobe(dbp.pid, debugname, off)
 		if err != nil {
 			return err
 		}
@@ -783,7 +783,7 @@ func (dbp *nativeProcess) SetUProbe(fnName string, goidOffset int64, args []ebpf
 		return err
 	}
 
-	return dbp.os.ebpf.AttachUprobe(dbp.Pid(), debugname, off)
+	return dbp.os.ebpf.AttachUprobe(dbp.pid, debugname, off)
 }
 
 func killProcess(pid int) error {
diff --git a/pkg/proc/native/proc_windows.go b/pkg/proc/native/proc_windows.go
index a53b918f..5b8c3d35 100644
--- a/pkg/proc/native/proc_windows.go
+++ b/pkg/proc/native/proc_windows.go
@@ -235,10 +235,6 @@ func (dbp *nativeProcess) addThread(hThread syscall.Handle, threadID int, attach
 	return thread, nil
 }
 
-func findExecutable(path string, pid int) string {
-	return path
-}
-
 type waitForDebugEventFlags int
 
 const (
@@ -428,7 +424,7 @@ func (dbp *nativeProcess) resume() error {
 // stop stops all running threads threads and sets breakpoints
 func (dbp *nativeProcess) stop(trapthread *nativeThread) (*nativeThread, error) {
 	if dbp.exited {
-		return nil, proc.ErrProcessExited{Pid: dbp.Pid()}
+		return nil, proc.ErrProcessExited{Pid: dbp.pid}
 	}
 
 	dbp.os.running = false
diff --git a/pkg/proc/native/ptrace_linux.go b/pkg/proc/native/ptrace_linux.go
index d1312755..38915d98 100644
--- a/pkg/proc/native/ptrace_linux.go
+++ b/pkg/proc/native/ptrace_linux.go
@@ -24,3 +24,12 @@ func ptraceDetach(tid, sig int) error {
 func ptraceCont(tid, sig int) error {
 	return sys.PtraceCont(tid, sig)
 }
+
+// ptraceSingleStep executes ptrace PTRACE_SINGLESTEP
+func ptraceSingleStep(pid, sig int) error {
+	_, _, e1 := sys.Syscall6(sys.SYS_PTRACE, uintptr(sys.PTRACE_SINGLESTEP), uintptr(pid), uintptr(0), uintptr(sig), 0, 0)
+	if e1 != 0 {
+		return e1
+	}
+	return nil
+}
diff --git a/pkg/proc/native/threads_linux.go b/pkg/proc/native/threads_linux.go
index ae278160..9618c3da 100644
--- a/pkg/proc/native/threads_linux.go
+++ b/pkg/proc/native/threads_linux.go
@@ -48,8 +48,10 @@ func (t *nativeThread) resumeWithSig(sig int) (err error) {
 }
 
 func (t *nativeThread) singleStep() (err error) {
+	sig := 0
 	for {
-		t.dbp.execPtraceFunc(func() { err = sys.PtraceSingleStep(t.ID) })
+		t.dbp.execPtraceFunc(func() { err = ptraceSingleStep(t.ID, sig) })
+		sig = 0
 		if err != nil {
 			return err
 		}
@@ -65,8 +67,19 @@ func (t *nativeThread) singleStep() (err error) {
 			}
 			return proc.ErrProcessExited{Pid: t.dbp.pid, Status: rs}
 		}
-		if wpid == t.ID && status.StopSignal() == sys.SIGTRAP {
-			return nil
+		if wpid == t.ID {
+			switch s := status.StopSignal(); s {
+			case sys.SIGTRAP:
+				return nil
+			case sys.SIGSTOP:
+				// delayed SIGSTOP, ignore it
+			case sys.SIGILL, sys.SIGBUS, sys.SIGFPE, sys.SIGSEGV, sys.SIGSTKFLT:
+				// propagate signals that can have been caused by the current instruction
+				sig = int(s)
+			default:
+				// delay propagation of all other signals
+				t.os.delayedSignal = int(s)
+			}
 		}
 	}
 }
diff --git a/pkg/proc/target.go b/pkg/proc/target.go
index dc503e19..66e61d71 100644
--- a/pkg/proc/target.go
+++ b/pkg/proc/target.go
@@ -36,8 +36,12 @@ const (
 // Target represents the process being debugged.
 type Target struct {
 	Process
+	RecordingManipulation
 
-	proc ProcessInternal
+	proc   ProcessInternal
+	recman RecordingManipulationInternal
+
+	pid int
 
 	// StopReason describes the reason why the target process is stopped.
 	// A process could be stopped for multiple simultaneous reasons, in which
@@ -169,7 +173,8 @@ func DisableAsyncPreemptEnv() []string {
 }
 
 // NewTarget returns an initialized Target object.
-func NewTarget(p Process, currentThread Thread, cfg NewTargetConfig) (*Target, error) {
+// The p argument can optionally implement the RecordingManipulation interface.
+func NewTarget(p ProcessInternal, pid int, currentThread Thread, cfg NewTargetConfig) (*Target, error) {
 	entryPoint, err := p.EntryPoint()
 	if err != nil {
 		return nil, err
@@ -187,13 +192,21 @@ func NewTarget(p Process, currentThread Thread, cfg NewTargetConfig) (*Target, e
 
 	t := &Target{
 		Process:       p,
-		proc:          p.(ProcessInternal),
+		proc:          p,
 		fncallForG:    make(map[int]*callInjection),
 		StopReason:    cfg.StopReason,
 		currentThread: currentThread,
 		CanDump:       cfg.CanDump,
+		pid:           pid,
 	}
 
+	if recman, ok := p.(RecordingManipulationInternal); ok {
+		t.recman = recman
+	} else {
+		t.recman = &dummyRecordingManipulation{}
+	}
+	t.RecordingManipulation = t.recman
+
 	g, _ := GetG(currentThread)
 	t.selectedGoroutine = g
 
@@ -210,6 +223,11 @@ func NewTarget(p Process, currentThread Thread, cfg NewTargetConfig) (*Target, e
 	return t, nil
 }
 
+// Pid returns the pid of the target process.
+func (t *Target) Pid() int {
+	return t.pid
+}
+
 // IsCgo returns the value of runtime.iscgo
 func (t *Target) IsCgo() bool {
 	if t.iscgo != nil {
@@ -265,7 +283,7 @@ func (t *Target) ClearCaches() {
 // Restarting of a normal process happens at a higher level (debugger.Restart).
 func (t *Target) Restart(from string) error {
 	t.ClearCaches()
-	currentThread, err := t.proc.Restart(from)
+	currentThread, err := t.recman.Restart(from)
 	if err != nil {
 		return err
 	}
@@ -544,3 +562,40 @@ func (t *Target) dwrapUnwrap(fn *Function) *Function {
 	}
 	return fn
 }
+
+type dummyRecordingManipulation struct {
+}
+
+// Recorded always returns false for the native proc backend.
+func (*dummyRecordingManipulation) Recorded() (bool, string) { return false, "" }
+
+// ChangeDirection will always return an error in the native proc backend, only for
+// recorded traces.
+func (*dummyRecordingManipulation) ChangeDirection(dir Direction) error {
+	if dir != Forward {
+		return ErrNotRecorded
+	}
+	return nil
+}
+
+// GetDirection will always return Forward.
+func (*dummyRecordingManipulation) GetDirection() Direction { return Forward }
+
+// When will always return an empty string and nil, not supported on native proc backend.
+func (*dummyRecordingManipulation) When() (string, error) { return "", nil }
+
+// Checkpoint will always return an error on the native proc backend,
+// only supported for recorded traces.
+func (*dummyRecordingManipulation) Checkpoint(string) (int, error) { return -1, ErrNotRecorded }
+
+// Checkpoints will always return an error on the native proc backend,
+// only supported for recorded traces.
+func (*dummyRecordingManipulation) Checkpoints() ([]Checkpoint, error) { return nil, ErrNotRecorded }
+
+// ClearCheckpoint will always return an error on the native proc backend,
+// only supported in recorded traces.
+func (*dummyRecordingManipulation) ClearCheckpoint(int) error { return ErrNotRecorded }
+
+// Restart will always return an error in the native proc backend, only for
+// recorded traces.
+func (*dummyRecordingManipulation) Restart(string) (Thread, error) { return nil, ErrNotRecorded }
diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go
index 5dfe5224..272f6071 100644
--- a/pkg/terminal/command.go
+++ b/pkg/terminal/command.go
@@ -772,27 +772,17 @@ func (a byGoroutineID) Len() int           { return len(a) }
 func (a byGoroutineID) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
 func (a byGoroutineID) Less(i, j int) bool { return a[i].ID < a[j].ID }
 
-// The number of goroutines we're going to request on each RPC call
-const goroutineBatchSize = 10000
-
-type printGoroutinesFlags uint8
-
-const (
-	printGoroutinesStack printGoroutinesFlags = 1 << iota
-	printGoroutinesLabels
-)
-
-func printGoroutines(t *Term, indent string, gs []*api.Goroutine, fgl formatGoroutineLoc, flags printGoroutinesFlags, depth int, state *api.DebuggerState) error {
+func printGoroutines(t *Term, indent string, gs []*api.Goroutine, fgl api.FormatGoroutineLoc, flags api.PrintGoroutinesFlags, depth int, state *api.DebuggerState) error {
 	for _, g := range gs {
 		prefix := indent + "  "
 		if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID {
 			prefix = indent + "* "
 		}
 		fmt.Printf("%sGoroutine %s\n", prefix, t.formatGoroutine(g, fgl))
-		if flags&printGoroutinesLabels != 0 {
+		if flags&api.PrintGoroutinesLabels != 0 {
 			writeGoroutineLabels(os.Stdout, g, indent+"\t")
 		}
-		if flags&printGoroutinesStack != 0 {
+		if flags&api.PrintGoroutinesStack != 0 {
 			stack, err := t.client.Stacktrace(g.ID, depth, 0, nil)
 			if err != nil {
 				return err
@@ -803,83 +793,10 @@ func printGoroutines(t *Term, indent string, gs []*api.Goroutine, fgl formatGoro
 	return nil
 }
 
-const (
-	maxGroupMembers    = 5
-	maxGoroutineGroups = 50
-)
-
 func goroutines(t *Term, ctx callContext, argstr string) error {
-	args := strings.Split(argstr, " ")
-	var filters []api.ListGoroutinesFilter
-	var group api.GoroutineGroupingOptions
-	var fgl = fglUserCurrent
-	var flags printGoroutinesFlags
-	var depth = 10
-	var batchSize = goroutineBatchSize
-
-	group.MaxGroupMembers = maxGroupMembers
-	group.MaxGroups = maxGoroutineGroups
-
-	for i := 0; i < len(args); i++ {
-		arg := args[i]
-		switch arg {
-		case "-u":
-			fgl = fglUserCurrent
-		case "-r":
-			fgl = fglRuntimeCurrent
-		case "-g":
-			fgl = fglGo
-		case "-s":
-			fgl = fglStart
-		case "-l":
-			flags |= printGoroutinesLabels
-		case "-t":
-			flags |= printGoroutinesStack
-			// optional depth argument
-			if i+1 < len(args) && len(args[i+1]) > 0 {
-				n, err := strconv.Atoi(args[i+1])
-				if err == nil {
-					depth = n
-					i++
-				}
-			}
-
-		case "-w", "-with":
-			filter, err := readGoroutinesFilter(args, &i)
-			if err != nil {
-				return err
-			}
-			filters = append(filters, *filter)
-
-		case "-wo", "-without":
-			filter, err := readGoroutinesFilter(args, &i)
-			if err != nil {
-				return err
-			}
-			filter.Negated = true
-			filters = append(filters, *filter)
-
-		case "-group":
-			var err error
-			group.GroupBy, err = readGoroutinesFilterKind(args, i+1)
-			if err != nil {
-				return err
-			}
-			i++
-			if group.GroupBy == api.GoroutineLabel {
-				if i+1 >= len(args) {
-					return errors.New("-group label must be followed by an argument")
-				}
-				group.GroupByKey = args[i+1]
-				i++
-			}
-			batchSize = 0 // grouping only works well if run on all goroutines
-
-		case "":
-			// nothing to do
-		default:
-			return fmt.Errorf("wrong argument: '%s'", arg)
-		}
+	filters, group, fgl, flags, depth, batchSize, err := api.ParseGoroutineArgs(argstr)
+	if err != nil {
+		return err
 	}
 
 	state, err := t.client.GetState()
@@ -933,52 +850,6 @@ func goroutines(t *Term, ctx callContext, argstr string) error {
 	return nil
 }
 
-func readGoroutinesFilterKind(args []string, i int) (api.GoroutineField, error) {
-	if i >= len(args) {
-		return api.GoroutineFieldNone, fmt.Errorf("%s must be followed by an argument", args[i-1])
-	}
-
-	switch args[i] {
-	case "curloc":
-		return api.GoroutineCurrentLoc, nil
-	case "userloc":
-		return api.GoroutineUserLoc, nil
-	case "goloc":
-		return api.GoroutineGoLoc, nil
-	case "startloc":
-		return api.GoroutineStartLoc, nil
-	case "label":
-		return api.GoroutineLabel, nil
-	case "running":
-		return api.GoroutineRunning, nil
-	case "user":
-		return api.GoroutineUser, nil
-	default:
-		return api.GoroutineFieldNone, fmt.Errorf("unrecognized argument to %s %s", args[i-1], args[i])
-	}
-}
-
-func readGoroutinesFilter(args []string, pi *int) (*api.ListGoroutinesFilter, error) {
-	r := new(api.ListGoroutinesFilter)
-	var err error
-	r.Kind, err = readGoroutinesFilterKind(args, *pi+1)
-	if err != nil {
-		return nil, err
-	}
-	*pi++
-	switch r.Kind {
-	case api.GoroutineRunning, api.GoroutineUser:
-		return r, nil
-	}
-	if *pi+1 >= len(args) {
-		return nil, fmt.Errorf("%s %s needs to be followed by an expression", args[*pi-1], args[*pi])
-	}
-	r.Arg = args[*pi+1]
-	*pi++
-
-	return r, nil
-}
-
 func selectedGID(state *api.DebuggerState) int {
 	if state.SelectedGoroutine == nil {
 		return 0
@@ -1080,7 +951,10 @@ func (c *Commands) frameCommand(t *Term, ctx callContext, argstr string, directi
 func (c *Commands) deferredCommand(t *Term, ctx callContext, argstr string) error {
 	ctx.Prefix = deferredPrefix
 
-	space := strings.Index(argstr, " ")
+	space := strings.IndexRune(argstr, ' ')
+	if space < 0 {
+		return errors.New("not enough arguments")
+	}
 
 	var err error
 	ctx.Scope.DeferredCall, err = strconv.Atoi(argstr[:space])
@@ -1113,20 +987,11 @@ func (t *Term) formatThread(th *api.Thread) string {
 	return fmt.Sprintf("%d at %s:%d", th.ID, t.formatPath(th.File), th.Line)
 }
 
-type formatGoroutineLoc int
-
-const (
-	fglRuntimeCurrent = formatGoroutineLoc(iota)
-	fglUserCurrent
-	fglGo
-	fglStart
-)
-
 func (t *Term) formatLocation(loc api.Location) string {
 	return fmt.Sprintf("%s:%d %s (%#v)", t.formatPath(loc.File), loc.Line, loc.Function.Name(), loc.PC)
 }
 
-func (t *Term) formatGoroutine(g *api.Goroutine, fgl formatGoroutineLoc) string {
+func (t *Term) formatGoroutine(g *api.Goroutine, fgl api.FormatGoroutineLoc) string {
 	if g == nil {
 		return "<nil>"
 	}
@@ -1136,16 +1001,16 @@ func (t *Term) formatGoroutine(g *api.Goroutine, fgl formatGoroutineLoc) string
 	var locname string
 	var loc api.Location
 	switch fgl {
-	case fglRuntimeCurrent:
+	case api.FglRuntimeCurrent:
 		locname = "Runtime"
 		loc = g.CurrentLoc
-	case fglUserCurrent:
+	case api.FglUserCurrent:
 		locname = "User"
 		loc = g.UserCurrentLoc
-	case fglGo:
+	case api.FglGo:
 		locname = "Go"
 		loc = g.GoStatementLoc
-	case fglStart:
+	case api.FglStart:
 		locname = "Start"
 		loc = g.StartLoc
 	}
diff --git a/pkg/version/fixbuild.go b/pkg/version/fixbuild.go
new file mode 100644
index 00000000..e2dad6e0
--- /dev/null
+++ b/pkg/version/fixbuild.go
@@ -0,0 +1,23 @@
+//go:build go1.18
+// +build go1.18
+
+package version
+
+import "runtime/debug"
+
+func init() {
+	fixBuild = buildInfoFixBuild
+}
+
+func buildInfoFixBuild(v *Version) {
+	info, ok := debug.ReadBuildInfo()
+	if !ok {
+		return
+	}
+	for i := range info.Settings {
+		if info.Settings[i].Key == "gitrevision" {
+			v.Build = info.Settings[i].Value
+			break
+		}
+	}
+}
diff --git a/pkg/version/version.go b/pkg/version/version.go
index 4c81a2a3..c532746b 100644
--- a/pkg/version/version.go
+++ b/pkg/version/version.go
@@ -24,6 +24,7 @@ var (
 )
 
 func (v Version) String() string {
+	fixBuild(&v)
 	ver := fmt.Sprintf("Version: %s.%s.%s", v.Major, v.Minor, v.Patch)
 	if v.Metadata != "" {
 		ver += "-" + v.Metadata
@@ -38,3 +39,7 @@ var buildInfo = func() string {
 func BuildInfo() string {
 	return fmt.Sprintf("%s\n%s", runtime.Version(), buildInfo())
 }
+
+var fixBuild = func(v *Version) {
+	// does nothing
+}
diff --git a/service/api/command.go b/service/api/command.go
new file mode 100644
index 00000000..503dc54c
--- /dev/null
+++ b/service/api/command.go
@@ -0,0 +1,153 @@
+package api
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+type PrintGoroutinesFlags uint8
+
+const (
+	PrintGoroutinesStack PrintGoroutinesFlags = 1 << iota
+	PrintGoroutinesLabels
+)
+
+type FormatGoroutineLoc int
+
+const (
+	FglRuntimeCurrent = FormatGoroutineLoc(iota)
+	FglUserCurrent
+	FglGo
+	FglStart
+)
+
+const (
+	maxGroupMembers    = 5
+	maxGoroutineGroups = 50
+)
+
+// The number of goroutines we're going to request on each RPC call
+const goroutineBatchSize = 10000
+
+func ParseGoroutineArgs(argstr string) ([]ListGoroutinesFilter, GoroutineGroupingOptions, FormatGoroutineLoc, PrintGoroutinesFlags, int, int, error) {
+	args := strings.Split(argstr, " ")
+	var filters []ListGoroutinesFilter
+	var group GoroutineGroupingOptions
+	var fgl = FglUserCurrent
+	var flags PrintGoroutinesFlags
+	var depth = 10
+	var batchSize = goroutineBatchSize
+
+	group.MaxGroupMembers = maxGroupMembers
+	group.MaxGroups = maxGoroutineGroups
+
+	for i := 0; i < len(args); i++ {
+		arg := args[i]
+		switch arg {
+		case "-u":
+			fgl = FglUserCurrent
+		case "-r":
+			fgl = FglRuntimeCurrent
+		case "-g":
+			fgl = FglGo
+		case "-s":
+			fgl = FglStart
+		case "-l":
+			flags |= PrintGoroutinesLabels
+		case "-t":
+			flags |= PrintGoroutinesStack
+			// optional depth argument
+			if i+1 < len(args) && len(args[i+1]) > 0 {
+				n, err := strconv.Atoi(args[i+1])
+				if err == nil {
+					depth = n
+					i++
+				}
+			}
+
+		case "-w", "-with":
+			filter, err := readGoroutinesFilter(args, &i)
+			if err != nil {
+				return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, fmt.Errorf("wrong argument: '%s'", arg)
+			}
+			filters = append(filters, *filter)
+
+		case "-wo", "-without":
+			filter, err := readGoroutinesFilter(args, &i)
+			if err != nil {
+				return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, fmt.Errorf("wrong argument: '%s'", arg)
+			}
+			filter.Negated = true
+			filters = append(filters, *filter)
+
+		case "-group":
+			var err error
+			group.GroupBy, err = readGoroutinesFilterKind(args, i+1)
+			if err != nil {
+				return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, fmt.Errorf("wrong argument: '%s'", arg)
+			}
+			i++
+			if group.GroupBy == GoroutineLabel {
+				if i+1 >= len(args) {
+					return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, fmt.Errorf("wrong argument: '%s'", arg)
+				}
+				group.GroupByKey = args[i+1]
+				i++
+			}
+			batchSize = 0 // grouping only works well if run on all goroutines
+
+		case "":
+			// nothing to do
+		default:
+			return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, fmt.Errorf("wrong argument: '%s'", arg)
+		}
+	}
+	return filters, group, fgl, flags, depth, batchSize, nil
+}
+
+func readGoroutinesFilterKind(args []string, i int) (GoroutineField, error) {
+	if i >= len(args) {
+		return GoroutineFieldNone, fmt.Errorf("%s must be followed by an argument", args[i-1])
+	}
+
+	switch args[i] {
+	case "curloc":
+		return GoroutineCurrentLoc, nil
+	case "userloc":
+		return GoroutineUserLoc, nil
+	case "goloc":
+		return GoroutineGoLoc, nil
+	case "startloc":
+		return GoroutineStartLoc, nil
+	case "label":
+		return GoroutineLabel, nil
+	case "running":
+		return GoroutineRunning, nil
+	case "user":
+		return GoroutineUser, nil
+	default:
+		return GoroutineFieldNone, fmt.Errorf("unrecognized argument to %s %s", args[i-1], args[i])
+	}
+}
+
+func readGoroutinesFilter(args []string, pi *int) (*ListGoroutinesFilter, error) {
+	r := new(ListGoroutinesFilter)
+	var err error
+	r.Kind, err = readGoroutinesFilterKind(args, *pi+1)
+	if err != nil {
+		return nil, err
+	}
+	*pi++
+	switch r.Kind {
+	case GoroutineRunning, GoroutineUser:
+		return r, nil
+	}
+	if *pi+1 >= len(args) {
+		return nil, fmt.Errorf("%s %s needs to be followed by an expression", args[*pi-1], args[*pi])
+	}
+	r.Arg = args[*pi+1]
+	*pi++
+
+	return r, nil
+}
diff --git a/service/dap/command.go b/service/dap/command.go
index 0b08c569..ab318e09 100644
--- a/service/dap/command.go
+++ b/service/dap/command.go
@@ -4,9 +4,11 @@ import (
 	"bytes"
 	"errors"
 	"fmt"
+	"sort"
 	"strings"
 
 	"github.com/go-delve/delve/pkg/config"
+	"github.com/google/go-dap"
 )
 
 func (s *Session) delveCmd(goid, frame int, cmdstr string) (string, error) {
@@ -37,24 +39,33 @@ type command struct {
 const (
 	msgHelp = `Prints the help message.
 	
-help [command]
+dlv help [command]
 
 Type "help" followed by the name of a command for more information about it.`
 
 	msgConfig = `Changes configuration parameters.
 	
-	config -list
+	dlv config -list
 	
 	Show all configuration parameters.
+
+	config -list <parameter>
+	
+	Show value of a configuration parameter.
 	
-	config <parameter> <value>
+	dlv config <parameter> <value>
 	
 	Changes the value of a configuration parameter.
 	
-	config substitutePath <from> <to>
-	config substitutePath <from>
+	dlv config substitutePath <from> <to>
+	dlv config substitutePath <from>
 	
 	Adds or removes a path substitution rule.`
+	msgSources = `Print list of source files.
+
+	dlv sources [<regex>]
+
+If regex is specified only the source files matching it will be returned.`
 )
 
 // debugCommands returns a list of commands with default commands defined.
@@ -62,6 +73,7 @@ func debugCommands(s *Session) []command {
 	return []command{
 		{aliases: []string{"help", "h"}, cmdFn: s.helpMessage, helpMsg: msgHelp},
 		{aliases: []string{"config"}, cmdFn: s.evaluateConfig, helpMsg: msgConfig},
+		{aliases: []string{"sources", "s"}, cmdFn: s.sources, helpMsg: msgSources},
 	}
 }
 
@@ -88,28 +100,61 @@ func (s *Session) helpMessage(_, _ int, args string) (string, error) {
 			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)
+			fmt.Fprintf(&buf, "    dlv %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.Fprintf(&buf, "    dlv %s \t %s\n", cmd.aliases[0], h)
 		}
 	}
 
 	fmt.Fprintln(&buf)
-	fmt.Fprintln(&buf, "Type help followed by a command for full documentation.")
+	fmt.Fprintln(&buf, "Type 'dlv 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":
+	if name == "-list" {
+		if len(argv) > 1 {
+			return config.ConfigureListByName(&s.args, argv[1], "cfgName"), nil
+		}
 		return listConfig(&s.args), nil
-	default:
-		res, err := configureSet(&s.args, expr)
-		if err != nil {
-			return "", err
+	}
+	updated, res, err := configureSet(&s.args, expr)
+	if err != nil {
+		return "", err
+	}
+
+	if updated {
+		// Send invalidated events for areas that are affected by configuration changes.
+		switch name {
+		case "showGlobalVariables", "showRegisters":
+			// Variable data has become invalidated.
+			s.send(&dap.InvalidatedEvent{
+				Event: *newEvent("invalidated"),
+				Body: dap.InvalidatedEventBody{
+					Areas: []dap.InvalidatedAreas{"variables"},
+				},
+			})
+		case "goroutineFilters", "hideSystemGoroutines":
+			// Thread related data has become invalidated.
+			s.send(&dap.InvalidatedEvent{
+				Event: *newEvent("invalidated"),
+				Body: dap.InvalidatedEventBody{
+					Areas: []dap.InvalidatedAreas{"threads"},
+				},
+			})
 		}
-		return res, nil
+		res += "\nUpdated"
+	}
+	return res, nil
+}
+
+func (s *Session) sources(_, _ int, filter string) (string, error) {
+	sources, err := s.debugger.Sources(filter)
+	if err != nil {
+		return "", err
 	}
+	sort.Strings(sources)
+	return strings.Join(sources, "\n"), nil
 }
diff --git a/service/dap/config.go b/service/dap/config.go
index ec824211..3a13c12d 100644
--- a/service/dap/config.go
+++ b/service/dap/config.go
@@ -13,7 +13,7 @@ func listConfig(args *launchAttachArgs) string {
 	return buf.String()
 }
 
-func configureSet(sargs *launchAttachArgs, args string) (string, error) {
+func configureSet(sargs *launchAttachArgs, args string) (bool, string, error) {
 	v := config.Split2PartsBySpace(args)
 
 	cfgname := v[0]
@@ -24,28 +24,23 @@ func configureSet(sargs *launchAttachArgs, args string) (string, error) {
 
 	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
+		return false, "", fmt.Errorf("%q is not a configuration parameter", cfgname)
 	}
 
 	if cfgname == "substitutePath" {
 		err := configureSetSubstitutePath(sargs, rest)
 		if err != nil {
-			return "", err
+			return false, "", err
 		}
 		// Print the updated client to server and server to client maps.
-		return fmt.Sprintf("%s\nUpdated", config.ConfigureListByName(sargs, cfgname, "cfgName")), nil
+		return true, config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
 	}
 
 	err := config.ConfigureSetSimple(rest, cfgname, field)
 	if err != nil {
-		return "", err
+		return false, "", err
 	}
-	return fmt.Sprintf("%s\nUpdated", config.ConfigureListByName(sargs, cfgname, "cfgName")), nil
+	return true, config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
 }
 
 func configureSetSubstitutePath(args *launchAttachArgs, rest string) error {
diff --git a/service/dap/server.go b/service/dap/server.go
index 3db64834..c9001351 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -213,6 +213,8 @@ type launchAttachArgs struct {
 	ShowGlobalVariables bool `cfgName:"showGlobalVariables"`
 	// ShowRegisters indicates if register values should be loaded.
 	ShowRegisters bool `cfgName:"showRegisters"`
+	// GoroutineFilters are the filters used when loading goroutines.
+	GoroutineFilters string `cfgName:"goroutineFilters"`
 	// HideSystemGoroutines indicates if system goroutines should be removed from threads
 	// responses.
 	HideSystemGoroutines bool `cfgName:"hideSystemGoroutines"`
@@ -233,6 +235,7 @@ var defaultArgs = launchAttachArgs{
 	ShowGlobalVariables:          false,
 	HideSystemGoroutines:         false,
 	ShowRegisters:                false,
+	GoroutineFilters:             "",
 	substitutePathClientToServer: [][2]string{},
 	substitutePathServerToClient: [][2]string{},
 }
@@ -345,6 +348,7 @@ func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) error {
 	s.args.ShowGlobalVariables = args.ShowGlobalVariables
 	s.args.ShowRegisters = args.ShowRegisters
 	s.args.HideSystemGoroutines = args.HideSystemGoroutines
+	s.args.GoroutineFilters = args.GoroutineFilters
 	if paths := args.SubstitutePath; len(paths) > 0 {
 		clientToServer := make([][2]string, 0, len(paths))
 		serverToClient := make([][2]string, 0, len(paths))
@@ -491,6 +495,16 @@ func (s *Server) RunWithClient(conn net.Conn) {
 	go s.runSession(conn)
 }
 
+func (s *Session) address() string {
+	if s.config.Listener != nil {
+		return s.config.Listener.Addr().String()
+	}
+	if netconn, ok := s.conn.ReadWriteCloser.(net.Conn); ok {
+		return netconn.LocalAddr().String()
+	}
+	return ""
+}
+
 // ServeDAPCodec reads and decodes requests from the client
 // until it encounters an error or EOF, when it sends
 // a disconnect signal and returns.
@@ -934,8 +948,8 @@ func cleanExeName(name string) string {
 func (s *Session) onLaunchRequest(request *dap.LaunchRequest) {
 	var err error
 	if s.debugger != nil {
-		s.sendShowUserErrorResponse(request.Request, FailedToLaunch,
-			"Failed to launch", "debugger already started - use remote attach to connect to a server with an active debug session")
+		s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch",
+			fmt.Sprintf("debug session already in progress at %s - use remote attach mode to connect to a server with an active debug session", s.address()))
 		return
 	}
 
@@ -1178,7 +1192,13 @@ func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
 		// This is a multi-use server/debugger, so a disconnect request that doesn't
 		// 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())
+		status := "halted"
+		if s.isRunningCmd() {
+			status = "running"
+		} else if s, err := s.debugger.State(false); processExited(s, err) {
+			status = "exited"
+		}
+		s.logToConsole(fmt.Sprintf("Closing client session, but leaving multi-client DAP server at %s with debuggee %s", s.config.Listener.Addr().String(), status))
 		s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
 		s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
 		s.conn.Close()
@@ -1643,11 +1663,19 @@ 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 {
-			gs = s.debugger.FilterGoroutines(gs, []api.ListGoroutinesFilter{{
-				Kind:    api.GoroutineUser,
-				Negated: false,
-			}})
+		if err == nil {
+			// Parse the goroutine arguments.
+			filters, _, _, _, _, _, parseErr := api.ParseGoroutineArgs(s.args.GoroutineFilters)
+			if parseErr != nil {
+				s.logToConsole(parseErr.Error())
+			}
+			if s.args.HideSystemGoroutines {
+				filters = append(filters, api.ListGoroutinesFilter{
+					Kind:    api.GoroutineUser,
+					Negated: false,
+				})
+			}
+			gs = s.debugger.FilterGoroutines(gs, filters)
 		}
 	}
 
@@ -1752,7 +1780,8 @@ func (s *Session) onAttachRequest(request *dap.AttachRequest) {
 		if s.debugger != nil {
 			s.sendShowUserErrorResponse(
 				request.Request, FailedToAttach,
-				"Failed to attach", "debugger already started - use remote mode to connect")
+				"Failed to attach",
+				fmt.Sprintf("debug session already in progress at %s - use remote mode to connect to a server with an active debug session", s.address()))
 			return
 		}
 		if args.ProcessID == 0 {
@@ -1991,7 +2020,7 @@ func (s *Session) onStackTraceRequest(request *dap.StackTraceRequest) {
 
 		packageName := fnPackageName(loc)
 		if !isSystemGoroutine && packageName == "runtime" {
-			stackFrame.Source.PresentationHint = "deemphasize"
+			stackFrame.PresentationHint = "subtle"
 		}
 		stackFrames = append(stackFrames, stackFrame)
 	}
diff --git a/service/dap/types.go b/service/dap/types.go
index 3b442be9..5625355a 100644
--- a/service/dap/types.go
+++ b/service/dap/types.go
@@ -154,9 +154,14 @@ type LaunchAttachCommonConfig struct {
 	ShowRegisters bool `json:"showRegisters,omitempty"`
 
 	// Boolean value to indicate whether system goroutines
-	// should be should be hidden from the call stack view.
+	// should be hidden from the call stack view.
 	HideSystemGoroutines bool `json:"hideSystemGoroutines,omitempty"`
 
+	// String value to indicate which system goroutines should be
+	// shown in the call stack view. See filtering documentation:
+	// https://github.com/go-delve/delve/blob/master/Documentation/cli/README.md#goroutines
+	GoroutineFilters string `json:"goroutineFilters,omitempty"`
+
 	// An array of mappings from a local path (client) to the remote path (debugger).
 	// This setting is useful when working in a file system with symbolic links,
 	// running remote debugging, or debugging an executable compiled externally.
@@ -195,6 +200,7 @@ func (m *SubstitutePath) UnmarshalJSON(data []byte) error {
 type AttachConfig struct {
 	// Acceptable values are:
 	//   "local": attaches to the local process with the given ProcessID.
+	//   "remote": expects the debugger to already be running to "attach" to an in-progress debug session.
 	//
 	// Default is "local".
 	Mode string `json:"mode"`
diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go
index 055eaf17..24fbae0a 100644
--- a/service/debugger/debugger.go
+++ b/service/debugger/debugger.go
@@ -1332,7 +1332,7 @@ func (d *Debugger) collectBreakpointInformation(state *api.DebuggerState) error
 			bpi.Variables = make([]api.Variable, len(bp.Variables))
 		}
 		for i := range bp.Variables {
-			v, err := s.EvalVariable(bp.Variables[i], proc.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1})
+			v, err := s.EvalExpression(bp.Variables[i], proc.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1})
 			if err != nil {
 				bpi.Variables[i] = api.Variable{Name: bp.Variables[i], Unreadable: fmt.Sprintf("eval error: %v", err)}
 			} else {
@@ -1516,7 +1516,7 @@ func (d *Debugger) Function(goid, frame, deferredCall int, cfg proc.LoadConfig)
 
 // EvalVariableInScope will attempt to evaluate the variable represented by 'symbol'
 // in the scope provided.
-func (d *Debugger) EvalVariableInScope(goid, frame, deferredCall int, symbol string, cfg proc.LoadConfig) (*proc.Variable, error) {
+func (d *Debugger) EvalVariableInScope(goid, frame, deferredCall int, expr string, cfg proc.LoadConfig) (*proc.Variable, error) {
 	d.targetMutex.Lock()
 	defer d.targetMutex.Unlock()
 
@@ -1524,7 +1524,7 @@ func (d *Debugger) EvalVariableInScope(goid, frame, deferredCall int, symbol str
 	if err != nil {
 		return nil, err
 	}
-	return s.EvalVariable(symbol, cfg)
+	return s.EvalExpression(expr, cfg)
 }
 
 // LoadResliced will attempt to 'reslice' a map, array or slice so that the values


Test patch:
diff --git a/_scripts/latestver.py b/_scripts/latestver.py
index 886d476e..b8483009 100644
--- a/_scripts/latestver.py
+++ b/_scripts/latestver.py
@@ -19,7 +19,7 @@ def splitver(x):
 	return v
 
 ver = sys.argv[1]
-d = json.loads(urllib.urlopen('https://golang.org/dl/?mode=json&include=all').read())
+d = json.loads(urllib.urlopen('https://go.dev/dl/?mode=json&include=all').read())
 ds = sorted(d, reverse=True, key=lambda it: splitver(it['version'][2:]))
 for x in ds:
 	if x['version'][:len(ver)] == ver:
diff --git a/_scripts/test_linux.sh b/_scripts/test_linux.sh
index 5bd251f8..846f733e 100755
--- a/_scripts/test_linux.sh
+++ b/_scripts/test_linux.sh
@@ -26,7 +26,7 @@ if [ "$version" = "gotip" ]; then
 		exit 0
 	fi
 	echo Building Go from tip
-	getgo $(curl https://golang.org/VERSION?m=text)
+	getgo $(curl https://go.dev/VERSION?m=text)
 	export GOROOT_BOOTSTRAP=$GOROOT
 	export GOROOT=/usr/local/go/go-tip
 	git clone https://go.googlesource.com/go /usr/local/go/go-tip
@@ -35,7 +35,7 @@ if [ "$version" = "gotip" ]; then
 	cd -
 else
 	echo Finding latest patch version for $version
-	version=$(curl 'https://golang.org/dl/?mode=json&include=all' | jq '.[].version' --raw-output | egrep ^$version'($|\.|beta|rc)' | sort -rV | head -1)
+	version=$(curl 'https://go.dev/dl/?mode=json&include=all' | jq '.[].version' --raw-output | egrep ^$version'($|\.|beta|rc)' | sort -rV | head -1)
 	echo "Go $version on $arch"
 	getgo $version
 fi
@@ -57,7 +57,7 @@ cd delve
 # with the current VCS revision, which does not work with TeamCity
 if [ "$version" = "gotip" ]; then
 	export GOFLAGS=-buildvcs=false
-elif [ ${version:4} -gt 17 ]; then
+elif [ ${version:4:2} -gt 17 ]; then
 	export GOFLAGS=-buildvcs=false
 fi
 
diff --git a/_scripts/test_mac.sh b/_scripts/test_mac.sh
index f4395a84..e48cbf0e 100644
--- a/_scripts/test_mac.sh
+++ b/_scripts/test_mac.sh
@@ -12,7 +12,7 @@ if [ "$GOVERSION" = "gotip" ]; then
     if [ "$arch" != "amd64" ]; then
         exit 0
     fi
-    bootstrapver=$(curl https://golang.org/VERSION?m=text)
+    bootstrapver=$(curl https://go.dev/VERSION?m=text)
     cd $TMPDIR
     curl -sSL "https://storage.googleapis.com/golang/$bootstrapver.darwin-$ARCH.tar.gz" | tar -xz
     cd -
diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go
index 9bd56606..b3f9b6d3 100644
--- a/cmd/dlv/dlv_test.go
+++ b/cmd/dlv/dlv_test.go
@@ -750,9 +750,9 @@ func TestRemoteDAPClient(t *testing.T) {
 	cmd.Wait()
 }
 
-func closeDAPRemoteMultiClient(t *testing.T, c *daptest.Client) {
+func closeDAPRemoteMultiClient(t *testing.T, c *daptest.Client, expectStatus string) {
 	c.DisconnectRequest()
-	c.ExpectOutputEventClosingClient(t)
+	c.ExpectOutputEventClosingClient(t, expectStatus)
 	c.ExpectDisconnectResponse(t)
 	c.ExpectTerminatedEvent(t)
 	c.Close()
@@ -787,6 +787,11 @@ func TestRemoteDAPClientMulti(t *testing.T) {
 		}
 	}()
 
+	// Client 0 connects but with the wrong attach request
+	dapclient0 := daptest.NewClient(listenAddr)
+	dapclient0.AttachRequest(map[string]interface{}{"mode": "local"})
+	dapclient0.ExpectErrorResponse(t)
+
 	// Client 1 connects and continues to main.main
 	dapclient := newDAPRemoteClient(t, listenAddr)
 	dapclient.SetFunctionBreakpointsRequest([]godap.FunctionBreakpoint{{Name: "main.main"}})
@@ -795,7 +800,7 @@ func TestRemoteDAPClientMulti(t *testing.T) {
 	dapclient.ExpectContinueResponse(t)
 	dapclient.ExpectStoppedEvent(t)
 	dapclient.CheckStopLocation(t, 1, "main.main", 5)
-	closeDAPRemoteMultiClient(t, dapclient)
+	closeDAPRemoteMultiClient(t, dapclient, "halted")
 
 	// Client 2 reconnects at main.main and continues to process exit
 	dapclient2 := newDAPRemoteClient(t, listenAddr)
@@ -803,13 +808,13 @@ func TestRemoteDAPClientMulti(t *testing.T) {
 	dapclient2.ContinueRequest(1)
 	dapclient2.ExpectContinueResponse(t)
 	dapclient2.ExpectTerminatedEvent(t)
-	closeDAPRemoteMultiClient(t, dapclient2)
+	closeDAPRemoteMultiClient(t, dapclient2, "exited")
 
 	// 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)
+	closeDAPRemoteMultiClient(t, dapclient3, "exited")
 
 	// But rpc clients can still connect and restart
 	rpcclient := rpc2.NewClient(listenAddr)
@@ -862,7 +867,7 @@ func TestRemoteDAPClientAfterContinue(t *testing.T) {
 	c.ContinueRequest(1)
 	c.ExpectContinueResponse(t)
 	c.DisconnectRequest()
-	c.ExpectOutputEventClosingClient(t)
+	c.ExpectOutputEventClosingClient(t, "running")
 	c.ExpectDisconnectResponse(t)
 	c.ExpectTerminatedEvent(t)
 	c.Close()
diff --git a/pkg/proc/core/core_test.go b/pkg/proc/core/core_test.go
index 4c39dab5..25c08826 100644
--- a/pkg/proc/core/core_test.go
+++ b/pkg/proc/core/core_test.go
@@ -297,7 +297,7 @@ func TestCore(t *testing.T) {
 	if mainFrame == nil {
 		t.Fatalf("Couldn't find main in stack %v", panickingStack)
 	}
-	msg, err := proc.FrameToScope(p, p.Memory(), nil, *mainFrame).EvalVariable("msg", proc.LoadConfig{MaxStringLen: 64})
+	msg, err := proc.FrameToScope(p, p.Memory(), nil, *mainFrame).EvalExpression("msg", proc.LoadConfig{MaxStringLen: 64})
 	if err != nil {
 		t.Fatalf("Couldn't EvalVariable(msg, ...): %v", err)
 	}
@@ -429,11 +429,11 @@ mainSearch:
 
 	scope := proc.FrameToScope(p, p.Memory(), nil, *mainFrame)
 	loadConfig := proc.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1}
-	v1, err := scope.EvalVariable("t", loadConfig)
+	v1, err := scope.EvalExpression("t", loadConfig)
 	assertNoError(err, t, "EvalVariable(t)")
 	assertNoError(v1.Unreadable, t, "unreadable variable 't'")
 	t.Logf("t = %#v\n", v1)
-	v2, err := scope.EvalVariable("s", loadConfig)
+	v2, err := scope.EvalExpression("s", loadConfig)
 	assertNoError(err, t, "EvalVariable(s)")
 	assertNoError(v2.Unreadable, t, "unreadable variable 's'")
 	t.Logf("s = %#v\n", v2)
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 30251769..8f39a82b 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -1164,7 +1164,7 @@ func evalVariableOrError(p *proc.Target, symbol string) (*proc.Variable, error)
 	if err != nil {
 		return nil, err
 	}
-	return scope.EvalVariable(symbol, normalLoadConfig)
+	return scope.EvalExpression(symbol, normalLoadConfig)
 }
 
 func evalVariable(p *proc.Target, t testing.TB, symbol string) *proc.Variable {
@@ -1310,7 +1310,7 @@ func TestFrameEvaluation(t *testing.T) {
 			scope, err := proc.ConvertEvalScope(p, g.ID, frame, 0)
 			assertNoError(err, t, "ConvertEvalScope()")
 			t.Logf("scope = %v", scope)
-			v, err := scope.EvalVariable("i", normalLoadConfig)
+			v, err := scope.EvalExpression("i", normalLoadConfig)
 			t.Logf("v = %v", v)
 			if err != nil {
 				t.Logf("Goroutine %d: %v\n", g.ID, err)
@@ -1338,7 +1338,7 @@ func TestFrameEvaluation(t *testing.T) {
 		for i := 0; i <= 3; i++ {
 			scope, err := proc.ConvertEvalScope(p, g.ID, i+1, 0)
 			assertNoError(err, t, fmt.Sprintf("ConvertEvalScope() on frame %d", i+1))
-			v, err := scope.EvalVariable("n", normalLoadConfig)
+			v, err := scope.EvalExpression("n", normalLoadConfig)
 			assertNoError(err, t, fmt.Sprintf("EvalVariable() on frame %d", i+1))
 			n, _ := constant.Int64Val(v.Value)
 			t.Logf("frame %d n %d\n", i+1, n)
@@ -1367,7 +1367,7 @@ func TestThreadFrameEvaluation(t *testing.T) {
 		// be a thread scope.
 		scope, err := proc.ConvertEvalScope(p, 0, 0, 0)
 		assertNoError(err, t, "ConvertEvalScope() on frame 0")
-		_, err = scope.EvalVariable("s", normalLoadConfig)
+		_, err = scope.EvalExpression("s", normalLoadConfig)
 		assertNoError(err, t, "EvalVariable(\"s\") on frame 0")
 	})
 }
@@ -1520,10 +1520,10 @@ func TestBreakpointCountsWithDetection(t *testing.T) {
 				}
 				scope, err := proc.GoroutineScope(p, th)
 				assertNoError(err, t, "Scope()")
-				v, err := scope.EvalVariable("i", normalLoadConfig)
+				v, err := scope.EvalExpression("i", normalLoadConfig)
 				assertNoError(err, t, "evalVariable")
 				i, _ := constant.Int64Val(v.Value)
-				v, err = scope.EvalVariable("id", normalLoadConfig)
+				v, err = scope.EvalExpression("id", normalLoadConfig)
 				assertNoError(err, t, "evalVariable")
 				id, _ := constant.Int64Val(v.Value)
 				m[id] = i
@@ -4253,11 +4253,11 @@ func TestReadDeferArgs(t *testing.T) {
 				}
 			}
 
-			avar, err := scope.EvalVariable("a", normalLoadConfig)
+			avar, err := scope.EvalExpression("a", normalLoadConfig)
 			if err != nil {
 				t.Fatal(err)
 			}
-			bvar, err := scope.EvalVariable("b", normalLoadConfig)
+			bvar, err := scope.EvalExpression("b", normalLoadConfig)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -5736,3 +5736,42 @@ func TestSetYMMRegister(t *testing.T) {
 		}
 	})
 }
+
+func TestNilPtrDerefInBreakInstr(t *testing.T) {
+	// Checks that having a breakpoint on the exact instruction that causes a
+	// nil pointer dereference does not cause problems.
+
+	var asmfile string
+	switch runtime.GOARCH {
+	case "amd64":
+		asmfile = "main_amd64.s"
+	case "arm64":
+		asmfile = "main_arm64.s"
+	case "386":
+		asmfile = "main_386.s"
+	default:
+		t.Fatalf("assembly file for %s not provided", runtime.GOARCH)
+	}
+
+	withTestProcess("asmnilptr/", t, func(p *proc.Target, fixture protest.Fixture) {
+		f := filepath.Join(fixture.BuildDir, asmfile)
+		f = strings.Replace(f, "\\", "/", -1)
+		setFileBreakpoint(p, t, f, 5)
+		t.Logf("first continue")
+		assertNoError(p.Continue(), t, "Continue()")
+		t.Logf("second continue")
+		err := p.Continue()
+		if runtime.GOOS == "darwin" && err != nil && err.Error() == "bad access" {
+			// this is also ok
+			return
+		}
+		assertNoError(err, t, "Continue()")
+		bp := p.CurrentThread().Breakpoint()
+		if bp != nil {
+			t.Logf("%#v\n", bp.Breakpoint)
+		}
+		if bp == nil || (bp.Name != proc.UnrecoveredPanic) {
+			t.Fatalf("no breakpoint hit or wrong breakpoint hit: %#v", bp)
+		}
+	})
+}
diff --git a/pkg/proc/scope_test.go b/pkg/proc/scope_test.go
index 03a4cf0e..f95a3711 100644
--- a/pkg/proc/scope_test.go
+++ b/pkg/proc/scope_test.go
@@ -285,7 +285,7 @@ func (check *scopeCheck) checkVar(v *proc.Variable, t *testing.T) {
 }
 
 func (varCheck *varCheck) checkInScope(line int, scope *proc.EvalScope, t *testing.T) {
-	v, err := scope.EvalVariable(varCheck.name, normalLoadConfig)
+	v, err := scope.EvalExpression(varCheck.name, normalLoadConfig)
 	assertNoError(err, t, fmt.Sprintf("EvalVariable(%s)", varCheck.name))
 	varCheck.check(line, v, t, "EvalExpression")
 
diff --git a/service/dap/config_test.go b/service/dap/config_test.go
index 2b5eb0fa..225d05dd 100644
--- a/service/dap/config_test.go
+++ b/service/dap/config_test.go
@@ -18,14 +18,14 @@ func TestListConfig(t *testing.T) {
 			args: args{
 				args: &launchAttachArgs{},
 			},
-			want: formatConfig(0, false, false, false, [][2]string{}),
+			want: formatConfig(0, false, false, "", false, [][2]string{}),
 		},
 		{
 			name: "default values",
 			args: args{
 				args: &defaultArgs,
 			},
-			want: formatConfig(50, false, false, false, [][2]string{}),
+			want: formatConfig(50, false, false, "", false, [][2]string{}),
 		},
 		{
 			name: "custom values",
@@ -37,7 +37,7 @@ func TestListConfig(t *testing.T) {
 					substitutePathServerToClient: [][2]string{{"world", "hello"}},
 				},
 			},
-			want: formatConfig(35, true, false, false, [][2]string{{"hello", "world"}}),
+			want: formatConfig(35, true, false, "", false, [][2]string{{"hello", "world"}}),
 		},
 	}
 	for _, tt := range tests {
diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go
index 0dfb1e23..42a623ca 100644
--- a/service/dap/daptest/client.go
+++ b/service/dap/daptest/client.go
@@ -153,6 +153,11 @@ func (c *Client) ExpectOutputEventProcessExited(t *testing.T, status int) *dap.O
 	return c.ExpectOutputEventRegex(t, fmt.Sprintf(ProcessExited, fmt.Sprintf("%d", status)))
 }
 
+func (c *Client) ExpectOutputEventProcessExitedAnyStatus(t *testing.T) *dap.OutputEvent {
+	t.Helper()
+	return c.ExpectOutputEventRegex(t, fmt.Sprintf(ProcessExited, `-?\d+`))
+}
+
 func (c *Client) ExpectOutputEventDetaching(t *testing.T) *dap.OutputEvent {
 	t.Helper()
 	return c.ExpectOutputEventRegex(t, `Detaching\n`)
@@ -173,9 +178,11 @@ 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 {
+const ClosingClient = "Closing client session, but leaving multi-client DAP server at .+:[0-9]+ with debuggee %s\n"
+
+func (c *Client) ExpectOutputEventClosingClient(t *testing.T, status string) *dap.OutputEvent {
 	t.Helper()
-	return c.ExpectOutputEventRegex(t, `Closing client session, but leaving multi-client DAP server running at .+:[0-9]+\n`)
+	return c.ExpectOutputEventRegex(t, fmt.Sprintf(ClosingClient, status))
 }
 
 func (c *Client) CheckStopLocation(t *testing.T, thread int, name string, line int) {
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index cbd75b3a..09a59154 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -805,6 +805,113 @@ func checkStackFramesExact(t *testing.T, got *dap.StackTraceResponse,
 	checkStackFramesNamed("", t, got, wantStartName, wantStartLine, wantStartID, wantFrames, wantTotalFrames, true)
 }
 
+func TestFilterGoroutines(t *testing.T) {
+	tt := []struct {
+		name    string
+		filter  string
+		want    []string
+		wantLen int
+		wantErr bool
+	}{
+		{
+			name:    "user goroutines",
+			filter:  "-with user",
+			want:    []string{"main.main", "main.agoroutine"},
+			wantLen: 11,
+		},
+		{
+			name:    "filter by user loc",
+			filter:  "-with userloc main.main",
+			want:    []string{"main.main"},
+			wantLen: 1,
+		},
+		{
+			name:    "multiple filters",
+			filter:  "-with user -with userloc main.agoroutine",
+			want:    []string{"main.agoroutine"},
+			wantLen: 10,
+		},
+		{
+			name:   "system goroutines",
+			filter: "-without user",
+			want:   []string{"runtime."},
+		},
+		// Filters that should return all goroutines.
+		{
+			name:    "empty filter string",
+			filter:  "",
+			want:    []string{"main.main", "main.agoroutine", "runtime."},
+			wantLen: -1,
+		},
+		{
+			name:    "bad filter string",
+			filter:  "not parsable to filters",
+			want:    []string{"main.main", "main.agoroutine", "runtime."},
+			wantLen: -1,
+			wantErr: true,
+		},
+		// Filters that should produce none.
+		{
+			name:    "no match to user loc",
+			filter:  "-with userloc main.NotAUserFrame",
+			want:    []string{"Dummy"},
+			wantLen: 1,
+		},
+		{
+			name:    "no match to user and not user",
+			filter:  "-with user -without user",
+			want:    []string{"Dummy"},
+			wantLen: 1,
+		},
+	}
+	runTest(t, "goroutinestackprog", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequestWithArgs(map[string]interface{}{
+					"mode":        "exec",
+					"program":     fixture.Path,
+					"stopOnEntry": !stopOnEntry})
+			},
+			// Set breakpoints
+			fixture.Source, []int{30},
+			[]onBreakpoint{{
+				// Stop at line 30
+				execute: func() {
+					for _, tc := range tt {
+						command := fmt.Sprintf("dlv config goroutineFilters %s", tc.filter)
+						client.EvaluateRequest(command, 1000, "repl")
+						client.ExpectInvalidatedEvent(t)
+						client.ExpectEvaluateResponse(t)
+
+						client.ThreadsRequest()
+						if tc.wantErr {
+							client.ExpectOutputEvent(t)
+						}
+						tr := client.ExpectThreadsResponse(t)
+						if tc.wantLen > 0 && len(tr.Body.Threads) != tc.wantLen {
+							t.Errorf("got Threads=%#v, want Len=%d\n", tr.Body.Threads, tc.wantLen)
+						}
+						for i, frame := range tr.Body.Threads {
+							var found bool
+							for _, wantName := range tc.want {
+								if strings.Contains(frame.Name, wantName) {
+									found = true
+									break
+								}
+							}
+							if !found {
+								t.Errorf("got Threads[%d]=%#v, want Name=%v\n", i, frame, tc.want)
+							}
+						}
+					}
+				},
+				disconnect: false,
+			}})
+
+	})
+}
+
 func checkStackFramesHasMore(t *testing.T, got *dap.StackTraceResponse,
 	wantStartName string, wantStartLine, wantStartID, wantFrames, wantTotalFrames int) {
 	t.Helper()
@@ -3822,14 +3929,15 @@ func TestEvaluateRequest(t *testing.T) {
 	})
 }
 
-func formatConfig(depth int, showGlobals, showRegisters, hideSystemGoroutines bool, substitutePath [][2]string) string {
+func formatConfig(depth int, showGlobals, showRegisters bool, goroutineFilters string, hideSystemGoroutines bool, substitutePath [][2]string) string {
 	formatStr := `stackTraceDepth	%d
 showGlobalVariables	%v
 showRegisters	%v
+goroutineFilters	%q
 hideSystemGoroutines	%v
 substitutePath	%v
 `
-	return fmt.Sprintf(formatStr, depth, showGlobals, showRegisters, hideSystemGoroutines, substitutePath)
+	return fmt.Sprintf(formatStr, depth, showGlobals, showRegisters, goroutineFilters, hideSystemGoroutines, substitutePath)
 }
 
 func TestEvaluateCommandRequest(t *testing.T) {
@@ -3846,10 +3954,11 @@ func TestEvaluateCommandRequest(t *testing.T) {
 
 					// Request help.
 					const dlvHelp = `The following commands are available:
-    help (alias: h) 	 Prints the help message.
-    config 	 Changes configuration parameters.
+    dlv help (alias: h) 	 Prints the help message.
+    dlv config 	 Changes configuration parameters.
+    dlv sources (alias: s) 	 Print list of source files.
 
-Type help followed by a command for full documentation.
+Type 'dlv help' followed by a command for full documentation.
 `
 					client.EvaluateRequest("dlv help", 1000, "repl")
 					got := client.ExpectEvaluateResponse(t)
@@ -3862,10 +3971,10 @@ Type help followed by a command for full documentation.
 					// Test config.
 					client.EvaluateRequest("dlv config -list", 1000, "repl")
 					got = client.ExpectEvaluateResponse(t)
-					checkEval(t, got, formatConfig(50, false, false, false, [][2]string{}), noChildren)
+					checkEval(t, got, formatConfig(50, false, false, "", false, [][2]string{}), noChildren)
 
 					// Read and modify showGlobalVariables.
-					client.EvaluateRequest("dlv config showGlobalVariables", 1000, "repl")
+					client.EvaluateRequest("dlv config -list showGlobalVariables", 1000, "repl")
 					got = client.ExpectEvaluateResponse(t)
 					checkEval(t, got, "showGlobalVariables\tfalse\n", noChildren)
 
@@ -3877,12 +3986,13 @@ Type help followed by a command for full documentation.
 					checkScope(t, scopes, 0, "Locals", -1)
 
 					client.EvaluateRequest("dlv config showGlobalVariables true", 1000, "repl")
+					client.ExpectInvalidatedEvent(t)
 					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)
+					checkEval(t, got, formatConfig(50, true, false, "", false, [][2]string{}), noChildren)
 
 					client.ScopesRequest(1000)
 					scopes = client.ExpectScopesResponse(t)
@@ -3893,7 +4003,7 @@ Type help followed by a command for full documentation.
 					checkScope(t, scopes, 1, "Globals (package main)", -1)
 
 					// Read and modify substitutePath.
-					client.EvaluateRequest("dlv config substitutePath", 1000, "repl")
+					client.EvaluateRequest("dlv config -list substitutePath", 1000, "repl")
 					got = client.ExpectEvaluateResponse(t)
 					checkEval(t, got, "substitutePath\t[]\n", noChildren)
 
@@ -3909,6 +4019,25 @@ Type help followed by a command for full documentation.
 					got = client.ExpectEvaluateResponse(t)
 					checkEval(t, got, "substitutePath\t[]\n\nUpdated", noChildren)
 
+					// Test sources.
+					client.EvaluateRequest("dlv sources", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					if !strings.Contains(got.Body.Result, fixture.Source) {
+						t.Errorf("\ngot: %#v, want sources contains %s", got, fixture.Source)
+					}
+
+					client.EvaluateRequest(fmt.Sprintf("dlv sources .*%s", strings.ReplaceAll(filepath.Base(fixture.Source), ".", "\\.")), 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					if got.Body.Result != fixture.Source {
+						t.Errorf("\ngot: %#v, want sources=%q", got, fixture.Source)
+					}
+
+					client.EvaluateRequest("dlv sources nonexistentsource", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					if got.Body.Result != "" {
+						t.Errorf("\ngot: %#v, want sources=\"\"", got)
+					}
+
 					// Test bad inputs.
 					client.EvaluateRequest("dlv help bad", 1000, "repl")
 					client.ExpectErrorResponse(t)
@@ -4406,6 +4535,8 @@ func getPC(t *testing.T, client *daptest.Client, threadId int) (uint64, error) {
 	return strconv.ParseUint(st.Body.StackFrames[0].InstructionPointerReference, 0, 64)
 }
 
+// TestNextParked tests that we can switched selected goroutine to a parked one
+// and perform next operation on it.
 func TestNextParked(t *testing.T) {
 	if runtime.GOOS == "freebsd" {
 		t.SkipNow()
@@ -4420,40 +4551,48 @@ func TestNextParked(t *testing.T) {
 			fixture.Source, []int{15},
 			[]onBreakpoint{{ // Stop at line 15
 				execute: func() {
-					if goroutineId := testStepParkedHelper(t, client, fixture); goroutineId >= 0 {
-
-						client.NextRequest(goroutineId)
+					if parkedGoid := testNextParkedHelper(t, client, fixture); parkedGoid >= 0 {
+						client.NextRequest(parkedGoid)
 						client.ExpectNextResponse(t)
 
 						se := client.ExpectStoppedEvent(t)
-						if se.Body.ThreadId != goroutineId {
-							t.Fatalf("Next did not continue on the selected goroutine, expected %d got %d", goroutineId, se.Body.ThreadId)
+						if se.Body.ThreadId != parkedGoid {
+							t.Fatalf("Next did not continue on the newly selected goroutine, expected %d got %d", parkedGoid, se.Body.ThreadId)
 						}
 					}
 				},
+				// Let the test harness continue to process termination
+				// if it hasn't gotten there already.
 				disconnect: false,
 			}})
 	})
 }
 
-func testStepParkedHelper(t *testing.T, client *daptest.Client, fixture protest.Fixture) int {
+// Finds a goroutine other than the selected one that is parked inside of main.sayhi and therefore
+// still has a line to execute if resumed with next.
+func testNextParkedHelper(t *testing.T, client *daptest.Client, fixture protest.Fixture) int {
 	t.Helper()
-	// Set a breakpoint at main.sayHi
+	// Set a breakpoint at main.sayhi
 	client.SetBreakpointsRequest(fixture.Source, []int{8})
 	client.ExpectSetBreakpointsResponse(t)
 
-	var goroutineId = -1
-	for goroutineId < 0 {
+	var parkedGoid = -1
+	for parkedGoid < 0 {
 		client.ContinueRequest(1)
-		contResp := client.ExpectMessage(t)
-		switch contResp.(type) {
-		case *dap.ContinueResponse:
+		client.ExpectContinueResponse(t)
+		event := client.ExpectMessage(t)
+		switch event.(type) {
+		case *dap.StoppedEvent:
 			// ok
 		case *dap.TerminatedEvent:
+			// This is very unlikely to happen. But in theory if all sayhi
+			// gouritines are run serially, there will never be a second parked
+			// sayhi goroutine when another breaks and we will keep trying
+			// until process termination.
 			return -1
 		}
 
-		se := client.ExpectStoppedEvent(t)
+		se := event.(*dap.StoppedEvent)
 
 		client.ThreadsRequest()
 		threads := client.ExpectThreadsResponse(t)
@@ -4464,9 +4603,7 @@ func testStepParkedHelper(t *testing.T, client *daptest.Client, fixture protest.
 		// 2. hasn't called wg.Done yet
 		// 3. is not the currently selected goroutine
 		for _, g := range threads.Body.Threads {
-			// We do not need to check the thread that the program
-			// is currently stopped on.
-			if g.Id == se.Body.ThreadId {
+			if g.Id == se.Body.ThreadId { // Skip selected goroutine
 				continue
 			}
 			client.StackTraceRequest(g.Id, 0, 5)
@@ -4474,11 +4611,11 @@ func testStepParkedHelper(t *testing.T, client *daptest.Client, fixture protest.
 			for _, frame := range frames.Body.StackFrames {
 				// line 11 is the line where wg.Done is called
 				if frame.Name == "main.sayhi" && frame.Line < 11 {
-					goroutineId = g.Id
+					parkedGoid = g.Id
 					break
 				}
 			}
-			if goroutineId >= 0 {
+			if parkedGoid >= 0 {
 				break
 			}
 		}
@@ -4487,8 +4624,7 @@ func testStepParkedHelper(t *testing.T, client *daptest.Client, fixture protest.
 	// Clear all breakpoints.
 	client.SetBreakpointsRequest(fixture.Source, []int{})
 	client.ExpectSetBreakpointsResponse(t)
-
-	return goroutineId
+	return parkedGoid
 }
 
 // TestStepOutPreservesGoroutine is inspired by proc_test.TestStepOutPreservesGoroutine
@@ -4759,8 +4895,8 @@ func TestPanicBreakpointOnContinue(t *testing.T) {
 					st := client.ExpectStackTraceResponse(t)
 					for i, frame := range st.Body.StackFrames {
 						if strings.HasPrefix(frame.Name, "runtime.") {
-							if frame.Source.PresentationHint != "deemphasize" {
-								t.Errorf("\ngot Body.StackFrames[%d]=%#v\nwant Source.PresentationHint=\"deemphasize\"", i, frame)
+							if frame.PresentationHint != "subtle" {
+								t.Errorf("\ngot Body.StackFrames[%d]=%#v\nwant Source.PresentationHint=\"subtle\"", i, frame)
 							}
 						} else if frame.Source.PresentationHint != "" {
 							t.Errorf("\ngot Body.StackFrames[%d]=%#v\nwant Source.PresentationHint=\"\"", i, frame)
@@ -4969,7 +5105,7 @@ func runDebugSessionWithBPs(t *testing.T, client *daptest.Client, cmd string, cm
 		if onBP.disconnect {
 			client.DisconnectRequestWithKillOption(true)
 			if onBP.terminated {
-				client.ExpectOutputEventProcessExited(t, 0)
+				client.ExpectOutputEventProcessExitedAnyStatus(t)
 				client.ExpectOutputEventDetaching(t)
 			} else {
 				client.ExpectOutputEventDetachingKill(t)
@@ -4988,7 +5124,7 @@ func runDebugSessionWithBPs(t *testing.T, client *daptest.Client, cmd string, cm
 	}
 	client.DisconnectRequestWithKillOption(true)
 	if cmd == "launch" {
-		client.ExpectOutputEventProcessExited(t, 0)
+		client.ExpectOutputEventProcessExitedAnyStatus(t)
 		client.ExpectOutputEventDetaching(t)
 	} else if cmd == "attach" {
 		client.ExpectOutputEventDetachingKill(t)
@@ -6331,10 +6467,10 @@ func TestAttachRemoteToRunningTargetContinueOnEntry(t *testing.T) {
 	})
 }
 
-// TestMultiClient tests that that remote attach doesn't take down
+// TestAttachRemoteMultiClientDisconnect 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) {
-	closingClientSessionOnly := "Closing client session, but leaving multi-client DAP server running at"
+func TestAttachRemoteMultiClientDisconnect(t *testing.T) {
+	closingClientSessionOnly := fmt.Sprintf(daptest.ClosingClient, "halted")
 	detachingAndTerminating := "Detaching and terminating target process"
 	tests := []struct {
 		name              string
@@ -6414,18 +6550,18 @@ func TestLaunchAttachErrorWhenDebugInProgress(t *testing.T) {
 				// Both launch and attach requests should go through for additional error checking
 				client.AttachRequest(map[string]interface{}{"mode": "local", "processId": 100})
 				er := client.ExpectVisibleErrorResponse(t)
-				msg := "Failed to attach: debugger already started - use remote mode to connect"
-				if er.Body.Error.Id != FailedToAttach || er.Body.Error.Format != msg {
-					t.Errorf("got %#v, want Id=%d Format=%q", er, FailedToAttach, msg)
+				msgRe, _ := regexp.Compile("Failed to attach: debug session already in progress at [0-9]+:[0-9]+ - use remote mode to connect to a server with an active debug session")
+				if er.Body.Error.Id != FailedToAttach || msgRe.MatchString(er.Body.Error.Format) {
+					t.Errorf("got %#v, want Id=%d Format=%q", er, FailedToAttach, msgRe)
 				}
 				tests := []string{"debug", "test", "exec", "replay", "core"}
 				for _, mode := range tests {
 					t.Run(mode, func(t *testing.T) {
 						client.LaunchRequestWithArgs(map[string]interface{}{"mode": mode})
 						er := client.ExpectVisibleErrorResponse(t)
-						msg := "Failed to launch: debugger already started - use remote attach to connect to a server with an active debug session"
-						if er.Body.Error.Id != FailedToLaunch || er.Body.Error.Format != msg {
-							t.Errorf("got %#v, want Id=%d Format=%q", er, FailedToLaunch, msg)
+						msgRe, _ := regexp.Compile("Failed to launch: debug session already in progress at [0-9]+:[0-9]+ - use remote attach mode to connect to a server with an active debug session")
+						if er.Body.Error.Id != FailedToLaunch || msgRe.MatchString(er.Body.Error.Format) {
+							t.Errorf("got %#v, want Id=%d Format=%q", er, FailedToLaunch, msgRe)
 						}
 					})
 				}
diff --git a/service/test/variables_test.go b/service/test/variables_test.go
index 0b67aa6c..caffbf6e 100644
--- a/service/test/variables_test.go
+++ b/service/test/variables_test.go
@@ -101,7 +101,7 @@ func evalVariable(p *proc.Target, symbol string, cfg proc.LoadConfig) (*proc.Var
 		return nil, err
 	}
 
-	return scope.EvalVariable(symbol, cfg)
+	return scope.EvalExpression(symbol, cfg)
 }
 
 func (tc *varTest) alternateVarTest() varTest {