From 7b98f5870d1506a4551cc45b0fd8eea5372c35f7 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Wed, 23 Jun 2021 19:51:57 -0400 Subject: [PATCH] service/dap: add string value of byte/rune slice as child (#2541) Load limit for metadata string is set to MaxStringLen --- service/dap/daptest/client.go | 8 ++++ service/dap/server.go | 74 +++++++++++++++++++++++++++--- service/dap/server_test.go | 84 ++++++++++++++++++++++++++++++++++- 3 files changed, 160 insertions(+), 6 deletions(-) diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go index f62a7136..3ad61ae1 100644 --- a/service/dap/daptest/client.go +++ b/service/dap/daptest/client.go @@ -353,6 +353,14 @@ func (c *Client) IndexedVariablesRequest(variablesReference, start, count int) { c.send(request) } +// NamedVariablesRequest sends a 'variables' request. +func (c *Client) NamedVariablesRequest(variablesReference int) { + request := &dap.VariablesRequest{Request: *c.newRequest("variables")} + request.Arguments.VariablesReference = variablesReference + request.Arguments.Filter = "named" + c.send(request) +} + // TeriminateRequest sends a 'terminate' request. func (c *Client) TerminateRequest() { c.send(&dap.TerminateRequest{Request: *c.newRequest("terminate")}) diff --git a/service/dap/server.go b/service/dap/server.go index f7c8cb42..2514b3aa 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -1707,10 +1707,22 @@ func (s *Server) onVariablesRequest(request *dap.VariablesRequest) { } } - children, err := s.childrenToDAPVariables(v) - if err != nil { - s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", err.Error()) - return + var children []dap.Variable + if request.Arguments.Filter == "named" || request.Arguments.Filter == "" { + named, err := s.metadataToDAPVariables(v) + if err != nil { + s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", err.Error()) + return + } + children = append(children, named...) + } + if request.Arguments.Filter == "indexed" || request.Arguments.Filter == "" { + indexed, err := s.childrenToDAPVariables(v) + if err != nil { + s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", err.Error()) + return + } + children = append(children, indexed...) } response := &dap.VariablesResponse{ Response: *newResponse(request.Request), @@ -1785,6 +1797,7 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab Value: key, VariablesReference: keyref, IndexedVariables: getIndexedVariableCount(keyv), + NamedVariables: getNamedVariableCount(keyv), } valvar := dap.Variable{ Name: fmt.Sprintf("[val %d]", v.startIndex+kvIndex), @@ -1793,6 +1806,7 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab Value: val, VariablesReference: valref, IndexedVariables: getIndexedVariableCount(valv), + NamedVariables: getNamedVariableCount(valv), } children = append(children, keyvar, valvar) } else { // At least one is a scalar @@ -1813,9 +1827,11 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab } kvvar.VariablesReference = keyref kvvar.IndexedVariables = getIndexedVariableCount(keyv) + kvvar.NamedVariables = getNamedVariableCount(keyv) } else if valref != 0 { // val is a type to be expanded kvvar.VariablesReference = valref kvvar.IndexedVariables = getIndexedVariableCount(valv) + kvvar.NamedVariables = getNamedVariableCount(valv) } children = append(children, kvvar) } @@ -1833,6 +1849,7 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab Value: cvalue, VariablesReference: cvarref, IndexedVariables: getIndexedVariableCount(&v.Children[i]), + NamedVariables: getNamedVariableCount(&v.Children[i]), } } default: @@ -1872,12 +1889,59 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab Value: cvalue, VariablesReference: cvarref, IndexedVariables: getIndexedVariableCount(c), + NamedVariables: getNamedVariableCount(c), } } } return children, nil } +func getNamedVariableCount(v *proc.Variable) int { + namedVars := 0 + if isListOfBytesOrRunes(v) { + // string value of array/slice of bytes and runes. + namedVars += 1 + } + return namedVars +} + +// metadataToDAPVariables returns the DAP presentation of the referenced variable's metadata. +// These are included as named variables +func (s *Server) metadataToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variable, error) { + var children []dap.Variable + + if isListOfBytesOrRunes(v.Variable) { + // Return the string value of []byte or []rune. + typeName := api.PrettyTypeName(v.DwarfType) + loadExpr := fmt.Sprintf("string(*(*%q)(%#x))", typeName, v.Addr) + + s.log.Debugf("loading %s (type %s) with %s", v.fullyQualifiedNameOrExpr, typeName, loadExpr) + // We know that this is an array/slice of Uint8 or Int32, so we will load up to MaxStringLen. + config := DefaultLoadConfig + config.MaxArrayValues = config.MaxStringLen + vLoaded, err := s.debugger.EvalVariableInScope(-1, 0, 0, loadExpr, config) + val := s.convertVariableToString(vLoaded) + if err == nil { + // TODO(suzmue): Add evaluate name. Using string(name) will not get the same result because the + // MaxArrayValues is not auto adjusted in evaluate requests like MaxStringLen is adjusted. + children = append(children, dap.Variable{ + Name: "string()", + Value: val, + Type: "string", + }) + } + } + return children, nil +} + +func isListOfBytesOrRunes(v *proc.Variable) bool { + if len(v.Children) > 0 && (v.Kind == reflect.Array || v.Kind == reflect.Slice) { + childKind := v.Children[0].RealType.Common().ReflectKind + return childKind == reflect.Uint8 || childKind == reflect.Int32 + } + return false +} + func (s *Server) getTypeIfSupported(v *proc.Variable) string { if !s.clientCapabilities.supportsVariableType { return "" @@ -2147,7 +2211,7 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) { opts |= showFullValue } exprVal, exprRef := s.convertVariableWithOpts(exprVar, fmt.Sprintf("(%s)", request.Arguments.Expression), opts) - response.Body = dap.EvaluateResponseBody{Result: exprVal, VariablesReference: exprRef, IndexedVariables: getIndexedVariableCount(exprVar)} + response.Body = dap.EvaluateResponseBody{Result: exprVal, VariablesReference: exprRef, IndexedVariables: getIndexedVariableCount(exprVar), NamedVariables: getNamedVariableCount(exprVar)} } s.send(response) } diff --git a/service/dap/server_test.go b/service/dap/server_test.go index 0e042a87..c615d86f 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -1524,7 +1524,6 @@ func TestVariablesLoading(t *testing.T) { longarr = client.ExpectVariablesResponse(t) checkChildren(t, longarr, "longarr", 50) checkArrayChildren(t, longarr, "longarr", 50) - } // Slice not fully loaded based on LoadConfig.MaxArrayValues. @@ -1789,6 +1788,89 @@ func TestVariablesLoading(t *testing.T) { }) } +// TestVariablesMetadata exposes test cases where variables contain metadata that +// can be accessed by requesting named variables. +func TestVariablesMetadata(t *testing.T) { + runTest(t, "testvariables2", 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 + fixture.Source, []int{}, + []onBreakpoint{{ + execute: func() {}, + disconnect: false, + }, { + execute: func() { + checkStop(t, client, 1, "main.main", 368) + + client.VariablesRequest(1001) // Locals + locals := client.ExpectVariablesResponse(t) + + checkNamedChildren := func(ref int, name, typeStr string, vals []string, evaluate bool) { + // byteslice, request named variables + client.NamedVariablesRequest(ref) + named := client.ExpectVariablesResponse(t) + checkChildren(t, named, name, 1) + checkVarExact(t, named, 0, "string()", "", "\"tèst\"", "string", false) + + client.VariablesRequest(ref) + all := client.ExpectVariablesResponse(t) + checkChildren(t, all, name, len(vals)+1) + checkVarExact(t, all, 0, "string()", "", "\"tèst\"", "string", false) + for i, v := range vals { + idx := fmt.Sprintf("[%d]", i) + evalName := fmt.Sprintf("%s[%d]", name, i) + if evaluate { + evalName = fmt.Sprintf("(%s)[%d]", name, i) + } + checkVarExact(t, all, i+1, idx, evalName, v, typeStr, false) + } + } + + // byteslice + ref := checkVarExactIndexed(t, locals, -1, "byteslice", "byteslice", "[]uint8 len: 5, cap: 5, [116,195,168,115,116]", "[]uint8", true, 5, 1) + checkNamedChildren(ref, "byteslice", "uint8", []string{"116", "195", "168", "115", "116"}, false) + + client.EvaluateRequest("byteslice", 0, "") + got := client.ExpectEvaluateResponse(t) + ref = checkEvalIndexed(t, got, "[]uint8 len: 5, cap: 5, [116,195,168,115,116]", hasChildren, 5, 1) + checkNamedChildren(ref, "byteslice", "uint8", []string{"116", "195", "168", "115", "116"}, true) + + // runeslice + ref = checkVarExactIndexed(t, locals, -1, "runeslice", "runeslice", "[]int32 len: 4, cap: 4, [116,232,115,116]", "[]int32", true, 4, 1) + checkNamedChildren(ref, "runeslice", "int32", []string{"116", "232", "115", "116"}, false) + + client.EvaluateRequest("runeslice", 0, "repl") + got = client.ExpectEvaluateResponse(t) + ref = checkEvalIndexed(t, got, "[]int32 len: 4, cap: 4, [116,232,115,116]", hasChildren, 4, 1) + checkNamedChildren(ref, "runeslice", "int32", []string{"116", "232", "115", "116"}, true) + + // bytearray + ref = checkVarExactIndexed(t, locals, -1, "bytearray", "bytearray", "[5]uint8 [116,195,168,115,116]", "[5]uint8", true, 5, 1) + checkNamedChildren(ref, "bytearray", "uint8", []string{"116", "195", "168", "115", "116"}, false) + + client.EvaluateRequest("bytearray", 0, "hover") + got = client.ExpectEvaluateResponse(t) + ref = checkEvalIndexed(t, got, "[5]uint8 [116,195,168,115,116]", hasChildren, 5, 1) + checkNamedChildren(ref, "bytearray", "uint8", []string{"116", "195", "168", "115", "116"}, true) + + // runearray + ref = checkVarExactIndexed(t, locals, -1, "runearray", "runearray", "[4]int32 [116,232,115,116]", "[4]int32", true, 4, 1) + checkNamedChildren(ref, "runearray", "int32", []string{"116", "232", "115", "116"}, false) + + client.EvaluateRequest("runearray", 0, "watch") + got = client.ExpectEvaluateResponse(t) + ref = checkEvalIndexed(t, got, "[4]int32 [116,232,115,116]", hasChildren, 4, 1) + checkNamedChildren(ref, "runearray", "int32", []string{"116", "232", "115", "116"}, true) + }, + disconnect: true, + }}) + }) +} + // TestGlobalScopeAndVariables launches the program with showGlobalVariables // arg set, executes to a breakpoint in the main package and tests that global // package main variables got loaded. It then steps into a function