Repository: go-delve/delve
Pull request #2546 (2021-06-21 19:08:19)

service/dap: increase default string loading limit and refactor
This PR proposes a limit of 512 with a TODO to consider 1024. 
Vscode truncates display values at 1023, so any higher won't give additional viewing power.
For additional inspiration, the C/C++ vscode adapter appears to be limiting string loads at 1024.

updates https://github.com/golang/vscode-go/issues/1318



Patch:
diff --git a/_fixtures/longstrings.go b/_fixtures/longstrings.go
new file mode 100644
index 00000000..8ad14dbe
--- /dev/null
+++ b/_fixtures/longstrings.go
@@ -0,0 +1,22 @@
+package main
+
+import (
+	"runtime"
+)
+
+func buildString(length int) string {
+	s := ""
+	for i := 0; i < length; i++ {
+		s = s + "x"
+	}
+	return s
+}
+
+func main() {
+	s513 := buildString(513)
+	s1025 := buildString(1025)
+	s4097 := buildString(4097)
+	nested := map[int]string{513: s513, 1025: s1025, 4097: s4097}
+	runtime.Breakpoint()
+	_ = nested
+}
diff --git a/pkg/dwarf/line/line_parser.go b/pkg/dwarf/line/line_parser.go
index 09dd6de7..e330ae4c 100644
--- a/pkg/dwarf/line/line_parser.go
+++ b/pkg/dwarf/line/line_parser.go
@@ -39,6 +39,9 @@ type DebugLineInfo struct {
 	// lastMachineCache[pc] is a state machine stopped at an address after pc
 	lastMachineCache map[uint64]*StateMachine
 
+	// debugLineStr is the contents of the .debug_line_str section.
+	debugLineStr []byte
+
 	// staticBase is the address at which the executable is loaded, 0 for non-PIEs
 	staticBase uint64
 
@@ -59,7 +62,7 @@ type FileEntry struct {
 type DebugLines []*DebugLineInfo
 
 // ParseAll parses all debug_line segments found in data
-func ParseAll(data []byte, logfn func(string, ...interface{}), staticBase uint64, normalizeBackslash bool, ptrSize int) DebugLines {
+func ParseAll(data []byte, debugLineStr []byte, logfn func(string, ...interface{}), staticBase uint64, normalizeBackslash bool, ptrSize int) DebugLines {
 	var (
 		lines = make(DebugLines, 0)
 		buf   = bytes.NewBuffer(data)
@@ -67,7 +70,7 @@ func ParseAll(data []byte, logfn func(string, ...interface{}), staticBase uint64
 
 	// We have to parse multiple file name tables here.
 	for buf.Len() > 0 {
-		lines = append(lines, Parse("", buf, logfn, staticBase, normalizeBackslash, ptrSize))
+		lines = append(lines, Parse("", buf, debugLineStr, logfn, staticBase, normalizeBackslash, ptrSize))
 	}
 
 	return lines
@@ -75,9 +78,12 @@ func ParseAll(data []byte, logfn func(string, ...interface{}), staticBase uint64
 
 // Parse parses a single debug_line segment from buf. Compdir is the
 // DW_AT_comp_dir attribute of the associated compile unit.
-func Parse(compdir string, buf *bytes.Buffer, logfn func(string, ...interface{}), staticBase uint64, normalizeBackslash bool, ptrSize int) *DebugLineInfo {
+func Parse(compdir string, buf *bytes.Buffer, debugLineStr []byte, logfn func(string, ...interface{}), staticBase uint64, normalizeBackslash bool, ptrSize int) *DebugLineInfo {
 	dbl := new(DebugLineInfo)
 	dbl.Logf = logfn
+	if logfn == nil {
+		dbl.Logf = func(string, ...interface{}) {}
+	}
 	dbl.staticBase = staticBase
 	dbl.ptrSize = ptrSize
 	dbl.Lookup = make(map[string]*FileEntry)
@@ -86,6 +92,7 @@ func Parse(compdir string, buf *bytes.Buffer, logfn func(string, ...interface{})
 	dbl.stateMachineCache = make(map[uint64]*StateMachine)
 	dbl.lastMachineCache = make(map[uint64]*StateMachine)
 	dbl.normalizeBackslash = normalizeBackslash
+	dbl.debugLineStr = debugLineStr
 
 	parseDebugLinePrologue(dbl, buf)
 	if dbl.Prologue.Version >= 5 {
@@ -123,10 +130,9 @@ func parseDebugLinePrologue(dbl *DebugLineInfo, buf *bytes.Buffer) {
 		dbl.ptrSize += int(buf.Next(1)[0]) // segment_selector_size
 	}
 
-	// Version 4 or earlier
 	p.Length = binary.LittleEndian.Uint32(buf.Next(4))
 	p.MinInstrLength = uint8(buf.Next(1)[0])
-	if p.Version == 4 {
+	if p.Version >= 4 {
 		p.MaxOpPerInstr = uint8(buf.Next(1)[0])
 	} else {
 		p.MaxOpPerInstr = 1
@@ -174,10 +180,14 @@ func parseIncludeDirs5(info *DebugLineInfo, buf *bytes.Buffer) bool {
 		for dirEntryFormReader.next(buf) {
 			switch dirEntryFormReader.contentType {
 			case _DW_LNCT_path:
-				if dirEntryFormReader.formCode != _DW_FORM_string {
+				switch dirEntryFormReader.formCode {
+				case _DW_FORM_string:
 					info.IncludeDirs = append(info.IncludeDirs, dirEntryFormReader.str)
-				} else {
-					//TODO(aarzilli): support debug_string, debug_line_str
+				case _DW_FORM_line_strp:
+					buf := bytes.NewBuffer(info.debugLineStr[dirEntryFormReader.u64:])
+					dir, _ := util.ParseString(buf)
+					info.IncludeDirs = append(info.IncludeDirs, dir)
+				default:
 					info.Logf("unsupported string form %#x", dirEntryFormReader.formCode)
 				}
 			case _DW_LNCT_directory_index:
@@ -277,10 +287,13 @@ func parseFileEntries5(info *DebugLineInfo, buf *bytes.Buffer) bool {
 
 			switch fileEntryFormReader.contentType {
 			case _DW_LNCT_path:
-				if fileEntryFormReader.formCode != _DW_FORM_string {
+				switch fileEntryFormReader.formCode {
+				case _DW_FORM_string:
 					p = fileEntryFormReader.str
-				} else {
-					//TODO(aarzilli): support debug_string, debug_line_str
+				case _DW_FORM_line_strp:
+					buf := bytes.NewBuffer(info.debugLineStr[fileEntryFormReader.u64:])
+					p, _ = util.ParseString(buf)
+				default:
 					info.Logf("unsupported string form %#x", fileEntryFormReader.formCode)
 				}
 			case _DW_LNCT_directory_index:
diff --git a/pkg/dwarf/line/parse_util.go b/pkg/dwarf/line/parse_util.go
index 6f96569e..867d9b7b 100644
--- a/pkg/dwarf/line/parse_util.go
+++ b/pkg/dwarf/line/parse_util.go
@@ -165,13 +165,14 @@ func (rdr *formReader) next(buf *bytes.Buffer) bool {
 		}
 		rdr.u64 = uint64(binary.LittleEndian.Uint32(append(buf.Next(3), 0x0)))
 
+	case ^uint64(0):
+		// do nothing
+
 	default:
 		if rdr.logf != nil {
 			rdr.logf("unknown form code %#x", rdr.formCode)
 		}
 		rdr.formCodes[rdr.nexti] = ^uint64(0) // only print error once
-	case ^uint64(0):
-		// do nothing
 	}
 
 	rdr.nexti++
diff --git a/pkg/dwarf/line/state_machine.go b/pkg/dwarf/line/state_machine.go
index d7e11c26..04501a97 100644
--- a/pkg/dwarf/line/state_machine.go
+++ b/pkg/dwarf/line/state_machine.go
@@ -103,9 +103,13 @@ func newStateMachine(dbl *DebugLineInfo, instructions []byte, ptrSize int) *Stat
 	for op := range standardopcodes {
 		opcodes[op] = standardopcodes[op]
 	}
+	var file string
+	if len(dbl.FileNames) > 0 {
+		file = dbl.FileNames[0].Path
+	}
 	sm := &StateMachine{
 		dbl:         dbl,
-		file:        dbl.FileNames[0].Path,
+		file:        file,
 		line:        1,
 		buf:         bytes.NewBuffer(instructions),
 		opcodes:     opcodes,
diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go
index b8af4e91..a7e0ea69 100644
--- a/pkg/proc/bininfo.go
+++ b/pkg/proc/bininfo.go
@@ -645,11 +645,12 @@ type Image struct {
 	closer         io.Closer
 	sepDebugCloser io.Closer
 
-	dwarf       *dwarf.Data
-	dwarfReader *dwarf.Reader
-	loclist2    *loclist.Dwarf2Reader
-	loclist5    *loclist.Dwarf5Reader
-	debugAddr   *godwarf.DebugAddrSection
+	dwarf        *dwarf.Data
+	dwarfReader  *dwarf.Reader
+	loclist2     *loclist.Dwarf2Reader
+	loclist5     *loclist.Dwarf5Reader
+	debugAddr    *godwarf.DebugAddrSection
+	debugLineStr []byte
 
 	typeCache map[dwarf.Offset]godwarf.Type
 
@@ -1202,6 +1203,8 @@ func loadBinaryInfoElf(bi *BinaryInfo, image *Image, path string, addr uint64, w
 	image.loclist5 = loclist.NewDwarf5Reader(debugLoclistBytes)
 	debugAddrBytes, _ := godwarf.GetDebugSectionElf(dwarfFile, "addr")
 	image.debugAddr = godwarf.ParseAddr(debugAddrBytes)
+	debugLineStrBytes, _ := godwarf.GetDebugSectionElf(dwarfFile, "line_str")
+	image.debugLineStr = debugLineStrBytes
 
 	wg.Add(3)
 	go bi.parseDebugFrameElf(image, dwarfFile, debugInfoBytes, wg)
@@ -1723,7 +1726,7 @@ func (bi *BinaryInfo) loadDebugInfoMaps(image *Image, debugInfoBytes, debugLineB
 						logger.Printf(fmt, args)
 					}
 				}
-				cu.lineInfo = line.Parse(compdir, bytes.NewBuffer(debugLineBytes[lineInfoOffset:]), logfn, image.StaticBase, bi.GOOS == "windows", bi.Arch.PtrSize())
+				cu.lineInfo = line.Parse(compdir, bytes.NewBuffer(debugLineBytes[lineInfoOffset:]), image.debugLineStr, logfn, image.StaticBase, bi.GOOS == "windows", bi.Arch.PtrSize())
 			}
 			cu.producer, _ = entry.Val(dwarf.AttrProducer).(string)
 			if cu.isgo && cu.producer != "" {
diff --git a/pkg/proc/interface.go b/pkg/proc/interface.go
index 3b8f17c6..8a4cf606 100644
--- a/pkg/proc/interface.go
+++ b/pkg/proc/interface.go
@@ -26,6 +26,10 @@ type Process interface {
 // the `proc` package.
 // This is temporary and in support of an ongoing refactor.
 type ProcessInternal interface {
+	// Valid returns true if this Process can be used. When it returns false it
+	// also returns an error describing why the Process is invalid (either
+	// ErrProcessExited or ErrProcessDetached).
+	Valid() (bool, error)
 	// Restart restarts the recording from the specified position, or from the
 	// last checkpoint if pos == "".
 	// If pos starts with 'c' it's a checkpoint ID, otherwise it's an event
@@ -87,10 +91,6 @@ type Info interface {
 	// ResumeNotify specifies a channel that will be closed the next time
 	// ContinueOnce finishes resuming the target.
 	ResumeNotify(chan<- struct{})
-	// Valid returns true if this Process can be used. When it returns false it
-	// also returns an error describing why the Process is invalid (either
-	// ErrProcessExited or ErrProcessDetached).
-	Valid() (bool, error)
 	BinInfo() *BinaryInfo
 	EntryPoint() (uint64, error)
 
diff --git a/pkg/proc/target.go b/pkg/proc/target.go
index a923bec6..fc61a7f0 100644
--- a/pkg/proc/target.go
+++ b/pkg/proc/target.go
@@ -62,6 +62,10 @@ type Target struct {
 	// This must be cleared whenever the target is resumed.
 	gcache goroutineCache
 	iscgo  *bool
+
+	// exitStatus is the exit status of the process we are debugging.
+	// Saved here to relay to any future commands.
+	exitStatus int
 }
 
 // ErrProcessExited indicates that the process has exited and contains both
@@ -203,6 +207,20 @@ func (t *Target) IsCgo() bool {
 	return false
 }
 
+// Valid returns true if this Process can be used. When it returns false it
+// also returns an error describing why the Process is invalid (either
+// ErrProcessExited or ErrProcessDetached).
+func (t *Target) Valid() (bool, error) {
+	ok, err := t.proc.Valid()
+	if !ok && err != nil {
+		if pe, ok := err.(ErrProcessExited); ok {
+			pe.Status = t.exitStatus
+			err = pe
+		}
+	}
+	return ok, err
+}
+
 // SupportsFunctionCalls returns whether or not the backend supports
 // calling functions during a debug session.
 // Currently only non-recorded processes running on AMD64 support
diff --git a/pkg/proc/target_exec.go b/pkg/proc/target_exec.go
index 068dee9a..0106c5b6 100644
--- a/pkg/proc/target_exec.go
+++ b/pkg/proc/target_exec.go
@@ -83,6 +83,9 @@ func (dbp *Target) Continue() error {
 					dbp.selectedGoroutine, _ = GetG(curth)
 				}
 			}
+			if pe, ok := err.(ErrProcessExited); ok {
+				dbp.exitStatus = pe.Status
+			}
 			return err
 		}
 		if dbp.StopReason == StopLaunched {
diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go
index f025285d..8c4e36d3 100644
--- a/pkg/terminal/command.go
+++ b/pkg/terminal/command.go
@@ -1308,7 +1308,7 @@ func scopePrefixSwitch(t *Term, ctx callContext) error {
 
 func exitedToError(state *api.DebuggerState, err error) (*api.DebuggerState, error) {
 	if err == nil && state.Exited {
-		return nil, fmt.Errorf("Process has exited with status %d", state.ExitStatus)
+		return nil, fmt.Errorf("Process %d has exited with status %d", state.Pid, state.ExitStatus)
 	}
 	return state, err
 }
diff --git a/service/api/types.go b/service/api/types.go
index f9163237..0767f457 100644
--- a/service/api/types.go
+++ b/service/api/types.go
@@ -17,6 +17,8 @@ var ErrNotExecutable = errors.New("not an executable file")
 
 // DebuggerState represents the current context of the debugger.
 type DebuggerState struct {
+	// PID of the process we are debugging.
+	Pid int
 	// Running is true if the process is running and no other information can be collected.
 	Running bool
 	// Recording is true if the process is currently being recorded and no other
diff --git a/service/dap/server.go b/service/dap/server.go
index 9403913d..d5e1aeff 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -165,18 +165,31 @@ type dapClientCapabilites struct {
 	supportsProgressReporting    bool
 }
 
-// DefaultLoadConfig controls how variables are loaded from the target's memory, borrowing the
-// default value from the original vscode-go debug adapter and rpc server.
-// With dlv-dap, users currently do not have a way to adjust these.
-// TODO(polina): Support setting config via launch/attach args or only rely on on-demand loading?
+// DefaultLoadConfig controls how variables are loaded from the target's memory.
+// These limits are conservative to minimize performace overhead for bulk loading.
+// With dlv-dap, users do not have a way to adjust these.
+// Instead we are focusing in interacive loading with nested reloads, array/map
+// paging and context-specific string limits.
 var DefaultLoadConfig = proc.LoadConfig{
 	FollowPointers:     true,
 	MaxVariableRecurse: 1,
-	MaxStringLen:       64,
-	MaxArrayValues:     64,
-	MaxStructFields:    -1,
+	// TODO(polina): consider 1024 limit instead:
+	// - vscode+C appears to use 1024 as the load limit
+	// - vscode viewlet hover truncates at 1023 characters
+	MaxStringLen:    512,
+	MaxArrayValues:  64,
+	MaxStructFields: -1,
 }
 
+const (
+	// When a user examines a single string, we can relax the loading limit.
+	maxSingleStringLen = 4 << 10 // 4096
+	// Results of a call are single-use and transient. We need to maximize
+	// what is presented. A common use case of a call injection is to
+	// stringify complex data conveniently.
+	maxStringLenInCallRetVars = 1 << 10 // 1024
+)
+
 // NewServer creates a new DAP Server. It takes an opened Listener
 // via config and assumes its ownership. config.DisconnectChan has to be set;
 // it will be closed by the server when the client fails to connect,
@@ -1794,9 +1807,9 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab
 					Value:        val,
 				}
 				if keyref != 0 { // key is a type to be expanded
-					if len(key) > DefaultLoadConfig.MaxStringLen {
+					if len(key) > maxMapKeyValueLen {
 						// Truncate and make unique
-						kvvar.Name = fmt.Sprintf("%s... @ %#x", key[0:DefaultLoadConfig.MaxStringLen], keyv.Addr)
+						kvvar.Name = fmt.Sprintf("%s... @ %#x", key[0:maxMapKeyValueLen], keyv.Addr)
 					}
 					kvvar.VariablesReference = keyref
 					kvvar.IndexedVariables = getIndexedVariableCount(keyv)
@@ -1890,9 +1903,12 @@ func (s *Server) convertVariableToString(v *proc.Variable) string {
 	return val
 }
 
-// defaultMaxValueLen is the max length of a string representation of a compound or reference
-// type variable.
-const defaultMaxValueLen = 1 << 8 // 256
+const (
+	// Limit the length of a string representation of a compound or reference type variable.
+	maxVarValueLen = 1 << 8 // 256
+	// Limit the length of an inlined map key.
+	maxMapKeyValueLen = 64
+)
 
 // Flags for convertVariableWithOpts option.
 type convertVariableFlags uint8
@@ -2048,8 +2064,8 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
 	// By default, only values of variables that have children can be truncated.
 	// If showFullValue is set, then all value strings are not truncated.
 	canTruncateValue := showFullValue&opts == 0
-	if len(value) > defaultMaxValueLen && canTruncateValue && canHaveRef {
-		value = value[:defaultMaxValueLen] + "..."
+	if len(value) > maxVarValueLen && canTruncateValue && canHaveRef {
+		value = value[:maxVarValueLen] + "..."
 	}
 	return value, variablesReference
 }
@@ -2113,9 +2129,9 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
 		case "repl", "variables", "hover", "clipboard":
 			if exprVar.Kind == reflect.String {
 				if strVal := constant.StringVal(exprVar.Value); exprVar.Len > int64(len(strVal)) {
-					// reload string value with a bigger limit.
+					// Reload the string value with a bigger limit.
 					loadCfg := DefaultLoadConfig
-					loadCfg.MaxStringLen = 4 << 10
+					loadCfg.MaxStringLen = maxSingleStringLen
 					if v, err := s.debugger.EvalVariableInScope(goid, frame, 0, request.Arguments.Expression, loadCfg); err != nil {
 						s.log.Debugf("Failed to load more for %v: %v", request.Arguments.Expression, err)
 					} else {
@@ -2152,13 +2168,11 @@ func (s *Server) doCall(goid, frame int, expr string) (*api.DebuggerState, []*pr
 	}
 	// The return values of injected function calls are volatile.
 	// Load as much useful data as possible.
-	// - String: a common use case of call injection is to stringify complex
-	// data conveniently. That can easily exceed the default limit, 64.
 	// TODO: investigate whether we need to increase other limits. For example,
 	// the return value is a pointer to a temporary object, which can become
 	// invalid by other injected function calls. Do we care about such use cases?
 	loadCfg := DefaultLoadConfig
-	loadCfg.MaxStringLen = 1 << 10
+	loadCfg.MaxStringLen = maxStringLenInCallRetVars
 
 	// TODO(polina): since call will resume execution of all goroutines,
 	// we should do this asynchronously and send a continued event to the
diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go
index 64e73645..6b3d29c2 100644
--- a/service/debugger/debugger.go
+++ b/service/debugger/debugger.go
@@ -1189,11 +1189,12 @@ func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struc
 	}
 
 	if err != nil {
-		if exitedErr, exited := err.(proc.ErrProcessExited); command.Name != api.SwitchGoroutine && command.Name != api.SwitchThread && exited {
+		if pe, ok := err.(proc.ErrProcessExited); ok && command.Name != api.SwitchGoroutine && command.Name != api.SwitchThread {
 			state := &api.DebuggerState{}
+			state.Pid = d.target.Pid()
 			state.Exited = true
-			state.ExitStatus = exitedErr.Status
-			state.Err = errors.New(exitedErr.Error())
+			state.ExitStatus = pe.Status
+			state.Err = pe
 			return state, nil
 		}
 		return nil, err


Test patch:
diff --git a/_fixtures/testvariables2.go b/_fixtures/testvariables2.go
index a8a4ced7..57174831 100644
--- a/_fixtures/testvariables2.go
+++ b/_fixtures/testvariables2.go
@@ -329,7 +329,7 @@ func main() {
 	longstr := "very long string 0123456789a0123456789b0123456789c0123456789d0123456789e0123456789f0123456789g012345678h90123456789i0123456789j0123456789"
 	m5 := map[C]int{{longstr}: 1}
 	m6 := map[string]int{longstr: 123}
-	m7 := map[C]C{{longstr}: {longstr}}
+	m7 := map[C]C{{longstr}: {"hello"}}
 	cl := C{s: longstr}
 	var nilstruct *astruct = nil
 
diff --git a/pkg/dwarf/line/line_parser_test.go b/pkg/dwarf/line/line_parser_test.go
index cbb5b71c..043a16bf 100644
--- a/pkg/dwarf/line/line_parser_test.go
+++ b/pkg/dwarf/line/line_parser_test.go
@@ -74,7 +74,7 @@ func ptrSizeByRuntimeArch() int {
 
 func testDebugLinePrologueParser(p string, t *testing.T) {
 	data := grabDebugLineSection(p, t)
-	debugLines := ParseAll(data, nil, 0, true, ptrSizeByRuntimeArch())
+	debugLines := ParseAll(data, nil, nil, 0, true, ptrSizeByRuntimeArch())
 	mainFileFound := false
 
 	for _, dbl := range debugLines {
@@ -181,7 +181,7 @@ func BenchmarkLineParser(b *testing.B) {
 
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
-		_ = ParseAll(data, nil, 0, true, ptrSizeByRuntimeArch())
+		_ = ParseAll(data, nil, nil, 0, true, ptrSizeByRuntimeArch())
 	}
 }
 
@@ -196,7 +196,7 @@ func loadBenchmarkData(tb testing.TB) DebugLines {
 		tb.Fatal("Could not read test data", err)
 	}
 
-	return ParseAll(data, nil, 0, true, ptrSizeByRuntimeArch())
+	return ParseAll(data, nil, nil, 0, true, ptrSizeByRuntimeArch())
 }
 
 func BenchmarkStateMachine(b *testing.B) {
@@ -316,7 +316,7 @@ func TestDebugLineC(t *testing.T) {
 		t.Fatal("Could not read test data", err)
 	}
 
-	parsed := ParseAll(data, nil, 0, true, ptrSizeByRuntimeArch())
+	parsed := ParseAll(data, nil, nil, 0, true, ptrSizeByRuntimeArch())
 
 	if len(parsed) == 0 {
 		t.Fatal("Parser result is empty")
@@ -365,7 +365,7 @@ func TestDebugLineDwarf4(t *testing.T) {
 		t.Fatal("Could not read test data", err)
 	}
 
-	debugLines := ParseAll(data, nil, 0, true, 8)
+	debugLines := ParseAll(data, nil, nil, 0, true, 8)
 
 	for _, dbl := range debugLines {
 		if dbl.Prologue.Version == 4 {
diff --git a/pkg/dwarf/line/state_machine_test.go b/pkg/dwarf/line/state_machine_test.go
index 24e62762..b7281151 100644
--- a/pkg/dwarf/line/state_machine_test.go
+++ b/pkg/dwarf/line/state_machine_test.go
@@ -74,7 +74,7 @@ func TestGrafana(t *testing.T) {
 		}
 		cuname, _ := e.Val(dwarf.AttrName).(string)
 
-		lineInfo := Parse(e.Val(dwarf.AttrCompDir).(string), debugLineBuffer, t.Logf, 0, false, 8)
+		lineInfo := Parse(e.Val(dwarf.AttrCompDir).(string), debugLineBuffer, nil, t.Logf, 0, false, 8)
 		lineInfo.endSeqIsValid = true
 		sm := newStateMachine(lineInfo, lineInfo.Instructions, 8)
 
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 194f552b..a455a445 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -3200,7 +3200,7 @@ func stacktraceCheck(t *testing.T, tc []string, frames []proc.Stackframe) []int
 
 func frameInFile(frame proc.Stackframe, file string) bool {
 	for _, loc := range []proc.Location{frame.Current, frame.Call} {
-		if !strings.HasSuffix(loc.File, "/"+file) && !strings.HasSuffix(loc.File, "\\"+file) {
+		if !strings.HasSuffix(loc.File, file) && !strings.HasSuffix(loc.File, "/"+file) && !strings.HasSuffix(loc.File, "\\"+file) {
 			return false
 		}
 		if loc.Line <= 0 {
@@ -3338,7 +3338,7 @@ func TestCgoSources(t *testing.T) {
 		for _, needle := range []string{"main.go", "hello.c"} {
 			found := false
 			for _, k := range sources {
-				if strings.HasSuffix(k, "/"+needle) || strings.HasSuffix(k, "\\"+needle) {
+				if strings.HasSuffix(k, needle) || strings.HasSuffix(k, "/"+needle) || strings.HasSuffix(k, "\\"+needle) {
 					found = true
 					break
 				}
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index e010be34..b1670435 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -682,7 +682,7 @@ func checkVar(t *testing.T, got *dap.VariablesResponse, i int, name, evalName, v
 		}
 	}
 	if i < 0 {
-		t.Errorf("\ngot  %#v\nwant Variables[i].Name=%q", got, name)
+		t.Errorf("\ngot  %#v\nwant Variables[i].Name=%q (not found)", got, name)
 		return 0
 	}
 
@@ -1379,7 +1379,7 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
 					checkVarExact(t, locals, -1, "emptyslice", "emptyslice", "[]string len: 0, cap: 0, []", "[]string", noChildren)
 					checkVarExact(t, locals, -1, "nilslice", "nilslice", "[]int len: 0, cap: 0, nil", "[]int", noChildren)
 					// reflect.Kind == String
-					checkVarExact(t, locals, -1, "longstr", "longstr", `"very long string 0123456789a0123456789b0123456789c0123456789d012...+73 more"`, "string", noChildren)
+					checkVarExact(t, locals, -1, "longstr", "longstr", longstr, "string", noChildren)
 					// reflect.Kind == Struct
 					checkVarExact(t, locals, -1, "zsvar", "zsvar", "struct {} {}", "struct {}", noChildren)
 					// reflect.Kind == UnsafePointer
@@ -1476,8 +1476,12 @@ func TestVariablesLoading(t *testing.T) {
 			}, {
 				execute: func() {
 					// Change default config values to trigger certain unloaded corner cases
+					saveDefaultConfig := DefaultLoadConfig
 					DefaultLoadConfig.MaxStructFields = 5
-					defer func() { DefaultLoadConfig.MaxStructFields = -1 }()
+					DefaultLoadConfig.MaxStringLen = 64
+					defer func() {
+						DefaultLoadConfig = saveDefaultConfig
+					}()
 
 					client.StackTraceRequest(1, 0, 0)
 					client.ExpectStackTraceResponse(t)
@@ -1489,7 +1493,8 @@ func TestVariablesLoading(t *testing.T) {
 					locals := client.ExpectVariablesResponse(t)
 
 					// String partially missing based on LoadConfig.MaxStringLen
-					checkVarExact(t, locals, -1, "longstr", "longstr", "\"very long string 0123456789a0123456789b0123456789c0123456789d012...+73 more\"", "string", noChildren)
+					// See also TestVariableLoadingOfLongStrings
+					checkVarExact(t, locals, -1, "longstr", "longstr", longstrLoaded64, "string", noChildren)
 
 					checkArrayChildren := func(t *testing.T, longarr *dap.VariablesResponse, parentName string, start int) {
 						t.Helper()
@@ -2827,7 +2832,7 @@ func TestEvaluateRequest(t *testing.T) {
 // From testvariables2 fixture
 const (
 	// As defined in the code
-	longstrFull = `"very long string 0123456789a0123456789b0123456789c0123456789d0123456789e0123456789f0123456789g012345678h90123456789i0123456789j0123456789"`
+	longstr = `"very long string 0123456789a0123456789b0123456789c0123456789d0123456789e0123456789f0123456789g012345678h90123456789i0123456789j0123456789"`
 	// Loaded with MaxStringLen=64
 	longstrLoaded64   = `"very long string 0123456789a0123456789b0123456789c0123456789d012...+73 more"`
 	longstrLoaded64re = `\"very long string 0123456789a0123456789b0123456789c0123456789d012\.\.\.\+73 more\"`
@@ -2880,28 +2885,27 @@ func TestVariableValueTruncation(t *testing.T) {
 
 					// Compound map keys may be truncated even further
 					// As the keys are always inside of a map container,
-					// this applies to variable requests.
+					// this applies to variables requests only, not evalute requests.
 
 					// key - compound, value - scalar (inlined key:value display) => truncate key if too long
-					ref := checkVarExact(t, locals, -1, "m5", "m5", "map[main.C]int [{s: "+longstrLoaded64+"}: 1, ]", "map[main.C]int", hasChildren)
+					ref := checkVarExact(t, locals, -1, "m5", "m5", "map[main.C]int [{s: "+longstr+"}: 1, ]", "map[main.C]int", hasChildren)
 					if ref > 0 {
 						client.VariablesRequest(ref)
 						// Key format: <truncated>... @<address>
 						checkVarRegex(t, client.ExpectVariablesResponse(t), 0, `main\.C {s: "very long string 0123456789.+\.\.\. @ 0x[0-9a-f]+`, `m5\[\(\*\(\*"main\.C"\)\(0x[0-9a-f]+\)\)\]`, "1", `int`, hasChildren)
 					}
-					// key - scalar, value - scalar (inlined key:value display) => not truncated
-					ref = checkVarExact(t, locals, -1, "m6", "m6", "map[string]int ["+longstrLoaded64+": 123, ]", "map[string]int", hasChildren)
+					// key - scalar, value - scalar (inlined key:value display) => key not truncated
+					ref = checkVarExact(t, locals, -1, "m6", "m6", "map[string]int ["+longstr+": 123, ]", "map[string]int", hasChildren)
 					if ref > 0 {
 						client.VariablesRequest(ref)
-						checkVarRegex(t, client.ExpectVariablesResponse(t), 0, longstrLoaded64re, `m6[\(\*\(\*\"string\"\)\(0x[0-9a-f]+\)\)]`, "123", "string: int", noChildren)
+						checkVarExact(t, client.ExpectVariablesResponse(t), 0, longstr, `m6[`+longstr+`]`, "123", "string: int", noChildren)
 					}
-					// key - compound, value - compound (array-like display) => not truncated
-					ref = checkVarExact(t, locals, -1, "m7", "m7", "map[main.C]main.C [{s: "+longstrLoaded64+"}: {s: "+longstrLoaded64+"}, ]", "map[main.C]main.C", hasChildren)
+					// key - compound, value - compound (array-like display) => key not truncated
+					ref = checkVarExact(t, locals, -1, "m7", "m7", "map[main.C]main.C [{s: "+longstr+"}: {s: \"hello\"}, ]", "map[main.C]main.C", hasChildren)
 					if ref > 0 {
 						client.VariablesRequest(ref)
 						m7 := client.ExpectVariablesResponse(t)
-						checkVarRegex(t, m7, 0, "[key 0]", `\(\*\(\*\"main\.C\"\)\(0x[0-9a-f]+\)\)`, `main\.C {s: `+longstrLoaded64re+`}`, `main\.C`, hasChildren)
-						checkVarRegex(t, m7, 1, "[val 0]", `\(\*\(\*\"main\.C\"\)\(0x[0-9a-f]+\)\)`, `main\.C {s: `+longstrLoaded64re+`}`, `main\.C`, hasChildren)
+						checkVarRegex(t, m7, 0, "[key 0]", `\(\*\(\*\"main\.C\"\)\(0x[0-9a-f]+\)\)`, `main\.C {s: `+longstr+`}`, `main\.C`, hasChildren)
 					}
 				},
 				disconnect: true,
@@ -2909,16 +2913,17 @@ func TestVariableValueTruncation(t *testing.T) {
 	})
 }
 
-// TestVariableLoadingOfLargeStrings tests that different string loading limits
+// TestVariableLoadingOfLongStrings tests that different string loading limits
 // apply that depending on the context.
-func TestVariableLoadingOfLargeStrings(t *testing.T) {
-	runTest(t, "testvariables2", func(client *daptest.Client, fixture protest.Fixture) {
+func TestVariableLoadingOfLongStrings(t *testing.T) {
+	protest.MustSupportFunctionCalls(t, testBackend)
+	runTest(t, "longstrings", func(client *daptest.Client, fixture protest.Fixture) {
 		runDebugSessionWithBPs(t, client, "launch",
 			// Launch
 			func() {
 				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
 			},
-			// Breakpoints are set within the program
+			// Breakpoint set within the program
 			fixture.Source, []int{},
 			[]onBreakpoint{{
 				execute: func() {
@@ -2927,29 +2932,56 @@ func TestVariableLoadingOfLargeStrings(t *testing.T) {
 					client.VariablesRequest(1001) // Locals
 					locals := client.ExpectVariablesResponse(t)
 
-					// Limits vary for evaluate requests
-					for _, evalContext := range []string{"", "watch", "repl", "variables", "hover", "clipboard", "somethingelse"} {
-						t.Run(evalContext, func(t *testing.T) {
-							// long string by itself (limits vary)
-							client.EvaluateRequest("longstr", 0, evalContext)
-							got := client.ExpectEvaluateResponse(t)
-							want := longstrLoaded64
-							switch evalContext {
-							case "repl", "variables", "hover", "clipboard":
-								want = longstrFull
+					// Limits vary for evaluate requests with different contexts
+					tests := []struct {
+						context string
+						limit   int
+					}{
+						{"", DefaultLoadConfig.MaxStringLen},
+						{"watch", DefaultLoadConfig.MaxStringLen},
+						{"repl", maxSingleStringLen},
+						{"hover", maxSingleStringLen},
+						{"variables", maxSingleStringLen},
+						{"clipboard", maxSingleStringLen},
+						{"somethingelse", DefaultLoadConfig.MaxStringLen},
+					}
+					for _, tc := range tests {
+						t.Run(tc.context, func(t *testing.T) {
+							// Long string by itself (limits vary)
+							client.EvaluateRequest("s4097", 0, tc.context)
+							want := fmt.Sprintf(`"x+\.\.\.\+%d more"`, 4097-tc.limit)
+							checkEvalRegex(t, client.ExpectEvaluateResponse(t), want, noChildren)
+
+							// Evaluated container variables return values with minimally loaded
+							// strings, which are further truncated for displaying, so we
+							// can't test for loading limit except in contexts where an untruncated
+							// value is returned.
+							client.EvaluateRequest("&s4097", 0, tc.context)
+							switch tc.context {
+							case "variables", "clipboard":
+								want = fmt.Sprintf(`\*"x+\.\.\.\+%d more`, 4097-DefaultLoadConfig.MaxStringLen)
+							default:
+								want = fmt.Sprintf(`\*"x{%d}\.\.\.`, maxVarValueLen-2)
 							}
-							checkEval(t, got, want, false)
+							checkEvalRegex(t, client.ExpectEvaluateResponse(t), want, hasChildren)
+						})
+					}
 
-							// long string as a child (same limits)
-							client.EvaluateRequest("(cl).s", 0, evalContext)
-							got2 := client.ExpectEvaluateResponse(t)
-							checkEval(t, got2, want, false)
+					// Long strings returned from calls are subject to a different limit,
+					// same limit regardless of context
+					for _, context := range []string{"", "watch", "repl", "variables", "hover", "clipboard", "somethingelse"} {
+						t.Run(context, func(t *testing.T) {
+							client.EvaluateRequest(`call buildString(4097)`, 1000, context)
+							want := fmt.Sprintf(`"x+\.\.\.\+%d more"`, 4097-maxStringLenInCallRetVars)
+							got := client.ExpectEvaluateResponse(t)
+							checkEvalRegex(t, got, want, hasChildren)
 						})
 					}
 
-					// Variables requests are not affected.
-					checkVarExact(t, locals, -1, "longstr", "longstr", longstrLoaded64, "string", noChildren)
-					checkVarExact(t, locals, -1, "cl", "cl", `main.C {s: `+longstrLoaded64+`}`, "main.C", hasChildren)
+					// Variables requests use the most conservative loading limit
+					checkVarRegex(t, locals, -1, "s513", "s513", `"x{512}\.\.\.\+1 more"`, "string", noChildren)
+					// Container variables are subject to additional stricter value truncation that drops +more part
+					checkVarRegex(t, locals, -1, "nested", "nested", `map\[int\]string \[513: \"x+\.\.\.`, "string", hasChildren)
 				},
 				disconnect: true,
 			}})
@@ -3086,13 +3118,6 @@ func TestEvaluateCallRequest(t *testing.T) {
 					client.EvaluateRequest("call ", 1000, "watch")
 					got = client.ExpectEvaluateResponse(t)
 					checkEval(t, got, "\"this is a variable named `call`\"", noChildren)
-					// Long string as a return value
-					client.EvaluateRequest(`call stringsJoin(longstrs, ",")`, 1000, "variables") // full string
-					got = client.ExpectEvaluateResponse(t)
-					checkEval(t, got, `"very long string 0123456789a0123456789b0123456789c0123456789d0123456789e0123456789f0123456789g012345678h90123456789i0123456789j0123456789"`, hasChildren)
-					client.EvaluateRequest(`call stringsJoin(longstrs, ",")`, 1000, "watch") // full string
-					got = client.ExpectEvaluateResponse(t)
-					checkEval(t, got, `"very long string 0123456789a0123456789b0123456789c0123456789d0123456789e0123456789f0123456789g012345678h90123456789i0123456789j0123456789"`, hasChildren)
 
 					// Call error
 					client.EvaluateRequest("call call1(one)", 1000, "watch")
diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go
index 9388a869..a59e13df 100644
--- a/service/test/integration2_test.go
+++ b/service/test/integration2_test.go
@@ -1773,6 +1773,23 @@ func TestClientServerConsistentExit(t *testing.T) {
 		if state.ExitStatus != 2 {
 			t.Fatalf("Process exit status is not 2, got: %v", state.ExitStatus)
 		}
+
+		// Ensure future commands also return the correct exit status.
+		// Previously there was a bug where the command which prompted the
+		// process to exit (continue, next, etc...) would return the corrent
+		// exit status but subsequent commands would return an incorrect exit
+		// status of 0. To test this we simply repeat the 'next' command and
+		// ensure we get the correct response again.
+		state, err = c.Next()
+		if err != nil {
+			t.Fatalf("Unexpected error: %v", err)
+		}
+		if !state.Exited {
+			t.Fatal("Second process state is not exited")
+		}
+		if state.ExitStatus != 2 {
+			t.Fatalf("Second process exit status is not 2, got: %v", state.ExitStatus)
+		}
 	})
 }