Repository: go-delve/delve
Pull request #1901 (2020-02-28 18:00:13)

service/dap: Add support for debug and test modes
This change also:
-- refactors building helper functions from commands.go into its own gobuild package.
-- makes processing untyped launch args (that are implementation-specific and not part of the DAP spec) more robust, verified by additional tests


Patch:
diff --git a/.travis.yml b/.travis.yml
index 1c1884ef..8cfabac3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,9 +11,9 @@ arch:
 
 go:
   - tip
+  - 1.14.x
   - 1.13.x
   - 1.12.x
-  - 1.11.x
 
 matrix:
   allow_failures:
diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go
index 39726f39..bc6a8d9f 100644
--- a/cmd/dlv/cmds/commands.go
+++ b/cmd/dlv/cmds/commands.go
@@ -13,6 +13,7 @@ import (
 	"syscall"
 
 	"github.com/go-delve/delve/pkg/config"
+	"github.com/go-delve/delve/pkg/gobuild"
 	"github.com/go-delve/delve/pkg/goversion"
 	"github.com/go-delve/delve/pkg/logflags"
 	"github.com/go-delve/delve/pkg/terminal"
@@ -354,14 +355,6 @@ and dap modes.
 	return RootCommand
 }
 
-// Remove the file at path and issue a warning to stderr if this fails.
-func remove(path string) {
-	err := os.Remove(path)
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "could not remove %v: %v\n", path, err)
-	}
-}
-
 func dapCmd(cmd *cobra.Command, args []string) {
 	status := func() int {
 		if err := logflags.Setup(Log, LogOutput, LogDest); err != nil {
@@ -428,12 +421,12 @@ func debugCmd(cmd *cobra.Command, args []string) {
 		}
 
 		dlvArgs, targetArgs := splitArgs(cmd, args)
-		err = gobuild(debugname, dlvArgs)
+		err = gobuild.GoBuild(debugname, dlvArgs, BuildFlags)
 		if err != nil {
 			fmt.Fprintf(os.Stderr, "%v\n", err)
 			return 1
 		}
-		defer remove(debugname)
+		defer gobuild.Remove(debugname)
 		processArgs := append([]string{debugname}, targetArgs...)
 		return execute(0, processArgs, conf, "", executingGeneratedFile)
 	}()
@@ -484,17 +477,17 @@ func traceCmd(cmd *cobra.Command, args []string) {
 					return 1
 				}
 				if traceTestBinary {
-					if err := gotestbuild(debugname, dlvArgs); err != nil {
+					if err := gobuild.GoTestBuild(debugname, dlvArgs, BuildFlags); err != nil {
 						fmt.Fprintf(os.Stderr, "%v\n", err)
 						return 1
 					}
 				} else {
-					if err := gobuild(debugname, dlvArgs); err != nil {
+					if err := gobuild.GoBuild(debugname, dlvArgs, BuildFlags); err != nil {
 						fmt.Fprintf(os.Stderr, "%v\n", err)
 						return 1
 					}
 				}
-				defer remove(debugname)
+				defer gobuild.Remove(debugname)
 			}
 
 			processArgs = append([]string{debugname}, targetArgs...)
@@ -576,11 +569,11 @@ func testCmd(cmd *cobra.Command, args []string) {
 		}
 
 		dlvArgs, targetArgs := splitArgs(cmd, args)
-		err = gotestbuild(debugname, dlvArgs)
+		err = gobuild.GoTestBuild(debugname, dlvArgs, BuildFlags)
 		if err != nil {
 			return 1
 		}
-		defer remove(debugname)
+		defer gobuild.Remove(debugname)
 		processArgs := append([]string{debugname}, targetArgs...)
 
 		return execute(0, processArgs, conf, "", executingGeneratedTest)
@@ -795,48 +788,3 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile
 
 	return connect(listener.Addr().String(), clientConn, conf, kind)
 }
-
-func optflags(args []string) []string {
-	// after go1.9 building with -gcflags='-N -l' and -a simultaneously works.
-	// after go1.10 specifying -a is unnecessary because of the new caching strategy, but we should pass -gcflags=all=-N -l to have it applied to all packages
-	// see https://github.com/golang/go/commit/5993251c015dfa1e905bdf44bdb41572387edf90
-
-	ver, _ := goversion.Installed()
-	switch {
-	case ver.Major < 0 || ver.AfterOrEqual(goversion.GoVersion{1, 10, -1, 0, 0, ""}):
-		args = append(args, "-gcflags", "all=-N -l")
-	case ver.AfterOrEqual(goversion.GoVersion{1, 9, -1, 0, 0, ""}):
-		args = append(args, "-gcflags", "-N -l", "-a")
-	default:
-		args = append(args, "-gcflags", "-N -l")
-	}
-	return args
-}
-
-func gobuild(debugname string, pkgs []string) error {
-	args := []string{"-o", debugname}
-	args = optflags(args)
-	if BuildFlags != "" {
-		args = append(args, config.SplitQuotedFields(BuildFlags, '\'')...)
-	}
-	args = append(args, pkgs...)
-	return gocommand("build", args...)
-}
-
-func gotestbuild(debugname string, pkgs []string) error {
-	args := []string{"-c", "-o", debugname}
-	args = optflags(args)
-	if BuildFlags != "" {
-		args = append(args, config.SplitQuotedFields(BuildFlags, '\'')...)
-	}
-	args = append(args, pkgs...)
-	return gocommand("test", args...)
-}
-
-func gocommand(command string, args ...string) error {
-	allargs := []string{command}
-	allargs = append(allargs, args...)
-	goBuild := exec.Command("go", allargs...)
-	goBuild.Stderr = os.Stderr
-	return goBuild.Run()
-}
diff --git a/go.mod b/go.mod
index bc38024c..1e8632c9 100644
--- a/go.mod
+++ b/go.mod
@@ -4,17 +4,12 @@ go 1.11
 
 require (
 	github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5
-	github.com/cpuguy83/go-md2man v1.0.8 // indirect
 	github.com/google/go-dap v0.2.0
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
-	github.com/kr/pretty v0.1.0 // indirect
 	github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561
 	github.com/mattn/go-isatty v0.0.3
-	github.com/onsi/ginkgo v1.8.0 // indirect
-	github.com/onsi/gomega v1.5.0 // indirect
 	github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b
 	github.com/pkg/profile v0.0.0-20170413231811-06b906832ed0
-	github.com/russross/blackfriday v0.0.0-20180428102519-11635eb403ff // indirect
 	github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973
 	github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372
 	github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1 // indirect
@@ -23,8 +18,5 @@ require (
 	golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4
 	golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb
 	golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f
-	gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
-	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
-	gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
 	gopkg.in/yaml.v2 v2.2.1
 )
diff --git a/go.sum b/go.sum
index a2317c59..8036c2fc 100644
--- a/go.sum
+++ b/go.sum
@@ -1,15 +1,11 @@
 github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5 h1:rIXlvz2IWiupMFlC45cZCXZFvKX/ExBcSLrDy2G0Lp8=
 github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
-github.com/cpuguy83/go-md2man v1.0.8 h1:DwoNytLphI8hzS2Af4D0dfaEaiSq2bN05mEm4R6vf8M=
-github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/google/go-dap v0.1.0 h1:UzVzngq6yBR+bJVnTM8mjcdKrQnYR7m7U7JpNSnEo48=
-github.com/google/go-dap v0.1.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
 github.com/google/go-dap v0.2.0 h1:whjIGQRumwbR40qRU7CEKuFLmePUUc2s4Nt9DoXXxWk=
 github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
@@ -36,8 +32,6 @@ github.com/pkg/profile v0.0.0-20170413231811-06b906832ed0 h1:wBza4Dlm/NCQF572oSG
 github.com/pkg/profile v0.0.0-20170413231811-06b906832ed0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/russross/blackfriday v0.0.0-20180428102519-11635eb403ff h1:g9ZlAHmkc/h5So+OjNCkZWh+FjuKEOOOoyRkqlGA8+c=
-github.com/russross/blackfriday v0.0.0-20180428102519-11635eb403ff/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973 h1:3AJZYTzw3gm3TNTt30x0CCKD7GOn2sdd50Hn35fQkGY=
 github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
 github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372 h1:eRfW1vRS4th8IX2iQeyqQ8cOUNOySvAYJ0IUvTXGoYA=
@@ -51,15 +45,15 @@ go.starlark.net v0.0.0-20190702223751-32f345186213 h1:lkYv5AKwvvduv5XWP6szk/bvvg
 go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
 golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4 h1:QlVATYS7JBoZMVaf+cNjb90WD/beKVHnIxFKT4QaHVI=
 golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
-golang.org/x/crypto v0.0.0-20180614174826-fd5f17ee7299 h1:zxP+xTjjk4kD+M5IFPweL7/4851FUhYkzbDqbzkN1JE=
-golang.org/x/crypto v0.0.0-20180614174826-fd5f17ee7299/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
diff --git a/pkg/gobuild/gobuild.go b/pkg/gobuild/gobuild.go
new file mode 100644
index 00000000..ed59d1ac
--- /dev/null
+++ b/pkg/gobuild/gobuild.go
@@ -0,0 +1,72 @@
+// Package gobuild provides utilities for building programs and tests
+// for the debugging session.
+package gobuild
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+
+	"github.com/go-delve/delve/pkg/config"
+	"github.com/go-delve/delve/pkg/goversion"
+)
+
+// Remove the file at path and issue a warning to stderr if this fails.
+// This can be used to remove the temporary binary generated for the session.
+func Remove(path string) {
+	err := os.Remove(path)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "could not remove %v: %v\n", path, err)
+	}
+}
+
+// optflags generates default build flags to turn off optimization and inlining.
+func optflags(args []string) []string {
+	// after go1.9 building with -gcflags='-N -l' and -a simultaneously works.
+	// after go1.10 specifying -a is unnecessary because of the new caching strategy,
+	// but we should pass -gcflags=all=-N -l to have it applied to all packages
+	// see https://github.com/golang/go/commit/5993251c015dfa1e905bdf44bdb41572387edf90
+
+	ver, _ := goversion.Installed()
+	switch {
+	case ver.Major < 0 || ver.AfterOrEqual(goversion.GoVersion{1, 10, -1, 0, 0, ""}):
+		args = append(args, "-gcflags", "all=-N -l")
+	case ver.AfterOrEqual(goversion.GoVersion{1, 9, -1, 0, 0, ""}):
+		args = append(args, "-gcflags", "-N -l", "-a")
+	default:
+		args = append(args, "-gcflags", "-N -l")
+	}
+	return args
+}
+
+// GoBuild builds non-test files in 'pkgs' with the specified 'buildflags'
+// and writes the output at 'debugname'.
+func GoBuild(debugname string, pkgs []string, buildflags string) error {
+	args := []string{"-o", debugname}
+	args = optflags(args)
+	if buildflags != "" {
+		args = append(args, config.SplitQuotedFields(buildflags, '\'')...)
+	}
+	args = append(args, pkgs...)
+	return gocommand("build", args...)
+}
+
+// GoBuild builds test files 'pkgs' with the specified 'buildflags'
+// and writes the output at 'debugname'.
+func GoTestBuild(debugname string, pkgs []string, buildflags string) error {
+	args := []string{"-c", "-o", debugname}
+	args = optflags(args)
+	if buildflags != "" {
+		args = append(args, config.SplitQuotedFields(buildflags, '\'')...)
+	}
+	args = append(args, pkgs...)
+	return gocommand("test", args...)
+}
+
+func gocommand(command string, args ...string) error {
+	allargs := []string{command}
+	allargs = append(allargs, args...)
+	goBuild := exec.Command("go", allargs...)
+	goBuild.Stderr = os.Stderr
+	return goBuild.Run()
+}
diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go
index b4862f14..b0add2d8 100644
--- a/pkg/proc/gdbserial/gdbserver.go
+++ b/pkg/proc/gdbserial/gdbserver.go
@@ -390,6 +390,10 @@ func LLDBLaunch(cmd []string, wd string, foreground bool, debugInfoDirs []string
 
 	process.SysProcAttr = sysProcAttr(foreground)
 
+	if runtime.GOOS == "darwin" {
+		process.Env = proc.DisableAsyncPreemptEnv()
+	}
+
 	err := process.Start()
 	if err != nil {
 		return nil, err
@@ -406,7 +410,7 @@ func LLDBLaunch(cmd []string, wd string, foreground bool, debugInfoDirs []string
 	if err != nil {
 		return nil, err
 	}
-	return proc.NewTarget(p, false), nil
+	return proc.NewTarget(p, runtime.GOOS == "darwin"), nil
 }
 
 // LLDBAttach starts an instance of lldb-server and connects to it, asking
@@ -655,7 +659,6 @@ continueLoop:
 		var err error
 		var sig uint8
 		tu.Reset()
-		//TODO: pass thread list to resume, which should use it to pass the correct signals
 		threadID, sig, err = p.conn.resume(p.threads, &tu)
 		if err != nil {
 			if _, exited := err.(proc.ErrProcessExited); exited {
diff --git a/pkg/proc/native/proc_windows.go b/pkg/proc/native/proc_windows.go
index 6c36443a..63a2b969 100644
--- a/pkg/proc/native/proc_windows.go
+++ b/pkg/proc/native/proc_windows.go
@@ -7,7 +7,6 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
-	"strings"
 	"syscall"
 	"unsafe"
 
@@ -56,14 +55,7 @@ func Launch(cmd []string, wd string, foreground bool, _ []string) (*proc.Target,
 	}
 	closer.Close()
 
-	env := os.Environ()
-	for i := range env {
-		if strings.HasPrefix(env[i], "GODEBUG=") {
-			// Go 1.14 asynchronous preemption mechanism is incompatible with
-			// debuggers, see: https://github.com/golang/go/issues/36494
-			env[i] += ",asyncpreemptoff=1"
-		}
-	}
+	env := proc.DisableAsyncPreemptEnv()
 
 	var p *os.Process
 	dbp := New(0)
diff --git a/pkg/proc/native/ptrace_linux.go b/pkg/proc/native/ptrace_linux.go
index ab4f5372..849516d8 100644
--- a/pkg/proc/native/ptrace_linux.go
+++ b/pkg/proc/native/ptrace_linux.go
@@ -49,3 +49,31 @@ func PtracePeekUser(tid int, off uintptr) (uintptr, error) {
 	}
 	return val, nil
 }
+
+// ProcessVmRead calls process_vm_readv
+func ProcessVmRead(tid int, addr uintptr, data []byte) (int, error) {
+	len_iov := uint64(len(data))
+	local_iov := sys.Iovec{Base: &data[0], Len: len_iov}
+	remote_iov := sys.Iovec{Base: (*byte)(unsafe.Pointer(addr)), Len: len_iov}
+	p_local := uintptr(unsafe.Pointer(&local_iov))
+	p_remote := uintptr(unsafe.Pointer(&remote_iov))
+	n, _, err := syscall.Syscall6(sys.SYS_PROCESS_VM_READV, uintptr(tid), p_local, 1, p_remote, 1, 0)
+	if err != syscall.Errno(0) {
+		return 0, err
+	}
+	return int(n), nil
+}
+
+// ProcessVmWrite calls process_vm_writev
+func ProcessVmWrite(tid int, addr uintptr, data []byte) (int, error) {
+	len_iov := uint64(len(data))
+	local_iov := sys.Iovec{Base: &data[0], Len: len_iov}
+	remote_iov := sys.Iovec{Base: (*byte)(unsafe.Pointer(addr)), Len: len_iov}
+	p_local := uintptr(unsafe.Pointer(&local_iov))
+	p_remote := uintptr(unsafe.Pointer(&remote_iov))
+	n, _, err := syscall.Syscall6(sys.SYS_PROCESS_VM_WRITEV, uintptr(tid), p_local, 1, p_remote, 1, 0)
+	if err != syscall.Errno(0) {
+		return 0, err
+	}
+	return int(n), nil
+}
diff --git a/pkg/proc/native/threads_linux.go b/pkg/proc/native/threads_linux.go
index c27971ea..5366c7d6 100644
--- a/pkg/proc/native/threads_linux.go
+++ b/pkg/proc/native/threads_linux.go
@@ -90,7 +90,14 @@ func (t *Thread) WriteMemory(addr uintptr, data []byte) (written int, err error)
 	if len(data) == 0 {
 		return
 	}
-	t.dbp.execPtraceFunc(func() { written, err = sys.PtracePokeData(t.ID, addr, data) })
+	// ProcessVmWrite can't poke read-only memory like ptrace, so don't
+	// even bother for small writes -- likely breakpoints and such.
+	if len(data) > sys.SizeofPtr {
+		written, _ = ProcessVmWrite(t.ID, addr, data)
+	}
+	if written == 0 {
+		t.dbp.execPtraceFunc(func() { written, err = sys.PtracePokeData(t.ID, addr, data) })
+	}
 	return
 }
 
@@ -101,9 +108,9 @@ func (t *Thread) ReadMemory(data []byte, addr uintptr) (n int, err error) {
 	if len(data) == 0 {
 		return
 	}
-	t.dbp.execPtraceFunc(func() { _, err = sys.PtracePeekData(t.ID, addr, data) })
-	if err == nil {
-		n = len(data)
+	n, _ = ProcessVmRead(t.ID, addr, data)
+	if n == 0 {
+		t.dbp.execPtraceFunc(func() { n, err = sys.PtracePeekData(t.ID, addr, data) })
 	}
 	return
 }
diff --git a/pkg/proc/proc.go b/pkg/proc/proc.go
index bcabab84..5121872c 100644
--- a/pkg/proc/proc.go
+++ b/pkg/proc/proc.go
@@ -7,8 +7,10 @@ import (
 	"go/ast"
 	"go/constant"
 	"go/token"
+	"os"
 	"path/filepath"
 	"strconv"
+	"strings"
 
 	"github.com/go-delve/delve/pkg/goversion"
 )
@@ -845,3 +847,17 @@ func setAsyncPreemptOff(p *Target, v int64) {
 	err = scope.setValue(asyncpreemptoffv, newConstant(constant.MakeInt64(v), scope.Mem), "")
 	logger.Warnf("could not set asyncpreemptoff %v", err)
 }
+
+// DisableAsyncPreemptEnv returns a process environment (like os.Environ)
+// where asyncpreemptoff is set to 1.
+func DisableAsyncPreemptEnv() []string {
+	env := os.Environ()
+	for i := range env {
+		if strings.HasPrefix(env[i], "GODEBUG=") {
+			// Go 1.14 asynchronous preemption mechanism is incompatible with
+			// debuggers, see: https://github.com/golang/go/issues/36494
+			env[i] += ",asyncpreemptoff=1"
+		}
+	}
+	return env
+}
diff --git a/service/api/prettyprint.go b/service/api/prettyprint.go
index 99e6381a..84b8e595 100644
--- a/service/api/prettyprint.go
+++ b/service/api/prettyprint.go
@@ -7,6 +7,7 @@ import (
 	"reflect"
 	"strconv"
 	"strings"
+	"text/tabwriter"
 )
 
 const (
@@ -383,6 +384,7 @@ func PrettyExamineMemory(address uintptr, memArea []byte, format byte) string {
 	default:
 		return fmt.Sprintf("not supprted format %q\n", string(format))
 	}
+	colFormat += "\t"
 
 	l := len(memArea)
 	rows := l / cols
@@ -394,17 +396,18 @@ func PrettyExamineMemory(address uintptr, memArea []byte, format byte) string {
 	if l != 0 {
 		addrLen = len(fmt.Sprintf("%x", uint64(address)+uint64(l)))
 	}
-	addrFmt = "0x%0" + strconv.Itoa(addrLen) + "x:"
+	addrFmt = "0x%0" + strconv.Itoa(addrLen) + "x:\t"
 
-	lines := ""
+	var b strings.Builder
+	w := tabwriter.NewWriter(&b, 0, 0, 3, ' ', 0)
 	for i := 0; i < rows; i++ {
-		lines += fmt.Sprintf(addrFmt, address)
+		fmt.Fprintf(w, addrFmt, address)
 		for j := 0; j < cols && i*cols+j < l; j++ {
-			curOutput := "   " + fmt.Sprintf(colFormat, memArea[i*cols+j])
-			lines += curOutput
+			fmt.Fprintf(w, colFormat, memArea[i*cols+j])
 		}
-		lines += "\n"
+		fmt.Fprintln(w, "")
 		address += uintptr(cols)
 	}
-	return lines
+	w.Flush()
+	return b.String()
 }
diff --git a/service/dap/server.go b/service/dap/server.go
index fe916023..83b68e70 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -16,6 +16,7 @@ import (
 	"net"
 	"path/filepath"
 
+	"github.com/go-delve/delve/pkg/gobuild"
 	"github.com/go-delve/delve/pkg/logflags"
 	"github.com/go-delve/delve/pkg/proc"
 	"github.com/go-delve/delve/service"
@@ -52,6 +53,8 @@ type Server struct {
 	log *logrus.Entry
 	// stopOnEntry is set to automatically stop the debugee after start.
 	stopOnEntry bool
+	// binaryToRemove is the compiled binary to be removed on disconnect.
+	binaryToRemove string
 }
 
 // NewServer creates a new DAP Server. It takes an opened Listener
@@ -115,6 +118,9 @@ func (s *Server) signalDisconnect() {
 		close(s.config.DisconnectChan)
 		s.config.DisconnectChan = nil
 	}
+	if s.binaryToRemove != "" {
+		gobuild.Remove(s.binaryToRemove)
+	}
 }
 
 // Run launches a new goroutine where it accepts a client connection
@@ -286,34 +292,72 @@ func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
 	s.send(response)
 }
 
+// Output path for the compiled binary in debug or test modes.
+const debugBinary string = "./__debug_bin"
+
 func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 	// TODO(polina): Respond with an error if debug session is in progress?
-	program, ok := request.Arguments["program"]
+
+	program, ok := request.Arguments["program"].(string)
 	if !ok || program == "" {
 		s.sendErrorResponse(request.Request,
 			FailedToContinue, "Failed to launch",
 			"The program attribute is missing in debug configuration.")
 		return
 	}
-	s.config.ProcessArgs = []string{program.(string)}
-	s.config.WorkingDir = filepath.Dir(program.(string))
-	// TODO: support program args
-
-	stop, ok := request.Arguments["stopOnEntry"]
-	s.stopOnEntry = (ok && stop == true)
 
 	mode, ok := request.Arguments["mode"]
 	if !ok || mode == "" {
 		mode = "debug"
 	}
-	// TODO(polina): support "debug", "test" and "remote" modes
-	if mode != "exec" {
+
+	if mode == "debug" || mode == "test" {
+		output, ok := request.Arguments["output"].(string)
+		if !ok || output == "" {
+			output = debugBinary
+		}
+		debugname, err := filepath.Abs(output)
+		if err != nil {
+			s.sendInternalErrorResponse(request.Seq, err.Error())
+			return
+		}
+
+		buildFlags, ok := request.Arguments["buildFlags"].(string)
+		if !ok {
+			buildFlags = ""
+		}
+
+		switch mode {
+		case "debug":
+			err = gobuild.GoBuild(debugname, []string{program}, buildFlags)
+		case "test":
+			err = gobuild.GoTestBuild(debugname, []string{program}, buildFlags)
+		}
+		if err != nil {
+			s.sendErrorResponse(request.Request,
+				FailedToContinue, "Failed to launch",
+				fmt.Sprintf("Build error: %s", err.Error()))
+			return
+		}
+		program = debugname
+		s.binaryToRemove = debugname
+	}
+
+	// TODO(polina): support "remote" mode
+	if mode != "exec" && mode != "debug" && mode != "test" {
 		s.sendErrorResponse(request.Request,
 			FailedToContinue, "Failed to launch",
 			fmt.Sprintf("Unsupported 'mode' value %q in debug configuration.", mode))
 		return
 	}
 
+	stop, ok := request.Arguments["stopOnEntry"]
+	s.stopOnEntry = (ok && stop == true)
+
+	// TODO(polina): support target args
+	s.config.ProcessArgs = []string{program}
+	s.config.WorkingDir = filepath.Dir(program)
+
 	config := &debugger.Config{
 		WorkingDir:           s.config.WorkingDir,
 		AttachPid:            0,


Test patch:
diff --git a/_fixtures/buildtest/main_test.go b/_fixtures/buildtest/main_test.go
new file mode 100644
index 00000000..3b7d16cc
--- /dev/null
+++ b/_fixtures/buildtest/main_test.go
@@ -0,0 +1,10 @@
+package main
+
+import (
+	"os"
+	"testing"
+)
+
+func TestMain(m *testing.M) {
+	os.Exit(m.Run())
+}
diff --git a/service/api/prettyprint_test.go b/service/api/prettyprint_test.go
index 348ed12e..9ec90686 100644
--- a/service/api/prettyprint_test.go
+++ b/service/api/prettyprint_test.go
@@ -13,9 +13,9 @@ func TestPrettyExamineMemory(t *testing.T) {
 	format := byte('o')
 
 	display := []string{
-		"0x0ffff:   0141   0142   0143   0144   0145   0146   0147   0150",
-		"0x10007:   0151   0152   0153   0154   0155   0156   0157   0160",
-		"0x1000f:   0161   0162   0163   0164   0165   0166   0167   0170",
+		"0x0ffff:   0141   0142   0143   0144   0145   0146   0147   0150   ",
+		"0x10007:   0151   0152   0153   0154   0155   0156   0157   0160   ",
+		"0x1000f:   0161   0162   0163   0164   0165   0166   0167   0170   ",
 		"0x10017:   0171   0172"}
 	res := strings.Split(strings.TrimSpace(PrettyExamineMemory(addr, memArea, format)), "\n")
 
diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go
index 35d90b9f..7d607bc7 100644
--- a/service/dap/daptest/client.go
+++ b/service/dap/daptest/client.go
@@ -136,18 +136,27 @@ func (c *Client) InitializeRequest() {
 	c.send(request)
 }
 
-// LaunchRequest sends a 'launch' request.
-func (c *Client) LaunchRequest(program string, stopOnEntry bool) {
+// LaunchRequest sends a 'launch' request with the specified args.
+func (c *Client) LaunchRequest(mode string, program string, stopOnEntry bool) {
 	request := &dap.LaunchRequest{Request: *c.newRequest("launch")}
 	request.Arguments = map[string]interface{}{
 		"request":     "launch",
-		"mode":        "exec",
+		"mode":        mode,
 		"program":     program,
 		"stopOnEntry": stopOnEntry,
 	}
 	c.send(request)
 }
 
+// LaunchRequestWithArgs takes a map of untyped implementation-specific
+// arguments to send a 'launch' request. This version can be used to
+// test for values of unexpected types or unspecified values.
+func (c *Client) LaunchRequestWithArgs(arguments map[string]interface{}) {
+	request := &dap.LaunchRequest{Request: *c.newRequest("launch")}
+	request.Arguments = arguments
+	c.send(request)
+}
+
 // DisconnectRequest sends a 'disconnect' request.
 func (c *Client) DisconnectRequest() {
 	request := &dap.DisconnectRequest{Request: *c.newRequest("disconnect")}
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index 6160fc92..5e1ceeef 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -5,6 +5,7 @@ import (
 	"io"
 	"net"
 	"os"
+	"path/filepath"
 	"sync"
 	"testing"
 	"time"
@@ -16,6 +17,8 @@ import (
 	"github.com/google/go-dap"
 )
 
+const stopOnEntry bool = true
+
 func TestMain(m *testing.M) {
 	var logOutput string
 	flag.StringVar(&logOutput, "log-output", "", "configures log output")
@@ -73,7 +76,7 @@ func TestStopOnEntry(t *testing.T) {
 			t.Errorf("got %#v, want Seq=0, RequestSeq=0", initResp)
 		}
 
-		client.LaunchRequest(fixture.Path, true /*stopOnEntry*/)
+		client.LaunchRequest("exec", fixture.Path, stopOnEntry)
 		initEv := client.ExpectInitializedEvent(t)
 		if initEv.Seq != 0 {
 			t.Errorf("got %#v, want Seq=0", initEv)
@@ -128,7 +131,7 @@ func TestSetBreakpoint(t *testing.T) {
 		client.InitializeRequest()
 		client.ExpectInitializeResponse(t)
 
-		client.LaunchRequest(fixture.Path, false /*stopOnEntry*/)
+		client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
 		client.ExpectInitializedEvent(t)
 		launchResp := client.ExpectLaunchResponse(t)
 		if launchResp.RequestSeq != 1 {
@@ -172,14 +175,54 @@ func TestSetBreakpoint(t *testing.T) {
 	})
 }
 
-func TestBadLaunchRequests(t *testing.T) {
+// runDebugSesion is a helper for executing the standard init and shutdown
+// sequences while specifying unique launch criteria via parameters.
+func runDebugSession(t *testing.T, client *daptest.Client, launchRequest func()) {
+	client.InitializeRequest()
+	client.ExpectInitializeResponse(t)
+
+	launchRequest()
+	client.ExpectInitializedEvent(t)
+	client.ExpectLaunchResponse(t)
+
+	client.ConfigurationDoneRequest()
+	client.ExpectConfigurationDoneResponse(t)
+
+	client.ExpectTerminatedEvent(t)
+	client.DisconnectRequest()
+	client.ExpectDisconnectResponse(t)
+}
+
+func TestLaunchDebugRequest(t *testing.T) {
 	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
-		client.LaunchRequest("", true)
+		// We reuse the harness that builds, but ignore the actual binary.
+		runDebugSession(t, client, func() {
+			// Use the default output directory.
+			client.LaunchRequestWithArgs(map[string]interface{}{
+				"mode": "debug", "program": fixture.Source})
+		})
+	})
+}
 
-		expectFailedToLaunch := func(response *dap.ErrorResponse, seq int) {
+func TestLaunchTestRequest(t *testing.T) {
+	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSession(t, client, func() {
+			// We reuse the harness that builds, but ignore the actual binary.
+			fixtures := protest.FindFixturesDir()
+			testdir, _ := filepath.Abs(filepath.Join(fixtures, "buildtest"))
+			client.LaunchRequestWithArgs(map[string]interface{}{
+				"mode": "test", "program": testdir, "output": "__mytestdir"})
+		})
+	})
+}
+
+func TestBadLaunchRequests(t *testing.T) {
+	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
+		seqCnt := 0
+		expectFailedToLaunch := func(response *dap.ErrorResponse) {
 			t.Helper()
-			if response.RequestSeq != seq {
-				t.Errorf("RequestSeq got %d, want %d", seq, response.RequestSeq)
+			if response.RequestSeq != seqCnt {
+				t.Errorf("RequestSeq got %d, want %d", seqCnt, response.RequestSeq)
 			}
 			if response.Command != "launch" {
 				t.Errorf("Command got %q, want \"launch\"", response.Command)
@@ -190,28 +233,61 @@ func TestBadLaunchRequests(t *testing.T) {
 			if response.Body.Error.Id != 3000 {
 				t.Errorf("Id got %d, want 3000", response.Body.Error.Id)
 			}
+			seqCnt++
 		}
 
-		resp := client.ExpectErrorResponse(t)
-		expectFailedToLaunch(resp, 0)
-		// Test for the DAP-specific detailed error message.
-		wantErrorFormat := "Failed to launch: The program attribute is missing in debug configuration."
-		if resp.Body.Error.Format != wantErrorFormat {
-			t.Errorf("got %q, want %q", resp.Body.Error.Format, wantErrorFormat)
+		expectFailedToLaunchWithMessage := func(response *dap.ErrorResponse, errmsg string) {
+			t.Helper()
+			expectFailedToLaunch(response)
+			if response.Body.Error.Format != errmsg {
+				t.Errorf("\ngot  %q\nwant %q", response.Body.Error.Format, errmsg)
+			}
 		}
 
+		// Test for the DAP-specific detailed error message.
+		client.LaunchRequest("exec", "", stopOnEntry)
+		expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
+			"Failed to launch: The program attribute is missing in debug configuration.")
+
+		client.LaunchRequestWithArgs(map[string]interface{}{"program": 12345})
+		expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
+			"Failed to launch: The program attribute is missing in debug configuration.")
+
+		client.LaunchRequestWithArgs(map[string]interface{}{"program": nil})
+		expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
+			"Failed to launch: The program attribute is missing in debug configuration.")
+
+		client.LaunchRequestWithArgs(map[string]interface{}{})
+		expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
+			"Failed to launch: The program attribute is missing in debug configuration.")
+
+		client.LaunchRequest("remote", fixture.Path, stopOnEntry)
+		expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
+			"Failed to launch: Unsupported 'mode' value \"remote\" in debug configuration.")
+
+		client.LaunchRequest("notamode", fixture.Path, stopOnEntry)
+		expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
+			"Failed to launch: Unsupported 'mode' value \"notamode\" in debug configuration.")
+
+		client.LaunchRequestWithArgs(map[string]interface{}{"mode": 12345, "program": fixture.Path})
+		expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
+			"Failed to launch: Unsupported 'mode' value %!q(float64=12345) in debug configuration.")
+
 		// Skip detailed message checks for potentially different OS-specific errors.
-		client.LaunchRequest(fixture.Path+"_does_not_exist", false)
-		expectFailedToLaunch(client.ExpectErrorResponse(t), 1)
+		client.LaunchRequest("exec", fixture.Path+"_does_not_exist", stopOnEntry)
+		expectFailedToLaunch(client.ExpectErrorResponse(t))
+
+		client.LaunchRequest("debug", fixture.Path+"_does_not_exist", stopOnEntry)
+		expectFailedToLaunch(client.ExpectErrorResponse(t)) // Build error
 
-		client.LaunchRequest(fixture.Source, true) // Not an executable
-		expectFailedToLaunch(client.ExpectErrorResponse(t), 2)
+		client.LaunchRequest("exec", fixture.Source, stopOnEntry)
+		expectFailedToLaunch(client.ExpectErrorResponse(t)) // Not an executable
 
 		// We failed to launch the program. Make sure shutdown still works.
 		client.DisconnectRequest()
 		dresp := client.ExpectDisconnectResponse(t)
-		if dresp.RequestSeq != 3 {
-			t.Errorf("got %#v, want RequestSeq=3", dresp)
+		if dresp.RequestSeq != seqCnt {
+			t.Errorf("got %#v, want RequestSeq=%d", dresp, seqCnt)
 		}
 	})
 }