Repository: go-delve/delve
Pull request #2487 (2021-05-15 09:24:55)

gdbserial: for the 'g' command on debugserver treat E74 as unsupported
The maintainer of debugserver says he wants to fix the problem with the
'g' command but doesn't know when it will happen. Treat the error 'E74'
for the 'g' command on debugserver as if the server had returned an
unsupported error so that, for this specific problem, the error doesn't
resurface in the future.



Patch:
diff --git a/pkg/dwarf/op/op.go b/pkg/dwarf/op/op.go
index 7410f7c1..1d19bc10 100644
--- a/pkg/dwarf/op/op.go
+++ b/pkg/dwarf/op/op.go
@@ -30,12 +30,20 @@ type context struct {
 
 // Piece is a piece of memory stored either at an address or in a register.
 type Piece struct {
-	Size       int
-	Addr       int64
-	RegNum     uint64
-	IsRegister bool
+	Size int
+	Kind PieceKind
+	Val  uint64
 }
 
+// PieceKind describes the kind of a piece.
+type PieceKind uint8
+
+const (
+	AddrPiece PieceKind = iota // The piece is stored in memory, Val is the address
+	RegPiece                   // The piece is stored in a register, Val is the register number
+	ImmPiece                   // The piece is an immediate value, Val is the value
+)
+
 // ExecuteStackProgram executes a DWARF location expression and returns
 // either an address (int64), or a slice of Pieces for location expressions
 // that don't evaluate to an address (such as register and composite expressions).
@@ -56,7 +64,7 @@ func ExecuteStackProgram(regs DwarfRegisters, instructions []byte, ptrSize int)
 		if ctxt.reg && opcode != DW_OP_piece {
 			// last opcode was DW_OP_regN and next one isn't DW_OP_piece so convert
 			// the register piece into a stack value.
-			ctxt.stack = append(ctxt.stack, int64(regs.Uint64Val(ctxt.pieces[len(ctxt.pieces)-1].RegNum)))
+			ctxt.stack = append(ctxt.stack, int64(regs.Uint64Val(ctxt.pieces[len(ctxt.pieces)-1].Val)))
 			ctxt.pieces = ctxt.pieces[:len(ctxt.pieces)-1]
 			ctxt.reg = false
 		}
@@ -72,8 +80,8 @@ func ExecuteStackProgram(regs DwarfRegisters, instructions []byte, ptrSize int)
 	}
 
 	if ctxt.pieces != nil {
-		if len(ctxt.pieces) == 1 && ctxt.pieces[0].IsRegister {
-			return int64(regs.Uint64Val(ctxt.pieces[0].RegNum)), ctxt.pieces, nil
+		if len(ctxt.pieces) == 1 && ctxt.pieces[0].Kind == RegPiece {
+			return int64(regs.Uint64Val(ctxt.pieces[0].Val)), ctxt.pieces, nil
 		}
 		return 0, ctxt.pieces, nil
 	}
@@ -187,9 +195,9 @@ func register(opcode Opcode, ctxt *context) error {
 	ctxt.reg = true
 	if opcode == DW_OP_regx {
 		n, _ := util.DecodeSLEB128(ctxt.buf)
-		ctxt.pieces = append(ctxt.pieces, Piece{IsRegister: true, RegNum: uint64(n)})
+		ctxt.pieces = append(ctxt.pieces, Piece{Kind: RegPiece, Val: uint64(n)})
 	} else {
-		ctxt.pieces = append(ctxt.pieces, Piece{IsRegister: true, RegNum: uint64(opcode - DW_OP_reg0)})
+		ctxt.pieces = append(ctxt.pieces, Piece{Kind: RegPiece, Val: uint64(opcode - DW_OP_reg0)})
 	}
 	return nil
 }
@@ -203,11 +211,14 @@ func piece(opcode Opcode, ctxt *context) error {
 	}
 
 	if len(ctxt.stack) == 0 {
-		return errors.New("empty OP stack")
+		// nothing on the stack means this piece is unavailable (padding,
+		// optimized away...), see DWARFv4 sec. 2.6.1.3 page 30.
+		ctxt.pieces = append(ctxt.pieces, Piece{Size: int(sz), Kind: ImmPiece, Val: 0})
+		return nil
 	}
 
 	addr := ctxt.stack[len(ctxt.stack)-1]
-	ctxt.pieces = append(ctxt.pieces, Piece{Size: int(sz), Addr: addr})
+	ctxt.pieces = append(ctxt.pieces, Piece{Size: int(sz), Kind: AddrPiece, Val: uint64(addr)})
 	ctxt.stack = ctxt.stack[:0]
 	return nil
 }
diff --git a/pkg/gobuild/gobuild.go b/pkg/gobuild/gobuild.go
index 0f3cc051..bf571180 100644
--- a/pkg/gobuild/gobuild.go
+++ b/pkg/gobuild/gobuild.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"os/exec"
 	"runtime"
+	"strings"
 	"time"
 
 	"github.com/go-delve/delve/pkg/config"
@@ -60,7 +61,7 @@ func GoBuild(debugname string, pkgs []string, buildflags string) error {
 
 // GoBuildCombinedOutput builds non-test files in 'pkgs' with the specified 'buildflags'
 // and writes the output at 'debugname'.
-func GoBuildCombinedOutput(debugname string, pkgs []string, buildflags string) ([]byte, error) {
+func GoBuildCombinedOutput(debugname string, pkgs []string, buildflags string) (string, []byte, error) {
 	args := goBuildArgs(debugname, pkgs, buildflags, false)
 	return gocommandCombinedOutput("build", args...)
 }
@@ -74,7 +75,7 @@ func GoTestBuild(debugname string, pkgs []string, buildflags string) error {
 
 // GoTestBuildCombinedOutput builds test files 'pkgs' with the specified 'buildflags'
 // and writes the output at 'debugname'.
-func GoTestBuildCombinedOutput(debugname string, pkgs []string, buildflags string) ([]byte, error) {
+func GoTestBuildCombinedOutput(debugname string, pkgs []string, buildflags string) (string, []byte, error) {
 	args := goBuildArgs(debugname, pkgs, buildflags, true)
 	return gocommandCombinedOutput("test", args...)
 }
@@ -93,19 +94,21 @@ func goBuildArgs(debugname string, pkgs []string, buildflags string, isTest bool
 }
 
 func gocommandRun(command string, args ...string) error {
-	goBuild := gocommandExecCmd(command, args...)
+	_, goBuild := gocommandExecCmd(command, args...)
 	goBuild.Stderr = os.Stdout
 	goBuild.Stdout = os.Stderr
 	return goBuild.Run()
 }
 
-func gocommandCombinedOutput(command string, args ...string) ([]byte, error) {
-	return gocommandExecCmd(command, args...).CombinedOutput()
+func gocommandCombinedOutput(command string, args ...string) (string, []byte, error) {
+	buildCmd, goBuild := gocommandExecCmd(command, args...)
+	out, err := goBuild.CombinedOutput()
+	return buildCmd, out, err
 }
 
-func gocommandExecCmd(command string, args ...string) *exec.Cmd {
+func gocommandExecCmd(command string, args ...string) (string, *exec.Cmd) {
 	allargs := []string{command}
 	allargs = append(allargs, args...)
 	goBuild := exec.Command("go", allargs...)
-	return goBuild
+	return strings.Join(append([]string{"go"}, allargs...), " "), goBuild
 }
diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go
index 176ccf9f..37743667 100644
--- a/pkg/proc/gdbserial/gdbserver.go
+++ b/pkg/proc/gdbserial/gdbserver.go
@@ -1556,7 +1556,8 @@ func (t *gdbThread) reloadRegisters() error {
 
 	if t.p.gcmdok {
 		if err := t.p.conn.readRegisters(t.strID, t.regs.buf); err != nil {
-			if isProtocolErrorUnsupported(err) {
+			gdberr, isProt := err.(*GdbProtocolError)
+			if isProtocolErrorUnsupported(err) || (t.p.conn.isDebugserver && isProt && gdberr.code == "E74") {
 				t.p.gcmdok = false
 			} else {
 				return err
diff --git a/pkg/proc/mem.go b/pkg/proc/mem.go
index 70ff9434..8b19fbfb 100644
--- a/pkg/proc/mem.go
+++ b/pkg/proc/mem.go
@@ -1,6 +1,7 @@
 package proc
 
 import (
+	"encoding/binary"
 	"errors"
 	"fmt"
 
@@ -99,22 +100,33 @@ func newCompositeMemory(mem MemoryReadWriter, arch *Arch, regs op.DwarfRegisters
 	cmem := &compositeMemory{realmem: mem, arch: arch, regs: regs, pieces: pieces, data: []byte{}}
 	for i := range pieces {
 		piece := &pieces[i]
-		if piece.IsRegister {
-			reg := regs.Bytes(piece.RegNum)
+		switch piece.Kind {
+		case op.RegPiece:
+			reg := regs.Bytes(piece.Val)
 			if piece.Size == 0 && len(pieces) == 1 {
 				piece.Size = len(reg)
 			}
 			if piece.Size > len(reg) {
 				if regs.FloatLoadError != nil {
-					return nil, fmt.Errorf("could not read %d bytes from register %d (size: %d), also error loading floating point registers: %v", piece.Size, piece.RegNum, len(reg), regs.FloatLoadError)
+					return nil, fmt.Errorf("could not read %d bytes from register %d (size: %d), also error loading floating point registers: %v", piece.Size, piece.Val, len(reg), regs.FloatLoadError)
 				}
-				return nil, fmt.Errorf("could not read %d bytes from register %d (size: %d)", piece.Size, piece.RegNum, len(reg))
+				return nil, fmt.Errorf("could not read %d bytes from register %d (size: %d)", piece.Size, piece.Val, len(reg))
 			}
 			cmem.data = append(cmem.data, reg[:piece.Size]...)
-		} else {
+		case op.AddrPiece:
 			buf := make([]byte, piece.Size)
-			mem.ReadMemory(buf, uint64(piece.Addr))
+			mem.ReadMemory(buf, uint64(piece.Val))
 			cmem.data = append(cmem.data, buf...)
+		case op.ImmPiece:
+			sz := 8
+			if piece.Size > sz {
+				sz = piece.Size
+			}
+			buf := make([]byte, sz)
+			binary.LittleEndian.PutUint64(buf, piece.Val)
+			cmem.data = append(cmem.data, buf[:piece.Size]...)
+		default:
+			panic("unsupported piece kind")
 		}
 	}
 	return cmem, nil
@@ -147,17 +159,22 @@ func (mem *compositeMemory) WriteMemory(addr uint64, data []byte) (int, error) {
 			// changed memory interval overlaps current piece
 			pieceMem := mem.data[curAddr : curAddr+uint64(piece.Size)]
 
-			if piece.IsRegister {
-				err := mem.regs.ChangeFunc(piece.RegNum, op.DwarfRegisterFromBytes(pieceMem))
+			switch piece.Kind {
+			case op.RegPiece:
+				err := mem.regs.ChangeFunc(piece.Val, op.DwarfRegisterFromBytes(pieceMem))
 				if err != nil {
 					return donesz, err
 				}
-			} else {
-				n, err := mem.realmem.WriteMemory(uint64(piece.Addr), pieceMem)
+			case op.AddrPiece:
+				n, err := mem.realmem.WriteMemory(uint64(piece.Val), pieceMem)
 				if err != nil {
 					return donesz + n, err
 				}
-
+			case op.ImmPiece:
+				//TODO(aarzilli): maybe return an error if the user tried to change the value?
+				// nothing to do
+			default:
+				panic("unsupported piece kind")
 			}
 			donesz += piece.Size
 		}
diff --git a/pkg/proc/native/proc_linux.go b/pkg/proc/native/proc_linux.go
index 652990f1..1745f07c 100644
--- a/pkg/proc/native/proc_linux.go
+++ b/pkg/proc/native/proc_linux.go
@@ -342,6 +342,16 @@ func (dbp *nativeProcess) trapWaitInternal(pid int, options trapWaitOptions) (*n
 			delete(dbp.threads, wpid)
 			continue
 		}
+		if status.Signaled() {
+			// Signaled means the thread was terminated due to a signal.
+			if wpid == dbp.pid {
+				dbp.postExit()
+				return nil, proc.ErrProcessExited{Pid: wpid, Status: -int(status.Signal())}
+			}
+			// does this ever happen?
+			delete(dbp.threads, wpid)
+			continue
+		}
 		if status.StopSignal() == sys.SIGTRAP && status.TrapCause() == sys.PTRACE_EVENT_CLONE {
 			// A traced thread has cloned a new thread, grab the pid and
 			// add it to our list of traced threads.
@@ -566,12 +576,28 @@ func (dbp *nativeProcess) stop(trapthread *nativeThread) (*nativeThread, error)
 	switchTrapthread := false
 
 	// set breakpoints on SIGTRAP threads
+	var err1 error
 	for _, th := range dbp.threads {
+		pc, _ := th.PC()
+
+		if !th.os.setbp && pc != th.os.phantomBreakpointPC {
+			// check if this could be a breakpoint hit anyway that the OS hasn't notified us about, yet.
+			if _, ok := dbp.FindBreakpoint(pc, dbp.BinInfo().Arch.BreakInstrMovesPC()); ok {
+				th.os.phantomBreakpointPC = pc
+			}
+		}
+
+		if pc != th.os.phantomBreakpointPC {
+			th.os.phantomBreakpointPC = 0
+		}
+
 		if th.CurrentBreakpoint.Breakpoint == nil && th.os.setbp {
 			if err := th.SetCurrentBreakpoint(true); err != nil {
-				return nil, err
+				err1 = err
+				continue
 			}
 		}
+
 		if th.CurrentBreakpoint.Breakpoint == nil && th.os.setbp && (th.Status != nil) && ((*sys.WaitStatus)(th.Status).StopSignal() == sys.SIGTRAP) && dbp.BinInfo().Arch.BreakInstrMovesPC() {
 			manualStop := false
 			if th.ThreadID() == trapthread.ThreadID() {
@@ -579,7 +605,7 @@ func (dbp *nativeProcess) stop(trapthread *nativeThread) (*nativeThread, error)
 				manualStop = dbp.manualStopRequested
 				dbp.stopMu.Unlock()
 			}
-			if !manualStop {
+			if !manualStop && th.os.phantomBreakpointPC == pc {
 				// Thread received a SIGTRAP but we don't have a breakpoint for it and
 				// it wasn't sent by a manual stop request. It's either a hardcoded
 				// breakpoint or a phantom breakpoint hit (a breakpoint that was hit but
@@ -614,6 +640,9 @@ func (dbp *nativeProcess) stop(trapthread *nativeThread) (*nativeThread, error)
 			}
 		}
 	}
+	if err1 != nil {
+		return nil, err1
+	}
 
 	if switchTrapthread {
 		trapthreadID := trapthread.ID
diff --git a/pkg/proc/native/threads_linux.go b/pkg/proc/native/threads_linux.go
index 4c9f4a55..ae278160 100644
--- a/pkg/proc/native/threads_linux.go
+++ b/pkg/proc/native/threads_linux.go
@@ -13,9 +13,10 @@ type waitStatus sys.WaitStatus
 // osSpecificDetails hold Linux specific
 // process details.
 type osSpecificDetails struct {
-	delayedSignal int
-	running       bool
-	setbp         bool
+	delayedSignal       int
+	running             bool
+	setbp               bool
+	phantomBreakpointPC uint64
 }
 
 func (t *nativeThread) stop() (err error) {
diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go
index b7c30b81..7209c782 100644
--- a/pkg/terminal/command.go
+++ b/pkg/terminal/command.go
@@ -2279,6 +2279,10 @@ func digits(n int) int {
 const stacktraceTruncatedMessage = "(truncated)"
 
 func printStack(t *Term, out io.Writer, stack []api.Stackframe, ind string, offsets bool) {
+	PrintStack(t.formatPath, out, stack, ind, offsets)
+}
+
+func PrintStack(formatPath func(string) string, out io.Writer, stack []api.Stackframe, ind string, offsets bool) {
 	if len(stack) == 0 {
 		return
 	}
@@ -2301,7 +2305,7 @@ func printStack(t *Term, out io.Writer, stack []api.Stackframe, ind string, offs
 			continue
 		}
 		fmt.Fprintf(out, fmtstr, ind, i, stack[i].PC, stack[i].Function.Name())
-		fmt.Fprintf(out, "%sat %s:%d\n", s, t.formatPath(stack[i].File), stack[i].Line)
+		fmt.Fprintf(out, "%sat %s:%d\n", s, formatPath(stack[i].File), stack[i].Line)
 
 		if offsets {
 			fmt.Fprintf(out, "%sframe: %+#x frame pointer %+#x\n", s, stack[i].FrameOffset, stack[i].FramePointerOffset)
@@ -2315,8 +2319,8 @@ func printStack(t *Term, out io.Writer, stack []api.Stackframe, ind string, offs
 				continue
 			}
 			fmt.Fprintf(out, "%s%#016x in %s\n", deferHeader, d.DeferredLoc.PC, d.DeferredLoc.Function.Name())
-			fmt.Fprintf(out, "%sat %s:%d\n", s2, t.formatPath(d.DeferredLoc.File), d.DeferredLoc.Line)
-			fmt.Fprintf(out, "%sdeferred by %s at %s:%d\n", s2, d.DeferLoc.Function.Name(), t.formatPath(d.DeferLoc.File), d.DeferLoc.Line)
+			fmt.Fprintf(out, "%sat %s:%d\n", s2, formatPath(d.DeferredLoc.File), d.DeferredLoc.Line)
+			fmt.Fprintf(out, "%sdeferred by %s at %s:%d\n", s2, d.DeferLoc.Function.Name(), formatPath(d.DeferLoc.File), d.DeferLoc.Line)
 		}
 
 		for j := range stack[i].Arguments {
diff --git a/service/api/types.go b/service/api/types.go
index 6e46cd80..31e26005 100644
--- a/service/api/types.go
+++ b/service/api/types.go
@@ -414,6 +414,9 @@ const (
 	// SwitchGoroutine switches the debugger's current thread context to the thread running the specified goroutine
 	SwitchGoroutine = "switchGoroutine"
 	// Halt suspends the process.
+	// The effect of Halt while the target process is stopped, or in the
+	// process of stopping, is operating system and timing dependent. It will
+	// either have no effect or cause the following resume to stop immediately.
 	Halt = "halt"
 	// Call resumes process execution injecting a function call.
 	Call = "call"
diff --git a/service/dap/error_ids.go b/service/dap/error_ids.go
index 9cab0bcb..1a5344df 100644
--- a/service/dap/error_ids.go
+++ b/service/dap/error_ids.go
@@ -21,7 +21,8 @@ const (
 	UnableToListGlobals        = 2007
 	UnableToLookupVariable     = 2008
 	UnableToEvaluateExpression = 2009
-
+	UnableToGetExceptionInfo   = 2010
+	// Add more codes as we support more requests
 	DebuggeeIsRunning = 4000
 	DisconnectError   = 5000
 )
diff --git a/service/dap/server.go b/service/dap/server.go
index bf293d5d..0d808558 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -10,6 +10,7 @@ package dap
 
 import (
 	"bufio"
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"go/constant"
@@ -29,6 +30,7 @@ import (
 	"github.com/go-delve/delve/pkg/locspec"
 	"github.com/go-delve/delve/pkg/logflags"
 	"github.com/go-delve/delve/pkg/proc"
+	"github.com/go-delve/delve/pkg/terminal"
 	"github.com/go-delve/delve/service"
 	"github.com/go-delve/delve/service/api"
 	"github.com/go-delve/delve/service/debugger"
@@ -102,6 +104,8 @@ type Server struct {
 	variableHandles *variablesHandlesMap
 	// args tracks special settings for handling debug session requests.
 	args launchAttachArgs
+	// exceptionErr tracks the runtime error that last occurred.
+	exceptionErr error
 	// clientCapabilities tracks special settings for handling debug session requests.
 	clientCapabilities dapClientCapabilites
 
@@ -188,6 +192,7 @@ func NewServer(config *service.Config) *Server {
 		stackFrameHandles: newHandlesMap(),
 		variableHandles:   newVariablesHandlesMap(),
 		args:              defaultArgs,
+		exceptionErr:      nil,
 	}
 }
 
@@ -399,8 +404,7 @@ func (s *Server) handleRequest(request dap.Message) {
 	// We have a couple of options for handling these without blocking
 	// the request loop indefinitely when we are in running state.
 	// --1-- Return a dummy response or an error right away.
-	// --2-- Halt execution, process the request, resume execution.
-	// TODO(polina): do this for setting breakpoints
+	// --2-- Halt execution, process the request, maybe resume execution.
 	// --3-- Handle such requests asynchronously and let them block until
 	// the process stops or terminates (e.g. using a channel and a single
 	// goroutine to preserve the order). This might not be appropriate
@@ -427,6 +431,28 @@ func (s *Server) handleRequest(request dap.Message) {
 				Body:     dap.ThreadsResponseBody{Threads: []dap.Thread{{Id: -1, Name: "Current"}}},
 			}
 			s.send(response)
+		case *dap.SetBreakpointsRequest:
+			s.log.Debug("halting execution to set breakpoints")
+			_, err := s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
+			if err != nil {
+				s.sendErrorResponse(request.Request, UnableToSetBreakpoints, "Unable to set or clear breakpoints", err.Error())
+				return
+			}
+			s.onSetBreakpointsRequest(request)
+			// TODO(polina): consider resuming execution here automatically after suppressing
+			// a stop event when an operation in doRunCommand returns. In case that operation
+			// was already stopping for a different reason, we would need to examine the state
+			// that is returned to determine if this halt was the cause of the stop or not.
+			// We should stop with an event and not resume if one of the following is true:
+			// - StopReason is anything but manual
+			// - Any thread has a breakpoint or CallReturn set
+			// - NextInProgress is false and the last command sent by the user was: next,
+			//   step, stepOut, reverseNext, reverseStep or reverseStepOut
+			// Otherwise, we can skip the stop event and resume the temporarily
+			// interrupted process execution with api.DirectionCongruentContinue.
+			// For this to apply in cases other than api.Continue, we would also need to
+			// introduce a new version of halt that skips ClearInternalBreakpoints
+			// in proc.(*Target).Continue, leaving NextInProgress as true.
 		default:
 			r := request.(dap.RequestMessage).GetRequest()
 			s.sendErrorResponse(*r, DebuggeeIsRunning, fmt.Sprintf("Unable to process `%s`", r.Command), "debuggee is running")
@@ -550,6 +576,9 @@ func (s *Server) handleRequest(request dap.Message) {
 		// Optional (capability ‘supportsCancelRequest’)
 		// TODO: does this request make sense for delve?
 		s.onCancelRequest(request)
+	case *dap.ExceptionInfoRequest:
+		// Optional (capability ‘supportsExceptionInfoRequest’)
+		s.onExceptionInfoRequest(request)
 	//--- Requests that we do not plan to support ---
 	case *dap.RestartFrameRequest:
 		// Optional (capability ’supportsRestartFrame’)
@@ -574,10 +603,6 @@ func (s *Server) handleRequest(request dap.Message) {
 	case *dap.CompletionsRequest:
 		// Optional (capability ‘supportsCompletionsRequest’)
 		s.sendUnsupportedErrorResponse(request.Request)
-	case *dap.ExceptionInfoRequest:
-		// Optional (capability ‘supportsExceptionInfoRequest’)
-		// TODO: does this request make sense for delve?
-		s.sendUnsupportedErrorResponse(request.Request)
 	case *dap.DataBreakpointInfoRequest:
 		// Optional (capability ‘supportsDataBreakpoints’)
 		s.sendUnsupportedErrorResponse(request.Request)
@@ -643,6 +668,7 @@ func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
 	response.Body.SupportsConditionalBreakpoints = true
 	response.Body.SupportsDelayedStackTraceLoading = true
 	response.Body.SupportTerminateDebuggee = true
+	response.Body.SupportsExceptionInfoRequest = true
 	// TODO(polina): support this to match vscode-go functionality
 	response.Body.SupportsSetVariable = false
 	// TODO(polina): support these requests in addition to vscode-go feature parity
@@ -723,17 +749,24 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 		}
 
 		s.log.Debugf("building binary at %s", debugbinary)
+		var cmd string
 		var out []byte
 		switch mode {
 		case "debug":
-			out, err = gobuild.GoBuildCombinedOutput(debugbinary, []string{program}, buildFlags)
+			cmd, out, err = gobuild.GoBuildCombinedOutput(debugbinary, []string{program}, buildFlags)
 		case "test":
-			out, err = gobuild.GoTestBuildCombinedOutput(debugbinary, []string{program}, buildFlags)
+			cmd, out, err = gobuild.GoTestBuildCombinedOutput(debugbinary, []string{program}, buildFlags)
 		}
 		if err != nil {
+			s.send(&dap.OutputEvent{
+				Event: *newEvent("output"),
+				Body: dap.OutputEventBody{
+					Output:   fmt.Sprintf("Build Error: %s\n%s (%s)\n", cmd, strings.TrimSpace(string(out)), err.Error()),
+					Category: "stderr",
+				}})
 			s.sendErrorResponse(request.Request,
 				FailedToLaunch, "Failed to launch",
-				fmt.Sprintf("Build error: %s (%s)", strings.TrimSpace(string(out)), err.Error()))
+				"Build error: Check the debug console for details.")
 			return
 		}
 		program = debugbinary
@@ -1058,7 +1091,7 @@ func (s *Server) onConfigurationDoneRequest(request *dap.ConfigurationDoneReques
 	}
 	s.send(&dap.ConfigurationDoneResponse{Response: *newResponse(request.Request)})
 	if !s.args.stopOnEntry {
-		s.doCommand(api.Continue, asyncSetupDone)
+		s.doRunCommand(api.Continue, asyncSetupDone)
 	}
 }
 
@@ -1068,7 +1101,7 @@ func (s *Server) onContinueRequest(request *dap.ContinueRequest, asyncSetupDone
 	s.send(&dap.ContinueResponse{
 		Response: *newResponse(request.Request),
 		Body:     dap.ContinueResponseBody{AllThreadsContinued: true}})
-	s.doCommand(api.Continue, asyncSetupDone)
+	s.doRunCommand(api.Continue, asyncSetupDone)
 }
 
 func fnName(loc *proc.Location) string {
@@ -1133,6 +1166,7 @@ func (s *Server) onThreadsRequest(request *dap.ThreadsRequest) {
 			threads[i].Id = g.ID
 		}
 	}
+
 	response := &dap.ThreadsResponse{
 		Response: *newResponse(request.Request),
 		Body:     dap.ThreadsResponseBody{Threads: threads},
@@ -1214,7 +1248,7 @@ func stoppedGoroutineID(state *api.DebuggerState) (id int) {
 	return id
 }
 
-// doStepCommand is a wrapper around doCommand that
+// doStepCommand is a wrapper around doRunCommand that
 // first switches selected goroutine. asyncSetupDone is
 // a channel that will be closed to signal that an
 // asynchornous command has completed setup or was interrupted
@@ -1238,7 +1272,7 @@ func (s *Server) doStepCommand(command string, threadId int, asyncSetupDone chan
 		s.send(stopped)
 		return
 	}
-	s.doCommand(command, asyncSetupDone)
+	s.doRunCommand(command, asyncSetupDone)
 }
 
 // onPauseRequest sends a not-yet-implemented error response.
@@ -1310,13 +1344,23 @@ func (s *Server) onScopesRequest(request *dap.ScopesRequest) {
 	goid := sf.(stackFrame).goroutineID
 	frame := sf.(stackFrame).frameIndex
 
+	// Check if the function is optimized.
+	fn, err := s.debugger.Function(goid, frame, 0, DefaultLoadConfig)
+	if fn == nil || err != nil {
+		s.sendErrorResponse(request.Request, UnableToListArgs, "Unable to find enclosing function", err.Error())
+		return
+	}
+	suffix := ""
+	if fn.Optimized() {
+		suffix = " (warning: optimized function)"
+	}
 	// Retrieve arguments
 	args, err := s.debugger.FunctionArguments(goid, frame, 0, DefaultLoadConfig)
 	if err != nil {
 		s.sendErrorResponse(request.Request, UnableToListArgs, "Unable to list args", err.Error())
 		return
 	}
-	argScope := &fullyQualifiedVariable{&proc.Variable{Name: "Arguments", Children: slicePtrVarToSliceVar(args)}, "", true}
+	argScope := &fullyQualifiedVariable{&proc.Variable{Name: fmt.Sprintf("Arguments%s", suffix), Children: slicePtrVarToSliceVar(args)}, "", true}
 
 	// Retrieve local variables
 	locals, err := s.debugger.LocalVariables(goid, frame, 0, DefaultLoadConfig)
@@ -1324,7 +1368,7 @@ func (s *Server) onScopesRequest(request *dap.ScopesRequest) {
 		s.sendErrorResponse(request.Request, UnableToListLocals, "Unable to list locals", err.Error())
 		return
 	}
-	locScope := &fullyQualifiedVariable{&proc.Variable{Name: "Locals", Children: slicePtrVarToSliceVar(locals)}, "", true}
+	locScope := &fullyQualifiedVariable{&proc.Variable{Name: fmt.Sprintf("Locals%s", suffix), Children: slicePtrVarToSliceVar(locals)}, "", true}
 
 	scopeArgs := dap.Scope{Name: argScope.Name, VariablesReference: s.variableHandles.create(argScope)}
 	scopeLocals := dap.Scope{Name: locScope.Name, VariablesReference: s.variableHandles.create(locScope)}
@@ -1875,6 +1919,81 @@ func (s *Server) onCancelRequest(request *dap.CancelRequest) {
 	s.sendNotYetImplementedErrorResponse(request.Request)
 }
 
+// onExceptionInfoRequest handles 'exceptionInfo' requests.
+// Capability 'supportsExceptionInfoRequest' is set in 'initialize' response.
+func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
+	goroutineID := request.Arguments.ThreadId
+	var body dap.ExceptionInfoResponseBody
+	// Get the goroutine and the current state.
+	g, err := s.debugger.FindGoroutine(goroutineID)
+	if err != nil {
+		s.sendErrorResponse(request.Request, UnableToGetExceptionInfo, "Unable to get exception info", err.Error())
+		return
+	}
+	if g == nil {
+		s.sendErrorResponse(request.Request, UnableToGetExceptionInfo, "Unable to get exception info", fmt.Sprintf("could not find goroutine %d", goroutineID))
+		return
+	}
+	var bpState *proc.BreakpointState
+	if g.Thread != nil {
+		bpState = g.Thread.Breakpoint()
+	}
+	// Check if this goroutine ID is stopped at a breakpoint.
+	if bpState != nil && bpState.Breakpoint != nil && (bpState.Breakpoint.Name == proc.FatalThrow || bpState.Breakpoint.Name == proc.UnrecoveredPanic) {
+		switch bpState.Breakpoint.Name {
+		case proc.FatalThrow:
+			// TODO(suzmue): add the fatal throw reason to body.Description.
+			body.ExceptionId = "fatal error"
+		case proc.UnrecoveredPanic:
+			body.ExceptionId = "panic"
+			// Attempt to get the value of the panic message.
+			exprVar, err := s.debugger.EvalVariableInScope(goroutineID, 0, 0, "(*msgs).arg.(data)", DefaultLoadConfig)
+			if err == nil {
+				body.Description = exprVar.Value.String()
+			}
+		}
+	} else {
+		// If this thread is not stopped on a breakpoint, then a runtime error must have occurred.
+		// If we do not have any error saved, or if this thread is not current thread,
+		// return an error.
+		if s.exceptionErr == nil {
+			s.sendErrorResponse(request.Request, UnableToGetExceptionInfo, "Unable to get exception info", "no runtime error found")
+			return
+		}
+
+		state, err := s.debugger.State( /*nowait*/ true)
+		if err != nil {
+			s.sendErrorResponse(request.Request, UnableToGetExceptionInfo, "Unable to get exception info", err.Error())
+			return
+		}
+		if state == nil || state.CurrentThread == nil || g.Thread == nil || state.CurrentThread.ID != g.Thread.ThreadID() {
+			s.sendErrorResponse(request.Request, UnableToGetExceptionInfo, "Unable to get exception info", fmt.Sprintf("no exception found for goroutine %d", goroutineID))
+			return
+		}
+		body.ExceptionId = "runtime error"
+		body.Description = s.exceptionErr.Error()
+		if body.Description == "bad access" {
+			body.Description = BetterBadAccessError
+		}
+	}
+
+	frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0)
+	if err == nil && len(frames) > 0 {
+		apiFrames, err := s.debugger.ConvertStacktrace(frames, nil)
+		if err == nil {
+			var buf bytes.Buffer
+			fmt.Fprintln(&buf, "Stack:")
+			terminal.PrintStack(s.toClientPath, &buf, apiFrames, "\t", false)
+			body.Details.StackTrace = buf.String()
+		}
+	}
+	response := &dap.ExceptionInfoResponse{
+		Response: *newResponse(request.Request),
+		Body:     body,
+	}
+	s.send(response)
+}
+
 // sendErrorResponseWithOpts offers configuration options.
 //   showUser - if true, the error will be shown to the user (e.g. via a visible pop-up)
 func (s *Server) sendErrorResponseWithOpts(request dap.Request, id int, summary, details string, showUser bool) {
@@ -1949,16 +2068,17 @@ Unable to propagate EXC_BAD_ACCESS signal to target process and panic (see https
 func (s *Server) resetHandlesForStoppedEvent() {
 	s.stackFrameHandles.reset()
 	s.variableHandles.reset()
+	s.exceptionErr = nil
 }
 
-// doCommand runs a debugger command until it stops on
+// doRunCommand runs a debugger command until it stops on
 // termination, error, breakpoint, etc, when an appropriate
 // event needs to be sent to the client. asyncSetupDone is
 // a channel that will be closed to signal that an
 // asynchornous command has completed setup or was interrupted
 // due to an error, so the server is ready to receive new requests.
-func (s *Server) doCommand(command string, asyncSetupDone chan struct{}) {
-	// TODO(polina): it appears that debugger.Command doesn't close
+func (s *Server) doRunCommand(command string, asyncSetupDone chan struct{}) {
+	// TODO(polina): it appears that debugger.Command doesn't always close
 	// asyncSetupDone (e.g. when having an error next while nexting).
 	// So we should always close it ourselves just in case.
 	defer s.asyncCommandDone(asyncSetupDone)
@@ -1968,16 +2088,20 @@ func (s *Server) doCommand(command string, asyncSetupDone chan struct{}) {
 		return
 	}
 
+	stopReason := s.debugger.StopReason()
+	s.log.Debugf("%q command stopped - reason %q", command, stopReason)
+
 	s.resetHandlesForStoppedEvent()
 	stopped := &dap.StoppedEvent{Event: *newEvent("stopped")}
 	stopped.Body.AllThreadsStopped = true
 
 	if err == nil {
+		// TODO(suzmue): If stopped.Body.ThreadId is not a valid goroutine
+		// then the stopped reason does not show up anywhere in the
+		// vscode ui.
 		stopped.Body.ThreadId = stoppedGoroutineID(state)
 
-		sr := s.debugger.StopReason()
-		s.log.Debugf("%q command stopped - reason %q", command, sr)
-		switch sr {
+		switch stopReason {
 		case proc.StopNextFinished:
 			stopped.Body.Reason = "step"
 		case proc.StopManual: // triggered by halt
@@ -1992,14 +2116,18 @@ func (s *Server) doCommand(command string, asyncSetupDone chan struct{}) {
 		if state.CurrentThread != nil && state.CurrentThread.Breakpoint != nil {
 			switch state.CurrentThread.Breakpoint.Name {
 			case proc.FatalThrow:
-				stopped.Body.Reason = "fatal error"
+				stopped.Body.Reason = "exception"
+				stopped.Body.Description = "Paused on fatal error"
 			case proc.UnrecoveredPanic:
-				stopped.Body.Reason = "panic"
+				stopped.Body.Reason = "exception"
+				stopped.Body.Description = "Paused on panic"
 			}
 		}
 	} else {
+		s.exceptionErr = err
 		s.log.Error("runtime error: ", err)
-		stopped.Body.Reason = "runtime error"
+		stopped.Body.Reason = "exception"
+		stopped.Body.Description = "Paused on runtime error"
 		stopped.Body.Text = err.Error()
 		// Special case in the spirit of https://github.com/microsoft/vscode-go/issues/1903
 		if stopped.Body.Text == "bad access" {
@@ -2009,19 +2137,6 @@ func (s *Server) doCommand(command string, asyncSetupDone chan struct{}) {
 		if err == nil {
 			stopped.Body.ThreadId = stoppedGoroutineID(state)
 		}
-
-		// TODO(polina): according to the spec, the extra 'text' is supposed to show up in the UI (e.g. on hover),
-		// but so far I am unable to get this to work in vscode - see https://github.com/microsoft/vscode/issues/104475.
-		// Options to explore:
-		//   - supporting ExceptionInfo request
-		//   - virtual variable scope for Exception that shows the message (details here: https://github.com/microsoft/vscode/issues/3101)
-		// In the meantime, provide the extra details by outputing an error message.
-		s.send(&dap.OutputEvent{
-			Event: *newEvent("output"),
-			Body: dap.OutputEventBody{
-				Output:   fmt.Sprintf("ERROR: %s\n", stopped.Body.Text),
-				Category: "stderr",
-			}})
 	}
 
 	// NOTE: If we happen to be responding to another request with an is-running
diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go
index 14ecd46d..f58dc337 100644
--- a/service/debugger/debugger.go
+++ b/service/debugger/debugger.go
@@ -967,6 +967,14 @@ func (d *Debugger) FindThread(id int) (proc.Thread, error) {
 	return nil, nil
 }
 
+// FindGoroutine returns the goroutine for the given 'id'.
+func (d *Debugger) FindGoroutine(id int) (*proc.G, error) {
+	d.targetMutex.Lock()
+	defer d.targetMutex.Unlock()
+
+	return proc.FindGoroutine(d.target, id)
+}
+
 func (d *Debugger) setRunning(running bool) {
 	d.runningMutex.Lock()
 	d.running = running
@@ -1357,6 +1365,18 @@ func (d *Debugger) FunctionArguments(goid, frame, deferredCall int, cfg proc.Loa
 	return s.FunctionArguments(cfg)
 }
 
+// Function returns the current function.
+func (d *Debugger) Function(goid, frame, deferredCall int, cfg proc.LoadConfig) (*proc.Function, error) {
+	d.targetMutex.Lock()
+	defer d.targetMutex.Unlock()
+
+	s, err := proc.ConvertEvalScope(d.target, goid, frame, deferredCall)
+	if err != nil {
+		return nil, err
+	}
+	return s.Fn, nil
+}
+
 // 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) {


Test patch:
diff --git a/Documentation/backend_test_health.md b/Documentation/backend_test_health.md
index 0aa52306..8b8a8d72 100644
--- a/Documentation/backend_test_health.md
+++ b/Documentation/backend_test_health.md
@@ -1,30 +1,30 @@
 Tests skipped by each supported backend:
 
-* 386 skipped = 4% (6/149)
+* 386 skipped = 4% (6/150)
 	* 1 broken
 	* 3 broken - cgo stacktraces
 	* 2 not implemented
-* arm64 skipped = 3.4% (5/149)
+* arm64 skipped = 3.3% (5/150)
 	* 1 broken
 	* 1 broken - cgo stacktraces
 	* 1 broken - global variable symbolication
 	* 2 not implemented
-* darwin skipped = 1.3% (2/149)
+* darwin skipped = 1.3% (2/150)
 	* 2 not implemented
-* darwin/arm64 skipped = 0.67% (1/149)
+* darwin/arm64 skipped = 0.67% (1/150)
 	* 1 broken - cgo stacktraces
-* darwin/lldb skipped = 0.67% (1/149)
+* darwin/lldb skipped = 0.67% (1/150)
 	* 1 upstream issue
-* freebsd skipped = 9.4% (14/149)
+* freebsd skipped = 9.3% (14/150)
 	* 11 broken
 	* 3 not implemented
-* linux/386/pie skipped = 0.67% (1/149)
+* linux/386/pie skipped = 0.67% (1/150)
 	* 1 broken
-* pie skipped = 0.67% (1/149)
+* pie skipped = 0.67% (1/150)
 	* 1 upstream issue - https://github.com/golang/go/issues/29322
-* rr skipped = 1.3% (2/149)
+* rr skipped = 1.3% (2/150)
 	* 2 not implemented
-* windows skipped = 2.7% (4/149)
+* windows skipped = 2.7% (4/150)
 	* 1 broken
 	* 2 not implemented
 	* 1 upstream issue
diff --git a/pkg/proc/dwarf_expr_test.go b/pkg/proc/dwarf_expr_test.go
index ed514c64..51860728 100644
--- a/pkg/proc/dwarf_expr_test.go
+++ b/pkg/proc/dwarf_expr_test.go
@@ -152,9 +152,11 @@ func TestDwarfExprRegisters(t *testing.T) {
 
 func TestDwarfExprComposite(t *testing.T) {
 	testCases := map[string]uint16{
-		"pair.k": 0x8765,
-		"pair.v": 0x5678,
-		"n":      42,
+		"pair.k":  0x8765,
+		"pair.v":  0x5678,
+		"n":       42,
+		"pair2.k": 0x8765,
+		"pair2.v": 0,
 	}
 
 	const stringVal = "this is a string"
@@ -188,6 +190,9 @@ func TestDwarfExprComposite(t *testing.T) {
 		op.DW_OP_reg1, op.DW_OP_piece, uint(8),
 		op.DW_OP_reg0, op.DW_OP_piece, uint(8)))
 	dwb.AddVariable("n", intoff, dwarfbuilder.LocationBlock(op.DW_OP_reg3))
+	dwb.AddVariable("pair2", pairoff, dwarfbuilder.LocationBlock(
+		op.DW_OP_reg2, op.DW_OP_piece, uint(2),
+		op.DW_OP_piece, uint(2)))
 	dwb.TagClose()
 
 	bi, _ := fakeBinaryInfo(t, dwb)
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 6a3fb32c..6241a1b9 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -5169,9 +5169,9 @@ func TestCompositeMemoryWrite(t *testing.T) {
 		oldPc, oldRax, oldXmm1 := getregs()
 		t.Logf("PC %#x AX %#x XMM1 %#x", oldPc, oldRax, oldXmm1)
 
-		memRax, err := proc.NewCompositeMemory(p, []op.Piece{{Size: 0, RegNum: 0, IsRegister: true}})
+		memRax, err := proc.NewCompositeMemory(p, []op.Piece{{Size: 0, Val: 0, Kind: op.RegPiece}})
 		assertNoError(err, t, "NewCompositeMemory (rax)")
-		memXmm1, err := proc.NewCompositeMemory(p, []op.Piece{{Size: 0, RegNum: 18, IsRegister: true}})
+		memXmm1, err := proc.NewCompositeMemory(p, []op.Piece{{Size: 0, Val: 18, Kind: op.RegPiece}})
 		assertNoError(err, t, "NewCompositeMemory (xmm1)")
 
 		if memRax := getmem(memRax); memRax != oldRax {
@@ -5307,3 +5307,51 @@ func TestWatchpointCounts(t *testing.T) {
 		}
 	})
 }
+
+func TestManualStopWhileStopped(t *testing.T) {
+	// Checks that RequestManualStop sent to a stopped thread does not cause the target process to die.
+	withTestProcess("loopprog", t, func(p *proc.Target, fixture protest.Fixture) {
+		asyncCont := func(done chan struct{}) {
+			defer close(done)
+			err := p.Continue()
+			t.Logf("%v\n", err)
+			if err != nil {
+				panic(err)
+			}
+			for _, th := range p.ThreadList() {
+				if th.Breakpoint().Breakpoint != nil {
+					t.Logf("unexpected stop at breakpoint: %v", th.Breakpoint().Breakpoint)
+					panic("unexpected stop at breakpoint")
+				}
+			}
+		}
+
+		const (
+			repeatsSlow = 3
+			repeatsFast = 5
+		)
+
+		for i := 0; i < repeatsSlow; i++ {
+			t.Logf("Continue %d (slow)", i)
+			done := make(chan struct{})
+			go asyncCont(done)
+			time.Sleep(1 * time.Second)
+			p.RequestManualStop()
+			time.Sleep(1 * time.Second)
+			p.RequestManualStop()
+			time.Sleep(1 * time.Second)
+			<-done
+		}
+		for i := 0; i < repeatsFast; i++ {
+			t.Logf("Continue %d (fast)", i)
+			rch := make(chan struct{})
+			done := make(chan struct{})
+			p.ResumeNotify(rch)
+			go asyncCont(done)
+			<-rch
+			p.RequestManualStop()
+			p.RequestManualStop()
+			<-done
+		}
+	})
+}
diff --git a/pkg/proc/proc_unix_test.go b/pkg/proc/proc_unix_test.go
index 40034920..5d7e2f85 100644
--- a/pkg/proc/proc_unix_test.go
+++ b/pkg/proc/proc_unix_test.go
@@ -4,11 +4,17 @@ package proc_test
 
 import (
 	"fmt"
+	"os"
+	"os/exec"
+	"runtime"
 	"syscall"
 	"testing"
 	"time"
 
+	"golang.org/x/sys/unix"
+
 	"github.com/go-delve/delve/pkg/proc"
+	"github.com/go-delve/delve/pkg/proc/native"
 	protest "github.com/go-delve/delve/pkg/proc/test"
 )
 
@@ -66,3 +72,31 @@ func TestIssue419(t *testing.T) {
 		}
 	}
 }
+
+func TestSignalDeath(t *testing.T) {
+	if testBackend != "native" || runtime.GOOS != "linux" {
+		t.Skip("skipped on non-linux non-native backends")
+	}
+	var buildFlags protest.BuildFlags
+	if buildMode == "pie" {
+		buildFlags |= protest.BuildModePIE
+	}
+	fixture := protest.BuildFixture("loopprog", buildFlags)
+	cmd := exec.Command(fixture.Path)
+	stdout, err := cmd.StdoutPipe()
+	assertNoError(err, t, "StdoutPipe")
+	cmd.Stderr = os.Stderr
+	assertNoError(cmd.Start(), t, "starting fixture")
+	p, err := native.Attach(cmd.Process.Pid, []string{})
+	assertNoError(err, t, "Attach")
+	stdout.Close() // target will receive SIGPIPE later on
+	err = p.Continue()
+	t.Logf("error is %v", err)
+	exitErr, isexited := err.(proc.ErrProcessExited)
+	if !isexited {
+		t.Fatal("did not exit")
+	}
+	if exitErr.Status != -int(unix.SIGPIPE) {
+		t.Fatalf("expected SIGPIPE got %d\n", exitErr.Status)
+	}
+}
diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go
index abc3a11f..b58bfad7 100644
--- a/service/dap/daptest/client.go
+++ b/service/dap/daptest/client.go
@@ -100,6 +100,7 @@ func (c *Client) ExpectInitializeResponseAndCapabilities(t *testing.T) *dap.Init
 		SupportsConditionalBreakpoints:   true,
 		SupportsDelayedStackTraceLoading: true,
 		SupportTerminateDebuggee:         true,
+		SupportsExceptionInfoRequest:     true,
 	}
 	if !reflect.DeepEqual(initResp.Body, wantCapabilities) {
 		t.Errorf("capabilities in initializeResponse: got %+v, want %v", pretty(initResp.Body), pretty(wantCapabilities))
@@ -399,8 +400,10 @@ func (c *Client) CompletionsRequest() {
 }
 
 // ExceptionInfoRequest sends a 'exceptionInfo' request.
-func (c *Client) ExceptionInfoRequest() {
-	c.send(&dap.ExceptionInfoRequest{Request: *c.newRequest("exceptionInfo")})
+func (c *Client) ExceptionInfoRequest(threadID int) {
+	request := &dap.ExceptionInfoRequest{Request: *c.newRequest("exceptionInfo")}
+	request.Arguments.ThreadId = threadID
+	c.send(request)
 }
 
 // LoadedSourcesRequest sends a 'loadedSources' request.
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index 4dec14af..64a2ed75 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -47,7 +47,11 @@ func TestMain(m *testing.M) {
 
 // name is for _fixtures/<name>.go
 func runTest(t *testing.T, name string, test func(c *daptest.Client, f protest.Fixture)) {
-	var buildFlags protest.BuildFlags = protest.AllNonOptimized
+	runTestBuildFlags(t, name, test, protest.AllNonOptimized)
+}
+
+// name is for _fixtures/<name>.go
+func runTestBuildFlags(t *testing.T, name string, test func(c *daptest.Client, f protest.Fixture), buildFlags protest.BuildFlags) {
 	fixture := protest.BuildFixture(name, buildFlags)
 
 	// Start the DAP server.
@@ -364,39 +368,21 @@ func TestAttachStopOnEntry(t *testing.T) {
 		// 13 >> disconnect, << disconnect
 		client.DisconnectRequestWithKillOption(true)
 
-		// Both of these scenarios are somehow possible.
-		// Even though the program has an infininte loop,
-		// it apears that a halt can cause it to terminate.
-		// Since we are in async mode while running, we might receive messages in either order.
-		msg := client.ExpectMessage(t)
-		switch m := msg.(type) {
-		case *dap.StoppedEvent:
-			if m.Seq != 0 || m.Body.Reason != "pause" { // continue is interrupted
-				t.Errorf("\ngot %#v\nwant Seq=0 Reason='pause'", m)
-			}
-			oed := client.ExpectOutputEventDetachingKill(t)
-			if oed.Seq != 0 || oed.Body.Category != "console" {
-				t.Errorf("\ngot %#v\nwant Seq=0 Category='console'", oed)
-			}
-		case *dap.TerminatedEvent:
-			if m.Seq != 0 {
-				t.Errorf("\ngot %#v\nwant Seq=0'", m)
-			}
-			oep := client.ExpectOutputEventProcessExited(t, 0)
-			if oep.Seq != 0 || oep.Body.Category != "console" {
-				t.Errorf("\ngot %#v\nwant Seq=0 Category='console'", oep)
-			}
-			oed := client.ExpectOutputEventDetaching(t)
-			if oed.Seq != 0 || oed.Body.Category != "console" {
-				t.Errorf("\ngot %#v\nwant Seq=0 Category='console'", oed)
-			}
-		default:
-			t.Fatalf("got %#v, want StoppedEvent or TerminatedEvent", m)
-		}
-		dResp := client.ExpectDisconnectResponse(t)
-		if dResp.Seq != 0 || dResp.RequestSeq != 13 {
-			t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=13", dResp)
-		}
+		msg := expectMessageFilterStopped(t, client)
+		if _, ok := msg.(*dap.OutputEvent); !ok {
+			// want detach kill output message
+			t.Errorf("got %#v, want *dap.OutputEvent", msg)
+		}
+		msg = expectMessageFilterStopped(t, client)
+		if _, ok := msg.(*dap.DisconnectResponse); !ok {
+			t.Errorf("got %#v, want *dap.DisconnectResponse", msg)
+		}
+		// If this call to KeepAlive isn't here there's a chance that stdout will
+		// be garbage collected (since it is no longer alive long before this
+		// point), when that happens, on unix-like OSes, the read end of the pipe
+		// will be closed by the finalizer and the target process will die by
+		// SIGPIPE, which the rest of this test does not expect.
+		runtime.KeepAlive(stdout)
 	})
 }
 
@@ -437,9 +423,9 @@ func TestContinueOnEntry(t *testing.T) {
 				}
 				// Single current thread is sent when the program is running
 				// because DAP spec expects at least one thread.
-				// TODO(polina): accept empty already-terminated response here as well?
-				if len(m.Body.Threads) != 1 || m.Body.Threads[0].Id != -1 || m.Body.Threads[0].Name != "Current" {
-					t.Errorf("\ngot  %#v\nwant Id=-1, Name=\"Current\"", m.Body.Threads)
+				// Also accept empty already-terminated response.
+				if len(m.Body.Threads) != 0 && (len(m.Body.Threads) != 1 || m.Body.Threads[0].Id != -1 || m.Body.Threads[0].Name != "Current") {
+					t.Errorf("\ngot  %#v\nwant Id=-1, Name=\"Current\" or empty", m.Body.Threads)
 				}
 			case *dap.TerminatedEvent:
 			default:
@@ -737,6 +723,14 @@ func expectVarRegex(t *testing.T, got *dap.VariablesResponse, i int, name, evalN
 	return expectVar(t, got, i, name, evalName, value, typ, false, hasRef)
 }
 
+func expectMessageFilterStopped(t *testing.T, client *daptest.Client) dap.Message {
+	msg := client.ExpectMessage(t)
+	if _, isStopped := msg.(*dap.StoppedEvent); isStopped {
+		msg = client.ExpectMessage(t)
+	}
+	return msg
+}
+
 // validateEvaluateName issues an evaluate request with evaluateName of a variable and
 // confirms that it succeeds and returns the same variable record as the original.
 func validateEvaluateName(t *testing.T, client *daptest.Client, got *dap.VariablesResponse, i int) {
@@ -1383,6 +1377,63 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
 	})
 }
 
+// TestScopesRequestsOptimized executes to a breakpoint and tests different
+// that the names of the "Locals" and "Arguments" scopes are correctly annotated with
+// a warning about debugging an optimized function.
+func TestScopesRequestsOptimized(t *testing.T) {
+	runTestBuildFlags(t, "testvariables", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequestWithArgs(map[string]interface{}{
+					"mode": "exec", "program": fixture.Path, "showGlobalVariables": true,
+				})
+			},
+			// Breakpoints are set within the program
+			fixture.Source, []int{},
+			[]onBreakpoint{{
+				// Stop at first breakpoint
+				execute: func() {
+					client.StackTraceRequest(1, 0, 20)
+					stack := client.ExpectStackTraceResponse(t)
+
+					startLineno := 66
+					if runtime.GOOS == "windows" && goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) {
+						// Go1.15 on windows inserts a NOP after the call to
+						// runtime.Breakpoint and marks it same line as the
+						// runtime.Breakpoint call, making this flaky, so skip the line check.
+						startLineno = -1
+					}
+
+					expectStackFrames(t, stack, "main.foobar", startLineno, 1000, 4, 4)
+
+					client.ScopesRequest(1000)
+					scopes := client.ExpectScopesResponse(t)
+					expectScope(t, scopes, 0, "Arguments (warning: optimized function)", 1000)
+					expectScope(t, scopes, 1, "Locals (warning: optimized function)", 1001)
+					expectScope(t, scopes, 2, "Globals (package main)", 1002)
+				},
+				disconnect: false,
+			}, {
+				// Stop at second breakpoint
+				execute: func() {
+					// Frame ids get reset at each breakpoint.
+					client.StackTraceRequest(1, 0, 20)
+					stack := client.ExpectStackTraceResponse(t)
+					expectStackFrames(t, stack, "main.barfoo", 27, 1000, 5, 5)
+
+					client.ScopesRequest(1000)
+					scopes := client.ExpectScopesResponse(t)
+					expectScope(t, scopes, 0, "Arguments (warning: optimized function)", 1000)
+					expectScope(t, scopes, 1, "Locals (warning: optimized function)", 1001)
+					expectScope(t, scopes, 2, "Globals (package main)", 1002)
+				},
+				disconnect: false,
+			}})
+	},
+		protest.EnableOptimization)
+}
+
 // TestVariablesLoading exposes test cases where variables might be partiall or
 // fully unloaded.
 func TestVariablesLoading(t *testing.T) {
@@ -1778,6 +1829,32 @@ func TestLaunchRequestWithStackTraceDepth(t *testing.T) {
 	})
 }
 
+type Breakpoint struct {
+	line      int
+	path      string
+	verified  bool
+	msgPrefix string
+}
+
+func expectSetBreakpointsResponse(t *testing.T, client *daptest.Client, bps []Breakpoint) {
+	t.Helper()
+	checkSetBreakpointsResponse(t, client, bps, client.ExpectSetBreakpointsResponse(t))
+}
+
+func checkSetBreakpointsResponse(t *testing.T, client *daptest.Client, bps []Breakpoint, got *dap.SetBreakpointsResponse) {
+	t.Helper()
+	if len(got.Body.Breakpoints) != len(bps) {
+		t.Errorf("got %#v,\nwant len(Breakpoints)=%d", got, len(bps))
+		return
+	}
+	for i, bp := range got.Body.Breakpoints {
+		if bp.Line != bps[i].line || bp.Verified != bps[i].verified || bp.Source.Path != bps[i].path ||
+			!strings.HasPrefix(bp.Message, bps[i].msgPrefix) {
+			t.Errorf("got breakpoints[%d] = %#v, \nwant %#v", i, bp, bps[i])
+		}
+	}
+}
+
 // TestSetBreakpoint executes to a breakpoint and tests different
 // configurations of setBreakpoint requests.
 func TestSetBreakpoint(t *testing.T) {
@@ -1793,34 +1870,13 @@ func TestSetBreakpoint(t *testing.T) {
 				execute: func() {
 					handleStop(t, client, 1, "main.main", 16)
 
-					type Breakpoint struct {
-						line      int
-						path      string
-						verified  bool
-						msgPrefix string
-					}
-					expectSetBreakpointsResponse := func(bps []Breakpoint) {
-						t.Helper()
-						got := client.ExpectSetBreakpointsResponse(t)
-						if len(got.Body.Breakpoints) != len(bps) {
-							t.Errorf("got %#v,\nwant len(Breakpoints)=%d", got, len(bps))
-							return
-						}
-						for i, bp := range got.Body.Breakpoints {
-							if bp.Line != bps[i].line || bp.Verified != bps[i].verified || bp.Source.Path != bps[i].path ||
-								!strings.HasPrefix(bp.Message, bps[i].msgPrefix) {
-								t.Errorf("got breakpoints[%d] = %#v, \nwant %#v", i, bp, bps[i])
-							}
-						}
-					}
-
 					// Set two breakpoints at the next two lines in main
 					client.SetBreakpointsRequest(fixture.Source, []int{17, 18})
-					expectSetBreakpointsResponse([]Breakpoint{{17, fixture.Source, true, ""}, {18, fixture.Source, true, ""}})
+					expectSetBreakpointsResponse(t, client, []Breakpoint{{17, fixture.Source, true, ""}, {18, fixture.Source, true, ""}})
 
 					// Clear 17, reset 18
 					client.SetBreakpointsRequest(fixture.Source, []int{18})
-					expectSetBreakpointsResponse([]Breakpoint{{18, fixture.Source, true, ""}})
+					expectSetBreakpointsResponse(t, client, []Breakpoint{{18, fixture.Source, true, ""}})
 
 					// Skip 17, continue to 18
 					client.ContinueRequest(1)
@@ -1830,7 +1886,7 @@ func TestSetBreakpoint(t *testing.T) {
 
 					// Set another breakpoint inside the loop in loop(), twice to trigger error
 					client.SetBreakpointsRequest(fixture.Source, []int{8, 8})
-					expectSetBreakpointsResponse([]Breakpoint{{8, fixture.Source, true, ""}, {8, "", false, "Breakpoint exists"}})
+					expectSetBreakpointsResponse(t, client, []Breakpoint{{8, fixture.Source, true, ""}, {8, "", false, "Breakpoint exists"}})
 
 					// Continue into the loop
 					client.ContinueRequest(1)
@@ -1843,7 +1899,7 @@ func TestSetBreakpoint(t *testing.T) {
 
 					// Edit the breakpoint to add a condition
 					client.SetConditionalBreakpointsRequest(fixture.Source, []int{8}, map[int]string{8: "i == 3"})
-					expectSetBreakpointsResponse([]Breakpoint{{8, fixture.Source, true, ""}})
+					expectSetBreakpointsResponse(t, client, []Breakpoint{{8, fixture.Source, true, ""}})
 
 					// Continue until condition is hit
 					client.ContinueRequest(1)
@@ -1856,7 +1912,7 @@ func TestSetBreakpoint(t *testing.T) {
 
 					// Edit the breakpoint to remove a condition
 					client.SetConditionalBreakpointsRequest(fixture.Source, []int{8}, map[int]string{8: ""})
-					expectSetBreakpointsResponse([]Breakpoint{{8, fixture.Source, true, ""}})
+					expectSetBreakpointsResponse(t, client, []Breakpoint{{8, fixture.Source, true, ""}})
 
 					// Continue for one more loop iteration
 					client.ContinueRequest(1)
@@ -1869,7 +1925,7 @@ func TestSetBreakpoint(t *testing.T) {
 
 					// Set at a line without a statement
 					client.SetBreakpointsRequest(fixture.Source, []int{1000})
-					expectSetBreakpointsResponse([]Breakpoint{{1000, "", false, "could not find statement"}}) // all cleared, none set
+					expectSetBreakpointsResponse(t, client, []Breakpoint{{1000, "", false, "could not find statement"}}) // all cleared, none set
 				},
 				// The program has an infinite loop, so we must kill it by disconnecting.
 				disconnect: true,
@@ -1877,6 +1933,78 @@ func TestSetBreakpoint(t *testing.T) {
 	})
 }
 
+func expectSetBreakpointsResponseAndStoppedEvent(t *testing.T, client *daptest.Client) (se *dap.StoppedEvent, br *dap.SetBreakpointsResponse) {
+	for i := 0; i < 2; i++ {
+		switch m := client.ExpectMessage(t).(type) {
+		case *dap.StoppedEvent:
+			se = m
+		case *dap.SetBreakpointsResponse:
+			br = m
+		default:
+			t.Fatalf("Unexpected message type: expect StoppedEvent or SetBreakpointsResponse, got %#v", m)
+		}
+	}
+	if se == nil || br == nil {
+		t.Fatal("Expected StoppedEvent and SetBreakpointsResponse")
+	}
+	return se, br
+}
+
+func TestSetBreakpointWhileRunning(t *testing.T) {
+	runTest(t, "integrationprog", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			// Set breakpoints
+			fixture.Source, []int{16},
+			[]onBreakpoint{{
+				execute: func() {
+					// The program loops 3 times over lines 14-15-8-9-10-16
+					handleStop(t, client, 1, "main.main", 16) // Line that sleeps for 1 second
+
+					// We can set breakpoints while nexting
+					client.NextRequest(1)
+					client.ExpectNextResponse(t)
+					client.SetBreakpointsRequest(fixture.Source, []int{15}) // [16,] => [15,]
+					se, br := expectSetBreakpointsResponseAndStoppedEvent(t, client)
+					if se.Body.Reason != "pause" || !se.Body.AllThreadsStopped || se.Body.ThreadId != 0 && se.Body.ThreadId != 1 {
+						t.Errorf("\ngot  %#v\nwant Reason='pause' AllThreadsStopped=true ThreadId=0/1", se)
+					}
+					checkSetBreakpointsResponse(t, client, []Breakpoint{{15, fixture.Source, true, ""}}, br)
+					// Halt cancelled next, so if we continue we will not stop at line 14.
+					client.ContinueRequest(1)
+					client.ExpectContinueResponse(t)
+					se = client.ExpectStoppedEvent(t)
+					if se.Body.Reason != "breakpoint" || !se.Body.AllThreadsStopped || se.Body.ThreadId != 1 {
+						t.Errorf("\ngot  %#v\nwant Reason='breakpoint' AllThreadsStopped=true ThreadId=1", se)
+					}
+					handleStop(t, client, 1, "main.main", 15)
+
+					// We can set breakpoints while continuing
+					client.ContinueRequest(1)
+					client.ExpectContinueResponse(t)
+					client.SetBreakpointsRequest(fixture.Source, []int{9}) // [15,] => [9,]
+					se, br = expectSetBreakpointsResponseAndStoppedEvent(t, client)
+					if se.Body.Reason != "pause" || !se.Body.AllThreadsStopped || se.Body.ThreadId != 0 && se.Body.ThreadId != 1 {
+						t.Errorf("\ngot  %#v\nwant Reason='pause' AllThreadsStopped=true ThreadId=0/1", se)
+					}
+					checkSetBreakpointsResponse(t, client, []Breakpoint{{9, fixture.Source, true, ""}}, br)
+					client.ContinueRequest(1)
+					client.ExpectContinueResponse(t)
+					se = client.ExpectStoppedEvent(t)
+					if se.Body.Reason != "breakpoint" || !se.Body.AllThreadsStopped || se.Body.ThreadId != 1 {
+						t.Errorf("\ngot  %#v\nwant Reason='breakpoint' AllThreadsStopped=true ThreadId=1", se)
+					}
+					handleStop(t, client, 1, "main.sayhi", 9)
+
+				},
+				disconnect: true,
+			}})
+	})
+}
+
 // TestLaunchSubstitutePath sets a breakpoint using a path
 // that does not exist and expects the substitutePath attribute
 // in the launch configuration to take care of the mapping.
@@ -2560,14 +2688,16 @@ func TestBadAccess(t *testing.T) {
 
 					expectStoppedOnError := func(errorPrefix string) {
 						t.Helper()
-						oe := client.ExpectOutputEvent(t)
-						if oe.Body.Category != "stderr" || !strings.HasPrefix(oe.Body.Output, "ERROR: "+errorPrefix) {
-							t.Errorf("\ngot  %#v\nwant Category=\"stderr\" Output=\"%s ...\"", oe, errorPrefix)
-						}
 						se := client.ExpectStoppedEvent(t)
-						if se.Body.ThreadId != 1 || se.Body.Reason != "runtime error" || !strings.HasPrefix(se.Body.Text, errorPrefix) {
-							t.Errorf("\ngot  %#v\nwant ThreadId=1 Reason=\"runtime error\" Text=\"%s\"", se, errorPrefix)
+						if se.Body.ThreadId != 1 || se.Body.Reason != "exception" || se.Body.Description != "Paused on runtime error" || !strings.HasPrefix(se.Body.Text, errorPrefix) {
+							t.Errorf("\ngot  %#v\nwant ThreadId=1 Reason=\"exception\" Description=\"Paused on runtime error\" Text=\"%s\"", se, errorPrefix)
+						}
+						client.ExceptionInfoRequest(1)
+						eInfo := client.ExpectExceptionInfoResponse(t)
+						if eInfo.Body.ExceptionId != "runtime error" || !strings.HasPrefix(eInfo.Body.Description, errorPrefix) {
+							t.Errorf("\ngot  %#v\nwant ExceptionId=\"runtime error\" Text=\"%s\"", eInfo, errorPrefix)
 						}
+
 					}
 
 					client.ContinueRequest(1)
@@ -2612,8 +2742,14 @@ func TestPanicBreakpointOnContinue(t *testing.T) {
 					client.ExpectContinueResponse(t)
 
 					se := client.ExpectStoppedEvent(t)
-					if se.Body.ThreadId != 1 || se.Body.Reason != "panic" {
-						t.Errorf("\ngot  %#v\nwant ThreadId=1 Reason=\"panic\"", se)
+					if se.Body.ThreadId != 1 || se.Body.Reason != "exception" || se.Body.Description != "Paused on panic" {
+						t.Errorf("\ngot  %#v\nwant ThreadId=1 Reason=\"exception\" Description=\"Paused on panic\"", se)
+					}
+
+					client.ExceptionInfoRequest(1)
+					eInfo := client.ExpectExceptionInfoResponse(t)
+					if eInfo.Body.ExceptionId != "panic" || eInfo.Body.Description != "\"BOOM!\"" {
+						t.Errorf("\ngot  %#v\nwant ExceptionId=\"panic\" Description=\"\"BOOM!\"\"", eInfo)
 					}
 				},
 				disconnect: true,
@@ -2644,9 +2780,14 @@ func TestPanicBreakpointOnNext(t *testing.T) {
 					client.ExpectNextResponse(t)
 
 					se := client.ExpectStoppedEvent(t)
+					if se.Body.ThreadId != 1 || se.Body.Reason != "exception" || se.Body.Description != "Paused on panic" {
+						t.Errorf("\ngot  %#v\nwant ThreadId=1 Reason=\"exception\" Description=\"Paused on panic\"", se)
+					}
 
-					if se.Body.ThreadId != 1 || se.Body.Reason != "panic" {
-						t.Errorf("\ngot  %#v\nexpected ThreadId=1 Reason=\"panic\"", se)
+					client.ExceptionInfoRequest(1)
+					eInfo := client.ExpectExceptionInfoResponse(t)
+					if eInfo.Body.ExceptionId != "panic" || eInfo.Body.Description != "\"BOOM!\"" {
+						t.Errorf("\ngot  %#v\nwant ExceptionId=\"panic\" Description=\"\"BOOM!\"\"", eInfo)
 					}
 				},
 				disconnect: true,
@@ -2671,8 +2812,8 @@ func TestFatalThrowBreakpoint(t *testing.T) {
 					client.ExpectContinueResponse(t)
 
 					se := client.ExpectStoppedEvent(t)
-					if se.Body.Reason != "fatal error" {
-						t.Errorf("\ngot  %#v\nwant Reason=\"fatal error\"", se)
+					if se.Body.Reason != "exception" || se.Body.Description != "Paused on fatal error" {
+						t.Errorf("\ngot  %#v\nwant Reason=\"exception\" Description=\"Paused on fatal error\"", se)
 					}
 				},
 				disconnect: true,
@@ -3033,9 +3174,6 @@ func TestUnupportedCommandResponses(t *testing.T) {
 		client.CompletionsRequest()
 		expectUnsupportedCommand("completions")
 
-		client.ExceptionInfoRequest()
-		expectUnsupportedCommand("exceptionInfo")
-
 		client.DataBreakpointInfoRequest()
 		expectUnsupportedCommand("dataBreakpointInfo")
 
@@ -3144,14 +3282,6 @@ func TestBadLaunchRequests(t *testing.T) {
 			}
 		}
 
-		expectFailedToLaunchWithMessageRegex := func(response *dap.ErrorResponse, errmsg string) {
-			t.Helper()
-			expectFailedToLaunch(response)
-			if matched, _ := regexp.MatchString(errmsg, response.Body.Error.Format); !matched {
-				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.ExpectInvisibleErrorResponse(t),
@@ -3233,18 +3363,34 @@ func TestBadLaunchRequests(t *testing.T) {
 		expectFailedToLaunch(client.ExpectInvisibleErrorResponse(t)) // No such file or directory
 
 		client.LaunchRequest("debug", fixture.Path+"_does_not_exist", stopOnEntry)
+		oe := client.ExpectOutputEvent(t)
+		if !strings.HasPrefix(oe.Body.Output, "Build Error: ") || oe.Body.Category != "stderr" {
+			t.Errorf("got %#v, want Category=\"stderr\" Output=\"Build Error: ...\"", oe)
+		}
 		expectFailedToLaunch(client.ExpectInvisibleErrorResponse(t))
 
 		client.LaunchRequest("" /*debug by default*/, fixture.Path+"_does_not_exist", stopOnEntry)
+		oe = client.ExpectOutputEvent(t)
+		if !strings.HasPrefix(oe.Body.Output, "Build Error: ") || oe.Body.Category != "stderr" {
+			t.Errorf("got %#v, want Category=\"stderr\" Output=\"Build Error: ...\"", oe)
+		}
 		expectFailedToLaunch(client.ExpectInvisibleErrorResponse(t))
 
 		client.LaunchRequest("exec", fixture.Source, stopOnEntry)
 		expectFailedToLaunch(client.ExpectInvisibleErrorResponse(t)) // Not an executable
 
 		client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "buildFlags": "-bad -flags"})
-		expectFailedToLaunchWithMessageRegex(client.ExpectInvisibleErrorResponse(t), `Failed to launch: Build error: .*flag provided but not defined.*`)
+		oe = client.ExpectOutputEvent(t)
+		if !strings.HasPrefix(oe.Body.Output, "Build Error: ") || oe.Body.Category != "stderr" {
+			t.Errorf("got %#v, want Category=\"stderr\" Output=\"Build Error: ...\"", oe)
+		}
+		expectFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t), "Failed to launch: Build error: Check the debug console for details.")
 		client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "noDebug": true, "buildFlags": "-bad -flags"})
-		expectFailedToLaunchWithMessageRegex(client.ExpectInvisibleErrorResponse(t), `Failed to launch: Build error: .*flag provided but not defined.*`)
+		oe = client.ExpectOutputEvent(t)
+		if !strings.HasPrefix(oe.Body.Output, "Build Error: ") || oe.Body.Category != "stderr" {
+			t.Errorf("got %#v, want Category=\"stderr\" Output=\"Build Error: ...\"", oe)
+		}
+		expectFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t), "Failed to launch: Build error: Check the debug console for details.")
 
 		// Bad "wd".
 		client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "noDebug": false, "cwd": "dir/invalid"})